async def wrapped(self, *args): if not getattr(self.consumer, "user", None): # pragma: no cover # Just a precaution, should never be called since MainConsumer.receive_json already checks this raise ConsumerException( "protocol.unauthenticated", "No authentication provided." ) if not await self.consumer.world.has_permission_async( user=self.consumer.user, permission=permission ): raise ConsumerException("auth.denied", "Permission denied.") return await func(self, *args)
async def call_url(self, body): service = BBBService(self.consumer.world) if not self.consumer.user.profile.get("display_name"): raise ConsumerException("bbb.join.missing_profile") url = await service.get_join_url_for_call_id( body.get("call"), self.consumer.user, ) if not url: raise ConsumerException("bbb.failed") await self.consumer.send_success({"url": url})
async def room_url(self, body): service = BBBService(self.consumer.world) if not self.consumer.user.profile.get("display_name"): raise ConsumerException("bbb.join.missing_profile") url = await service.get_join_url_for_room( self.room, self.consumer.user, moderator=await self.consumer.world.has_permission_async( user=self.consumer.user, permission=Permission.ROOM_BBB_MODERATE, room=self.room, ), ) if not url: raise ConsumerException("bbb.failed") await self.consumer.send_success({"url": url})
async def change_schedule_data(self, body): old = RoomConfigSerializer(self.room).data data = body.get("schedule_data") if data and not all(key in ["title", "session"] for key in data.keys()): raise ConsumerException(code="room.unknown_schedule_data", message="Unknown schedule data") await self.consumer.send_success({}) self.room.schedule_data = data await save_room( self.consumer.world, self.room, ["schedule_data"], by_user=self.consumer.user, old_data=old, ) await self.consumer.channel_layer.group_send( GROUP_ROOM.format(id=self.room.pk), { "type": "room.schedule", "schedule_data": data, "room": str(self.room.pk) }, )
async def direct_create(self, body): user_ids = set(body.get("users", [])) user_ids.add(self.consumer.user.id) hide = body.get("hide", True) channel, created, users = await self.service.get_or_create_direct_channel( user_ids=user_ids, hide=hide, hide_except=str(self.consumer.user.id) ) if not channel: raise ConsumerException("chat.denied") self.channel_id = str(channel.id) self.channel = channel reply = await self._subscribe() if created: for user in users: event = await self.service.create_event( channel=channel, event_type="channel.member", content={ "membership": "join", "user": user.serialize_public( trait_badges_map=self.consumer.world.config.get( "trait_badges_map" ) ), }, sender=user, ) await self.consumer.channel_layer.group_send( GROUP_CHAT.format(channel=self.channel_id), event, ) if not hide or user == self.consumer.user: await self.service.broadcast_channel_list( user=user, socket_id=self.consumer.socket_id ) async with aioredis() as redis: await redis.sadd( f"chat:unread.notify:{self.channel_id}", str(user.id), ) if not hide: async with aioredis() as redis: await redis.setex( f"chat:direct:shownall:{self.channel_id}", 3600 * 24 * 7, "true" ) reply["id"] = str(channel.id) await self.consumer.send_success(reply)
async def wrapped(self, body, *args): if "room" in body: self.room = self.consumer.room_cache.get(body["room"]) if self.room: await self.room.refresh_from_db_if_outdated() else: self.room = await get_room( world=self.consumer.world, id=body["room"] ) self.consumer.room_cache[body["room"]] = self.room elif "channel" in body: channel = self.consumer.channel_cache.get(body["channel"]) if not channel: channel = await get_channel( world=self.consumer.world, pk=body["channel"] ) self.consumer.channel_cache[body["channel"]] = channel elif channel.room: await channel.room.refresh_from_db_if_outdated() if channel and channel.room: self.room = channel.room self.channel = channel else: raise ConsumerException("room.unknown", "Unknown room ID") else: raise ConsumerException("room.unknown", "Unknown room ID") if not self.room: raise ConsumerException("room.unknown", "Unknown room ID") if module_required is not None: module_config = [ m.get("config", {}) for m in self.room.module_config if m["type"] == module_required ] if module_config: self.module_config = module_config[0] else: raise ConsumerException( "room.unknown", "Room does not contain a matching module." ) if permission_required is not None: if not getattr(self.consumer, "user", None): # pragma: no cover # Just a precaution, should never be called since MainConsumer.receive_json already checks this raise ConsumerException( "protocol.unauthenticated", "No authentication provided." ) if not await self.consumer.world.has_permission_async( user=self.consumer.user, permission=permission_required, room=self.room, ): raise ConsumerException("protocol.denied", "Permission denied.") try: return await func(self, body, *args) finally: del self.room
async def join(self, body): if not self.consumer.user.profile.get("display_name"): raise ConsumerException("channel.join.missing_profile") reply = await self._subscribe() volatile_config = self.module_config.get("volatile", False) volatile_client = body.get("volatile", volatile_config) if ( volatile_client != volatile_config and await self.consumer.world.has_permission_async( user=self.consumer.user, room=self.room, permission=Permission.ROOM_CHAT_MODERATE, ) ): volatile_config = volatile_client joined = await self.service.add_channel_user( self.channel_id, self.consumer.user, volatile=volatile_config ) if joined: event = await self.service.create_event( channel=self.channel, event_type="channel.member", content={ "membership": "join", "user": self.consumer.user.serialize_public( trait_badges_map=self.consumer.world.config.get( "trait_badges_map" ) ), }, sender=self.consumer.user, ) await self.consumer.channel_layer.group_send( GROUP_CHAT.format(channel=self.channel_id), event, ) await self.service.broadcast_channel_list( self.consumer.user, self.consumer.socket_id ) if not volatile_config: async with aioredis() as redis: await redis.sadd( f"chat:unread.notify:{self.channel_id}", str(self.consumer.user.id), ) await self.consumer.send_success(reply)
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 change_schedule_data(self, body): data = body.get("schedule_data") if data and not all(key in ["title", "session"] for key in data.keys()): raise ConsumerException(code="room.unknown_schedule_data", message="Unknown schedule data") await self.consumer.send_success({}) self.room.schedule_data = data await update_room(self.room) await self.consumer.channel_layer.group_send( GROUP_ROOM.format(id=self.room.pk), { "type": "room.schedule", "schedule_data": data, "room": str(self.room.pk) }, )
async def dispatch_command(self, content): action = content[0].split(".", 1)[1] if action not in self._commands: raise ConsumerException(f"{self.prefix}.unsupported_command") await self._commands[action](self, content[2])
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"], })
async def send_reaction(self, body): reaction = body.get("reaction") if reaction not in ( "+1", "clap", "heart", "open_mouth", "rolling_on_the_floor_laughing", ): raise ConsumerException(code="room.unknown_reaction", message="Unknown reaction") redis_key = f"reactions:{self.consumer.world.id}:{body['room']}" redis_debounce_key = f"reactions:{self.consumer.world.id}:{body['room']}:{reaction}:{self.consumer.user.id}" # We want to send reactions out to anyone, but we want to aggregate them over short time frames ("ticks") to # make sure we do not send 500 messages if 500 people react in the same second, but just one. async with aioredis() as redis: debounce = await redis.set(redis_debounce_key, "1", expire=2, exist=redis.SET_IF_NOT_EXIST) if not debounce: # User reacted in the 2 seconds, let's ignore this. await self.consumer.send_success({}) return # First, increase the number of reactions tr = redis.multi_exec() tr.hsetnx(redis_key, "tick", int(time.time())) tr.hget(redis_key, "tick") tr.hincrby(redis_key, reaction, 1) tick_new, tick_start, _ = await tr.execute() await self.consumer.send_success({}) if tick_new or time.time() - int(tick_start.decode()) > 3: # We're the first one to react since the last tick! It's our job to wait for the length of a tick, then # distribute the value to everyone. await asyncio.sleep(1) tr = redis.multi_exec() tr.hgetall(redis_key) tr.delete(redis_key) val, _ = await tr.execute() if not val: return await self.consumer.channel_layer.group_send( GROUP_ROOM.format(id=self.room.pk), { "type": "room.reaction", "reactions": { k.decode(): int(v.decode()) for k, v in val.items() if k.decode() != "tick" }, "room": str(body["room"]), }, ) for k, v in val.items(): if k.decode() != "tick": await store_reaction(body["room"], k.decode(), int(v.decode()))