From a128249b6324dd045561cb0be85cdabf8acd7694 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 13 Jul 2016 20:11:18 -0400 Subject: [PATCH] Add support for different message types and call message. --- discord/__init__.py | 3 +- discord/calls.py | 48 +++++++++++++++++++++++++++ discord/channel.py | 5 ++- discord/enums.py | 8 +++++ discord/message.py | 81 ++++++++++++++++++++++++++++++++++++++++++--- discord/state.py | 5 ++- docs/api.rst | 33 ++++++++++++++++++ 7 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 discord/calls.py diff --git a/discord/__init__.py b/discord/__init__.py index 57e91a9c9..d422f6520 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -25,6 +25,7 @@ from .server import Server from .member import Member from .message import Message from .errors import * +from .calls import CallMessage from .permissions import Permissions, PermissionOverwrite from .role import Role from .colour import Color, Colour @@ -32,7 +33,7 @@ from .invite import Invite from .object import Object from . import utils, opus, compat from .voice_client import VoiceClient -from .enums import ChannelType, ServerRegion, Status +from .enums import ChannelType, ServerRegion, Status, MessageType from collections import namedtuple import logging diff --git a/discord/calls.py b/discord/calls.py new file mode 100644 index 000000000..ce9e6803d --- /dev/null +++ b/discord/calls.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +""" +The MIT License (MIT) + +Copyright (c) 2015-2016 Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from . import utils + +class CallMessage: + """Represents a group call from Discord. + + This is only received in cases where the message type is equivalent to + :attr:`MessageType.call`. + + Attributes + ----------- + ended_timestamp: Optional[datetime.datetime] + A naive UTC datetime object that represents the time that the call has ended. + participants: List[:class:`User`] + The list of users that are participating in this call. + channel: :class:`PrivateChannel` + The private channel associated with this call. + """ + + def __init__(self, channel, **kwargs): + self.channel = channel + self.ended_timestamp = utils.parse_time(kwargs.get('ended_timestamp')) + self.participants = kwargs.get('participants') diff --git a/discord/channel.py b/discord/channel.py index ecccb24a0..2c1c3495d 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -301,6 +301,8 @@ class PrivateChannel(Hashable): ---------- recipients: list of :class:`User` The users you are participating with in the private channel. + me: :class:`User` + The user presenting yourself. id: str The private channel ID. is_private: bool @@ -318,11 +320,12 @@ class PrivateChannel(Hashable): :attr:`ChannelType.group` then this is always ``None``. """ - __slots__ = ['id', 'is_private', 'recipients', 'type', 'owner', 'icon', 'name'] + __slots__ = ['id', 'is_private', 'recipients', 'type', 'owner', 'icon', 'name', 'me'] def __init__(self, me, **kwargs): self.recipients = [User(**u) for u in kwargs['recipients']] self.id = kwargs['id'] + self.me = me self.is_private = True self.type = ChannelType(kwargs['type']) self._update_group(**kwargs) diff --git a/discord/enums.py b/discord/enums.py index 1ae7faf65..f99d5fe59 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -35,6 +35,14 @@ class ChannelType(Enum): def __str__(self): return self.name +class MessageType(Enum): + default = 0 + recipient_add = 1 + recipient_remove = 2 + call = 3 + channel_name_change = 4 + channel_icon_change = 5 + class ServerRegion(Enum): us_west = 'us-west' us_east = 'us-east' diff --git a/discord/message.py b/discord/message.py index 365d1148d..69f37ed43 100644 --- a/discord/message.py +++ b/discord/message.py @@ -27,7 +27,9 @@ DEALINGS IN THE SOFTWARE. from . import utils from .user import User from .object import Object +from .calls import CallMessage import re +from .enums import MessageType, try_enum class Message: """Represents a message from Discord. @@ -42,6 +44,9 @@ class Message: A naive UTC datetime object containing the time the message was created. tts : bool Specifies if the message was done with text-to-speech. + type: :class:`MessageType` + The type of message. In most cases this should not be checked, but it is helpful + in cases where it might be a system message for :attr:`system_content`. author A :class:`Member` that sent the message. If :attr:`channel` is a private channel, then it is a :class:`User` instead. @@ -62,6 +67,9 @@ class Message: For the sake of convenience, this :class:`Object` instance has an attribute ``is_private`` set to ``True``. server : Optional[:class:`Server`] The server that the message belongs to. If not applicable (i.e. a PM) then it's None instead. + call: Optional[:class:`CallMessage`] + The call that the message refers to. This is only applicable to messages of type + :attr:`MessageType.call`. mention_everyone : bool Specifies if the message mentions everyone. @@ -71,9 +79,11 @@ class Message: Rather this boolean indicates if the ``@everyone`` text is in the message **and** it did end up mentioning everyone. - mentions : list + mentions: list A list of :class:`Member` that were mentioned. If the message is in a private message - then the list is always empty. + then the list will be of :class:`User` instead. For messages that are not of type + :attr:`MessageType.default`\, this array can be used to aid in system messages. + For more information, see :attr:`system_content`. .. warning:: @@ -98,7 +108,8 @@ class Message: 'mention_everyone', 'embeds', 'id', 'mentions', 'author', 'channel_mentions', 'server', '_raw_mentions', 'attachments', '_clean_content', '_raw_channel_mentions', 'nonce', 'pinned', - 'role_mentions', '_raw_role_mentions' ] + 'role_mentions', '_raw_role_mentions', 'type', 'call', + '_system_content' ] def __init__(self, **kwargs): self._update(**kwargs) @@ -120,8 +131,10 @@ class Message: self.author = User(**data.get('author', {})) self.nonce = data.get('nonce') self.attachments = data.get('attachments') + self.type = try_enum(MessageType, data.get('type')) self._handle_upgrades(data.get('channel_id')) self._handle_mentions(data.get('mentions', []), data.get('mention_roles', [])) + self._handle_call(data.get('call')) # clear the cached properties cached = filter(lambda attr: attr[0] == '_', self.__slots__) @@ -136,16 +149,16 @@ class Message: self.channel_mentions = [] self.role_mentions = [] if getattr(self.channel, 'is_private', True): + self.mentions = [User(**m) for m in mentions] return - if self.channel is not None: + if self.server is not None: for mention in mentions: id_search = mention.get('id') member = self.server.get_member(id_search) if member is not None: self.mentions.append(member) - if self.server is not None: it = filter(None, map(lambda m: self.server.get_channel(m), self.raw_channel_mentions)) self.channel_mentions = utils._unique(it) @@ -154,6 +167,26 @@ class Message: if role is not None: self.role_mentions.append(role) + def _handle_call(self, call): + if call is None or self.type is not MessageType.call: + self.call = None + return + + # we get the participant source from the mentions array or + # the author + + participants = [] + for uid in call.get('participants', []): + if uid == self.author.id: + participants.append(self.author) + else: + user = utils.find(lambda u: u.id == uid, self.mentions) + if user is not None: + participants.append(user) + + call['participants'] = participants + self.call = CallMessage(channel=self.channel, **call) + @utils.cached_slot_property('_raw_mentions') def raw_mentions(self): """A property that returns an array of user IDs matched with @@ -248,3 +281,41 @@ class Message: found = self.server.get_member(self.author.id) if found is not None: self.author = found + + @utils.cached_slot_property('_system_content') + def system_content(self): + """A property that returns the content that is rendered + regardless of the :attr:`Message.type`. + + In the case of :attr:`MessageType.default`\, this just returns the + regular :attr:`Message.content`. Otherwise this returns an English + message denoting the contents of the system message. + """ + + if self.type is MessageType.default: + return self.content + + if self.type is MessageType.recipient_add: + return '{0.name} added {1.name} to the group.'.format(self.author, self.mentions[0]) + + if self.type is MessageType.recipient_remove: + return '{0.name} removed {1.name} from the group.'.format(self.author, self.mentions[0]) + + if self.type is MessageType.channel_name_change: + return '{0.author.name} changed the channel name: {0.content}'.format(self) + + if self.type is MessageType.channel_icon_change: + return '{0.author.name} changed the channel icon.'.format(self) + + # we're at the call message type now, which is a bit more complicated. + # we can make the assumption that Message.channel is a PrivateChannel + # with the type ChannelType.group or ChannelType.private + call_ended = self.call.ended_timestamp is not None + + if call_ended: + if self.channel.me in self.call.participants: + return '{0.author.name} started a call.'.format(self) + else: + return 'You missed a call from {0.author.name}'.format(self) + else: + return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self) diff --git a/discord/state.py b/discord/state.py index be4df9547..cd65ba462 100644 --- a/discord/state.py +++ b/discord/state.py @@ -238,7 +238,10 @@ class ConnectionState: message = self._get_message(data.get('id')) if message is not None: older_message = copy.copy(message) - if 'content' not in data: + if 'call' in data: + # call state message edit + message._handle_call(data['call']) + elif 'content' not in data: # embed only edit message.embeds = data['embeds'] else: diff --git a/docs/api.rst b/docs/api.rst index 32014d6b9..fb7da5cde 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -398,6 +398,33 @@ All enumerations are subclasses of `enum`_. A private group text channel. +.. class:: MessageType + + Specifies the type of :class:`Message`. This is used to denote if a message + is to be interpreted as a system message or a regular message. + + .. attribute:: default + + The default message type. This is the same as regular messages. + .. attribute:: recipient_add + + The system message when a recipient is added to a group private + message, i.e. a private channel of type :attr:`ChannelType.group`. + .. attribute:: recipient_remove + + The system message when a recipient is removed from a group private + message, i.e. a private channel of type :attr:`ChannelType.group`. + .. attribute:: call + + The system message denoting call state, e.g. missed call, started call, + etc. + .. attribute:: channel_name_change + + The system message denoting that a channel's name has been changed. + .. attribute:: channel_icon_change + + The system message denoting that a channel's icon has been changed. + .. class:: ServerRegion Specifies the region a :class:`Server`'s voice server belongs to. @@ -488,6 +515,12 @@ Message .. autoclass:: Message :members: +CallMessage +~~~~~~~~~~~~ + +.. autoclass:: CallMessage + :members: + Server ~~~~~~