Compare commits

...

63 Commits
v3.0.0 ... main

Author SHA1 Message Date
Drylozu
9ae4290ef2
fix: subcommand group handling 2025-07-15 00:20:34 -05:00
20cca8a71b
fix: nulleable nick 2025-07-13 20:19:37 -04:00
ab080fb89d
fix: edit current member 2025-07-13 19:52:09 -04:00
b597d9fc69
fix: revert undefined 2025-07-11 11:40:35 -04:00
83e8e2165b
fix: bot id's undefined when not ready 2025-07-11 11:30:20 -04:00
dd9859a102
fix: subscription event 2025-07-07 14:05:40 -04:00
6d5b162a2d
chore: bump version to 3.2.4 2025-06-23 10:13:49 -04:00
a15eb2035f
Merge branch 'main' of https://github.com/tiramisulabs/seyfert 2025-06-23 10:04:21 -04:00
c4d1c1ce90
fix(api): handle 429 in shared scopes 2025-06-23 10:04:07 -04:00
eadf63450b chore: bump version to 3.2.3 2025-06-22 23:55:03 -04:00
0720d0c170 fix: accept null as content value 2025-06-22 00:20:52 -04:00
798f648955
feat: waitFor modals (#346)
* feat: modal#waitFor

* refactor: update waitFor method to improve promise handling and timeout logic

* feat: enhance modal handling with options for wait time and improved promise resolution

---------

Co-authored-by: MARCROCK22 <marcos22dev@gmail.com>
2025-06-19 23:12:42 -04:00
6e037ae964 chore: bump version to 3.2.2 2025-06-17 16:48:01 -04:00
0608c7ad85 fix: i dont know why it was like that 2025-06-17 16:47:46 -04:00
db38f49ca9 chore: bump 3.2.1 2025-06-17 16:19:49 -04:00
325a39f1bf ci: add npm token verification step in dev workflow 2025-06-14 19:42:42 -04:00
e810b6eb52 refactor: update customId matching logic and rename asyncRun to waitFor 2025-06-14 19:24:53 -04:00
Free 公園
b84d462ce2
feat: use regex in customId component command (#345) 2025-06-13 22:08:47 -04:00
61413181bb Merge branch 'main' of https://github.com/tiramisulabs/seyfert 2025-06-13 20:34:56 -04:00
30a066e68d feat: awaitable collector 2025-06-13 20:34:43 -04:00
95619a8a89 refactor: change listActiveThreads method to remove async keyword 2025-06-13 13:38:26 -04:00
84806f3c54 Merge branch 'main' of https://github.com/tiramisulabs/seyfert 2025-06-13 13:30:49 -04:00
422cfb2a80 feat: list active threads 2025-06-13 04:11:07 -04:00
David
3f6c6dc4d4
fix: add error handling for undefined locale (#344) 2025-06-13 01:11:56 -04:00
d6a405469d fix: revert, rerun later 2025-06-12 22:32:09 -04:00
1903257e46 fix: streamline publish workflow by removing unnecessary steps 2025-06-12 22:27:50 -04:00
0b00e2d19b fix: add id-token write permission to publish workflow 2025-06-12 22:23:18 -04:00
970ed980cf chore: update version 3.2.0 2025-06-12 22:14:20 -04:00
935cce99f6 fix: mentionableMenu accept channels 2025-06-12 18:12:53 -04:00
5de23ffe58
fix: Error with ThreadChannel#webhooks for bad patchClass interaction (#341) 2025-05-23 14:53:50 -04:00
e4f715515c fix: the fix 2025-05-23 06:43:57 -04:00
7998577b07 fix: workers correctly assing ws properties 2025-05-23 06:15:08 -04:00
05cdc20d7f fix: remove unused ObjectToLower type declarations from index.ts 2025-05-22 17:02:06 -04:00
e095edd20f fix: update ObjectToLower and ObjectToSnake types to preserve numeric keys 2025-05-22 17:01:12 -04:00
d9aef4335d fix: xd 2025-05-20 20:20:11 -04:00
3350570334 fix: make pendingGuilds optional and update related logic for guild handling 2025-05-19 18:16:47 -04:00
c20f2fd0a3 feat: implement heartbeater for managing worker heartbeat messages 2025-05-17 12:58:39 -04:00
0d8ad177b7 fix: replace ws.removeListener with ws.removeEventListener for 'pong' event 2025-05-16 08:46:25 -04:00
089cfab6da fix: replace ws.on with ws.addEventListener for 'pong' event 2025-05-16 08:18:52 -04:00
fb9c59a51b feat: add resetTimeouts method to ComponentHandler 2025-05-12 18:12:10 -04:00
92ab65be7b feat: make deferReplyResponse and reply awaitable 2025-05-08 21:09:15 -04:00
23c9c2a710 feat: workerClient.calculateShardId 2025-05-07 18:41:46 -04:00
b4324f9487 fix: type error 2025-05-05 20:01:16 -04:00
f74b75fcef fix: change return types from 'never' to 'undefined' for various API methods 2025-04-29 08:30:01 -04:00
9cbde76ad0 fix: update promise return types to undefined for delete and post methods 2025-04-29 08:25:05 -04:00
66b5ca34a9 fix: dx 2025-04-28 04:04:35 -04:00
4574ca018f
fix: xd 2025-04-28 03:44:17 -04:00
34ca3e293e
fix(roles): delete don't return role anymore 2025-04-28 03:42:46 -04:00
a358bb0a04 chore: this thing 2025-04-27 21:30:28 -04:00
6cf75e9ad2 chore: 3.1.2 2025-04-27 21:24:24 -04:00
60108b4c42 fix: empty data 2025-04-27 16:25:30 -04:00
e8c686a1df chore: update dependencies 2025-04-27 14:27:21 -04:00
c5ae765263 chore: package version 2025-04-27 14:23:12 -04:00
e8e89e60a8 fix: check if attachments is an array 2025-04-27 13:02:53 -04:00
5a90b7edb4 fix: type typo 2025-04-27 12:26:47 -04:00
f4bcaa58ec fix: components type 2025-04-27 12:19:30 -04:00
e3b6f57741
chore: 3.1.0 (#339)
* perf: optimize members cache

* feat: components V2 (#337)

* feat: components v2

* fix: build

* chore: apply formatting

* refactor(components): some types

* refactor(types): replace TopLevelComponents with APITopLevelComponent in REST

* fix: unify components

* refactor(TextDisplay): rename content method to setContent for clarity

* refactor(builders): add missing builder from component

* fix: touche

* feat(webhook): webhook params for components v2

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: use protected instead of private

* fix(editOrReply): accept flags when editing message

* feat: add onBeforeMiddlewares and onBeforeOptions (#338)

* chore: package version

---------

Co-authored-by: MARCROCK22 <marcos22dev@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: MARCROCK22 <57925328+MARCROCK22@users.noreply.github.com>
2025-04-27 01:21:56 -04:00
7baeedd67c chore: package version 2025-04-27 01:19:38 -04:00
MARCROCK22
ecc007b438
feat: add onBeforeMiddlewares and onBeforeOptions (#338) 2025-04-26 18:17:38 -04:00
53231465c0 fix(editOrReply): accept flags when editing message 2025-04-26 11:28:20 -04:00
faa222275f fix: use protected instead of private 2025-04-25 00:51:59 -04:00
589a59c5f6
feat: components V2 (#337)
* feat: components v2

* fix: build

* chore: apply formatting

* refactor(components): some types

* refactor(types): replace TopLevelComponents with APITopLevelComponent in REST

* fix: unify components

* refactor(TextDisplay): rename content method to setContent for clarity

* refactor(builders): add missing builder from component

* fix: touche

* feat(webhook): webhook params for components v2

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-25 00:10:09 -04:00
f03eb57ed5 perf: optimize members cache 2025-04-23 20:58:23 -04:00
128 changed files with 2711 additions and 1865 deletions

View File

@ -12,8 +12,10 @@ jobs:
permissions:
id-token: write
if: github.repository == 'tiramisulabs/seyfert'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: check out code
- name: Check out code
uses: actions/checkout@v4
- name: Install Node
@ -29,12 +31,13 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Verify npm token
run: npm whoami
- name: Publish dev tag
id: publish
run: |
new_version=$(npm version prerelease --preid dev-${{github.run_id}} --no-git-tag-version)
echo "New version: $new_version"
npm config set //registry.npmjs.org/:_authToken ${NODE_AUTH_TOKEN}
npm publish --provenance --tag=dev
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
npm publish --provenance --tag=dev

View File

@ -1,80 +0,0 @@
name: Create Release
on:
push:
branches:
- build
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: check out code
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Get version from package.json
id: get_version
run: echo "::set-output name=version::$(node -p "require('./package.json').version")"
- name: Set repository variable
run: echo "REPO=${{ github.repository }}" >> $GITHUB_ENV
- name: Get and format commits
run: |
last_tag=$(git tag --sort=-v:refname | head -n 1)
if [ -z "$last_tag" ]; then
git log --pretty=format:"%h - %s" > commits.txt
else
git log ${last_tag}..HEAD --pretty=format:"%h - %s" > commits.txt
fi
sed -e "s|^\([a-f0-9]\+\) - \(.*\)|- [\1](https://github.com/$REPO/commit/\1) - \2|" commits.txt > formatted_commits.txt
echo "Changes in this release:" > release_body.txt
cat formatted_commits.txt >> release_body.txt
- name: Create tag
run: |
git tag v${{ steps.get_version.outputs.version }}
git push origin v${{ steps.get_version.outputs.version }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.get_version.outputs.version }}
release_name: Release v${{ steps.get_version.outputs.version }}
body_path: release_body.txt
draft: false
prerelease: false
- name: Zip dist folder
run: zip -r dist.zip src
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist.zip
asset_name: dist.zip
asset_content_type: application/zip

View File

@ -1 +1 @@
npx lint-staged
npx biome check --write

View File

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@ -8,7 +8,16 @@
},
"files": {
"ignoreUnknown": true,
"ignore": ["node_modules/", "build", "lib", "__test__", "package.json", "tsconfig.json", ".vscode"]
"includes": [
"**/src/**",
"!/node_modules/",
"!/build",
"!/lib",
"!/__test__",
"!/package.json",
"!/tsconfig.json",
"!/.vscode"
]
},
"formatter": {
"enabled": true,
@ -18,14 +27,17 @@
"lineEnding": "crlf",
"formatWithErrors": true
},
"organizeImports": {
"enabled": true
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"all": true,
"security": {
"noGlobalEval": "off"
},
@ -41,9 +53,7 @@
},
"correctness": {
"noNodejsModules": "off",
"useImportExtensions": "off",
"noUnusedFunctionParameters": "off",
"noUnusedVariables": "off"
"useImportExtensions": "off"
},
"style": {
"noDefaultExport": "off",
@ -56,8 +66,7 @@
"noParameterAssign": "off",
"useFilenamingConvention": "off",
"useEnumInitializers": "off",
"useExplicitLengthCheck": "off",
"noNamespaceImport": "off"
"useExplicitLengthCheck": "off"
},
"complexity": {
"noForEach": "off",
@ -69,7 +78,8 @@
"noBarrelFile": "off",
"noDelete": "off",
"noReExportAll": "off",
"useTopLevelRegex": "off"
"useTopLevelRegex": "off",
"noNamespaceImport": "off"
}
}
},

View File

@ -1,6 +1,6 @@
{
"name": "seyfert",
"version": "3.0.0",
"version": "3.2.4",
"description": "The most advanced framework for discord bots",
"main": "./lib/index.js",
"module": "./lib/index.js",
@ -21,15 +21,14 @@
"author": "MARCROCK22",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@changesets/cli": "^2.28.1",
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@types/node": "^22.14.0",
"@biomejs/biome": "2.0.0",
"@changesets/cli": "^2.29.4",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@types/node": "^24.0.3",
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"typescript": "^5.8.2",
"vitest": "^3.1.1"
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"homepage": "https://seyfert.dev",
"repository": {
@ -65,11 +64,6 @@
"url": "https://github.com/Drylozu"
}
],
"lint-staged": {
"*.ts": [
"biome check --write"
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",

1311
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -71,7 +71,7 @@ export interface ApplicationRoutes {
patch(
args: RestArguments<RESTPatchAPIApplicationGuildCommandJSONBody>,
): Promise<RESTPatchAPIApplicationGuildCommandResult>;
delete(args?: RestArgumentsNoBody): Promise<never>;
delete(args?: RestArgumentsNoBody): Promise<undefined>;
permissions: {
get(args?: RestArgumentsNoBody): Promise<RESTGetAPIGuildApplicationCommandsPermissionsResult>;
put(
@ -92,7 +92,7 @@ export interface ApplicationRoutes {
patch(
args: RestArguments<RESTPatchAPIApplicationCommandJSONBody>,
): Promise<RESTPatchAPIApplicationCommandResult>;
delete(args?: RestArgumentsNoBody): Promise<never>;
delete(args?: RestArgumentsNoBody): Promise<undefined>;
};
};
'role-connections': {
@ -122,10 +122,10 @@ export interface ApplicationRoutes {
(
id: string,
): {
get(args?: RestArgumentsNoBody<never>): Promise<RESTGetAPIEntitlementResult>;
delete(args?: RestArgumentsNoBody): Promise<never>;
get(args?: RestArgumentsNoBody): Promise<RESTGetAPIEntitlementResult>;
delete(args?: RestArgumentsNoBody): Promise<undefined>;
consume: {
post(args?: RestArgumentsNoBody): Promise<never>;
post(args?: RestArgumentsNoBody): Promise<undefined>;
};
};
};

View File

@ -190,7 +190,7 @@ export interface ChannelRoutes {
post(args: RestArguments<RESTPostAPIChannelWebhookJSONBody>): Promise<RESTPostAPIChannelWebhookResult>;
};
'voice-status': {
put(args: RestArguments<{ status: string | null }>): Promise<never>;
put(args: RestArguments<{ status: string | null }>): Promise<undefined>;
};
polls(messageId: string): {
answers(id: ValidAnswerId): {
@ -201,7 +201,7 @@ export interface ChannelRoutes {
};
};
'send-soundboard-sound': {
post(args: RestArguments<RESTPostAPISendSoundboardSound>): Promise<never>;
post(args: RestArguments<RESTPostAPISendSoundboardSound>): Promise<undefined>;
};
};
}

View File

@ -39,10 +39,10 @@ import type {
RESTGetAPIGuildRolesResult,
RESTGetAPIGuildScheduledEventQuery,
RESTGetAPIGuildScheduledEventResult,
RESTGetAPIGuildScheduledEventUsersQuery,
RESTGetAPIGuildScheduledEventUsersResult,
RESTGetAPIGuildScheduledEventsQuery,
RESTGetAPIGuildScheduledEventsResult,
RESTGetAPIGuildScheduledEventUsersQuery,
RESTGetAPIGuildScheduledEventUsersResult,
RESTGetAPIGuildSoundboardSoundsResult,
RESTGetAPIGuildStickerResult,
RESTGetAPIGuildStickersResult,
@ -107,12 +107,12 @@ import type {
RESTPostAPIGuildSoundboardSoundResult,
RESTPostAPIGuildStickerFormDataBody,
RESTPostAPIGuildStickerResult,
RESTPostAPIGuildTemplatesJSONBody,
RESTPostAPIGuildTemplatesResult,
RESTPostAPIGuildsJSONBody,
RESTPostAPIGuildsMFAJSONBody,
RESTPostAPIGuildsMFAResult,
RESTPostAPIGuildsResult,
RESTPostAPIGuildTemplatesJSONBody,
RESTPostAPIGuildTemplatesResult,
RESTPostAPITemplateCreateGuildJSONBody,
RESTPostAPITemplateCreateGuildResult,
RESTPutAPIGuildBanJSONBody,
@ -361,7 +361,7 @@ export interface GuildRoutes {
patch(
args?: RestArguments<RESTPatchAPIGuildSoundboardSound>,
): Promise<RESTPatchAPIGuildSoundboardSoundResult>;
delete(args?: RestArgumentsNoBody): Promise<never>;
delete(args?: RestArgumentsNoBody): Promise<undefined>;
};
};
};

View File

@ -9,6 +9,7 @@ import type {
RESTPatchAPIWebhookResult,
RESTPatchAPIWebhookWithTokenJSONBody,
RESTPatchAPIWebhookWithTokenMessageJSONBody,
RESTPatchAPIWebhookWithTokenMessageQuery,
RESTPatchAPIWebhookWithTokenMessageResult,
RESTPatchAPIWebhookWithTokenResult,
RESTPostAPIWebhookWithTokenGitHubQuery,
@ -33,7 +34,9 @@ export interface WebhookRoutes {
token: string,
): {
get(args?: RestArgumentsNoBody): Promise<RESTGetAPIWebhookWithTokenResult>;
patch(args: RestArguments<RESTPatchAPIWebhookWithTokenJSONBody>): Promise<RESTPatchAPIWebhookWithTokenResult>;
patch(
args: RestArguments<RESTPatchAPIWebhookWithTokenJSONBody, RESTPatchAPIWebhookWithTokenMessageQuery>,
): Promise<RESTPatchAPIWebhookWithTokenResult>;
delete(args?: RestArgumentsNoBody): Promise<RESTDeleteAPIWebhookWithTokenResult>;
post(
args: RestArguments<RESTPostAPIWebhookWithTokenJSONBody, RESTPostAPIWebhookWithTokenQuery>,

View File

@ -1,11 +1,11 @@
import { type UUID, randomUUID } from 'node:crypto';
import { type Awaitable, BASE_HOST, Logger, delay, lazyLoadPackage, snowflakeToTimestamp } from '../common';
import { randomUUID, type UUID } from 'node:crypto';
import { type Awaitable, BASE_HOST, delay, Logger, lazyLoadPackage, snowflakeToTimestamp } from '../common';
import { toArrayBuffer, toBuffer } from '../common/it/utils';
import type { WorkerData } from '../websocket';
import type { WorkerSendApiRequest } from '../websocket/discord/worker';
import { Bucket } from './bucket';
import { CDNRouter, Router } from './Router';
import type { APIRoutes } from './Routes';
import { Bucket } from './bucket';
import {
type ApiHandlerInternalOptions,
type ApiHandlerOptions,
@ -98,13 +98,13 @@ export class ApiHandler {
}
}
#randomUUID(): UUID {
randomUUID(): UUID {
const uuid = randomUUID();
if (this.workerPromises!.has(uuid)) return this.#randomUUID();
if (this.workerPromises!.has(uuid)) return this.randomUUID();
return uuid;
}
private sendMessage(_body: WorkerSendApiRequest) {
protected sendMessage(_body: WorkerSendApiRequest) {
throw new Error('Function not implemented');
}
@ -121,7 +121,7 @@ export class ApiHandler {
{ auth = true, ...request }: ApiRequestOptions = {},
): Promise<T> {
if (this.options.workerProxy) {
const nonce = this.#randomUUID();
const nonce = this.randomUUID();
return this.postMessage<T>({
method,
url,
@ -291,17 +291,19 @@ export class ApiHandler {
await this.onRatelimit?.(response, request);
const content = `${JSON.stringify(request)} `;
let retryAfter =
let retryAfter: number | undefined;
const data = JSON.parse(result);
if (data.retry_after) retryAfter = Math.ceil(data.retry_after * 1000);
retryAfter ??=
Number(response.headers.get('x-ratelimit-reset-after') || response.headers.get('retry-after')) * 1000;
if (Number.isNaN(retryAfter)) {
try {
retryAfter = JSON.parse(result).retry_after * 1000;
} catch (err) {
this.debugger?.warn(`Unexpected error: ${err}`);
reject(err);
return false;
}
this.debugger?.warn(`${route} Could not extract retry_after from 429 response. ${result}`);
next();
reject(new Error('Could not extract retry_after from 429 response.'));
return false;
}
this.debugger?.info(
@ -384,11 +386,7 @@ export class ApiHandler {
}
}
parseRequest(options: {
url: string;
headers: RequestHeaders;
request: ApiRequestOptions;
}) {
parseRequest(options: { url: string; headers: RequestHeaders; request: ApiRequestOptions }) {
let finalUrl = options.url;
let data: string | FormData | undefined;
if (options.request.auth) {

View File

@ -1,4 +1,4 @@
export * from './api';
export * from './Router';
export * from './Routes';
export * from './api';
export * from './shared';

View File

@ -7,13 +7,13 @@ import {
} from '../types';
import { BaseComponentBuilder } from './Base';
import { fromComponent } from './index';
import type { BuilderComponents, FixedComponents } from './types';
import type { ActionBuilderComponents, FixedComponents } from './types';
/**
* Represents an Action Row component in a message.
* @template T - The type of components in the Action Row.
*/
export class ActionRow<T extends BuilderComponents> extends BaseComponentBuilder<
export class ActionRow<T extends ActionBuilderComponents = ActionBuilderComponents> extends BaseComponentBuilder<
APIActionRowComponent<APIActionRowComponentTypes>
> {
/**
@ -50,8 +50,8 @@ export class ActionRow<T extends BuilderComponents> extends BaseComponentBuilder
* @example
* actionRow.setComponents([buttonComponent1, buttonComponent2]);
*/
setComponents(component: FixedComponents<T>[]): this {
this.components = [...component];
setComponents(...component: RestOrArray<FixedComponents<T>>): this {
this.components = component.flat() as FixedComponents<T>[];
return this;
}

View File

@ -1,17 +1,14 @@
import { type EmojiResolvable, resolvePartialEmoji } from '../common';
import { type APIButtonComponent, type APIMessageComponentEmoji, type ButtonStyle, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a button component.
* @template Type - The type of the button component.
*/
export class Button {
/**
* Creates a new Button instance.
* @param data - The initial data for the button.
*/
constructor(public data: Partial<APIButtonComponent> = {}) {
this.data.type = ComponentType.Button;
export class Button extends BaseComponentBuilder<APIButtonComponent> {
constructor(data: Partial<APIButtonComponent> = {}) {
super({ type: ComponentType.Button, ...data });
}
/**
@ -76,12 +73,4 @@ export class Button {
(this.data as Extract<APIButtonComponent, { sku_id?: string }>).sku_id = skuId;
return this;
}
/**
* Converts the Button instance to its JSON representation.
* @returns The JSON representation of the Button instance.
*/
toJSON() {
return { ...this.data } as Partial<APIButtonComponent>;
}
}

100
src/builders/Container.ts Normal file
View File

@ -0,0 +1,100 @@
import { type ColorResolvable, type RestOrArray, resolveColor } from '../common';
import { type APIContainerComponent, ComponentType } from '../types';
import { type ActionRow, fromComponent } from '.';
import { BaseComponentBuilder } from './Base';
import type { File } from './File';
import type { MediaGallery } from './MediaGallery';
import type { Section } from './Section';
import type { Separator } from './Separator';
import type { TextDisplay } from './TextDisplay';
/**
* Represents the possible component types that can be added to a Container.
*/
export type ContainerBuilderComponents = ActionRow | TextDisplay | Section | MediaGallery | Separator | File;
/**
* Represents a container component builder.
* Containers group other components together.
* @example
* ```ts
* const container = new Container()
* .addComponents(
* new TextDisplay('This is text inside a container!'),
* new ActionRow().addComponents(new Button().setLabel('Click me!'))
* )
* .setColor('Blue');
* ```
*/
export class Container extends BaseComponentBuilder<APIContainerComponent> {
/**
* The components held within this container.
*/
components: ContainerBuilderComponents[];
/**
* Constructs a new Container.
* @param data Optional initial data for the container.
*/
constructor({ components, ...data }: Partial<APIContainerComponent> = {}) {
super({ ...data, type: ComponentType.Container });
this.components = (components?.map(fromComponent) ?? []) as ContainerBuilderComponents[];
}
/**
* Adds components to the container.
* @param components The components to add. Can be a single component, an array of components, or multiple components as arguments.
* @returns The updated Container instance.
*/
addComponents(...components: RestOrArray<ContainerBuilderComponents>) {
this.components = this.components.concat(components.flat());
return this;
}
/**
* Sets the components for the container, replacing any existing components.
* @param components The components to set. Can be a single component, an array of components, or multiple components as arguments.
* @returns The updated Container instance.
*/
setComponents(...components: RestOrArray<ContainerBuilderComponents>) {
this.components = components.flat();
return this;
}
/**
* Sets whether the container's content should be visually marked as a spoiler.
* @param spoiler Whether the content is a spoiler (defaults to true).
* @returns The updated Container instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Sets the accent color for the container.
* @param color The color resolvable (e.g., hex code, color name, integer).
* @returns The updated Container instance.
*/
setColor(color: ColorResolvable) {
this.data.accent_color = resolveColor(color);
return this;
}
/**
* Sets the ID for the container.
* @param id The ID to set.
* @returns The updated Container instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
toJSON() {
return {
...this.data,
components: this.components.map(c => c.toJSON()),
} as APIContainerComponent;
}
}

52
src/builders/File.ts Normal file
View File

@ -0,0 +1,52 @@
import { type APIFileComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a file component builder.
* Used to display files within containers.
* @example
* ```ts
* const file = new File()
* .setMedia('https://example.com/image.png')
* .setSpoiler();
* ```
*/
export class File extends BaseComponentBuilder<APIFileComponent> {
/**
* Constructs a new File component.
* @param data Optional initial data for the file component.
*/
constructor(data: Partial<APIFileComponent> = {}) {
super({ type: ComponentType.File, ...data });
}
/**
* Sets the ID for the file component.
* @param id The ID to set.
* @returns The updated File instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets the media URL for the file.
* @param url The URL of the file to display.
* @returns The updated File instance.
*/
setMedia(url: string) {
this.data.file = { url };
return this;
}
/**
* Sets whether the file should be visually marked as a spoiler.
* @param spoiler Whether the file is a spoiler (defaults to true).
* @returns The updated File instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
}

View File

@ -0,0 +1,112 @@
import type { RestOrArray } from '../common';
import { type APIMediaGalleryComponent, type APIMediaGalleryItems, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a media gallery component builder.
* Used to display a collection of media items.
* @example
* ```ts
* const gallery = new MediaGallery()
* .addItems(
* new MediaGalleryItem().setMedia('https://example.com/image1.png').setDescription('Image 1'),
* new MediaGalleryItem().setMedia('https://example.com/image2.jpg').setSpoiler()
* );
* ```
*/
export class MediaGallery extends BaseComponentBuilder<APIMediaGalleryComponent> {
items: MediaGalleryItem[];
/**
* Constructs a new MediaGallery.
* @param data Optional initial data for the media gallery.
*/
constructor({ items, ...data }: Partial<APIMediaGalleryComponent> = {}) {
super({ type: ComponentType.MediaGallery, ...data });
this.items = (items?.map(i => new MediaGalleryItem(i)) ?? []) as MediaGalleryItem[];
}
/**
* Sets the ID for the media gallery component.
* @param id The ID to set.
* @returns The updated MediaGallery instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Adds items to the media gallery.
* @param items The items to add. Can be a single item, an array of items, or multiple items as arguments.
* @returns The updated MediaGallery instance.
*/
addItems(...items: RestOrArray<MediaGalleryItem>) {
this.items = this.items.concat(items.flat());
return this;
}
/**
* Sets the items for the media gallery, replacing any existing items.
* @param items The items to set. Can be a single item, an array of items, or multiple items as arguments.
* @returns The updated MediaGallery instance.
*/
setItems(...items: RestOrArray<MediaGalleryItem>) {
this.items = items.flat();
return this;
}
toJSON() {
return {
...this.data,
items: this.items.map(i => i.toJSON()),
} as APIMediaGalleryComponent;
}
}
/**
* Represents an item within a MediaGallery.
*/
export class MediaGalleryItem {
/**
* Constructs a new MediaGalleryItem.
* @param data Optional initial data for the media gallery item.
*/
constructor(public data: Partial<APIMediaGalleryItems> = {}) {}
/**
* Sets the media URL for this gallery item.
* @param url The URL of the media.
* @returns The updated MediaGalleryItem instance.
*/
setMedia(url: string) {
this.data.media = { url };
return this;
}
/**
* Sets the description for this gallery item.
* @param desc The description text.
* @returns The updated MediaGalleryItem instance.
*/
setDescription(desc: string) {
this.data.description = desc;
return this;
}
/**
* Sets whether this gallery item should be visually marked as a spoiler.
* @param spoiler Whether the item is a spoiler (defaults to true).
* @returns The updated MediaGalleryItem instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Converts this MediaGalleryItem instance to its JSON representation.
* @returns The JSON representation of the item data.
*/
toJSON() {
return { ...this.data };
}
}

56
src/builders/Section.ts Normal file
View File

@ -0,0 +1,56 @@
import type { RestOrArray } from '../common';
import { type APISectionComponent, ComponentType } from '../types';
import { type Button, fromComponent } from '.';
import { BaseComponentBuilder } from './Base';
import type { TextDisplay } from './TextDisplay';
import type { Thumbnail } from './Thumbnail';
export class Section<
Ac extends Button | Thumbnail = Button | Thumbnail,
> extends BaseComponentBuilder<APISectionComponent> {
components: TextDisplay[];
accessory?: Ac;
constructor({ components, accessory, ...data }: Partial<APISectionComponent> = {}) {
super({ type: ComponentType.Section, ...data });
this.components = (components?.map(component => fromComponent(component)) ?? []) as TextDisplay[];
if (accessory) this.accessory = fromComponent(accessory) as Ac;
}
/**
* Adds components to this section.
* @param components The components to add
* @example section.addComponents(new TextDisplay().content('Hello'));
*/
addComponents(...components: RestOrArray<TextDisplay>) {
this.components = this.components.concat(components.flat());
return this;
}
/**
* Sets the components for this section.
* @param components The components to set
* @example section.setComponents(new TextDisplay().content('Hello'));
*/
setComponents(...components: RestOrArray<TextDisplay>) {
this.components = components.flat();
return this;
}
setAccessory(accessory: Ac) {
this.accessory = accessory;
return this;
}
/**
* Converts this section to JSON.
* @returns The JSON representation of this section
*/
toJSON() {
if (!this.accessory) throw new Error('Cannot convert to JSON without an accessory.');
return {
...this.data,
components: this.components.map(component => component.toJSON()),
accessory: this.accessory.toJSON(),
} as APISectionComponent;
}
}

View File

@ -161,7 +161,7 @@ export class RoleSelectMenu extends SelectMenu<APIRoleSelectComponent> {
}
}
export type MentionableDefaultElement = { id: string; type: keyof Omit<typeof SelectMenuDefaultValueType, 'Channel'> };
export type MentionableDefaultElement = { id: string; type: keyof typeof SelectMenuDefaultValueType };
/**
* Represents a Select Menu for selecting mentionable entities.

54
src/builders/Separator.ts Normal file
View File

@ -0,0 +1,54 @@
import { type APISeparatorComponent, ComponentType, type Spacing } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a separator component builder.
* Used to add visual spacing or dividers between components.
* @example
* ```ts
* // A simple separator for spacing
* const spacingSeparator = new Separator().setSpacing(Spacing.Small);
*
* // A separator acting as a visual divider
* const dividerSeparator = new Separator().setDivider(true);
* ```
*/
export class Separator extends BaseComponentBuilder<APISeparatorComponent> {
/**
* Constructs a new Separator component.
* @param data Optional initial data for the separator component.
*/
constructor(data: Partial<APISeparatorComponent> = {}) {
super({ type: ComponentType.Separator, ...data });
}
/**
* Sets the ID for the separator component.
* @param id The ID to set.
* @returns The updated Separator instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets whether this separator should act as a visual divider.
* @param divider Whether to render as a divider (defaults to false).
* @returns The updated Separator instance.
*/
setDivider(divider = false) {
this.data.divider = divider;
return this;
}
/**
* Sets the amount of spacing this separator provides.
* @param spacing The desired spacing level ('None', 'Small', 'Medium', 'Large').
* @returns The updated Separator instance.
*/
setSpacing(spacing: Spacing) {
this.data.spacing = spacing;
return this;
}
}

View File

@ -0,0 +1,40 @@
import { type APITextDisplayComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a text display component builder.
* Used to display simple text content.
* @example
* ```ts
* const text = new TextDisplay().content('Hello, world!');
* ```
*/
export class TextDisplay extends BaseComponentBuilder<APITextDisplayComponent> {
/**
* Constructs a new TextDisplay component.
* @param data Optional initial data for the text display component.
*/
constructor(data: Partial<APITextDisplayComponent> = {}) {
super({ type: ComponentType.TextDisplay, ...data });
}
/**
* Sets the ID for the text display component.
* @param id The ID to set.
* @returns The updated TextDisplay instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets the text content to display.
* @param content The text content.
* @returns The updated TextDisplay instance.
*/
setContent(content: string) {
this.data.content = content;
return this;
}
}

62
src/builders/Thumbnail.ts Normal file
View File

@ -0,0 +1,62 @@
import { type APIThumbnailComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a thumbnail component builder.
* Used to display a small image preview, often alongside other content.
* @example
* ```ts
* const thumbnail = new Thumbnail()
* .setMedia('https://example.com/thumbnail.jpg')
* .setDescription('A cool thumbnail');
* ```
*/
export class Thumbnail extends BaseComponentBuilder<APIThumbnailComponent> {
/**
* Constructs a new Thumbnail component.
* @param data Optional initial data for the thumbnail component.
*/
constructor(data: Partial<APIThumbnailComponent> = {}) {
super({ type: ComponentType.Thumbnail, ...data });
}
/**
* Sets whether the thumbnail should be visually marked as a spoiler.
* @param spoiler Whether the thumbnail is a spoiler (defaults to true).
* @returns The updated Thumbnail instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Sets the description for the thumbnail.
* @param description The description text. Can be undefined to remove the description.
* @returns The updated Thumbnail instance.
*/
setDescription(description: string | undefined) {
this.data.description = description;
return this;
}
/**
* Sets the ID for the thumbnail component.
* @param id The ID to set.
* @returns The updated Thumbnail instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets the media URL for the thumbnail.
* @param url The URL of the image to display as a thumbnail.
* @returns The updated Thumbnail instance.
*/
setMedia(url: string) {
this.data.media = { url };
return this;
}
}

View File

@ -1,7 +1,11 @@
import { type APIActionRowComponent, type APIActionRowComponentTypes, ComponentType } from '../types';
import { type APIComponents, ComponentType } from '../types';
import { ActionRow } from './ActionRow';
import { Button } from './Button';
import { Container } from './Container';
import { File } from './File';
import { MediaGallery } from './MediaGallery';
import { TextInput } from './Modal';
import { Section } from './Section';
import {
ChannelSelectMenu,
MentionableSelectMenu,
@ -9,29 +13,32 @@ import {
StringSelectMenu,
UserSelectMenu,
} from './SelectMenu';
import { Separator } from './Separator';
import { TextDisplay } from './TextDisplay';
import { Thumbnail } from './Thumbnail';
import type { BuilderComponents } from './types';
export * from './ActionRow';
export * from './Attachment';
export * from './Base';
export * from './Button';
export * from './Container';
export * from './Embed';
export * from './File';
export * from './MediaGallery';
export * from './Modal';
export * from './SelectMenu';
export * from './Poll';
export * from './Section';
export * from './SelectMenu';
export * from './Separator';
export * from './TextDisplay';
export * from './Thumbnail';
export * from './types';
export function fromComponent(
data:
| BuilderComponents
| APIActionRowComponentTypes
| APIActionRowComponent<APIActionRowComponentTypes>
| ActionRow<BuilderComponents>,
): BuilderComponents | ActionRow<BuilderComponents> {
export function fromComponent(data: BuilderComponents | APIComponents): BuilderComponents {
if ('toJSON' in data) {
return data;
}
switch (data.type) {
case ComponentType.Button:
return new Button(data);
@ -49,5 +56,19 @@ export function fromComponent(
return new ChannelSelectMenu(data);
case ComponentType.ActionRow:
return new ActionRow(data);
case ComponentType.Section:
return new Section(data);
case ComponentType.TextDisplay:
return new TextDisplay(data);
case ComponentType.Thumbnail:
return new Thumbnail(data);
case ComponentType.Container:
return new Container(data);
case ComponentType.MediaGallery:
return new MediaGallery(data);
case ComponentType.Separator:
return new Separator(data);
case ComponentType.File:
return new File(data);
}
}

View File

@ -3,9 +3,17 @@ import type {
ModalSubmitInteraction,
StringSelectMenuInteraction,
} from '../structures/Interaction';
import type { ActionRow } from './ActionRow';
import type { Button } from './Button';
import type { Container } from './Container';
import type { File } from './File';
import type { MediaGallery } from './MediaGallery';
import type { TextInput } from './Modal';
import type { Section } from './Section';
import type { BuilderSelectMenus } from './SelectMenu';
import type { Separator } from './Separator';
import type { TextDisplay } from './TextDisplay';
import type { Thumbnail } from './Thumbnail';
export type ComponentCallback<
T extends ComponentInteraction | StringSelectMenuInteraction = ComponentInteraction | StringSelectMenuInteraction,
@ -25,7 +33,22 @@ export type ButtonID = Omit<Button, 'setURL'>;
export type MessageBuilderComponents = FixedComponents<Button> | BuilderSelectMenus;
export type ModalBuilderComponents = TextInput;
export type BuilderComponents = MessageBuilderComponents | TextInput;
export type ActionBuilderComponents = MessageBuilderComponents | TextInput;
export type BuilderComponents =
| ActionRow
| ActionBuilderComponents
| Section<Button | Thumbnail>
| Thumbnail
| TextDisplay
| Container
| Separator
| MediaGallery
| File
| TextInput;
export type TopLevelBuilders = Exclude<BuilderComponents, Thumbnail | TextInput>;
export type FixedComponents<T = Button> = T extends Button ? ButtonLink | ButtonID : T;
export interface ListenerOptions {
timeout?: number;

View File

@ -94,7 +94,11 @@ export class LimitedMemoryAdapter<T> implements Adapter {
}
private __set(key: string, data: any) {
const __guildId = Array.isArray(data) ? data[0].guild_id : data.guild_id;
const isArray = Array.isArray(data);
if (isArray && data.length === 0) {
return;
}
const __guildId = isArray ? data[0].guild_id : data.guild_id;
const namespace = `${key.split('.')[0]}${__guildId ? `.${__guildId}` : ''}`;
const self = this;
if (!this.storage.has(namespace)) {

12
src/cache/index.ts vendored
View File

@ -1,11 +1,5 @@
import { type If, Logger } from '../common';
import type { Adapter } from './adapters';
import { Guilds } from './resources/guilds';
import { Users } from './resources/users';
import type { InternalOptions, UsingClient } from '../commands';
import { type If, Logger } from '../common';
import {
type APIChannel,
type APIEmoji,
@ -20,9 +14,11 @@ import {
GuildMemberFlags,
OverwriteType,
} from '../types';
import type { Adapter } from './adapters';
import { Bans } from './resources/bans';
import { Channels } from './resources/channels';
import { Emojis } from './resources/emojis';
import { Guilds } from './resources/guilds';
import { Members } from './resources/members';
import { Messages } from './resources/messages';
import { Overwrites } from './resources/overwrites';
@ -30,7 +26,9 @@ import { Presences } from './resources/presence';
import { Roles } from './resources/roles';
import { StageInstances } from './resources/stage-instances';
import { Stickers } from './resources/stickers';
import { Users } from './resources/users';
import { VoiceStates } from './resources/voice-states';
export { BaseResource } from './resources/default/base';
export { GuildBasedResource } from './resources/default/guild-based';
export { GuildRelatedResource } from './resources/default/guild-related';

View File

@ -1,7 +1,7 @@
import type { Cache, CacheFrom, ReturnCache } from '..';
import { type GuildStructure, Transformers } from '../../client/transformers';
import { fakePromise } from '../../common';
import type { APIGuild, GatewayGuildCreateDispatchData } from '../../types';
import type { Cache, CacheFrom, ReturnCache } from '..';
import { BaseResource } from './default/base';
export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispatchData> {
@ -18,7 +18,7 @@ export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispa
);
}
raw(id: string): ReturnCache<APIGuild | undefined> {
raw(id: string): ReturnCache<(APIGuild & { member_count?: number }) | undefined> {
return super.get(id);
}
@ -28,7 +28,7 @@ export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispa
);
}
bulkRaw(ids: string[]): ReturnCache<APIGuild[]> {
bulkRaw(ids: string[]): ReturnCache<(APIGuild & { member_count?: number })[]> {
return super.bulk(ids);
}
@ -38,7 +38,7 @@ export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispa
);
}
valuesRaw(): ReturnCache<APIGuild[]> {
valuesRaw(): ReturnCache<(APIGuild & { member_count?: number })[]> {
return super.values();
}

View File

@ -1,7 +1,7 @@
import type { CacheFrom, ReturnCache } from '../..';
import { type GuildMemberStructure, Transformers } from '../../client/transformers';
import { fakePromise } from '../../common';
import type { APIGuildMember } from '../../types';
import type { APIGuildMember, APIUser } from '../../types';
import { GuildBasedResource } from './default/guild-based';
export class Members extends GuildBasedResource<any, APIGuildMember> {
namespace = 'member';
@ -39,14 +39,21 @@ export class Members extends GuildBasedResource<any, APIGuildMember> {
override bulk(ids: string[], guild: string): ReturnCache<GuildMemberStructure[]> {
return fakePromise(super.bulk(ids, guild)).then(members =>
fakePromise(this.client.cache.users?.bulkRaw(ids)).then(users =>
members
fakePromise(this.client.cache.users?.bulkRaw(ids)).then(users => {
if (!users) return [];
let usersRecord: null | Partial<Record<string, APIUser>> = {};
for (const user of users) {
usersRecord[user.id] = user;
}
const result = members
.map(rawMember => {
const user = users?.find(x => x.id === rawMember.id);
const user = usersRecord![rawMember.id];
return user ? Transformers.GuildMember(this.client, rawMember, user, guild) : undefined;
})
.filter(x => x !== undefined),
),
.filter(x => x !== undefined);
usersRecord = null;
return result;
}),
);
}
@ -56,14 +63,21 @@ export class Members extends GuildBasedResource<any, APIGuildMember> {
override values(guild: string): ReturnCache<GuildMemberStructure[]> {
return fakePromise(super.values(guild)).then(members =>
fakePromise(this.client.cache.users?.valuesRaw()).then(users =>
members
fakePromise(this.client.cache.users?.bulkRaw(members.map(member => member.id))).then(users => {
if (!users) return [];
let usersRecord: null | Partial<Record<string, APIUser>> = {};
for (const user of users) {
usersRecord[user.id] = user;
}
const result = members
.map(rawMember => {
const user = users?.find(x => x.id === rawMember.id);
const user = usersRecord![rawMember.id];
return user ? Transformers.GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined;
})
.filter(x => x !== undefined),
),
.filter(x => x !== undefined);
usersRecord = null;
return result;
}),
);
}

View File

@ -1,5 +1,7 @@
import { promises } from 'node:fs';
import { join } from 'node:path';
import { ApiHandler } from '../api';
import { isBufferLike } from '../api/utils/utils';
import type { Adapter, DisabledCache } from '../cache';
import { Cache, MemoryAdapter } from '../cache';
import type {
@ -16,34 +18,31 @@ import type {
UsingClient,
} from '../commands';
import { IgnoreCommand, type InferWithPrefix, type MiddlewareContext } from '../commands/applications/shared';
import { HandleCommand } from '../commands/handle';
import { CommandHandler } from '../commands/handler';
import {
ApplicationShorter,
assertString,
ChannelShorter,
EmojiShorter,
filterSplit,
GuildShorter,
InteractionShorter,
InvitesShorter,
LogLevels,
Logger,
LogLevels,
type MakeRequired,
MemberShorter,
MergeOptions,
MessageShorter,
magicImport,
ReactionShorter,
RoleShorter,
TemplateShorter,
ThreadShorter,
UsersShorter,
WebhookShorter,
assertString,
filterSplit,
magicImport,
} from '../common';
import { promises } from 'node:fs';
import { isBufferLike } from '../api/utils/utils';
import { HandleCommand } from '../commands/handle';
import { BanShorter } from '../common/shorters/bans';
import { SoundboardShorter } from '../common/shorters/soundboard';
import { VoiceStateShorter } from '../common/shorters/voiceStates';
@ -180,15 +179,15 @@ export class BaseClient {
this._botId = id;
}
get botId() {
return this._botId ?? BaseClient.getBotIdFromToken(this.rest.options.token);
get botId(): string {
return this._botId ?? BaseClient.getBotIdFromToken(this.rest.options.token) ?? '';
}
set applicationId(id: string) {
this._applicationId = id;
}
get applicationId() {
get applicationId(): string {
return this._applicationId ?? this.botId;
}
@ -470,6 +469,8 @@ export interface BaseClientOptions {
globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[];
commands?: {
defaults?: {
onBeforeMiddlewares?: (context: CommandContext | MenuCommandContext<any, never>) => unknown;
onBeforeOptions?: Command['onBeforeOptions'];
onRunError?: (context: MenuCommandContext<any, never> | CommandContext, error: unknown) => unknown;
onPermissionsFail?: Command['onPermissionsFail'];
onBotPermissionsFail?: (
@ -489,6 +490,7 @@ export interface BaseClientOptions {
};
components?: {
defaults?: {
onBeforeMiddlewares?: ComponentCommand['onBeforeMiddlewares'];
onRunError?: ComponentCommand['onRunError'];
onInternalError?: ComponentCommand['onInternalError'];
onMiddlewaresError?: ComponentCommand['onMiddlewaresError'];
@ -497,6 +499,7 @@ export interface BaseClientOptions {
};
modals?: {
defaults?: {
onBeforeMiddlewares?: ModalCommand['onBeforeMiddlewares'];
onRunError?: ModalCommand['onRunError'];
onInternalError?: ModalCommand['onInternalError'];
onMiddlewaresError?: ModalCommand['onMiddlewaresError'];

View File

@ -1,17 +1,17 @@
import type { CommandContext, Message } from '..';
import {
type Awaitable,
assertString,
type DeepPartial,
type If,
lazyLoadPackage,
type PickPartial,
type WatcherPayload,
type WatcherSendToShard,
assertString,
lazyLoadPackage,
When,
} from '../common';
import { EventHandler } from '../events';
import type { GatewayDispatchPayload, GatewayPresenceUpdateData } from '../types';
import { ShardManager, type ShardManagerOptions, properties } from '../websocket';
import { properties, ShardManager, type ShardManagerOptions } from '../websocket';
import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate';
import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate';
import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base';
@ -23,7 +23,7 @@ let parentPort: import('node:worker_threads').MessagePort;
export class Client<Ready extends boolean = boolean> extends BaseClient {
gateway!: ShardManager;
me!: If<Ready, ClientUserStructure>;
me!: When<Ready, ClientUserStructure>;
declare options: Omit<ClientOptions, 'commands'> & {
commands: NonNullable<ClientOptions['commands']>;
};
@ -36,8 +36,8 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
super(options);
}
get applicationId() {
return this.me?.application.id ?? super.applicationId;
get applicationId(): When<Ready, string, ''> {
return (this.me?.application.id ?? super.applicationId) as never;
}
set applicationId(id: string) {
@ -219,8 +219,8 @@ export interface ClientOptions extends BaseClientOptions {
};
commands?: BaseClientOptions['commands'] & {
prefix?: (message: MessageStructure) => Awaitable<string[]>;
deferReplyResponse?: (ctx: CommandContext) => Parameters<Message['write']>[0];
reply?: (ctx: CommandContext) => boolean;
deferReplyResponse?: (ctx: CommandContext) => Awaitable<Parameters<Message['write']>[0]>;
reply?: (ctx: CommandContext) => Awaitable<boolean>;
};
handlePayload?: ShardManagerOptions['handlePayload'];
resharding?: PickPartial<NonNullable<ShardManagerOptions['resharding']>, 'getInfo'>;

View File

@ -1,4 +1,4 @@
import { type UUID, randomUUID } from 'node:crypto';
import { randomUUID, type UUID } from 'node:crypto';
import type { UsingClient } from '../commands';
import type { Awaitable, CamelCase } from '../common';
import type { CallbackEventHandler, CustomEventsKeys, GatewayEvents } from '../events';

View File

@ -1,5 +1,5 @@
export type { RuntimeConfig, RuntimeConfigHTTP } from './base';
export * from './client';
export * from './httpclient';
export * from './workerclient';
export * from './transformers';
export * from './workerclient';

View File

@ -8,8 +8,8 @@ import {
BaseGuildChannel,
CategoryChannel,
ClientUser,
DMChannel,
DirectoryChannel,
DMChannel,
Emoji,
Entitlement,
ForumChannel,

View File

@ -1,11 +1,24 @@
import { type UUID, randomUUID } from 'node:crypto';
import { randomUUID, type UUID } from 'node:crypto';
import { ApiHandler, Logger } from '..';
import { WorkerAdapter } from '../cache';
import { type DeepPartial, LogLevels, type MakeRequired, type When, lazyLoadPackage } from '../common';
import {
type Awaitable,
calculateShardId,
type DeepPartial,
LogLevels,
lazyLoadPackage,
type MakeRequired,
type When,
} from '../common';
import { EventHandler } from '../events';
import type { GatewayDispatchPayload, GatewaySendPayload } from '../types';
import { Shard, type ShardManagerOptions, ShardSocketCloseCodes, type WorkerData, properties } from '../websocket';
import { properties, Shard, type ShardManagerOptions, ShardSocketCloseCodes, type WorkerData } from '../websocket';
import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate';
import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate';
import type { WorkerHeartbeaterMessages } from '../websocket/discord/heartbeater';
import type { ShardData } from '../websocket/discord/shared';
import type {
ClientHeartbeaterMessages,
WorkerDisconnectedAllShardsResharding,
WorkerMessages,
WorkerReady,
@ -27,10 +40,6 @@ import type { ManagerMessages, ManagerSpawnShards } from '../websocket/discord/w
import type { BaseClientOptions, ServicesOptions, StartOptions } from './base';
import { BaseClient } from './base';
import type { Client, ClientOptions } from './client';
import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate';
import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate';
import type { ShardData } from '../websocket/discord/shared';
import { Collectors } from './collectors';
import { type ClientUserStructure, Transformers } from './transformers';
@ -96,8 +105,8 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
return acc / this.shards.size;
}
get applicationId() {
return this.me?.application.id ?? super.applicationId;
get applicationId(): When<Ready, string, ''> {
return (this.me?.application.id ?? super.applicationId) as never;
}
set applicationId(id: string) {
@ -166,13 +175,19 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
}
}
postMessage(body: WorkerMessages): unknown {
postMessage(body: WorkerMessages | ClientHeartbeaterMessages): unknown {
if (manager) return manager.postMessage(body);
return process.send!(body);
}
async handleManagerMessages(data: ManagerMessages) {
async handleManagerMessages(data: ManagerMessages | WorkerHeartbeaterMessages) {
switch (data.type) {
case 'HEARTBEAT':
this.postMessage({
type: 'ACK_HEARTBEAT',
workerId: workerData.workerId,
});
break;
case 'CACHE_RESULT':
if (this.cache.adapter instanceof WorkerAdapter && this.cache.adapter.promises.has(data.nonce)) {
const cacheData = this.cache.adapter.promises.get(data.nonce)!;
@ -377,12 +392,18 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
return this.onPacket(packet, shardId);
};
}
workerData.totalShards = data.totalShards;
workerData.shards = [...this.shards.keys()];
this.resharding.clear();
}
break;
}
}
calculateShardId(guildId: string) {
return calculateShardId(guildId, this.workerData.totalShards);
}
private generateNonce(): UUID {
const uuid = randomUUID();
if (this.promises.has(uuid)) return this.generateNonce();
@ -420,7 +441,7 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
return Promise.all(promises);
}
createShard(id: number, data: Pick<ManagerSpawnShards, 'info' | 'compress'>) {
createShard(id: number, data: Pick<ManagerSpawnShards, 'info' | 'compress' | 'properties'>) {
const onPacket = this.onPacket.bind(this);
const handlePayload = this.options?.handlePayload?.bind(this);
const self = this;
@ -432,6 +453,7 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
debugger: this.debugger,
properties: {
...properties,
...data.properties,
...this.options.gateway?.properties,
},
async handlePayload(shardId, payload) {
@ -553,12 +575,12 @@ export function generateShardInfo(shard: Shard): WorkerShardInfo {
};
}
interface WorkerClientOptions extends BaseClientOptions {
export interface WorkerClientOptions extends BaseClientOptions {
commands?: NonNullable<Client['options']>['commands'];
handlePayload?: ShardManagerOptions['handlePayload'];
gateway?: ClientOptions['gateway'];
postMessage?: (body: unknown) => unknown;
postMessage?: (body: unknown) => Awaitable<unknown>;
/** can have perfomance issues in big bots if the client sends every event, specially in startup (false by default) */
sendPayloadToParent?: boolean;
handleManagerMessages?(message: ManagerMessages): any;
handleManagerMessages?(message: ManagerMessages | WorkerHeartbeaterMessages): Awaitable<unknown>;
}

View File

@ -285,6 +285,8 @@ export class BaseCommand {
Object.setPrototypeOf(this, __tempCommand.prototype);
}
onBeforeMiddlewares?(context: CommandContext): any;
onBeforeOptions?(context: CommandContext): any;
run?(context: CommandContext): any;
onAfterRun?(context: CommandContext, error: unknown | undefined): any;
onRunError?(context: CommandContext, error: unknown): any;

View File

@ -79,7 +79,7 @@ export class CommandContext<
if (this.interaction) return this.interaction.write(body, withResponse);
const options = (this.client as Client | WorkerClient).options?.commands;
return (this.messageResponse = await (this.message! as Message)[
!this.messageResponse && options?.reply?.(this) ? 'reply' : 'write'
!this.messageResponse && (await options?.reply?.(this)) ? 'reply' : 'write'
](body)) as never;
}
@ -97,8 +97,8 @@ export class CommandContext<
return this.interaction.deferReply(ephemeral ? MessageFlags.Ephemeral : undefined, withResponse);
this.__deferred = true;
const options = (this.client as Client | WorkerClient).options?.commands;
return (this.messageResponse = await (this.message! as Message)[options?.reply?.(this) ? 'reply' : 'write'](
options?.deferReplyResponse?.(this) ?? { content: 'Thinking...' },
return (this.messageResponse = await (this.message! as Message)[(await options?.reply?.(this)) ? 'reply' : 'write'](
(await options?.deferReplyResponse?.(this)) ?? { content: 'Thinking...' },
)) as never;
}

View File

@ -1,4 +1,4 @@
import { type PermissionStrings, magicImport } from '../../common';
import { magicImport, type PermissionStrings } from '../../common';
import {
ApplicationCommandType,
type ApplicationIntegrationType,
@ -54,6 +54,7 @@ export abstract class EntryPointCommand {
Object.setPrototypeOf(this, __tempCommand.prototype);
}
onBeforeMiddlewares?(context: EntryPointContext): any;
abstract run?(context: EntryPointContext): any;
onAfterRun?(context: EntryPointContext, error: unknown | undefined): any;
onRunError(context: EntryPointContext<never>, error: unknown): any {

View File

@ -12,10 +12,11 @@ import type {
MakeRequired,
MessageWebhookCreateBodyRequest,
ModalCreateBodyRequest,
ModalCreateOptions,
UnionToTuple,
When,
} from '../../common';
import type { AllChannels, EntryPointInteraction } from '../../structures';
import type { AllChannels, EntryPointInteraction, ModalSubmitInteraction } from '../../structures';
import { MessageFlags, type RESTGetAPIGuildQuery } from '../../types';
import { BaseContext } from '../basecontext';
import type { RegisteredMiddlewares } from '../decorators';
@ -52,8 +53,11 @@ export class EntryPointContext<M extends keyof RegisteredMiddlewares = never> ex
return this.interaction.write<WR>(body, withResponse);
}
modal(body: ModalCreateBodyRequest) {
return this.interaction.modal(body);
modal(body: ModalCreateBodyRequest, options?: undefined): Promise<undefined>;
modal(body: ModalCreateBodyRequest, options: ModalCreateOptions): Promise<ModalSubmitInteraction | null>;
modal(body: ModalCreateBodyRequest, options?: ModalCreateOptions | undefined) {
if (options === undefined) return this.interaction.modal(body);
return this.interaction.modal(body, options);
}
deferReply<WR extends boolean = false>(

View File

@ -1,4 +1,4 @@
import { type PermissionStrings, magicImport } from '../../common';
import { magicImport, type PermissionStrings } from '../../common';
import type {
ApplicationCommandType,
ApplicationIntegrationType,
@ -53,6 +53,7 @@ export abstract class ContextMenuCommand {
Object.setPrototypeOf(this, __tempCommand.prototype);
}
onBeforeMiddlewares?(context: MenuCommandContext<any>): any;
abstract run?(context: MenuCommandContext<any>): any;
onAfterRun?(context: MenuCommandContext<any>, error: unknown | undefined): any;
onRunError?(context: MenuCommandContext<any, never>, error: unknown): any;

View File

@ -8,16 +8,22 @@ import {
type WebhookMessageStructure,
} from '../../client/transformers';
import {
type InteractionCreateBodyRequest,
type InteractionMessageUpdateBodyRequest,
type MakeRequired,
type MessageWebhookCreateBodyRequest,
type ModalCreateBodyRequest,
type UnionToTuple,
type When,
InteractionCreateBodyRequest,
InteractionMessageUpdateBodyRequest,
MakeRequired,
MessageWebhookCreateBodyRequest,
ModalCreateBodyRequest,
ModalCreateOptions,
toSnakeCase,
UnionToTuple,
When,
} from '../../common';
import type { AllChannels, MessageCommandInteraction, UserCommandInteraction } from '../../structures';
import {
AllChannels,
MessageCommandInteraction,
ModalSubmitInteraction,
UserCommandInteraction,
} from '../../structures';
import { type APIMessage, ApplicationCommandType, MessageFlags, type RESTGetAPIGuildQuery } from '../../types';
import { BaseContext } from '../basecontext';
import type { RegisteredMiddlewares } from '../decorators';
@ -47,7 +53,6 @@ export class MenuCommandContext<
metadata: CommandMetadata<UnionToTuple<M>> = {} as never;
globalMetadata: GlobalMetadata = {};
// biome-ignore lint/suspicious/useGetterReturn: default don't exist.
get target(): InteractionTarget<T> {
switch (this.interaction.data.type) {
case ApplicationCommandType.Message: {
@ -76,8 +81,11 @@ export class MenuCommandContext<
return this.interaction.write<WR>(body, withResponse);
}
modal(body: ModalCreateBodyRequest) {
return this.interaction.modal(body);
modal(body: ModalCreateBodyRequest, options?: undefined): Promise<undefined>;
modal(body: ModalCreateBodyRequest, options: ModalCreateOptions): Promise<ModalSubmitInteraction | null>;
modal(body: ModalCreateBodyRequest, options?: ModalCreateOptions | undefined) {
if (options === undefined) return this.interaction.modal(body);
return this.interaction.modal(body, options);
}
deferReply<WR extends boolean = false>(

View File

@ -1,10 +1,3 @@
import type {
AutocompleteCallback,
EntryPointContext,
MenuCommandContext,
OnAutocompleteErrorCallback,
ReturnOptionsTypes,
} from '..';
import type { Awaitable, FlatObjectKeys } from '../../common';
import type { ModalContext } from '../../components';
import type { ComponentContext } from '../../components/componentcontext';
@ -15,6 +8,13 @@ import {
ApplicationCommandOptionType,
} from '../../types';
import type { LocalizationMap } from '../../types/payloads';
import type {
AutocompleteCallback,
EntryPointContext,
MenuCommandContext,
OnAutocompleteErrorCallback,
ReturnOptionsTypes,
} from '..';
import type { CommandContext } from './chatcontext';
import type { DefaultLocale, MiddlewareContext, OKFunction, SeyfertChannelMap, StopFunction } from './shared';

View File

@ -1,7 +1,7 @@
import type {
CategoryChannelStructure,
DMChannelStructure,
DirectoryChannelStructure,
DMChannelStructure,
ForumChannelStructure,
MediaChannelStructure,
NewsChannelStructure,

View File

@ -1,3 +1,32 @@
import type { Client, WorkerClient } from '../client';
import { type MessageStructure, type OptionResolverStructure, Transformers } from '../client/transformers';
import type { MakeRequired } from '../common';
import { INTEGER_OPTION_VALUE_LIMIT } from '../common/it/constants';
import { ComponentContext, ModalContext } from '../components';
import {
type __InternalReplyFunction,
AutocompleteInteraction,
BaseInteraction,
type ChatInputCommandInteraction,
type ComponentInteraction,
type EntryPointInteraction,
type MessageCommandInteraction,
type ModalSubmitInteraction,
type UserCommandInteraction,
} from '../structures';
import type { PermissionsBitField } from '../structures/extra/Permissions';
import {
type APIApplicationCommandInteraction,
type APIApplicationCommandInteractionDataOption,
type APIInteraction,
type APIInteractionDataResolvedChannel,
ApplicationCommandOptionType,
ApplicationCommandType,
ChannelType,
type GatewayMessageCreateDispatchData,
InteractionContextType,
InteractionType,
} from '../types';
import {
BaseCommand,
Command,
@ -19,35 +48,6 @@ import {
SubCommand,
type UsingClient,
} from '.';
import type { Client, WorkerClient } from '../client';
import { type MessageStructure, type OptionResolverStructure, Transformers } from '../client/transformers';
import type { MakeRequired } from '../common';
import { INTEGER_OPTION_VALUE_LIMIT } from '../common/it/constants';
import { ComponentContext, ModalContext } from '../components';
import {
AutocompleteInteraction,
BaseInteraction,
type ChatInputCommandInteraction,
type ComponentInteraction,
type EntryPointInteraction,
type MessageCommandInteraction,
type ModalSubmitInteraction,
type UserCommandInteraction,
type __InternalReplyFunction,
} from '../structures';
import type { PermissionsBitField } from '../structures/extra/Permissions';
import {
type APIApplicationCommandInteraction,
type APIApplicationCommandInteractionDataOption,
type APIInteraction,
type APIInteractionDataResolvedChannel,
ApplicationCommandOptionType,
ApplicationCommandType,
ChannelType,
type GatewayMessageCreateDispatchData,
InteractionContextType,
InteractionType,
} from '../types';
export type CommandOptionWithType = CommandOption & {
type: ApplicationCommandOptionType;
@ -91,7 +91,7 @@ export class HandleCommand {
await optionsResolver.getCommand()?.onInternalError?.(this.client, optionsResolver.getCommand()!, error);
}
} catch (error) {
// pass
this.client.logger.error(`[${optionsResolver.fullCommandName}] Internal error:`, error);
}
}
@ -100,17 +100,18 @@ export class HandleCommand {
interaction: MessageCommandInteraction | UserCommandInteraction,
context: MenuCommandContext<MessageCommandInteraction | UserCommandInteraction>,
) {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return command.onBotPermissionsFail?.(context, permissions);
}
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return await command.onBotPermissionsFail?.(context, permissions);
}
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
@ -121,8 +122,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError?.(this.client, command, error);
} catch {
// pass
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -144,17 +145,18 @@ export class HandleCommand {
}
async entryPoint(command: EntryPointCommand, interaction: EntryPointInteraction, context: EntryPointContext) {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return command.onBotPermissionsFail(context, permissions);
}
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return await command.onBotPermissionsFail(context, permissions);
}
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
@ -165,8 +167,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError(this.client, command, error);
} catch {
// pass
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -177,26 +179,28 @@ export class HandleCommand {
resolver: OptionResolverStructure,
context: CommandContext,
) {
if (context.guildId) {
if (command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return command.onBotPermissionsFail?.(context, permissions);
}
if (command.defaultMemberPermissions) {
const permissions = this.checkPermissions(interaction.member!.permissions, command.defaultMemberPermissions);
if (permissions) return command.onPermissionsFail?.(context, permissions);
}
}
if (!(await this.runOptions(command, context, resolver))) return;
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
if (context.guildId) {
if (command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return await command.onBotPermissionsFail?.(context, permissions);
}
if (command.defaultMemberPermissions) {
const permissions = this.checkPermissions(interaction.member!.permissions, command.defaultMemberPermissions);
if (permissions) return await command.onPermissionsFail?.(context, permissions);
}
}
await command.onBeforeOptions?.(context);
if (!(await this.runOptions(command, context, resolver))) return;
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
@ -207,8 +211,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError?.(this.client, command, error);
} catch {
// pass
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -350,17 +354,17 @@ export class HandleCommand {
attachments: {},
};
const args = this.argsParser(argsContent, command, message);
const { options, errors } = await this.argsOptionsParser(command, rawMessage, args, resolved);
const optionsResolver = this.makeResolver(self, options, parent as Command, rawMessage.guild_id, resolved);
const context = new CommandContext(self, message, optionsResolver, shardId, command);
//@ts-expect-error
const extendContext = self.options?.context?.(message) ?? {};
Object.assign(context, extendContext);
try {
const args = this.argsParser(argsContent, command, message);
const { options, errors } = await this.argsOptionsParser(command, rawMessage, args, resolved);
const optionsResolver = this.makeResolver(self, options, parent as Command, rawMessage.guild_id, resolved);
const context = new CommandContext(self, message, optionsResolver, shardId, command);
//@ts-expect-error
const extendContext = self.options?.context?.(message) ?? {};
Object.assign(context, extendContext);
if (errors.length) {
return command.onOptionsError?.(
return await command.onOptionsError?.(
context,
Object.fromEntries(
errors.map(x => {
@ -383,7 +387,7 @@ export class HandleCommand {
const permissions = this.checkPermissions(memberPermissions, command.defaultMemberPermissions);
const guild = await this.client.guilds.raw(rawMessage.guild_id);
if (permissions && guild.owner_id !== rawMessage.author.id) {
return command.onPermissionsFail?.(context, memberPermissions.keys(permissions));
return await command.onPermissionsFail?.(context, memberPermissions.keys(permissions));
}
}
@ -391,13 +395,15 @@ export class HandleCommand {
const appPermissions = await self.members.permissions(rawMessage.guild_id, self.botId);
const permissions = this.checkPermissions(appPermissions, command.botPermissions);
if (permissions) {
return command.onBotPermissionsFail?.(context, permissions);
return await command.onBotPermissionsFail?.(context, permissions);
}
}
}
await command.onBeforeOptions?.(context);
if (!(await this.runOptions(command, context, optionsResolver))) return;
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
@ -412,8 +418,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError?.(this.client, command, error);
} catch {
// http 418
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -574,8 +580,8 @@ export class HandleCommand {
} catch (e) {
try {
await command.onInternalError?.(this.client, command as never, e);
} catch {
// http 418
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
return false;
@ -602,8 +608,8 @@ export class HandleCommand {
} catch (e) {
try {
await command.onInternalError?.(this.client, command as never, e);
} catch {
// http 418
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
return false;
@ -627,15 +633,7 @@ export class HandleCommand {
async runOptions(command: Command | SubCommand, context: CommandContext, resolver: OptionResolverStructure) {
const [erroredOptions, result] = await command.__runOptions(context, resolver);
if (erroredOptions) {
try {
await command.onOptionsError?.(context, result);
} catch (e) {
try {
await command.onInternalError?.(this.client, command, e);
} catch {
// http 418
}
}
await command.onOptionsError?.(context, result);
return false;
}
return true;

View File

@ -1,6 +1,5 @@
import { promises } from 'node:fs';
import { basename, dirname } from 'node:path';
import type { EntryPointCommand } from '.';
import { dirname } from 'node:path';
import type { Logger, NulleableCoalising, OmitInsert } from '../common';
import { BaseHandler, isCloudfareWorker } from '../common';
import { PermissionsBitField } from '../structures/extra/Permissions';
@ -17,6 +16,7 @@ import {
type LocaleString,
type LocalizationMap,
} from '../types';
import type { EntryPointCommand } from '.';
import { Command, type CommandOption, SubCommand } from './applications/chat';
import { ContextMenuCommand } from './applications/menu';
import { IgnoreCommand, type UsingClient } from './applications/shared';
@ -299,7 +299,7 @@ export class CommandHandler extends BaseHandler {
//@AutoLoad
const options = await this.getFiles(dirname(file.path));
for (const option of options) {
if (file.name === basename(option)) {
if (file.path === option) {
continue;
}
try {
@ -466,6 +466,8 @@ export class CommandHandler extends BaseHandler {
stablishContextCommandDefaults(commandInstance: InstanceType<HandleableCommand>): ContextMenuCommand | false {
if (!(commandInstance instanceof ContextMenuCommand)) return false;
commandInstance.onBeforeMiddlewares ??= this.client.options.commands?.defaults?.onBeforeMiddlewares;
commandInstance.onAfterRun ??= this.client.options.commands?.defaults?.onAfterRun;
commandInstance.onBotPermissionsFail ??= this.client.options.commands?.defaults?.onBotPermissionsFail;
@ -482,6 +484,8 @@ export class CommandHandler extends BaseHandler {
commandInstance: InstanceType<HandleableCommand>,
): OmitInsert<Command, 'options', { options: NonNullable<Command['options']> }> | false {
if (!(commandInstance instanceof Command)) return false;
commandInstance.onBeforeMiddlewares ??= this.client.options.commands?.defaults?.onBeforeMiddlewares;
commandInstance.onBeforeOptions ??= this.client.options.commands?.defaults?.onBeforeOptions;
commandInstance.onAfterRun ??= this.client.options.commands?.defaults?.onAfterRun;
commandInstance.onBotPermissionsFail ??= this.client.options.commands?.defaults?.onBotPermissionsFail;
commandInstance.onInternalError ??= this.client.options.commands?.defaults?.onInternalError;
@ -495,6 +499,14 @@ export class CommandHandler extends BaseHandler {
stablishSubCommandDefaults(commandInstance: Command, option: SubCommand): SubCommand {
option.middlewares = (commandInstance.middlewares ?? []).concat(option.middlewares ?? []);
option.onBeforeMiddlewares =
option.onBeforeMiddlewares?.bind(option) ??
commandInstance.onBeforeMiddlewares?.bind(commandInstance) ??
this.client.options.commands?.defaults?.onBeforeMiddlewares;
option.onBeforeOptions =
option.onBeforeOptions?.bind(option) ??
commandInstance.onBeforeOptions?.bind(commandInstance) ??
this.client.options.commands?.defaults?.onBeforeOptions;
option.onMiddlewaresError =
option.onMiddlewaresError?.bind(option) ??
commandInstance.onMiddlewaresError?.bind(commandInstance) ??

View File

@ -1,11 +1,11 @@
export * from './applications/shared';
//
export * from './applications/chat';
export * from './applications/chatcontext';
export * from './applications/entrycontext';
export * from './applications/entryPoint';
export * from './applications/menu';
export * from './applications/menucontext';
export * from './applications/options';
export * from './applications/entryPoint';
export * from './applications/entrycontext';
export * from './applications/shared';
export * from './decorators';
export * from './optionresolver';

View File

@ -33,7 +33,7 @@ export class OptionResolver {
readonly options: OptionResolved[];
public hoistedOptions: OptionResolved[];
private subCommand: string | null = null;
private group: string | null = null;
private group?: string;
constructor(
private client: UsingClient,
options: APIApplicationCommandInteractionDataOption[],
@ -63,7 +63,7 @@ export class OptionResolver {
getCommand() {
if (this.subCommand) {
return (this.parent?.options as SubCommand[] | undefined)?.find(
x => (this.group ? x.group === this.group : true) && x.name === this.subCommand,
x => this.group === x.group && x.name === this.subCommand,
);
}
return this.parent;

View File

@ -1,28 +1,28 @@
export * from './it/constants';
export * from './it/utils';
//
export * from './bot/watcher';
export * from './it/colors';
export { CustomizeLoggerCallback, AssignFilenameCallback, LogLevels, Logger, LoggerOptions } from './it/logger';
export * from './it/constants';
export * from './it/formatter';
// circular lol
export * from './shorters/invites';
export { AssignFilenameCallback, CustomizeLoggerCallback, Logger, LoggerOptions, LogLevels } from './it/logger';
export * from './it/utils';
export * from './shorters/application';
//
export * from './shorters/channels';
export * from './shorters/emojis';
export * from './shorters/guilds';
export * from './shorters/interaction';
// circular lol
export * from './shorters/invites';
export * from './shorters/members';
export * from './shorters/messages';
export * from './shorters/reactions';
export * from './shorters/roles';
export * from './shorters/templates';
export * from './shorters/users';
export * from './shorters/threads';
export * from './shorters/users';
export * from './shorters/webhook';
export * from './shorters/interaction';
export * from './shorters/application';
//
export * from './types/options';
export * from './types/resolvables';
export * from './types/util';
export * from './types/write';
//
export * from './bot/watcher';

View File

@ -1,4 +1,4 @@
import { type WriteStream, createWriteStream, existsSync, mkdirSync, promises } from 'node:fs';
import { createWriteStream, existsSync, mkdirSync, promises, type WriteStream } from 'node:fs';
import { join } from 'node:path';
import { bgBrightWhite, black, bold, brightBlack, cyan, gray, italic, red, stripColor, yellow } from './colors';
import { MergeOptions } from './utils';

View File

@ -1,5 +1,7 @@
import { promises } from 'node:fs';
import { basename, join } from 'node:path';
import type { Cache } from '../../cache';
import { type APIPartialEmoji, FormattingPatterns, GatewayIntentBits } from '../../types';
import {
type ColorResolvable,
DiscordEpoch,
@ -10,8 +12,6 @@ import {
type ObjectToSnake,
type TypeArray,
} from '..';
import type { Cache } from '../../cache';
import { type APIPartialEmoji, FormattingPatterns, GatewayIntentBits } from '../../types';
/**
* Calculates the shard ID for a guild based on its ID.
@ -304,8 +304,6 @@ export function lazyLoadPackage<T>(mod: string): T | undefined {
try {
return require(mod);
} catch (e) {
// biome-ignore lint/suspicious/noConsoleLog:
// biome-ignore lint/suspicious/noConsole:
console.log(`Cannot import ${mod}`, e);
return;
}

View File

@ -1,7 +1,7 @@
import { CacheFrom } from '../../cache';
import type { Overwrites } from '../../cache/resources/overwrites';
import { type MessageStructure, type ThreadChannelStructure, Transformers } from '../../client/transformers';
import { type AllChannels, BaseChannel, type GuildMember, type GuildRole, channelFrom } from '../../structures';
import { type AllChannels, BaseChannel, channelFrom, type GuildMember, type GuildRole } from '../../structures';
import { PermissionsBitField } from '../../structures/extra/Permissions';
import type {
APIChannel,

View File

@ -14,8 +14,8 @@ import {
type AllChannels,
BaseChannel,
type CreateStickerBodyRequest,
type GuildChannelTypes,
channelFrom,
type GuildChannelTypes,
} from '../../structures';
import type {
APIChannel,

View File

@ -1,5 +1,6 @@
import { BaseInteraction, Modal, type ReplyInteractionBody, resolveFiles } from '../..';
import { Transformers, type WebhookMessageStructure } from '../../client/transformers';
import type { RESTPostAPIWebhookWithTokenWaitResult } from '../../types';
import type { InteractionMessageUpdateBodyRequest, MessageWebhookCreateBodyRequest } from '../types/write';
import { BaseShorter } from './base';
@ -69,12 +70,12 @@ export class InteractionShorter extends BaseShorter {
async followup(token: string, { files, ...body }: MessageWebhookCreateBodyRequest): Promise<WebhookMessageStructure> {
const parsedFiles = files ? await resolveFiles(files) : undefined;
const apiMessage = await this.client.proxy
const apiMessage = (await this.client.proxy
.webhooks(this.client.applicationId)(token)
.post({
body: BaseInteraction.transformBody(body, parsedFiles, this.client),
files: parsedFiles,
});
})) as RESTPostAPIWebhookWithTokenWaitResult;
return Transformers.WebhookMessage(this.client, apiMessage, this.client.applicationId, token);
}
}

View File

@ -110,9 +110,10 @@ export class MemberShorter extends BaseShorter {
async edit(
guildId: string,
memberId: string,
body: RESTPatchAPIGuildMemberJSONBody,
body: RESTPatchAPIGuildMemberJSONBody | { nick?: string | null },
reason?: string,
): Promise<GuildMemberStructure> {
memberId = memberId === this.client.botId && 'nick' in body ? '@me' : memberId;
const member = await this.client.proxy.guilds(guildId).members(memberId).patch({ body, reason });
await this.client.cache.members?.setIfNI(CacheFrom.Rest, 'GuildMembers', memberId, guildId, member);
return Transformers.GuildMember(this.client, member, member.user, guildId);

View File

@ -1,4 +1,7 @@
import type { ValidAnswerId } from '../../api/Routes/channels';
import { resolveFiles } from '../../builders';
import { CacheFrom } from '../../cache';
import { type MessageStructure, type ThreadChannelStructure, Transformers, type UserStructure } from '../../client';
import { MessagesMethods } from '../../structures';
import type {
RESTGetAPIChannelMessagesQuery,
@ -6,10 +9,6 @@ import type {
RESTPostAPIChannelMessageJSONBody,
RESTPostAPIChannelMessagesThreadsJSONBody,
} from '../../types';
import type { ValidAnswerId } from '../../api/Routes/channels';
import { CacheFrom } from '../../cache';
import { type MessageStructure, type ThreadChannelStructure, Transformers, type UserStructure } from '../../client';
import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../types/write';
import { BaseShorter } from './base';

View File

@ -92,10 +92,9 @@ export class RoleShorter extends BaseShorter {
* @param reason The reason for deleting the role.
* @returns A Promise that resolves when the role is deleted.
*/
async delete(guildId: string, roleId: string, reason?: string): Promise<GuildRoleStructure> {
const res = await this.client.proxy.guilds(guildId).roles(roleId).delete({ reason });
async delete(guildId: string, roleId: string, reason?: string) {
await this.client.proxy.guilds(guildId).roles(roleId).delete({ reason });
this.client.cache.roles?.removeIfNI('Guilds', roleId, guildId);
return Transformers.GuildRole(this.client, res, guildId);
}
/**

View File

@ -1,15 +1,16 @@
import { CacheFrom } from '../..';
import type { ThreadChannelStructure } from '../../client/transformers';
import { channelFrom } from '../../structures';
import type {
APIThreadChannel,
APIThreadMember,
RESTGetAPIChannelThreadMembersQuery,
RESTGetAPIChannelThreadsArchivedQuery,
RESTPatchAPIChannelJSONBody,
RESTPostAPIChannelMessagesThreadsJSONBody,
RESTPostAPIChannelThreadsJSONBody,
RESTPostAPIGuildForumThreadsJSONBody,
import {
type APIThreadChannel,
type APIThreadMember,
ChannelType,
type RESTGetAPIChannelThreadMembersQuery,
type RESTGetAPIChannelThreadsArchivedQuery,
type RESTPatchAPIChannelJSONBody,
type RESTPostAPIChannelMessagesThreadsJSONBody,
type RESTPostAPIChannelThreadsJSONBody,
type RESTPostAPIGuildForumThreadsJSONBody,
} from '../../types';
import type { MakeRequired, When } from '../types/util';
import { BaseShorter } from './base';
@ -44,27 +45,22 @@ export class ThreadShorter extends BaseShorter {
);
}
fromMessage(
async fromMessage(
channelId: string,
messageId: string,
options: RESTPostAPIChannelMessagesThreadsJSONBody & { reason?: string },
): Promise<ThreadChannelStructure> {
const { reason, ...body } = options;
return this.client.proxy
.channels(channelId)
.messages(messageId)
.threads.post({ body, reason })
.then(async thread => {
await this.client.cache.channels?.setIfNI(
CacheFrom.Rest,
'Guilds',
thread.id,
(thread as APIThreadChannel).guild_id!,
thread,
);
return channelFrom(thread, this.client) as ThreadChannelStructure;
});
const thread = await this.client.proxy.channels(channelId).messages(messageId).threads.post({ body, reason });
await this.client.cache.channels?.setIfNI(
CacheFrom.Rest,
'Guilds',
thread.id,
(thread as APIThreadChannel).guild_id!,
thread,
);
return await (channelFrom(thread, this.client) as ThreadChannelStructure);
}
join(threadId: string) {
@ -75,8 +71,9 @@ export class ThreadShorter extends BaseShorter {
return this.client.proxy.channels(threadId)['thread-members']('@me').delete();
}
lock(threadId: string, locked = true, reason?: string): Promise<ThreadChannelStructure> {
return this.edit(threadId, { locked }, reason).then(x => channelFrom(x, this.client) as ThreadChannelStructure);
async lock(threadId: string, locked = true, reason?: string): Promise<ThreadChannelStructure> {
const x = await this.edit(threadId, { locked }, reason);
return channelFrom(x, this.client) as ThreadChannelStructure;
}
async edit(threadId: string, body: RESTPatchAPIChannelJSONBody, reason?: string): Promise<ThreadChannelStructure> {
@ -110,7 +107,7 @@ export class ThreadShorter extends BaseShorter {
return this.client.proxy.channels(threadId)['thread-members'].get({ query }) as never;
}
async listArchivedThreads(
async listArchived(
channelId: string,
type: 'public' | 'private',
query?: RESTGetAPIChannelThreadsArchivedQuery,
@ -128,6 +125,25 @@ export class ThreadShorter extends BaseShorter {
};
}
async listGuildActive(guildId: string, force = false): Promise<ThreadChannelStructure[]> {
if (!force) {
const cached = await this.client.cache.channels?.valuesRaw(guildId);
if (cached)
return cached
.filter(x =>
[ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread].includes(x.type),
)
.map(x => channelFrom(x, this.client) as ThreadChannelStructure);
}
const data = await this.client.proxy.guilds(guildId).threads.active.get();
return Promise.all(
data.threads.map(async thread => {
await this.client.cache.channels?.setIfNI(CacheFrom.Rest, 'Guilds', thread.id, guildId, thread);
return channelFrom(thread, this.client) as ThreadChannelStructure;
}),
);
}
async listJoinedArchivedPrivate(
channelId: string,
query?: RESTGetAPIChannelThreadsArchivedQuery,

View File

@ -1,9 +1,9 @@
import { resolveFiles } from '../../builders';
import { Transformers, type WebhookMessageStructure, type WebhookStructure } from '../../client/transformers';
import {
MessagesMethods,
type MessageWebhookMethodEditParams,
type MessageWebhookMethodWriteParams,
MessagesMethods,
} from '../../structures';
import type {
APIWebhook,

View File

@ -1,6 +1,6 @@
import type { Identify } from '..';
import type { CDNUrlOptions } from '../../api';
import type { UsingClient } from '../../commands';
import type { Identify } from '..';
export type ImageOptions = CDNUrlOptions;

View File

@ -1,7 +1,7 @@
import type { EmbedColors, OmitInsert } from '..';
import type { Attachment, AttachmentDataType, AttachmentResolvable } from '../../builders';
import type { GuildMember } from '../../structures';
import type { APIGuildMember, APIPartialEmoji, RESTPostAPIApplicationEmojiJSONBody } from '../../types';
import type { EmbedColors, OmitInsert } from '..';
export type EmojiResolvable = string | Partial<APIPartialEmoji> | `<${string | undefined}:${string}:${string}>`;
export type GuildMemberResolvable = string | Partial<GuildMember> | APIGuildMember;

View File

@ -107,7 +107,7 @@ export type SnakeCase<S extends string> = S extends `${infer A}${infer Rest}`
export type ObjectToLower<T> = T extends unknown[]
? ObjectToLower<T[0]>[]
: Identify<{
[K in keyof T as CamelCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
[K in keyof T as K extends number ? K : CamelCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
? Identify<ObjectToLower<T[K][0]>[]>
: T[K] extends object
? Identify<ObjectToLower<T[K]>>
@ -119,7 +119,7 @@ export type ObjectToLower<T> = T extends unknown[]
export type ObjectToLowerUndefined<T> = T extends unknown[]
? ObjectToLower<T[0]>[]
: Identify<{
[K in keyof T as CamelCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
[K in keyof T as K extends number ? K : CamelCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
? ObjectToLower<T[K][0]>[]
: T[K] extends object
? ObjectToLower<T[K]>
@ -127,7 +127,7 @@ export type ObjectToLowerUndefined<T> = T extends unknown[]
}>;
export type ObjectToSnake<T> = Identify<{
[K in keyof T as SnakeCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
[K in keyof T as K extends number ? K : SnakeCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
? Identify<ObjectToSnake<T[K][0]>[]>
: T[K] extends object
? Identify<ObjectToSnake<T[K]>>
@ -139,7 +139,7 @@ export type ObjectToSnake<T> = Identify<{
export type ObjectToSnakeUndefined<T> = T extends unknown[]
? ObjectToSnake<T[0]>[]
: Identify<{
[K in keyof T as SnakeCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
[K in keyof T as K extends number ? K : SnakeCase<Exclude<K, symbol | number>>]: T[K] extends unknown[]
? ObjectToSnake<T[K][0]>[]
: T[K] extends object
? ObjectToSnake<T[K]>
@ -171,14 +171,10 @@ type OptionalizeAux<T extends object> = Identify<
* it is recursive
*/
export type Optionalize<T> = T extends object
? // biome-ignore lint/style/useShorthandArrayType: typescript things
// biome-ignore lint/style/useConsistentArrayType: <explanation>
T extends Array<unknown>
? T extends Array<unknown>
? number extends T['length']
? T[number] extends object
? // biome-ignore lint/style/useShorthandArrayType: <explanation>
// biome-ignore lint/style/useConsistentArrayType: <explanation>
Array<OptionalizeAux<T[number]>>
? Array<OptionalizeAux<T[number]>>
: T
: Partial<T>
: OptionalizeAux<T>

View File

@ -1,20 +1,11 @@
import type { RawFile } from '../../api';
import type { Attachment, AttachmentBuilder, Embed, Modal, PollBuilder, TopLevelBuilders } from '../../builders';
import type {
ActionRow,
Attachment,
AttachmentBuilder,
BuilderComponents,
Embed,
Modal,
PollBuilder,
} from '../../builders';
import type {
APIActionRowComponent,
APIEmbed,
APIInteractionResponseCallbackData,
APIInteractionResponseChannelMessageWithSource,
APIMessageActionRowComponent,
APIModalInteractionResponse,
MessageFlags,
RESTAPIPollCreate,
RESTPatchAPIChannelMessageJSONBody,
RESTPatchAPIWebhookWithTokenMessageJSONBody,
@ -25,8 +16,9 @@ import type {
import type { OmitInsert } from './util';
export interface ResolverProps {
content?: string | undefined | null;
embeds?: Embed[] | APIEmbed[] | undefined;
components?: APIActionRowComponent<APIMessageActionRowComponent>[] | ActionRow<BuilderComponents>[] | undefined;
components?: TopLevelBuilders[] | ReturnType<TopLevelBuilders['toJSON']>[];
files?: AttachmentBuilder[] | Attachment[] | RawFile[] | undefined;
}
@ -36,44 +28,50 @@ export interface SendResolverProps extends ResolverProps {
export type MessageCreateBodyRequest = OmitInsert<
RESTPostAPIChannelMessageJSONBody,
'components' | 'embeds' | 'poll',
'components' | 'embeds' | 'poll' | 'content',
SendResolverProps
>;
export type MessageUpdateBodyRequest = OmitInsert<
RESTPatchAPIChannelMessageJSONBody,
'components' | 'embeds',
'components' | 'embeds' | 'content',
ResolverProps
>;
export type MessageWebhookCreateBodyRequest = OmitInsert<
RESTPostAPIWebhookWithTokenJSONBody,
'components' | 'embeds' | 'poll',
'components' | 'embeds' | 'poll' | 'content',
SendResolverProps
>;
export type MessageWebhookUpdateBodyRequest = OmitInsert<
RESTPatchAPIWebhookWithTokenMessageJSONBody,
'components' | 'embeds' | 'poll',
'components' | 'embeds' | 'poll' | 'content',
ResolverProps
>;
export type InteractionMessageUpdateBodyRequest = OmitInsert<
RESTPatchAPIWebhookWithTokenMessageJSONBody,
'components' | 'embeds' | 'poll',
'components' | 'embeds' | 'poll' | 'content',
SendResolverProps
>;
> & {
flags?: MessageFlags;
};
export type ComponentInteractionMessageUpdate = OmitInsert<
APIInteractionResponseCallbackData,
'components' | 'embeds',
'components' | 'embeds' | 'content',
ResolverProps
>;
export type InteractionCreateBodyRequest = OmitInsert<
APIInteractionResponseChannelMessageWithSource['data'],
'components' | 'embeds' | 'poll',
'components' | 'embeds' | 'poll' | 'content',
SendResolverProps
>;
export type ModalCreateBodyRequest = APIModalInteractionResponse['data'] | Modal;
export interface ModalCreateOptions {
waitFor?: number;
}

View File

@ -4,7 +4,7 @@ import type { ActionRowMessageComponents } from './index';
import { componentFactory } from './index';
export class MessageActionRowComponent<
T extends ActionRowMessageComponents,
T extends ActionRowMessageComponents = ActionRowMessageComponents,
> extends BaseComponent<ComponentType.ActionRow> {
private ComponentsFactory: T[];
constructor(data: {

View File

@ -1,13 +1,37 @@
import { fromComponent } from '../builders';
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type Container,
type File,
fromComponent,
type MediaGallery,
type MentionableSelectMenu,
type RoleSelectMenu,
type Section,
type Separator,
type StringSelectMenu,
type TextDisplay,
type TextInput,
type Thumbnail,
type UserSelectMenu,
} from '../builders';
import {
type APIActionRowComponent,
type APIActionRowComponentTypes,
type APIButtonComponent,
type APIChannelSelectComponent,
type APIContainerComponent,
type APIFileComponent,
type APIMediaGalleryComponent,
type APIMentionableSelectComponent,
type APIRoleSelectComponent,
type APISectionComponent,
type APISeparatorComponent,
type APIStringSelectComponent,
type APITextDisplayComponent,
type APITextInputComponent,
type APIThumbnailComponent,
type APIUserSelectComponent,
ComponentType,
} from '../types';
@ -24,7 +48,7 @@ export class BaseComponent<T extends ComponentType> {
}
toBuilder() {
return fromComponent(this.data);
return fromComponent(this.data) as BuilderComponentsMap[T];
}
}
export interface APIComponentsMap {
@ -36,4 +60,29 @@ export interface APIComponentsMap {
[ComponentType.StringSelect]: APIStringSelectComponent;
[ComponentType.UserSelect]: APIUserSelectComponent;
[ComponentType.TextInput]: APITextInputComponent;
[ComponentType.File]: APIFileComponent;
[ComponentType.Thumbnail]: APIThumbnailComponent;
[ComponentType.Section]: APISectionComponent;
[ComponentType.Container]: APIContainerComponent;
[ComponentType.MediaGallery]: APIMediaGalleryComponent;
[ComponentType.Separator]: APISeparatorComponent;
[ComponentType.TextDisplay]: APITextDisplayComponent;
}
export interface BuilderComponentsMap {
[ComponentType.ActionRow]: ActionRow;
[ComponentType.Button]: Button;
[ComponentType.ChannelSelect]: ChannelSelectMenu;
[ComponentType.MentionableSelect]: MentionableSelectMenu;
[ComponentType.RoleSelect]: RoleSelectMenu;
[ComponentType.StringSelect]: StringSelectMenu;
[ComponentType.UserSelect]: UserSelectMenu;
[ComponentType.TextInput]: TextInput;
[ComponentType.File]: File;
[ComponentType.Thumbnail]: Thumbnail;
[ComponentType.Section]: Section;
[ComponentType.Container]: Container;
[ComponentType.MediaGallery]: MediaGallery;
[ComponentType.Separator]: Separator;
[ComponentType.TextDisplay]: TextDisplay;
}

View File

@ -0,0 +1,23 @@
import type { APIContainerComponent, ComponentType } from '../types';
import { type ContainerComponents, componentFactory } from '.';
import { BaseComponent } from './BaseComponent';
export class ContainerComponent extends BaseComponent<ComponentType.Container> {
_components: ContainerComponents[];
constructor(data: APIContainerComponent) {
super(data);
this._components = this.data.components.map(componentFactory) as ContainerComponents[];
}
get components() {
return this.data.components;
}
get accentColor() {
return this.data.accent_color;
}
get spoiler() {
return this.data.spoiler;
}
}

16
src/components/File.ts Normal file
View File

@ -0,0 +1,16 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class FileComponent extends BaseComponent<ComponentType.File> {
get spoiler() {
return this.data.spoiler;
}
get file() {
return this.data.file;
}
get id() {
return this.data.id;
}
}

View File

@ -0,0 +1,12 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class MediaGalleryComponent extends BaseComponent<ComponentType.MediaGallery> {
get items() {
return this.data.items;
}
get id() {
return this.data.id;
}
}

23
src/components/Section.ts Normal file
View File

@ -0,0 +1,23 @@
import type { APISectionComponent, ComponentType } from '../types';
import { componentFactory } from '.';
import { BaseComponent } from './BaseComponent';
import type { ButtonComponent } from './ButtonComponent';
import type { TextDisplayComponent } from './TextDisplay';
import type { ThumbnailComponent } from './Thumbnail';
export class SectionComponent extends BaseComponent<ComponentType.Section> {
protected _components: TextDisplayComponent[];
protected _accessory: ThumbnailComponent | ButtonComponent;
constructor(data: APISectionComponent) {
super(data);
this._components = data.components?.map(componentFactory) as TextDisplayComponent[];
this._accessory = componentFactory(data.accessory) as ThumbnailComponent | ButtonComponent;
}
get components() {
return this._components;
}
get accessory() {
return this._accessory;
}
}

View File

@ -0,0 +1,16 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class SeparatorComponent extends BaseComponent<ComponentType.Separator> {
get id() {
return this.data.id;
}
get spacing() {
return this.data.spacing;
}
get divider() {
return this.data.divider;
}
}

View File

@ -0,0 +1,8 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class TextDisplayComponent extends BaseComponent<ComponentType.TextDisplay> {
get content() {
return this.data.content;
}
}

View File

@ -0,0 +1,20 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class ThumbnailComponent extends BaseComponent<ComponentType.Thumbnail> {
get id() {
return this.data.id;
}
get media() {
return this.data.media;
}
get description() {
return this.data.description;
}
get spoiler() {
return this.data.spoiler;
}
}

View File

@ -14,13 +14,17 @@ export interface ComponentCommand {
export abstract class ComponentCommand {
type = InteractionCommandType.COMPONENT;
abstract componentType: keyof ContextComponentCommandInteractionMap;
customId?: string;
customId?: string | RegExp;
filter?(context: ComponentContext<typeof this.componentType>): Promise<boolean> | boolean;
abstract run(context: ComponentContext<typeof this.componentType>): any;
/** @internal */
_filter(context: ComponentContext) {
if (this.customId && this.customId !== context.customId) return false;
if (this.customId) {
const matches =
typeof this.customId === 'string' ? this.customId === context.customId : context.customId.match(this.customId);
if (!matches) return false;
}
if (this.filter) return this.filter(context);
return true;
}
@ -33,6 +37,7 @@ export abstract class ComponentCommand {
return ComponentType[this.componentType];
}
onBeforeMiddlewares?(context: ComponentContext): any;
onAfterRun?(context: ComponentContext, error: unknown | undefined): any;
onRunError?(context: ComponentContext, error: unknown): any;
onMiddlewaresError?(context: ComponentContext, error: string): any;

View File

@ -18,16 +18,18 @@ import type {
} from '../client/transformers';
import type { CommandMetadata, ExtendContext, GlobalMetadata, RegisteredMiddlewares, UsingClient } from '../commands';
import { BaseContext } from '../commands/basecontext';
import type {
import {
ComponentInteractionMessageUpdate,
InteractionCreateBodyRequest,
InteractionMessageUpdateBodyRequest,
MakeRequired,
MessageWebhookCreateBodyRequest,
ModalCreateBodyRequest,
ModalCreateOptions,
UnionToTuple,
When,
} from '../common';
import { ModalSubmitInteraction } from '../structures';
import { ComponentType, MessageFlags, type RESTGetAPIGuildQuery } from '../types';
export interface ComponentContext<
@ -150,8 +152,11 @@ export class ComponentContext<
return this.interaction.deleteResponse();
}
modal(body: ModalCreateBodyRequest) {
return this.interaction.modal(body);
modal(body: ModalCreateBodyRequest, options?: undefined): Promise<undefined>;
modal(body: ModalCreateBodyRequest, options: ModalCreateOptions): Promise<ModalSubmitInteraction | null>;
modal(body: ModalCreateBodyRequest, options?: ModalCreateOptions | undefined) {
if (options === undefined) return this.interaction.modal(body);
return this.interaction.modal(body, options);
}
/**

View File

@ -9,7 +9,7 @@ import type {
import { LimitedCollection } from '../collection';
import { BaseCommand, type RegisteredMiddlewares, type UsingClient } from '../commands';
import type { FileLoaded } from '../commands/handler';
import { BaseHandler, type Logger, type OnFailCallback, isCloudfareWorker, magicImport } from '../common';
import { BaseHandler, isCloudfareWorker, type Logger, magicImport, type OnFailCallback } from '../common';
import type { ComponentInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from '../structures';
import { ComponentCommand, InteractionCommandType } from './componentcommand';
import type { ComponentContext } from './componentcontext';
@ -38,6 +38,11 @@ export interface CreateComponentCollectorResult {
callback: ComponentCallback<T>,
): void;
stop(reason?: string): void;
waitFor<T extends CollectorInteraction = CollectorInteraction>(
customId: UserMatches,
timeout?: number,
): Promise<T | null>;
resetTimeouts(): void;
}
export class ComponentHandler extends BaseHandler {
@ -115,6 +120,28 @@ export class ComponentHandler extends BaseHandler {
this.createComponentCollector(messageId, channelId, guildId, options, old.components);
});
},
waitFor: (customId, timeout) =>
new Promise(resolve => {
const collector = this.values.get(messageId);
if (!collector) return resolve(null);
let nodeTimeout: NodeJS.Timeout | undefined;
this.values.get(messageId)!.__run(customId, interaction => {
clearTimeout(nodeTimeout);
//@ts-expect-error generic
resolve(interaction);
});
if (timeout && timeout > 0)
nodeTimeout = setTimeout(() => {
resolve(null);
// by default 15 seconds in case user don't do anything
}, timeout);
}),
resetTimeouts: () => {
this.resetTimeouts(messageId);
},
};
}
@ -204,6 +231,7 @@ export class ComponentHandler extends BaseHandler {
component.onMiddlewaresError ??= this.client.options?.[is]?.defaults?.onMiddlewaresError;
component.onRunError ??= this.client.options?.[is]?.defaults?.onRunError;
component.onAfterRun ??= this.client.options?.[is]?.defaults?.onAfterRun;
component.onBeforeMiddlewares ??= this.client.options?.[is]?.defaults?.onBeforeMiddlewares;
}
set(instances: (new () => ComponentCommands)[]) {
@ -289,6 +317,7 @@ export class ComponentHandler extends BaseHandler {
async execute(i: ComponentCommands, context: ComponentContext | ModalContext) {
try {
await i.onBeforeMiddlewares?.(context as never);
const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares(
context,
(context.client.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares,
@ -298,7 +327,7 @@ export class ComponentHandler extends BaseHandler {
return;
}
if ('error' in resultRunGlobalMiddlewares) {
return i.onMiddlewaresError?.(context as never, resultRunGlobalMiddlewares.error ?? 'Unknown error');
return await i.onMiddlewaresError?.(context as never, resultRunGlobalMiddlewares.error ?? 'Unknown error');
}
const resultRunMiddlewares = await BaseCommand.__runMiddlewares(context, i.middlewares, false);
@ -306,7 +335,7 @@ export class ComponentHandler extends BaseHandler {
return;
}
if ('error' in resultRunMiddlewares) {
return i.onMiddlewaresError?.(context as never, resultRunMiddlewares.error ?? 'Unknown error');
return await i.onMiddlewaresError?.(context as never, resultRunMiddlewares.error ?? 'Unknown error');
}
try {
@ -319,9 +348,8 @@ export class ComponentHandler extends BaseHandler {
} catch (error) {
try {
await i.onInternalError?.(this.client, error);
} catch (e) {
// supress error
this.logger.error(e);
} catch (err) {
this.client.logger.error(`[${i.customId ?? 'Component/Modal command'}] Internal error:`, err);
}
}
}

View File

@ -1,11 +1,19 @@
import { type APIMessageActionRowComponent, ButtonStyle, ComponentType } from '../types';
import { type APIComponents, type APITopLevelComponent, ButtonStyle, ComponentType } from '../types';
import { MessageActionRowComponent } from './ActionRow';
import { BaseComponent } from './BaseComponent';
import { ButtonComponent, LinkButtonComponent, SKUButtonComponent } from './ButtonComponent';
import { ChannelSelectMenuComponent } from './ChannelSelectMenuComponent';
import { ContainerComponent } from './Container';
import { FileComponent } from './File';
import { MediaGalleryComponent } from './MediaGallery';
import { MentionableSelectMenuComponent } from './MentionableSelectMenuComponent';
import { RoleSelectMenuComponent } from './RoleSelectMenuComponent';
import { SectionComponent } from './Section';
import { SeparatorComponent } from './Separator';
import { StringSelectMenuComponent } from './StringSelectMenuComponent';
import type { TextInputComponent } from './TextInputComponent';
import { TextDisplayComponent } from './TextDisplay';
import { TextInputComponent } from './TextInputComponent';
import { ThumbnailComponent } from './Thumbnail';
import { UserSelectMenuComponent } from './UserSelectMenuComponent';
export type MessageComponents =
@ -21,20 +29,37 @@ export type MessageComponents =
export type ActionRowMessageComponents = Exclude<MessageComponents, TextInputComponent>;
export type AllComponents = MessageComponents | TopLevelComponents | ContainerComponents | BaseComponent<ComponentType>;
export * from './componentcommand';
export * from './componentcontext';
export * from './modalcommand';
export * from './modalcontext';
export type TopLevelComponents =
| SectionComponent
| ActionRowMessageComponents
| TextDisplayComponent
| ContainerComponent
| FileComponent
| MediaGalleryComponent
| BaseComponent<APITopLevelComponent['type']>;
export type ContainerComponents =
| MessageActionRowComponent
| TextDisplayComponent
| MediaGalleryComponent
| SectionComponent
| SeparatorComponent
| FileComponent;
/**
* Return a new component instance based on the component type.
*
* @param component The component to create.
* @returns The component instance.
*/
export function componentFactory(
component: APIMessageActionRowComponent,
): ActionRowMessageComponents | BaseComponent<ActionRowMessageComponents['type']> {
export function componentFactory(component: APIComponents): AllComponents {
switch (component.type) {
case ComponentType.Button: {
if (component.style === ButtonStyle.Link) {
@ -55,7 +80,25 @@ export function componentFactory(
return new UserSelectMenuComponent(component);
case ComponentType.MentionableSelect:
return new MentionableSelectMenuComponent(component);
case ComponentType.ActionRow:
return new MessageActionRowComponent(component as any);
case ComponentType.Container:
return new ContainerComponent(component);
case ComponentType.File:
return new FileComponent(component);
case ComponentType.MediaGallery:
return new MediaGalleryComponent(component);
case ComponentType.Section:
return new SectionComponent(component);
case ComponentType.TextDisplay:
return new TextDisplayComponent(component);
case ComponentType.Separator:
return new SeparatorComponent(component);
case ComponentType.Thumbnail:
return new ThumbnailComponent(component);
case ComponentType.TextInput:
return new TextInputComponent(component);
default:
return new BaseComponent<ActionRowMessageComponents['type']>(component);
return new BaseComponent<ComponentType>(component);
}
}

View File

@ -23,6 +23,7 @@ export abstract class ModalCommand {
props!: ExtraProps;
onBeforeMiddlewares?(context: ModalContext): any;
onAfterRun?(context: ModalContext, error: unknown | undefined): any;
onRunError?(context: ModalContext, error: unknown): any;
onMiddlewaresError?(context: ModalContext, error: string): any;

View File

@ -1,4 +1,4 @@
import type { AllChannels, Interaction, ModalCommand, ModalSubmitInteraction, ReturnCache } from '..';
import type { AllChannels, ModalCommand, ModalSubmitInteraction, ReturnCache } from '..';
import type {
GuildMemberStructure,
GuildStructure,
@ -8,12 +8,13 @@ import type {
} from '../client/transformers';
import type { CommandMetadata, ExtendContext, GlobalMetadata, RegisteredMiddlewares, UsingClient } from '../commands';
import { BaseContext } from '../commands/basecontext';
import type {
import {
InteractionCreateBodyRequest,
InteractionMessageUpdateBodyRequest,
MakeRequired,
MessageWebhookCreateBodyRequest,
ModalCreateBodyRequest,
ModalCreateOptions,
UnionToTuple,
When,
} from '../common';
@ -119,9 +120,11 @@ export class ModalContext<M extends keyof RegisteredMiddlewares = never> extends
return this.interaction.deleteResponse();
}
modal(body: ModalCreateBodyRequest): ReturnType<Interaction['modal']> {
//@ts-expect-error
return this.interaction.modal(body);
modal(body: ModalCreateBodyRequest, options?: undefined): Promise<undefined>;
modal(body: ModalCreateBodyRequest, options: ModalCreateOptions): Promise<ModalSubmitInteraction | null>;
modal(body: ModalCreateBodyRequest, options?: ModalCreateOptions | undefined) {
// @ts-expect-error
return this.interaction.modal(body, options);
}
/**

View File

@ -4,11 +4,11 @@ import type { FileLoaded } from '../commands/handler';
import {
BaseHandler,
type CamelCase,
isCloudfareWorker,
type MakeRequired,
magicImport,
ReplaceRegex,
type SnakeCase,
isCloudfareWorker,
magicImport,
} from '../common';
import type { ClientEvents } from '../events/hooks';
import * as RawEvents from '../events/hooks';

View File

@ -1,3 +1,6 @@
import type { UsingClient } from '../../commands';
import { toCamelCase } from '../../common';
import { type AllChannels, channelFrom } from '../../structures';
import type {
GatewayChannelCreateDispatchData,
GatewayChannelDeleteDispatchData,
@ -5,10 +8,6 @@ import type {
GatewayChannelUpdateDispatchData,
} from '../../types';
import type { UsingClient } from '../../commands';
import { toCamelCase } from '../../common';
import { type AllChannels, channelFrom } from '../../structures';
export const CHANNEL_CREATE = (self: UsingClient, data: GatewayChannelCreateDispatchData): AllChannels => {
return channelFrom(data, self);
};

View File

@ -20,8 +20,8 @@ import type {
GatewayGuildIntegrationsUpdateDispatchData,
GatewayGuildMemberAddDispatchData,
GatewayGuildMemberRemoveDispatchData,
GatewayGuildMemberUpdateDispatchData,
GatewayGuildMembersChunkDispatchData,
GatewayGuildMemberUpdateDispatchData,
GatewayGuildRoleCreateDispatchData,
GatewayGuildRoleDeleteDispatchData,
GatewayGuildRoleUpdateDispatchData,

View File

@ -10,13 +10,14 @@ export * from './interactions';
export * from './invite';
export * from './message';
export * from './presence';
export * from './soundboard';
export * from './stage';
export * from './subscriptions';
export * from './thread';
export * from './typing';
export * from './user';
export * from './voice';
export * from './webhook';
export * from './soundboard';
import type { CamelCase } from '../../common';
import type * as RawEvents from './index';

View File

@ -1,6 +1,6 @@
import { type MessageStructure, Transformers } from '../../client/transformers';
import type { UsingClient } from '../../commands';
import { type ObjectToLower, type OmitInsert, fakePromise, toCamelCase } from '../../common';
import { fakePromise, type ObjectToLower, type OmitInsert, toCamelCase } from '../../common';
import type {
GatewayMessageCreateDispatchData,
GatewayMessageDeleteBulkDispatchData,

View File

@ -5,8 +5,8 @@ import type {
APISoundBoard,
GatewayGuildSoundboardSoundCreateDispatchData,
GatewayGuildSoundboardSoundDeleteDispatchData,
GatewayGuildSoundboardSoundUpdateDispatchData,
GatewayGuildSoundboardSoundsUpdateDispatchData,
GatewayGuildSoundboardSoundUpdateDispatchData,
GatewaySoundboardSoundsDispatchData,
} from '../../types';

View File

@ -5,8 +5,8 @@ import type {
GatewayThreadCreateDispatchData,
GatewayThreadDeleteDispatchData,
GatewayThreadListSyncDispatchData,
GatewayThreadMemberUpdateDispatchData,
GatewayThreadMembersUpdateDispatchData,
GatewayThreadMemberUpdateDispatchData,
GatewayThreadUpdateDispatchData,
} from '../../types';

View File

@ -1,4 +1,5 @@
export * from './client';
import {
BaseClient,
type BaseClientOptions,
@ -10,21 +11,22 @@ import {
import { isCloudfareWorker } from './common';
import type { ClientNameEvents, CustomEventsKeys, ResolveEventParams } from './events';
import { GatewayIntentBits } from './types';
export { Logger, PermissionStrings, Formatter } from './common';
//
export { Collection, LimitedCollection } from './collection';
//
export * from './api';
export * from './builders';
export * from './cache';
//
export { Collection, LimitedCollection } from './collection';
export * from './commands';
export { Formatter, Logger, PermissionStrings } from './common';
export * from './components';
export * from './events';
export * from './langs';
//
export { ShardManager, WorkerManager } from './websocket/discord';
//
export * from './structures';
//
export { ShardManager, WorkerManager } from './websocket/discord';
/**
* Creates an event with the specified data and run function.

View File

@ -14,6 +14,7 @@ export const LangRouter = (userLocale: string, defaultLang: string, langs: Parti
function getValue(locale?: string) {
if (typeof locale === 'undefined') throw new Error('Undefined locale');
let value = langs[locale] as Record<string, any>;
if (typeof value === 'undefined') throw new Error(`Locale "${locale}" not found`);
for (const i of route) value = value[i];
return value;
}
@ -51,4 +52,4 @@ export type __InternalParseLocale<T extends Record<string, any>> = {
};
export type ParseLocales<T extends Record<string, any>> = T;
/**Idea inspiration from: FreeAoi */
/**Idea inspiration from: FreeAoi | Fixed by: Drylozu */

View File

@ -1,18 +1,18 @@
import type { GuildMemberStructure, GuildStructure } from '../client';
import type { GuildMemberStructure, GuildStructure, ThreadChannelStructure } from '../client';
import type { UsingClient } from '../commands';
import type { CreateInviteFromChannel } from '../common';
import type { ObjectToLower, StructPropState, StructStates, ToClass } from '../common/types/util';
import type { APIGuild, APIPartialGuild, GatewayGuildCreateDispatchData, RESTPatchAPIGuildJSONBody } from '../types';
import { AutoModerationRule } from './AutoModerationRule';
import { BaseChannel, WebhookGuildMethods } from './channels';
import { GuildEmoji } from './Emoji';
import { BaseGuild } from './extra/BaseGuild';
import type { DiscordBase } from './extra/DiscordBase';
import { GuildBan } from './GuildBan';
import { GuildMember } from './GuildMember';
import { GuildRole } from './GuildRole';
import { GuildTemplate } from './GuildTemplate';
import { Sticker } from './Sticker';
import { BaseChannel, WebhookGuildMethods } from './channels';
import { BaseGuild } from './extra/BaseGuild';
import type { DiscordBase } from './extra/DiscordBase';
export interface Guild extends ObjectToLower<Omit<APIGuild, 'stickers' | 'emojis' | 'roles'>>, DiscordBase {}
export class Guild<State extends StructStates = 'api'> extends (BaseGuild as unknown as ToClass<
@ -71,6 +71,10 @@ export class Guild<State extends StructStates = 'api'> extends (BaseGuild as unk
return this.members.fetch(this.ownerId, force);
}
listActiveThreads(force = false): Promise<ThreadChannelStructure[]> {
return this.client.threads.listGuildActive(this.id, force);
}
templates = GuildTemplate.methods({ client: this.client, guildId: this.id });
stickers = Sticker.methods({ client: this.client, guildId: this.id });
members = GuildMember.methods({ client: this.client, guildId: this.id });

View File

@ -3,7 +3,7 @@ import type { GuildBanStructure, GuildStructure } from '../client';
import type { UsingClient } from '../commands';
import { Formatter, type MethodContext, type ObjectToLower } from '../common';
import type { BanShorter } from '../common/shorters/bans';
import type { APIBan, ActuallyBan, RESTGetAPIGuildBansQuery } from '../types';
import type { ActuallyBan, APIBan, RESTGetAPIGuildBansQuery } from '../types';
import { DiscordBase } from './extra/DiscordBase';
export interface GuildBan extends DiscordBase, ObjectToLower<Omit<APIBan, 'id'>> {}

View File

@ -46,7 +46,7 @@ export class GuildRole extends DiscordBase {
return this.client.roles.edit(this.guildId, this.id, body);
}
delete(reason?: string): Promise<GuildRoleStructure> {
delete(reason?: string) {
return this.client.roles.delete(this.guildId, this.id, reason);
}
@ -61,8 +61,7 @@ export class GuildRole extends DiscordBase {
list: (force = false): Promise<GuildRoleStructure[]> => ctx.client.roles.list(ctx.guildId, force),
edit: (roleId: string, body: RESTPatchAPIGuildRoleJSONBody, reason?: string): Promise<GuildRoleStructure> =>
ctx.client.roles.edit(ctx.guildId, roleId, body, reason),
delete: (roleId: string, reason?: string): Promise<GuildRoleStructure> =>
ctx.client.roles.delete(ctx.guildId, roleId, reason),
delete: (roleId: string, reason?: string) => ctx.client.roles.delete(ctx.guildId, roleId, reason),
editPositions: (body: RESTPatchAPIGuildRolePositionsJSONBody): Promise<GuildRoleStructure[]> =>
ctx.client.roles.editPositions(ctx.guildId, body),
};

View File

@ -1,3 +1,34 @@
import type { RawFile } from '../api';
import { ActionRow, Embed, Modal, PollBuilder, resolveAttachment, resolveFiles } from '../builders';
import type { ReturnCache } from '../cache';
import {
type EntitlementStructure,
type GuildRoleStructure,
type GuildStructure,
type InteractionGuildMemberStructure,
type MessageStructure,
type OptionResolverStructure,
Transformers,
type UserStructure,
type WebhookMessageStructure,
} from '../client/transformers';
import type { ContextOptionsResolved, UsingClient } from '../commands';
import {
type ComponentInteractionMessageUpdate,
type InteractionCreateBodyRequest,
type InteractionMessageUpdateBodyRequest,
type MessageCreateBodyRequest,
type MessageUpdateBodyRequest,
type MessageWebhookCreateBodyRequest,
type ModalCreateBodyRequest,
ModalCreateOptions,
type ObjectToLower,
type OmitInsert,
type ToClass,
toCamelCase,
type When,
} from '../common';
import { mix } from '../deps/mixer';
import {
type APIActionRowComponent,
type APIApplicationCommandAutocompleteInteraction,
@ -41,37 +72,6 @@ import {
type RESTPostAPIInteractionCallbackJSONBody,
type RESTPostAPIInteractionCallbackResult,
} from '../types';
import type { RawFile } from '../api';
import { ActionRow, Embed, Modal, PollBuilder, resolveAttachment, resolveFiles } from '../builders';
import type { ReturnCache } from '../cache';
import {
type EntitlementStructure,
type GuildRoleStructure,
type GuildStructure,
type InteractionGuildMemberStructure,
type MessageStructure,
type OptionResolverStructure,
Transformers,
type UserStructure,
type WebhookMessageStructure,
} from '../client/transformers';
import type { ContextOptionsResolved, UsingClient } from '../commands';
import {
type ComponentInteractionMessageUpdate,
type InteractionCreateBodyRequest,
type InteractionMessageUpdateBodyRequest,
type MessageCreateBodyRequest,
type MessageUpdateBodyRequest,
type MessageWebhookCreateBodyRequest,
type ModalCreateBodyRequest,
type ObjectToLower,
type OmitInsert,
type ToClass,
type When,
toCamelCase,
} from '../common';
import { mix } from '../deps/mixer';
import { type AllChannels, channelFrom } from './';
import { DiscordBase } from './extra/DiscordBase';
import { PermissionsBitField } from './extra/Permissions';
@ -148,6 +148,7 @@ export class BaseInteraction<
//@ts-ignore
return {
type: body.type,
// @ts-expect-error
data: BaseInteraction.transformBody(body.data ?? {}, files, self),
};
}
@ -195,7 +196,7 @@ export class BaseInteraction<
poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined,
};
if ('attachments' in body) {
if (Array.isArray(body.attachments)) {
payload.attachments =
body.attachments?.map((x, i) => ({
id: x.id ?? i.toString(),
@ -314,7 +315,6 @@ export class BaseInteraction<
switch (gateway.type) {
case InteractionType.ApplicationCommandAutocomplete:
return new AutocompleteInteraction(client, gateway, undefined, __reply);
// biome-ignore lint/suspicious/noFallthroughSwitchClause: bad interaction between biome and ts-server
case InteractionType.ApplicationCommand:
switch (gateway.data.type) {
case ApplicationCommandType.ChatInput:
@ -330,7 +330,6 @@ export class BaseInteraction<
case ApplicationCommandType.PrimaryEntryPoint:
return new EntryPointInteraction(client, gateway as APIEntryPointCommandInteraction, __reply);
}
// biome-ignore lint/suspicious/noFallthroughSwitchClause: bad interaction between biome and ts-server
case InteractionType.MessageComponent:
switch (gateway.data.component_type) {
case ComponentType.Button:
@ -357,6 +356,8 @@ export class BaseInteraction<
gateway as APIMessageComponentSelectMenuInteraction,
__reply,
);
default:
return;
}
case InteractionType.ModalSubmit:
return new ModalSubmitInteraction(client, gateway);
@ -472,11 +473,37 @@ export class Interaction<
) as never;
}
modal(body: ModalCreateBodyRequest) {
return this.reply({
modal(body: ModalCreateBodyRequest, options?: undefined): Promise<undefined>;
modal(body: ModalCreateBodyRequest, options: ModalCreateOptions): Promise<ModalSubmitInteraction | null>;
async modal(body: ModalCreateBodyRequest, options?: ModalCreateOptions | undefined) {
if (options !== undefined && !(body instanceof Modal)) {
body = new Modal(body);
}
if (options === undefined)
return this.reply({
type: InteractionResponseType.Modal,
data: body,
});
const promise = new Promise<ModalSubmitInteraction | null>(res => {
let nodeTimeout: NodeJS.Timeout | undefined;
// body is always a modal here, so we can safely cast it
(body as Modal).__exec = (interaction: ModalSubmitInteraction) => {
res(interaction);
clearTimeout(nodeTimeout);
};
if (options?.waitFor && options?.waitFor > 0) {
nodeTimeout = setTimeout(() => {
res(null);
}, options.waitFor);
}
});
await this.reply({
type: InteractionResponseType.Modal,
data: body,
});
return promise;
}
async editOrReply<FR extends boolean = false>(
@ -488,8 +515,8 @@ export class Interaction<
fetchReply?: FR,
): Promise<WebhookMessageStructure> {
if (await this.replied) {
const { content, embeds, allowed_mentions, components, files, attachments, poll } = body;
return this.editResponse({ content, embeds, allowed_mentions, components, files, attachments, poll });
const { content, embeds, allowed_mentions, components, files, attachments, poll, flags } = body;
return this.editResponse({ content, embeds, allowed_mentions, components, files, attachments, poll, flags });
}
return this.write(body as InteractionCreateBodyRequest, fetchReply);
}

View File

@ -1,4 +1,4 @@
import { type AllChannels, Embed, type ReturnCache } from '..';
import { type AllChannels, componentFactory, Embed, type ReturnCache } from '..';
import type { ListenerOptions } from '../builders';
import {
type GuildMemberStructure,
@ -11,12 +11,10 @@ import {
type WebhookStructure,
} from '../client/transformers';
import type { UsingClient } from '../commands';
import { type ObjectToLower, toCamelCase } from '../common';
import { Formatter } from '../common';
import { Formatter, type ObjectToLower, toCamelCase } from '../common';
import type { EmojiResolvable } from '../common/types/resolvables';
import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../common/types/write';
import type { ActionRowMessageComponents } from '../components';
import { MessageActionRowComponent } from '../components/ActionRow';
import type { TopLevelComponents } from '../components';
import type {
APIChannelMention,
APIEmbed,
@ -25,8 +23,8 @@ import type {
APIUser,
GatewayMessageCreateDispatchData,
} from '../types';
import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook';
import { DiscordBase } from './extra/DiscordBase';
import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook';
export type MessageData = APIMessage | GatewayMessageCreateDispatchData;
@ -37,7 +35,7 @@ export interface BaseMessage
guildId?: string;
author: UserStructure;
member?: GuildMemberStructure;
components: MessageActionRowComponent<ActionRowMessageComponents>[];
components: TopLevelComponents[];
poll?: PollStructure;
mentions: {
roles: string[];
@ -55,7 +53,7 @@ export class BaseMessage extends DiscordBase {
channels: data.mention_channels ?? [],
users: [],
};
this.components = data.components?.map(x => new MessageActionRowComponent(x)) ?? [];
this.components = (data.components?.map(componentFactory) as TopLevelComponents[]) ?? [];
this.embeds = data.embeds.map(embed => new InMessageEmbed(embed));
this.patch(data);
}

View File

@ -21,9 +21,9 @@ import type {
*/
import type {
APIWebhook,
RESTGetAPIWebhookWithTokenMessageQuery,
RESTPatchAPIWebhookJSONBody,
RESTPatchAPIWebhookWithTokenJSONBody,
RESTPatchAPIWebhookWithTokenMessageQuery,
RESTPostAPIWebhookWithTokenQuery,
} from '../types';
import type { AllChannels } from './channels';
@ -181,7 +181,7 @@ export class Webhook extends DiscordBase {
/** Type definition for parameters of editing a message through a webhook. */
export type MessageWebhookMethodEditParams = MessageWebhookPayload<
MessageWebhookUpdateBodyRequest,
{ messageId: string; query?: RESTGetAPIWebhookWithTokenMessageQuery }
{ messageId: string; query?: RESTPatchAPIWebhookWithTokenMessageQuery }
>;
/** Type definition for parameters of writing a message through a webhook. */
export type MessageWebhookMethodWriteParams = MessageWebhookPayload<

View File

@ -1,12 +1,12 @@
import { Collection, Formatter, type RawFile, type ReturnCache } from '..';
import { ActionRow, Embed, PollBuilder, resolveAttachment } from '../builders';
import { Embed, PollBuilder, resolveAttachment } from '../builders';
import type { Overwrites } from '../cache/resources/overwrites';
import {
type BaseChannelStructure,
type BaseGuildChannelStructure,
type CategoryChannelStructure,
type DMChannelStructure,
type DirectoryChannelStructure,
type DMChannelStructure,
type ForumChannelStructure,
type GuildMemberStructure,
type GuildStructure,
@ -26,13 +26,13 @@ import type { SeyfertChannelMap, UsingClient } from '../commands';
import {
type CreateInviteFromChannel,
type EmojiResolvable,
fakePromise,
type MessageCreateBodyRequest,
type MessageUpdateBodyRequest,
type MethodContext,
type ObjectToLower,
type StringToNumber,
type ToClass,
fakePromise,
} from '../common';
import { mix } from '../deps/mixer';
import {
@ -63,9 +63,9 @@ import {
type ThreadAutoArchiveDuration,
VideoQualityMode,
} from '../types';
import { DiscordBase } from './extra/DiscordBase';
import type { GuildMember } from './GuildMember';
import type { GuildRole } from './GuildRole';
import { DiscordBase } from './extra/DiscordBase';
export class BaseNoEditableChannel<T extends ChannelType> extends DiscordBase<APIChannelBase<ChannelType>> {
declare type: T;
@ -353,8 +353,8 @@ export class MessagesMethods extends DiscordBase {
const payload = {
allowed_mentions: self.options?.allowedMentions,
...body,
components: body.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)) ?? undefined,
embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined,
embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)),
components: body.components?.map(x => ('toJSON' in x ? x.toJSON() : x)) ?? undefined,
poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined,
};
@ -581,13 +581,15 @@ export class ForumChannel extends BaseGuildChannel {
export interface ThreadChannel
extends ObjectToLower<Omit<APIThreadChannel, 'permission_overwrites' | 'guild_id'>>,
Omit<TextBaseGuildChannel, 'edit' | 'parentId'> {}
Omit<TextBaseGuildChannel, 'edit' | 'parentId'> {
parentId: string;
}
@mix(TextBaseGuildChannel)
export class ThreadChannel extends BaseChannel<
ChannelType.PublicThread | ChannelType.AnnouncementThread | ChannelType.PrivateThread
> {
parentId!: string;
declare type: ChannelType.PublicThread | ChannelType.AnnouncementThread | ChannelType.PrivateThread;
webhooks = WebhookChannelMethods.channel({
client: this.client,
channelId: this.parentId,

View File

@ -1,6 +1,6 @@
import type { ReturnCache, WorkerClient } from '../..';
import type { GuildStructure } from '../../client';
import { type ObjectToLower, calculateShardId } from '../../common';
import { calculateShardId, type ObjectToLower } from '../../common';
import type { ImageOptions } from '../../common/types/options';
import { type APIPartialGuild, GuildFeature } from '../../types';
import type { ShardManager } from '../../websocket';

View File

@ -1,19 +1,19 @@
export * from './AnonymousGuild';
export * from './AutoModerationRule';
export * from './ClientUser';
export * from './Guild';
export * from './channels';
export * from './Emoji';
export * from './Entitlement';
export * from './Guild';
export * from './GuildBan';
export * from './GuildMember';
export * from './GuildPreview';
export * from './GuildRole';
export * from './GuildTemplate';
export * from './Interaction';
export * from './Message';
export * from './Poll';
export * from './Sticker';
export * from './User';
export * from './VoiceState';
export * from './Webhook';
export * from './channels';
export * from './Poll';
export * from './GuildBan';
export * from './Entitlement';

View File

@ -23,9 +23,9 @@
*/
export * from './gateway';
export * from './rest';
export * from './payloads';
export * from './rest';
export * from './rest';
export * from './utils';
/**

View File

@ -1,7 +1,6 @@
import type { ApplicationIntegrationType, InteractionContextType } from '..';
import type { ChannelType, Permissions, Snowflake } from '../..';
import type { LocaleString } from '../../rest';
import type { ApplicationIntegrationType, InteractionContextType } from '..';
import type {
APIAttachment,
APIChannel,

View File

@ -1,5 +1,5 @@
import type { Snowflake } from '../../index';
import type { ComponentType } from '../channel';
import type { ComponentType, Snowflake } from '../../index';
import type { APIBaseInteraction, InteractionType } from '../interactions';
import type {
APIDMInteractionWrapper,

View File

@ -1,8 +1,9 @@
import type { APIActionRowComponent, APIModalActionRowComponent } from '../channel';
import type {
APIActionRowComponent,
APIBaseInteraction,
APIDMInteractionWrapper,
APIGuildInteractionWrapper,
APIModalActionRowComponent,
ComponentType,
InteractionType,
} from '../index';

View File

@ -1,7 +1,7 @@
import type { MakeRequired } from '../../../common';
import type { RESTPostAPIWebhookWithTokenJSONBody } from '../../index';
import type { APIActionRowComponent, APIModalActionRowComponent } from '../channel';
import type { MessageFlags } from '../index';
import type { APIActionRowComponent, APIModalActionRowComponent, MessageFlags } from '../index';
import type { APIApplicationCommandOptionChoice } from './applicationCommands';
/**

View File

@ -2,9 +2,9 @@
* Types extracted from https://discord.com/developers/docs/resources/application
*/
import type { APIEmoji, LocalizationMap } from '.';
import type { Permissions, Snowflake } from '..';
import type { MakeRequired } from '../../common';
import type { Permissions, Snowflake } from '..';
import type { APIEmoji, LocalizationMap } from '.';
import type { APIPartialGuild } from './guild';
import type { ApplicationIntegrationType } from './interactions';
import type { OAuth2Scopes } from './oauth2';

Some files were not shown because too many files have changed in this diff Show More