Merge branch 'main' of https://github.com/deno-biscuit/biscuit into add-n128

This commit is contained in:
Nicolás Serna 2022-07-07 23:56:07 -03:00
commit 263e8c53fb
45 changed files with 4025 additions and 296 deletions

View File

@ -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<T> = (...args: [Session, number, T]) => void;
export type Handler<T extends unknown[]> = (...args: T) => unknown;
@ -38,6 +58,25 @@ export const MESSAGE_CREATE: RawHandler<DiscordMessage> = (session, _shardId, me
};
export const MESSAGE_UPDATE: RawHandler<DiscordMessage> = (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<DiscordMessageDelete> = (session, _shard
session.emit("messageDelete", { id, channelId: channel_id, guildId: guild_id });
};
export const GUILD_CREATE: RawHandler<DiscordGuild> = (session, _shardId, guild) => {
session.emit("guildCreate", new Guild(session, guild));
};
export const GUILD_DELETE: RawHandler<DiscordGuild> = (session, _shardId, guild) => {
session.emit("guildDelete", { id: guild.id, unavailable: true });
};
export const GUILD_MEMBER_ADD: RawHandler<DiscordGuildMemberAdd> = (session, _shardId, member) => {
session.emit("guildMemberAdd", new Member(session, member, member.guild_id));
};
@ -57,13 +104,32 @@ export const GUILD_MEMBER_REMOVE: RawHandler<DiscordGuildMemberRemove> = (sessio
session.emit("guildMemberRemove", new User(session, member.user), member.guild_id);
};
export const GUILD_BAN_ADD: RawHandler<DiscordGuildBanAddRemove> = (session, _shardId, data) => {
session.emit("guildBanAdd", { guildId: data.guild_id, user: data.user });
};
export const GUILD_BAN_REMOVE: RawHandler<DiscordGuildBanAddRemove> = (session, _shardId, data) => {
session.emit("guildBanRemove", { guildId: data.guild_id, user: data.user });
};
export const GUILD_EMOJIS_UPDATE: RawHandler<DiscordGuildEmojisUpdate> = (session, _shardId, data) => {
session.emit("guildEmojisUpdate", { guildId: data.guild_id, emojis: data.emojis });
};
export const GUILD_ROLE_CREATE: RawHandler<DiscordGuildRoleCreate> = (session, _shardId, data) => {
session.emit("guildRoleCreate", { guildId: data.guild_id, role: data.role });
};
export const GUILD_ROLE_UPDATE: RawHandler<DiscordGuildRoleUpdate> = (session, _shardId, data) => {
session.emit("guildRoleUpdate", { guildId: data.guild_id, role: data.role });
};
export const GUILD_ROLE_DELETE: RawHandler<DiscordGuildRoleDelete> = (session, _shardId, data) => {
session.emit("guildRoleDelete", { guildId: data.guild_id, roleId: data.role_id });
};
export const INTERACTION_CREATE: RawHandler<DiscordInteraction> = (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<DiscordChannel> = (session, _shardId, channel) => {
@ -103,10 +169,7 @@ export const THREAD_LIST_SYNC: RawHandler<DiscordThreadListSync> = (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<DiscordChannelPinsUpdate> = (sessio
});
};
export const WEBHOOKS_UPDATE: RawHandler<DiscordWebhookUpdate> = (session, _shardId, webhook) => {
session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id });
};
export const INTEGRATION_CREATE: RawHandler<DiscordIntegration & { guildId?: Snowflake }> = (
session,
_shardId,
payload,
) => {
session.emit("integrationCreate", new Integration(session, payload));
};
export const INTEGRATION_UPDATE: RawHandler<DiscordIntegration & { guildId?: Snowflake }> = (
session,
_shardId,
payload,
) => {
session.emit("integrationCreate", new Integration(session, payload));
};
export const INTEGRATION_DELETE: RawHandler<DiscordIntegrationDelete> = (session, _shardId, payload) => {
session.emit("integrationDelete", {
id: payload.id,
guildId: payload.guild_id,
applicationId: payload.application_id,
});
};
export const MESSAGE_REACTION_ADD: RawHandler<DiscordMessageReactionAdd> = (session, _shardId, reaction) => {
session.emit("messageReactionAdd", null);
};
export const MESSAGE_REACTION_REMOVE: RawHandler<DiscordMessageReactionRemove> = (session, _shardId, reaction) => {
session.emit("messageReactionRemove", null);
};
export const MESSAGE_REACTION_REMOVE_ALL: RawHandler<DiscordMessageReactionRemoveAll> = (
session,
_shardId,
reaction,
) => {
session.emit("messageReactionRemoveAll", null);
};
export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler<DiscordMessageReactionRemoveEmoji> = (
session,
_shardId,
reaction,
) => {
session.emit("messageReactionRemoveEmoji", null);
};
export const raw: RawHandler<unknown> = (session, shardId, data) => {
session.emit("raw", data, shardId);
};
@ -126,23 +241,42 @@ export interface Ready extends Omit<DiscordReady, "user"> {
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<Message>]>;
"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 }]>;
}

