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) [![Discord](https://img.shields.io/discord/785384884197392384?color=7289da&logo=discord&logoColor=dark)](https://discord.com/invite/5vBgXk3UcZ) ![Lint](https://github.com/discordeno/discordeno/workflows/Lint/badge.svg) @@ -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, });