diff --git a/discord/__init__.py b/discord/__init__.py index d422f6520..f173e776f 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -25,7 +25,7 @@ from .server import Server from .member import Member from .message import Message from .errors import * -from .calls import CallMessage +from .calls import CallMessage, GroupCall from .permissions import Permissions, PermissionOverwrite from .role import Role from .colour import Color, Colour diff --git a/discord/calls.py b/discord/calls.py index ce9e6803d..a502dfbe1 100644 --- a/discord/calls.py +++ b/discord/calls.py @@ -25,9 +25,11 @@ DEALINGS IN THE SOFTWARE. """ from . import utils +from .enums import ServerRegion, try_enum +from .member import VoiceState class CallMessage: - """Represents a group call from Discord. + """Represents a group call message from Discord. This is only received in cases where the message type is equivalent to :attr:`MessageType.call`. @@ -46,3 +48,78 @@ class CallMessage: self.channel = channel self.ended_timestamp = utils.parse_time(kwargs.get('ended_timestamp')) self.participants = kwargs.get('participants') + +class GroupCall: + """Represents the actual group call from Discord. + + This is accompanied with a :class:`CallMessage` denoting the information. + + Attributes + ----------- + message: :class:`CallMessage` + The message associated with this group call. + unavailable: bool + Denotes if this group call is unavailable. + ringing: List[:class:`User`] + A list of users that are currently being rung to join the call. + region: :class:`ServerRegion` + The server region the group call is being hosted on. + """ + + def __init__(self, **kwargs): + self.message = kwargs.get('message') + self.unavailable = kwargs.get('unavailable') + self._voice_states = {} + + for state in kwargs.get('voice_states', []): + self._update_voice_state(state) + + self._update(**kwargs) + + def _update(self, **kwargs): + self.region = try_enum(ServerRegion, kwargs.get('region')) + lookup = {u.id: u for u in self.message.channel.recipients} + self.ringing = list(filter(None, map(lambda i: lookup.get(i), kwargs.get('ringing', [])))) + + def _update_voice_state(self, data): + user_id = data['user_id'] + # left the voice channel? + if data['channel_id'] is None: + self._voice_states.pop(user_id, None) + else: + self._voice_states[user_id] = VoiceState(**data, voice_channel=self.channel) + + @property + def connected(self): + """A property that returns the list of :class:`User` that are currently in this call.""" + ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None] + me = self.channel.me + if self.voice_state_for(me) is not None: + ret.append(me) + + return ret + + @property + def channel(self): + """:class:`PrivateChannel`\: Returns the channel the group call is in.""" + return self.message.channel + + def voice_state_for(self, user): + """Retrieves the :class:`VoiceState` for a specified :class:`User`. + + If the :class:`User` has no voice state then this function returns + ``None``. + + Parameters + ------------ + user: :class:`User` + The user to retrieve the voice state for. + + Returns + -------- + Optiona[:class:`VoiceState`] + The voice state associated with this user. + """ + + return self._voice_states.get(user.id) + diff --git a/discord/client.py b/discord/client.py index a97eef182..b92120a9a 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2651,6 +2651,22 @@ class Client: """ return self.connection._get_voice_client(server.id) + def group_call_in(self, channel): + """Returns the :class:`GroupCall` associated with a private channel. + + If no group call is found then ``None`` is returned. + + Parameters + ----------- + channel: :class:`PrivateChannel` + The group private channel to query the group call for. + + Returns + -------- + Optional[:class:`GroupCall`] + The group call. + """ + return self.connection._calls.get(channel.id) # Miscellaneous stuff diff --git a/discord/member.py b/discord/member.py index 20d829e30..adff37293 100644 --- a/discord/member.py +++ b/discord/member.py @@ -27,9 +27,55 @@ DEALINGS IN THE SOFTWARE. from .user import User from .game import Game from . import utils -from .enums import Status +from .enums import Status, ChannelType from .colour import Colour +class VoiceState: + """Represents a Discord user's voice state. + + Attributes + ------------ + deaf: bool + Indicates if the user is currently deafened by the server. + mute: bool + Indicates if the user is currently muted by the server. + self_mute: bool + Indicates if the user is currently muted by their own accord. + self_deaf: bool + Indicates if the user is currently deafened by their own accord. + is_afk: bool + Indicates if the user is currently in the AFK channel in the server. + voice_channel: Optional[Union[:class:`Channel`, :class:`PrivateChannel`]] + The voice channel that the user is currently connected to. None if the user + is not currently in a voice channel. + """ + + __slots__ = [ 'session_id', 'deaf', 'mute', 'self_mute', + 'self_deaf', 'is_afk', 'voice_channel' ] + + def __init__(self, **kwargs): + self.session_id = kwargs.get('session_id') + self._update_voice_state(**kwargs) + + def _update_voice_state(self, **kwargs): + self.self_mute = kwargs.get('self_mute', False) + self.self_deaf = kwargs.get('self_deaf', False) + self.is_afk = kwargs.get('suppress', False) + self.mute = kwargs.get('mute', False) + self.deaf = kwargs.get('deaf', False) + self._handle_voice_channel(kwargs.get('voice_channel'), kwargs.get('user_id')) + + def _handle_voice_channel(self, voice_channel, user_id): + self.voice_channel = voice_channel + +def flatten_voice_states(cls): + for attr in VoiceState.__slots__: + def getter(self, x=attr): + return getattr(self.voice, x) + setattr(cls, attr, property(getter)) + return cls + +@flatten_voice_states class Member(User): """Represents a Discord member to a :class:`Server`. @@ -38,19 +84,9 @@ class Member(User): Attributes ---------- - deaf : bool - Indicates if the member is currently deafened by the server. - mute : bool - Indicates if the member is currently muted by the server. - self_mute : bool - Indicates if the member is currently muted by their own accord. - self_deaf : bool - Indicates if the member is currently deafened by their own accord. - is_afk : bool - Indicates if the member is currently in the AFK channel in the server. - voice_channel : :class:`Channel` - The voice channel that the member is currently connected to. None if the member - is not currently in a voice channel. + voice: :class:`VoiceState` + The member's voice state. Properties are defined to mirror access of the attributes. + e.g. ``Member.is_afk`` is equivalent to `Member.voice.is_afk``. roles A list of :class:`Role` that the member belongs to. Note that the first element of this list is always the default '@everyone' role. @@ -68,14 +104,11 @@ class Member(User): The server specific nickname of the user. """ - __slots__ = [ 'deaf', 'mute', 'self_mute', 'self_deaf', 'is_afk', - 'voice_channel', 'roles', 'joined_at', 'status', 'game', - 'server', 'nick' ] + __slots__ = [ 'roles', 'joined_at', 'status', 'game', 'server', 'nick', 'voice' ] def __init__(self, **kwargs): super().__init__(**kwargs.get('user')) - self.deaf = kwargs.get('deaf') - self.mute = kwargs.get('mute') + self.voice = VoiceState(**kwargs) self.joined_at = utils.parse_time(kwargs.get('joined_at')) self.roles = kwargs.get('roles', []) self.status = Status.offline @@ -83,14 +116,33 @@ class Member(User): self.game = Game(**game) if game else None self.server = kwargs.get('server', None) self.nick = kwargs.get('nick', None) - self._update_voice_state(mute=self.mute, deaf=self.deaf) def _update_voice_state(self, **kwargs): - self.self_mute = kwargs.get('self_mute', False) - self.self_deaf = kwargs.get('self_deaf', False) - self.is_afk = kwargs.get('suppress', False) - self.mute = kwargs.get('mute', False) - self.deaf = kwargs.get('deaf', False) + self.voice.self_mute = kwargs.get('self_mute', False) + self.voice.self_deaf = kwargs.get('self_deaf', False) + self.voice.is_afk = kwargs.get('suppress', False) + self.voice.mute = kwargs.get('mute', False) + self.voice.deaf = kwargs.get('deaf', False) + old_channel = getattr(self, 'voice_channel', None) + vc = kwargs.get('voice_channel') + + if old_channel is None and vc is not None: + # we joined a channel + vc.voice_members.append(self) + elif old_channel is not None: + try: + # we either left a channel or we switched channels + old_channel.voice_members.remove(self) + except ValueError: + pass + finally: + # we switched channels + if vc is not None: + vc.voice_members.append(self) + + self.voice.voice_channel = vc + + def _handle_voice_channel(self, voice_channel, user_id): old_channel = getattr(self, 'voice_channel', None) self.voice_channel = kwargs.get('voice_channel') diff --git a/discord/state.py b/discord/state.py index cf2af379a..adac7f5f8 100644 --- a/discord/state.py +++ b/discord/state.py @@ -33,7 +33,7 @@ from .member import Member from .role import Role from . import utils, compat from .enums import Status, ChannelType, try_enum - +from .calls import GroupCall from collections import deque, namedtuple import copy, enum, math @@ -63,6 +63,7 @@ class ConnectionState: self.user = None self.sequence = None self.session_id = None + self._calls = {} self._servers = {} self._voice_clients = {} self._private_channels = {} @@ -563,16 +564,21 @@ class ConnectionState: def parse_voice_state_update(self, data): server = self._get_server(data.get('guild_id')) - user_id = data.get('user_id') if server is not None: - if user_id == self.user.id: + channel = server.get_channel(data.get('channel_id')) + if data.get('user_id') == self.user.id: voice = self._get_voice_client(server.id) if voice is not None: - voice.channel = server.get_channel(data.get('channel_id')) + voice.channel = channel before, after = server._update_voice_state(data) if after is not None: self.dispatch('voice_state_update', before, after) + else: + # in here we're either at private or group calls + call = self._calls.get(data.get('channel_id'), None) + if call is not None: + call._update_voice_state(data) def parse_typing_start(self, data): channel = self.get_channel(data.get('channel_id')) @@ -592,6 +598,25 @@ class ConnectionState: timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp')) self.dispatch('typing', channel, member, timestamp) + def parse_call_create(self, data): + message = self._get_message(data.get('message_id')) + if message is not None: + call = GroupCall(message=message, **data) + self._calls[data['channel_id']] = call + self.dispatch('call', call) + + def parse_call_update(self, data): + call = self._calls.get(data.get('channel_id'), None) + if call is not None: + before = copy.copy(call) + call._update(**data) + self.dispatch('call_update', before, call) + + def parse_call_delete(self, data): + call = self._calls.pop(data.get('channel_id'), None) + if call is not None: + self.dispatch('call_remove', call) + def get_channel(self, id): if id is None: return None diff --git a/docs/api.rst b/docs/api.rst index 5ac83bef0..ce61b5ad9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -543,6 +543,12 @@ CallMessage .. autoclass:: CallMessage :members: +GroupCall +~~~~~~~~~~ + +.. autoclass:: GroupCall + :members: + Server ~~~~~~