mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-19 15:36:02 +00:00
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.
This commit is contained in:
parent
2f562bd695
commit
f8f8f418f3
@ -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
|
||||
|
565
discord/activity.py
Normal file
565
discord/activity.py
Normal file
@ -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)
|
@ -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
|
||||
|
@ -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``.
|
||||
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
47
docs/api.rst
47
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
|
||||
~~~~~~~~~~~~
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user