async def contact_accept(self, body): channel = body["channel"] request = await self.service.accept( contact_request_id=body["contact_request"], staff=self.consumer.user) if not request: await self.consumer.send_error("exhibition.unknown_contact_request" ) return staff = await self.service.get_staff( exhibitor_id=request["exhibitor"]["id"]) if self.consumer.user.id not in staff: await self.consumer.send_error("exhibition.not_staff_member") return await self.consumer.send_success() await self.consumer.channel_layer.group_send( GROUP_USER.format(id=request["user"]["id"]), { "type": "exhibition.contact_accepted", "contact_request": request, "channel": channel, }, ) for user_id in staff: await self.consumer.channel_layer.group_send( GROUP_USER.format(id=str(user_id)), { "type": "exhibition.contact_close", "contact_request": request }, )
async def patch(self, body): staff = [] if body["id"] != "": staff += await self.service.get_staff(exhibitor_id=body["id"]) exhibitor = await self.service.patch(exhibitor=body, world=self.consumer.world) for user in exhibitor["staff"]: if user["id"] not in staff: staff.append(user["id"]) for user_id in staff: data = await database_sync_to_async( self.service.get_exhibition_data_for_user)(user_id) await self.consumer.channel_layer.group_send( GROUP_USER.format(id=str(user_id)), { "type": "exhibition.exhibition_data_update", "data": data }, ) if not exhibitor: await self.consumer.send_error("exhibition.unknown_room") else: await self.consumer.send_success({"exhibitor": exhibitor})
async def dispatch_disconnect(self, close_code): if self.consumer.user: await self.consumer.channel_layer.group_discard( GROUP_USER.format(id=self.consumer.user.id), self.consumer.channel_name, ) await self.consumer.channel_layer.group_discard( GROUP_WORLD.format(id=self.consumer.world.id), self.consumer.channel_name, )
async def login(self, body): kwargs = { "world": self.consumer.world, } if not body or "token" not in body: client_id = body.get("client_id") if not client_id: await self.consumer.send_error(code="auth.missing_id_or_token") return kwargs["client_id"] = client_id else: token = self.consumer.world.decode_token(body["token"]) if not token: await self.consumer.send_error(code="auth.invalid_token") return kwargs["token"] = token login_result = await database_sync_to_async(login)(**kwargs) if not login_result: await self.consumer.send_error(code="auth.denied") return self.consumer.user = login_result.user if settings.SENTRY_DSN: with configure_scope() as scope: scope.user = {"id": str(self.consumer.user.id)} async with aioredis() as redis: redis_read = await redis.hgetall(f"chat:read:{self.consumer.user.id}") read_pointers = {k.decode(): int(v.decode()) for k, v in redis_read.items()} await self.consumer.send_json( [ "authenticated", { "user.config": self.consumer.user.serialize_public(), "world.config": login_result.world_config, "chat.channels": login_result.chat_channels, "chat.read_pointers": read_pointers, "exhibition": login_result.exhibition_data, }, ] ) await self._enforce_connection_limit() await self.consumer.channel_layer.group_add( GROUP_USER.format(id=self.consumer.user.id), self.consumer.channel_name ) await self.consumer.channel_layer.group_add( GROUP_WORLD.format(id=self.consumer.world.id), self.consumer.channel_name ) await ChatService(self.consumer.world).enforce_forced_joins(self.consumer.user)
async def _enforce_connection_limit(self): connection_limit = self.consumer.world.config.get("connection_limit") if not connection_limit: return message = {"type": "connection.replaced"} channel_names = [] group = GROUP_USER.format(id=self.consumer.user.id) cl = self.consumer.channel_layer key = cl._group_key(group) async with cl.connection(cl.consistent_hash(group)) as connection: # Discard old channels based on group_expiry await connection.zremrangebyscore( key, min=0, max=int(time.time()) - cl.group_expiry ) channel_names += [ x.decode("utf8") for x in await connection.zrange(key, 0, -1) ] if len(channel_names) < connection_limit: return if connection_limit == 1: channels_to_drop = channel_names else: channels_to_drop = channel_names[: -1 * (connection_limit - 1)] ( connection_to_channel_keys, channel_keys_to_message, channel_keys_to_capacity, ) = cl._map_channel_keys_to_connection(channels_to_drop, message) for connection_index, channel_redis_keys in connection_to_channel_keys.items(): group_send_lua = ( """ for i=1,#KEYS do redis.call('LPUSH', KEYS[i], ARGV[i]) redis.call('EXPIRE', KEYS[i], %d) end """ % cl.expiry ) args = [ channel_keys_to_message[channel_key] for channel_key in channel_redis_keys ] async with cl.connection(connection_index) as connection: await connection.eval( group_send_lua, keys=channel_redis_keys, args=args )
async def delete(self, body): staff = await self.service.get_staff(exhibitor_id=body["exhibitor"]) if not await self.service.delete(exhibitor_id=body["exhibitor"]): await self.consumer.send_error("exhibition.unknown_exhibitor") else: for user_id in staff: data = await database_sync_to_async( self.service.get_exhibition_data_for_user)(user_id) await self.consumer.channel_layer.group_send( GROUP_USER.format(id=str(user_id)), { "type": "exhibition.exhibition_data_update", "data": data }, ) await self.consumer.send_success({})
async def mark_read(self, body): if not body.get("id"): raise ConsumerException("chat.invalid_body") async with aioredis() as redis: await redis.hset(f"chat:read:{self.consumer.user.id}", self.channel_id, body.get("id")) await redis.sadd(f"chat:unread.notify:{self.channel_id}", str(self.consumer.user.id)) await self.consumer.send_success() await self.consumer.channel_layer.group_send( GROUP_USER.format(id=self.consumer.user.id), { "type": "chat.read_pointers", "socket": self.consumer.socket_id, }, )
async def silence(self, body): if body.get("id") == str(self.consumer.user.id): await self.consumer.send_error(code="user.silence.self") return ok = await set_user_silenced(self.consumer.world, body.get("id"), by_user=self.consumer.user) if ok: await self.consumer.send_success({}) # Force user browser to reload instead of drop to kick out of e.g. BBB sessions await self.consumer.channel_layer.group_send( GROUP_USER.format(id=body.get("id")), {"type": "connection.reload"}, ) else: await self.consumer.send_error(code="user.not_found")
async def contact(self, body): exhibitor = await self.service.get_exhibitor( exhibitor_id=body["exhibitor"]) if not exhibitor: await self.consumer.send_error("exhibition.unknown_exhibitor") return request = await self.service.contact(exhibitor_id=exhibitor["id"], user=self.consumer.user) await self.consumer.send_success({"contact_request": request}) for staff_member in exhibitor["staff"]: await self.consumer.channel_layer.group_send( GROUP_USER.format(id=str(staff_member["id"])), { "type": "exhibition.contact_request", "contact_request": request, }, )
async def contact_cancel(self, body): request = await self.service.missed( contact_request_id=body["contact_request"]) if not request: await self.consumer.send_error("exhibition.unknown_contact_request" ) return await self.consumer.send_success() staff = await self.service.get_staff( exhibitor_id=request["exhibitor"]["id"]) for user_id in staff: await self.consumer.channel_layer.group_send( GROUP_USER.format(id=str(user_id)), { "type": "exhibition.contact_close", "contact_request": request, }, )
async def send(self, body): content = body["content"] event_type = body["event_type"] if event_type != "channel.message": raise ConsumerException("chat.unsupported_event_type") if not (content.get("type") == "text" or (content.get("type") == "deleted" and "replaces" in body) or (content.get("type") == "call" and not self.channel.room)): raise ConsumerException("chat.unsupported_content_type") if body.get("replaces"): other_message = await self.service.get_event( pk=body["replaces"], channel_id=self.channel_id, ) if self.consumer.user.id != other_message.sender_id: # Users may only edit messages by other users if they are mods, # and even then only delete them is_moderator = (self.channel.room and await self.consumer.world.has_permission_async( user=self.consumer.user, room=self.channel.room, permission=Permission.ROOM_CHAT_MODERATE, )) if body["content"]["type"] != "deleted" or not is_moderator: raise ConsumerException("chat.denied") await self.service.update_event(other_message.id, new_content=content) if content.get("type") == "text" and not content.get("body"): raise ConsumerException("chat.empty") if await self.consumer.user.is_blocked_in_channel_async(self.channel): raise ConsumerException("chat.denied") if self.consumer.user.is_silenced: # In regular channels, this is already prevented by room permissions, but we need to check for DMs raise ConsumerException("chat.denied") # Re-open direct messages. If a user hid a direct message channel, it should re-appear once they get a message if not self.channel.room: async with aioredis() as redis: all_visible = await redis.exists( f"chat:direct:shownall:{self.channel_id}") if not all_visible: users = await self.service.show_channels_to_hidden_users( self.channel_id) for user in users: await self.service.broadcast_channel_list( user, self.consumer.socket_id) async with aioredis() as redis: await redis.sadd( f"chat:unread.notify:{self.channel_id}", str(user.id), ) async with aioredis() as redis: await redis.setex(f"chat:direct:shownall:{self.channel_id}", 3600 * 24 * 7, "true") event = await self.service.create_event( channel=self.channel, event_type=event_type, content=content, sender=self.consumer.user, replaces=body.get("replaces", None), ) await self.consumer.send_success( {"event": {k: v for k, v in event.items() if k != "type"}}) await self.consumer.channel_layer.group_send( GROUP_CHAT.format(channel=self.channel_id), event) # Unread notifications # We pop user IDs from the list of users to notify, because once they've been notified they don't need a # notification again until they sent a new read pointer. async with aioredis() as redis: batch_size = 100 while True: users = await redis.spop( f"chat:unread.notify:{self.channel_id}", 100) for user in users: await self.consumer.channel_layer.group_send( GROUP_USER.format(id=user.decode()), { "type": "chat.notification_pointers", "data": { self.channel_id: event["event_id"] }, }, ) if len(users) < batch_size: break if content.get("type") == "text": match = re.search(r"(?P<url>https?://[^\s]+)", content.get("body")) if match: await asgiref.sync.sync_to_async( retrieve_preview_information.apply_async )(kwargs={ "world": str(self.consumer.world.id), "event_id": event["event_id"], })