From 58641601391cff3c66826d0d0e487ed66b9ce967 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Tue, 8 Oct 2019 18:26:19 +0000 Subject: [PATCH] Switch to requests session persistant headers, add api bits, vanity_url property to guild and fix some event & embed behaviours. (#153) * guild_subscriptions * Replace get_vanity_url function with vanity_url property on guild object. * Add voice regions + conform guild.vanity_url to standard seen for other url properties on guild. * style change. * Add guild prune and prune count. * Switch to requests session persistent headers * add Stream permission * guild.preferred_locale * Fix logic on GuildCreate and GuildDelete shotcut properties. * preferred_locale already added in another pr. * Convert embed datetime timestamp to iso8610 format for api calls. * Update disco/types/voice.py Co-Authored-By: Andrei Zbikowski * remove botch isoformat on embed method * Apply suggestions from code review Co-Authored-By: Andrei Zbikowski * More suggestions from code review --- disco/api/client.py | 22 +++++++++++++++++++++- disco/api/http.py | 24 +++++++++++------------- disco/client.py | 3 +++ disco/gateway/client.py | 1 + disco/gateway/events.py | 10 +++++----- disco/types/guild.py | 26 +++++++++++++++++++++++--- disco/types/permissions.py | 1 + disco/types/voice.py | 17 ++++++++++++++++- 8 files changed, 81 insertions(+), 23 deletions(-) diff --git a/disco/api/client.py b/disco/api/client.py index a319bd8..e1ca1f7 100644 --- a/disco/api/client.py +++ b/disco/api/client.py @@ -11,9 +11,10 @@ from disco.util.logging import LoggingClass from disco.util.sanitize import S from disco.types.user import User from disco.types.message import Message -from disco.types.guild import Guild, GuildMember, GuildBan, Role, GuildEmoji, AuditLogEntry +from disco.types.guild import Guild, GuildMember, GuildBan, PruneCount, Role, GuildEmoji, AuditLogEntry from disco.types.channel import Channel from disco.types.invite import Invite +from disco.types.voice import VoiceRegion from disco.types.webhook import Webhook @@ -432,6 +433,17 @@ class APIClient(LoggingClass): dict(guild=guild, user=user), headers=_reason_header(reason)) + def guilds_prune_count_get(self, guild, days=None): + r = self.http(Routes.GUILDS_PRUNE_COUNT, dict(guild=guild), params=optional(days=days)) + return PruneCount.create(self.client, r.json()) + + def guilds_prune_create(self, guild, days=None, compute_prune_count=None): + r = self.http(Routes.GUILDS_PRUNE_CREATE, dict(guild=guild), params=optional( + days=days, + compute_prune_count=compute_prune_count, + )) + return PruneCount.create(self.client, r.json()) + def guilds_roles_list(self, guild): r = self.http(Routes.GUILDS_ROLES_LIST, dict(guild=guild)) return Role.create_map(self.client, r.json(), guild_id=guild) @@ -492,6 +504,10 @@ class APIClient(LoggingClass): def guilds_roles_delete(self, guild, role, reason=None): self.http(Routes.GUILDS_ROLES_DELETE, dict(guild=guild, role=role), headers=_reason_header(reason)) + def guilds_voice_regions_list(self, guild): + r = self.http(Routes.GUILDS_VOICE_REGIONS_LIST, dict(guild=guild)) + return VoiceRegion.create_hash(self.client, 'id', r.json()) + def guilds_invites_list(self, guild): r = self.http(Routes.GUILDS_INVITES_LIST, dict(guild=guild)) return Invite.create_map(self.client, r.json()) @@ -576,6 +592,10 @@ class APIClient(LoggingClass): r = self.http(Routes.INVITES_DELETE, dict(invite=invite), headers=_reason_header(reason)) return Invite.create(self.client, r.json()) + def voice_regions_list(self): + r = self.http(Routes.VOICE_REGIONS_LIST) + return VoiceRegion.create_hash(self.client, 'id', r.json()) + def webhooks_get(self, webhook): r = self.http(Routes.WEBHOOKS_GET, dict(webhook=webhook)) return Webhook.create(self.client, r.json()) diff --git a/disco/api/http.py b/disco/api/http.py index fb2c1a4..a035ccf 100644 --- a/disco/api/http.py +++ b/disco/api/http.py @@ -89,7 +89,7 @@ class Routes(object): GUILDS_ROLES_MODIFY = (HTTPMethod.PATCH, GUILDS + '/roles/{role}') GUILDS_ROLES_DELETE = (HTTPMethod.DELETE, GUILDS + '/roles/{role}') GUILDS_PRUNE_COUNT = (HTTPMethod.GET, GUILDS + '/prune') - GUILDS_PRUNE_BEGIN = (HTTPMethod.POST, GUILDS + '/prune') + GUILDS_PRUNE_CREATE = (HTTPMethod.POST, GUILDS + '/prune') GUILDS_VOICE_REGIONS_LIST = (HTTPMethod.GET, GUILDS + '/regions') GUILDS_VANITY_URL_GET = (HTTPMethod.GET, GUILDS + '/vanity-url') GUILDS_INVITES_LIST = (HTTPMethod.GET, GUILDS + '/invites') @@ -124,6 +124,10 @@ class Routes(object): INVITES_GET = (HTTPMethod.GET, INVITES + '/{invite}') INVITES_DELETE = (HTTPMethod.DELETE, INVITES + '/{invite}') + # Voice + VOICE = '/voice' + VOICE_REGIONS_LIST = (HTTPMethod.GET, VOICE + '/regions') + # Webhooks WEBHOOKS = '/webhooks/{webhook}' WEBHOOKS_GET = (HTTPMethod.GET, WEBHOOKS) @@ -202,18 +206,18 @@ class HTTPClient(LoggingClass): sys.version_info.micro) self.limiter = RateLimiter() - self.headers = { + self.after_request = after_request + + self.session = requests.Session() + self.session.headers.update({ 'User-Agent': 'DiscordBot (https://github.com/b1naryth1ef/disco {}) Python/{} requests/{}'.format( disco_version, py_version, requests_version), - } + }) if token: - self.headers['Authorization'] = 'Bot ' + token - - self.after_request = after_request - self.session = requests.Session() + self.session.headers['Authorization'] = 'Bot ' + token def __call__(self, route, args=None, **kwargs): return self.call(route, args, **kwargs) @@ -251,12 +255,6 @@ class HTTPClient(LoggingClass): args = args or {} retry = kwargs.pop('retry_number', 0) - # Merge or set headers - if 'headers' in kwargs: - kwargs['headers'].update(self.headers) - else: - kwargs['headers'] = self.headers - # Build the bucket URL args = {k: to_bytes(v) for k, v in six.iteritems(args)} filtered = {k: (v if k in ('guild', 'channel') else '') for k, v in six.iteritems(args)} diff --git a/disco/client.py b/disco/client.py index c480af9..6445bad 100644 --- a/disco/client.py +++ b/disco/client.py @@ -25,6 +25,8 @@ class ClientConfig(Config): The shard ID for the current client instance. shard_count : int The total count of shards running. + guild_subscriptions : bool + Whether to enable subscription events (e.g. presence and typing). max_reconnects : int The maximum number of connection retries to make before giving up (0 = never give up). log_level: str @@ -42,6 +44,7 @@ class ClientConfig(Config): token = '' shard_id = 0 shard_count = 1 + guild_subscriptions = True max_reconnects = 5 log_level = 'info' diff --git a/disco/gateway/client.py b/disco/gateway/client.py index e997a98..2876f48 100644 --- a/disco/gateway/client.py +++ b/disco/gateway/client.py @@ -215,6 +215,7 @@ class GatewayClient(LoggingClass): 'token': self.client.config.token, 'compress': True, 'large_threshold': 250, + 'guild_subscriptions': self.client.config.guild_subscriptions, 'shard': [ int(self.client.config.shard_id), int(self.client.config.shard_count), diff --git a/disco/gateway/events.py b/disco/gateway/events.py index e187502..c5abd28 100644 --- a/disco/gateway/events.py +++ b/disco/gateway/events.py @@ -7,7 +7,7 @@ from disco.types.channel import Channel, PermissionOverwrite from disco.types.message import Message, MessageReactionEmoji from disco.types.voice import VoiceState from disco.types.guild import Guild, GuildMember, Role, GuildEmoji -from disco.types.base import Model, ModelMeta, Field, ListField, AutoDictField, snowflake, datetime +from disco.types.base import Model, ModelMeta, Field, ListField, AutoDictField, UNSET, snowflake, datetime from disco.util.string import underscore # Mapping of discords event name to our event classes @@ -163,7 +163,7 @@ class GuildCreate(GatewayEvent): The guild being created (e.g. joined) unavailable : bool If false, this guild is coming online from a previously unavailable state, - and if None, this is a normal guild join event. + and if UNSET, this is a normal guild join event. """ unavailable = Field(bool) presences = ListField(Presence) @@ -173,7 +173,7 @@ class GuildCreate(GatewayEvent): """ Shortcut property which is true when we actually joined the guild. """ - return self.unavailable is None + return self.unavailable is UNSET @wraps_model(Guild) @@ -197,7 +197,7 @@ class GuildDelete(GatewayEvent): id : snowflake The ID of the guild being deleted. unavailable : bool - If true, this guild is becoming unavailable, if None this is a normal + If true, this guild is becoming unavailable, if UNSET this is a normal guild leave event. """ id = Field(snowflake) @@ -208,7 +208,7 @@ class GuildDelete(GatewayEvent): """ Shortcut property which is true when we actually have left the guild. """ - return self.unavailable is None + return self.unavailable is UNSET @wraps_model(Channel) diff --git a/disco/types/guild.py b/disco/types/guild.py index 55efdb5..e7c16a7 100644 --- a/disco/types/guild.py +++ b/disco/types/guild.py @@ -79,6 +79,10 @@ class GuildEmoji(Emoji): return self.client.state.guilds.get(self.guild_id) +class PruneCount(SlottedModel): + pruned = Field(int, default=None) + + class Role(SlottedModel): """ A role object. @@ -417,6 +421,12 @@ class Guild(SlottedModel, Permissible): return self.members.get(user) + def get_prune_count(self, days=None): + return self.client.api.guilds_prune_count_get(self.id, days=days) + + def prune(self, days=None, compute_prune_count=None): + return self.client.api.guilds_prune_create(self.id, days=days, compute_prune_count=compute_prune_count) + def create_role(self, **kwargs): """ Create a new role. @@ -517,15 +527,15 @@ class Guild(SlottedModel, Permissible): def get_invites(self): return self.client.api.guilds_invites_list(self.id) - def get_vanity_url(self): - return self.client.api.guilds_vanity_url_get(self.id) - def get_emojis(self): return self.client.api.guilds_emojis_list(self.id) def get_emoji(self, emoji): return self.client.api.guilds_emojis_get(self.id, emoji) + def get_voice_regions(self): + return self.client.api.guilds_voice_regions_list(self.id) + def get_icon_url(self, still_format='webp', animated_format='gif', size=1024): if not self.icon: return '' @@ -539,6 +549,12 @@ class Guild(SlottedModel, Permissible): self.id, self.icon, still_format, size ) + def get_vanity_url(self): + if not self.vanity_url_code: + return '' + + return 'https://discord.gg/' + self.vanity_url_code + def get_splash_url(self, fmt='webp', size=1024): if not self.splash: return '' @@ -555,6 +571,10 @@ class Guild(SlottedModel, Permissible): def icon_url(self): return self.get_icon_url() + @property + def vanity_url(self): + return self.get_vanity_url() + @property def splash_url(self): return self.get_splash_url() diff --git a/disco/types/permissions.py b/disco/types/permissions.py index f6ebe0f..b4f5f7d 100644 --- a/disco/types/permissions.py +++ b/disco/types/permissions.py @@ -11,6 +11,7 @@ class Permissions(object): ADD_REACTIONS = 1 << 6 VIEW_AUDIT_LOG = 1 << 7 PRIORITY_SPEAKER = 1 << 8 + STREAM = 1 << 9 VIEW_CHANNEL = 1 << 10 SEND_MESSAGES = 1 << 11 SEND_TSS_MESSAGES = 1 << 12 diff --git a/disco/types/voice.py b/disco/types/voice.py index bb7c055..fc00087 100644 --- a/disco/types/voice.py +++ b/disco/types/voice.py @@ -1,4 +1,4 @@ -from disco.types.base import SlottedModel, Field, snowflake, cached_property +from disco.types.base import SlottedModel, text, Field, snowflake, cached_property class VoiceState(SlottedModel): @@ -23,3 +23,18 @@ class VoiceState(SlottedModel): @cached_property def user(self): return self.client.state.users.get(self.user_id) + + +class VoiceRegion(SlottedModel): + id = Field(text) + name = Field(text) + vip = Field(bool) + optimal = Field(bool) + deprecated = Field(bool) + custom = Field(bool) + + def __str__(self): + return self.id + + def __repr__(self): + return u''.format(self.name)