18
mod.ts
View File

@ -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";

6
scripts.ts Normal file
View File

@ -0,0 +1,6 @@
export default {
scripts: {
fmt: "deno fmt",
check: "deno check mod.ts",
},
};

View File

@ -39,8 +39,6 @@ export class Session extends EventEmitter {
rest: ReturnType<typeof createRestManager>;
gateway: ReturnType<typeof createGatewayManager>;
unrepliedInteractions: Set<bigint> = 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;
}

77
structures/Integration.ts Normal file
View File

@ -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;

View File

@ -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<Member>[];
/** 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<GuildChannel>;
stageInstance?: InviteStageInstance;
guildScheduledEvent?: InviteScheduledEvent;
// TODO: create Application structure
// targetApplication?: Partial<Application>
async delete(): Promise<Invite> {
await Guild.prototype.deleteInvite.call(this.guild, this.code);

View File

@ -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() {

View File

@ -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<User>; webhook: WebhookAuthor; member: undefined } {
return !!this.webhook;
}
}
export default Message;

View File

@ -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;

View File

@ -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<DiscordStageInstance>(
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<undefined>(this.session.rest, "DELETE", Routes.STAGE_INSTANCE(this.id));
}
}
export default StageInstance;

View File

@ -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<undefined>(
this.session.rest,
"DELETE",
Routes.THREAD_USER(this.id, memberId),
);
}
async fetchMember(memberId: Snowflake = this.session.botId) {
const member = await this.session.rest.runMethod<DiscordThreadMember>(
this.session.rest,
"GET",
Routes.THREAD_USER(this.id, memberId),
);
return new ThreadMember(this.session, member);
}
}
export default ThreadMember;

View File

@ -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<DiscordMessage>(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<DiscordWebhook>(
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<DiscordMessage>(
this.session.rest,
"GET",
Routes.WEBHOOK_MESSAGE(this.id, this.token, messageId),
);
return new Message(this.session, message);
}
}
export default Webhook;

View File

@ -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;
}
}

View File

@ -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 };
}
}

View File

@ -0,0 +1,29 @@
import { MessageComponentTypes } from "../../vendor/external.ts";
import { AnyComponentBuilder } from "../../util/builders.ts";
export class ActionRowBuilder<T extends AnyComponentBuilder> {
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()) };
}
}

View File

@ -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 };
}
}

View File

@ -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()) };
}
}

View File

@ -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 };
}
}

691
structures/channels.ts Normal file
View File

@ -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<keyof TextChannel> = []) {
const methods: Array<keyof TextChannel> = [
"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<Message[] | []> {
const messages = await this.session.rest.runMethod<DiscordMessage[]>(
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<DiscordInvite>(
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<Message[] | []> {
if (options?.limit! > 100) throw Error("Values must be between 0-100");
const messages = await this.session.rest.runMethod<DiscordMessage[]>(
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<undefined>(
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<DiscordWebhook>(
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<ChannelTypes, ChannelTypes.DM | ChannelTypes.GroupDm>;
guildId: Snowflake;
topic?: string;
position?: number;
parentId?: Snowflake;
async fetchInvites(): Promise<Invite[]> {
const invites = await this.session.rest.runMethod<DiscordInviteMetadata[]>(
this.session.rest,
"GET",
Routes.CHANNEL_INVITES(this.id),
);
return invites.map((invite) => new Invite(this.session, invite));
}
async edit(options: EditNewsChannelOptions): Promise<NewsChannel>;
async edit(options: EditStageChannelOptions): Promise<StageChannel>;
async edit(options: EditVoiceChannelOptions): Promise<VoiceChannel>;
async edit(
options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions,
): Promise<Channel> {
const channel = await this.session.rest.runMethod<DiscordChannel>(
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<DiscordListArchivedThreads>(
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<Snowflake, ThreadChannel>,
members: Object.fromEntries(
members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]),
) as Record<Snowflake, ThreadMember>,
hasMore: has_more,
};
}
async createThread(options: ThreadCreateOptions): Promise<ThreadChannel> {
const thread = await this.session.rest.runMethod<DiscordChannel>(
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<DiscordChannel>(
this.session.rest,
"DELETE",
Routes.CHANNEL(this.id),
);
return new DMChannel(this.session, channel);
}
}
TextChannel.applyTo(DMChannel);
export interface DMChannel extends Omit<TextChannel, "type">, Omit<BaseChannel, "type"> {}
/** 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<Message> {
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<undefined>(
this.session.rest,
"PUT",
Routes.THREAD_ME(this.id),
);
}
async addToThread(guildMemberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.THREAD_USER(this.id, guildMemberId),
);
}
async leaveToThread(guildMemberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
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<ThreadMember[]> {
const members = await this.session.rest.runMethod<DiscordThreadMember[]>(
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<GuildChannel, "type">, Omit<TextChannel, "type"> {}
/** 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");
}
}
}

View File

@ -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}>`;
}

View File

@ -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;

View File

@ -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);

View File

@ -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<NewsChannel>;
async edit(options: EditStageChannelOptions): Promise<StageChannel>;
async edit(options: EditVoiceChannelOptions): Promise<VoiceChannel>;
async edit(
options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions,
): Promise<Channel> {
const channel = await this.session.rest.runMethod<DiscordChannel>(
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<DiscordListArchivedThreads>(
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<Snowflake, ThreadChannel>,
members: Object.fromEntries(
members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]),
) as Record<Snowflake, ThreadMember>,
hasMore: has_more,
};
}
async createThread(options: ThreadCreateOptions): Promise<ThreadChannel> {
const thread = await this.session.rest.runMethod<DiscordChannel>(
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<DiscordChannel>(
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;

View File

@ -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;

View File

@ -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<keyof TextChannel> = []) {
const methods: Array<keyof TextChannel> = [
"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<Message[] | []> {

View File

@ -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<undefined>(
this.session.rest,
"PUT",
Routes.THREAD_ME(this.id),
);
}
async addToThread(guildMemberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.THREAD_USER(this.id, guildMemberId),
);
}
async leaveToThread(guildMemberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
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<ThreadMember[]> {
const members = await this.session.rest.runMethod<DiscordThreadMember[]>(
this.session.rest,
"GET",
Routes.THREAD_MEMBERS(this.id),
);
return members.map((threadMember) => new ThreadMember(this.session, threadMember));
}
}
TextChannel.applyTo(ThreadChannel);

View File

@ -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;

View File

@ -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<Partial<DiscordOverwrite>, "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<GuildCreateOptions, "roles" | "channels"> {
ownerId?: Snowflake;
splashURL?: string;
bannerURL?: string;
discoverySplashURL?: string;
features?: GuildFeatures[];
rulesChannelId?: Snowflake;
description?: string;
premiumProgressBarEnabled?: boolean;
}
export interface GuildEditOptions extends Omit<GuildCreateOptions, "roles" | "channels"> {
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<DiscordListActiveThreads>(
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<Snowflake, ThreadChannel>,
members: Object.fromEntries(
members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]),
) as Record<Snowflake, ThreadMember>,
};
}
/***
* Makes the bot leave the guild
*/
async leave() {
}
/***
* Deletes a guild
*/
async delete() {
await this.session.rest.runMethod<undefined>(
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<DiscordGuild>(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<DiscordGuild>(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;

View File

@ -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<undefined>(
this.session.rest,
"POST",
Routes.INTERACTION_ID_TOKEN(this.id, this.token),
{
data: { choices },
type: InteractionResponseTypes.ApplicationCommandAutocompleteResult,
},
);
}
}
export default AutoCompleteInteraction;

View File

@ -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;

View File

@ -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<CreateMessage, "allowedMentions" | "content" | "embeds" | "files"> {
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<Snowflake, User>;
members: Map<Snowflake, Member>;
roles: Map<Snowflake, Role>;
attachments: Map<Snowflake, Attachment>;
messages: Map<Snowflake, Message>;
};
options: CommandInteractionOptionResolver;
responded = false;
async sendFollowUp(options: InteractionApplicationCommandCallbackData): Promise<Message> {
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<Message | undefined> {
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<undefined>(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;

View File

@ -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<DiscordInteractionDataOption, "value"> {
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<keyof CommandInteractionOption>,
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;

View File

@ -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;

View File

@ -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<undefined>(
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<DiscordMessage>(
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;

View File

@ -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;

View File

@ -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;

View File

@ -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<undefined>(
this.session.rest,
"POST",
Routes.INTERACTION_ID_TOKEN(this.id, this.token),
{
type: InteractionResponseTypes.Pong,
},
);
}
}
export default PingInteraction;

View File

@ -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) => {

View File

@ -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}`;
}

9
util/builders.ts Normal file
View File

@ -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;
};

9
util/permissions.ts Normal file
View File

@ -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;
}

View File

@ -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";

View File

@ -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<any>): Promise<void> {
message = message.data;
export async function handleMessage(shard: Shard, message_: MessageEvent<any>): Promise<void> {
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.

View File

@ -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 {

1023
vendor/zlib.js vendored Normal file

File diff suppressed because it is too large Load Diff