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 unregister_connection(): async with aioredis() as redis: await redis.hincrby( "connections", f"{settings.VENUELESS_COMMIT}.{settings.VENUELESS_ENVIRONMENT}", -1, )
async def ping_connection(): async with aioredis() as redis: await redis.setex( f"connections:{settings.VENUELESS_COMMIT}.{settings.VENUELESS_ENVIRONMENT}", 15, "exists", )
async def leave(self, body): await self._unsubscribe(clean_volatile_membership=False) if self.channel.room: await self._leave() async with aioredis() as redis: await redis.srem(f"chat:unread.notify:{self.channel_id}", str(self.consumer.user.id)) else: await self.service.hide_channel_user(self.channel_id, self.consumer.user.id) async with aioredis() as redis: await redis.delete(f"chat:direct:shownall:{self.channel_id}") await self.service.broadcast_channel_list(self.consumer.user, self.consumer.socket_id) await self.consumer.send_success()
async def test_autofix_numbers(chat_room): async with world_communicator() as c1, aioredis() as redis: await redis.delete("chat.event_id") await c1.send_json_to( ["chat.join", 123, { "channel": str(chat_room.channel.id) }]) await c1.receive_json_from() response = await c1.receive_json_from() assert response[1]["event_id"] == 1 await redis.delete("chat.event_id") await c1.send_json_to([ "chat.send", 123, { "channel": str(chat_room.channel.id), "event_type": "channel.message", "content": { "type": "text", "body": "Hello world" }, }, ]) await c1.receive_json_from() response = await c1.receive_json_from() assert response[1]["event_id"] == 3
async def publish_read_pointers(self, body): if self.consumer.socket_id != body["socket"]: 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(["chat.read_pointers", read_pointers])
async def get_connections(): async with aioredis() as redis: conns = await redis.hgetall("connections") ret = {} for k, v in conns.items(): if v == 0 or await redis.get(f"connections:{k.decode()}") != b"exists": await redis.hdel("connections", k) else: ret[k.decode()] = int(v.decode()) return ret
async def _set_cache_version(self): async with aioredis() as redis: await redis.eval( SETIFHIGHER, [f"{self._cachekey}:version"], [self.version], ) cache = caches["process"] cache.set(self._cachekey, self, timeout=600)
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 register_connection(): async with aioredis() as redis: await redis.hincrby( "connections", f"{settings.VENUELESS_COMMIT}.{settings.VENUELESS_ENVIRONMENT}", 1, ) await redis.setex( f"connections:{settings.VENUELESS_COMMIT}.{settings.VENUELESS_ENVIRONMENT}", 15, "exists", )
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 refresh_from_db_if_outdated(self): async with aioredis() as redis: latest_version = await redis.get(f"{self._cachekey}:version") if latest_version: if latest_version == b"deleted": raise self.__class__.DoesNotExist latest_version = int(latest_version.decode()) else: latest_version = 0 if latest_version == self.version: return cache = caches["process"] cached_instance = cache.get(self._cachekey) if cached_instance and cached_instance.version == latest_version: self._refresh_from_cache(cached_instance) return await database_sync_to_async(self.refresh_from_db)() cache.set(self._cachekey, self, timeout=600) if latest_version < self.version: await self._set_cache_version()
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()))
async def _check_redis(self): async with aioredis() as redis: await redis.set("healthcheck", "1")
async def _set_cache_deleted(self): async with aioredis() as redis: await redis.set( f"{self._cachekey}:version", "deleted", )
async def clear_redis(): from venueless.core.utils.redis import aioredis async with aioredis() as redis: await redis.flushall()
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"], })