aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--discord/__init__.py2
-rw-r--r--discord/calls.py79
-rw-r--r--discord/client.py16
-rw-r--r--discord/member.py102
-rw-r--r--discord/state.py33
-rw-r--r--docs/api.rst6
6 files changed, 207 insertions, 31 deletions
diff --git a/discord/__init__.py b/discord/__init__.py
index d422f652..f173e776 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 ce9e6803..a502dfbe 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 a97eef18..b92120a9 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 20d829e3..adff3729 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 cf2af379..adac7f5f 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 5ac83bef..ce61b5ad 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -543,6 +543,12 @@ CallMessage
.. autoclass:: CallMessage
:members:
+GroupCall
+~~~~~~~~~~
+
+.. autoclass:: GroupCall
+ :members:
+
Server
~~~~~~