mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-02 04:56:07 +00:00
Merge branch 'main' of https://github.com/deno-biscuit/biscuit into add-n128
This commit is contained in:
commit
263e8c53fb
@ -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
18
mod.ts
@ -13,12 +13,14 @@ export * from "./structures/WelcomeChannel.ts";
|
||||
export * from "./structures/WelcomeScreen.ts";
|
||||
|
||||
export * from "./structures/channels/BaseChannel.ts";
|
||||
export * from "./structures/channels/BaseVoiceChannel.ts";
|
||||
export * from "./structures/channels/ChannelFactory.ts";
|
||||
export * from "./structures/channels/DMChannel.ts";
|
||||
export * from "./structures/channels/GuildChannel.ts";
|
||||
export * from "./structures/channels/NewsChannel.ts";
|
||||
export * from "./structures/channels/TextChannel.ts";
|
||||
export * from "./structures/channels/ThreadChannel.ts";
|
||||
export * from "./structures/channels/StageChannel.ts";
|
||||
export * from "./structures/channels/VoiceChannel.ts";
|
||||
|
||||
export * from "./structures/components/ActionRowComponent.ts";
|
||||
@ -33,7 +35,21 @@ export * from "./structures/guilds/BaseGuild.ts";
|
||||
export * from "./structures/guilds/Guild.ts";
|
||||
export * from "./structures/guilds/InviteGuild.ts";
|
||||
|
||||
export * from "./structures/interactions/Interaction.ts";
|
||||
export * from "./structures/builders/EmbedBuilder.ts";
|
||||
export * from "./structures/builders/InputTextComponentBuilder.ts";
|
||||
export * from "./structures/builders/MessageActionRow.ts";
|
||||
export * from "./structures/builders/MessageButton.ts";
|
||||
export * from "./structures/builders/MessageSelectMenu.ts";
|
||||
export * from "./structures/builders/SelectMenuOptionBuilder.ts";
|
||||
|
||||
export * from "./structures/interactions/AutoCompleteInteraction.ts";
|
||||
export * from "./structures/interactions/BaseInteraction.ts";
|
||||
export * from "./structures/interactions/CommandInteraction.ts";
|
||||
export * from "./structures/interactions/CommandInteractionOptionResolver.ts";
|
||||
export * from "./structures/interactions/ComponentInteraction.ts";
|
||||
export * from "./structures/interactions/InteractionFactory.ts";
|
||||
export * from "./structures/interactions/ModalSubmitInteraction.ts";
|
||||
export * from "./structures/interactions/PingInteraction.ts";
|
||||
|
||||
export * from "./session/Session.ts";
|
||||
|
||||
|
6
scripts.ts
Normal file
6
scripts.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
scripts: {
|
||||
fmt: "deno fmt",
|
||||
check: "deno check mod.ts",
|
||||
},
|
||||
};
|
@ -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
77
structures/Integration.ts
Normal 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;
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
23
structures/MessageReaction.ts
Normal file
23
structures/MessageReaction.ts
Normal 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;
|
61
structures/StageInstance.ts
Normal file
61
structures/StageInstance.ts
Normal 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;
|
47
structures/ThreadMember.ts
Normal file
47
structures/ThreadMember.ts
Normal 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;
|
@ -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;
|
||||
|
109
structures/builders/EmbedBuilder.ts
Normal file
109
structures/builders/EmbedBuilder.ts
Normal 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;
|
||||
}
|
||||
}
|
49
structures/builders/InputTextComponentBuilder.ts
Normal file
49
structures/builders/InputTextComponentBuilder.ts
Normal 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 };
|
||||
}
|
||||
}
|
29
structures/builders/MessageActionRow.ts
Normal file
29
structures/builders/MessageActionRow.ts
Normal 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()) };
|
||||
}
|
||||
}
|
44
structures/builders/MessageButton.ts
Normal file
44
structures/builders/MessageButton.ts
Normal 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 };
|
||||
}
|
||||
}
|
53
structures/builders/MessageSelectMenu.ts
Normal file
53
structures/builders/MessageSelectMenu.ts
Normal 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()) };
|
||||
}
|
||||
}
|
38
structures/builders/SelectMenuOptionBuilder.ts
Normal file
38
structures/builders/SelectMenuOptionBuilder.ts
Normal 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
691
structures/channels.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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}>`;
|
||||
}
|
||||
|
62
structures/channels/BaseVoiceChannel.ts
Normal file
62
structures/channels/BaseVoiceChannel.ts
Normal 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;
|
@ -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);
|
||||
|
@ -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;
|
||||
|
16
structures/channels/StageChannel.ts
Normal file
16
structures/channels/StageChannel.ts
Normal 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;
|
@ -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[] | []> {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
39
structures/interactions/AutoCompleteInteraction.ts
Normal file
39
structures/interactions/AutoCompleteInteraction.ts
Normal 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;
|
84
structures/interactions/BaseInteraction.ts
Normal file
84
structures/interactions/BaseInteraction.ts
Normal 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;
|
156
structures/interactions/CommandInteraction.ts
Normal file
156
structures/interactions/CommandInteraction.ts
Normal 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;
|
233
structures/interactions/CommandInteractionOptionResolver.ts
Normal file
233
structures/interactions/CommandInteractionOptionResolver.ts
Normal 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;
|
45
structures/interactions/ComponentInteraction.ts
Normal file
45
structures/interactions/ComponentInteraction.ts
Normal 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;
|
@ -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;
|
34
structures/interactions/InteractionFactory.ts
Normal file
34
structures/interactions/InteractionFactory.ts
Normal 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;
|
54
structures/interactions/ModalSubmitInteraction.ts
Normal file
54
structures/interactions/ModalSubmitInteraction.ts
Normal 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;
|
37
structures/interactions/PingInteraction.ts
Normal file
37
structures/interactions/PingInteraction.ts
Normal 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;
|
@ -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) => {
|
||||
|
105
util/Routes.ts
105
util/Routes.ts
@ -120,6 +120,10 @@ export interface GetInvite {
|
||||
scheduledEventId?: Snowflake;
|
||||
}
|
||||
|
||||
export function GUILDS() {
|
||||
return `/guilds`;
|
||||
}
|
||||
|
||||
export function INVITE(inviteCode: string, options?: GetInvite) {
|
||||
let url = `/invites/${inviteCode}?`;
|
||||
|
||||
@ -140,11 +144,26 @@ export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string) {
|
||||
return `/interactions/${interactionId}/${token}/callback`;
|
||||
}
|
||||
|
||||
export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: boolean; threadId?: Snowflake }) {
|
||||
let url = `/webhooks/${webhookId}/${token}?`;
|
||||
export function WEBHOOK_MESSAGE(webhookId: Snowflake, token: string, messageId: Snowflake) {
|
||||
return `/webhooks/${webhookId}/${token}/messages/${messageId}`;
|
||||
}
|
||||
|
||||
if (options?.wait !== undefined) url += `wait=${options.wait}`;
|
||||
if (options?.threadId) url += `threadId=${options.threadId}`;
|
||||
export function WEBHOOK_TOKEN(webhookId: Snowflake, token?: string) {
|
||||
if (!token) return `/webhooks/${webhookId}`;
|
||||
return `/webhooks/${webhookId}/${token}`;
|
||||
}
|
||||
|
||||
export interface WebhookOptions {
|
||||
wait?: boolean;
|
||||
threadId?: Snowflake;
|
||||
}
|
||||
|
||||
export function WEBHOOK(webhookId: Snowflake, token: string, options?: WebhookOptions) {
|
||||
let url = `/webhooks/${webhookId}/${token}`;
|
||||
|
||||
if (options?.wait) url += `?wait=${options.wait}`;
|
||||
if (options?.threadId) url += `?threadId=${options.threadId}`;
|
||||
if (options?.wait && options.threadId) url += `?wait=${options.wait}&threadId=${options.threadId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
@ -228,3 +247,81 @@ export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleI
|
||||
export function CHANNEL_WEBHOOKS(channelId: Snowflake) {
|
||||
return `/channels/${channelId}/webhooks`;
|
||||
}
|
||||
|
||||
export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake) {
|
||||
return `/channels/${channelId}/messages/${messageId}/threads`;
|
||||
}
|
||||
|
||||
export function THREAD_START_PRIVATE(channelId: Snowflake) {
|
||||
return `/channels/${channelId}/threads`;
|
||||
}
|
||||
|
||||
export function THREAD_ACTIVE(guildId: Snowflake) {
|
||||
return `/guilds/${guildId}/threads/active`;
|
||||
}
|
||||
|
||||
export interface ListArchivedThreads {
|
||||
before?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export function THREAD_ME(channelId: Snowflake) {
|
||||
return `/channels/${channelId}/thread-members/@me`;
|
||||
}
|
||||
|
||||
export function THREAD_MEMBERS(channelId: Snowflake) {
|
||||
return `/channels/${channelId}/thread-members`;
|
||||
}
|
||||
|
||||
export function THREAD_USER(channelId: Snowflake, userId: Snowflake) {
|
||||
return `/channels/${channelId}/thread-members/${userId}`;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED(channelId: Snowflake) {
|
||||
return `/channels/${channelId}/threads/archived`;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchivedThreads) {
|
||||
let url = `/channels/${channelId}/threads/archived/public?`;
|
||||
|
||||
if (options) {
|
||||
if (options.before) url += `before=${new Date(options.before).toISOString()}`;
|
||||
if (options.limit) url += `&limit=${options.limit}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED_PRIVATE(channelId: Snowflake, options?: ListArchivedThreads) {
|
||||
let url = `/channels/${channelId}/threads/archived/private?`;
|
||||
|
||||
if (options) {
|
||||
if (options.before) url += `before=${new Date(options.before).toISOString()}`;
|
||||
if (options.limit) url += `&limit=${options.limit}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: ListArchivedThreads) {
|
||||
let url = `/channels/${channelId}/users/@me/threads/archived/private?`;
|
||||
|
||||
if (options) {
|
||||
if (options.before) url += `before=${new Date(options.before).toISOString()}`;
|
||||
if (options.limit) url += `&limit=${options.limit}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function FORUM_START(channelId: Snowflake) {
|
||||
return `/channels/${channelId}/threads?has_message=true`;
|
||||
}
|
||||
|
||||
export function STAGE_INSTANCES() {
|
||||
return `/stage-instances`;
|
||||
}
|
||||
|
||||
export function STAGE_INSTANCE(channelId: Snowflake) {
|
||||
return `/stage-instances/${channelId}`;
|
||||
}
|
||||
|
9
util/builders.ts
Normal file
9
util/builders.ts
Normal 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
9
util/permissions.ts
Normal 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;
|
||||
}
|
2
vendor/gateway/shard/deps.ts
vendored
2
vendor/gateway/shard/deps.ts
vendored
@ -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";
|
||||
|
11
vendor/gateway/shard/handleMessage.ts
vendored
11
vendor/gateway/shard/handleMessage.ts
vendored
@ -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.
|
||||
|
2
vendor/types/discord.ts
vendored
2
vendor/types/discord.ts
vendored
@ -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
1023
vendor/zlib.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user