diff --git a/litecord/enums.py b/litecord/enums.py index 60037d6..4107fd5 100644 --- a/litecord/enums.py +++ b/litecord/enums.py @@ -83,6 +83,12 @@ GUILD_CHANS = (ChannelType.GUILD_TEXT, ChannelType.GUILD_CATEGORY) +VOICE_CHANNELS = ( + ChannelType.DM, ChannelType.GUILD_VOICE, + ChannelType.GUILD_CATEGORY +) + + class ActivityType(EasyEnum): PLAYING = 0 STREAMING = 1 diff --git a/litecord/gateway/websocket.py b/litecord/gateway/websocket.py index ca2d444..5c5fe05 100644 --- a/litecord/gateway/websocket.py +++ b/litecord/gateway/websocket.py @@ -30,12 +30,12 @@ from logbook import Logger import earl from litecord.auth import raw_token_check -from litecord.enums import RelationshipType +from litecord.enums import RelationshipType, ChannelType, VOICE_CHANNELS from litecord.schemas import validate, GW_STATUS_UPDATE from litecord.utils import ( task_wrapper, LitecordJSONEncoder, yield_chunks ) -from litecord.permissions import get_permissions +from litecord.permissions import get_permissions, ALL_PERMISSIONS from litecord.gateway.opcodes import OP from litecord.gateway.state import GatewayState @@ -48,14 +48,17 @@ from litecord.gateway.errors import ( ) log = Logger(__name__) + WebsocketProperties = collections.namedtuple( 'WebsocketProperties', 'v encoding compress zctx tasks' ) WebsocketObjects = collections.namedtuple( - 'WebsocketObjects', ('db', 'state_manager', 'storage', - 'loop', 'dispatcher', 'presence', 'ratelimiter', - 'user_storage') + 'WebsocketObjects', ( + 'db', 'state_manager', 'storage', + 'loop', 'dispatcher', 'presence', 'ratelimiter', + 'user_storage', 'voice' + ) ) @@ -113,7 +116,7 @@ class GatewayWebsocket: self.ext = WebsocketObjects( app.db, app.state_manager, app.storage, app.loop, app.dispatcher, app.presence, app.ratelimiter, - app.user_storage + app.user_storage, app.voice ) self.storage = self.ext.storage @@ -598,16 +601,93 @@ class GatewayWebsocket: # setting new presence to state await self.update_status(presence) + @property + def voice_key(self): + """Voice state key.""" + return (self.state.user_id, self.state.session_id) + + async def _voice_check(self, guild_id: int, channel_id: int): + """Check if the user can join the given guild/channel pair.""" + guild = None + if guild_id: + guild = await self.storage.get_guild(guild_id) + + channel = await self.storage.get_channel(channel_id) + ctype = ChannelType(channel['type']) + + if ctype not in VOICE_CHANNELS: + return + + if guild and channel.get(['guild_id']) != guild['id']: + return + + is_guild_voice = ctype == ChannelType.GUILD_VOICE + + states = await self.ext.voice.state_count(channel_id) + perms = (ALL_PERMISSIONS + if not is_guild_voice else + await get_permissions(self.state.user_id, + channel_id, storage=self.storage) + ) + + is_full = states >= channel['user_limit'] + is_bot = self.state.bot + + is_manager = perms.bits.manage_channels + + # if the channel is full AND: + # - user is not a bot + # - user is not manage channels + # then it fails + if not is_bot and not is_manager and is_full: + return + + # all checks passed. + return True + + async def _move_voice(self, guild_id, channel_id): + """Move an existing voice state to the given target.""" + if channel_id is None: + return await self.ext.voice.del_state(self.voice_key) + + if not await self._voice_check(guild_id, channel_id): + return + + await self.ext.voice.move_state( + self.voice_key, guild_id, channel_id) + + async def _create_voice(self, guild_id, channel_id): + """Create a voice state.""" + if not await self._voice_check(guild_id, channel_id): + return + + await self.ext.voice.create_state(self.voice_key, guild_id, channel_id) + async def handle_4(self, payload: Dict[str, Any]): """Handle OP 4 Voice Status Update.""" data = payload['d'] - # for now, ignore - log.debug('got VSU cid={} gid={} deaf={} mute={} video={}', - data.get('channel_id'), - data.get('guild_id'), - data.get('self_deaf'), - data.get('self_mute'), - data.get('self_video')) + + if not self.state: + return + + try: + channel_id = int(data['channel_id']) + guild_id = int(data['guild_id']) + + # TODO: fetch from settings if not provided + # self_deaf = bool(data['self_deaf']) + # self_mute = bool(data['self_mute']) + + # NOTE: self_video is NOT handled. + except (KeyError, ValueError): + pass + + # fetch an existing voice state + user_id, session_id = self.state.user_id, self.state.session_id + voice_state = await self.ext.voice.fetch_state(user_id, session_id) + + func = self._move_voice if voice_state else self._create_voice + await func(guild_id, channel_id) async def _handle_5(self, payload: Dict[str, Any]): """Handle OP 5 Voice Server Ping. diff --git a/litecord/voice/manager.py b/litecord/voice/manager.py new file mode 100644 index 0000000..7457623 --- /dev/null +++ b/litecord/voice/manager.py @@ -0,0 +1,23 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +class VoiceManager: + """Main voice manager class.""" + def __init__(self, app): + self.app = app diff --git a/run.py b/run.py index ddebbe8..1c11556 100644 --- a/run.py +++ b/run.py @@ -70,6 +70,7 @@ from litecord.dispatcher import EventDispatcher from litecord.presence import PresenceManager from litecord.images import IconManager from litecord.jobs import JobManager +from litecord.voice.manager import VoiceManager from litecord.utils import LitecordJSONEncoder @@ -232,6 +233,8 @@ def init_app_managers(app_): app_.storage.presence = app_.presence + app_.voice = VoiceManager(app_) + async def api_index(app_): to_find = {}