From c4df02782efadbfa152a697d6085191efb54d272 Mon Sep 17 00:00:00 2001 From: Kyra Date: Fri, 24 Aug 2018 16:03:29 +0200 Subject: [PATCH] feat: Guild Integrations (#2760) * feat: Guild Integrations * feat: Guild#createIntegration * feat: Add ws event GUILD_INTEGRATIONS_UPDATE * docs: Add `GUILD_INTEGRATIONS_UPDATE` to WSEventType * misc: Fixed requested change * docs: Updated typings * typings: Sort members by name --- .../packets/WebSocketPacketManager.js | 1 + .../handlers/GuildIntegrationsUpdate.js | 19 +++ src/index.js | 1 + src/structures/Guild.js | 37 +++++ src/structures/Integration.js | 153 ++++++++++++++++++ src/util/Constants.js | 3 + typings/index.d.ts | 39 +++++ 7 files changed, 253 insertions(+) create mode 100644 src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js create mode 100644 src/structures/Integration.js diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js index 06fcae12f..a58871380 100644 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ b/src/client/websocket/packets/WebSocketPacketManager.js @@ -31,6 +31,7 @@ class WebSocketPacketManager { this.register(WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); this.register(WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); this.register(WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); + this.register(WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); this.register(WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); this.register(WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); this.register(WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); diff --git a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js new file mode 100644 index 000000000..5adfb5b0f --- /dev/null +++ b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class GuildIntegrationsHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsHandler; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/src/index.js b/src/index.js index 1c9f068ce..a124108f6 100644 --- a/src/index.js +++ b/src/index.js @@ -68,6 +68,7 @@ module.exports = { GuildChannel: require('./structures/GuildChannel'), GuildEmoji: require('./structures/GuildEmoji'), GuildMember: require('./structures/GuildMember'), + Integration: require('./structures/Integration'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), MessageAttachment: require('./structures/MessageAttachment'), diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a698c1049..d24b3c175 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,4 +1,5 @@ const Invite = require('./Invite'); +const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); @@ -407,6 +408,42 @@ class Guild extends Base { ); } + /** + * Fetches a collection of integrations to this guild. + * Resolves with a collection mapping integrations by their ids. + * @returns {Promise>} + * @example + * // Fetch integrations + * guild.fetchIntegrations() + * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) + * .catch(console.error); + */ + fetchIntegrations() { + return this.client.api.guilds(this.id).integrations.get().then(data => + data.reduce((collection, integration) => + collection.set(integration.id, new Integration(this.client, integration, this)), + new Collection()) + ); + } + + /** + * The data for creating an integration. + * @typedef {Object} IntegrationData + * @property {string} id The integration id + * @property {string} type The integration type + */ + + /** + * Creates an integration by attaching an integration object + * @param {IntegrationData} data The data for thes integration + * @param {string} reason Reason for creating the integration + * @returns {Promise} + */ + createIntegration(data, reason) { + return this.client.api.guilds(this.id).integrations.post({ data, reason }) + .then(() => this); + } + /** * Fetches a collection of invites to this guild. * Resolves with a collection mapping invites by their codes. diff --git a/src/structures/Integration.js b/src/structures/Integration.js new file mode 100644 index 000000000..359df9eb3 --- /dev/null +++ b/src/structures/Integration.js @@ -0,0 +1,153 @@ +const Base = require('./Base'); + +/** + * The information account for an integration + * @typedef {Object} IntegrationAccount + * @property {string} id The id of the account + * @property {string} name The name of the account + */ + +class Integration extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The guild this integration belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The integration id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The integration name + * @type {string} + */ + this.name = data.name; + + /** + * The integration type (twitch, youtube, etc) + * @type {string} + */ + this.type = data.type; + + /** + * Whether this integration is enabled + * @type {boolean} + */ + this.enabled = data.enabled; + + /** + * Whether this integration is syncing + * @type {boolean} + */ + this.syncing = data.syncing; + + /** + * The role that this integration uses for subscribers + * @type {Role} + */ + this.role = this.guild.roles.get(data.role_id); + + /** + * The user for this integration + * @type {User} + */ + this.user = this.client.users.add(data.user); + + /** + * The account integration information + * @type {IntegrationAccount} + */ + this.account = data.account; + + /** + * The last time this integration was last synced + * @type {number} + */ + this.syncedAt = data.synced_at; + this._patch(data); + } + + _patch(data) { + /** + * The behavior of expiring subscribers + * @type {number} + */ + this.expireBehavior = data.expire_behavior; + + /** + * The grace period before expiring subscribers + * @type {number} + */ + this.expireGracePeriod = data.expire_grace_period; + } + + /** + * Sync this integration + * @returns {Promise} + */ + sync() { + this.syncing = true; + return this.client.api.guilds(this.guild.id).integrations(this.id).post() + .then(() => { + this.syncing = false; + this.syncedAt = Date.now(); + return this; + }); + } + + /** + * The data for editing an integration. + * @typedef {Object} IntegrationEditData + * @property {number} [expireBehavior] The new behaviour of expiring subscribers + * @property {number} [expireGracePeriod] The new grace period before expiring subscribers + */ + + /** + * Edits this integration. + * @param {IntegrationEditData} data The data to edit this integration with + * @param {string} reason Reason for editing this integration + * @returns {Promise} + */ + edit(data, reason) { + if ('expireBehavior' in data) { + data.expire_behavior = data.expireBehavior; + data.expireBehavior = null; + } + if ('expireGracePeriod' in data) { + data.expire_grace_period = data.expireGracePeriod; + data.expireGracePeriod = null; + } + // The option enable_emoticons is only available for Twitch at this moment + return this.client.api.guilds(this.guild.id).integrations(this.id).patch({ data, reason }) + .then(() => { + this._patch(data); + return this; + }); + } + + /** + * Deletes this integration. + * @returns {Promise} + * @param {string} [reason] Reason for deleting this integration + */ + delete(reason) { + return this.client.api.guilds(this.guild.id).integrations(this.id).delete({ reason }) + .then(() => this); + } + + toJSON() { + return super.toJSON({ + role: 'roleID', + guild: 'guildID', + user: 'userID', + }); + } +} + +module.exports = Integration; diff --git a/src/util/Constants.js b/src/util/Constants.js index 1bc05ef78..7f5e7429b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -219,6 +219,7 @@ exports.Events = { GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable', GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking', GUILD_MEMBERS_CHUNK: 'guildMembersChunk', + GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', GUILD_ROLE_CREATE: 'roleCreate', GUILD_ROLE_DELETE: 'roleDelete', GUILD_ROLE_UPDATE: 'roleUpdate', @@ -266,6 +267,7 @@ exports.Events = { * * GUILD_MEMBER_REMOVE * * GUILD_MEMBER_UPDATE * * GUILD_MEMBERS_CHUNK + * * GUILD_INTEGRATIONS_UPDATE * * GUILD_ROLE_CREATE * * GUILD_ROLE_DELETE * * GUILD_ROLE_UPDATE @@ -302,6 +304,7 @@ exports.WSEvents = keyMirror([ 'GUILD_MEMBER_REMOVE', 'GUILD_MEMBER_UPDATE', 'GUILD_MEMBERS_CHUNK', + 'GUILD_INTEGRATIONS_UPDATE', 'GUILD_ROLE_CREATE', 'GUILD_ROLE_DELETE', 'GUILD_ROLE_UPDATE', diff --git a/typings/index.d.ts b/typings/index.d.ts index a0d4ec83d..244555d9e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -178,6 +178,7 @@ declare module 'discord.js' { public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; @@ -209,6 +210,7 @@ declare module 'discord.js' { public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; @@ -431,11 +433,13 @@ declare module 'discord.js' { public readonly verified: boolean; public readonly voiceConnection: VoiceConnection; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; + public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; public equals(guild: Guild): boolean; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; + public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; @@ -578,6 +582,25 @@ declare module 'discord.js' { public toString(): string; } + export class Integration extends Base { + constructor(client: Client, data: object, guild: Guild); + public account: IntegrationAccount; + public enabled: boolean; + public expireBehavior: number; + public expireGracePeriod: number; + public guild: Guild; + public id: Snowflake; + public name: string; + public role: Role; + public syncedAt: number; + public syncing: boolean; + public type: number; + public user: User; + public delete(reason?: string): Promise; + public edit(data: IntegrationEditData, reason?: string): Promise; + public sync(): Promise; + } + export class Invite extends Base { constructor(client: Client, data: object); public channel: GuildChannel | GroupDMChannel; @@ -1789,6 +1812,21 @@ declare module 'discord.js' { | 1024 | 2048; + type IntegrationData = { + id: string; + type: string; + }; + + type IntegrationEditData = { + expireBehavior?: number; + expireGracePeriod?: number; + }; + + type IntegrationAccount = { + id: string; + name: string; + }; + type InviteOptions = { temporary?: boolean; maxAge?: number; @@ -2018,6 +2056,7 @@ declare module 'discord.js' { | 'GUILD_BAN_ADD' | 'GUILD_BAN_REMOVE' | 'GUILD_EMOJIS_UPDATE' + | 'GUILD_INTEGRATIONS_UPDATE' | 'CHANNEL_CREATE' | 'CHANNEL_DELETE' | 'CHANNEL_UPDATE'