diff --git a/disco/api/client.py b/disco/api/client.py index 7a5a4e5..b63eeed 100644 --- a/disco/api/client.py +++ b/disco/api/client.py @@ -8,6 +8,7 @@ from disco.types.message import Message from disco.types.guild import Guild, GuildMember, Role from disco.types.channel import Channel from disco.types.invite import Invite +from disco.types.webhook import Webhook def optional(**kwargs): @@ -115,6 +116,17 @@ class APIClient(LoggingClass): def channels_pins_delete(self, channel, message): self.http(Routes.CHANNELS_PINS_DELETE, dict(channel=channel, message=message)) + def channels_webhooks_create(self, channel, name=None, avatar=None): + r = self.http(Routes.CHANNELS_WEBHOOKS_CREATE, dict(channel=channel), json=optional( + name=name, + avatar=avatar, + )) + return Webhook.create(self.client, r.json()) + + def channels_webhooks_list(self, channel): + r = self.http(Routes.CHANNELS_WEBHOOKS_LIST, dict(channel=channel)) + return Webhook.create_map(self.client, r.json()) + def guilds_get(self, guild): r = self.http(Routes.GUILDS_GET, dict(guild=guild)) return Guild.create(self.client, r.json()) @@ -186,6 +198,10 @@ class APIClient(LoggingClass): def guilds_roles_delete(self, guild, role): self.http(Routes.GUILDS_ROLES_DELETE, dict(guild=guild, role=role)) + def guilds_webhooks_list(self, guild): + r = self.http(Routes.GUILDS_WEBHOOKS_LIST, dict(guild=guild)) + return Webhook.create_map(self.client, r.json()) + def invites_get(self, invite): r = self.http(Routes.INVITES_GET, dict(invite=invite)) return Invite.create(self.client, r.json()) @@ -193,3 +209,37 @@ class APIClient(LoggingClass): def invites_delete(self, invite): r = self.http(Routes.INVITES_DELETE, dict(invite=invite)) return Invite.create(self.client, r.json()) + + def webhooks_get(self, webhook): + r = self.http(Routes.WEBHOOKS_GET, dict(webhook=webhook)) + return Webhook.create(self.client, r.json()) + + def webhooks_modify(self, webhook, name=None, avatar=None): + r = self.http(Routes.WEBHOOKS_MODIFY, dict(webhook=webhook), json=optional( + name=name, + avatar=avatar, + )) + return Webhook.create(self.client, r.json()) + + def webhooks_delete(self, webhook): + self.http(Routes.WEBHOOKS_DELETE, dict(webhook=webhook)) + + def webhooks_token_get(self, webhook, token): + r = self.http(Routes.WEBHOOKS_TOKEN_GET, dict(webhook=webhook, token=token)) + return Webhook.create(self.client, r.json()) + + def webhooks_token_modify(self, webhook, token, name=None, avatar=None): + r = self.http(Routes.WEBHOOKS_TOKEN_MODIFY, dict(webhook=webhook, token=token), json=optional( + name=name, + avatar=avatar, + )) + return Webhook.create(self.client, r.json()) + + def webhooks_token_delete(self, webhook, token): + self.http(Routes.WEBHOOKS_TOKEN_DLEETE, dict(webhook=webhook, token=token)) + + def webhooks_token_execute(self, webhook, token, data, wait=False): + self.http( + Routes.WEBHOOKS_TOKEN_EXECUTE, + dict(webhook=webhook, token=token), + json=optional(**data), params={'wait': int(wait)}) diff --git a/disco/api/http.py b/disco/api/http.py index 88dbc50..c1930bd 100644 --- a/disco/api/http.py +++ b/disco/api/http.py @@ -31,22 +31,21 @@ class Routes(object): CHANNELS_GET = (HTTPMethod.GET, CHANNELS) CHANNELS_MODIFY = (HTTPMethod.PATCH, CHANNELS) CHANNELS_DELETE = (HTTPMethod.DELETE, CHANNELS) - CHANNELS_MESSAGES_LIST = (HTTPMethod.GET, CHANNELS + '/messages') CHANNELS_MESSAGES_GET = (HTTPMethod.GET, CHANNELS + '/messages/{message}') CHANNELS_MESSAGES_CREATE = (HTTPMethod.POST, CHANNELS + '/messages') CHANNELS_MESSAGES_MODIFY = (HTTPMethod.PATCH, CHANNELS + '/messages/{message}') CHANNELS_MESSAGES_DELETE = (HTTPMethod.DELETE, CHANNELS + '/messages/{message}') CHANNELS_MESSAGES_DELETE_BULK = (HTTPMethod.POST, CHANNELS + '/messages/bulk_delete') - CHANNELS_PERMISSIONS_MODIFY = (HTTPMethod.PUT, CHANNELS + '/permissions/{permission}') CHANNELS_PERMISSIONS_DELETE = (HTTPMethod.DELETE, CHANNELS + '/permissions/{permission}') CHANNELS_INVITES_LIST = (HTTPMethod.GET, CHANNELS + '/invites') CHANNELS_INVITES_CREATE = (HTTPMethod.POST, CHANNELS + '/invites') - CHANNELS_PINS_LIST = (HTTPMethod.GET, CHANNELS + '/pins') CHANNELS_PINS_CREATE = (HTTPMethod.PUT, CHANNELS + '/pins/{pin}') CHANNELS_PINS_DELETE = (HTTPMethod.DELETE, CHANNELS + '/pins/{pin}') + CHANNELS_WEBHOOKS_CREATE = (HTTPMethod.POST, CHANNELS + '/webhooks') + CHANNELS_WEBHOOKS_LIST = (HTTPMethod.GET, CHANNELS + '/webhooks') # Guilds GUILDS = '/guilds/{guild}' @@ -79,6 +78,7 @@ class Routes(object): GUILDS_INTEGRATIONS_SYNC = (HTTPMethod.POST, GUILDS + '/integrations/{integration}/sync') GUILDS_EMBED_GET = (HTTPMethod.GET, GUILDS + '/embed') GUILDS_EMBED_MODIFY = (HTTPMethod.PATCH, GUILDS + '/embed') + GUILDS_WEBHOOKS_LIST = (HTTPMethod.GET, GUILDS + '/webhooks') # Users USERS = '/users' @@ -96,6 +96,16 @@ class Routes(object): INVITES_GET = (HTTPMethod.GET, INVITES + '/{invite}') INVITES_DELETE = (HTTPMethod.DELETE, INVITES + '/{invite}') + # Webhooks + WEBHOOKS = '/webhooks/{webhook}' + WEBHOOKS_GET = (HTTPMethod.GET, WEBHOOKS) + WEBHOOKS_MODIFY = (HTTPMethod.PATCH, WEBHOOKS) + WEBHOOKS_DELETE = (HTTPMethod.DELETE, WEBHOOKS) + WEBHOOKS_TOKEN_GET = (HTTPMethod.GET, WEBHOOKS + '/{token}') + WEBHOOKS_TOKEN_MODIFY = (HTTPMethod.PATCH, WEBHOOKS + '/{token}') + WEBHOOKS_TOKEN_DELETE = (HTTPMethod.DELETE, WEBHOOKS + '/{token}') + WEBHOOKS_TOKEN_EXECUTE = (HTTPMethod.POST, WEBHOOKS + '/{token}') + class APIException(Exception): """ @@ -201,7 +211,9 @@ class HTTPClient(LoggingClass): raise APIException('Request failed after {} attempts'.format(self.MAX_RETRIES), r.status_code, r.content) backoff = self.random_backoff() - self.log.warning('Request to `{}` failed with code {}, retrying after {}s'.format(url, r.status_code, backoff)) + self.log.warning('Request to `{}` failed with code {}, retrying after {}s ({})'.format( + url, r.status_code, backoff, r.content + )) gevent.sleep(backoff) # Otherwise just recurse and try again diff --git a/disco/gateway/events.py b/disco/gateway/events.py index 5b2e8b5..0928cd5 100644 --- a/disco/gateway/events.py +++ b/disco/gateway/events.py @@ -140,6 +140,7 @@ class GuildDelete(GatewayEvent): unavailable = Field(bool) +@debug() @wraps_model(Channel) class ChannelCreate(GatewayEvent): """ diff --git a/disco/types/base.py b/disco/types/base.py index 87f2aab..63bc628 100644 --- a/disco/types/base.py +++ b/disco/types/base.py @@ -104,7 +104,7 @@ def snowflake(data): def enum(typ): def _f(data): - return typ.get(data) if data else None + return typ.get(data) if data is not None else None return _f diff --git a/disco/types/channel.py b/disco/types/channel.py index 9781ee0..f824a35 100644 --- a/disco/types/channel.py +++ b/disco/types/channel.py @@ -214,6 +214,12 @@ class Channel(SlottedModel, Permissible): def delete_pin(self, message): self.client.api.channels_pins_delete(self.id, to_snowflake(message)) + def get_webhooks(self): + return self.client.api.channels_webhooks_list(self.id) + + def create_webhook(self, name=None, avatar=None): + return self.client.api.channels_webhooks_create(self.id, name, avatar) + def send_message(self, content, nonce=None, tts=False): """ Send a message in this channel diff --git a/disco/types/message.py b/disco/types/message.py index 55ba42b..3e9959f 100644 --- a/disco/types/message.py +++ b/disco/types/message.py @@ -19,6 +19,45 @@ MessageType = Enum( ) +class MessageEmbedFooter(SlottedModel): + text = Field(text) + icon_url = Field(text) + proxy_icon_url = Field(text) + + +class MessageEmbedImage(SlottedModel): + url = Field(text) + proxy_url = Field(text) + width = Field(int) + height = Field(int) + + +class MessageEmbedThumbnail(SlottedModel): + url = Field(text) + proxy_url = Field(text) + width = Field(int) + height = Field(int) + + +class MessageEmbedVideo(SlottedModel): + url = Field(text) + height = Field(int) + width = Field(int) + + +class MessageEmbedAuthor(SlottedModel): + name = Field(text) + url = Field(text) + icon_url = Field(text) + icon_proxy_url = Field(text) + + +class MessageEmbedField(SlottedModel): + name = Field(text) + value = Field(text) + inline = Field(bool) + + class MessageEmbed(SlottedModel): """ Message embed object @@ -35,9 +74,17 @@ class MessageEmbed(SlottedModel): URL of the embed. """ title = Field(text) - type = Field(str) + type = Field(str, default='rich') description = Field(text) url = Field(str) + timestamp = Field(lazy_datetime) + color = Field(int) + footer = Field(MessageEmbedFooter) + image = Field(MessageEmbedImage) + thumbnail = Field(MessageEmbedThumbnail) + video = Field(MessageEmbedVideo) + author = Field(MessageEmbedAuthor) + fields = Field(listof(MessageEmbedField)) class MessageAttachment(SlottedModel): @@ -109,6 +156,7 @@ class Message(SlottedModel): """ id = Field(snowflake) channel_id = Field(snowflake) + webhook_id = Field(snowflake) type = Field(enum(MessageType)) author = Field(User) content = Field(text) diff --git a/disco/types/webhook.py b/disco/types/webhook.py new file mode 100644 index 0000000..da0aadc --- /dev/null +++ b/disco/types/webhook.py @@ -0,0 +1,43 @@ +from disco.types.base import SlottedModel, Field, snowflake +from disco.types.user import User +from disco.util.functional import cached_property + + +class Webhook(SlottedModel): + id = Field(snowflake) + guild_id = Field(snowflake) + channel_id = Field(snowflake) + user = Field(User) + name = Field(str) + avatar = Field(str) + token = Field(str) + + @cached_property + def guild(self): + return self.client.state.guilds.get(self.guild_id) + + @cached_property + def channel(self): + return self.client.state.channels.get(self.channel_id) + + def delete(self): + if self.token: + self.client.api.webhooks_token_delete(self.id, self.token) + else: + self.client.api.webhooks_delete(self.id) + + def modify(self, name, avatar): + if self.token: + return self.client.api.webhooks_token_modify(self.id, self.token, name, avatar) + else: + return self.client.api.webhooks_modify(self.id, name, avatar) + + def execute(self, content=None, username=None, avatar_url=None, tts=False, file=None, embeds=None, wait=False): + return self.client.api.webhooks_token_execute(self.id, self.token, { + 'content': content, + 'username': username, + 'avatar_url': avatar_url, + 'tts': tts, + 'file': file, + 'embeds': [i.to_dict() for i in embeds], + }, wait)