From 9cd9d307c535236df84ceb927dc481eb6d82d4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Susa=C3=B1a?= <66887817+socram03@users.noreply.github.com> Date: Fri, 8 Jul 2022 18:18:57 -0400 Subject: [PATCH 01/10] USER_UPDATE & TYPING_START (#37) * USER_UPDATE * TYPING_START * fix: fmt --- packages/biscuit/Actions.ts | 26 +- packages/biscuit/Util.ts | 7 +- packages/biscuit/structures/Member.ts | 2 +- packages/biscuit/structures/channels.ts | 1384 +++++++++-------- .../structures/components/ComponentFactory.ts | 2 +- 5 files changed, 719 insertions(+), 702 deletions(-) diff --git a/packages/biscuit/Actions.ts b/packages/biscuit/Actions.ts index 6f3a0fd..7c109c1 100644 --- a/packages/biscuit/Actions.ts +++ b/packages/biscuit/Actions.ts @@ -14,6 +14,7 @@ import type { DiscordIntegration, DiscordIntegrationDelete, DiscordInteraction, + DiscordMemberWithUser, DiscordMessage, DiscordMessageDelete, DiscordMessageReactionAdd, @@ -25,6 +26,7 @@ import type { // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, DiscordThreadListSync, + DiscordTypingStart, DiscordUser, DiscordWebhookUpdate, } from "../discordeno/mod.ts"; @@ -34,11 +36,7 @@ import type { Session } from "./Session.ts"; import type { Channel } from "./structures/channels.ts"; import type { Interaction } from "./structures/interactions/InteractionFactory.ts"; -import { - ChannelFactory, - GuildChannel, - ThreadChannel, -} from "./structures/channels.ts"; +import { ChannelFactory, GuildChannel, ThreadChannel } from "./structures/channels.ts"; import ThreadMember from "./structures/ThreadMember.ts"; import Member from "./structures/Member.ts"; @@ -132,6 +130,18 @@ export const GUILD_ROLE_DELETE: RawHandler = (session, _ session.emit("guildRoleDelete", { guildId: data.guild_id, roleId: data.role_id }); }; +export const TYPING_START: RawHandler = (session, _shardId, payload) => { + session.emit("typingStart", { + channelId: payload.channel_id, + guildId: payload.guild_id ? payload.guild_id : undefined, + userId: payload.user_id, + timestamp: payload.timestamp, + member: payload.guild_id + ? new Member(session, payload.member as DiscordMemberWithUser, payload.guild_id) + : undefined, + }); +}; + export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { session.emit("interactionCreate", InteractionFactory.from(session, interaction)); }; @@ -185,6 +195,10 @@ export const CHANNEL_PINS_UPDATE: RawHandler = (sessio }); }; +export const USER_UPDATE: RawHandler = (session, _shardId, payload) => { + session.emit("userUpdate", new User(session, payload)); +}; + export const WEBHOOKS_UPDATE: RawHandler = (session, _shardId, webhook) => { session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }); }; @@ -269,6 +283,7 @@ export interface Events { "guildRoleCreate": Handler<[{ guildId: Snowflake, role: DiscordRole }]>; "guildRoleUpdate": Handler<[{ guildId: Snowflake, role: DiscordRole }]>; "guildRoleDelete": Handler<[{ guildId: Snowflake, roleId: Snowflake }]>; + "typingStart": Handler<[{channelId: Snowflake, guildId?: Snowflake, userId: Snowflake, timestamp: number, member?: Member}]> "channelCreate": Handler<[Channel]>; "channelUpdate": Handler<[Channel]>; "channelDelete": Handler<[GuildChannel]>; @@ -283,4 +298,5 @@ export interface Events { "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; "raw": Handler<[unknown, number]>; "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; + "userUpdate": Handler<[User]>; } diff --git a/packages/biscuit/Util.ts b/packages/biscuit/Util.ts index f98a292..37c865d 100644 --- a/packages/biscuit/Util.ts +++ b/packages/biscuit/Util.ts @@ -33,17 +33,16 @@ export type ComponentBuilder = /*** * Utility type - * */ + */ export type ComponentEmoji = { id: Snowflake; name: string; animated?: boolean; }; - /** * Utility type - * */ + */ export interface PermissionsOverwrites { id: Snowflake; type: 0 | 1; @@ -63,7 +62,7 @@ export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096; /** * Utility functions - * */ + */ export class Util { static formatImageURL(url: string, size: ImageSize = 128, format?: ImageFormat) { return `${url}.${format || (url.includes("/a_") ? "gif" : "jpg")}?size=${size}`; diff --git a/packages/biscuit/structures/Member.ts b/packages/biscuit/structures/Member.ts index 4612319..3b711e9 100644 --- a/packages/biscuit/structures/Member.ts +++ b/packages/biscuit/structures/Member.ts @@ -1,7 +1,7 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../Snowflake.ts"; import type { Session } from "../Session.ts"; -import type { DiscordMemberWithUser } from "../../discordeno/mod.ts" +import type { DiscordMemberWithUser } from "../../discordeno/mod.ts"; import type { ImageFormat, ImageSize } from "../Util.ts"; import type { CreateGuildBan, ModifyGuildMember } from "./guilds/Guild.ts"; import Util from "../Util.ts"; diff --git a/packages/biscuit/structures/channels.ts b/packages/biscuit/structures/channels.ts index ee4bad8..8c29c20 100644 --- a/packages/biscuit/structures/channels.ts +++ b/packages/biscuit/structures/channels.ts @@ -1,691 +1,693 @@ -/** Types */ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../Snowflake.ts"; -import type { Session } from "../Session.ts"; -import type { PermissionsOverwrites } from "../Util.ts"; - -/** External from vendor */ -import { - DiscordChannel, - VideoQualityModes, - ChannelTypes, - GatewayOpcodes, - DiscordInvite, - DiscordMessage, - DiscordWebhook, - TargetTypes, - DiscordInviteMetadata, - DiscordThreadMember, - DiscordListArchivedThreads -} from "../../discordeno/mod.ts"; - -/** Functions and others */ -import { calculateShardId } from "../../discordeno/gateway/calculateShardId.ts"; -import { urlToBase64 } from "../util/urlToBase64.ts"; - -/** Classes and routes */ -import * as Routes from "../Routes.ts"; -import Message, { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; -import Invite from "./Invite.ts"; -import Webhook from "./Webhook.ts"; -import User from "./User.ts"; -import ThreadMember from "./ThreadMember.ts"; - -export abstract class BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel) { - this.id = data.id; - this.session = session; - this.name = data.name; - this.type = data.type; - } - readonly id: Snowflake; - readonly session: Session; - - name?: string; - type: ChannelTypes; - - isText(): this is TextChannel { - return textBasedChannels.includes(this.type); - } - - isVoice(): this is VoiceChannel { - return this.type === ChannelTypes.GuildVoice; - } - - isDM(): this is DMChannel { - return this.type === ChannelTypes.DM; - } - - isNews(): this is NewsChannel { - return this.type === ChannelTypes.GuildNews; - } - - isThread(): this is ThreadChannel { - return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; - } - - isStage(): this is StageChannel { - return this.type === ChannelTypes.GuildStageVoice; - } - - toString(): string { - return `<#${this.id}>`; - } -} - -/** TextChannel */ -/** - * Represents the options object to create an invitation - * @link https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params - */ - export interface DiscordInviteOptions { - maxAge?: number; - maxUses?: number; - unique?: boolean; - temporary: boolean; - reason?: string; - targetType?: TargetTypes; - targetUserId?: Snowflake; - targetApplicationId?: Snowflake; -} - -export interface CreateWebhook { - name: string; - avatar?: string; - reason?: string; -} - -export const textBasedChannels = [ - ChannelTypes.DM, - ChannelTypes.GroupDm, - ChannelTypes.GuildPrivateThread, - ChannelTypes.GuildPublicThread, - ChannelTypes.GuildNews, - ChannelTypes.GuildVoice, - ChannelTypes.GuildText, -]; - -export type TextBasedChannels = - | ChannelTypes.DM - | ChannelTypes.GroupDm - | ChannelTypes.GuildPrivateThread - | ChannelTypes.GuildPublicThread - | ChannelTypes.GuildNews - | ChannelTypes.GuildVoice - | ChannelTypes.GuildText; - -export class TextChannel { - constructor(session: Session, data: DiscordChannel) { - this.session = session; - this.id = data.id; - this.name = data.name; - this.type = data.type as number; - this.rateLimitPerUser = data.rate_limit_per_user ?? 0; - this.nsfw = !!data.nsfw ?? false; - - if (data.last_message_id) { - this.lastMessageId = data.last_message_id; - } - - if (data.last_pin_timestamp) { - this.lastPinTimestamp = data.last_pin_timestamp; - } - } - - readonly session: Session; - readonly id: Snowflake; - name?: string; - type: TextBasedChannels; - lastMessageId?: Snowflake; - lastPinTimestamp?: string; - rateLimitPerUser: number; - nsfw: boolean; - - /** - * Mixin - */ - // deno-lint-ignore ban-types - static applyTo(klass: Function, ignore: Array = []) { - const methods: Array = [ - "fetchPins", - "createInvite", - "fetchMessages", - "sendTyping", - "pinMessage", - "unpinMessage", - "addReaction", - "removeReaction", - "nukeReactions", - "fetchPins", - "sendMessage", - "editMessage", - "createWebhook", - ]; - - for (const method of methods) { - if (ignore.includes(method)) continue; - - klass.prototype[method] = TextChannel.prototype[method]; - } - } - - async fetchPins(): Promise { - const messages = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.CHANNEL_PINS(this.id), - ); - return messages[0] ? messages.map((x: DiscordMessage) => new Message(this.session, x)) : []; - } - - async createInvite(options?: DiscordInviteOptions) { - const invite = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_INVITES(this.id), - options - ? { - max_age: options.maxAge, - max_uses: options.maxUses, - temporary: options.temporary, - unique: options.unique, - target_type: options.targetType, - target_user_id: options.targetUserId, - target_application_id: options.targetApplicationId, - } - : {}, - ); - - return new Invite(this.session, invite); - } - - async fetchMessages(options?: Routes.GetMessagesOptions): Promise { - if (options?.limit! > 100) throw Error("Values must be between 0-100"); - const messages = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.CHANNEL_MESSAGES(this.id, options), - ); - return messages[0] ? messages.map((x) => new Message(this.session, x)) : []; - } - - async sendTyping() { - await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_TYPING(this.id), - ); - } - - async pinMessage(messageId: Snowflake) { - await Message.prototype.pin.call({ id: messageId, channelId: this.id, session: this.session }); - } - - async unpinMessage(messageId: Snowflake) { - await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session }); - } - - async addReaction(messageId: Snowflake, reaction: ReactionResolvable) { - await Message.prototype.addReaction.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - ); - } - - async removeReaction(messageId: Snowflake, reaction: ReactionResolvable, options?: { userId: Snowflake }) { - await Message.prototype.removeReaction.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - options, - ); - } - - async removeReactionEmoji(messageId: Snowflake, reaction: ReactionResolvable) { - await Message.prototype.removeReactionEmoji.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - ); - } - - async nukeReactions(messageId: Snowflake) { - await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId }); - } - - async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: Routes.GetReactions) { - const users = await Message.prototype.fetchReactions.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - options, - ); - - return users; - } - - sendMessage(options: CreateMessage) { - return Message.prototype.reply.call({ channelId: this.id, session: this.session }, options); - } - - editMessage(messageId: Snowflake, options: EditMessage) { - return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options); - } - - async createWebhook(options: CreateWebhook) { - const webhook = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_WEBHOOKS(this.id), - { - name: options.name, - avatar: options.avatar ? urlToBase64(options.avatar) : undefined, - reason: options.reason, - }, - ); - - return new Webhook(this.session, webhook); - } -} - -/** GuildChannel */ -/** - * Represent the options object to create a thread channel - * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message - */ - export interface ThreadCreateOptions { - name: string; - autoArchiveDuration?: 60 | 1440 | 4320 | 10080; - type: 10 | 11 | 12; - invitable?: boolean; - rateLimitPerUser?: number; - reason?: string; -} - -/** - * Representations of the objects to edit a guild channel - * @link https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel - */ -export interface EditGuildChannelOptions { - name?: string; - position?: number; - permissionOverwrites?: PermissionsOverwrites[]; -} - -export interface EditNewsChannelOptions extends EditGuildChannelOptions { - type?: ChannelTypes.GuildNews | ChannelTypes.GuildText; - topic?: string | null; - nfsw?: boolean | null; - parentId?: Snowflake | null; - defaultAutoArchiveDuration?: number | null; -} - -export interface EditGuildTextChannelOptions extends EditNewsChannelOptions { - rateLimitPerUser?: number | null; -} - -export interface EditStageChannelOptions extends EditGuildChannelOptions { - bitrate?: number | null; - rtcRegion?: Snowflake | null; -} - -export interface EditVoiceChannelOptions extends EditStageChannelOptions { - nsfw?: boolean | null; - userLimit?: number | null; - parentId?: Snowflake | null; - videoQualityMode?: VideoQualityModes | null; -} - -/** - * Represents the option object to create a thread channel from a message - * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message - */ -export interface ThreadCreateOptions { - name: string; - autoArchiveDuration?: 60 | 1440 | 4320 | 10080; - rateLimitPerUser?: number; - messageId: Snowflake; -} - -export class GuildChannel extends BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data); - this.type = data.type as number; - this.guildId = guildId; - this.position = data.position; - data.topic ? this.topic = data.topic : null; - data.parent_id ? this.parentId = data.parent_id : undefined; - } - - override type: Exclude; - guildId: Snowflake; - topic?: string; - position?: number; - parentId?: Snowflake; - - async fetchInvites(): Promise { - const invites = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.CHANNEL_INVITES(this.id), - ); - - return invites.map((invite) => new Invite(this.session, invite)); - } - - async edit(options: EditNewsChannelOptions): Promise; - async edit(options: EditStageChannelOptions): Promise; - async edit(options: EditVoiceChannelOptions): Promise; - async edit( - options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions, - ): Promise { - const channel = await this.session.rest.runMethod( - this.session.rest, - "PATCH", - Routes.CHANNEL(this.id), - { - name: options.name, - type: "type" in options ? options.type : undefined, - position: options.position, - topic: "topic" in options ? options.topic : undefined, - nsfw: "nfsw" in options ? options.nfsw : undefined, - rate_limit_per_user: "rateLimitPerUser" in options ? options.rateLimitPerUser : undefined, - bitrate: "bitrate" in options ? options.bitrate : undefined, - user_limit: "userLimit" in options ? options.userLimit : undefined, - permissions_overwrites: options.permissionOverwrites, - parent_id: "parentId" in options ? options.parentId : undefined, - rtc_region: "rtcRegion" in options ? options.rtcRegion : undefined, - video_quality_mode: "videoQualityMode" in options ? options.videoQualityMode : undefined, - default_auto_archive_duration: "defaultAutoArchiveDuration" in options - ? options.defaultAutoArchiveDuration - : undefined, - }, - ); - return ChannelFactory.from(this.session, channel); - } - - async getArchivedThreads(options: Routes.ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { - let func: (channelId: Snowflake, options: Routes.ListArchivedThreads) => string; - - switch (options.type) { - case "public": - func = Routes.THREAD_ARCHIVED_PUBLIC; - break; - case "private": - func = Routes.THREAD_START_PRIVATE; - break; - case "privateJoinedThreads": - func = Routes.THREAD_ARCHIVED_PRIVATE_JOINED; - break; - } - - const { threads, members, has_more } = await this.session.rest.runMethod( - this.session.rest, - "GET", - func(this.id, options), - ); - - return { - threads: Object.fromEntries( - threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), - ) as Record, - members: Object.fromEntries( - members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]), - ) as Record, - hasMore: has_more, - }; - } - - async createThread(options: ThreadCreateOptions): Promise { - const thread = await this.session.rest.runMethod( - this.session.rest, - "POST", - "messageId" in options - ? Routes.THREAD_START_PUBLIC(this.id, options.messageId) - : Routes.THREAD_START_PRIVATE(this.id), - { - name: options.name, - auto_archive_duration: options.autoArchiveDuration, - }, - ); - - return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); - } -} - -/** BaseVoiceChannel */ -/** - * @link https://discord.com/developers/docs/topics/gateway#update-voice-state - */ - export interface UpdateVoiceState { - guildId: string; - channelId?: string; - selfMute: boolean; - selfDeaf: boolean; -} - -export abstract class BaseVoiceChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.bitRate = data.bitrate; - this.userLimit = data.user_limit ?? 0; - this.videoQuality = data.video_quality_mode; - this.nsfw = !!data.nsfw; - this.type = data.type as number; - - if (data.rtc_region) { - this.rtcRegion = data.rtc_region; - } - } - override type: ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice; - bitRate?: number; - userLimit: number; - rtcRegion?: Snowflake; - - videoQuality?: VideoQualityModes; - nsfw: boolean; - - /** - * This function was gathered from Discordeno it may not work - */ - async connect(options?: UpdateVoiceState) { - const shardId = calculateShardId(this.session.gateway, BigInt(super.guildId)); - const shard = this.session.gateway.manager.shards.get(shardId); - - if (!shard) { - throw new Error(`Shard (id: ${shardId} not found`); - } - - await shard.send({ - op: GatewayOpcodes.VoiceStateUpdate, - d: { - guild_id: super.guildId, - channel_id: super.id, - self_mute: Boolean(options?.selfMute), - self_deaf: options?.selfDeaf ?? true, - }, - }); - } -} - -/** DMChannel */ -export class DMChannel extends BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel) { - super(session, data); - this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!); - this.type = data.type as ChannelTypes.DM | ChannelTypes.GroupDm; - if (data.last_message_id) { - this.lastMessageId = data.last_message_id; - } - } - - override type: ChannelTypes.DM | ChannelTypes.GroupDm; - user: User; - lastMessageId?: Snowflake; - - async close() { - const channel = await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.CHANNEL(this.id), - ); - - return new DMChannel(this.session, channel); - } -} - -TextChannel.applyTo(DMChannel); - -export interface DMChannel extends Omit, Omit {} - -/** VoiceChannel */ -export class VoiceChannel extends BaseVoiceChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - } - override type: ChannelTypes.GuildVoice; -} - -export interface VoiceChannel extends TextChannel, BaseVoiceChannel {} - -TextChannel.applyTo(VoiceChannel); - -/** NewsChannel */ -export class NewsChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as ChannelTypes.GuildNews; - this.defaultAutoArchiveDuration = data.default_auto_archive_duration; - } - - override type: ChannelTypes.GuildNews; - defaultAutoArchiveDuration?: number; - - crosspostMessage(messageId: Snowflake): Promise { - return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session }); - } - - get publishMessage() { - return this.crosspostMessage; - } -} - -TextChannel.applyTo(NewsChannel); - -export interface NewsChannel extends TextChannel, GuildChannel {} - -/** StageChannel */ -export class StageChannel extends BaseVoiceChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - this.topic = data.topic ? data.topic : undefined; - } - override type: ChannelTypes.GuildStageVoice; - topic?: string; -} - -/** ThreadChannel */ -export class ThreadChannel extends GuildChannel implements Model { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - this.archived = !!data.thread_metadata?.archived; - this.archiveTimestamp = data.thread_metadata?.archive_timestamp; - this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration; - this.locked = !!data.thread_metadata?.locked; - this.messageCount = data.message_count; - this.memberCount = data.member_count; - this.ownerId = data.owner_id; - - if (data.member) { - this.member = new ThreadMember(session, data.member); - } - } - - override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; - archived?: boolean; - archiveTimestamp?: string; - autoArchiveDuration?: number; - locked?: boolean; - messageCount?: number; - memberCount?: number; - member?: ThreadMember; - ownerId?: Snowflake; - - async joinThread() { - await this.session.rest.runMethod( - this.session.rest, - "PUT", - Routes.THREAD_ME(this.id), - ); - } - - async addToThread(guildMemberId: Snowflake) { - await this.session.rest.runMethod( - this.session.rest, - "PUT", - Routes.THREAD_USER(this.id, guildMemberId), - ); - } - - async leaveToThread(guildMemberId: Snowflake) { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.THREAD_USER(this.id, guildMemberId), - ); - } - - removeMember(memberId: Snowflake = this.session.botId) { - return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId); - } - - fetchMember(memberId: Snowflake = this.session.botId) { - return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId); - } - - async fetchMembers(): Promise { - const members = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.THREAD_MEMBERS(this.id), - ); - - return members.map((threadMember) => new ThreadMember(this.session, threadMember)); - } -} - -TextChannel.applyTo(ThreadChannel); - -export interface ThreadChannel extends Omit, Omit {} - -/** ChannelFactory */ -export type Channel = - | TextChannel - | VoiceChannel - | DMChannel - | NewsChannel - | ThreadChannel - | StageChannel; - -export class ChannelFactory { - static from(session: Session, channel: DiscordChannel): Channel { - switch (channel.type) { - case ChannelTypes.GuildPublicThread: - case ChannelTypes.GuildPrivateThread: - return new ThreadChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildNews: - return new NewsChannel(session, channel, channel.guild_id!); - case ChannelTypes.DM: - return new DMChannel(session, channel); - case ChannelTypes.GuildVoice: - return new VoiceChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildStageVoice: - return new StageChannel(session, channel, channel.guild_id!); - default: - if (textBasedChannels.includes(channel.type)) { - return new TextChannel(session, channel); - } - throw new Error("Channel was not implemented"); - } - } -} +/** Types */ +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../Snowflake.ts"; +import type { Session } from "../Session.ts"; +import type { PermissionsOverwrites } from "../Util.ts"; + +/** External from vendor */ +import { + ChannelTypes, + DiscordChannel, + DiscordInvite, + DiscordInviteMetadata, + DiscordListArchivedThreads, + DiscordMessage, + DiscordThreadMember, + DiscordWebhook, + GatewayOpcodes, + TargetTypes, + VideoQualityModes, +} from "../../discordeno/mod.ts"; + +/** Functions and others */ +import { calculateShardId } from "../../discordeno/gateway/calculateShardId.ts"; +import { urlToBase64 } from "../util/urlToBase64.ts"; + +/** Classes and routes */ +import * as Routes from "../Routes.ts"; +import Message, { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; +import Invite from "./Invite.ts"; +import Webhook from "./Webhook.ts"; +import User from "./User.ts"; +import ThreadMember from "./ThreadMember.ts"; + +export abstract class BaseChannel implements Model { + constructor(session: Session, data: DiscordChannel) { + this.id = data.id; + this.session = session; + this.name = data.name; + this.type = data.type; + } + readonly id: Snowflake; + readonly session: Session; + + name?: string; + type: ChannelTypes; + + isText(): this is TextChannel { + return textBasedChannels.includes(this.type); + } + + isVoice(): this is VoiceChannel { + return this.type === ChannelTypes.GuildVoice; + } + + isDM(): this is DMChannel { + return this.type === ChannelTypes.DM; + } + + isNews(): this is NewsChannel { + return this.type === ChannelTypes.GuildNews; + } + + isThread(): this is ThreadChannel { + return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; + } + + isStage(): this is StageChannel { + return this.type === ChannelTypes.GuildStageVoice; + } + + toString(): string { + return `<#${this.id}>`; + } +} + +/** TextChannel */ +/** + * Represents the options object to create an invitation + * @link https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params + */ +export interface DiscordInviteOptions { + maxAge?: number; + maxUses?: number; + unique?: boolean; + temporary: boolean; + reason?: string; + targetType?: TargetTypes; + targetUserId?: Snowflake; + targetApplicationId?: Snowflake; +} + +export interface CreateWebhook { + name: string; + avatar?: string; + reason?: string; +} + +export const textBasedChannels = [ + ChannelTypes.DM, + ChannelTypes.GroupDm, + ChannelTypes.GuildPrivateThread, + ChannelTypes.GuildPublicThread, + ChannelTypes.GuildNews, + ChannelTypes.GuildVoice, + ChannelTypes.GuildText, +]; + +export type TextBasedChannels = + | ChannelTypes.DM + | ChannelTypes.GroupDm + | ChannelTypes.GuildPrivateThread + | ChannelTypes.GuildPublicThread + | ChannelTypes.GuildNews + | ChannelTypes.GuildVoice + | ChannelTypes.GuildText; + +export class TextChannel { + constructor(session: Session, data: DiscordChannel) { + this.session = session; + this.id = data.id; + this.name = data.name; + this.type = data.type as number; + this.rateLimitPerUser = data.rate_limit_per_user ?? 0; + this.nsfw = !!data.nsfw ?? false; + + if (data.last_message_id) { + this.lastMessageId = data.last_message_id; + } + + if (data.last_pin_timestamp) { + this.lastPinTimestamp = data.last_pin_timestamp; + } + } + + readonly session: Session; + readonly id: Snowflake; + name?: string; + type: TextBasedChannels; + lastMessageId?: Snowflake; + lastPinTimestamp?: string; + rateLimitPerUser: number; + nsfw: boolean; + + /** + * Mixin + */ + // deno-lint-ignore ban-types + static applyTo(klass: Function, ignore: Array = []) { + const methods: Array = [ + "fetchPins", + "createInvite", + "fetchMessages", + "sendTyping", + "pinMessage", + "unpinMessage", + "addReaction", + "removeReaction", + "nukeReactions", + "fetchPins", + "sendMessage", + "editMessage", + "createWebhook", + ]; + + for (const method of methods) { + if (ignore.includes(method)) continue; + + klass.prototype[method] = TextChannel.prototype[method]; + } + } + + async fetchPins(): Promise { + const messages = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_PINS(this.id), + ); + return messages[0] ? messages.map((x: DiscordMessage) => new Message(this.session, x)) : []; + } + + async createInvite(options?: DiscordInviteOptions) { + const invite = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_INVITES(this.id), + options + ? { + max_age: options.maxAge, + max_uses: options.maxUses, + temporary: options.temporary, + unique: options.unique, + target_type: options.targetType, + target_user_id: options.targetUserId, + target_application_id: options.targetApplicationId, + } + : {}, + ); + + return new Invite(this.session, invite); + } + + async fetchMessages(options?: Routes.GetMessagesOptions): Promise { + if (options?.limit! > 100) throw Error("Values must be between 0-100"); + const messages = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_MESSAGES(this.id, options), + ); + return messages[0] ? messages.map((x) => new Message(this.session, x)) : []; + } + + async sendTyping() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_TYPING(this.id), + ); + } + + async pinMessage(messageId: Snowflake) { + await Message.prototype.pin.call({ id: messageId, channelId: this.id, session: this.session }); + } + + async unpinMessage(messageId: Snowflake) { + await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session }); + } + + async addReaction(messageId: Snowflake, reaction: ReactionResolvable) { + await Message.prototype.addReaction.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + ); + } + + async removeReaction(messageId: Snowflake, reaction: ReactionResolvable, options?: { userId: Snowflake }) { + await Message.prototype.removeReaction.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + options, + ); + } + + async removeReactionEmoji(messageId: Snowflake, reaction: ReactionResolvable) { + await Message.prototype.removeReactionEmoji.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + ); + } + + async nukeReactions(messageId: Snowflake) { + await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId }); + } + + async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: Routes.GetReactions) { + const users = await Message.prototype.fetchReactions.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + options, + ); + + return users; + } + + sendMessage(options: CreateMessage) { + return Message.prototype.reply.call({ channelId: this.id, session: this.session }, options); + } + + editMessage(messageId: Snowflake, options: EditMessage) { + return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options); + } + + async createWebhook(options: CreateWebhook) { + const webhook = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_WEBHOOKS(this.id), + { + name: options.name, + avatar: options.avatar ? urlToBase64(options.avatar) : undefined, + reason: options.reason, + }, + ); + + return new Webhook(this.session, webhook); + } +} + +/** GuildChannel */ +/** + * Represent the options object to create a thread channel + * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message + */ +export interface ThreadCreateOptions { + name: string; + autoArchiveDuration?: 60 | 1440 | 4320 | 10080; + type: 10 | 11 | 12; + invitable?: boolean; + rateLimitPerUser?: number; + reason?: string; +} + +/** + * Representations of the objects to edit a guild channel + * @link https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel + */ +export interface EditGuildChannelOptions { + name?: string; + position?: number; + permissionOverwrites?: PermissionsOverwrites[]; +} + +export interface EditNewsChannelOptions extends EditGuildChannelOptions { + type?: ChannelTypes.GuildNews | ChannelTypes.GuildText; + topic?: string | null; + nfsw?: boolean | null; + parentId?: Snowflake | null; + defaultAutoArchiveDuration?: number | null; +} + +export interface EditGuildTextChannelOptions extends EditNewsChannelOptions { + rateLimitPerUser?: number | null; +} + +export interface EditStageChannelOptions extends EditGuildChannelOptions { + bitrate?: number | null; + rtcRegion?: Snowflake | null; +} + +export interface EditVoiceChannelOptions extends EditStageChannelOptions { + nsfw?: boolean | null; + userLimit?: number | null; + parentId?: Snowflake | null; + videoQualityMode?: VideoQualityModes | null; +} + +/** + * Represents the option object to create a thread channel from a message + * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message + */ +export interface ThreadCreateOptions { + name: string; + autoArchiveDuration?: 60 | 1440 | 4320 | 10080; + rateLimitPerUser?: number; + messageId: Snowflake; +} + +export class GuildChannel extends BaseChannel implements Model { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data); + this.type = data.type as number; + this.guildId = guildId; + this.position = data.position; + data.topic ? this.topic = data.topic : null; + data.parent_id ? this.parentId = data.parent_id : undefined; + } + + override type: Exclude; + guildId: Snowflake; + topic?: string; + position?: number; + parentId?: Snowflake; + + async fetchInvites(): Promise { + const invites = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_INVITES(this.id), + ); + + return invites.map((invite) => new Invite(this.session, invite)); + } + + async edit(options: EditNewsChannelOptions): Promise; + async edit(options: EditStageChannelOptions): Promise; + async edit(options: EditVoiceChannelOptions): Promise; + async edit( + options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions, + ): Promise { + const channel = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.CHANNEL(this.id), + { + name: options.name, + type: "type" in options ? options.type : undefined, + position: options.position, + topic: "topic" in options ? options.topic : undefined, + nsfw: "nfsw" in options ? options.nfsw : undefined, + rate_limit_per_user: "rateLimitPerUser" in options ? options.rateLimitPerUser : undefined, + bitrate: "bitrate" in options ? options.bitrate : undefined, + user_limit: "userLimit" in options ? options.userLimit : undefined, + permissions_overwrites: options.permissionOverwrites, + parent_id: "parentId" in options ? options.parentId : undefined, + rtc_region: "rtcRegion" in options ? options.rtcRegion : undefined, + video_quality_mode: "videoQualityMode" in options ? options.videoQualityMode : undefined, + default_auto_archive_duration: "defaultAutoArchiveDuration" in options + ? options.defaultAutoArchiveDuration + : undefined, + }, + ); + return ChannelFactory.from(this.session, channel); + } + + async getArchivedThreads( + options: Routes.ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }, + ) { + let func: (channelId: Snowflake, options: Routes.ListArchivedThreads) => string; + + switch (options.type) { + case "public": + func = Routes.THREAD_ARCHIVED_PUBLIC; + break; + case "private": + func = Routes.THREAD_START_PRIVATE; + break; + case "privateJoinedThreads": + func = Routes.THREAD_ARCHIVED_PRIVATE_JOINED; + break; + } + + const { threads, members, has_more } = await this.session.rest.runMethod( + this.session.rest, + "GET", + func(this.id, options), + ); + + return { + threads: Object.fromEntries( + threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), + ) as Record, + members: Object.fromEntries( + members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]), + ) as Record, + hasMore: has_more, + }; + } + + async createThread(options: ThreadCreateOptions): Promise { + const thread = await this.session.rest.runMethod( + this.session.rest, + "POST", + "messageId" in options + ? Routes.THREAD_START_PUBLIC(this.id, options.messageId) + : Routes.THREAD_START_PRIVATE(this.id), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + }, + ); + + return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); + } +} + +/** BaseVoiceChannel */ +/** + * @link https://discord.com/developers/docs/topics/gateway#update-voice-state + */ +export interface UpdateVoiceState { + guildId: string; + channelId?: string; + selfMute: boolean; + selfDeaf: boolean; +} + +export abstract class BaseVoiceChannel extends GuildChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.bitRate = data.bitrate; + this.userLimit = data.user_limit ?? 0; + this.videoQuality = data.video_quality_mode; + this.nsfw = !!data.nsfw; + this.type = data.type as number; + + if (data.rtc_region) { + this.rtcRegion = data.rtc_region; + } + } + override type: ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice; + bitRate?: number; + userLimit: number; + rtcRegion?: Snowflake; + + videoQuality?: VideoQualityModes; + nsfw: boolean; + + /** + * This function was gathered from Discordeno it may not work + */ + async connect(options?: UpdateVoiceState) { + const shardId = calculateShardId(this.session.gateway, BigInt(super.guildId)); + const shard = this.session.gateway.manager.shards.get(shardId); + + if (!shard) { + throw new Error(`Shard (id: ${shardId} not found`); + } + + await shard.send({ + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id: super.guildId, + channel_id: super.id, + self_mute: Boolean(options?.selfMute), + self_deaf: options?.selfDeaf ?? true, + }, + }); + } +} + +/** DMChannel */ +export class DMChannel extends BaseChannel implements Model { + constructor(session: Session, data: DiscordChannel) { + super(session, data); + this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!); + this.type = data.type as ChannelTypes.DM | ChannelTypes.GroupDm; + if (data.last_message_id) { + this.lastMessageId = data.last_message_id; + } + } + + override type: ChannelTypes.DM | ChannelTypes.GroupDm; + user: User; + lastMessageId?: Snowflake; + + async close() { + const channel = await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL(this.id), + ); + + return new DMChannel(this.session, channel); + } +} + +TextChannel.applyTo(DMChannel); + +export interface DMChannel extends Omit, Omit {} + +/** VoiceChannel */ +export class VoiceChannel extends BaseVoiceChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + } + override type: ChannelTypes.GuildVoice; +} + +export interface VoiceChannel extends TextChannel, BaseVoiceChannel {} + +TextChannel.applyTo(VoiceChannel); + +/** NewsChannel */ +export class NewsChannel extends GuildChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as ChannelTypes.GuildNews; + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } + + override type: ChannelTypes.GuildNews; + defaultAutoArchiveDuration?: number; + + crosspostMessage(messageId: Snowflake): Promise { + return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session }); + } + + get publishMessage() { + return this.crosspostMessage; + } +} + +TextChannel.applyTo(NewsChannel); + +export interface NewsChannel extends TextChannel, GuildChannel {} + +/** StageChannel */ +export class StageChannel extends BaseVoiceChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + this.topic = data.topic ? data.topic : undefined; + } + override type: ChannelTypes.GuildStageVoice; + topic?: string; +} + +/** ThreadChannel */ +export class ThreadChannel extends GuildChannel implements Model { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + this.archived = !!data.thread_metadata?.archived; + this.archiveTimestamp = data.thread_metadata?.archive_timestamp; + this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration; + this.locked = !!data.thread_metadata?.locked; + this.messageCount = data.message_count; + this.memberCount = data.member_count; + this.ownerId = data.owner_id; + + if (data.member) { + this.member = new ThreadMember(session, data.member); + } + } + + override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; + archived?: boolean; + archiveTimestamp?: string; + autoArchiveDuration?: number; + locked?: boolean; + messageCount?: number; + memberCount?: number; + member?: ThreadMember; + ownerId?: Snowflake; + + async joinThread() { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.THREAD_ME(this.id), + ); + } + + async addToThread(guildMemberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.THREAD_USER(this.id, guildMemberId), + ); + } + + async leaveToThread(guildMemberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.THREAD_USER(this.id, guildMemberId), + ); + } + + removeMember(memberId: Snowflake = this.session.botId) { + return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId); + } + + fetchMember(memberId: Snowflake = this.session.botId) { + return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId); + } + + async fetchMembers(): Promise { + const members = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_MEMBERS(this.id), + ); + + return members.map((threadMember) => new ThreadMember(this.session, threadMember)); + } +} + +TextChannel.applyTo(ThreadChannel); + +export interface ThreadChannel extends Omit, Omit {} + +/** ChannelFactory */ +export type Channel = + | TextChannel + | VoiceChannel + | DMChannel + | NewsChannel + | ThreadChannel + | StageChannel; + +export class ChannelFactory { + static from(session: Session, channel: DiscordChannel): Channel { + switch (channel.type) { + case ChannelTypes.GuildPublicThread: + case ChannelTypes.GuildPrivateThread: + return new ThreadChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildNews: + return new NewsChannel(session, channel, channel.guild_id!); + case ChannelTypes.DM: + return new DMChannel(session, channel); + case ChannelTypes.GuildVoice: + return new VoiceChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildStageVoice: + return new StageChannel(session, channel, channel.guild_id!); + default: + if (textBasedChannels.includes(channel.type)) { + return new TextChannel(session, channel); + } + throw new Error("Channel was not implemented"); + } + } +} diff --git a/packages/biscuit/structures/components/ComponentFactory.ts b/packages/biscuit/structures/components/ComponentFactory.ts index 9b4c51e..395f453 100644 --- a/packages/biscuit/structures/components/ComponentFactory.ts +++ b/packages/biscuit/structures/components/ComponentFactory.ts @@ -1,5 +1,5 @@ import type { Session } from "../../Session.ts"; -import type { DiscordComponent } from "../../../discordeno/mod.ts" +import type { DiscordComponent } from "../../../discordeno/mod.ts"; import type { Component } from "./Component.ts"; import { ButtonStyles, MessageComponentTypes } from "../../../discordeno/mod.ts"; import ActionRow from "./ActionRowComponent.ts"; From 77929f8e6ba99c560c604c706d8987e1bde30835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Susa=C3=B1a?= <66887817+socram03@users.noreply.github.com> Date: Fri, 8 Jul 2022 20:19:14 -0400 Subject: [PATCH 02/10] Auto Moderation Rules & Execution (#38) * structure * 'rules' events --- packages/biscuit/Actions.ts | 32 ++++++++- .../structures/AutoModerationExecution.ts | 51 +++++++++++++++ .../biscuit/structures/AutoModerationRule.ts | 65 +++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 packages/biscuit/structures/AutoModerationExecution.ts create mode 100644 packages/biscuit/structures/AutoModerationRule.ts diff --git a/packages/biscuit/Actions.ts b/packages/biscuit/Actions.ts index 7c109c1..3ce6bea 100644 --- a/packages/biscuit/Actions.ts +++ b/packages/biscuit/Actions.ts @@ -1,4 +1,6 @@ import type { + DiscordAutoModerationActionExecution, + DiscordAutoModerationRule, DiscordChannel, DiscordChannelPinsUpdate, DiscordEmoji, @@ -33,10 +35,12 @@ import type { import type { Snowflake } from "./Snowflake.ts"; import type { Session } from "./Session.ts"; -import type { Channel } from "./structures/channels.ts"; import type { Interaction } from "./structures/interactions/InteractionFactory.ts"; -import { ChannelFactory, GuildChannel, ThreadChannel } from "./structures/channels.ts"; + +import { AutoModerationRule } from "./structures/AutoModerationRule.ts"; +import { AutoModerationExecution } from "./structures/AutoModerationExecution.ts"; +import { type Channel, ChannelFactory, GuildChannel, ThreadChannel } from "./structures/channels.ts"; import ThreadMember from "./structures/ThreadMember.ts"; import Member from "./structures/Member.ts"; @@ -227,6 +231,26 @@ export const INTEGRATION_DELETE: RawHandler = (session }); }; +export const AUTO_MODERATION_RULE_CREATE: RawHandler = (session, _shardId, payload) => { + session.emit("autoModerationRuleCreate", new AutoModerationRule(session, payload)); +}; + +export const AUTO_MODERATION_RULE_UPDATE: RawHandler = (session, _shardId, payload) => { + session.emit("autoModerationRuleUpdate", new AutoModerationRule(session, payload)); +}; + +export const AUTO_MODERATION_RULE_DELETE: RawHandler = (session, _shardId, payload) => { + session.emit("autoModerationRuleDelete", new AutoModerationRule(session, payload)); +}; + +export const AUTO_MODERATION_ACTION_EXECUTE: RawHandler = ( + session, + _shardId, + payload, +) => { + session.emit("autoModerationActionExecution", new AutoModerationExecution(session, payload)); +}; + export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { session.emit("messageReactionAdd", null); }; @@ -296,6 +320,10 @@ export interface Events { "integrationCreate": Handler<[Integration]>; "integrationUpdate": Handler<[Integration]>; "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; + "autoModerationRuleCreate": Handler<[AutoModerationRule]>; + "autoModerationRuleUpdate": Handler<[AutoModerationRule]>; + "autoModerationRuleDelete": Handler<[AutoModerationRule]>; + "autoModerationActionExecution":Handler<[AutoModerationExecution]> "raw": Handler<[unknown, number]>; "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; "userUpdate": Handler<[User]>; diff --git a/packages/biscuit/structures/AutoModerationExecution.ts b/packages/biscuit/structures/AutoModerationExecution.ts new file mode 100644 index 0000000..6e2c7ab --- /dev/null +++ b/packages/biscuit/structures/AutoModerationExecution.ts @@ -0,0 +1,51 @@ +import { AutoModerationTriggerTypes, DiscordAutoModerationActionExecution } from "../../discordeno/mod.ts"; +import type { Session } from "../Session.ts"; +import type { Snowflake } from "../Snowflake.ts"; +import { AutoModerationAction } from "./AutoModerationRule.ts"; + +export class AutoModerationExecution { + constructor(session: Session, data: DiscordAutoModerationActionExecution) { + this.session = session; + this.guildId = data.guild_id; + this.action = Object.create({ + type: data.action.type, + metadata: { + channelId: data.action.metadata.channel_id, + durationSeconds: data.action.metadata.duration_seconds, + }, + }); + this.ruleId = data.rule_id; + this.ruleTriggerType = data.rule_trigger_type; + this.userId = data.user_id; + this.content = data.content; + if (data.channel_id) { + this.channelId = data.channel_id; + } + if (data.message_id) { + this.messageId = data.message_id; + } + if (data.alert_system_message_id) { + this.alertSystemMessageId = data.alert_system_message_id; + } + + if (data.matched_keyword) { + this.matchedKeyword = data.matched_keyword; + } + + if (data.matched_content) { + this.matched_content = data.matched_content; + } + } + session: Session; + guildId: Snowflake; + action: AutoModerationAction; + ruleId: Snowflake; + ruleTriggerType: AutoModerationTriggerTypes; + userId: Snowflake; + channelId?: Snowflake; + messageId?: Snowflake; + alertSystemMessageId?: Snowflake; + content?: string; + matchedKeyword?: string; + matched_content?: string; +} diff --git a/packages/biscuit/structures/AutoModerationRule.ts b/packages/biscuit/structures/AutoModerationRule.ts new file mode 100644 index 0000000..a221d2a --- /dev/null +++ b/packages/biscuit/structures/AutoModerationRule.ts @@ -0,0 +1,65 @@ +import { + AutoModerationActionType, + AutoModerationEventTypes, + AutoModerationTriggerTypes, + DiscordAutoModerationRule, + DiscordAutoModerationRuleTriggerMetadataPresets, +} from "../../discordeno/mod.ts"; +import { Model } from "./Base.ts"; +import type { Session } from "../Session.ts"; +import type { Snowflake } from "../Snowflake.ts"; + +export interface AutoModerationRuleTriggerMetadata { + keywordFilter?: string[]; + presets?: DiscordAutoModerationRuleTriggerMetadataPresets[]; +} + +export interface ActionMetadata { + channelId: Snowflake; + durationSeconds: number; +} + +export interface AutoModerationAction { + type: AutoModerationActionType; + metadata: ActionMetadata; +} + +export class AutoModerationRule implements Model { + constructor(session: Session, data: DiscordAutoModerationRule) { + this.session = session; + this.id = data.id; + this.guildId = data.guild_id; + this.name = data.name; + this.creatorId = data.creator_id; + this.eventType = data.event_type; + this.triggerType = data.trigger_type; + this.triggerMetadata = { + keywordFilter: data.trigger_metadata.keyword_filter, + presets: data.trigger_metadata.presets, + }; + this.actions = data.actions.map((action) => + Object.create({ + type: action.type, + metadata: { + channelId: action.metadata.channel_id, + durationSeconds: action.metadata.duration_seconds, + }, + }) + ); + this.enabled = !!data.enabled; + this.exemptRoles = data.exempt_roles; + this.exemptChannels = data.exempt_channels; + } + session: Session; + id: Snowflake; + guildId: Snowflake; + name: string; + creatorId: Snowflake; + eventType: AutoModerationEventTypes; + triggerType: AutoModerationTriggerTypes; + triggerMetadata: AutoModerationRuleTriggerMetadata; + actions: AutoModerationAction[]; + enabled: boolean; + exemptRoles: Snowflake[]; + exemptChannels: Snowflake[]; +} From d060e7cdd6a93cdb61b3b13f732027e8a88f7451 Mon Sep 17 00:00:00 2001 From: socram03 Date: Fri, 8 Jul 2022 20:51:34 -0400 Subject: [PATCH 03/10] fix: auto mod route --- packages/biscuit/Routes.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/biscuit/Routes.ts b/packages/biscuit/Routes.ts index e928d63..393efed 100644 --- a/packages/biscuit/Routes.ts +++ b/packages/biscuit/Routes.ts @@ -124,6 +124,13 @@ export function GUILDS() { return `/guilds`; } +export function AUTO_MODERATION_RULES(guildId: Snowflake, ruleId?: Snowflake) { + if (ruleId) { + return `/guilds/${guildId}/auto-moderation/rules/${ruleId}`; + } + return `/guilds/${guildId}/auto-moderation/rules`; +} + export function INVITE(inviteCode: string, options?: GetInvite) { let url = `/invites/${inviteCode}?`; From c4e62631498eb7aaf38d9e65624352f4671ad7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:08:18 -0300 Subject: [PATCH 04/10] Add Application structure --- packages/biscuit/structures/Application.ts | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 packages/biscuit/structures/Application.ts diff --git a/packages/biscuit/structures/Application.ts b/packages/biscuit/structures/Application.ts new file mode 100644 index 0000000..bc2eaad --- /dev/null +++ b/packages/biscuit/structures/Application.ts @@ -0,0 +1,107 @@ +import { Model } from "./Base.ts"; +import type { Snowflake } from "../Snowflake.ts"; +import type { Session } from "../Session.ts"; +import { + DiscordApplication, + TeamMembershipStates, + DiscordInstallParams, + DiscordUser, + DiscordTeam +} from "../../discordeno/mod.ts"; +import User from "./User.ts"; + +type SummaryDeprecated = "" + +export interface Team { + /** a hash of the image of the team's icon */ + icon?: string; + /** the unique id of the team */ + id: string; + /** the members of the team */ + members: TeamMember[]; + /** user id of the current team owner */ + ownerUserId: string; + /** team name */ + name: string; +} + +export interface TeamMember { + /** the user's membership state on the team */ + membershipState: TeamMembershipStates; + permissions: "*"[]; + + teamId: string; + + user: Partial & Pick +} + +// NewTeam create a new Team object for discord applications +export function NewTeam(session: Session, data: DiscordTeam): Team { + return { + icon: data.icon ? data.icon : undefined, + id: data.id, + members: data.members.map(member => { + return { + membershipState: member.membership_state, + permissions: member.permissions, + teamId: member.team_id, + user: new User(session, member.user) + } + }), + ownerUserId: data.owner_user_id, + name: data.name, + } +} + + +export class Application implements Model { + + constructor(session: Session, data: DiscordApplication) { + this.id = data.id; + this.session = session; + + this.name = data.name; + this.icon = data.icon || undefined; + this.description = data.description; + this.rpcOrigins = data.rpc_origins; + this.botPublic = data.bot_public; + this.botRequireCodeGrant = data.bot_require_code_grant; + this.termsOfServiceURL = data.terms_of_service_url; + this.privacyPolicyURL = data.privacy_policy_url; + this.owner = data.owner ? new User(session, data.owner as DiscordUser) : undefined; + this.summary = ""; + this.verifyKey = data.verify_key; + this.team = data.team ? NewTeam(session, data.team) : undefined; + this.guildId = data.guild_id; + this.coverImage = data.cover_image; + this.tags = data.tags; + this.installParams = data.install_params; + this.customInstallURL = data.custom_install_url; + } + + session: Session; + + id: Snowflake; + name: string; + icon?: string; + description: string; + rpcOrigins?: string[]; + botPublic: boolean; + botRequireCodeGrant: boolean; + termsOfServiceURL?: string; + privacyPolicyURL?: string; + owner?: Partial; + summary: SummaryDeprecated; + verifyKey: string; + team?: Team; + guildId?: Snowflake; + primarySkuId?: Snowflake; + slug?: string; + coverImage?: string; + flags?: number; + tags?: string[]; + installParams?: DiscordInstallParams; + customInstallURL?: string; +} + +export default Application; \ No newline at end of file From 4177ab162d00284887aea322b0fad510456459b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:14:39 -0300 Subject: [PATCH 05/10] Update: minor changes --- packages/biscuit/structures/Application.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/biscuit/structures/Application.ts b/packages/biscuit/structures/Application.ts index bc2eaad..beb179b 100644 --- a/packages/biscuit/structures/Application.ts +++ b/packages/biscuit/structures/Application.ts @@ -53,7 +53,6 @@ export function NewTeam(session: Session, data: DiscordTeam): Team { } } - export class Application implements Model { constructor(session: Session, data: DiscordApplication) { @@ -79,8 +78,7 @@ export class Application implements Model { this.customInstallURL = data.custom_install_url; } - session: Session; - + readonly session: Session; id: Snowflake; name: string; icon?: string; From a7317a0a3ca2a86ebccd804d15a59b37e8a6f938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:15:03 -0300 Subject: [PATCH 06/10] TODO done: targetApplication implemented & Minor changes --- packages/biscuit/structures/Invite.ts | 48 ++++++++------------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/packages/biscuit/structures/Invite.ts b/packages/biscuit/structures/Invite.ts index 5e79787..df6cad2 100644 --- a/packages/biscuit/structures/Invite.ts +++ b/packages/biscuit/structures/Invite.ts @@ -8,6 +8,7 @@ import type { ScheduledEventEntityType, ScheduledEventPrivacyLevel, ScheduledEventStatus, + DiscordApplication } from "../../discordeno/mod.ts"; import { TargetTypes } from "../../discordeno/mod.ts"; import { GuildChannel } from "./channels.ts"; @@ -15,6 +16,7 @@ import { Member } from "./Member.ts"; import InviteGuild from "./guilds/InviteGuild.ts"; import User from "./User.ts"; import Guild from "./guilds/Guild.ts"; +import Application from "./Application.ts"; export interface InviteStageInstance { /** The members speaking in the Stage */ @@ -53,29 +55,21 @@ export class Invite { constructor(session: Session, data: DiscordInvite) { this.session = session; - if (data.guild) { - this.guild = new InviteGuild(session, data.guild); - } - - if (data.approximate_member_count) { - this.approximateMemberCount = data.approximate_member_count; - } - - if (data.approximate_presence_count) { - this.approximatePresenceCount = data.approximate_presence_count; - } - + this.guild = data.guild ? new InviteGuild(session, data.guild) : undefined; + this.approximateMemberCount = data.approximate_member_count ? data.approximate_member_count : undefined; + this.approximatePresenceCount = data.approximate_presence_count ? data.approximate_presence_count : undefined; + this.code = data.code; + this.expiresAt = data.expires_at ? Number.parseInt(data.expires_at) : undefined; + this.inviter = data.inviter ? new User(session, data.inviter) : undefined; + this.targetUser = data.target_user ? new User(session, data.target_user) : undefined; + this.targetApplication = data.target_application ? new Application(session, data.target_application as DiscordApplication) : undefined; + this.targetType = data.target_type; + if (data.channel) { const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; this.channel = new GuildChannel(session, data.channel as DiscordChannel, guildId); } - this.code = data.code; - - if (data.expires_at) { - this.expiresAt = Number.parseInt(data.expires_at); - } - if (data.guild_scheduled_event) { this.guildScheduledEvent = { id: data.guild_scheduled_event.id, @@ -105,14 +99,6 @@ export class Invite { }; } - if (data.inviter) { - this.inviter = new User(session, data.inviter); - } - - if (data.target_user) { - this.targetUser = new User(session, data.target_user); - } - if (data.stage_instance) { const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; this.stageInstance = { @@ -124,13 +110,6 @@ export class Invite { topic: data.stage_instance.topic, }; } - - // TODO: create Application structure - // this.targetApplication = data.target_application - - if (data.target_type) { - this.targetType = data.target_type; - } } readonly session: Session; @@ -145,8 +124,7 @@ export class Invite { channel?: Partial; stageInstance?: InviteStageInstance; guildScheduledEvent?: InviteScheduledEvent; - // TODO: create Application structure - // targetApplication?: Partial + targetApplication?: Partial async delete(): Promise { await Guild.prototype.deleteInvite.call(this.guild, this.code); From af512954def0caa32d221749394977aad9bd59a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:19:48 -0300 Subject: [PATCH 07/10] Update Integration structure: add readonly to session --- packages/biscuit/structures/Integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/biscuit/structures/Integration.ts b/packages/biscuit/structures/Integration.ts index 0fcca51..f7bf481 100644 --- a/packages/biscuit/structures/Integration.ts +++ b/packages/biscuit/structures/Integration.ts @@ -53,8 +53,8 @@ export class Integration implements Model { } } + readonly session: Session; id: Snowflake; - session: Session; guildId?: Snowflake; name: string; From 4f1cbcd9f31499940b1d8df62941997e407dbabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:28:53 -0300 Subject: [PATCH 08/10] Update Application docs --- packages/biscuit/structures/Application.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/biscuit/structures/Application.ts b/packages/biscuit/structures/Application.ts index beb179b..0fed63b 100644 --- a/packages/biscuit/structures/Application.ts +++ b/packages/biscuit/structures/Application.ts @@ -52,7 +52,9 @@ export function NewTeam(session: Session, data: DiscordTeam): Team { name: data.name, } } - +/** + * @link https://discord.com/developers/docs/resources/application#application-object + */ export class Application implements Model { constructor(session: Session, data: DiscordApplication) { From 24ed499800e05b06702f362f3779c60f7e427fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:39:35 -0300 Subject: [PATCH 09/10] Add inviteCreate event --- packages/biscuit/Actions.ts | 7 ++++++ packages/biscuit/structures/Invite.ts | 35 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/biscuit/Actions.ts b/packages/biscuit/Actions.ts index 3ce6bea..7b7f4e0 100644 --- a/packages/biscuit/Actions.ts +++ b/packages/biscuit/Actions.ts @@ -31,6 +31,7 @@ import type { DiscordTypingStart, DiscordUser, DiscordWebhookUpdate, + DiscordInviteCreate } from "../discordeno/mod.ts"; import type { Snowflake } from "./Snowflake.ts"; @@ -49,6 +50,7 @@ import User from "./structures/User.ts"; import Integration from "./structures/Integration.ts"; import Guild from "./structures/guilds/Guild.ts"; import InteractionFactory from "./structures/interactions/InteractionFactory.ts"; +import { NewInviteCreate, InviteCreate } from "./structures/Invite.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -275,6 +277,10 @@ export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler = (session, _shardId, invite) => { + session.emit("inviteCreate", NewInviteCreate(session, invite)); +} + export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; @@ -320,6 +326,7 @@ export interface Events { "integrationCreate": Handler<[Integration]>; "integrationUpdate": Handler<[Integration]>; "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; + "inviteCreate": Handler<[InviteCreate]>; "autoModerationRuleCreate": Handler<[AutoModerationRule]>; "autoModerationRuleUpdate": Handler<[AutoModerationRule]>; "autoModerationRuleDelete": Handler<[AutoModerationRule]>; diff --git a/packages/biscuit/structures/Invite.ts b/packages/biscuit/structures/Invite.ts index df6cad2..1aa857d 100644 --- a/packages/biscuit/structures/Invite.ts +++ b/packages/biscuit/structures/Invite.ts @@ -8,7 +8,8 @@ import type { ScheduledEventEntityType, ScheduledEventPrivacyLevel, ScheduledEventStatus, - DiscordApplication + DiscordApplication, + DiscordInviteCreate } from "../../discordeno/mod.ts"; import { TargetTypes } from "../../discordeno/mod.ts"; import { GuildChannel } from "./channels.ts"; @@ -48,6 +49,38 @@ export interface InviteScheduledEvent { image?: string; } +export interface InviteCreate { + channelId: string; + code: string; + createdAt: string; + guildId?: string; + inviter?: User; + maxAge: number; + maxUses: number; + targetType: TargetTypes; + targetUser?: User; + targetApplication?: Partial; + temporary: boolean; + uses: number; +} + +export function NewInviteCreate(session: Session, invite: DiscordInviteCreate): InviteCreate { + return { + channelId: invite.channel_id, + code: invite.code, + createdAt: invite.created_at, + guildId: invite.guild_id, + inviter: invite.inviter ? new User(session, invite.inviter) : undefined, + maxAge: invite.max_age, + maxUses: invite.max_uses, + targetType: invite.target_type, + targetUser: invite.target_user ? new User(session, invite.target_user) : undefined, + targetApplication: invite.target_application ? new Application(session, invite.target_application as DiscordApplication) : undefined, + temporary: invite.temporary, + uses: invite.uses + } +} + /** * @link https://discord.com/developers/docs/resources/invite#invite-object */ From bbb3d739dcab1c742ac99fa461f48cdc56268ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Fri, 8 Jul 2022 22:49:59 -0300 Subject: [PATCH 10/10] Add inviteDelete event --- packages/biscuit/Actions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/biscuit/Actions.ts b/packages/biscuit/Actions.ts index 7b7f4e0..1ba95a5 100644 --- a/packages/biscuit/Actions.ts +++ b/packages/biscuit/Actions.ts @@ -31,7 +31,8 @@ import type { DiscordTypingStart, DiscordUser, DiscordWebhookUpdate, - DiscordInviteCreate + DiscordInviteCreate, + DiscordInviteDelete } from "../discordeno/mod.ts"; import type { Snowflake } from "./Snowflake.ts"; @@ -281,6 +282,10 @@ export const INVITE_CREATE: RawHandler = (session, _shardId session.emit("inviteCreate", NewInviteCreate(session, invite)); } +export const INVITE_DELETE: RawHandler = (session, _shardId, data) => { + session.emit("inviteDelete", { channelId: data.channel_id, guildId: data.guild_id, code: data.code }); +} + export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; @@ -327,6 +332,7 @@ export interface Events { "integrationUpdate": Handler<[Integration]>; "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; "inviteCreate": Handler<[InviteCreate]>; + "inviteDelete": Handler<[{ channelId: string, guildId?: string, code: string }]>; "autoModerationRuleCreate": Handler<[AutoModerationRule]>; "autoModerationRuleUpdate": Handler<[AutoModerationRule]>; "autoModerationRuleDelete": Handler<[AutoModerationRule]>;