gateway.websocket: add basic vsu handling

this adds *theoretical* interfaces to a VoiceManager. The actual
VoiceManager does not work and probably any VSU being sent over will
crash the websocket with an AttributionError.

 - enums: add VOICE_CHANNELS
 - run: add App.voice attribute
This commit is contained in:
Luna 2019-02-28 20:51:09 -03:00
parent 9a42d20259
commit 0d38c7cc0c
4 changed files with 125 additions and 13 deletions

View File

@ -83,6 +83,12 @@ GUILD_CHANS = (ChannelType.GUILD_TEXT,
ChannelType.GUILD_CATEGORY) ChannelType.GUILD_CATEGORY)
VOICE_CHANNELS = (
ChannelType.DM, ChannelType.GUILD_VOICE,
ChannelType.GUILD_CATEGORY
)
class ActivityType(EasyEnum): class ActivityType(EasyEnum):
PLAYING = 0 PLAYING = 0
STREAMING = 1 STREAMING = 1

View File

@ -30,12 +30,12 @@ from logbook import Logger
import earl import earl
from litecord.auth import raw_token_check 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.schemas import validate, GW_STATUS_UPDATE
from litecord.utils import ( from litecord.utils import (
task_wrapper, LitecordJSONEncoder, yield_chunks 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.opcodes import OP
from litecord.gateway.state import GatewayState from litecord.gateway.state import GatewayState
@ -48,14 +48,17 @@ from litecord.gateway.errors import (
) )
log = Logger(__name__) log = Logger(__name__)
WebsocketProperties = collections.namedtuple( WebsocketProperties = collections.namedtuple(
'WebsocketProperties', 'v encoding compress zctx tasks' 'WebsocketProperties', 'v encoding compress zctx tasks'
) )
WebsocketObjects = collections.namedtuple( WebsocketObjects = collections.namedtuple(
'WebsocketObjects', ('db', 'state_manager', 'storage', 'WebsocketObjects', (
'loop', 'dispatcher', 'presence', 'ratelimiter', 'db', 'state_manager', 'storage',
'user_storage') 'loop', 'dispatcher', 'presence', 'ratelimiter',
'user_storage', 'voice'
)
) )
@ -113,7 +116,7 @@ class GatewayWebsocket:
self.ext = WebsocketObjects( self.ext = WebsocketObjects(
app.db, app.state_manager, app.storage, app.loop, app.db, app.state_manager, app.storage, app.loop,
app.dispatcher, app.presence, app.ratelimiter, app.dispatcher, app.presence, app.ratelimiter,
app.user_storage app.user_storage, app.voice
) )
self.storage = self.ext.storage self.storage = self.ext.storage
@ -598,16 +601,93 @@ class GatewayWebsocket:
# setting new presence to state # setting new presence to state
await self.update_status(presence) 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]): async def handle_4(self, payload: Dict[str, Any]):
"""Handle OP 4 Voice Status Update.""" """Handle OP 4 Voice Status Update."""
data = payload['d'] data = payload['d']
# for now, ignore
log.debug('got VSU cid={} gid={} deaf={} mute={} video={}', if not self.state:
data.get('channel_id'), return
data.get('guild_id'),
data.get('self_deaf'), try:
data.get('self_mute'), channel_id = int(data['channel_id'])
data.get('self_video')) 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]): async def _handle_5(self, payload: Dict[str, Any]):
"""Handle OP 5 Voice Server Ping. """Handle OP 5 Voice Server Ping.

23
litecord/voice/manager.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
"""
class VoiceManager:
"""Main voice manager class."""
def __init__(self, app):
self.app = app

3
run.py
View File

@ -70,6 +70,7 @@ from litecord.dispatcher import EventDispatcher
from litecord.presence import PresenceManager from litecord.presence import PresenceManager
from litecord.images import IconManager from litecord.images import IconManager
from litecord.jobs import JobManager from litecord.jobs import JobManager
from litecord.voice.manager import VoiceManager
from litecord.utils import LitecordJSONEncoder from litecord.utils import LitecordJSONEncoder
@ -232,6 +233,8 @@ def init_app_managers(app_):
app_.storage.presence = app_.presence app_.storage.presence = app_.presence
app_.voice = VoiceManager(app_)
async def api_index(app_): async def api_index(app_):
to_find = {} to_find = {}