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

This commit is contained in:
Nicolás Serna 2022-07-01 21:17:07 -03:00
commit fd15e1a5f4
19 changed files with 977 additions and 118 deletions

View File

@ -1,39 +1,78 @@
import type { DiscordMessage, DiscordReady } from "../vendor/external.ts";
import type {
DiscordGuildMemberAdd,
DiscordGuildMemberRemove,
DiscordGuildMemberUpdate,
DiscordInteraction,
DiscordMessage,
DiscordMessageDelete,
DiscordReady,
} from "../vendor/external.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import Member from "../structures/Member.ts";
import Message from "../structures/Message.ts";
import User from "../structures/User.ts";
import Interaction from "../structures/Interaction.ts";
export type RawHandler<T extends unknown[]> = (...args: [Session, number, ...T]) => void;
export type RawHandler<T> = (...args: [Session, number, T]) => void;
export type Handler<T extends unknown[]> = (...args: T) => unknown;
export type Ready = [DiscordReady];
export const READY: RawHandler<Ready> = (session, shardId, payload) => {
session.emit("ready", payload, shardId);
export const READY: RawHandler<DiscordReady> = (session, shardId, payload) => {
session.applicationId = payload.application.id;
session.botId = payload.user.id;
session.emit("ready", { ...payload, user: new User(session, payload.user) }, shardId);
};
export type MessageCreate = [DiscordMessage];
export const MESSAGE_CREATE: RawHandler<MessageCreate> = (session, _shardId, message) => {
export const MESSAGE_CREATE: RawHandler<DiscordMessage> = (session, _shardId, message) => {
session.emit("messageCreate", new Message(session, message));
};
export type MessageUpdate = [DiscordMessage];
export const MESSAGE_UPDATE: RawHandler<MessageUpdate> = (session, _shardId, new_message) => {
export const MESSAGE_UPDATE: RawHandler<DiscordMessage> = (session, _shardId, new_message) => {
session.emit("messageUpdate", new Message(session, new_message));
};
export type MessageDelete = [Snowflake];
export const MESSAGE_DELETE: RawHandler<MessageDelete> = (session, _shardId, deleted_message_id) => {
session.emit("messageDelete", deleted_message_id);
export const MESSAGE_DELETE: RawHandler<DiscordMessageDelete> = (session, _shardId, { id, channel_id, guild_id }) => {
session.emit("messageDelete", { id, channelId: channel_id, guildId: guild_id });
};
export const raw: RawHandler<[unknown]> = (session, shardId, data) => {
export const GUILD_MEMBER_ADD: RawHandler<DiscordGuildMemberAdd> = (session, _shardId, member) => {
session.emit("guildMemberAdd", new Member(session, member, member.guild_id));
};
export const GUILD_MEMBER_UPDATE: RawHandler<DiscordGuildMemberUpdate> = (session, _shardId, member) => {
session.emit("guildMemberUpdate", new Member(session, member, member.guild_id));
};
export const GUILD_MEMBER_REMOVE: RawHandler<DiscordGuildMemberRemove> = (session, _shardId, member) => {
session.emit("guildMemberRemove", new User(session, member.user), member.guild_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));
};
export const raw: RawHandler<unknown> = (session, shardId, data) => {
session.emit("raw", data, shardId);
};
export interface Events {
"ready": Handler<[DiscordReady, number]>;
"messageCreate": Handler<[Message]>;
"messageUpdate": Handler<[Message]>;
"messageDelete": Handler<[Snowflake]>;
"raw": Handler<[unknown]>;
export interface Ready extends Omit<DiscordReady, "user"> {
user: User;
}
// 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]>;
"interactionCreate": Handler<[Interaction]>;
"raw": Handler<[unknown, number]>;
}

5
mod.ts
View File

@ -2,12 +2,15 @@ export * from "./structures/AnonymousGuild.ts";
export * from "./structures/Attachment.ts";
export * from "./structures/Base.ts";
export * from "./structures/BaseGuild.ts";
export * from "./structures/Channel.ts";
export * from "./structures/BaseChannel.ts";
export * from "./structures/Component.ts";
export * from "./structures/DMChannel.ts";
export * from "./structures/Embed.ts";
export * from "./structures/Emoji.ts";
export * from "./structures/Guild.ts";
export * from "./structures/GuildChannel.ts";
export * from "./structures/GuildEmoji.ts";
export * from "./structures/Interaction.ts";
export * from "./structures/Invite.ts";
export * from "./structures/InviteGuild.ts";
export * from "./structures/Member.ts";

View File

@ -4,7 +4,7 @@ import type { Events } from "../handlers/Actions.ts";
import { Snowflake } from "../util/Snowflake.ts";
import { EventEmitter } from "../util/EventEmmiter.ts";
import { createGatewayManager, createRestManager } from "../vendor/external.ts";
import { createGatewayManager, createRestManager, getBotIdFromToken } from "../vendor/external.ts";
import * as Routes from "../util/Routes.ts";
import * as Actions from "../handlers/Actions.ts";
@ -39,6 +39,27 @@ export class Session extends EventEmitter {
rest: ReturnType<typeof createRestManager>;
gateway: ReturnType<typeof createGatewayManager>;
unrepliedInteractions: Set<bigint> = new Set();
#botId: Snowflake;
#applicationId?: Snowflake;
set applicationId(id: Snowflake) {
this.#applicationId = id;
}
get applicationId() {
return this.#applicationId!;
}
set botId(id: Snowflake) {
this.#botId = id;
}
get botId() {
return this.#botId;
}
constructor(options: SessionOptions) {
super();
this.options = options;
@ -71,7 +92,8 @@ export class Session extends EventEmitter {
},
handleDiscordPayload: this.options.rawHandler ?? defHandler,
});
// TODO: set botId in Session.botId or something
this.#botId = getBotIdFromToken(options.token).toString();
}
override on<K extends keyof Events>(event: K, func: Events[K]): this;
@ -89,6 +111,11 @@ export class Session extends EventEmitter {
return super.once(event, func);
}
override emit<K extends keyof Events>(event: K, ...params: Parameters<Events[K]>): boolean;
override emit<K extends string>(event: K, ...params: unknown[]): boolean {
return super.emit(event, ...params);
}
async start() {
const getGatewayBot = () => this.rest.runMethod<DiscordGetGatewayBot>(this.rest, "GET", Routes.GATEWAY_BOT());

49
structures/BaseChannel.ts Normal file
View File

@ -0,0 +1,49 @@
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 TextChannel from "./TextChannel.ts";
import VoiceChannel from "./VoiceChannel.ts";
import DMChannel from "./DMChannel.ts";
import NewsChannel from "./NewsChannel.ts";
import ThreadChannel from "./ThreadChannel.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 this instanceof TextChannel;
}
isVoice(): this is VoiceChannel {
return this instanceof VoiceChannel;
}
isDM(): this is DMChannel {
return this instanceof DMChannel;
}
isNews(): this is NewsChannel {
return this instanceof NewsChannel;
}
isThread(): this is ThreadChannel {
return this instanceof ThreadChannel;
}
toString(): string {
return `<#${this.id}>`;
}
}
export default BaseChannel;

View File

@ -1,24 +0,0 @@
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";
export abstract class Channel 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;
toString(): string {
return `<#${this.id}>`;
}
}
export default Channel;

41
structures/Component.ts Normal file
View File

@ -0,0 +1,41 @@
import type { Session } from "../session/Session.ts";
import type { DiscordComponent, MessageComponentTypes } from "../vendor/external.ts";
import Emoji from "./Emoji.ts";
export class Component {
constructor(session: Session, data: DiscordComponent) {
this.session = session;
this.customId = data.custom_id;
this.type = data.type;
this.components = data.components?.map((component) => new Component(session, component));
this.disabled = !!data.disabled;
if (data.emoji) {
this.emoji = new Emoji(session, data.emoji);
}
this.maxValues = data.max_values;
this.minValues = data.min_values;
this.label = data.label;
this.value = data.value;
this.options = data.options ?? [];
this.placeholder = data.placeholder;
}
readonly session: Session;
customId?: string;
type: MessageComponentTypes;
components?: Component[];
disabled: boolean;
emoji?: Emoji;
maxValues?: number;
minValues?: number;
label?: string;
value?: string;
// deno-lint-ignore no-explicit-any
options: any[];
placeholder?: string;
}
export default Component;

View File

@ -1,16 +1,23 @@
import type { Model } from "./Base.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type { DiscordChannel } from "../vendor/external.ts";
import Channel from "./Channel.ts";
import BaseChannel from "./BaseChannel.ts";
import User from "./User.ts";
import * as Routes from "../util/Routes.ts";
export class DMChannel extends Channel {
export class DMChannel extends BaseChannel implements Model {
constructor(session: Session, data: DiscordChannel) {
super(session, data);
data.last_message_id ? this.lastMessageId = data.last_message_id : undefined;
// Waiting for implementation of botId in session
//this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId));
this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!);
if (data.last_message_id) {
this.lastMessageId = data.last_message_id;
}
}
user: User;
lastMessageId?: Snowflake;
async close() {

101
structures/Embed.ts Normal file
View File

@ -0,0 +1,101 @@
import type { DiscordEmbed, EmbedTypes } from "../vendor/external.ts";
export interface Embed {
title?: string;
timestamp?: string;
type?: EmbedTypes;
url?: string;
color?: number;
description?: string;
author?: {
name: string;
iconURL?: string;
proxyIconURL?: string;
url?: string;
};
footer?: {
text: string;
iconURL?: string;
proxyIconURL?: string;
};
fields?: Array<{
name: string;
value: string;
inline?: boolean;
}>;
thumbnail?: {
url: string;
proxyURL?: string;
width?: number;
height?: number;
};
video?: {
url?: string;
proxyURL?: string;
width?: number;
height?: number;
};
image?: {
url: string;
proxyURL?: string;
width?: number;
height?: number;
};
provider?: {
url?: string;
name?: string;
};
}
export function embed(data: Embed): DiscordEmbed {
return {
title: data.title,
timestamp: data.timestamp,
type: data.type,
url: data.url,
color: data.color,
description: data.description,
author: {
name: data.author?.name!,
url: data.author?.url,
icon_url: data.author?.iconURL,
proxy_icon_url: data.author?.proxyIconURL,
},
footer: data.footer || {
text: data.footer!.text,
icon_url: data.footer!.iconURL,
proxy_icon_url: data.footer!.proxyIconURL,
},
fields: data.fields?.map((f) => {
return {
name: f.name,
value: f.value,
inline: f.inline,
};
}),
thumbnail: data.thumbnail || {
url: data.thumbnail!.url,
proxy_url: data.thumbnail!.proxyURL,
width: data.thumbnail!.width,
height: data.thumbnail!.height,
},
video: {
url: data.video?.url,
proxy_url: data.video?.proxyURL,
width: data.video?.width,
height: data.video?.height,
},
image: data.image || {
url: data.image!.url,
proxy_url: data.image!.proxyURL,
width: data.image!.width,
height: data.image!.height,
},
provider: {
url: data.provider?.url,
name: data.provider?.name,
},
};
}
export default Embed;

View File

@ -1,7 +1,13 @@
import type { Model } from "./Base.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type { DiscordEmoji, DiscordGuild, DiscordInviteMetadata, DiscordRole } from "../vendor/external.ts";
import type {
DiscordEmoji,
DiscordGuild,
DiscordInviteMetadata,
DiscordMemberWithUser,
DiscordRole,
} from "../vendor/external.ts";
import type { GetInvite } from "../util/Routes.ts";
import {
DefaultMessageNotificationLevels,
@ -46,6 +52,40 @@ export interface ModifyGuildEmoji {
roles?: Snowflake[];
}
/**
* @link https://discord.com/developers/docs/resources/guild#create-guild-ban
*/
export interface CreateGuildBan {
deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
reason?: string;
}
/**
* @link https://discord.com/developers/docs/resources/guild#modify-guild-member
*/
export interface ModifyGuildMember {
nick?: string;
roles?: Snowflake[];
mute?: boolean;
deaf?: boolean;
channelId?: Snowflake;
communicationDisabledUntil?: number;
}
/**
* @link https://discord.com/developers/docs/resources/guild#begin-guild-prune
*/
export interface BeginGuildPrune {
days?: number;
computePruneCount?: boolean;
includeRoles?: Snowflake[];
}
export interface ModifyRolePositions {
id: Snowflake;
position?: number | null;
}
/**
* Represents a guild
* @link https://discord.com/developers/docs/resources/guild#guild-object
@ -62,7 +102,8 @@ export class Guild extends BaseGuild implements Model {
this.vefificationLevel = data.verification_level;
this.defaultMessageNotificationLevel = data.default_message_notifications;
this.explicitContentFilterLevel = data.explicit_content_filter;
this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! })) ?? [];
this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! }, data.id)) ??
[];
this.roles = data.roles.map((role) => new Role(session, role, data.id));
this.emojis = data.emojis.map((guildEmoji) => new GuildEmoji(session, guildEmoji, data.id));
}
@ -79,6 +120,20 @@ export class Guild extends BaseGuild implements Model {
roles: Role[];
emojis: GuildEmoji[];
/**
* 'null' would reset the nickname
*/
async editBotNickname(options: { nick: string | null; reason?: string }) {
const result = await this.session.rest.runMethod<{ nick?: string } | undefined>(
this.session.rest,
"PATCH",
Routes.USER_NICK(this.id),
options,
);
return result?.nick;
}
async createEmoji(options: CreateGuildEmoji): Promise<GuildEmoji> {
if (options.image && !options.image.startsWith("data:image/")) {
options.image = await urlToBase64(options.image);
@ -162,6 +217,40 @@ export class Guild extends BaseGuild implements Model {
return new Role(this.session, role, this.id);
}
async addRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId),
{ reason },
);
}
async removeRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId),
{ reason },
);
}
/**
* Returns the roles moved
* */
async moveRoles(options: ModifyRolePositions[]) {
const roles = await this.session.rest.runMethod<DiscordRole[]>(
this.session.rest,
"PATCH",
Routes.GUILD_ROLES(this.id),
options,
);
return roles.map((role) => new Role(this.session, role, this.id));
}
async deleteInvite(inviteCode: string): Promise<void> {
await this.session.rest.runMethod<undefined>(
this.session.rest,
@ -190,6 +279,91 @@ export class Guild extends BaseGuild implements Model {
return invites.map((invite) => new Invite(this.session, invite));
}
/**
* Bans the member
*/
async banMember(memberId: Snowflake, options: CreateGuildBan) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.GUILD_BAN(this.id, memberId),
options
? {
delete_message_days: options.deleteMessageDays,
reason: options.reason,
}
: {},
);
}
/**
* Kicks the member
*/
async kickMember(memberId: Snowflake, { reason }: { reason?: string }) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.GUILD_MEMBER(this.id, memberId),
{ reason },
);
}
/*
* Unbans the member
* */
async unbanMember(memberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.GUILD_BAN(this.id, memberId),
);
}
async editMember(memberId: Snowflake, options: ModifyGuildMember) {
const member = await this.session.rest.runMethod<DiscordMemberWithUser>(
this.session.rest,
"PATCH",
Routes.GUILD_MEMBER(this.id, memberId),
{
nick: options.nick,
roles: options.roles,
mute: options.mute,
deaf: options.deaf,
channel_id: options.channelId,
communication_disabled_until: options.communicationDisabledUntil
? new Date(options.communicationDisabledUntil).toISOString()
: undefined,
},
);
return new Member(this.session, member, this.id);
}
async pruneMembers(options: BeginGuildPrune): Promise<number> {
const result = await this.session.rest.runMethod<{ pruned: number }>(
this.session.rest,
"POST",
Routes.GUILD_PRUNE(this.id),
{
days: options.days,
compute_prune_count: options.computePruneCount,
include_roles: options.includeRoles,
},
);
return result.pruned;
}
async getPruneCount(): Promise<number> {
const result = await this.session.rest.runMethod<{ pruned: number }>(
this.session.rest,
"GET",
Routes.GUILD_PRUNE(this.id),
);
return result.pruned;
}
}
export default Guild;

