diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 124c70cdb..358558525 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -15,9 +15,12 @@
Examples of good PR title:
-- fix(controllers): cache member from INTERACTION_CREATE payload
+- fix(controllers/interactions): cache member from INTERACTION_CREATE payload
- docs: improve wording
-- feat(handlers): add editGuild() function Examples of bad PR title:
+- feat(handlers/guild): add editGuild() function Examples of bad PR title:
+
+Examples of bad PR title:
+
- fix #7123
- update docs
- fix bugs
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index 9330c4d3c..000000000
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,14 +0,0 @@
-**Please describe the changes this PR makes and why it should be merged:**
-
-**Status**
-
-- [ ] Code changes have been tested against the Discord API, or there are no
- code changes
-
-**Semantic versioning classification:**
-
-- [ ] This PR changes the library's interface (methods or parameters added)
- - [ ] This PR includes breaking changes (methods removed or renamed,
- parameters moved or removed)
-- [ ] This PR **only** includes non-code changes, like changes to documentation,
- README, etc.
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d1782e240..9161e12e8 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,6 +7,6 @@ jobs:
- uses: actions/checkout@v2
- uses: denolib/setup-deno@v2
- name: Run fmt check script
- run: deno fmt --check
+ run: deno fmt --check --ignore=./src/types/util.ts
- name: Run lint script
run: deno lint src/** test/** --unstable
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4c31dcf43..6bf456c31 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,8 +13,10 @@ jobs:
deno-version: ${{ matrix.deno }}
- name: Cache dependencies
run: deno cache mod.ts
- - name: Run test script
+ - name: Run local tests
+ run: TEST_TYPE=local deno test --allow-env
+ - name: Run API tests
if: github.ref == 'refs/heads/master'
- run: deno test --allow-net --allow-env
+ run: TEST_TYPE=api deno test --allow-net --allow-env
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
diff --git a/README.md b/README.md
index 9378b48a9..4bd590024 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# Discordeno
-> Discord API library for [Deno](https://deno.land)
+
+
+Discord API library for [Deno](https://deno.land)
[](https://discord.com/invite/5vBgXk3UcZ)

@@ -8,13 +10,10 @@
## Features
-- **Secure & stable**: Discordeno is secure and stable. One of the greatest
- issues with almost every library is stability; types are outdated, less (or
- minimal) parity with the API, core maintainers have quit or no longer actively
- maintain the library, and whatnot. Discordeno, on the other hand, is actively
- maintained to ensure great performance and convenience. Moreover, it
- internally checks all missing permissions before forwarding a request to the
- Discord API so that the client does not get globally-banned by Discord.
+- **Secure & stable**: Discordeno is actively maintained to ensure great
+ performance and convenience. Moreover, it internally checks all missing
+ permissions before forwarding a request to the Discord API so that the client
+ does not get globally-banned by Discord.
- **Simple, Efficient, & Lightweight**: Discordeno is simplistic, easy-to-use,
versatile while being efficient and lightweight. Follows
[Convention Over Configuration](https://en.wikipedia.org/wiki/Convention_over_configuration)
diff --git a/src/api/controllers/interactions.ts b/src/api/controllers/interactions.ts
index c8def72ba..d33b2a7a9 100644
--- a/src/api/controllers/interactions.ts
+++ b/src/api/controllers/interactions.ts
@@ -1,6 +1,6 @@
import { eventHandlers } from "../../bot.ts";
import {
- Application,
+ ApplicationCommandEvent,
DiscordPayload,
InteractionCommandPayload,
} from "../../types/mod.ts";
@@ -23,5 +23,47 @@ export function handleInternalApplicationCommandCreate(
) {
if (data.t !== "APPLICATION_COMMAND_CREATE") return;
- eventHandlers.applicationCommandCreate?.(data.d as Application);
+ const {
+ guild_id: guildID,
+ application_id: applicationID,
+ ...rest
+ } = data.d as ApplicationCommandEvent;
+
+ eventHandlers.applicationCommandCreate?.({
+ ...rest,
+ guildID,
+ applicationID,
+ });
+}
+
+export function handleInternalApplicationCommandUpdate(data: DiscordPayload) {
+ if (data.t !== "APPLICATION_COMMAND_UPDATE") return;
+
+ const {
+ application_id: applicationID,
+ guild_id: guildID,
+ ...rest
+ } = data.d as ApplicationCommandEvent;
+
+ eventHandlers.applicationCommandUpdate?.({
+ ...rest,
+ guildID,
+ applicationID,
+ });
+}
+
+export function handleInternalApplicationCommandDelete(data: DiscordPayload) {
+ if (data.t !== "APPLICATION_COMMAND_DELETE") return;
+
+ const {
+ application_id: applicationID,
+ guild_id: guildID,
+ ...rest
+ } = data.d as ApplicationCommandEvent;
+
+ eventHandlers.applicationCommandDelete?.({
+ ...rest,
+ guildID,
+ applicationID,
+ });
}
diff --git a/src/api/controllers/members.ts b/src/api/controllers/members.ts
index 72894621b..5d253786d 100644
--- a/src/api/controllers/members.ts
+++ b/src/api/controllers/members.ts
@@ -80,6 +80,11 @@ export async function handleInternalGuildMemberUpdate(data: DiscordPayload) {
guildMember?.nick,
);
}
+
+ if (payload.pending === false && guildMember?.pending === true) {
+ eventHandlers.membershipScreeningPassed?.(guild, member);
+ }
+
const roleIDs = guildMember?.roles || [];
roleIDs.forEach((id) => {
diff --git a/src/api/controllers/misc.ts b/src/api/controllers/misc.ts
index f3ff65254..839d4e9e5 100644
--- a/src/api/controllers/misc.ts
+++ b/src/api/controllers/misc.ts
@@ -1,6 +1,8 @@
import { eventHandlers, setApplicationID, setBotID } from "../../bot.ts";
import {
DiscordPayload,
+ IntegrationCreateUpdateEvent,
+ IntegrationDeleteEvent,
PresenceUpdatePayload,
ReadyPayload,
TypingStartPayload,
@@ -30,7 +32,14 @@ export async function handleInternalReady(
eventHandlers.shardReady?.(shardID);
if (payload.shard && shardID === payload.shard[1] - 1) {
const loadedAllGuilds = async () => {
- if (payload.guilds.some((g) => !cache.guilds.has(g.id))) {
+ const guildsMissing = async () => {
+ for (const g of payload.guilds) {
+ if (!(await cacheHandlers.has("guilds", g.id))) return true;
+ }
+ return false;
+ };
+
+ if (await guildsMissing()) {
setTimeout(loadedAllGuilds, 2000);
} else {
// The bot has already started, the last shard is resumed, however.
@@ -155,3 +164,73 @@ export function handleInternalWebhooksUpdate(data: DiscordPayload) {
options.guild_id,
);
}
+
+export function handleInternalIntegrationCreate(
+ data: DiscordPayload,
+) {
+ if (data.t !== "INTEGRATION_CREATE") return;
+
+ const {
+ guild_id: guildID,
+ enable_emoticons: enableEmoticons,
+ expire_behavior: expireBehavior,
+ expire_grace_period: expireGracePeriod,
+ subscriber_count: subscriberCount,
+ role_id: roleID,
+ synced_at: syncedAt,
+ ...rest
+ } = data.d as IntegrationCreateUpdateEvent;
+
+ eventHandlers.integrationCreate?.({
+ ...rest,
+ guildID,
+ enableEmoticons,
+ expireBehavior,
+ expireGracePeriod,
+ syncedAt,
+ subscriberCount,
+ roleID,
+ });
+}
+
+export function handleInternalIntegrationUpdate(data: DiscordPayload) {
+ if (data.t !== "INTEGRATION_UPDATE") return;
+
+ const {
+ enable_emoticons: enableEmoticons,
+ expire_behavior: expireBehavior,
+ expire_grace_period: expireGracePeriod,
+ role_id: roleID,
+ subscriber_count: subscriberCount,
+ synced_at: syncedAt,
+ guild_id: guildID,
+ ...rest
+ } = data.d as IntegrationCreateUpdateEvent;
+
+ eventHandlers.integrationUpdate?.({
+ ...rest,
+ guildID,
+ subscriberCount,
+ enableEmoticons,
+ expireGracePeriod,
+ roleID,
+ expireBehavior,
+ syncedAt,
+ });
+}
+
+export function handleInternalIntegrationDelete(data: DiscordPayload) {
+ if (data.t !== "INTEGRATION_DELETE") return;
+
+ const {
+ guild_id: guildID,
+ application_id: applicationID,
+ ...rest
+ } = data.d as IntegrationDeleteEvent;
+
+ eventHandlers.integrationDelete?.({
+ ...rest,
+ applicationID,
+ guildID,
+ });
+}
diff --git a/src/api/controllers/mod.ts b/src/api/controllers/mod.ts
index e6250f8b5..a1b73e9dc 100644
--- a/src/api/controllers/mod.ts
+++ b/src/api/controllers/mod.ts
@@ -15,6 +15,8 @@ import {
} from "./guilds.ts";
import {
handleInternalApplicationCommandCreate,
+ handleInternalApplicationCommandDelete,
+ handleInternalApplicationCommandUpdate,
handleInternalInteractionCreate,
} from "./interactions.ts";
import {
@@ -30,6 +32,9 @@ import {
handleInternalMessageUpdate,
} from "./messages.ts";
import {
+ handleInternalIntegrationCreate,
+ handleInternalIntegrationDelete,
+ handleInternalIntegrationUpdate,
handleInternalPresenceUpdate,
handleInternalReady,
handleInternalTypingStart,
@@ -69,6 +74,8 @@ export let controllers = {
GUILD_ROLE_UPDATE: handleInternalGuildRoleUpdate,
INTERACTION_CREATE: handleInternalInteractionCreate,
APPLICATION_COMMAND_CREATE: handleInternalApplicationCommandCreate,
+ APPLICATION_COMMAND_DELETE: handleInternalApplicationCommandDelete,
+ APPLICATION_COMMAND_UPDATE: handleInternalApplicationCommandUpdate,
MESSAGE_CREATE: handleInternalMessageCreate,
MESSAGE_DELETE: handleInternalMessageDelete,
MESSAGE_DELETE_BULK: handleInternalMessageDeleteBulk,
@@ -82,6 +89,9 @@ export let controllers = {
USER_UPDATE: handleInternalUserUpdate,
VOICE_STATE_UPDATE: handleInternalVoiceStateUpdate,
WEBHOOKS_UPDATE: handleInternalWebhooksUpdate,
+ INTEGRATION_CREATE: handleInternalIntegrationCreate,
+ INTEGRATION_UPDATE: handleInternalIntegrationUpdate,
+ INTEGRATION_DELETE: handleInternalIntegrationDelete,
};
export type Controllers = typeof controllers;
diff --git a/src/api/handlers/guild.ts b/src/api/handlers/guild.ts
index ce40a2110..a5f3fbf90 100644
--- a/src/api/handlers/guild.ts
+++ b/src/api/handlers/guild.ts
@@ -20,12 +20,15 @@ import {
Errors,
FetchMembersOptions,
GetAuditLogsOptions,
+ GetMemberOptions,
GuildEditOptions,
GuildTemplate,
ImageFormats,
ImageSize,
Intents,
MemberCreatePayload,
+ MembershipScreeningFieldTypes,
+ MembershipScreeningPayload,
Overwrite,
PositionSwap,
PruneOptions,
@@ -578,6 +581,67 @@ export function fetchMembers(guild: Guild, options?: FetchMembersOptions) {
}) as Promise>;
}
+/**
+ * ⚠️ BEGINNER DEVS!! YOU SHOULD ALMOST NEVER NEED THIS AND YOU CAN GET FROM cache.members.get()
+ *
+ * ADVANCED:
+ * Highly recommended to **NOT** use this function to get members instead use fetchMembers().
+ * REST(this function): 50/s global(across all shards) rate limit with ALL requests this included
+ * GW(fetchMembers): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is 960/m.
+*/
+export async function getMembers(
+ guildID: string,
+ options?: GetMemberOptions,
+) {
+ if (!(identifyPayload.intents && Intents.GUILD_MEMBERS)) {
+ throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS);
+ }
+
+ const guild = await cacheHandlers.get("guilds", guildID);
+ if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
+
+ const members = new Collection();
+
+ let membersLeft = options?.limit ?? guild.memberCount;
+ let loops = 1;
+ while (
+ (options?.limit ?? guild.memberCount) > members.size && membersLeft > 0
+ ) {
+ if (options?.limit && options.limit > 1000) {
+ console.log(
+ `Paginating get members from REST. #${loops} / ${
+ Math.ceil((options?.limit ?? 1) / 1000)
+ }`,
+ );
+ }
+
+ const result = await RequestManager.get(
+ `${endpoints.GUILD_MEMBERS(guildID)}?limit=${
+ membersLeft > 1000 ? 1000 : membersLeft
+ }${options?.after ? `&after=${options.after}` : ""}`,
+ ) as MemberCreatePayload[];
+
+ const memberStructures = await Promise.all(
+ result.map((member) => structures.createMember(member, guildID)),
+ ) as Member[];
+
+ if (!memberStructures.length) break;
+
+ memberStructures.forEach((member) => members.set(member.id, member));
+
+ options = {
+ limit: options?.limit,
+ after: memberStructures[memberStructures.length - 1].id,
+ };
+
+ membersLeft -= 1000;
+
+ loops++;
+ }
+
+ return members;
+}
+
/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */
export async function getAuditLogs(
guildID: string,
@@ -1021,3 +1085,69 @@ export async function editGuildTemplate(
return structures.createTemplate(template);
}
+
+function createMembershipObj(
+ { form_fields: formFields, ...props }: MembershipScreeningPayload,
+) {
+ return {
+ ...props,
+ formFields: formFields.map(({ field_type, ...rest }) => ({
+ ...rest,
+ fieldType: field_type,
+ })),
+ };
+}
+
+export type MembershipScreening = ReturnType;
+
+/** Get the membership screening form of a guild. */
+export async function getGuildMembershipScreeningForm(guildID: string) {
+ const membershipScreeningPayload = await RequestManager.get(
+ endpoints.GUILD_MEMBER_VERIFICATION(guildID),
+ ) as MembershipScreeningPayload;
+
+ return createMembershipObj(membershipScreeningPayload);
+}
+
+/** Edit the guild's Membership Screening form. Requires the `MANAGE_GUILD` permission. */
+export async function editGuildMembershipScreeningForm(
+ guildID: string,
+ options?: EditGuildMembershipScreeningForm,
+) {
+ const membershipScreeningFormPayload = await RequestManager.patch(
+ endpoints.GUILD_MEMBER_VERIFICATION(guildID),
+ {
+ ...options,
+ form_fields: JSON.stringify(
+ options?.formFields?.map(({ fieldType, ...props }) => ({
+ ...props,
+ field_type: fieldType,
+ })),
+ ),
+ },
+ ) as MembershipScreeningPayload;
+
+ return createMembershipObj(
+ membershipScreeningFormPayload,
+ );
+}
+
+export interface EditGuildMembershipScreeningForm {
+ /** whether Membership Screening is enabled */
+ enabled?: boolean;
+ /** array of field objects */
+ formFields?: MembershipScreeningField[];
+ /** the steps in the screening form */
+ description?: string;
+}
+
+export interface MembershipScreeningField {
+ /** the type of field */
+ fieldType: MembershipScreeningFieldTypes;
+ /** the title of the field */
+ label: string;
+ /** the list of rules */
+ values?: string[];
+ /** whether the user has to fill out this field */
+ required: boolean;
+}
diff --git a/src/api/handlers/mod.ts b/src/api/handlers/mod.ts
index 93083ba57..ddd9070a0 100644
--- a/src/api/handlers/mod.ts
+++ b/src/api/handlers/mod.ts
@@ -57,6 +57,7 @@ import {
getIntegrations,
getInvites,
getMember,
+ getMembers,
getMembersByQuery,
getPruneCount,
getRoles,
@@ -183,6 +184,7 @@ export let handlers = {
getIntegrations,
getInvites,
getMember,
+ getMembers,
getTemplate,
getMembersByQuery,
getPruneCount,
diff --git a/src/types/discord.ts b/src/types/discord.ts
index 904dcb3fb..0cc6f741a 100644
--- a/src/types/discord.ts
+++ b/src/types/discord.ts
@@ -1,4 +1,9 @@
-import { CreateGuildPayload, PartialUser, UserPayload } from "./guild.ts";
+import {
+ CreateGuildPayload,
+ Integration,
+ PartialUser,
+ UserPayload,
+} from "./guild.ts";
import { MemberCreatePayload } from "./member.ts";
import { Activity, Application } from "./message.ts";
import { ClientStatusPayload } from "./presence.ts";
@@ -13,6 +18,8 @@ export interface DiscordPayload {
/** The event name for this payload. ONLY for OPCode 0 */
t?:
| "APPLICATION_COMMAND_CREATE"
+ | "APPLICATION_COMMAND_UPDATE"
+ | "APPLICATION_COMMAND_DELETE"
| "CHANNEL_CREATE"
| "CHANNEL_DELETE"
| "CHANNEL_UPDATE"
@@ -43,7 +50,10 @@ export interface DiscordPayload {
| "TYPING_START"
| "USER_UPDATE"
| "VOICE_STATE_UPDATE"
- | "WEBHOOKS_UPDATE";
+ | "WEBHOOKS_UPDATE"
+ | "INTEGRATION_CREATE"
+ | "INTEGRATION_UPDATE"
+ | "INTEGRATION_DELETE";
}
export interface DiscordBotGatewayData {
@@ -301,3 +311,17 @@ export type UnavailableGuildPayload = Pick<
CreateGuildPayload,
"id" | "unavailable"
>;
+
+export type IntegrationCreateUpdateEvent = Integration & {
+ /** id of the guild */
+ guild_id: string;
+};
+
+export interface IntegrationDeleteEvent {
+ /** integration id */
+ id: string;
+ /** id of the guild */
+ guild_id: string;
+ /** id of the bot/OAuth2 application for this discord integration */
+ application_id?: string;
+}
diff --git a/src/types/guild.ts b/src/types/guild.ts
index a9278e6d2..a9b2faaad 100644
--- a/src/types/guild.ts
+++ b/src/types/guild.ts
@@ -2,7 +2,7 @@ import { Guild } from "../api/structures/mod.ts";
import { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
import { Emoji, StatusType } from "./discord.ts";
import { MemberCreatePayload } from "./member.ts";
-import { Activity } from "./message.ts";
+import { Activity, Application } from "./message.ts";
import { Permission } from "./permission.ts";
import { ClientStatusPayload } from "./presence.ts";
import { RoleData } from "./role.ts";
@@ -49,6 +49,8 @@ export interface GuildMemberUpdatePayload {
nick: string;
/** When the user used their nitro boost on the guild. */
premium_since: string | null;
+ /** whether the user has not yet passed the guild's Membership Screening requirements */
+ pending?: boolean;
}
export interface GuildMemberAddPayload extends MemberCreatePayload {
@@ -172,7 +174,11 @@ export type GuildFeatures =
| "DISCOVERABLE"
| "FEATURABLE"
| "ANIMATED_ICON"
- | "BANNER";
+ | "BANNER"
+ /** guild has enabled Membership Screening */
+ | "MEMBER_VERIFICATION_GATE_ENABLED"
+ /** guild can be previewed before joining via Membership Screening or the directory */
+ | "PREVIEW_ENABLED";
export interface VoiceRegion {
/** unique ID for the region */
@@ -246,7 +252,7 @@ export interface EditIntegrationOptions {
enable_emoticons: boolean;
}
-export interface GuildIntegration {
+export interface Integration {
/** The integrations unique id */
id: string;
/** the integrations name */
@@ -256,19 +262,32 @@ export interface GuildIntegration {
/** Is this integration enabled */
enabled: boolean;
/** is this integration syncing */
- syncing: boolean;
+ syncing?: boolean;
/** id that this integration uses for "subscribers" */
- role_id: string;
+ role_id?: string;
+ /** whether emoticons should be synced for this integration (twitch only currently) */
+ enable_emoticons?: boolean;
/** The behavior of expiring subscribers */
- expire_behavior: number;
+ expire_behavior?: IntegrationExpireBehaviors;
/** The grace period before expiring subscribers */
- expire_grace_period: number;
+ expire_grace_period?: number;
/** The user for this integration */
- user: UserPayload;
+ user?: UserPayload;
/** The integration account information */
account: Account;
/** When this integration was last synced */
- synced_at: string;
+ synced_at?: string;
+ /** how many subscribers this integration has */
+ subscriber_count?: number;
+ /** has this integration been revoked */
+ revoked?: boolean;
+ /** The bot/OAuth2 application for discord integrations */
+ application?: Application;
+}
+
+export enum IntegrationExpireBehaviors {
+ RemoveRole,
+ Kick,
}
export interface Account {
@@ -589,6 +608,13 @@ export interface FetchMembersOptions {
limit?: number;
}
+export interface GetMemberOptions {
+ /** max number of members to return (1-1000), defaults to 1 */
+ limit?: number;
+ /** the highest user id in the previous page */
+ after?: string;
+}
+
export interface CreateServerOptions {
/** name of the guild (2-100 characters) */
name: string;
@@ -660,3 +686,27 @@ export interface EditGuildTemplate {
/** description for the template (0-120 characters) */
description?: string | null;
}
+
+export interface MembershipScreeningPayload {
+ /** when the fields were last updated */
+ version: string;
+ /** the steps in the screening form */
+ form_fields: MembershipScreeningFieldPayload[];
+ /** the server description shown in the screening form */
+ description: string | null;
+}
+
+export interface MembershipScreeningFieldPayload {
+ /** the type of field */
+ field_type: MembershipScreeningFieldTypes;
+ /** the title of the field */
+ label: string;
+ /** the list of rules */
+ values?: string[];
+ /** whether the user has to fill out this field */
+ required: boolean;
+}
+
+export type MembershipScreeningFieldTypes =
+ /** Server Rules */
+ "TERMS";
diff --git a/src/types/interactions.ts b/src/types/interactions.ts
index d1cdf741b..c78be8ed3 100644
--- a/src/types/interactions.ts
+++ b/src/types/interactions.ts
@@ -41,3 +41,60 @@ export interface InteractionDataOption {
/** present if this option is a group or subcommand */
options?: InteractionDataOption[];
}
+
+/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommand */
+export interface ApplicationCommand {
+ /** unique id of the command */
+ id: string;
+ /** unique id of the parent application */
+ application_id: string;
+ /** 3-32 character name matching `^[\w-]{3,32}$` */
+ name: string;
+ /** 1-100 character description */
+ description: string;
+ /** the parameters for the command */
+ options?: ApplicationCommandOption[];
+}
+
+/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption */
+export interface ApplicationCommandOption {
+ /** the type of the option */
+ type: ApplicationCommandOptionType;
+ /** 1-32 character name matching `^[\w-]{1,32}$` */
+ name: string;
+ /** 1-100 character description */
+ description: string;
+ /** the first `required` option for the user to complete--only one option can be `default` */
+ default?: boolean;
+ /** if the parameter is required or optional--default `false` */
+ required?: boolean;
+ /** choices for `string` and `int` types for the user to pick from */
+ choices?: ApplicationCommandOptionChoice[];
+ /** if the option is a subcommand or subcommand group type, this nested options will be the parameters */
+ options?: ApplicationCommandOption[];
+}
+
+/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype */
+export enum ApplicationCommandOptionType {
+ SUB_COMMAND = 1,
+ SUB_COMMAND_GROUP,
+ STRING,
+ INTEGER,
+ BOOLEAN,
+ USER,
+ CHANNEL,
+ ROLE,
+}
+
+/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */
+export interface ApplicationCommandOptionChoice {
+ /** 1-100 character choice name */
+ name: string;
+ /** value of the choice */
+ value: string | number;
+}
+
+export type ApplicationCommandEvent = ApplicationCommand & {
+ /** id of the guild the command is in */
+ guild_id?: string;
+};
diff --git a/src/types/message.ts b/src/types/message.ts
index 606655b01..e957c51ef 100644
--- a/src/types/message.ts
+++ b/src/types/message.ts
@@ -188,6 +188,10 @@ export interface Application {
icon: string | null;
/** The name of the application */
name: string;
+ /** the description of the app */
+ summary: string;
+ /** the bot associated with this application */
+ bot?: UserPayload;
}
export interface Reference {
diff --git a/src/types/options.ts b/src/types/options.ts
index e5c566527..0ec4008d2 100644
--- a/src/types/options.ts
+++ b/src/types/options.ts
@@ -8,14 +8,18 @@ import {
import {
DiscordPayload,
Emoji,
+ IntegrationCreateUpdateEvent,
+ IntegrationDeleteEvent,
PresenceUpdatePayload,
TypingStartPayload,
VoiceStateUpdatePayload,
} from "./discord.ts";
import { UserPayload } from "./guild.ts";
-import { InteractionCommandPayload } from "./interactions.ts";
import {
- Application,
+ ApplicationCommandEvent,
+ InteractionCommandPayload,
+} from "./interactions.ts";
+import {
Attachment,
BaseMessageReactionPayload,
Embed,
@@ -24,6 +28,7 @@ import {
PartialMessage,
ReactionPayload,
} from "./message.ts";
+import { Camelize } from "./util.ts";
export interface BotConfig {
token: string;
@@ -89,7 +94,18 @@ interface RateLimitData {
export interface EventHandlers {
rateLimit?: (data: RateLimitData) => unknown;
- applicationCommandCreate?: (data: Application) => unknown;
+ /** Sent when a new Slash Command is created, relevant to the current user. */
+ applicationCommandCreate?: (
+ data: Camelize,
+ ) => unknown;
+ /** Sent when a Slash Command relevant to the current user is updated. */
+ applicationCommandUpdate?: (
+ data: Camelize,
+ ) => unknown;
+ /** Sent when a Slash Command relevant to the current user is deleted. */
+ applicationCommandDelete?: (
+ data: Camelize,
+ ) => unknown;
/** Sent when properties about the user change. */
botUpdate?: (user: UserPayload) => unknown;
/** Sent when a new guild channel is created, relevant to the current user. */
@@ -208,6 +224,14 @@ export interface EventHandlers {
) => unknown;
/** Sent when a guild channel's webhook is created, updated, or deleted. */
webhooksUpdate?: (channelID: string, guildID: string) => unknown;
+ /** Sent when a member has passed the guild's Membership Screening requirements */
+ membershipScreeningPassed?: (guild: Guild, member: Member) => unknown;
+ /** Sent when an integration is created on a server such as twitch, youtube etc.. */
+ integrationCreate?: (data: Camelize) => unknown;
+ /** Sent when an integration is updated. */
+ integrationUpdate?: (data: Camelize) => unknown;
+ /** Sent when an integration is deleted. */
+ integrationDelete?: (data: Camelize) => undefined;
}
/** https://discord.com/developers/docs/topics/gateway#list-of-intents */
@@ -241,6 +265,9 @@ export enum Intents {
GUILD_EMOJIS = 1 << 3,
/** Enables the following events:
* - GUILD_INTEGRATIONS_UPDATE
+ * - INTEGRATION_CREATE
+ * - INTEGRATION_UPDATE
+ * - INTEGRATION_DELETE
*/
GUILD_INTEGRATIONS = 1 << 4,
/** Enables the following events:
@@ -297,5 +324,3 @@ export enum Intents {
*/
DIRECT_MESSAGE_TYPING = 1 << 14,
}
-
-export type ValueOf = T[keyof T];
diff --git a/src/types/util.ts b/src/types/util.ts
new file mode 100644
index 000000000..43af12643
--- /dev/null
+++ b/src/types/util.ts
@@ -0,0 +1,9 @@
+export type CamelizeString = T extends string
+ ? string extends T ? string
+ : T extends `${infer F}_${infer R}`
+ ? `${F}${T extends `${infer F}_id` ? Uppercase
+ : Capitalize>}`
+ : T
+ : T;
+
+export type Camelize = { [K in keyof T as CamelizeString]: T[K] }
diff --git a/src/util/constants.ts b/src/util/constants.ts
index afad4433c..c48fc2e03 100644
--- a/src/util/constants.ts
+++ b/src/util/constants.ts
@@ -116,6 +116,8 @@ export const endpoints = {
`${baseEndpoints.BASE_URL}/guilds/templates/${code}`,
GUILD_TEMPLATES: (guildID: string) => `${GUILDS_BASE(guildID)}/templates`,
GUILD_PREVIEW: (guildID: string) => `${GUILDS_BASE(guildID)}/preview`,
+ GUILD_MEMBER_VERIFICATION: (guildID: string) =>
+ `${GUILDS_BASE(guildID)}/member-verification`,
// Voice
VOICE_REGIONS: `${baseEndpoints.BASE_URL}/voice/regions`,
diff --git a/src/util/permissions.ts b/src/util/permissions.ts
index 9b1b57723..97a97ac27 100644
--- a/src/util/permissions.ts
+++ b/src/util/permissions.ts
@@ -31,7 +31,7 @@ export function memberHasPermission(
) {
if (memberID === guild.ownerID) return true;
- const permissionBits = memberRoleIDs.map((id) =>
+ const permissionBits = [guild.id, ...memberRoleIDs].map((id) =>
guild.roles.get(id)?.permissions
)
// Removes any edge case undefined
diff --git a/test/mod.test.ts b/test/mod.test.ts
index e877e2197..15fca7706 100644
--- a/test/mod.test.ts
+++ b/test/mod.test.ts
@@ -4,6 +4,7 @@ import {
deleteServer,
getChannel,
} from "../src/api/handlers/guild.ts";
+import { eventHandlers } from "../src/bot.ts";
import {
addReaction,
assertEquals,
@@ -31,18 +32,11 @@ import {
unpin,
} from "./deps.ts";
-const token = Deno.env.get("DISCORD_TOKEN");
-if (!token) throw new Error("Token is not provided");
-
-startBot({
- token,
- intents: ["GUILD_MESSAGES", "GUILDS"],
-});
-
// Default options for tests
-export const defaultTestOptions = {
+export const defaultTestOptions: Partial = {
sanitizeOps: false,
sanitizeResources: false,
+ ignore: Deno.env.get("TEST_TYPE") !== "api",
};
// Temporary data
@@ -56,7 +50,23 @@ export const tempData = {
// Main
Deno.test({
name: "[main] connect to gateway",
- fn: async () => {
+ async fn() {
+ const token = Deno.env.get("DISCORD_TOKEN");
+ if (!token) throw new Error("Token is not provided");
+
+ await startBot({
+ token,
+ intents: ["GUILD_MESSAGES", "GUILDS"],
+ });
+
+ eventHandlers.ready = () => {
+ if (cache.guilds.size >= 10) {
+ cache.guilds.map((guild) =>
+ guild.ownerID === botID && deleteServer(guild.id)
+ );
+ }
+ };
+
// Delay the execution by 5 seconds
await delay(5000);
@@ -185,6 +195,7 @@ Deno.test({
assertExists(channel);
assertEquals(channel.name, "discordeno-test-edited");
},
+ ...defaultTestOptions,
});
Deno.test({
@@ -244,6 +255,7 @@ Deno.test({
assertExists(message);
assertEquals(message.embeds[0].title, "Discordeno Test");
},
+ ...defaultTestOptions,
});
Deno.test({
@@ -318,6 +330,7 @@ Deno.test({
async fn() {
await deleteRole(tempData.guildID, tempData.roleID);
},
+ ...defaultTestOptions,
});
Deno.test({
@@ -339,5 +352,4 @@ Deno.test({
fn() {
Deno.exit();
},
- ...defaultTestOptions,
});