diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 5975a4b..ce32b9a 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -1,28 +1,48 @@ import type { DiscordChannel, DiscordChannelPinsUpdate, + DiscordEmoji, + DiscordGuild, + DiscordGuildBanAddRemove, + DiscordGuildEmojisUpdate, DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, + DiscordGuildRoleCreate, + DiscordGuildRoleDelete, + DiscordGuildRoleUpdate, + DiscordIntegration, + DiscordIntegrationDelete, DiscordInteraction, DiscordMemberWithUser, DiscordMessage, DiscordMessageDelete, + DiscordMessageReactionAdd, + DiscordMessageReactionRemove, + DiscordMessageReactionRemoveAll, + DiscordMessageReactionRemoveEmoji, DiscordReady, + DiscordRole, // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, DiscordThreadListSync, + DiscordUser, + DiscordWebhookUpdate, } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { Channel } from "../structures/channels/ChannelFactory.ts"; +import type { Interaction } from "../structures/interactions/InteractionFactory.ts"; import ChannelFactory from "../structures/channels/ChannelFactory.ts"; import GuildChannel from "../structures/channels/GuildChannel.ts"; import ThreadChannel from "../structures/channels/ThreadChannel.ts"; +import ThreadMember from "../structures/ThreadMember.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; -import Interaction from "../structures/interactions/Interaction.ts"; +import Integration from "../structures/Integration.ts"; +import Guild from "../structures/guilds/Guild.ts"; +import InteractionFactory from "../structures/interactions/InteractionFactory.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -38,6 +58,25 @@ export const MESSAGE_CREATE: RawHandler = (session, _shardId, me }; export const MESSAGE_UPDATE: RawHandler = (session, _shardId, new_message) => { + // message is partial + if (!new_message.edited_timestamp) { + const message = { + // TODO: improve this + // ...new_message, + session, + id: new_message.id, + guildId: new_message.guild_id, + channelId: new_message.channel_id, + }; + + // all methods of Message can run on partial messages + // we aknowledge people that their callback could be partial but giving them all functions of Message + Object.setPrototypeOf(message, Message.prototype); + + session.emit("messageUpdate", message); + return; + } + session.emit("messageUpdate", new Message(session, new_message)); }; @@ -45,6 +84,14 @@ export const MESSAGE_DELETE: RawHandler = (session, _shard session.emit("messageDelete", { id, channelId: channel_id, guildId: guild_id }); }; +export const GUILD_CREATE: RawHandler = (session, _shardId, guild) => { + session.emit("guildCreate", new Guild(session, guild)); +}; + +export const GUILD_DELETE: RawHandler = (session, _shardId, guild) => { + session.emit("guildDelete", { id: guild.id, unavailable: true }); +}; + export const GUILD_MEMBER_ADD: RawHandler = (session, _shardId, member) => { session.emit("guildMemberAdd", new Member(session, member, member.guild_id)); }; @@ -57,13 +104,32 @@ export const GUILD_MEMBER_REMOVE: RawHandler = (sessio session.emit("guildMemberRemove", new User(session, member.user), member.guild_id); }; +export const GUILD_BAN_ADD: RawHandler = (session, _shardId, data) => { + session.emit("guildBanAdd", { guildId: data.guild_id, user: data.user }); +}; + +export const GUILD_BAN_REMOVE: RawHandler = (session, _shardId, data) => { + session.emit("guildBanRemove", { guildId: data.guild_id, user: data.user }); +}; + +export const GUILD_EMOJIS_UPDATE: RawHandler = (session, _shardId, data) => { + session.emit("guildEmojisUpdate", { guildId: data.guild_id, emojis: data.emojis }); +}; + +export const GUILD_ROLE_CREATE: RawHandler = (session, _shardId, data) => { + session.emit("guildRoleCreate", { guildId: data.guild_id, role: data.role }); +}; + +export const GUILD_ROLE_UPDATE: RawHandler = (session, _shardId, data) => { + session.emit("guildRoleUpdate", { guildId: data.guild_id, role: data.role }); +}; + +export const GUILD_ROLE_DELETE: RawHandler = (session, _shardId, data) => { + session.emit("guildRoleDelete", { guildId: data.guild_id, roleId: data.role_id }); +}; + export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { - session.unrepliedInteractions.add(BigInt(interaction.id)); - - // could be improved - setTimeout(() => session.unrepliedInteractions.delete(BigInt(interaction.id)), 15 * 60 * 1000); - - session.emit("interactionCreate", new Interaction(session, interaction)); + session.emit("interactionCreate", InteractionFactory.from(session, interaction)); }; export const CHANNEL_CREATE: RawHandler = (session, _shardId, channel) => { @@ -103,10 +169,7 @@ export const THREAD_LIST_SYNC: RawHandler = (session, _sh guildId: payload.guild_id, channelIds: payload.channel_ids ?? [], threads: payload.threads.map((channel) => new ThreadChannel(session, channel, payload.guild_id)), - members: payload.members.map((member) => - // @ts-ignore: TODO: thread member structure - new Member(session, member as DiscordMemberWithUser, payload.guild_id) - ), + members: payload.members.map((member) => new ThreadMember(session, member)), }); }; @@ -118,6 +181,58 @@ export const CHANNEL_PINS_UPDATE: RawHandler = (sessio }); }; +export const WEBHOOKS_UPDATE: RawHandler = (session, _shardId, webhook) => { + session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }); +}; + +export const INTEGRATION_CREATE: RawHandler = ( + session, + _shardId, + payload, +) => { + session.emit("integrationCreate", new Integration(session, payload)); +}; + +export const INTEGRATION_UPDATE: RawHandler = ( + session, + _shardId, + payload, +) => { + session.emit("integrationCreate", new Integration(session, payload)); +}; + +export const INTEGRATION_DELETE: RawHandler = (session, _shardId, payload) => { + session.emit("integrationDelete", { + id: payload.id, + guildId: payload.guild_id, + applicationId: payload.application_id, + }); +}; + +export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { + session.emit("messageReactionAdd", null); +}; + +export const MESSAGE_REACTION_REMOVE: RawHandler = (session, _shardId, reaction) => { + session.emit("messageReactionRemove", null); +}; + +export const MESSAGE_REACTION_REMOVE_ALL: RawHandler = ( + session, + _shardId, + reaction, +) => { + session.emit("messageReactionRemoveAll", null); +}; + +export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler = ( + session, + _shardId, + reaction, +) => { + session.emit("messageReactionRemoveEmoji", null); +}; + export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; @@ -126,23 +241,42 @@ export interface Ready extends Omit { user: User; } +// TODO: add partial reactions or something +type MessageReaction = any; + // deno-fmt-ignore-file export interface Events { - "ready": Handler<[Ready, number]>; - "messageCreate": Handler<[Message]>; - "messageUpdate": Handler<[Message]>; - "messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>; - "guildMemberAdd": Handler<[Member]>; - "guildMemberUpdate": Handler<[Member]>; - "guildMemberRemove": Handler<[User, Snowflake]>; - "channelCreate": Handler<[Channel]>; - "channelUpdate": Handler<[Channel]>; - "channelDelete": Handler<[GuildChannel]>; - "channelPinsUpdate": Handler<[{ guildId?: Snowflake, channelId: Snowflake, lastPinTimestamp?: number }]> - "threadCreate": Handler<[ThreadChannel]>; - "threadUpdate": Handler<[ThreadChannel]>; - "threadDelete": Handler<[ThreadChannel]>; - "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: Member[] }]> - "interactionCreate": Handler<[Interaction]>; - "raw": Handler<[unknown, number]>; + "ready": Handler<[Ready, number]>; + "messageCreate": Handler<[Message]>; + "messageUpdate": Handler<[Partial]>; + "messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>; + "messageReactionAdd": Handler<[MessageReaction]>; + "messageReactionRemove": Handler<[MessageReaction]>; + "messageReactionRemoveAll": Handler<[MessageReaction]>; + "messageReactionRemoveEmoji": Handler<[MessageReaction]>; + "guildCreate": Handler<[Guild]>; + "guildDelete": Handler<[{ id: Snowflake, unavailable: boolean }]>; + "guildMemberAdd": Handler<[Member]>; + "guildMemberUpdate": Handler<[Member]>; + "guildMemberRemove": Handler<[User, Snowflake]>; + "guildBanAdd": Handler<[{ guildId: Snowflake, user: DiscordUser}]>; + "guildBanRemove": Handler<[{ guildId: Snowflake, user: DiscordUser }]> + "guildEmojisUpdate": Handler<[{ guildId: Snowflake, emojis: DiscordEmoji[] }]> + "guildRoleCreate": Handler<[{ guildId: Snowflake, role: DiscordRole }]>; + "guildRoleUpdate": Handler<[{ guildId: Snowflake, role: DiscordRole }]>; + "guildRoleDelete": Handler<[{ guildId: Snowflake, roleId: Snowflake }]>; + "channelCreate": Handler<[Channel]>; + "channelUpdate": Handler<[Channel]>; + "channelDelete": Handler<[GuildChannel]>; + "channelPinsUpdate": Handler<[{ guildId?: Snowflake, channelId: Snowflake, lastPinTimestamp?: number }]> + "threadCreate": Handler<[ThreadChannel]>; + "threadUpdate": Handler<[ThreadChannel]>; + "threadDelete": Handler<[ThreadChannel]>; + "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: ThreadMember[] }]> + "interactionCreate": Handler<[Interaction]>; + "integrationCreate": Handler<[Integration]>; + "integrationUpdate": Handler<[Integration]>; + "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; + "raw": Handler<[unknown, number]>; + "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; } diff --git a/mod.ts b/mod.ts index f7d725a..679d419 100644 --- a/mod.ts +++ b/mod.ts @@ -13,12 +13,14 @@ export * from "./structures/WelcomeChannel.ts"; export * from "./structures/WelcomeScreen.ts"; export * from "./structures/channels/BaseChannel.ts"; +export * from "./structures/channels/BaseVoiceChannel.ts"; export * from "./structures/channels/ChannelFactory.ts"; export * from "./structures/channels/DMChannel.ts"; export * from "./structures/channels/GuildChannel.ts"; export * from "./structures/channels/NewsChannel.ts"; export * from "./structures/channels/TextChannel.ts"; export * from "./structures/channels/ThreadChannel.ts"; +export * from "./structures/channels/StageChannel.ts"; export * from "./structures/channels/VoiceChannel.ts"; export * from "./structures/components/ActionRowComponent.ts"; @@ -33,7 +35,21 @@ export * from "./structures/guilds/BaseGuild.ts"; export * from "./structures/guilds/Guild.ts"; export * from "./structures/guilds/InviteGuild.ts"; -export * from "./structures/interactions/Interaction.ts"; +export * from "./structures/builders/EmbedBuilder.ts"; +export * from "./structures/builders/InputTextComponentBuilder.ts"; +export * from "./structures/builders/MessageActionRow.ts"; +export * from "./structures/builders/MessageButton.ts"; +export * from "./structures/builders/MessageSelectMenu.ts"; +export * from "./structures/builders/SelectMenuOptionBuilder.ts"; + +export * from "./structures/interactions/AutoCompleteInteraction.ts"; +export * from "./structures/interactions/BaseInteraction.ts"; +export * from "./structures/interactions/CommandInteraction.ts"; +export * from "./structures/interactions/CommandInteractionOptionResolver.ts"; +export * from "./structures/interactions/ComponentInteraction.ts"; +export * from "./structures/interactions/InteractionFactory.ts"; +export * from "./structures/interactions/ModalSubmitInteraction.ts"; +export * from "./structures/interactions/PingInteraction.ts"; export * from "./session/Session.ts"; diff --git a/scripts.ts b/scripts.ts new file mode 100644 index 0000000..eed003e --- /dev/null +++ b/scripts.ts @@ -0,0 +1,6 @@ +export default { + scripts: { + fmt: "deno fmt", + check: "deno check mod.ts", + }, +}; diff --git a/session/Session.ts b/session/Session.ts index 6b25993..cb82058 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -39,8 +39,6 @@ export class Session extends EventEmitter { rest: ReturnType; gateway: ReturnType; - unrepliedInteractions: Set = new Set(); - #botId: Snowflake; #applicationId?: Snowflake; @@ -67,7 +65,7 @@ export class Session extends EventEmitter { const defHandler: DiscordRawEventHandler = (shard, data) => { Actions.raw(this, shard.id, data); - if (!data.t) { + if (!data.t || !data.d) { return; } diff --git a/structures/Integration.ts b/structures/Integration.ts new file mode 100644 index 0000000..673e48a --- /dev/null +++ b/structures/Integration.ts @@ -0,0 +1,77 @@ +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/Session.ts"; +import type { DiscordIntegration, IntegrationExpireBehaviors } from "../vendor/external.ts"; +import User from "./User.ts"; + +export interface IntegrationAccount { + id: Snowflake; + name: string; +} + +export interface IntegrationApplication { + id: Snowflake; + name: string; + icon?: string; + description: string; + bot?: User; +} + +export class Integration implements Model { + constructor(session: Session, data: DiscordIntegration & { guild_id?: Snowflake }) { + this.id = data.id; + this.session = session; + + data.guild_id ? this.guildId = data.guild_id : null; + + this.name = data.name; + this.type = data.type; + this.enabled = !!data.enabled; + this.syncing = !!data.syncing; + this.roleId = data.role_id; + this.enableEmoticons = !!data.enable_emoticons; + this.expireBehavior = data.expire_behavior; + this.expireGracePeriod = data.expire_grace_period; + this.syncedAt = data.synced_at; + this.subscriberCount = data.subscriber_count; + this.revoked = !!data.revoked; + + this.user = data.user ? new User(session, data.user) : undefined; + this.account = { + id: data.account.id, + name: data.account.name, + }; + + if (data.application) { + this.application = { + id: data.application.id, + name: data.application.name, + icon: data.application.icon ? data.application.icon : undefined, + description: data.application.description, + bot: data.application.bot ? new User(session, data.application.bot) : undefined, + }; + } + } + + id: Snowflake; + session: Session; + guildId?: Snowflake; + + name: string; + type: "twitch" | "youtube" | "discord"; + enabled?: boolean; + syncing?: boolean; + roleId?: string; + enableEmoticons?: boolean; + expireBehavior?: IntegrationExpireBehaviors; + expireGracePeriod?: number; + syncedAt?: string; + subscriberCount?: number; + revoked?: boolean; + + user?: User; + account: IntegrationAccount; + application?: IntegrationApplication; +} + +export default Integration; diff --git a/structures/Invite.ts b/structures/Invite.ts index 5d238e7..5c511c2 100644 --- a/structures/Invite.ts +++ b/structures/Invite.ts @@ -1,9 +1,50 @@ import type { Session } from "../session/Session.ts"; -import type { DiscordInvite } from "../vendor/external.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { + DiscordChannel, + DiscordInvite, + DiscordMemberWithUser, + DiscordScheduledEventEntityMetadata, + ScheduledEventEntityType, + ScheduledEventPrivacyLevel, + ScheduledEventStatus, +} from "../vendor/external.ts"; import { TargetTypes } from "../vendor/external.ts"; import InviteGuild from "./guilds/InviteGuild.ts"; import User from "./User.ts"; import Guild from "./guilds/Guild.ts"; +import { GuildChannel } from "./channels/GuildChannel.ts"; +import { Member } from "./Member.ts"; + +export interface InviteStageInstance { + /** The members speaking in the Stage */ + members: Partial[]; + /** The number of users in the Stage */ + participantCount: number; + /** The number of users speaking in the Stage */ + speakerCount: number; + /** The topic of the Stage instance (1-120 characters) */ + topic: string; +} + +export interface InviteScheduledEvent { + id: Snowflake; + guildId: string; + channelId?: string; + creatorId?: string; + name: string; + description?: string; + scheduledStartTime: string; + scheduledEndTime?: string; + privacyLevel: ScheduledEventPrivacyLevel; + status: ScheduledEventStatus; + entityType: ScheduledEventEntityType; + entityId?: string; + entityMetadata?: DiscordScheduledEventEntityMetadata; + creator?: User; + userCount?: number; + image?: string; +} /** * @link https://discord.com/developers/docs/resources/invite#invite-object @@ -24,16 +65,45 @@ export class Invite { this.approximatePresenceCount = data.approximate_presence_count; } - // TODO: fix this - // this.channel = data.channel; + 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); } - // TODO: fix this - // this.xd = data.guild_scheduled_event + if (data.guild_scheduled_event) { + this.guildScheduledEvent = { + id: data.guild_scheduled_event.id, + guildId: data.guild_scheduled_event.guild_id, + channelId: data.guild_scheduled_event.channel_id ? data.guild_scheduled_event.channel_id : undefined, + creatorId: data.guild_scheduled_event.creator_id ? data.guild_scheduled_event.creator_id : undefined, + name: data.guild_scheduled_event.name, + description: data.guild_scheduled_event.description + ? data.guild_scheduled_event.description + : undefined, + scheduledStartTime: data.guild_scheduled_event.scheduled_start_time, + scheduledEndTime: data.guild_scheduled_event.scheduled_end_time + ? data.guild_scheduled_event.scheduled_end_time + : undefined, + privacyLevel: data.guild_scheduled_event.privacy_level, + status: data.guild_scheduled_event.status, + entityType: data.guild_scheduled_event.entity_type, + entityId: data.guild ? data.guild.id : undefined, + entityMetadata: data.guild_scheduled_event.entity_metadata + ? data.guild_scheduled_event.entity_metadata + : undefined, + creator: data.guild_scheduled_event.creator + ? new User(session, data.guild_scheduled_event.creator) + : undefined, + userCount: data.guild_scheduled_event.user_count ? data.guild_scheduled_event.user_count : undefined, + image: data.guild_scheduled_event.image ? data.guild_scheduled_event.image : undefined, + }; + } if (data.inviter) { this.inviter = new User(session, data.inviter); @@ -43,10 +113,19 @@ export class Invite { this.targetUser = new User(session, data.target_user); } - // TODO: fix this - // this.stageInstance = data.stage_instance + if (data.stage_instance) { + const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; + this.stageInstance = { + members: data.stage_instance.members.map((m) => + new Member(session, m as DiscordMemberWithUser, guildId) + ), + participantCount: data.stage_instance.participant_count, + speakerCount: data.stage_instance.speaker_count, + topic: data.stage_instance.topic, + }; + } - // TODO: fix this + // TODO: create Application structure // this.targetApplication = data.target_application if (data.target_type) { @@ -63,6 +142,11 @@ export class Invite { inviter?: User; targetUser?: User; targetType?: TargetTypes; + channel?: Partial; + stageInstance?: InviteStageInstance; + guildScheduledEvent?: InviteScheduledEvent; + // TODO: create Application structure + // targetApplication?: Partial async delete(): Promise { await Guild.prototype.deleteInvite.call(this.guild, this.code); diff --git a/structures/Member.ts b/structures/Member.ts index 601d5af..d0b0d92 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -5,6 +5,7 @@ import type { DiscordMemberWithUser } from "../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; import type { CreateGuildBan, ModifyGuildMember } from "./guilds/Guild.ts"; import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; +import { formatImageURL } from "../util/shared/images.ts"; import User from "./User.ts"; import Guild from "./guilds/Guild.ts"; import * as Routes from "../util/Routes.ts"; @@ -96,16 +97,20 @@ export class Member implements Model { } /** gets the user's avatar */ - avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { + avatarURL(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { let url: string; + if (this.user.bot) { + return this.user.avatarURL(); + } + if (!this.avatarHash) { url = Routes.USER_DEFAULT_AVATAR(Number(this.user.discriminator) % 5); } else { url = Routes.USER_AVATAR(this.user.id, iconBigintToHash(this.avatarHash)); } - return `${url}.${options.format ?? (url.includes("/a_") ? "gif" : "jpg")}?size=${options.size}`; + return formatImageURL(url, options.size, options.format); } toString() { diff --git a/structures/Message.ts b/structures/Message.ts index 3d734d8..9c9dd6d 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -1,5 +1,4 @@ import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { AllowedMentionsTypes, @@ -7,14 +6,20 @@ import type { DiscordMessage, DiscordUser, FileContent, + MessageActivityTypes, + MessageTypes, } from "../vendor/external.ts"; import type { Component } from "./components/Component.ts"; import type { GetReactions } from "../util/Routes.ts"; import { MessageFlags } from "../util/shared/flags.ts"; +import { iconHashToBigInt } from "../util/hash.ts"; +import { Snowflake } from "../util/Snowflake.ts"; import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; -import BaseComponent from "./components/Component.ts"; +import ComponentFactory from "./components/ComponentFactory.ts"; +import MessageReaction from "./MessageReaction.ts"; +// import ThreadChannel from "./channels/ThreadChannel.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -38,11 +43,12 @@ export interface CreateMessageReference { * @link https://discord.com/developers/docs/resources/channel#create-message-json-params */ export interface CreateMessage { + embeds?: DiscordEmbed[]; content?: string; allowedMentions?: AllowedMentions; files?: FileContent[]; messageReference?: CreateMessageReference; - embeds?: DiscordEmbed[]; + tts?: boolean; } /** @@ -57,6 +63,13 @@ export type ReactionResolvable = string | { id: Snowflake; }; +export interface WebhookAuthor { + id: string; + username: string; + discriminator: string; + avatar?: bigint; +} + /** * Represents a message * @link https://discord.com/developers/docs/resources/channel#message-object @@ -66,40 +79,108 @@ export class Message implements Model { this.session = session; this.id = data.id; + this.type = data.type; this.channelId = data.channel_id; this.guildId = data.guild_id; + this.applicationId = data.application_id; + + if (!data.webhook_id) { + this.author = new User(session, data.author); + } - this.author = new User(session, data.author); this.flags = data.flags; this.pinned = !!data.pinned; this.tts = !!data.tts; this.content = data.content!; + this.nonce = data.nonce; + this.mentionEveryone = data.mention_everyone; + this.timestamp = Date.parse(data.timestamp); + this.editedTimestamp = data.edited_timestamp ? Date.parse(data.edited_timestamp) : undefined; + + this.reactions = data.reactions?.map((react) => new MessageReaction(session, react)) ?? []; this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment)); + this.embeds = data.embeds; + + if (data.thread && data.guild_id) { + // this.thread = new ThreadChannel(session, data.thread, data.guild_id); + } + + // webhook handling + if (data.webhook_id && data.author.discriminator === "0000") { + this.webhook = { + id: data.webhook_id!, + username: data.author.username, + discriminator: data.author.discriminator, + avatar: data.author.avatar ? iconHashToBigInt(data.author.avatar) : undefined, + }; + } // user is always null on MessageCreate and its replaced with author - - if (data.guild_id && data.member) { + if (data.guild_id && data.member && !this.isWebhookMessage()) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } - this.components = data.components?.map((component) => BaseComponent.from(session, component)); + this.components = data.components?.map((component) => ComponentFactory.from(session, component)) ?? []; + + if (data.activity) { + this.activity = { + partyId: data.activity.party_id, + type: data.activity.type, + }; + } } readonly session: Session; readonly id: Snowflake; + type: MessageTypes; channelId: Snowflake; guildId?: Snowflake; - author: User; + applicationId?: Snowflake; + author!: User; flags?: MessageFlags; pinned: boolean; tts: boolean; content: string; + nonce?: string | number; + mentionEveryone: boolean; + timestamp: number; + editedTimestamp?: number; + + reactions: MessageReaction[]; attachments: Attachment[]; + embeds: DiscordEmbed[]; member?: Member; - components?: Component[]; + // thread?: ThreadChannel; + components: Component[]; + + webhook?: WebhookAuthor; + activity?: { + partyId?: Snowflake; + type: MessageActivityTypes; + }; + + get createdTimestamp() { + return Snowflake.snowflakeToTimestamp(this.id); + } + + get createdAt() { + return new Date(this.createdTimestamp); + } + + get sentAt() { + return new Date(this.timestamp); + } + + get editedAt() { + return this.editedTimestamp ? new Date(this.editedTimestamp) : undefined; + } + + get edited() { + return this.editedTimestamp; + } get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; @@ -190,6 +271,7 @@ export class Message implements Model { } : undefined, embeds: options.embeds, + tts: options.tts, }, ); @@ -281,9 +363,15 @@ export class Message implements Model { return this.crosspost; } - inGuild(): this is { guildId: Snowflake } & Message { + /** wheter the message comes from a guild **/ + inGuild(): this is Message & { guildId: Snowflake } { return !!this.guildId; } + + /** wheter the messages comes from a Webhook */ + isWebhookMessage(): this is Message & { author: Partial; webhook: WebhookAuthor; member: undefined } { + return !!this.webhook; + } } export default Message; diff --git a/structures/MessageReaction.ts b/structures/MessageReaction.ts new file mode 100644 index 0000000..5decdf4 --- /dev/null +++ b/structures/MessageReaction.ts @@ -0,0 +1,23 @@ +import type { Session } from "../session/Session.ts"; +import type { DiscordReaction } from "../vendor/external.ts"; +import Emoji from "./Emoji.ts"; + +/** + * Represents a reaction + * @link https://discord.com/developers/docs/resources/channel#reaction-object + */ +export class MessageReaction { + constructor(session: Session, data: DiscordReaction) { + this.session = session; + this.me = data.me; + this.count = data.count; + this.emoji = new Emoji(session, data.emoji); + } + + readonly session: Session; + me: boolean; + count: number; + emoji: Emoji; +} + +export default MessageReaction; diff --git a/structures/StageInstance.ts b/structures/StageInstance.ts new file mode 100644 index 0000000..5c2b91e --- /dev/null +++ b/structures/StageInstance.ts @@ -0,0 +1,61 @@ +import type { Model } from "./Base.ts"; +import type { Session } from "../session/Session.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { DiscordStageInstance as DiscordAutoClosingStageInstance } from "../vendor/external.ts"; +import * as Routes from "../util/Routes.ts"; + +interface DiscordStageInstance extends DiscordAutoClosingStageInstance { + privacy_level: PrivacyLevels; + discoverable_disabled: boolean; + guild_scheduled_event_id: Snowflake; +} + +export enum PrivacyLevels { + Public = 1, + GuildOnly = 2, +} + +export class StageInstance implements Model { + constructor(session: Session, data: DiscordStageInstance) { + this.session = session; + this.id = data.id; + this.channelId = data.channel_id; + this.guildId = data.guild_id; + this.topic = data.topic; + this.privacyLevel = data.privacy_level; + this.discoverableDisabled = data.discoverable_disabled; + this.guildScheduledEventId = data.guild_scheduled_event_id; + } + + readonly session: Session; + readonly id: Snowflake; + + channelId: Snowflake; + guildId: Snowflake; + topic: string; + + // TODO: see if this works + privacyLevel: PrivacyLevels; + discoverableDisabled: boolean; + guildScheduledEventId: Snowflake; + + async edit(options: { topic?: string; privacyLevel?: PrivacyLevels }) { + const stageInstance = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.STAGE_INSTANCE(this.id), + { + topic: options.topic, + privacy_level: options.privacyLevel, + }, + ); + + return new StageInstance(this.session, stageInstance); + } + + async delete() { + await this.session.rest.runMethod(this.session.rest, "DELETE", Routes.STAGE_INSTANCE(this.id)); + } +} + +export default StageInstance; diff --git a/structures/ThreadMember.ts b/structures/ThreadMember.ts new file mode 100644 index 0000000..7679d88 --- /dev/null +++ b/structures/ThreadMember.ts @@ -0,0 +1,47 @@ +import type { Model } from "./Base.ts"; +import type { Session } from "../session/Session.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { DiscordThreadMember } from "../vendor/external.ts"; +import * as Routes from "../util/Routes.ts"; + +/** + * A member that comes from a thread + * @link https://discord.com/developers/docs/resources/channel#thread-member-object + * **/ +export class ThreadMember implements Model { + constructor(session: Session, data: DiscordThreadMember) { + this.session = session; + this.id = data.id; + this.flags = data.flags; + this.timestamp = Date.parse(data.join_timestamp); + } + + readonly session: Session; + readonly id: Snowflake; + flags: number; + timestamp: number; + + get threadId() { + return this.id; + } + + async quitThread(memberId: Snowflake = this.session.botId) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.THREAD_USER(this.id, memberId), + ); + } + + async fetchMember(memberId: Snowflake = this.session.botId) { + const member = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_USER(this.id, memberId), + ); + + return new ThreadMember(this.session, member); + } +} + +export default ThreadMember; diff --git a/structures/Webhook.ts b/structures/Webhook.ts index 6a85a22..629ac36 100644 --- a/structures/Webhook.ts +++ b/structures/Webhook.ts @@ -1,9 +1,13 @@ import type { Model } from "./Base.ts"; import type { Session } from "../session/Session.ts"; import type { Snowflake } from "../util/Snowflake.ts"; -import type { DiscordWebhook, WebhookTypes } from "../vendor/external.ts"; +import type { DiscordMessage, DiscordWebhook, WebhookTypes } from "../vendor/external.ts"; +import type { WebhookOptions } from "../util/Routes.ts"; +import type { CreateMessage } from "./Message.ts"; import { iconHashToBigInt } from "../util/hash.ts"; import User from "./User.ts"; +import Message from "./Message.ts"; +import * as Routes from "../util/Routes.ts"; export class Webhook implements Model { constructor(session: Session, data: DiscordWebhook) { @@ -42,6 +46,62 @@ export class Webhook implements Model { channelId?: Snowflake; guildId?: Snowflake; user?: User; + + async execute(options?: WebhookOptions & CreateMessage & { avatarUrl?: string; username?: string }) { + if (!this.token) { + return; + } + + const data = { + content: options?.content, + embeds: options?.embeds, + tts: options?.tts, + allowed_mentions: options?.allowedMentions, + // @ts-ignore: TODO: component builder or something + components: options?.components, + file: options?.files, + }; + + const message = await this.session.rest.sendRequest(this.session.rest, { + url: Routes.WEBHOOK(this.id, this.token!, { + wait: options?.wait, + threadId: options?.threadId, + }), + method: "POST", + payload: this.session.rest.createRequestBody(this.session.rest, { + method: "POST", + body: { + ...data, + }, + }), + }); + + return (options?.wait ?? true) ? new Message(this.session, message) : undefined; + } + + async fetch() { + const message = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.WEBHOOK_TOKEN(this.id, this.token), + ); + + return new Webhook(this.session, message); + } + + async fetchMessage(messageId: Snowflake) { + if (!this.token) { + return; + } + + const message = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.WEBHOOK_MESSAGE(this.id, this.token, messageId), + ); + + return new Message(this.session, message); + } } export default Webhook; diff --git a/structures/builders/EmbedBuilder.ts b/structures/builders/EmbedBuilder.ts new file mode 100644 index 0000000..8e48e78 --- /dev/null +++ b/structures/builders/EmbedBuilder.ts @@ -0,0 +1,109 @@ +import type { DiscordEmbed, DiscordEmbedField, DiscordEmbedProvider } from "../../vendor/external.ts"; + +export interface EmbedFooter { + text: string; + iconUrl?: string; + proxyIconUrl?: string; +} + +export interface EmbedAuthor { + name: string; + text?: string; + url?: string; + iconUrl?: string; + proxyIconUrl?: string; +} + +export interface EmbedVideo { + height?: number; + proxyUrl?: string; + url?: string; + width?: number; +} + +export class EmbedBuilder { + #data: DiscordEmbed; + constructor(data: DiscordEmbed = {}) { + this.#data = data; + if (!this.#data.fields) this.#data.fields = []; + } + + setAuthor(author: EmbedAuthor) { + this.#data.author = { + name: author.name, + icon_url: author.iconUrl, + proxy_icon_url: author.proxyIconUrl, + url: author.url, + }; + return this; + } + + setColor(color: number) { + this.#data.color = color; + return this; + } + + setDescription(description: string) { + this.#data.description = description; + return this; + } + + addField(field: DiscordEmbedField) { + this.#data.fields!.push(field); + return this; + } + + setFooter(footer: EmbedFooter) { + this.#data.footer = { + text: footer.text, + icon_url: footer.iconUrl, + proxy_icon_url: footer.proxyIconUrl, + }; + return this; + } + + setImage(image: string) { + this.#data.image = { url: image }; + return this; + } + + setProvider(provider: DiscordEmbedProvider) { + this.#data.provider = provider; + return this; + } + + setThumbnail(thumbnail: string) { + this.#data.thumbnail = { url: thumbnail }; + return this; + } + + setTimestamp(timestamp: string | Date) { + this.#data.timestamp = timestamp instanceof Date ? timestamp.toISOString() : timestamp; + return this; + } + + setTitle(title: string, url?: string) { + this.#data.title = title; + if (url) this.setUrl(url); + return this; + } + + setUrl(url: string) { + this.#data.url = url; + return this; + } + + setVideo(video: EmbedVideo) { + this.#data.video = { + height: video.height, + proxy_url: video.proxyUrl, + url: video.url, + width: video.width, + }; + return this; + } + + toJSON(): DiscordEmbed { + return this.#data; + } +} diff --git a/structures/builders/InputTextComponentBuilder.ts b/structures/builders/InputTextComponentBuilder.ts new file mode 100644 index 0000000..3bba80d --- /dev/null +++ b/structures/builders/InputTextComponentBuilder.ts @@ -0,0 +1,49 @@ +import { DiscordInputTextComponent, MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; + +export class InputTextBuilder { + constructor() { + this.#data = {} as DiscordInputTextComponent; + this.type = 4; + } + #data: DiscordInputTextComponent; + type: MessageComponentTypes.InputText; + + setStyle(style: TextStyles) { + this.#data.style = style; + return this; + } + + setLabel(label: string) { + this.#data.label = label; + return this; + } + + setPlaceholder(placeholder: string) { + this.#data.placeholder = placeholder; + return this; + } + + setLength(max?: number, min?: number) { + this.#data.max_length = max; + this.#data.min_length = min; + return this; + } + + setCustomId(id: string) { + this.#data.custom_id = id; + return this; + } + + setValue(value: string) { + this.#data.value = value; + return this; + } + + setRequired(required = true) { + this.#data.required = required; + return this; + } + toJSON() { + return { ...this.#data }; + } +} diff --git a/structures/builders/MessageActionRow.ts b/structures/builders/MessageActionRow.ts new file mode 100644 index 0000000..856929a --- /dev/null +++ b/structures/builders/MessageActionRow.ts @@ -0,0 +1,29 @@ +import { MessageComponentTypes } from "../../vendor/external.ts"; +import { AnyComponentBuilder } from "../../util/builders.ts"; + +export class ActionRowBuilder { + constructor() { + this.components = [] as T[]; + this.type = 1; + } + components: T[]; + type: MessageComponentTypes.ActionRow; + + addComponents(...components: T[]) { + this.components.push(...components); + return this; + } + + setComponents(...components: T[]) { + this.components.splice( + 0, + this.components.length, + ...components, + ); + return this; + } + + toJSON() { + return { type: this.type, components: this.components.map((c) => c.toJSON()) }; + } +} diff --git a/structures/builders/MessageButton.ts b/structures/builders/MessageButton.ts new file mode 100644 index 0000000..4ef0910 --- /dev/null +++ b/structures/builders/MessageButton.ts @@ -0,0 +1,44 @@ +import { ButtonStyles, type DiscordButtonComponent, MessageComponentTypes } from "../../vendor/external.ts"; +import { ComponentEmoji } from "../../util/builders.ts"; + +export class ButtonBuilder { + constructor() { + this.#data = {} as DiscordButtonComponent; + this.type = 2; + } + #data: DiscordButtonComponent; + type: MessageComponentTypes.Button; + setStyle(style: ButtonStyles) { + this.#data.style = style; + return this; + } + + setLabel(label: string) { + this.#data.label = label; + return this; + } + + setCustomId(id: string) { + this.#data.custom_id = id; + return this; + } + + setEmoji(emoji: ComponentEmoji) { + this.#data.emoji = emoji; + return this; + } + + setDisabled(disabled = true) { + this.#data.disabled = disabled; + return this; + } + + setURL(url: string) { + this.#data.url = url; + return this; + } + + toJSON(): DiscordButtonComponent { + return { ...this.#data }; + } +} diff --git a/structures/builders/MessageSelectMenu.ts b/structures/builders/MessageSelectMenu.ts new file mode 100644 index 0000000..a932131 --- /dev/null +++ b/structures/builders/MessageSelectMenu.ts @@ -0,0 +1,53 @@ +import { type DiscordSelectMenuComponent, MessageComponentTypes } from "../../vendor/external.ts"; +import { SelectMenuOptionBuilder } from "./SelectMenuOptionBuilder.ts"; + +export class SelectMenuBuilder { + constructor() { + this.#data = {} as DiscordSelectMenuComponent; + this.type = 3; + this.options = []; + } + #data: DiscordSelectMenuComponent; + type: MessageComponentTypes.SelectMenu; + options: SelectMenuOptionBuilder[]; + + setPlaceholder(placeholder: string) { + this.#data.placeholder = placeholder; + return this; + } + + setValues(max?: number, min?: number) { + this.#data.max_values = max; + this.#data.min_values = min; + return this; + } + + setDisabled(disabled = true) { + this.#data.disabled = disabled; + return this; + } + + setCustomId(id: string) { + this.#data.custom_id = id; + return this; + } + + setOptions(...options: SelectMenuOptionBuilder[]) { + this.options.splice( + 0, + this.options.length, + ...options, + ); + return this; + } + + addOptions(...options: SelectMenuOptionBuilder[]) { + this.options.push( + ...options, + ); + } + + toJSON() { + return { ...this.#data, options: this.options.map((option) => option.toJSON()) }; + } +} diff --git a/structures/builders/SelectMenuOptionBuilder.ts b/structures/builders/SelectMenuOptionBuilder.ts new file mode 100644 index 0000000..ad84ff6 --- /dev/null +++ b/structures/builders/SelectMenuOptionBuilder.ts @@ -0,0 +1,38 @@ +import type { DiscordSelectOption } from "../../vendor/external.ts"; +import type { ComponentEmoji } from "../../util/builders.ts"; + +export class SelectMenuOptionBuilder { + constructor() { + this.#data = {} as DiscordSelectOption; + } + #data: DiscordSelectOption; + + setLabel(label: string) { + this.#data.label = label; + return this; + } + + setValue(value: string) { + this.#data.value = value; + return this; + } + + setDescription(description: string) { + this.#data.description = description; + return this; + } + + setDefault(Default = true) { + this.#data.default = Default; + return this; + } + + setEmoji(emoji: ComponentEmoji) { + this.#data.emoji = emoji; + return this; + } + + toJSON() { + return { ...this.#data }; + } +} diff --git a/structures/channels.ts b/structures/channels.ts new file mode 100644 index 0000000..35f7a63 --- /dev/null +++ b/structures/channels.ts @@ -0,0 +1,691 @@ +/** Types */ +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/Session.ts"; + +/** External from vendor */ +import { + DiscordChannel, + VideoQualityModes, + ChannelTypes, + GatewayOpcodes, + DiscordInvite, + DiscordMessage, + DiscordWebhook, + TargetTypes, + DiscordInviteMetadata, +DiscordThreadMember, +DiscordListArchivedThreads +} from "../vendor/external.ts"; + +/** Functions and others */ +import { calculateShardId } from "../vendor/gateway/calculateShardId.ts"; +import { urlToBase64 } from "../util/urlToBase64.ts"; + +/** Classes and routes */ +import * as Routes from "../util/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"; +import { PermissionsOverwrites } from "../util/permissions.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"); + } + } +} \ No newline at end of file diff --git a/structures/channels/BaseChannel.ts b/structures/channels/BaseChannel.ts index fc88e18..deb1809 100644 --- a/structures/channels/BaseChannel.ts +++ b/structures/channels/BaseChannel.ts @@ -7,6 +7,7 @@ import type VoiceChannel from "./VoiceChannel.ts"; import type DMChannel from "./DMChannel.ts"; import type NewsChannel from "./NewsChannel.ts"; import type ThreadChannel from "./ThreadChannel.ts"; +import type StageChannel from "./StageChannel.ts"; import { ChannelTypes } from "../../vendor/external.ts"; import { textBasedChannels } from "./TextChannel.ts"; @@ -43,6 +44,10 @@ export abstract class BaseChannel implements Model { return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; } + isStage(): this is StageChannel { + return this.type === ChannelTypes.GuildStageVoice; + } + toString(): string { return `<#${this.id}>`; } diff --git a/structures/channels/BaseVoiceChannel.ts b/structures/channels/BaseVoiceChannel.ts new file mode 100644 index 0000000..7f7cfbe --- /dev/null +++ b/structures/channels/BaseVoiceChannel.ts @@ -0,0 +1,62 @@ +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, DiscordChannel, VideoQualityModes } from "../../vendor/external.ts"; +import { GatewayOpcodes } from "../../vendor/external.ts"; +import { calculateShardId } from "../../vendor/gateway/calculateShardId.ts"; +import GuildChannel from "./GuildChannel.ts"; + +/** + * @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, + }, + }); + } +} + +export default BaseVoiceChannel; diff --git a/structures/channels/ChannelFactory.ts b/structures/channels/ChannelFactory.ts index 2603514..31a1578 100644 --- a/structures/channels/ChannelFactory.ts +++ b/structures/channels/ChannelFactory.ts @@ -7,13 +7,15 @@ import VoiceChannel from "./VoiceChannel.ts"; import DMChannel from "./DMChannel.ts"; import NewsChannel from "./NewsChannel.ts"; import ThreadChannel from "./ThreadChannel.ts"; +import StageChannel from "./StageChannel.ts"; export type Channel = | TextChannel | VoiceChannel | DMChannel | NewsChannel - | ThreadChannel; + | ThreadChannel + | StageChannel; export class ChannelFactory { static from(session: Session, channel: DiscordChannel): Channel { @@ -27,6 +29,8 @@ export class ChannelFactory { 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); diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index aaa4e53..d2b93a5 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -1,23 +1,82 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; +import type { PermissionsOverwrites } from "../../util/permissions.ts"; import type { Session } from "../../session/Session.ts"; -import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata } from "../../vendor/external.ts"; +import type { + ChannelTypes, + DiscordChannel, + DiscordInviteMetadata, + DiscordListArchivedThreads, + VideoQualityModes, +} from "../../vendor/external.ts"; +import type { ListArchivedThreads } from "../../util/Routes.ts"; import BaseChannel from "./BaseChannel.ts"; +import VoiceChannel from "./VoiceChannel.ts"; +import NewsChannel from "./NewsChannel.ts"; +import StageChannel from "./StageChannel.ts"; +import ThreadMember from "../ThreadMember.ts"; import Invite from "../Invite.ts"; import * as Routes from "../../util/Routes.ts"; +import { Channel, ChannelFactory } from "./ChannelFactory.ts"; /** - * Represent the options object to create a Thread Channel + * 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; + 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); @@ -44,27 +103,85 @@ export class GuildChannel extends BaseChannel implements Model { 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: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { + let func: (channelId: Snowflake, options: 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", - Routes.CHANNEL_CREATE_THREAD(this.id), - options, - ); - return new ThreadChannel(this.session, thread, this.guildId); - }*/ - - async delete(reason?: string) { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.CHANNEL(this.id), + "messageId" in options + ? Routes.THREAD_START_PUBLIC(this.id, options.messageId) + : Routes.THREAD_START_PRIVATE(this.id), { - reason, + name: options.name, + auto_archive_duration: options.autoArchiveDuration, }, ); - } + + return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); + }*/ } export default GuildChannel; diff --git a/structures/channels/StageChannel.ts b/structures/channels/StageChannel.ts new file mode 100644 index 0000000..41b49f7 --- /dev/null +++ b/structures/channels/StageChannel.ts @@ -0,0 +1,16 @@ +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import BaseVoiceChannel from "./BaseVoiceChannel.ts"; + +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; +} + +export default StageChannel; diff --git a/structures/channels/TextChannel.ts b/structures/channels/TextChannel.ts index 6f5e324..e2ae955 100644 --- a/structures/channels/TextChannel.ts +++ b/structures/channels/TextChannel.ts @@ -44,6 +44,7 @@ export const textBasedChannels = [ ChannelTypes.GuildPrivateThread, ChannelTypes.GuildPublicThread, ChannelTypes.GuildNews, + ChannelTypes.GuildVoice, ChannelTypes.GuildText, ]; @@ -53,6 +54,7 @@ export type TextBasedChannels = | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread | ChannelTypes.GuildNews + | ChannelTypes.GuildVoice | ChannelTypes.GuildText; export class TextChannel { @@ -85,21 +87,28 @@ export class TextChannel { /** * Mixin */ - static applyTo(klass: Function) { - klass.prototype.fetchPins = TextChannel.prototype.fetchPins; - klass.prototype.createInvite = TextChannel.prototype.createInvite; - klass.prototype.fetchMessages = TextChannel.prototype.fetchMessages; - klass.prototype.sendTyping = TextChannel.prototype.sendTyping; - klass.prototype.pinMessage = TextChannel.prototype.pinMessage; - klass.prototype.unpinMessage = TextChannel.prototype.unpinMessage; - klass.prototype.addReaction = TextChannel.prototype.addReaction; - klass.prototype.removeReaction = TextChannel.prototype.removeReaction; - klass.prototype.removeReactionEmoji = TextChannel.prototype.removeReactionEmoji; - klass.prototype.nukeReactions = TextChannel.prototype.nukeReactions; - klass.prototype.fetchReactions = TextChannel.prototype.fetchReactions; - klass.prototype.sendMessage = TextChannel.prototype.sendMessage; - klass.prototype.editMessage = TextChannel.prototype.editMessage; - klass.prototype.createWebhook = TextChannel.prototype.createWebhook; + 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 { diff --git a/structures/channels/ThreadChannel.ts b/structures/channels/ThreadChannel.ts index 459ba6e..0bb036d 100644 --- a/structures/channels/ThreadChannel.ts +++ b/structures/channels/ThreadChannel.ts @@ -1,9 +1,11 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; -import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import type { ChannelTypes, DiscordChannel, DiscordThreadMember } from "../../vendor/external.ts"; import GuildChannel from "./GuildChannel.ts"; import TextChannel from "./TextChannel.ts"; +import ThreadMember from "../ThreadMember.ts"; +import * as Routes from "../../util/Routes.ts"; export class ThreadChannel extends GuildChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { @@ -16,6 +18,10 @@ export class ThreadChannel extends GuildChannel implements Model { 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; @@ -25,7 +31,50 @@ export class ThreadChannel extends GuildChannel implements Model { 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); diff --git a/structures/channels/VoiceChannel.ts b/structures/channels/VoiceChannel.ts index 00da784..670e5fd 100644 --- a/structures/channels/VoiceChannel.ts +++ b/structures/channels/VoiceChannel.ts @@ -1,60 +1,19 @@ import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; -import type { DiscordChannel, VideoQualityModes } from "../../vendor/external.ts"; -import { GatewayOpcodes } from "../../vendor/external.ts"; -import { calculateShardId } from "../../vendor/gateway/calculateShardId.ts"; -import GuildChannel from "./GuildChannel.ts"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import BaseVoiceChannel from "./BaseVoiceChannel.ts"; +import TextChannel from "./TextChannel.ts"; -/** - * @link https://discord.com/developers/docs/topics/gateway#update-voice-state - */ -export interface UpdateVoiceState { - guildId: string; - channelId?: string; - selfMute: boolean; - selfDeaf: boolean; -} - -export class VoiceChannel extends GuildChannel { +export class VoiceChannel extends BaseVoiceChannel { 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; - - if (data.rtc_region) { - this.rtcRegion = data.rtc_region; - } - } - 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, - }, - }); + this.type = data.type as number; } + override type: ChannelTypes.GuildVoice; } +export interface VoiceChannel extends TextChannel, BaseVoiceChannel {} + +TextChannel.applyTo(VoiceChannel); + export default VoiceChannel; diff --git a/structures/guilds/Guild.ts b/structures/guilds/Guild.ts index fe39151..bae102c 100644 --- a/structures/guilds/Guild.ts +++ b/structures/guilds/Guild.ts @@ -2,11 +2,18 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; import type { + ChannelTypes, DiscordEmoji, DiscordGuild, DiscordInviteMetadata, + DiscordListActiveThreads, DiscordMemberWithUser, + DiscordOverwrite, DiscordRole, + GuildFeatures, + MakeRequired, + SystemChannelFlags, + VideoQualityModes, } from "../../vendor/external.ts"; import type { GetInvite } from "../../util/Routes.ts"; import { @@ -15,12 +22,14 @@ import { VerificationLevels, } from "../../vendor/external.ts"; import { iconBigintToHash, iconHashToBigInt } from "../../util/hash.ts"; -import { urlToBase64 } from "../../util/urlToBase64.ts"; +import { encode as _encode, urlToBase64 } from "../../util/urlToBase64.ts"; import Member from "../Member.ts"; import BaseGuild from "./BaseGuild.ts"; import Role from "../Role.ts"; import GuildEmoji from "../GuildEmoji.ts"; import Invite from "../Invite.ts"; +import ThreadMember from "../ThreadMember.ts"; +import ThreadChannel from "../channels/ThreadChannel.ts"; import * as Routes from "../../util/Routes.ts"; export interface CreateRole { @@ -86,6 +95,103 @@ export interface ModifyRolePositions { position?: number | null; } +export interface GuildCreateOptionsRole { + id: Snowflake; + name?: string; + color?: number; + hoist?: boolean; + position?: number; + permissions?: bigint; + mentionable?: boolean; + iconURL?: string; + unicodeEmoji?: string | null; +} + +export interface GuildCreateOptionsRole { + id: Snowflake; + name?: string; + color?: number; + hoist?: boolean; + position?: number; + permissions?: bigint; + mentionable?: boolean; + iconHash?: bigint; + unicodeEmoji?: string | null; +} + +export interface GuildCreateOptionsChannel { + id?: Snowflake; + parentId?: Snowflake; + type?: ChannelTypes.GuildText | ChannelTypes.GuildVoice | ChannelTypes.GuildCategory; + name: string; + topic?: string | null; + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + rtcRegion?: string | null; + videoQualityMode?: VideoQualityModes; + permissionOverwrites?: MakeRequired, "id">[]; + rateLimitPerUser?: number; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#create-guild + */ +export interface GuildCreateOptions { + name: string; + afkChannelId?: Snowflake; + afkTimeout?: number; + channels?: GuildCreateOptionsChannel[]; + defaultMessageNotifications?: DefaultMessageNotificationLevels; + explicitContentFilter?: ExplicitContentFilterLevels; + iconURL?: string; + roles?: GuildCreateOptionsRole[]; + systemChannelFlags?: SystemChannelFlags; + systemChannelId?: Snowflake; + verificationLevel?: VerificationLevels; +} + +export interface GuildCreateOptions { + name: string; + afkChannelId?: Snowflake; + afkTimeout?: number; + channels?: GuildCreateOptionsChannel[]; + defaultMessageNotifications?: DefaultMessageNotificationLevels; + explicitContentFilter?: ExplicitContentFilterLevels; + iconHash?: bigint; + roles?: GuildCreateOptionsRole[]; + systemChannelFlags?: SystemChannelFlags; + systemChannelId?: Snowflake; + verificationLevel?: VerificationLevels; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#modify-guild-json-params + */ +export interface GuildEditOptions extends Omit { + ownerId?: Snowflake; + splashURL?: string; + bannerURL?: string; + discoverySplashURL?: string; + features?: GuildFeatures[]; + rulesChannelId?: Snowflake; + description?: string; + premiumProgressBarEnabled?: boolean; +} + +export interface GuildEditOptions extends Omit { + ownerId?: Snowflake; + splashHash?: bigint; + bannerHash?: bigint; + discoverySplashHash?: bigint; + features?: GuildFeatures[]; + rulesChannelId?: Snowflake; + publicUpdatesChannelId?: Snowflake; + preferredLocale?: string | null; + description?: string; + premiumProgressBarEnabled?: boolean; +} + /** * Represents a guild * @link https://discord.com/developers/docs/resources/guild#guild-object @@ -362,6 +468,121 @@ export class Guild extends BaseGuild implements Model { return result.pruned; } + + async getActiveThreads() { + const { threads, members } = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_ACTIVE(this.id), + ); + + 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, + }; + } + + /*** + * Makes the bot leave the guild + */ + async leave() { + } + + /*** + * Deletes a guild + */ + async delete() { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILDS(), + ); + } + + /** + * Creates a guild and returns its data, the bot joins the guild + * This was modified from discord.js to make it compatible + * precondition: Bot should be in less than 10 servers + */ + static async create(session: Session, options: GuildCreateOptions) { + const guild = await session.rest.runMethod(session.rest, "POST", Routes.GUILDS(), { + name: options.name, + afk_channel_id: options.afkChannelId, + afk_timeout: options.afkTimeout, + default_message_notifications: options.defaultMessageNotifications, + explicit_content_filter: options.explicitContentFilter, + system_channel_flags: options.systemChannelFlags, + verification_level: options.verificationLevel, + icon: "iconURL" in options + ? options.iconURL || urlToBase64(options.iconURL!) + : options.iconHash || iconBigintToHash(options.iconHash!), + channels: options.channels?.map((channel) => ({ + name: channel.name, + nsfw: channel.nsfw, + id: channel.id, + bitrate: channel.bitrate, + parent_id: channel.parentId, + permission_overwrites: channel.permissionOverwrites, + rtc_region: channel.rtcRegion, + user_limit: channel.userLimit, + video_quality_mode: channel.videoQualityMode, + rate_limit_per_user: channel.rateLimitPerUser, + })), + roles: options.roles?.map((role) => ({ + name: role.name, + id: role.id, + color: role.color, + mentionable: role.mentionable, + hoist: role.hoist, + position: role.position, + unicode_emoji: role.unicodeEmoji, + icon: options.iconURL || urlToBase64(options.iconURL!), + })), + }); + + return new Guild(session, guild); + } + + /** + * Edits a guild and returns its data + */ + async edit(session: Session, options: GuildEditOptions) { + const guild = await session.rest.runMethod(session.rest, "PATCH", Routes.GUILDS(), { + name: options.name, + afk_channel_id: options.afkChannelId, + afk_timeout: options.afkTimeout, + default_message_notifications: options.defaultMessageNotifications, + explicit_content_filter: options.explicitContentFilter, + system_channel_flags: options.systemChannelFlags, + verification_level: options.verificationLevel, + icon: "iconURL" in options + ? options.iconURL || urlToBase64(options.iconURL!) + : options.iconHash || iconBigintToHash(options.iconHash!), + // extra props + splash: "splashURL" in options + ? options.splashURL || urlToBase64(options.splashURL!) + : options.splashHash || iconBigintToHash(options.iconHash!), + banner: "bannerURL" in options + ? options.bannerURL || urlToBase64(options.bannerURL!) + : options.bannerHash || iconBigintToHash(options.bannerHash!), + discovery_splash: "discoverySplashURL" in options + ? options.discoverySplashURL || urlToBase64(options.discoverySplashURL!) + : options.discoverySplashHash || iconBigintToHash(options.discoverySplashHash!), + owner_id: options.ownerId, + rules_channel_id: options.rulesChannelId, + public_updates_channel_id: options.publicUpdatesChannelId, + preferred_locale: options.preferredLocale, + features: options.features, + description: options.description, + premiumProgressBarEnabled: options.premiumProgressBarEnabled, + }); + + return new Guild(session, guild); + } } export default Guild; diff --git a/structures/interactions/AutoCompleteInteraction.ts b/structures/interactions/AutoCompleteInteraction.ts new file mode 100644 index 0000000..7fe886e --- /dev/null +++ b/structures/interactions/AutoCompleteInteraction.ts @@ -0,0 +1,39 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import type { ApplicationCommandOptionChoice } from "./CommandInteraction.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class AutoCompleteInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + } + + override type: InteractionTypes.ApplicationCommandAutocomplete; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async respond(choices: ApplicationCommandOptionChoice[]) { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + data: { choices }, + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + }, + ); + } +} + +export default AutoCompleteInteraction; diff --git a/structures/interactions/BaseInteraction.ts b/structures/interactions/BaseInteraction.ts new file mode 100644 index 0000000..a93ca8f --- /dev/null +++ b/structures/interactions/BaseInteraction.ts @@ -0,0 +1,84 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction } from "../../vendor/external.ts"; +import type CommandInteraction from "./CommandInteraction.ts"; +import type PingInteraction from "./PingInteraction.ts"; +import { InteractionTypes } from "../../vendor/external.ts"; +import { Snowflake } from "../../util/Snowflake.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; +import Permsisions from "../Permissions.ts"; + +export abstract class BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + this.session = session; + this.id = data.id; + this.token = data.token; + this.type = data.type; + this.guildId = data.guild_id; + this.channelId = data.channel_id; + this.applicationId = data.application_id; + this.version = data.version; + + // @ts-expect-error: vendor error + const perms = data.app_permissions as string; + + if (perms) { + this.appPermissions = new Permsisions(BigInt(perms)); + } + + if (!data.guild_id) { + this.user = new User(session, data.user!); + } else { + this.member = new Member(session, data.member!, data.guild_id); + } + } + + readonly session: Session; + readonly id: Snowflake; + readonly token: string; + + type: InteractionTypes; + guildId?: Snowflake; + channelId?: Snowflake; + applicationId?: Snowflake; + user?: User; + member?: Member; + appPermissions?: Permsisions; + + readonly version: 1; + + get createdTimestamp() { + return Snowflake.snowflakeToTimestamp(this.id); + } + + get createdAt() { + return new Date(this.createdTimestamp); + } + + isCommand(): this is CommandInteraction { + return this.type === InteractionTypes.ApplicationCommand; + } + + isAutoComplete() { + return this.type === InteractionTypes.ApplicationCommandAutocomplete; + } + + isComponent() { + return this.type === InteractionTypes.MessageComponent; + } + + isPing(): this is PingInteraction { + return this.type === InteractionTypes.Ping; + } + + isModalSubmit() { + return this.type === InteractionTypes.ModalSubmit; + } + + inGuild() { + return !!this.guildId; + } +} + +export default BaseInteraction; diff --git a/structures/interactions/CommandInteraction.ts b/structures/interactions/CommandInteraction.ts new file mode 100644 index 0000000..40bce4e --- /dev/null +++ b/structures/interactions/CommandInteraction.ts @@ -0,0 +1,156 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { + ApplicationCommandTypes, + DiscordInteraction, + DiscordMemberWithUser, + InteractionTypes, +} from "../../vendor/external.ts"; +import type { CreateMessage } from "../Message.ts"; +import type { MessageFlags } from "../../util/shared/flags.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import CommandInteractionOptionResolver from "./CommandInteractionOptionResolver.ts"; +import Attachment from "../Attachment.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; +import Message from "../Message.ts"; +import Role from "../Role.ts"; +import Webhook from "../Webhook.ts"; +import * as Routes from "../../util/Routes.ts"; + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response + */ +export interface InteractionResponse { + type: InteractionResponseTypes; + data?: InteractionApplicationCommandCallbackData; +} + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata + */ +export interface InteractionApplicationCommandCallbackData + extends Pick { + customId?: string; + title?: string; + // components?: MessageComponents; + flags?: MessageFlags; + choices?: ApplicationCommandOptionChoice[]; +} + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice + */ +export interface ApplicationCommandOptionChoice { + name: string; + value: string | number; +} + +export class CommandInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + this.options = new CommandInteractionOptionResolver(data.data!.options ?? []); + + this.resolved = { + users: new Map(), + members: new Map(), + roles: new Map(), + attachments: new Map(), + messages: new Map(), + }; + + if (data.data!.resolved?.users) { + for (const [id, u] of Object.entries(data.data!.resolved.users)) { + this.resolved.users.set(id, new User(session, u)); + } + } + + if (data.data!.resolved?.members && !!super.guildId) { + for (const [id, m] of Object.entries(data.data!.resolved.members)) { + this.resolved.members.set(id, new Member(session, m as DiscordMemberWithUser, super.guildId!)); + } + } + + if (data.data!.resolved?.roles && !!super.guildId) { + for (const [id, r] of Object.entries(data.data!.resolved.roles)) { + this.resolved.roles.set(id, new Role(session, r, super.guildId!)); + } + } + + if (data.data!.resolved?.attachments) { + for (const [id, a] of Object.entries(data.data!.resolved.attachments)) { + this.resolved.attachments.set(id, new Attachment(session, a)); + } + } + + if (data.data!.resolved?.messages) { + for (const [id, m] of Object.entries(data.data!.resolved.messages)) { + this.resolved.messages.set(id, new Message(session, m)); + } + } + } + + override type: InteractionTypes.ApplicationCommand; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + resolved: { + users: Map; + members: Map; + roles: Map; + attachments: Map; + messages: Map; + }; + options: CommandInteractionOptionResolver; + responded = false; + + async sendFollowUp(options: InteractionApplicationCommandCallbackData): Promise { + const message = await Webhook.prototype.execute.call({ + id: this.applicationId!, + token: this.token, + session: this.session, + }, options); + + return message!; + } + + async respond({ type, data: options }: InteractionResponse): Promise { + const data = { + content: options?.content, + custom_id: options?.customId, + file: options?.files, + allowed_mentions: options?.allowedMentions, + flags: options?.flags, + chocies: options?.choices, + embeds: options?.embeds, + title: options?.title, + }; + + if (!this.respond) { + await this.session.rest.sendRequest(this.session.rest, { + url: Routes.INTERACTION_ID_TOKEN(this.id, this.token), + method: "POST", + payload: this.session.rest.createRequestBody(this.session.rest, { + method: "POST", + body: { type, data, file: options?.files }, + headers: { "Authorization": "" }, + }), + }); + + this.responded = true; + return; + } + + return this.sendFollowUp(data); + } +} + +export default CommandInteraction; diff --git a/structures/interactions/CommandInteractionOptionResolver.ts b/structures/interactions/CommandInteractionOptionResolver.ts new file mode 100644 index 0000000..0f5f848 --- /dev/null +++ b/structures/interactions/CommandInteractionOptionResolver.ts @@ -0,0 +1,233 @@ +import type { DiscordInteractionDataOption, DiscordInteractionDataResolved } from "../../vendor/external.ts"; +import { ApplicationCommandOptionTypes } from "../../vendor/external.ts"; + +export function transformOasisInteractionDataOption(o: DiscordInteractionDataOption): CommandInteractionOption { + const output: CommandInteractionOption = { ...o, Otherwise: o.value as string | boolean | number | undefined }; + + switch (o.type) { + case ApplicationCommandOptionTypes.String: + output.String = o.value as string; + break; + case ApplicationCommandOptionTypes.Number: + output.Number = o.value as number; + break; + case ApplicationCommandOptionTypes.Integer: + output.Integer = o.value as number; + break; + case ApplicationCommandOptionTypes.Boolean: + output.Boolean = o.value as boolean; + break; + case ApplicationCommandOptionTypes.Role: + output.Role = BigInt(o.value as string); + break; + case ApplicationCommandOptionTypes.User: + output.User = BigInt(o.value as string); + break; + case ApplicationCommandOptionTypes.Channel: + output.Channel = BigInt(o.value as string); + break; + + case ApplicationCommandOptionTypes.Mentionable: + case ApplicationCommandOptionTypes.SubCommand: + case ApplicationCommandOptionTypes.SubCommandGroup: + default: + output.Otherwise = o.value as string | boolean | number | undefined; + } + + return output; +} + +export interface CommandInteractionOption extends Omit { + Attachment?: string; + Boolean?: boolean; + User?: bigint; + Role?: bigint; + Number?: number; + Integer?: number; + Channel?: bigint; + String?: string; + Mentionable?: string; + Otherwise: string | number | boolean | bigint | undefined; +} + +/** + * Utility class to get the resolved options for a command + * It is really typesafe + * @example const option = ctx.options.getStringOption("name"); + */ +export class CommandInteractionOptionResolver { + #subcommand?: string; + #group?: string; + + hoistedOptions: CommandInteractionOption[]; + resolved?: DiscordInteractionDataResolved; + + constructor(options?: DiscordInteractionDataOption[], resolved?: DiscordInteractionDataResolved) { + this.hoistedOptions = options?.map(transformOasisInteractionDataOption) ?? []; + + // warning: black magic do not edit and thank djs authors + + if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) { + this.#group = this.hoistedOptions[0].name; + this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption); + } + + if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommand) { + this.#subcommand = this.hoistedOptions[0].name; + this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption); + } + + this.resolved = resolved; + } + + private getTypedOption( + name: string | number, + type: ApplicationCommandOptionTypes, + properties: Array, + required: boolean, + ) { + const option = this.get(name, required); + + if (!option) { + return; + } + + if (option.type !== type) { + // pass + } + + if (required === true && properties.every((prop) => typeof option[prop] === "undefined")) { + throw new TypeError(`Properties ${properties.join(", ")} are missing in option ${name}`); + } + + return option; + } + + get(name: string | number, required: true): CommandInteractionOption; + get(name: string | number, required: boolean): CommandInteractionOption | undefined; + get(name: string | number, required?: boolean) { + const option = this.hoistedOptions.find((o) => + typeof name === "number" ? o.name === name.toString() : o.name === name + ); + + if (!option) { + if (required && name in this.hoistedOptions.map((o) => o.name)) { + throw new TypeError("Option marked as required was undefined"); + } + + return; + } + + return option; + } + + /** searches for a string option */ + getString(name: string | number, required: true): string; + getString(name: string | number, required?: boolean): string | undefined; + getString(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.String, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a number option */ + getNumber(name: string | number, required: true): number; + getNumber(name: string | number, required?: boolean): number | undefined; + getNumber(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Number, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searhces for an integer option */ + getInteger(name: string | number, required: true): number; + getInteger(name: string | number, required?: boolean): number | undefined; + getInteger(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Integer, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a boolean option */ + getBoolean(name: string | number, required: true): boolean; + getBoolean(name: string | number, required?: boolean): boolean | undefined; + getBoolean(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Boolean, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a user option */ + getUser(name: string | number, required: true): bigint; + getUser(name: string | number, required?: boolean): bigint | undefined; + getUser(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.User, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a channel option */ + getChannel(name: string | number, required: true): bigint; + getChannel(name: string | number, required?: boolean): bigint | undefined; + getChannel(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Channel, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a mentionable-based option */ + getMentionable(name: string | number, required: true): string; + getMentionable(name: string | number, required?: boolean): string | undefined; + getMentionable(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Mentionable, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a mentionable-based option */ + getRole(name: string | number, required: true): bigint; + getRole(name: string | number, required?: boolean): bigint | undefined; + getRole(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Role, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for an attachment option */ + getAttachment(name: string | number, required: true): string; + getAttachment(name: string | number, required?: boolean): string | undefined; + getAttachment(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Attachment, ["Otherwise"], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for the focused option */ + getFocused(full = false) { + const focusedOption = this.hoistedOptions.find((option) => option.focused); + + if (!focusedOption) { + throw new TypeError("No option found"); + } + + return full ? focusedOption : focusedOption.Otherwise; + } + + getSubCommand(required = true) { + if (required && !this.#subcommand) { + throw new TypeError("Option marked as required was undefined"); + } + + return [this.#subcommand, this.hoistedOptions]; + } + + getSubCommandGroup(required = false) { + if (required && !this.#group) { + throw new TypeError("Option marked as required was undefined"); + } + + return [this.#group, this.hoistedOptions]; + } +} + +export default CommandInteractionOptionResolver; diff --git a/structures/interactions/ComponentInteraction.ts b/structures/interactions/ComponentInteraction.ts new file mode 100644 index 0000000..a85524f --- /dev/null +++ b/structures/interactions/ComponentInteraction.ts @@ -0,0 +1,45 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import Message from "../Message.ts"; + +export class ComponentInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.componentType = data.data!.component_type!; + this.customId = data.data!.custom_id; + this.targetId = data.data!.target_id; + this.values = data.data!.values; + this.message = new Message(session, data.message!); + } + + override type: InteractionTypes.MessageComponent; + componentType: MessageComponentTypes; + customId?: string; + targetId?: Snowflake; + values?: string[]; + message: Message; + responded = false; + + isButton() { + return this.componentType === MessageComponentTypes.Button; + } + + isActionRow() { + return this.componentType === MessageComponentTypes.ActionRow; + } + + isTextInput() { + return this.componentType === MessageComponentTypes.InputText; + } + + isSelectMenu() { + return this.componentType === MessageComponentTypes.SelectMenu; + } +} + +export default ComponentInteraction; diff --git a/structures/interactions/Interaction.ts b/structures/interactions/Interaction.ts deleted file mode 100644 index 5ed2b94..0000000 --- a/structures/interactions/Interaction.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Model } from "../Base.ts"; -import type { Snowflake } from "../../util/Snowflake.ts"; -import type { Session } from "../../session/Session.ts"; -import type { - DiscordInteraction, - DiscordMessage, - FileContent, - InteractionResponseTypes, - InteractionTypes, -} from "../../vendor/external.ts"; -import type { MessageFlags } from "../../util/shared/flags.ts"; -import type { AllowedMentions } from "../Message.ts"; -import User from "../User.ts"; -import Message from "../Message.ts"; -import Member from "../Member.ts"; -import * as Routes from "../../util/Routes.ts"; - -export interface InteractionResponse { - type: InteractionResponseTypes; - data?: InteractionApplicationCommandCallbackData; -} - -export interface InteractionApplicationCommandCallbackData { - content?: string; - tts?: boolean; - allowedMentions?: AllowedMentions; - files?: FileContent[]; - customId?: string; - title?: string; - // components?: Component[]; - flags?: MessageFlags; - choices?: ApplicationCommandOptionChoice[]; -} - -/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */ -export interface ApplicationCommandOptionChoice { - name: string; - value: string | number; -} - -// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc - -export class Interaction implements Model { - constructor(session: Session, data: DiscordInteraction) { - this.session = session; - this.id = data.id; - this.token = data.token; - this.type = data.type; - this.guildId = data.guild_id; - this.channelId = data.channel_id; - this.applicationId = data.application_id; - this.locale = data.locale; - this.data = data.data; - - if (!data.guild_id) { - this.user = new User(session, data.user!); - } else { - this.member = new Member(session, data.member!, data.guild_id); - } - } - - readonly session: Session; - readonly id: Snowflake; - readonly token: string; - - type: InteractionTypes; - guildId?: Snowflake; - channelId?: Snowflake; - applicationId?: Snowflake; - locale?: string; - // deno-lint-ignore no-explicit-any - data: any; - user?: User; - member?: Member; - - async respond({ type, data }: InteractionResponse) { - const toSend = { - tts: data?.tts, - title: data?.title, - flags: data?.flags, - content: data?.content, - choices: data?.choices, - custom_id: data?.customId, - allowed_mentions: data?.allowedMentions - ? { - users: data.allowedMentions.users, - roles: data.allowedMentions.roles, - parse: data.allowedMentions.parse, - replied_user: data.allowedMentions.repliedUser, - } - : { parse: [] }, - }; - - if (this.session.unrepliedInteractions.delete(BigInt(this.id))) { - await this.session.rest.sendRequest( - this.session.rest, - { - url: Routes.INTERACTION_ID_TOKEN(this.id, this.token), - method: "POST", - payload: this.session.rest.createRequestBody(this.session.rest, { - method: "POST", - body: { - type: type, - data: toSend, - file: data?.files, - }, - headers: { - // remove authorization header - Authorization: "", - }, - }), - }, - ); - - return; - } - - const result = await this.session.rest.sendRequest( - this.session.rest, - { - url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token), - method: "POST", - payload: this.session.rest.createRequestBody(this.session.rest, { - method: "POST", - body: { - ...toSend, - file: data?.files, - }, - headers: { - // remove authorization header - Authorization: "", - }, - }), - }, - ); - - return new Message(this.session, result); - } - - inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } { - return !!this.guildId; - } -} - -export default Interaction; diff --git a/structures/interactions/InteractionFactory.ts b/structures/interactions/InteractionFactory.ts new file mode 100644 index 0000000..386d23d --- /dev/null +++ b/structures/interactions/InteractionFactory.ts @@ -0,0 +1,34 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction } from "../../vendor/external.ts"; +import { InteractionTypes } from "../../vendor/external.ts"; +import CommandInteraction from "./CommandInteraction.ts"; +import ComponentInteraction from "./ComponentInteraction.ts"; +import PingInteraction from "./PingInteraction.ts"; +import AutoCompleteInteraction from "./AutoCompleteInteraction.ts"; +import ModalSubmitInteraction from "./ModalSubmitInteraction.ts"; + +export type Interaction = + | CommandInteraction + | ComponentInteraction + | PingInteraction + | AutoCompleteInteraction + | ModalSubmitInteraction; + +export class InteractionFactory { + static from(session: Session, interaction: DiscordInteraction): Interaction { + switch (interaction.type) { + case InteractionTypes.Ping: + return new PingInteraction(session, interaction); + case InteractionTypes.ApplicationCommand: + return new CommandInteraction(session, interaction); + case InteractionTypes.MessageComponent: + return new ComponentInteraction(session, interaction); + case InteractionTypes.ApplicationCommandAutocomplete: + return new AutoCompleteInteraction(session, interaction); + case InteractionTypes.ModalSubmit: + return new ModalSubmitInteraction(session, interaction); + } + } +} + +export default InteractionFactory; diff --git a/structures/interactions/ModalSubmitInteraction.ts b/structures/interactions/ModalSubmitInteraction.ts new file mode 100644 index 0000000..4d5f6df --- /dev/null +++ b/structures/interactions/ModalSubmitInteraction.ts @@ -0,0 +1,54 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { + DiscordInteraction, + DiscordMessageComponents, + InteractionTypes, + MessageComponentTypes, +} from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import Message from "../Message.ts"; + +export class ModalSubmitInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.componentType = data.data!.component_type!; + this.customId = data.data!.custom_id; + this.targetId = data.data!.target_id; + this.values = data.data!.values; + + this.components = data.data?.components?.map(ModalSubmitInteraction.transformComponent); + + if (data.message) { + this.message = new Message(session, data.message); + } + } + + override type: InteractionTypes.MessageComponent; + componentType: MessageComponentTypes; + customId?: string; + targetId?: Snowflake; + values?: string[]; + message?: Message; + components; + + static transformComponent(component: DiscordMessageComponents[number]) { + return { + type: component.type, + components: component.components.map((component) => { + return { + customId: component.custom_id, + value: (component as typeof component & { value: string }).value, + }; + }), + }; + } + + inMessage(): this is ModalSubmitInteraction & { message: Message } { + return !!this.message; + } +} + +export default ModalSubmitInteraction; diff --git a/structures/interactions/PingInteraction.ts b/structures/interactions/PingInteraction.ts new file mode 100644 index 0000000..a335f7c --- /dev/null +++ b/structures/interactions/PingInteraction.ts @@ -0,0 +1,37 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class PingInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + } + + override type: InteractionTypes.Ping; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async pong() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + type: InteractionResponseTypes.Pong, + }, + ); + } +} + +export default PingInteraction; diff --git a/tests/mod.ts b/tests/mod.ts index 03fd262..1cef8bc 100644 --- a/tests/mod.ts +++ b/tests/mod.ts @@ -7,7 +7,8 @@ if (!token) { throw new Error("Please provide a token"); } -const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages; +const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages | + GatewayIntents.GuildMembers | GatewayIntents.GuildBans; const session = new Session({ token, intents }); session.on("ready", (payload) => { diff --git a/util/Routes.ts b/util/Routes.ts index 247cadd..e928d63 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -120,6 +120,10 @@ export interface GetInvite { scheduledEventId?: Snowflake; } +export function GUILDS() { + return `/guilds`; +} + export function INVITE(inviteCode: string, options?: GetInvite) { let url = `/invites/${inviteCode}?`; @@ -140,11 +144,26 @@ export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string) { return `/interactions/${interactionId}/${token}/callback`; } -export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: boolean; threadId?: Snowflake }) { - let url = `/webhooks/${webhookId}/${token}?`; +export function WEBHOOK_MESSAGE(webhookId: Snowflake, token: string, messageId: Snowflake) { + return `/webhooks/${webhookId}/${token}/messages/${messageId}`; +} - if (options?.wait !== undefined) url += `wait=${options.wait}`; - if (options?.threadId) url += `threadId=${options.threadId}`; +export function WEBHOOK_TOKEN(webhookId: Snowflake, token?: string) { + if (!token) return `/webhooks/${webhookId}`; + return `/webhooks/${webhookId}/${token}`; +} + +export interface WebhookOptions { + wait?: boolean; + threadId?: Snowflake; +} + +export function WEBHOOK(webhookId: Snowflake, token: string, options?: WebhookOptions) { + let url = `/webhooks/${webhookId}/${token}`; + + if (options?.wait) url += `?wait=${options.wait}`; + if (options?.threadId) url += `?threadId=${options.threadId}`; + if (options?.wait && options.threadId) url += `?wait=${options.wait}&threadId=${options.threadId}`; return url; } @@ -228,3 +247,81 @@ export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleI export function CHANNEL_WEBHOOKS(channelId: Snowflake) { return `/channels/${channelId}/webhooks`; } + +export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake) { + return `/channels/${channelId}/messages/${messageId}/threads`; +} + +export function THREAD_START_PRIVATE(channelId: Snowflake) { + return `/channels/${channelId}/threads`; +} + +export function THREAD_ACTIVE(guildId: Snowflake) { + return `/guilds/${guildId}/threads/active`; +} + +export interface ListArchivedThreads { + before?: number; + limit?: number; +} + +export function THREAD_ME(channelId: Snowflake) { + return `/channels/${channelId}/thread-members/@me`; +} + +export function THREAD_MEMBERS(channelId: Snowflake) { + return `/channels/${channelId}/thread-members`; +} + +export function THREAD_USER(channelId: Snowflake, userId: Snowflake) { + return `/channels/${channelId}/thread-members/${userId}`; +} + +export function THREAD_ARCHIVED(channelId: Snowflake) { + return `/channels/${channelId}/threads/archived`; +} + +export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchivedThreads) { + let url = `/channels/${channelId}/threads/archived/public?`; + + if (options) { + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; + } + + return url; +} + +export function THREAD_ARCHIVED_PRIVATE(channelId: Snowflake, options?: ListArchivedThreads) { + let url = `/channels/${channelId}/threads/archived/private?`; + + if (options) { + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; + } + + return url; +} + +export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: ListArchivedThreads) { + let url = `/channels/${channelId}/users/@me/threads/archived/private?`; + + if (options) { + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; + } + + return url; +} + +export function FORUM_START(channelId: Snowflake) { + return `/channels/${channelId}/threads?has_message=true`; +} + +export function STAGE_INSTANCES() { + return `/stage-instances`; +} + +export function STAGE_INSTANCE(channelId: Snowflake) { + return `/stage-instances/${channelId}`; +} diff --git a/util/builders.ts b/util/builders.ts new file mode 100644 index 0000000..19c9e8c --- /dev/null +++ b/util/builders.ts @@ -0,0 +1,9 @@ +import { ButtonBuilder, InputTextBuilder, SelectMenuBuilder } from "../mod.ts"; +import { Snowflake } from "./Snowflake.ts"; + +export type AnyComponentBuilder = InputTextBuilder | SelectMenuBuilder | ButtonBuilder; +export type ComponentEmoji = { + id: Snowflake; + name: string; + animated?: boolean; +}; diff --git a/util/permissions.ts b/util/permissions.ts new file mode 100644 index 0000000..e19f5ea --- /dev/null +++ b/util/permissions.ts @@ -0,0 +1,9 @@ +import { Snowflake } from "./Snowflake.ts"; +import { Permissions } from "../structures/Permissions.ts"; + +export interface PermissionsOverwrites { + id: Snowflake; + type: 0 | 1; + allow: Permissions; + deny: Permissions; +} diff --git a/vendor/gateway/shard/deps.ts b/vendor/gateway/shard/deps.ts index 32a9eb3..e2cb039 100644 --- a/vendor/gateway/shard/deps.ts +++ b/vendor/gateway/shard/deps.ts @@ -1 +1 @@ -export { decompress_with as decompressWith } from "https://unpkg.com/@evan/wasm@0.0.94/target/zlib/deno.js"; +export { inflate as decompressWith } from "../../zlib.js"; diff --git a/vendor/gateway/shard/handleMessage.ts b/vendor/gateway/shard/handleMessage.ts index 1bb3e89..cae9839 100644 --- a/vendor/gateway/shard/handleMessage.ts +++ b/vendor/gateway/shard/handleMessage.ts @@ -7,17 +7,14 @@ import { GATEWAY_RATE_LIMIT_RESET_INTERVAL, Shard, ShardState } from "./types.ts const decoder = new TextDecoder(); -export async function handleMessage(shard: Shard, message: MessageEvent): Promise { - message = message.data; +export async function handleMessage(shard: Shard, message_: MessageEvent): Promise { + let message = message_.data; // If message compression is enabled, // Discord might send zlib compressed payloads. if (shard.gatewayConfig.compress && message instanceof Blob) { - message = decompressWith( - new Uint8Array(await message.arrayBuffer()), - 0, - (slice: Uint8Array) => decoder.decode(slice), - ); + message = decoder.decode(decompressWith(new Uint8Array(await message.arrayBuffer()))); + console.log(message); } // Safeguard incase decompression failed to make a string. diff --git a/vendor/types/discord.ts b/vendor/types/discord.ts index 5123cbb..496e294 100644 --- a/vendor/types/discord.ts +++ b/vendor/types/discord.ts @@ -1171,6 +1171,8 @@ export interface DiscordSelectMenuComponent { max_values?: number; /** The choices! Maximum of 25 items. */ options: DiscordSelectOption[]; + /** Whether or not this select menu is disabled */ + disabled?: boolean; } export interface DiscordSelectOption { diff --git a/vendor/zlib.js b/vendor/zlib.js new file mode 100644 index 0000000..636af00 --- /dev/null +++ b/vendor/zlib.js @@ -0,0 +1,1023 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file +// This code was bundled using `deno bundle` and it's not recommended to edit it manually + +function calcAdler32(input) { + let s1 = 1; + let s2 = 0; + const inputLen = input.length; + for(let i = 0; i < inputLen; i++){ + s1 = (s1 + input[i]) % 65521; + s2 = (s1 + s2) % 65521; + } + return (s2 << 16) + s1; +} +const BTYPE = Object.freeze({ + UNCOMPRESSED: 0, + FIXED: 1, + DYNAMIC: 2 +}); +const BLOCK_MAX_BUFFER_LEN = 131072; +const LENGTH_EXTRA_BIT_LEN = [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, +]; +const LENGTH_EXTRA_BIT_BASE = [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 15, + 17, + 19, + 23, + 27, + 31, + 35, + 43, + 51, + 59, + 67, + 83, + 99, + 115, + 131, + 163, + 195, + 227, + 258, +]; +const DISTANCE_EXTRA_BIT_BASE = [ + 1, + 2, + 3, + 4, + 5, + 7, + 9, + 13, + 17, + 25, + 33, + 49, + 65, + 97, + 129, + 193, + 257, + 385, + 513, + 769, + 1025, + 1537, + 2049, + 3073, + 4097, + 6145, + 8193, + 12289, + 16385, + 24577, +]; +const DISTANCE_EXTRA_BIT_LEN = [ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, +]; +const CODELEN_VALUES = [ + 16, + 17, + 18, + 0, + 8, + 7, + 9, + 6, + 10, + 5, + 11, + 4, + 12, + 3, + 13, + 2, + 14, + 1, + 15, +]; +function generateHuffmanTable(codelenValues) { + const codelens = Object.keys(codelenValues); + let codelen = 0; + let codelenMax = 0; + let codelenMin = Number.MAX_SAFE_INTEGER; + codelens.forEach((key)=>{ + codelen = Number(key); + if (codelenMax < codelen) codelenMax = codelen; + if (codelenMin > codelen) codelenMin = codelen; + }); + let code = 0; + let values; + const bitlenTables = {}; + for(let bitlen = codelenMin; bitlen <= codelenMax; bitlen++){ + values = codelenValues[bitlen]; + if (values === undefined) values = []; + values.sort((a, b)=>{ + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + const table = {}; + values.forEach((value)=>{ + table[code] = value; + code++; + }); + bitlenTables[bitlen] = table; + code <<= 1; + } + return bitlenTables; +} +function makeFixedHuffmanCodelenValues() { + const codelenValues = {}; + codelenValues[7] = []; + codelenValues[8] = []; + codelenValues[9] = []; + for(let i = 0; i <= 287; i++){ + i <= 143 ? codelenValues[8].push(i) : i <= 255 ? codelenValues[9].push(i) : i <= 279 ? codelenValues[7].push(i) : codelenValues[8].push(i); + } + return codelenValues; +} +function generateDeflateHuffmanTable(values, maxLength = 15) { + const valuesCount = {}; + for (const value of values){ + if (!valuesCount[value]) { + valuesCount[value] = 1; + } else { + valuesCount[value]++; + } + } + const valuesCountKeys = Object.keys(valuesCount); + let tmpPackages = []; + let tmpPackageIndex = 0; + let packages = []; + if (valuesCountKeys.length === 1) { + packages.push({ + count: valuesCount[0], + simbles: [ + Number(valuesCountKeys[0]) + ] + }); + } else { + for(let i = 0; i < maxLength; i++){ + packages = []; + valuesCountKeys.forEach((value)=>{ + const pack = { + count: valuesCount[Number(value)], + simbles: [ + Number(value) + ] + }; + packages.push(pack); + }); + tmpPackageIndex = 0; + while(tmpPackageIndex + 2 <= tmpPackages.length){ + const pack = { + count: tmpPackages[tmpPackageIndex].count + tmpPackages[tmpPackageIndex + 1].count, + simbles: tmpPackages[tmpPackageIndex].simbles.concat(tmpPackages[tmpPackageIndex + 1].simbles) + }; + packages.push(pack); + tmpPackageIndex += 2; + } + packages = packages.sort((a, b)=>{ + if (a.count < b.count) return -1; + if (a.count > b.count) return 1; + return 0; + }); + if (packages.length % 2 !== 0) { + packages.pop(); + } + tmpPackages = packages; + } + } + const valuesCodelen = {}; + packages.forEach((pack)=>{ + pack.simbles.forEach((symble)=>{ + if (!valuesCodelen[symble]) { + valuesCodelen[symble] = 1; + } else { + valuesCodelen[symble]++; + } + }); + }); + let group; + const valuesCodelenKeys = Object.keys(valuesCodelen); + const codelenGroup = {}; + let code = 0; + let codelen = 3; + let codelenValueMin = Number.MAX_SAFE_INTEGER; + let codelenValueMax = 0; + valuesCodelenKeys.forEach((valuesCodelenKey)=>{ + codelen = valuesCodelen[Number(valuesCodelenKey)]; + if (!codelenGroup[codelen]) { + codelenGroup[codelen] = []; + if (codelenValueMin > codelen) codelenValueMin = codelen; + if (codelenValueMax < codelen) codelenValueMax = codelen; + } + codelenGroup[codelen].push(Number(valuesCodelenKey)); + }); + code = 0; + const table = new Map(); + for(let i1 = codelenValueMin; i1 <= codelenValueMax; i1++){ + group = codelenGroup[i1]; + if (group) { + group = group.sort((a, b)=>{ + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + group.forEach((value)=>{ + table.set(value, { + code, + bitlen: i1 + }); + code++; + }); + } + code <<= 1; + } + return table; +} +function generateLZ77IndexMap(input, startIndex, targetLength) { + const end = startIndex + targetLength - 3; + const indexMap = {}; + for(let i = startIndex; i <= end; i++){ + const indexKey = input[i] << 16 | input[i + 1] << 8 | input[i + 2]; + if (indexMap[indexKey] === undefined) { + indexMap[indexKey] = []; + } + indexMap[indexKey].push(i); + } + return indexMap; +} +function generateLZ77Codes(input, startIndex, targetLength) { + let nowIndex = startIndex; + const endIndex = startIndex + targetLength - 3; + let slideIndexBase = 0; + let repeatLength = 0; + let repeatLengthMax = 0; + let repeatLengthMaxIndex = 0; + let distance = 0; + let repeatLengthCodeValue = 0; + let repeatDistanceCodeValue = 0; + const codeTargetValues = []; + const startIndexMap = {}; + const endIndexMap = {}; + const indexMap = generateLZ77IndexMap(input, startIndex, targetLength); + while(nowIndex <= endIndex){ + const indexKey = input[nowIndex] << 16 | input[nowIndex + 1] << 8 | input[nowIndex + 2]; + const indexes = indexMap[indexKey]; + if (indexes === undefined || indexes.length <= 1) { + codeTargetValues.push([ + input[nowIndex] + ]); + nowIndex++; + continue; + } + slideIndexBase = nowIndex > 0x8000 ? nowIndex - 0x8000 : 0; + repeatLengthMax = 0; + repeatLengthMaxIndex = 0; + let skipindexes = startIndexMap[indexKey] || 0; + while(indexes[skipindexes] < slideIndexBase){ + skipindexes = skipindexes + 1 | 0; + } + startIndexMap[indexKey] = skipindexes; + skipindexes = endIndexMap[indexKey] || 0; + while(indexes[skipindexes] < nowIndex){ + skipindexes = skipindexes + 1 | 0; + } + endIndexMap[indexKey] = skipindexes; + let checkCount = 0; + indexMapLoop: for(let i = endIndexMap[indexKey] - 1, iMin = startIndexMap[indexKey]; iMin <= i; i--){ + if (checkCount >= 128 || repeatLengthMax >= 8 && checkCount >= 16) { + break; + } + checkCount++; + const index = indexes[i]; + for(let j = repeatLengthMax - 1; 0 < j; j--){ + if (input[index + j] !== input[nowIndex + j]) { + continue indexMapLoop; + } + } + repeatLength = 258; + for(let j1 = repeatLengthMax; j1 <= 258; j1++){ + if (input[index + j1] !== input[nowIndex + j1]) { + repeatLength = j1; + break; + } + } + if (repeatLengthMax < repeatLength) { + repeatLengthMax = repeatLength; + repeatLengthMaxIndex = index; + if (258 <= repeatLength) { + break; + } + } + } + if (repeatLengthMax >= 3 && nowIndex + repeatLengthMax <= endIndex) { + distance = nowIndex - repeatLengthMaxIndex; + for(let i1 = 0; i1 < LENGTH_EXTRA_BIT_BASE.length; i1++){ + if (LENGTH_EXTRA_BIT_BASE[i1] > repeatLengthMax) { + break; + } + repeatLengthCodeValue = i1; + } + for(let i2 = 0; i2 < DISTANCE_EXTRA_BIT_BASE.length; i2++){ + if (DISTANCE_EXTRA_BIT_BASE[i2] > distance) { + break; + } + repeatDistanceCodeValue = i2; + } + codeTargetValues.push([ + repeatLengthCodeValue, + repeatDistanceCodeValue, + repeatLengthMax, + distance, + ]); + nowIndex += repeatLengthMax; + } else { + codeTargetValues.push([ + input[nowIndex] + ]); + nowIndex++; + } + } + codeTargetValues.push([ + input[nowIndex] + ]); + codeTargetValues.push([ + input[nowIndex + 1] + ]); + return codeTargetValues; +} +class BitWriteStream { + buffer; + bufferIndex; + nowBits; + nowBitsIndex = 0; + isEnd = false; + constructor(buffer, bufferOffset = 0, bitsOffset = 0){ + this.buffer = buffer; + this.bufferIndex = bufferOffset; + this.nowBits = buffer[bufferOffset]; + this.nowBitsIndex = bitsOffset; + } + write(bit) { + if (this.isEnd) throw new Error("Lack of data length"); + bit <<= this.nowBitsIndex; + this.nowBits += bit; + this.nowBitsIndex++; + if (this.nowBitsIndex >= 8) { + this.buffer[this.bufferIndex] = this.nowBits; + this.bufferIndex++; + this.nowBits = 0; + this.nowBitsIndex = 0; + if (this.buffer.length <= this.bufferIndex) { + this.isEnd = true; + } + } + } + writeRange(value, length) { + let mask = 1; + let bit = 0; + for(let i = 0; i < length; i++){ + bit = value & mask ? 1 : 0; + this.write(bit); + mask <<= 1; + } + } + writeRangeCoded(value, length) { + let mask = 1 << length - 1; + let bit = 0; + for(let i = 0; i < length; i++){ + bit = value & mask ? 1 : 0; + this.write(bit); + mask >>>= 1; + } + } +} +function deflate(input) { + const inputLength = input.length; + const streamHeap = inputLength < 131072 / 2 ? 131072 : inputLength * 2; + const stream = new BitWriteStream(new Uint8Array(streamHeap)); + let processedLength = 0; + let targetLength = 0; + while(true){ + if (processedLength + 131072 >= inputLength) { + targetLength = inputLength - processedLength; + stream.writeRange(1, 1); + } else { + targetLength = BLOCK_MAX_BUFFER_LEN; + stream.writeRange(0, 1); + } + stream.writeRange(BTYPE.DYNAMIC, 2); + deflateDynamicBlock(stream, input, processedLength, targetLength); + processedLength += BLOCK_MAX_BUFFER_LEN; + if (processedLength >= inputLength) { + break; + } + } + if (stream.nowBitsIndex !== 0) { + stream.writeRange(0, 8 - stream.nowBitsIndex); + } + return stream.buffer.subarray(0, stream.bufferIndex); +} +function deflateDynamicBlock(stream, input, startIndex, targetLength) { + const lz77Codes = generateLZ77Codes(input, startIndex, targetLength); + const clCodeValues = [ + 256 + ]; + const distanceCodeValues = []; + let clCodeValueMax = 256; + let distanceCodeValueMax = 0; + for(let i = 0, iMax = lz77Codes.length; i < iMax; i++){ + const values = lz77Codes[i]; + let cl = values[0]; + const distance = values[1]; + if (distance !== undefined) { + cl += 257; + distanceCodeValues.push(distance); + if (distanceCodeValueMax < distance) { + distanceCodeValueMax = distance; + } + } + clCodeValues.push(cl); + if (clCodeValueMax < cl) { + clCodeValueMax = cl; + } + } + const dataHuffmanTables = generateDeflateHuffmanTable(clCodeValues); + const distanceHuffmanTables = generateDeflateHuffmanTable(distanceCodeValues); + const codelens = []; + for(let i1 = 0; i1 <= clCodeValueMax; i1++){ + if (dataHuffmanTables.has(i1)) { + codelens.push(dataHuffmanTables.get(i1).bitlen); + } else { + codelens.push(0); + } + } + const HLIT = codelens.length; + for(let i2 = 0; i2 <= distanceCodeValueMax; i2++){ + if (distanceHuffmanTables.has(i2)) { + codelens.push(distanceHuffmanTables.get(i2).bitlen); + } else { + codelens.push(0); + } + } + const HDIST = codelens.length - HLIT; + const runLengthCodes = []; + const runLengthRepeatCount = []; + let codelen = 0; + let repeatLength = 0; + for(let i3 = 0; i3 < codelens.length; i3++){ + codelen = codelens[i3]; + repeatLength = 1; + while(codelen === codelens[i3 + 1]){ + repeatLength++; + i3++; + if (codelen === 0) { + if (138 <= repeatLength) { + break; + } + } else { + if (6 <= repeatLength) { + break; + } + } + } + if (4 <= repeatLength) { + if (codelen === 0) { + if (11 <= repeatLength) { + runLengthCodes.push(18); + } else { + runLengthCodes.push(17); + } + } else { + runLengthCodes.push(codelen); + runLengthRepeatCount.push(1); + repeatLength--; + runLengthCodes.push(16); + } + runLengthRepeatCount.push(repeatLength); + } else { + for(let j = 0; j < repeatLength; j++){ + runLengthCodes.push(codelen); + runLengthRepeatCount.push(1); + } + } + } + const codelenHuffmanTable = generateDeflateHuffmanTable(runLengthCodes, 7); + let HCLEN = 0; + CODELEN_VALUES.forEach((value, index)=>{ + if (codelenHuffmanTable.has(value)) { + HCLEN = index + 1; + } + }); + stream.writeRange(HLIT - 257, 5); + stream.writeRange(HDIST - 1, 5); + stream.writeRange(HCLEN - 4, 4); + let codelenTableObj; + for(let i4 = 0; i4 < HCLEN; i4++){ + codelenTableObj = codelenHuffmanTable.get(CODELEN_VALUES[i4]); + if (codelenTableObj !== undefined) { + stream.writeRange(codelenTableObj.bitlen, 3); + } else { + stream.writeRange(0, 3); + } + } + runLengthCodes.forEach((value, index)=>{ + codelenTableObj = codelenHuffmanTable.get(value); + if (codelenTableObj !== undefined) { + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); + } else { + throw new Error("Data is corrupted"); + } + if (value === 18) { + stream.writeRange(runLengthRepeatCount[index] - 11, 7); + } else if (value === 17) { + stream.writeRange(runLengthRepeatCount[index] - 3, 3); + } else if (value === 16) { + stream.writeRange(runLengthRepeatCount[index] - 3, 2); + } + }); + for(let i5 = 0, iMax1 = lz77Codes.length; i5 < iMax1; i5++){ + const values1 = lz77Codes[i5]; + const clCodeValue = values1[0]; + const distanceCodeValue = values1[1]; + if (distanceCodeValue !== undefined) { + codelenTableObj = dataHuffmanTables.get(clCodeValue + 257); + if (codelenTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); + if (0 < LENGTH_EXTRA_BIT_LEN[clCodeValue]) { + repeatLength = values1[2]; + stream.writeRange(repeatLength - LENGTH_EXTRA_BIT_BASE[clCodeValue], LENGTH_EXTRA_BIT_LEN[clCodeValue]); + } + const distanceTableObj = distanceHuffmanTables.get(distanceCodeValue); + if (distanceTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(distanceTableObj.code, distanceTableObj.bitlen); + if (0 < DISTANCE_EXTRA_BIT_LEN[distanceCodeValue]) { + const distance1 = values1[3]; + stream.writeRange(distance1 - DISTANCE_EXTRA_BIT_BASE[distanceCodeValue], DISTANCE_EXTRA_BIT_LEN[distanceCodeValue]); + } + } else { + codelenTableObj = dataHuffmanTables.get(clCodeValue); + if (codelenTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); + } + } + codelenTableObj = dataHuffmanTables.get(256); + if (codelenTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); +} +class BitReadStream { + buffer; + bufferIndex; + nowBits; + nowBitsLength = 0; + isEnd = false; + constructor(buffer, offset = 0){ + this.buffer = buffer; + this.bufferIndex = offset; + this.nowBits = buffer[offset]; + this.nowBitsLength = 8; + } + read() { + if (this.isEnd) throw new Error("Lack of data length"); + const bit = this.nowBits & 1; + if (this.nowBitsLength > 1) { + this.nowBitsLength--; + this.nowBits >>= 1; + } else { + this.bufferIndex++; + if (this.bufferIndex < this.buffer.length) { + this.nowBits = this.buffer[this.bufferIndex]; + this.nowBitsLength = 8; + } else { + this.nowBitsLength = 0; + this.isEnd = true; + } + } + return bit; + } + readRange(length) { + while(this.nowBitsLength <= length){ + this.nowBits |= this.buffer[++this.bufferIndex] << this.nowBitsLength; + this.nowBitsLength += 8; + } + const bits = this.nowBits & (1 << length) - 1; + this.nowBits >>>= length; + this.nowBitsLength -= length; + return bits; + } + readRangeCoded(length) { + let bits = 0; + for(let i = 0; i < length; i++){ + bits <<= 1; + bits |= this.read(); + } + return bits; + } +} +class Uint8WriteStream { + index = 0; + buffer; + length; + _extendedSize; + constructor(extendedSize){ + this.buffer = new Uint8Array(extendedSize); + this.length = extendedSize; + this._extendedSize = extendedSize; + } + write(value) { + if (this.length <= this.index) { + this.length += this._extendedSize; + const newBuffer = new Uint8Array(this.length); + const nowSize = this.buffer.length; + for(let i = 0; i < nowSize; i++){ + newBuffer[i] = this.buffer[i]; + } + this.buffer = newBuffer; + } + this.buffer[this.index] = value; + this.index++; + } +} +const FIXED_HUFFMAN_TABLE = generateHuffmanTable(makeFixedHuffmanCodelenValues()); +function inflate(input, offset = 0) { + const buffer = new Uint8WriteStream(input.length * 10); + const stream = new BitReadStream(input, offset); + let bFinal = 0; + let bType = 0; + while(bFinal !== 1){ + bFinal = stream.readRange(1); + bType = stream.readRange(2); + if (bType === BTYPE.UNCOMPRESSED) { + inflateUncompressedBlock(stream, buffer); + } else if (bType === BTYPE.FIXED) { + inflateFixedBlock(stream, buffer); + } else if (bType === BTYPE.DYNAMIC) { + inflateDynamicBlock(stream, buffer); + } else { + throw new Error("Not supported BTYPE : " + bType); + } + if (bFinal === 0 && stream.isEnd) { + throw new Error("Data length is insufficient"); + } + } + return buffer.buffer.subarray(0, buffer.index); +} +function inflateUncompressedBlock(stream, buffer) { + if (stream.nowBitsLength < 8) { + stream.readRange(stream.nowBitsLength); + } + const LEN = stream.readRange(8) | stream.readRange(8) << 8; + const NLEN = stream.readRange(8) | stream.readRange(8) << 8; + if (LEN + NLEN !== 65535) { + throw new Error("Data is corrupted"); + } + for(let i = 0; i < LEN; i++){ + buffer.write(stream.readRange(8)); + } +} +function inflateFixedBlock(stream, buffer) { + const tables = FIXED_HUFFMAN_TABLE; + const codelens = Object.keys(tables); + let codelen = 0; + let codelenMax = 0; + let codelenMin = Number.MAX_SAFE_INTEGER; + codelens.forEach((key)=>{ + codelen = Number(key); + if (codelenMax < codelen) codelenMax = codelen; + if (codelenMin > codelen) codelenMin = codelen; + }); + let code = 0; + let value; + let repeatLengthCode; + let repeatLengthValue; + let repeatLengthExt; + let repeatDistanceCode; + let repeatDistanceValue; + let repeatDistanceExt; + let repeatStartIndex; + while(!stream.isEnd){ + value = undefined; + codelen = codelenMin; + code = stream.readRangeCoded(codelenMin); + while(true){ + value = tables[codelen][code]; + if (value !== undefined) { + break; + } + if (codelenMax <= codelen) { + throw new Error("Data is corrupted"); + } + codelen++; + code <<= 1; + code |= stream.read(); + } + if (value < 256) { + buffer.write(value); + continue; + } + if (value === 256) { + break; + } + repeatLengthCode = value - 257; + repeatLengthValue = LENGTH_EXTRA_BIT_BASE[repeatLengthCode]; + repeatLengthExt = LENGTH_EXTRA_BIT_LEN[repeatLengthCode]; + if (0 < repeatLengthExt) { + repeatLengthValue += stream.readRange(repeatLengthExt); + } + repeatDistanceCode = stream.readRangeCoded(5); + repeatDistanceValue = DISTANCE_EXTRA_BIT_BASE[repeatDistanceCode]; + repeatDistanceExt = DISTANCE_EXTRA_BIT_LEN[repeatDistanceCode]; + if (0 < repeatDistanceExt) { + repeatDistanceValue += stream.readRange(repeatDistanceExt); + } + repeatStartIndex = buffer.index - repeatDistanceValue; + for(let i = 0; i < repeatLengthValue; i++){ + buffer.write(buffer.buffer[repeatStartIndex + i]); + } + } +} +function inflateDynamicBlock(stream, buffer) { + const HLIT = stream.readRange(5) + 257; + const HDIST = stream.readRange(5) + 1; + const HCLEN = stream.readRange(4) + 4; + let codelenCodelen = 0; + const codelenCodelenValues = {}; + for(let i = 0; i < HCLEN; i++){ + codelenCodelen = stream.readRange(3); + if (codelenCodelen === 0) { + continue; + } + if (!codelenCodelenValues[codelenCodelen]) { + codelenCodelenValues[codelenCodelen] = []; + } + codelenCodelenValues[codelenCodelen].push(CODELEN_VALUES[i]); + } + const codelenHuffmanTables = generateHuffmanTable(codelenCodelenValues); + const codelenCodelens = Object.keys(codelenHuffmanTables); + let codelenCodelenMax = 0; + let codelenCodelenMin = Number.MAX_SAFE_INTEGER; + codelenCodelens.forEach((key)=>{ + codelenCodelen = Number(key); + if (codelenCodelenMax < codelenCodelen) codelenCodelenMax = codelenCodelen; + if (codelenCodelenMin > codelenCodelen) codelenCodelenMin = codelenCodelen; + }); + const dataCodelenValues = {}; + const distanceCodelenValues = {}; + let codelenCode = 0; + let runlengthCode; + let repeat = 0; + let codelen = 0; + const codesNumber = HLIT + HDIST; + for(let i1 = 0; i1 < codesNumber;){ + runlengthCode = undefined; + codelenCodelen = codelenCodelenMin; + codelenCode = stream.readRangeCoded(codelenCodelenMin); + while(true){ + runlengthCode = codelenHuffmanTables[codelenCodelen][codelenCode]; + if (runlengthCode !== undefined) { + break; + } + if (codelenCodelenMax <= codelenCodelen) { + throw new Error("Data is corrupted"); + } + codelenCodelen++; + codelenCode <<= 1; + codelenCode |= stream.read(); + } + if (runlengthCode === 16) { + repeat = 3 + stream.readRange(2); + } else if (runlengthCode === 17) { + repeat = 3 + stream.readRange(3); + codelen = 0; + } else if (runlengthCode === 18) { + repeat = 11 + stream.readRange(7); + codelen = 0; + } else { + repeat = 1; + codelen = runlengthCode; + } + if (codelen <= 0) { + i1 += repeat; + } else { + while(repeat){ + if (i1 < HLIT) { + if (!dataCodelenValues[codelen]) { + dataCodelenValues[codelen] = []; + } + dataCodelenValues[codelen].push(i1++); + } else { + if (!distanceCodelenValues[codelen]) { + distanceCodelenValues[codelen] = []; + } + distanceCodelenValues[codelen].push((i1++) - HLIT); + } + repeat--; + } + } + } + const dataHuffmanTables = generateHuffmanTable(dataCodelenValues); + const distanceHuffmanTables = generateHuffmanTable(distanceCodelenValues); + const dataCodelens = Object.keys(dataHuffmanTables); + let dataCodelen = 0; + let dataCodelenMax = 0; + let dataCodelenMin = Number.MAX_SAFE_INTEGER; + dataCodelens.forEach((key)=>{ + dataCodelen = Number(key); + if (dataCodelenMax < dataCodelen) dataCodelenMax = dataCodelen; + if (dataCodelenMin > dataCodelen) dataCodelenMin = dataCodelen; + }); + const distanceCodelens = Object.keys(distanceHuffmanTables); + let distanceCodelen = 0; + let distanceCodelenMax = 0; + let distanceCodelenMin = Number.MAX_SAFE_INTEGER; + distanceCodelens.forEach((key)=>{ + distanceCodelen = Number(key); + if (distanceCodelenMax < distanceCodelen) { + distanceCodelenMax = distanceCodelen; + } + if (distanceCodelenMin > distanceCodelen) { + distanceCodelenMin = distanceCodelen; + } + }); + let dataCode = 0; + let data; + let repeatLengthCode; + let repeatLengthValue; + let repeatLengthExt; + let repeatDistanceCode; + let repeatDistanceValue; + let repeatDistanceExt; + let repeatDistanceCodeCodelen; + let repeatDistanceCodeCode; + let repeatStartIndex; + while(!stream.isEnd){ + data = undefined; + dataCodelen = dataCodelenMin; + dataCode = stream.readRangeCoded(dataCodelenMin); + while(true){ + data = dataHuffmanTables[dataCodelen][dataCode]; + if (data !== undefined) { + break; + } + if (dataCodelenMax <= dataCodelen) { + throw new Error("Data is corrupted"); + } + dataCodelen++; + dataCode <<= 1; + dataCode |= stream.read(); + } + if (data < 256) { + buffer.write(data); + continue; + } + if (data === 256) { + break; + } + repeatLengthCode = data - 257; + repeatLengthValue = LENGTH_EXTRA_BIT_BASE[repeatLengthCode]; + repeatLengthExt = LENGTH_EXTRA_BIT_LEN[repeatLengthCode]; + if (0 < repeatLengthExt) { + repeatLengthValue += stream.readRange(repeatLengthExt); + } + repeatDistanceCode = undefined; + repeatDistanceCodeCodelen = distanceCodelenMin; + repeatDistanceCodeCode = stream.readRangeCoded(distanceCodelenMin); + while(true){ + repeatDistanceCode = distanceHuffmanTables[repeatDistanceCodeCodelen][repeatDistanceCodeCode]; + if (repeatDistanceCode !== undefined) { + break; + } + if (distanceCodelenMax <= repeatDistanceCodeCodelen) { + throw new Error("Data is corrupted"); + } + repeatDistanceCodeCodelen++; + repeatDistanceCodeCode <<= 1; + repeatDistanceCodeCode |= stream.read(); + } + repeatDistanceValue = DISTANCE_EXTRA_BIT_BASE[repeatDistanceCode]; + repeatDistanceExt = DISTANCE_EXTRA_BIT_LEN[repeatDistanceCode]; + if (0 < repeatDistanceExt) { + repeatDistanceValue += stream.readRange(repeatDistanceExt); + } + repeatStartIndex = buffer.index - repeatDistanceValue; + for(let i2 = 0; i2 < repeatLengthValue; i2++){ + buffer.write(buffer.buffer[repeatStartIndex + i2]); + } + } +} +function inflate1(input) { + const stream = new BitReadStream(input); + const CM = stream.readRange(4); + if (CM !== 8) { + throw new Error("Not compressed by deflate"); + } + stream.readRange(4); + stream.readRange(5); + stream.readRange(1); + stream.readRange(2); + return inflate(input, 2); +} +function deflate1(input) { + const data = deflate(input); + const CMF = new BitWriteStream(new Uint8Array(1)); + CMF.writeRange(8, 4); + CMF.writeRange(7, 4); + const FLG = new BitWriteStream(new Uint8Array(1)); + FLG.writeRange(28, 5); + FLG.writeRange(0, 1); + FLG.writeRange(2, 2); + const ADLER32 = new BitWriteStream(new Uint8Array(4)); + const adler32 = calcAdler32(input); + ADLER32.writeRange(adler32 >>> 24, 8); + ADLER32.writeRange(adler32 >>> 16 & 0xff, 8); + ADLER32.writeRange(adler32 >>> 8 & 0xff, 8); + ADLER32.writeRange(adler32 & 0xff, 8); + const output = new Uint8Array(data.length + 6); + output.set(CMF.buffer); + output.set(FLG.buffer, 1); + output.set(data, 2); + output.set(ADLER32.buffer, output.length - 4); + return output; +} +export { inflate1 as inflate }; +export { deflate1 as deflate };