View File

@ -2,11 +2,11 @@ import type { Model } from "./Base.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type { DiscordChannel, DiscordInviteMetadata } from "../vendor/external.ts";
import Channel from "./Channel.ts";
import BaseChannel from "./BaseChannel.ts";
import Invite from "./Invite.ts";
import * as Routes from "../util/Routes.ts";
export abstract class GuildChannel extends Channel implements Model {
export abstract class GuildChannel extends BaseChannel implements Model {
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
super(session, data);
this.guildId = guildId;

143
structures/Interaction.ts Normal file
View File

@ -0,0 +1,143 @@
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;
}
export class Interaction implements Model {
constructor(session: Session, data: DiscordInteraction) {
this.session = session;
this.id = data.id;
this.token = data.token;
this.type = data.type;
this.guildId = data.guild_id;
this.channelId = data.channel_id;
this.applicationId = data.application_id;
this.locale = data.locale;
this.data = data.data;
if (!data.guild_id) {
this.user = new User(session, data.user!);
} else {
this.member = new Member(session, data.member!, data.guild_id);
}
}
readonly session: Session;
readonly id: Snowflake;
readonly token: string;
type: InteractionTypes;
guildId?: Snowflake;
channelId?: Snowflake;
applicationId?: Snowflake;
locale?: string;
// deno-lint-ignore no-explicit-any
data: any;
user?: User;
member?: Member;
async respond({ type, data }: InteractionResponse) {
const toSend = {
tts: data?.tts,
title: data?.title,
flags: data?.flags,
content: data?.content,
choices: data?.choices,
custom_id: data?.customId,
allowed_mentions: data?.allowedMentions
? {
users: data.allowedMentions.users,
roles: data.allowedMentions.roles,
parse: data.allowedMentions.parse,
replied_user: data.allowedMentions.repliedUser,
}
: { parse: [] },
};
if (this.session.unrepliedInteractions.delete(BigInt(this.id))) {
await this.session.rest.sendRequest<undefined>(
this.session.rest,
{
url: Routes.INTERACTION_ID_TOKEN(this.id, this.token),
method: "POST",
payload: this.session.rest.createRequestBody(this.session.rest, {
method: "POST",
body: {
type: type,
data: toSend,
file: data?.files,
},
headers: {
// remove authorization header
Authorization: "",
},
}),
},
);
return;
}
const result = await this.session.rest.sendRequest<DiscordMessage>(
this.session.rest,
{
url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token),
method: "POST",
payload: this.session.rest.createRequestBody(this.session.rest, {
method: "POST",
body: {
...toSend,
file: data?.files,
},
headers: {
// remove authorization header
Authorization: "",
},
}),
},
);
return new Message(this.session, result);
}
inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } {
return !!this.guildId;
}
}
export default Interaction;

