aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2018-03-05 11:01:46 -0500
committerRapptz <[email protected]>2018-03-05 11:15:49 -0500
commitf8f8f418f3c51b6a885a1b6b7cd46c38c070b3bc (patch)
tree0f26ed361806cf4470b8d98b61f63d2055cf87d0
parentUpdate docstrings for channel.py (diff)
downloaddiscord.py-f8f8f418f3c51b6a885a1b6b7cd46c38c070b3bc.tar.xz
discord.py-f8f8f418f3c51b6a885a1b6b7cd46c38c070b3bc.zip
Split Game object to separate Activity subtypes for Rich Presences.
This is a massive breaking change. * All references to "game" have been renamed to "activity" * Activity objects contain a majority of the rich presence information * Game and Streaming are subtypes for memory optimisation purposes for the more common cases. * Introduce a more specialised read-only type, Spotify, for the official Spotify integration to make it easier to use.
-rw-r--r--discord/__init__.py2
-rw-r--r--discord/activity.py565
-rw-r--r--discord/client.py39
-rw-r--r--discord/enums.py11
-rw-r--r--discord/game.py87
-rw-r--r--discord/gateway.py18
-rw-r--r--discord/guild.py5
-rw-r--r--discord/member.py23
-rw-r--r--discord/message.py25
-rw-r--r--discord/shard.py24
-rw-r--r--discord/state.py12
-rw-r--r--docs/api.rst47
12 files changed, 708 insertions, 150 deletions
diff --git a/discord/__init__.py b/discord/__init__.py
index 20c81853..3f605a0a 100644
--- a/discord/__init__.py
+++ b/discord/__init__.py
@@ -19,8 +19,8 @@ __version__ = '1.0.0a'
from .client import Client, AppInfo
from .user import User, ClientUser, Profile
-from .game import Game
from .emoji import Emoji, PartialEmoji
+from .activity import *
from .channel import *
from .guild import Guild
from .relationship import Relationship
diff --git a/discord/activity.py b/discord/activity.py
new file mode 100644
index 00000000..db508a4e
--- /dev/null
+++ b/discord/activity.py
@@ -0,0 +1,565 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015-2017 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 .enums import ActivityType, try_enum
+import datetime
+
+__all__ = ('Activity', 'Streaming', 'Game', 'Spotify')
+
+"""If curious, this is the current schema for an activity.
+
+It's fairly long so I will document it here:
+
+All keys are optional.
+
+state: str (max: 128),
+details: str (max: 128)
+timestamps: dict
+ start: int (min: 1)
+ end: int (min: 1)
+assets: dict
+ large_image: str (max: 32)
+ large_text: str (max: 128)
+ small_image: str (max: 32)
+ small_text: str (max: 128)
+party: dict
+ id: str (max: 128),
+ size: List[int] (max-length: 2)
+ elem: int (min: 1)
+secrets: dict
+ match: str (max: 128)
+ join: str (max: 128)
+ spectate: str (max: 128)
+instance: bool
+application_id: str
+name: str (max: 128)
+url: str
+type: int
+sync_id: str
+session_id: str
+flags: int
+
+There are also activity flags which are mostly uninteresting for the library atm.
+
+t.ActivityFlags = {
+ INSTANCE: 1,
+ JOIN: 2,
+ SPECTATE: 4,
+ JOIN_REQUEST: 8,
+ SYNC: 16,
+ PLAY: 32
+}
+"""
+
+class _ActivityTag:
+ __slots__ = ()
+
+class Activity(_ActivityTag):
+ """Represents an activity in Discord.
+
+ This could be an activity such as streaming, playing, listening
+ or watching.
+
+ For memory optimisation purposes, some activities are offered in slimmed
+ down versions:
+
+ - :class:`Game`
+ - :class:`Streaming`
+
+ Attributes
+ ------------
+ application_id: :class:`str`
+ The application ID of the game.
+ name: :class:`str`
+ The name of the activity.
+ url: :class:`str`
+ A stream URL that the activity could be doing.
+ type: :class:`ActivityType`
+ The type of activity currently being done.
+ state: :class:`str`
+ The user's current state. For example, "In Game".
+ details: :class:`str`
+ The detail of the user's current activity.
+ timestamps: :class:`dict`
+ A dictionary of timestamps. It contains the following optional keys:
+
+ - ``start``: Corresponds to when the user started doing the
+ activity in milliseconds since Unix epoch.
+ - ``end``: Corresponds to when the user will finish doing the
+ activity in milliseconds since Unix epoch.
+
+ assets: :class:`dict`
+ A dictionary representing the images and their hover text of an activity.
+ It contains the following optional keys:
+
+ - ``large_image``: A string representing the ID for the large image asset.
+ - ``large_text``: A string representing the text when hovering over the large image asset.
+ - ``small_image``: A string representing the ID for the small image asset.
+ - ``small_text``: A string representing the text when hovering over the small image asset.
+
+ party: :class:`dict`
+ A dictionary representing the activity party. It contains the following optional keys:
+
+ - ``id``: A string representing the party ID.
+ - ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
+ """
+
+ __slots__ = ('state', 'details', 'timestamps', 'assets', 'party',
+ 'flags', 'sync_id', 'session_id', 'type', 'name', 'url', 'application_id')
+
+ def __init__(self, **kwargs):
+ self.state = kwargs.pop('state', None)
+ self.details = kwargs.pop('details', None)
+ self.timestamps = kwargs.pop('timestamps', {})
+ self.assets = kwargs.pop('assets', {})
+ self.party = kwargs.pop('party', {})
+ self.application_id = kwargs.pop('application_id', None)
+ self.name = kwargs.pop('name', None)
+ self.url = kwargs.pop('url', None)
+ self.flags = kwargs.pop('flags', 0)
+ self.sync_id = kwargs.pop('sync_id', None)
+ self.session_id = kwargs.pop('session_id', None)
+ self.type = try_enum(ActivityType, kwargs.pop('type', -1))
+
+ def to_dict(self):
+ ret = {}
+ for attr in self.__slots__:
+ value = getattr(self, attr, None)
+ if value is None:
+ continue
+
+ if isinstance(value, dict) and len(value) == 0:
+ continue
+
+ ret[attr] = value
+ ret['type'] = int(self.type)
+ return ret
+
+ @property
+ def start(self):
+ """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
+ try:
+ return datetime.datetime.utcfromtimestamp(self.timestamps['start'] / 1000)
+ except KeyError:
+ return None
+
+ @property
+ def end(self):
+ """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
+ try:
+ return datetime.datetime.utcfromtimestamp(self.timestamps['end'] / 1000)
+ except KeyError:
+ return None
+
+ @property
+ def large_image_url(self):
+ """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity if applicable."""
+ if self.application_id is None:
+ return None
+
+ try:
+ large_image = self.assets['large_image']
+ except KeyError:
+ return None
+ else:
+ return 'htps://cdn.discordapp.com/app-assets/{0}/{1}.png'.format(self.application_id, large_image)
+
+ @property
+ def small_image_url(self):
+ """Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity if applicable."""
+ if self.application_id is None:
+ return None
+
+ try:
+ small_image = self.assets['small_image']
+ except KeyError:
+ return None
+ else:
+ return 'htps://cdn.discordapp.com/app-assets/{0}/{1}.png'.format(self.application_id, small_image)
+ @property
+ def large_image_text(self):
+ """Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
+ return self.assets.get('large_text', None)
+
+ @property
+ def small_image_text(self):
+ """Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable."""
+ return self.assets.get('small_text', None)
+
+
+class Game(_ActivityTag):
+ """A slimmed down version of :class:`Activity` that represents a Discord game.
+
+ This is typically displayed via **Playing** on the official Discord client.
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two games are equal.
+
+ .. describe:: x != y
+
+ Checks if two games are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the game's hash.
+
+ .. describe:: str(x)
+
+ Returns the game's name.
+
+ Parameters
+ -----------
+ name: :class:`str`
+ The game's name.
+ start: Optional[:class:`datetime.datetime`]
+ A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
+ end: Optional[:class:`datetime.datetime`]
+ A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The game's name.
+ """
+
+ __slots__ = ('name', '_end', '_start')
+
+ def __init__(self, name, **extra):
+ self.name = name
+
+ try:
+ timestamps = extra['timestamps']
+ except KeyError:
+ self._extract_timestamp(extra, 'start')
+ self._extract_timestamp(extra, 'end')
+ else:
+ self._start = timestamps.get('start', 0)
+ self._end = timestamps.get('end', 0)
+
+ def _extract_timestamp(self, data, key):
+ try:
+ dt = data[key]
+ except KeyError:
+ setattr(self, '_' + key, 0)
+ else:
+ setattr(self, '_' + key, dt.timestamp() * 1000.0)
+
+ @property
+ def type(self):
+ """Returns the game's type. This is for compatibility with :class:`Activity`.
+
+ It always returns :attr:`ActivityType.playing`.
+ """
+ return ActivityType.playing
+
+ @property
+ def start(self):
+ """Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
+ if self._start:
+ return datetime.datetime.utcfromtimestamp(self._start / 1000)
+ return None
+
+ @property
+ def end(self):
+ """Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
+ if self._end:
+ return datetime.datetime.utcfromtimestamp(self._end / 1000)
+ return None
+
+ def __str__(self):
+ return str(self.name)
+
+ def __repr__(self):
+ return '<Game name={0.name!r}>'.format(self)
+
+ def to_dict(self):
+ timestamps = {}
+ if self._start:
+ timestamps['start'] = self._start
+
+ if self._end:
+ timestamps['end'] = self._end
+
+ return {
+ 'type': ActivityType.playing.value,
+ 'name': str(self.name),
+ 'timestamps': timestamps
+ }
+
+ def __eq__(self, other):
+ return isinstance(other, Game) and other.name == self.name
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return hash(self.name)
+
+class Streaming(_ActivityTag):
+ """A slimmed down version of :class:`Activity` that represents a Discord streaming status.
+
+ This is typically displayed via **Streaming** on the official Discord client.
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two streams are equal.
+
+ .. describe:: x != y
+
+ Checks if two streams are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the stream's hash.
+
+ .. describe:: str(x)
+
+ Returns the stream's name.
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The stream's name.
+ url: :class:`str`
+ The stream's URL. Currently only twitch.tv URLs are supported. Anything else is silently
+ discarded.
+ details: Optional[:class:`str`]
+ If provided, typically the game the streamer is playing.
+ assets: :class:`dict`
+ A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
+ """
+
+ __slots__ = ('name', 'url', 'details', 'assets')
+
+ def __init__(self, *, name, url, **extra):
+ self.name = name
+ self.url = url
+ self.details = extra.pop('details', None)
+ self.assets = extra.pop('assets', {})
+
+ @property
+ def type(self):
+ """Returns the game's type. This is for compatibility with :class:`Activity`.
+
+ It always returns :attr:`ActivityType.streaming`.
+ """
+ return ActivityType.streaming
+
+ def __str__(self):
+ return str(self.name)
+
+ def __repr__(self):
+ return '<Streaming name={0.name!r}>'.format(self)
+
+ @property
+ def twitch_name(self):
+ """Optional[:class:`str`]: If provided, the twitch name of the user streaming.
+
+ This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
+ dictionary if it starts with ``twitch:``. Typically set by the Discord client.
+ """
+
+ try:
+ name = self.assets['large_image']
+ except KeyError:
+ return None
+ else:
+ return name[7:] if name[:7] == 'twitch:' else None
+
+ def to_dict(self):
+ ret = {
+ 'type': ActivityType.streaming.value,
+ 'name': str(self.name),
+ 'url': str(self.url),
+ 'assets': self.assets
+ }
+ if self.details:
+ ret['details'] = self.details
+ return ret
+
+ def __eq__(self, other):
+ return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return hash(self.name)
+
+class Spotify:
+ """Represents a Spotify listening activity from Discord. This is a special case of
+ :class:`Activity` that makes it easier to work with the Spotify integration.
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two activities are equal.
+
+ .. describe:: x != y
+
+ Checks if two activities are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the activity's hash.
+
+ .. describe:: str(x)
+
+ Returns the string 'Spotify'.
+ """
+
+ __slots__ = ('_state', '_details', '_timestamps', '_assets', '_party', '_sync_id', '_session_id')
+
+ def __init__(self, **data):
+ self._state = data.pop('state', None)
+ self._details = data.pop('details', None)
+ self._timestamps = data.pop('timestamps', {})
+ self._assets = data.pop('assets', {})
+ self._party = data.pop('party', {})
+ self._sync_id = data.pop('sync_id')
+ self._session_id = data.pop('session_id')
+
+ @property
+ def type(self):
+ """Returns the activity's type. This is for compatibility with :class:`Activity`.
+
+ It always returns :attr:`ActivityType.listening`.
+ """
+ return ActivityType.listening
+
+ def to_dict(self):
+ return {
+ 'flags': 48, # SYNC | PLAY
+ 'name': 'Spotify',
+ 'assets': self._assets,
+ 'party': self._party,
+ 'sync_id': self._sync_id,
+ 'session_id': self.session_id,
+ 'timestamps': self._timestamps,
+ 'details': self._details,
+ 'state': self._state
+ }
+
+ @property
+ def name(self):
+ """:class:`str`: The activity's name. This will always return "Spotify"."""
+ return 'Spotify'
+
+ def __eq__(self, other):
+ return isinstance(other, Spotify) and other._session_id == self._session_id
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return hash(self._session_id)
+
+ def __str__(self):
+ return 'Spotify'
+
+ def __repr__(self):
+ return '<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>'.format(self)
+
+ @property
+ def title(self):
+ """:class:`str`: The title of the song being played."""
+ return self._details
+
+ @property
+ def artists(self):
+ """List[:class:`str`]: The artists of the song being played."""
+ return self._state.split(';')
+
+ @property
+ def artist(self):
+ """:class:`str`: The artist of the song being played.
+
+ This does not attempt to split the artist information into
+ multiple artists. Useful if there's only a single artist.
+ """
+ return self._state
+
+ @property
+ def album(self):
+ """:class:`str`: The album that the song being played belongs to."""
+ return self._assets.get('large_text', '')
+
+ @property
+ def album_cover_url(self):
+ """:class:`str`: The album cover image URL from Spotify's CDN."""
+ large_image = self._assets.get('large_image', '')
+ if large_image[:8] != 'spotify:':
+ return ''
+ album_image_id = large_image[8:]
+ return 'https://i.scdn.co/image/' + album_image_id
+
+ @property
+ def track_id(self):
+ """:class:`str`: The track ID used by Spotify to identify this song."""
+ return self._sync_id
+
+ @property
+ def start(self):
+ """:class:`datetime.datetime`: When the user started playing this song in UTC."""
+ return datetime.datetime.utcfromtimestamp(self._timestamps['start'] / 1000)
+
+ @property
+ def end(self):
+ """:class:`datetime.datetime`: When the user will stop playing this song in UTC."""
+ return datetime.datetime.utcfromtimestamp(self._timestamps['end'] / 1000)
+
+ @property
+ def duration(self):
+ """:class:`datetime.timedelta`: The duration of the song being played."""
+ return self.end - self.start
+
+ @property
+ def party_id(self):
+ """:class:`str`: The party ID of the listening party."""
+ return self._party.get('id', '')
+
+def create_activity(data):
+ if not data:
+ return None
+
+ game_type = try_enum(ActivityType, data.get('type', -1))
+ if game_type is ActivityType.playing:
+ if 'application_id' in data or 'session_id' in data:
+ return Activity(**data)
+ return Game(**data)
+ elif game_type is ActivityType.streaming:
+ if 'url' in data:
+ return Streaming(**data)
+ return Activity(**data)
+ elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data:
+ return Spotify(**data)
+ return Activity(**data)
diff --git a/discord/client.py b/discord/client.py
index d2a4951e..bd4c35e9 100644
--- a/discord/client.py
+++ b/discord/client.py
@@ -72,8 +72,8 @@ class Client:
.. _ProxyConnector: http://aiohttp.readthedocs.org/en/stable/client_reference.html#proxyconnector
Parameters
- ----------
- max_messages : Optional[int]
+ -----------
+ max_messages : Optional[:class:`int`]
The maximum number of messages to store in the internal message cache.
This defaults to 5000. Passing in `None` or a value less than 100
will use the default instead of the passed in value.
@@ -82,24 +82,24 @@ class Client:
in which case the default event loop is used via ``asyncio.get_event_loop()``.
connector : aiohttp.BaseConnector
The `connector`_ to use for connection pooling.
- proxy : Optional[str]
+ proxy : Optional[:class:`str`]
Proxy URL.
proxy_auth : Optional[aiohttp.BasicAuth]
An object that represents proxy HTTP Basic Authorization.
- shard_id : Optional[int]
+ shard_id : Optional[:class:`int`]
Integer starting at 0 and less than shard_count.
- shard_count : Optional[int]
+ shard_count : Optional[:class:`int`]
The total number of shards.
- fetch_offline_members: bool
+ fetch_offline_members: :class:`bool`
Indicates if :func:`on_ready` should be delayed to fetch all offline
members from the guilds the bot belongs to. If this is ``False``\, then
no offline members are received and :meth:`request_offline_members`
must be used to fetch the offline members of the guild.
- game: Optional[:class:`Game`]
- A game to start your presence with upon logging on to Discord.
status: Optional[:class:`Status`]
A status to start your presence with upon logging on to Discord.
- heartbeat_timeout: float
+ activity: Optional[Union[:class:`Activity`, :class:`Game`, :class:`Streaming`]]
+ An activity to start your presence with upon logging on to Discord.
+ heartbeat_timeout: :class:`float`
The maximum numbers of seconds before timing out and restarting the
WebSocket in the case of not receiving a HEARTBEAT_ACK. Useful if
processing the initial packets take too long to the point of disconnecting
@@ -794,23 +794,24 @@ class Client:
return self.event(coro)
@asyncio.coroutine
- def change_presence(self, *, game=None, status=None, afk=False):
+ def change_presence(self, *, activity=None, status=None, afk=False):
"""|coro|
Changes the client's presence.
- The game parameter is a Game object (not a string) that represents
- a game being played currently.
+ The activity parameter is a :class:`Activity` object (not a string) that represents
+ the activity being done currently. This could also be the slimmed down versions,
+ :class:`Game` and :class:`Streaming`.
Example: ::
- game = discord.Game(name="with the API")
- await client.change_presence(status=discord.Status.idle, game=game)
+ game = discord.Game("with the API")
+ await client.change_presence(status=discord.Status.idle, activity=game)
Parameters
----------
- game: Optional[:class:`Game`]
- The game being played. None if no game is being played.
+ activity: Optional[Union[:class:`Game`, :class:`Streaming`, :class:`Activity`]]
+ The activity being done. ``None`` if no currently active activity is done.
status: Optional[:class:`Status`]
Indicates what status to change to. If None, then
:attr:`Status.online` is used.
@@ -822,7 +823,7 @@ class Client:
Raises
------
InvalidArgument
- If the ``game`` parameter is not :class:`Game` or None.
+ If the ``activity`` parameter is not the proper type.
"""
if status is None:
@@ -835,14 +836,14 @@ class Client:
status_enum = status
status = str(status)
- yield from self.ws.change_presence(game=game, status=status, afk=afk)
+ yield from self.ws.change_presence(activity=activity, status=status, afk=afk)
for guild in self._connection.guilds:
me = guild.me
if me is None:
continue
- me.game = game
+ me.activity = activity
me.status = status_enum
# Guild stuff
diff --git a/discord/enums.py b/discord/enums.py
index e215e482..f243359e 100644
--- a/discord/enums.py
+++ b/discord/enums.py
@@ -28,7 +28,8 @@ from enum import Enum, IntEnum
__all__ = ['ChannelType', 'MessageType', 'VoiceRegion', 'VerificationLevel',
'ContentFilter', 'Status', 'DefaultAvatar', 'RelationshipType',
- 'AuditLogAction', 'AuditLogActionCategory', 'UserFlags', ]
+ 'AuditLogAction', 'AuditLogActionCategory', 'UserFlags',
+ 'ActivityType', ]
class ChannelType(Enum):
text = 0
@@ -212,6 +213,14 @@ class UserFlags(Enum):
partner = 2
hypesquad = 4
+class ActivityType(IntEnum):
+ unknown = -1
+ playing = 0
+ streaming = 1
+ listening = 2
+ watching = 3
+
+
def try_enum(cls, val):
"""A function that tries to turn the value into enum ``cls``.
diff --git a/discord/game.py b/discord/game.py
deleted file mode 100644
index 8ca83c1d..00000000
--- a/discord/game.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-The MIT License (MIT)
-
-Copyright (c) 2015-2017 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.
-"""
-
-class Game:
- """Represents a Discord game.
-
- .. container:: operations
-
- .. describe:: x == y
-
- Checks if two games are equal.
-
- .. describe:: x != y
-
- Checks if two games are not equal.
-
- .. describe:: hash(x)
-
- Returns the game's hash.
-
- .. describe:: str(x)
-
- Returns the game's name.
-
- Attributes
- -----------
- name: :class:`str`
- The game's name.
- url: :class:`str`
- The game's URL. Usually used for twitch streaming.
- type: :class:`int`
- The type of game being played. 1 indicates "Streaming".
- """
-
- __slots__ = ('name', 'type', 'url')
-
- def __init__(self, **kwargs):
- self.name = kwargs.get('name')
- self.url = kwargs.get('url')
- self.type = kwargs.get('type', 0)
-
- def __str__(self):
- return str(self.name)
-
- def __repr__(self):
- return '<Game name={0.name!r} type={0.type!r} url={0.url!r}>'.format(self)
-
- def _iterator(self):
- for attr in self.__slots__:
- value = getattr(self, attr, None)
- if value is not None:
- yield (attr, value)
-
- def __iter__(self):
- return self._iterator()
-
- def __eq__(self, other):
- return isinstance(other, Game) and other.name == self.name
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __hash__(self):
- return hash(self.name)
diff --git a/discord/gateway.py b/discord/gateway.py
index 6d535a38..7bd7c8de 100644
--- a/discord/gateway.py
+++ b/discord/gateway.py
@@ -30,7 +30,7 @@ import websockets
import asyncio
from . import utils, compat
-from .game import Game
+from .activity import create_activity, _ActivityTag
from .errors import ConnectionClosed, InvalidArgument
import logging
import zlib, json
@@ -283,10 +283,10 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol):
payload['d']['shard'] = [self.shard_id, self.shard_count]
state = self._connection
- if state._game is not None or state._status is not None:
+ if state._activity is not None or state._status is not None:
payload['d']['presence'] = {
'status': state._status,
- 'game': state._game,
+ 'game': state._activity,
'since': 0,
'afk': False
}
@@ -469,19 +469,19 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol):
raise ConnectionClosed(e, shard_id=self.shard_id) from e
@asyncio.coroutine
- def change_presence(self, *, game=None, status=None, afk=False, since=0.0):
- if game is not None and not isinstance(game, Game):
- raise InvalidArgument('game must be of type Game or None')
+ def change_presence(self, *, activity=None, status=None, afk=False, since=0.0):
+ if activity is not None:
+ if not isinstance(activity, _ActivityTag):
+ raise InvalidArgument('activity must be one of Game, Streaming, or Activity.')
+ activity = activity.to_dict()
if status == 'idle':
since = int(time.time() * 1000)
- sent_game = dict(game) if game else None
-
payload = {
'op': self.PRESENCE,
'd': {
- 'game': sent_game,
+ 'game': activity,
'afk': afk,
'since': since,
'status': status
diff --git a/discord/guild.py b/discord/guild.py
index 37f0c60c..6141d0f2 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -32,7 +32,7 @@ from collections import namedtuple, defaultdict
from . import utils
from .role import Role
from .member import Member, VoiceState
-from .game import Game
+from .activity import create_activity
from .permissions import PermissionOverwrite
from .colour import Colour
from .errors import InvalidArgument, ClientException
@@ -243,8 +243,7 @@ class Guild(Hashable):
member = self.get_member(user_id)
if member is not None:
member.status = try_enum(Status, presence['status'])
- game = presence.get('game', {})
- member.game = Game(**game) if game else None
+ member.activity = create_activity(presence.get('game'))
if 'channels' in data:
channels = data['channels']
diff --git a/discord/member.py b/discord/member.py
index 319d252b..11bbfcf4 100644
--- a/discord/member.py
+++ b/discord/member.py
@@ -32,7 +32,7 @@ import discord.abc
from . import utils
from .user import BaseUser, User
-from .game import Game
+from .activity import create_activity
from .permissions import Permissions
from .enums import Status, try_enum
from .colour import Colour
@@ -137,25 +137,25 @@ class Member(discord.abc.Messageable, _BaseUser):
Attributes
----------
- roles
+ roles: List[:class:`Role`]
A :class:`list` of :class:`Role` that the member belongs to. Note that the first element of this
list is always the default '@everyone' role. These roles are sorted by their position
in the role hierarchy.
- joined_at : `datetime.datetime`
+ joined_at: `datetime.datetime`
A datetime object that specifies the date and time in UTC that the member joined the guild for
the first time.
status : :class:`Status`
The member's status. There is a chance that the status will be a :class:`str`
if it is a value that is not recognised by the enumerator.
- game : :class:`Game`
- The game that the user is currently playing. Could be None if no game is being played.
- guild : :class:`Guild`
+ activity: Union[:class:`Game`, :class:`Streaming`, :class:`Activity`]
+ The activity that the user is currently doing. Could be None if no activity is being done.
+ guild: :class:`Guild`
The guild that the member belongs to.
- nick : Optional[:class:`str`]
+ nick: Optional[:class:`str`]
The guild specific nickname of the user.
"""
- __slots__ = ('roles', 'joined_at', 'status', 'game', 'guild', 'nick', '_user', '_state')
+ __slots__ = ('roles', 'joined_at', 'status', 'activity', 'guild', 'nick', '_user', '_state')
def __init__(self, *, data, guild, state):
self._state = state
@@ -164,8 +164,7 @@ class Member(discord.abc.Messageable, _BaseUser):
self.joined_at = utils.parse_time(data.get('joined_at'))
self._update_roles(data)
self.status = Status.offline
- game = data.get('game', {})
- self.game = Game(**game) if game else None
+ self.activity = create_activity(data.get('game'))
self.nick = data.get('nick', None)
def __str__(self):
@@ -218,8 +217,8 @@ class Member(discord.abc.Messageable, _BaseUser):
def _presence_update(self, data, user):
self.status = try_enum(Status, data['status'])
- game = data.get('game', {})
- self.game = Game(**game) if game else None
+ self.activity = create_activity(data.get('game'))
+
u = self._user
u.name = user.get('username', u.name)
u.avatar = user.get('avatar', u.avatar)
diff --git a/discord/message.py b/discord/message.py
index aef1cb1a..68842a11 100644
--- a/discord/message.py
+++ b/discord/message.py
@@ -175,6 +175,24 @@ class Message:
Specifies if the message is currently pinned.
reactions : List[:class:`Reaction`]
Reactions to a message. Reactions can be either custom emoji or standard unicode emoji.
+ activity: Optional[:class:`dict`]
+ The activity associated with this message. Sent with Rich-Presence related messages that for
+ example, request joining, spectating, or listening to or with another member.
+
+ It is a dictionary with the following optional keys:
+
+ - ``type``: An integer denoting the type of message activity being requested.
+ - ``party_id``: The party ID associated with the party.
+ application: Optional[:class:`dict`]
+ The rich presence enabled application associated with this message.
+
+ It is a dictionary with the following keys:
+
+ - ``id``: A string representing the application's ID.
+ - ``name``: A string representing the application's name.
+ - ``description``: A string representing the application's description.
+ - ``icon``: A string representing the icon ID of the application.
+ - ``cover_image``: A string representing the embed's image asset ID.
"""
__slots__ = ( '_edited_timestamp', 'tts', 'content', 'channel', 'webhook_id',
@@ -182,13 +200,16 @@ class Message:
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
'role_mentions', '_cs_raw_role_mentions', 'type', 'call',
- '_cs_system_content', '_cs_guild', '_state', 'reactions' )
+ '_cs_system_content', '_cs_guild', '_state', 'reactions',
+ 'application', 'activity' )
def __init__(self, *, state, channel, data):
self._state = state
self.id = int(data['id'])
self.webhook_id = utils._get_as_snowflake(data, 'webhook_id')
self.reactions = [Reaction(message=self, data=d) for d in data.get('reactions', [])]
+ self.application = data.get('application')
+ self.activity = data.get('activity')
self._update(channel, data)
def __repr__(self):
@@ -242,6 +263,8 @@ class Message:
self.channel = channel
self._edited_timestamp = utils.parse_time(data.get('edited_timestamp'))
self._try_patch(data, 'pinned')
+ self._try_patch(data, 'application')
+ self._try_patch(data, 'activity')
self._try_patch(data, 'mention_everyone')
self._try_patch(data, 'tts')
self._try_patch(data, 'type', lambda x: try_enum(MessageType, x))
diff --git a/discord/shard.py b/discord/shard.py
index b29e45d0..284f5cdc 100644
--- a/discord/shard.py
+++ b/discord/shard.py
@@ -307,18 +307,24 @@ class AutoShardedClient(Client):
yield from self.http.close()
@asyncio.coroutine
- def change_presence(self, *, game=None, status=None, afk=False, shard_id=None):
+ def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None):
"""|coro|
Changes the client's presence.
- The game parameter is a Game object (not a string) that represents
- a game being played currently.
+ The activity parameter is a :class:`Activity` object (not a string) that represents
+ the activity being done currently. This could also be the slimmed down versions,
+ :class:`Game` and :class:`Streaming`.
+
+ Example: ::
+
+ game = discord.Game("with the API")
+ await client.change_presence(status=discord.Status.idle, activity=game)
Parameters
----------
- game: Optional[:class:`Game`]
- The game being played. None if no game is being played.
+ activity: Optional[Union[:class:`Game`, :class:`Streaming`, :class:`Activity`]]
+ The activity being done. ``None`` if no currently active activity is done.
status: Optional[:class:`Status`]
Indicates what status to change to. If None, then
:attr:`Status.online` is used.
@@ -334,7 +340,7 @@ class AutoShardedClient(Client):
Raises
------
InvalidArgument
- If the ``game`` parameter is not :class:`Game` or None.
+ If the ``activity`` parameter is not of proper type.
"""
if status is None:
@@ -349,12 +355,12 @@ class AutoShardedClient(Client):
if shard_id is None:
for shard in self.shards.values():
- yield from shard.ws.change_presence(game=game, status=status, afk=afk)
+ yield from shard.ws.change_presence(activity=activity, status=status, afk=afk)
guilds = self._connection.guilds
else:
shard = self.shards[shard_id]
- yield from shard.ws.change_presence(game=game, status=status, afk=afk)
+ yield from shard.ws.change_presence(activity=activity, status=status, afk=afk)
guilds = [g for g in self._connection.guilds if g.shard_id == shard_id]
for guild in guilds:
@@ -362,5 +368,5 @@ class AutoShardedClient(Client):
if me is None:
continue
- me.game = game
+ me.activity = activity
me.status = status_enum
diff --git a/discord/state.py b/discord/state.py
index d9a53886..731cb096 100644
--- a/discord/state.py
+++ b/discord/state.py
@@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
"""
from .guild import Guild
+from .activity import _ActivityTag
from .user import User, ClientUser
from .emoji import Emoji, PartialEmoji
from .message import Message
@@ -67,9 +68,12 @@ class ConnectionState:
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
self._listeners = []
- game = options.get('game', None)
- if game:
- game = dict(game)
+ activity = options.get('activity', None)
+ if activity:
+ if not isinstance(activity, _ActivityTag):
+ raise TypeError('activity parameter must be one of Game, Streaming, or Activity.')
+
+ activity = activity.to_dict()
status = options.get('status', None)
if status:
@@ -78,7 +82,7 @@ class ConnectionState:
else:
status = str(status)
- self._game = game
+ self._activity = activity
self._status = status
self.clear()
diff --git a/docs/api.rst b/docs/api.rst
index 30b68374..fc08aee2 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -687,6 +687,27 @@ All enumerations are subclasses of `enum`_.
The system message denoting that a new member has joined a Guild.
+.. class:: ActivityType
+
+ Specifies the type of :class:`Activity`. This is used to check how to
+ interpret the activity itself.
+
+ .. attribute:: unknown
+
+ An unknown activity type. This should generally not happen.
+ .. attribute:: playing
+
+ A "Playing" activity type.
+ .. attribute:: streaming
+
+ A "Streaming" activity type.
+ .. attribute:: listening
+
+ A "Listening" activity type.
+ .. attribute:: watching
+
+ A "Watching" activity type.
+
.. class:: VoiceRegion
Specifies the region a voice server belongs to.
@@ -698,7 +719,7 @@ All enumerations are subclasses of `enum`_.
The US East region.
.. attribute:: us_south
-
+
The US South region.
.. attribute:: us_central
@@ -729,10 +750,10 @@ All enumerations are subclasses of `enum`_.
The Brazil region.
.. attribute:: hongkong
-
+
The Hong Kong region.
.. attribute:: russia
-
+
The Russia region.
.. attribute:: vip_us_east
@@ -1880,6 +1901,12 @@ Member
.. autocomethod:: typing
:async-with:
+Spotify
+~~~~~~~~
+
+.. autoclass:: Spotify()
+ :members:
+
VoiceState
~~~~~~~~~~~
@@ -2011,12 +2038,24 @@ Colour
.. autoclass:: Colour
:members:
+Activity
+~~~~~~~~~
+
+.. autoclass:: Activity
+ :members:
+
Game
-~~~~
+~~~~~
.. autoclass:: Game
:members:
+Streaming
+~~~~~~~~~~~
+
+.. autoclass:: Streaming
+ :members:
+
Permissions
~~~~~~~~~~~~