예제 #1
0
 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)
예제 #2
0
 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})
예제 #3
0
 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})
예제 #4
0
    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)
            },
        )
예제 #5
0
    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)
예제 #6
0
        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
예제 #7
0
    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)
예제 #8
0
 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,
         },
     )
예제 #9
0
    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)
            },
        )
예제 #10
0
 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])
예제 #11
0
    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"],
                })
예제 #12
0
    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()))