View File

@ -1,31 +1,24 @@
import type { Model } from "./Base.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type { DiscordMember, MakeRequired } from "../vendor/external.ts";
import type { DiscordMemberWithUser } from "../vendor/external.ts";
import type { ImageFormat, ImageSize } from "../util/shared/images.ts";
import type { CreateGuildBan, ModifyGuildMember } from "./Guild.ts";
import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts";
import User from "./User.ts";
import Guild from "./Guild.ts";
import * as Routes from "../util/Routes.ts";
/**
* @link https://discord.com/developers/docs/resources/guild#create-guild-ban
*/
export interface CreateGuildBan {
/** Number of days to delete messages for (0-7) */
deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
/** Reason for the ban */
reason?: string;
}
/**
* Represents a guild member
* TODO: add a `guild` property somehow
* @link https://discord.com/developers/docs/resources/guild#guild-member-object
*/
export class Member implements Model {
constructor(session: Session, data: MakeRequired<DiscordMember, "user">) {
constructor(session: Session, data: DiscordMemberWithUser, guildId: Snowflake) {
this.session = session;
this.user = new User(session, data.user);
this.guildId = guildId;
this.avatarHash = data.avatar ? iconHashToBigInt(data.avatar) : undefined;
this.nickname = data.nick ? data.nick : undefined;
this.joinedTimestamp = Number.parseInt(data.joined_at);
@ -39,8 +32,8 @@ export class Member implements Model {
}
readonly session: Session;
user: User;
guildId: Snowflake;
avatarHash?: bigint;
nickname?: string;
joinedTimestamp: number;
@ -63,39 +56,40 @@ export class Member implements Model {
return new Date(this.joinedTimestamp);
}
/**
* Bans the member
*/
async ban(guildId: Snowflake, options: CreateGuildBan): Promise<Member> {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.GUILD_BAN(guildId, this.id),
options
? {
delete_message_days: options.deleteMessageDays,
reason: options.reason,
}
: {},
);
async ban(options: CreateGuildBan): Promise<Member> {
await Guild.prototype.banMember.call({ id: this.guildId, session: this.session }, this.user.id, options);
return this;
}
/**
* Kicks the member
*/
async kick(guildId: Snowflake, { reason }: { reason?: string }): Promise<Member> {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.GUILD_MEMBER(guildId, this.id),
{ reason },
);
async kick(options: { reason?: string }): Promise<Member> {
await Guild.prototype.kickMember.call({ id: this.guildId, session: this.session }, this.user.id, options);
return this;
}
async unban() {
await Guild.prototype.unbanMember.call({ id: this.guildId, session: this.session }, this.user.id);
}
async edit(options: ModifyGuildMember): Promise<Member> {
const member = await Guild.prototype.editMember.call(
{ id: this.guildId, session: this.session },
this.user.id,
options,
);
return member;
}
async addRole(roleId: Snowflake, options: { reason?: string } = {}) {
await Guild.prototype.addRole.call({ id: this.guildId, session: this.session }, this.user.id, roleId, options);
}
async removeRole(roleId: Snowflake, options: { reason?: string } = {}) {
await Guild.prototype.removeRole.call({ id: this.guildId, session: this.session }, this.user.id, roleId, options);
}
/** gets the user's avatar */
avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) {
let url: string;
@ -103,7 +97,7 @@ export class Member implements Model {
if (!this.avatarHash) {
url = Routes.USER_DEFAULT_AVATAR(Number(this.user.discriminator) % 5);
} else {
url = Routes.USER_AVATAR(this.id, iconBigintToHash(this.avatarHash));
url = Routes.USER_AVATAR(this.user.id, iconBigintToHash(this.avatarHash));
}
return `${url}.${options.format ?? (url.includes("/a_") ? "gif" : "jpg")}?size=${options.size}`;

View File

@ -1,7 +1,14 @@
import type { Model } from "./Base.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type { AllowedMentionsTypes, DiscordMessage, FileContent } from "../vendor/external.ts";
import type {
AllowedMentionsTypes,
DiscordEmbed,
DiscordMessage,
DiscordUser,
FileContent,
} from "../vendor/external.ts";
import type { GetReactions } from "../util/Routes.ts";
import { MessageFlags } from "../util/shared/flags.ts";
import User from "./User.ts";
import Member from "./Member.ts";
@ -33,6 +40,7 @@ export interface CreateMessage {
allowedMentions?: AllowedMentions;
files?: FileContent[];
messageReference?: CreateMessageReference;
embeds?: DiscordEmbed[];
}
/**
@ -42,6 +50,11 @@ export interface EditMessage extends Partial<CreateMessage> {
flags?: MessageFlags;
}
export type ReactionResolvable = string | {
name: string;
id: Snowflake;
};
/**
* Represents a message
* @link https://discord.com/developers/docs/resources/channel#message-object
@ -63,12 +76,10 @@ export class Message implements Model {
this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment));
// user is always null on MessageCreate and its replaced with author
this.member = data.member
? new Member(session, {
...data.member,
user: data.author,
})
: undefined;
if (data.guild_id && data.member) {
this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id);
}
}
readonly session: Session;
@ -89,21 +100,38 @@ export class Message implements Model {
return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`;
}
async pin() {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.CHANNEL_PIN(this.channelId, this.id),
);
}
async unpin() {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.CHANNEL_PIN(this.channelId, this.id),
);
}
/** Edits the current message */
async edit({ content, allowedMentions, flags }: EditMessage): Promise<Message> {
async edit(options: EditMessage): Promise<Message> {
const message = await this.session.rest.runMethod(
this.session.rest,
"POST",
Routes.CHANNEL_MESSAGE(this.id, this.channelId),
{
content,
content: options.content,
allowed_mentions: {
parse: allowedMentions?.parse,
roles: allowedMentions?.roles,
users: allowedMentions?.users,
replied_user: allowedMentions?.repliedUser,
parse: options.allowedMentions?.parse,
roles: options.allowedMentions?.roles,
users: options.allowedMentions?.users,
replied_user: options.allowedMentions?.repliedUser,
},
flags,
flags: options.flags,
embeds: options.embeds,
},
);
@ -156,12 +184,98 @@ export class Message implements Model {
fail_if_not_exists: options.messageReference.failIfNotExists ?? true,
}
: undefined,
embeds: options.embeds,
},
);
return new Message(this.session, message);
}
/**
* alias for Message.addReaction
*/
get react() {
return this.addReaction;
}
async addReaction(reaction: ReactionResolvable) {
const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`;
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r),
{},
);
}
async removeReaction(reaction: ReactionResolvable, options?: { userId: Snowflake }) {
const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`;
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
options?.userId
? Routes.CHANNEL_MESSAGE_REACTION_USER(
this.channelId,
this.id,
r,
options.userId,
)
: Routes.CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r),
);
}
/**
* Get users who reacted with this emoji
*/
async fetchReactions(reaction: ReactionResolvable, options?: GetReactions): Promise<User[]> {
const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`;
const users = await this.session.rest.runMethod<DiscordUser[]>(
this.session.rest,
"GET",
Routes.CHANNEL_MESSAGE_REACTION(this.channelId, this.id, encodeURIComponent(r), options),
);
return users.map((user) => new User(this.session, user));
}
async removeReactionEmoji(reaction: ReactionResolvable) {
const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`;
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.CHANNEL_MESSAGE_REACTION(this.channelId, this.id, r),
);
}
async nukeReactions() {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.CHANNEL_MESSAGE_REACTIONS(this.channelId, this.id),
);
}
async crosspost() {
const message = await this.session.rest.runMethod<DiscordMessage>(
this.session.rest,
"POST",
Routes.CHANNEL_MESSAGE_CROSSPOST(this.channelId, this.id),
);
return new Message(this.session, message);
}
/*
* alias of Message.crosspost
* */
get publish() {
return this.crosspost;
}
inGuild(): this is { guildId: Snowflake } & Message {
return !!this.guildId;
}

View File

@ -2,6 +2,7 @@ import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type { DiscordChannel } from "../vendor/external.ts";
import TextChannel from "./TextChannel.ts";
import Message from "./Message.ts";
export class NewsChannel extends TextChannel {
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
@ -9,6 +10,14 @@ export class NewsChannel extends TextChannel {
this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
}
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;
}
}
export default NewsChannel;

View File

@ -48,7 +48,6 @@ export class Role implements Model {
}
async delete(): Promise<void> {
// cool jS trick
await Guild.prototype.deleteRole.call({ id: this.guildId, session: this.session }, this.id);
}
@ -57,6 +56,14 @@ export class Role implements Model {
return role;
}
async add(memberId: Snowflake, options: { reason?: string } = {}) {
await Guild.prototype.addRole.call({ id: this.guildId, session: this.session }, memberId, this.id, options);
}
async remove(memberId: Snowflake, options: { reason?: string } = {}) {
await Guild.prototype.removeRole.call({ id: this.guildId, session: this.session }, memberId, this.id, options);
}
toString() {
switch (this.id) {
case this.guildId:

View File

@ -1,9 +1,9 @@
import type { Session } from "../session/Session.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { GetMessagesOptions } from "../util/Routes.ts";
import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts";
import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts";
import type { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts";
import GuildChannel from "./GuildChannel.ts";
import Guild from "./Guild.ts";
import ThreadChannel from "./ThreadChannel.ts";
import Message from "./Message.ts";
import Invite from "./Invite.ts";
@ -37,7 +37,7 @@ export interface ThreadCreateOptions {
}
export class TextChannel extends GuildChannel {
constructor(session: Session, data: DiscordChannel, guildId: Guild["id"]) {
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
super(session, data, guildId);
data.last_message_id ? this.lastMessageId = data.last_message_id : undefined;
data.last_pin_timestamp ? this.lastPinTimestamp = data.last_pin_timestamp : undefined;
@ -107,6 +107,58 @@ export class TextChannel extends GuildChannel {
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?: 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);
}
}
export default TextChannel;

View File

@ -1,16 +1,31 @@
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";
/**
* @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 {
constructor(session: Session, guildId: Snowflake, data: DiscordChannel) {
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
super(session, data, guildId);
this.bitRate = data.bitrate;
this.userLimit = data.user_limit ?? 0;
data.rtc_region ? this.rtcRegion = data.rtc_region : undefined;
this.videoQuality = data.video_quality_mode;
this.nsfw = !!data.nsfw;
if (data.rtc_region) {
this.rtcRegion = data.rtc_region;
}
}
bitRate?: number;
userLimit: number;
@ -18,6 +33,28 @@ export class VoiceChannel extends GuildChannel {
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 VoiceChannel;

View File

@ -46,10 +46,6 @@ export function MESSAGE_CREATE_THREAD(channelId: Snowflake, messageId: Snowflake
return `/channels/${channelId}/messages/${messageId}/threads`;
}
export function CHANNEL_PINS(channelId: Snowflake) {
return `/channels/${channelId}/pins`;
}
/** used to send messages */
export function CHANNEL_MESSAGES(channelId: Snowflake, options?: GetMessagesOptions) {
let url = `/channels/${channelId}/messages?`;
@ -139,3 +135,92 @@ export function INVITE(inviteCode: string, options?: GetInvite) {
export function GUILD_INVITES(guildId: Snowflake) {
return `/guilds/${guildId}/invites`;
}
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}?`;
if (options?.wait !== undefined) url += `wait=${options.wait}`;
if (options?.threadId) url += `threadId=${options.threadId}`;
return url;
}
export function USER_NICK(guildId: Snowflake) {
return `/guilds/${guildId}/members/@me`;
}
/**
* @link https://discord.com/developers/docs/resources/guild#get-guild-prune-count
*/
export interface GetGuildPruneCountQuery {
days?: number;
includeRoles?: Snowflake | Snowflake[];
}
export function GUILD_PRUNE(guildId: Snowflake, options?: GetGuildPruneCountQuery) {
let url = `/guilds/${guildId}/prune?`;
if (options?.days) url += `days=${options.days}`;
if (options?.includeRoles) url += `&include_roles=${options.includeRoles}`;
return url;
}
export function CHANNEL_PIN(channelId: Snowflake, messageId: Snowflake) {
return `/channels/${channelId}/pins/${messageId}`;
}
export function CHANNEL_PINS(channelId: Snowflake) {
return `/channels/${channelId}/pins`;
}
export function CHANNEL_MESSAGE_REACTION_ME(channelId: Snowflake, messageId: Snowflake, emoji: string) {
return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`;
}
export function CHANNEL_MESSAGE_REACTION_USER(
channelId: Snowflake,
messageId: Snowflake,
emoji: string,
userId: Snowflake,
) {
return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/${userId}`;
}
export function CHANNEL_MESSAGE_REACTIONS(channelId: Snowflake, messageId: Snowflake) {
return `/channels/${channelId}/messages/${messageId}/reactions`;
}
/**
* @link https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params
*/
export interface GetReactions {
after?: string;
limit?: number;
}
export function CHANNEL_MESSAGE_REACTION(
channelId: Snowflake,
messageId: Snowflake,
emoji: string,
options?: GetReactions,
) {
let url = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}?`;
if (options?.after) url += `after=${options.after}`;
if (options?.limit) url += `&limit=${options.limit}`;
return url;
}
export function CHANNEL_MESSAGE_CROSSPOST(channelId: Snowflake, messageId: Snowflake) {
return `/channels/${channelId}/messages/${messageId}/crosspost`;
}
export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleId: Snowflake) {
return `/guilds/${guildId}/members/${memberId}/roles/${roleId}`;
}

1
vendor/external.ts vendored
View File

@ -2,3 +2,4 @@ export * from "./gateway/mod.ts";
export * from "./rest/mod.ts";
export * from "./types/mod.ts";
export * from "./util/constants.ts";
export * from "./util/token.ts";