feat: guild bans, formatter and minor functionality changes. (#203)

* fix(Colors#bold): bold returning weird values

* chore: testing something

* fix(Parsers): optionsParser & argsParser were being overriden by default options

* revert(colors): mistakenly pushed a unplanned change

* feat(Formatter): add a formatter class to add support to discord markdown, unix and etc

* chore: set exports for formatter class

* feat(AllGuildVoiceChannels): new type for all guild based voice channels

* feat(VoiceChannel#members): returns a list of members in the voice channel

* chore: small patch to voice channel methods

* feat(GuildBans): addition of bans into seyfert

* chore: request changes - incomplete

* fix(BanShorter#bulkCreate): fix method return, bulk-bans returns stats

* feat(GuildBan#methods): all methods added on the guild ban structure

* chore: missed some, here pushed them

* chore: requested change

---------

Co-authored-by: NotAditya69 <90441096+NotAditya69@users.noreply.github.com>
This commit is contained in:
Xeno 2024-06-10 13:48:54 +05:30 committed by GitHub
parent 37dfaef407
commit 390953082b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1330 additions and 641 deletions

View File

@ -43,7 +43,8 @@
"noUselessConstructor": "off",
"noThisInStatic": "off",
"noExcessiveCognitiveComplexity": "off",
"noVoid": "off"
"noVoid": "off",
"noStaticOnlyClass": "off"
},
"a11y": {
"all": false

View File

@ -84,6 +84,8 @@ import type {
RESTPatchAPIGuildWidgetSettingsResult,
RESTPostAPIAutoModerationRuleJSONBody,
RESTPostAPIAutoModerationRuleResult,
RESTPostAPIGuildBulkBanJSONBody,
RESTPostAPIGuildBulkBanResult,
RESTPostAPIGuildChannelJSONBody,
RESTPostAPIGuildChannelResult,
RESTPostAPIGuildEmojiJSONBody,
@ -233,6 +235,11 @@ export interface GuildRoutes {
delete(args?: RestArguments<ProxyRequestMethod.Delete>): Promise<RESTDeleteAPIGuildBanResult>;
};
};
'bulk-bans': {
post(
args: RestArguments<ProxyRequestMethod.Post, RESTPostAPIGuildBulkBanJSONBody>,
): Promise<RESTPostAPIGuildBulkBanResult>;
};
mfa: {
post(
args: RestArguments<ProxyRequestMethod.Post, RESTPostAPIGuildsMFAJSONBody>,

24
src/cache/index.ts vendored
View File

@ -14,6 +14,7 @@ import { StageInstances } from './resources/stage-instances';
import { Stickers } from './resources/stickers';
import { Threads } from './resources/threads';
import { VoiceStates } from './resources/voice-states';
import { Bans } from './resources/bans';
import { ChannelType, GatewayIntentBits, type GatewayDispatchPayload } from 'discord-api-types/v10';
import type { InternalOptions, UsingClient } from '../commands';
@ -36,7 +37,8 @@ export type GuildRelated =
| 'presences'
| 'stageInstances'
| 'overwrites'
| 'messages';
| 'messages'
| 'bans';
// ClientBased
export type NonGuildBased = 'users' | 'guilds';
@ -58,6 +60,8 @@ export type CachedEvents =
| 'GUILD_ROLE_CREATE'
| 'GUILD_ROLE_UPDATE'
| 'GUILD_ROLE_DELETE'
| 'GUILD_BAN_ADD'
| 'GUILD_BAN_REMOVE'
| 'GUILD_EMOJIS_UPDATE'
| 'GUILD_STICKERS_UPDATE'
| 'GUILD_MEMBER_ADD'
@ -93,6 +97,7 @@ export class Cache {
presences?: Presences;
stageInstances?: StageInstances;
messages?: Messages;
bans?: Bans;
constructor(
public intents: number,
@ -144,6 +149,9 @@ export class Cache {
if (!this.disabledCache.includes('messages')) {
this.messages = new Messages(this, client);
}
if (!this.disabledCache.includes('bans')) {
this.bans = new Bans(this, client);
}
}
/** @internal */
@ -163,6 +171,7 @@ export class Cache {
this.threads?.__setClient(client);
this.stageInstances?.__setClient(client);
this.messages?.__setClient(client);
this.bans?.__setClient(client);
}
flush(): ReturnCache<void> {
@ -206,6 +215,10 @@ export class Cache {
return this.hasIntent('DirectMessages');
}
get hasBansIntent() {
return this.hasIntent('GuildBans');
}
async bulkGet(
keys: (
| readonly [
@ -246,6 +259,7 @@ export class Cache {
case 'users':
case 'guilds':
case 'overwrites':
case 'bans':
case 'messages':
{
if (!allData[type]) {
@ -313,6 +327,7 @@ export class Cache {
case 'stageInstances':
case 'emojis':
case 'overwrites':
case 'bans':
case 'messages':
{
if (!this[type]?.filter(data, id, guildId)) continue;
@ -404,6 +419,7 @@ export class Cache {
case 'stageInstances':
case 'emojis':
case 'overwrites':
case 'bans':
case 'messages':
{
if (!this[type]?.filter(data, id, guildId)) continue;
@ -500,6 +516,12 @@ export class Cache {
case 'GUILD_ROLE_DELETE':
await this.roles?.remove(event.d.role_id, event.d.guild_id);
break;
case 'GUILD_BAN_ADD':
await this.bans?.set(event.d.user.id, event.d.guild_id, event.d);
break;
case 'GUILD_BAN_REMOVE':
await this.bans?.remove(event.d.user.id, event.d.guild_id);
break;
case 'GUILD_EMOJIS_UPDATE':
await this.emojis?.remove(await this.emojis?.keys(event.d.guild_id), event.d.guild_id);
await this.emojis?.set(

46
src/cache/resources/bans.ts vendored Normal file
View File

@ -0,0 +1,46 @@
import type { APIBan } from 'discord-api-types/v10';
import type { ReturnCache } from '../..';
import { fakePromise } from '../../common';
import { GuildBasedResource } from './default/guild-based';
import { GuildBan } from '../../structures/GuildBan';
export class Bans extends GuildBasedResource {
namespace = 'ban';
//@ts-expect-error
filter(data: APIBan, id: string, guild_id: string) {
return true;
}
override parse(data: any, key: string, guild_id: string) {
const { user, ...rest } = super.parse(data, data.user?.id ?? key, guild_id);
return rest;
}
override get(id: string, guild: string): ReturnCache<GuildBan | undefined> {
return fakePromise(super.get(id, guild)).then(rawBan =>
rawBan ? new GuildBan(this.client, rawBan, guild) : undefined,
);
}
override bulk(ids: string[], guild: string): ReturnCache<GuildBan[]> {
return fakePromise(super.bulk(ids, guild)).then(
bans =>
bans
.map(rawBan => {
return rawBan ? new GuildBan(this.client, rawBan, guild) : undefined;
})
.filter(Boolean) as GuildBan[],
);
}
override values(guild: string): ReturnCache<GuildBan[]> {
return fakePromise(super.values(guild)).then(
bans =>
bans
.map(rawBan => {
return rawBan ? new GuildBan(this.client, rawBan, guild) : undefined;
})
.filter(Boolean) as GuildBan[],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -26,13 +26,26 @@ import {
} from '../common';
import { EventHandler } from '../events';
import { ClientUser } from '../structures';
import { ShardManager, properties, type ShardManagerOptions } from '../websocket';
import {
ShardManager,
properties,
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';
import type {
BaseClientOptions,
InternalRuntimeConfig,
ServicesOptions,
StartOptions,
} from './base';
import { BaseClient } from './base';
import { onInteractionCreate } from './oninteractioncreate';
import { defaultArgsParser, defaultOptionsParser, onMessageCreate } from './onmessagecreate';
import {
defaultArgsParser,
defaultOptionsParser,
onMessageCreate,
} from './onmessagecreate';
import { Collectors } from './collectors';
let parentPort: import('node:worker_threads').MessagePort;
@ -42,7 +55,10 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
gateway!: ShardManager;
me!: If<Ready, ClientUser>;
declare options: Omit<ClientOptions, 'commands'> & {
commands: MakeRequired<NonNullable<ClientOptions['commands']>, 'argsParser' | 'optionsParser'>;
commands: MakeRequired<
NonNullable<ClientOptions['commands']>,
'argsParser' | 'optionsParser'
>;
};
memberUpdateHandler = new MemberUpdateHandler();
presenceUpdateHandler = new PresenceUpdateHandler();
@ -54,11 +70,14 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
this.options = MergeOptions(
{
commands: {
argsParser: defaultArgsParser,
optionsParser: defaultOptionsParser,
argsParser:
options?.commands?.argsParser ?? defaultArgsParser,
optionsParser:
options?.commands?.optionsParser ??
defaultOptionsParser,
},
} satisfies ClientOptions,
this.options,
this.options
);
}
@ -96,43 +115,60 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
}
async loadEvents(dir?: string) {
dir ??= await this.getRC().then(x => x.events);
dir ??= await this.getRC().then((x) => x.events);
if (dir && this.events) {
await this.events.load(dir);
this.logger.info('EventHandler loaded');
}
}
protected async execute(options: { token?: string; intents?: number } = {}) {
protected async execute(
options: { token?: string; intents?: number } = {}
) {
await super.execute(options);
const worker_threads = lazyLoadPackage<typeof import('node:worker_threads')>('node:worker_threads');
const worker_threads = lazyLoadPackage<
typeof import('node:worker_threads')
>('node:worker_threads');
if (worker_threads?.parentPort) {
parentPort = worker_threads.parentPort;
}
if (worker_threads?.workerData?.__USING_WATCHER__) {
parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => {
switch (data.type) {
case 'PAYLOAD':
this.gateway.options.handlePayload(data.shardId, data.payload);
break;
case 'SEND_TO_SHARD':
this.gateway.send(data.shardId, data.payload);
break;
parentPort?.on(
'message',
(data: WatcherPayload | WatcherSendToShard) => {
switch (data.type) {
case 'PAYLOAD':
this.gateway.options.handlePayload(
data.shardId,
data.payload
);
break;
case 'SEND_TO_SHARD':
this.gateway.send(data.shardId, data.payload);
break;
}
}
});
);
} else {
await this.gateway.spawnShards();
}
}
async start(options: Omit<DeepPartial<StartOptions>, 'httpConnection'> = {}, execute = true) {
async start(
options: Omit<DeepPartial<StartOptions>, 'httpConnection'> = {},
execute = true
) {
await super.start(options);
await this.loadEvents(options.eventsDir);
const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC<InternalRuntimeConfig>();
const {
token: tokenRC,
intents: intentsRC,
debug: debugRC,
} = await this.getRC<InternalRuntimeConfig>();
const token = options?.token ?? tokenRC;
const intents = options?.connection?.intents ?? intentsRC;
@ -149,9 +185,14 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
presence: this.options?.presence,
debug: debugRC,
shardStart: this.options?.shards?.start,
shardEnd: this.options?.shards?.end ?? this.options?.shards?.total,
totalShards: this.options?.shards?.total ?? this.options?.shards?.end,
properties: { ...properties, ...this.options?.gateway?.properties },
shardEnd:
this.options?.shards?.end ?? this.options?.shards?.total,
totalShards:
this.options?.shards?.total ?? this.options?.shards?.end,
properties: {
...properties,
...this.options?.gateway?.properties,
},
compress: this.options?.gateway?.compress,
});
}
@ -173,29 +214,59 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
if (!this.memberUpdateHandler.check(packet.d)) {
return;
}
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
await this.events?.execute(
packet.t,
packet,
this as Client<true>,
shardId
);
break;
case 'PRESENCE_UPDATE':
if (!this.presenceUpdateHandler.check(packet.d as any)) {
return;
}
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
await this.events?.execute(
packet.t,
packet,
this as Client<true>,
shardId
);
break;
case 'GUILD_CREATE': {
if (this.__handleGuilds?.has(packet.d.id)) {
this.__handleGuilds.delete(packet.d.id);
if (!this.__handleGuilds.size && [...this.gateway.values()].every(shard => shard.data.session_id)) {
await this.events?.runEvent('BOT_READY', this, this.me, -1);
if (
!this.__handleGuilds.size &&
[...this.gateway.values()].every(
(shard) => shard.data.session_id
)
) {
await this.events?.runEvent(
'BOT_READY',
this,
this.me,
-1
);
}
if (!this.__handleGuilds.size) delete this.__handleGuilds;
return this.cache.onPacket(packet);
}
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
await this.events?.execute(
packet.t,
packet,
this as Client<true>,
shardId
);
break;
}
//rest of the events
default: {
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
await this.events?.execute(
packet.t,
packet,
this as Client<true>,
shardId
);
switch (packet.t) {
case 'INTERACTION_CREATE':
await onInteractionCreate(this, packet.d, shardId);
@ -209,19 +280,36 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
}
this.botId = packet.d.user.id;
this.applicationId = packet.d.application.id;
this.me = new ClientUser(this, packet.d.user, packet.d.application) as never;
this.me = new ClientUser(
this,
packet.d.user,
packet.d.application
) as never;
if (
!(
this.__handleGuilds?.size &&
(this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds
(this.gateway.options.intents &
GatewayIntentBits.Guilds) ===
GatewayIntentBits.Guilds
)
) {
if ([...this.gateway.values()].every(shard => shard.data.session_id)) {
await this.events?.runEvent('BOT_READY', this, this.me, -1);
if (
[...this.gateway.values()].every(
(shard) => shard.data.session_id
)
) {
await this.events?.runEvent(
'BOT_READY',
this,
this.me,
-1
);
}
delete this.__handleGuilds;
}
this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`);
this.debugger?.debug(
`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`
);
break;
}
break;
@ -243,15 +331,21 @@ export interface ClientOptions extends BaseClientOptions {
};
commands?: BaseClientOptions['commands'] & {
prefix?: (message: Message) => Promise<string[]> | string[];
deferReplyResponse?: (ctx: CommandContext) => Parameters<Message['write']>[0];
deferReplyResponse?: (
ctx: CommandContext
) => Parameters<Message['write']>[0];
reply?: (ctx: CommandContext) => boolean;
argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record<string, string>;
argsParser?: (
content: string,
command: SubCommand | Command,
message: Message
) => Record<string, string>;
optionsParser?: (
self: UsingClient,
command: Command | SubCommand,
message: GatewayMessageCreateDispatchData,
args: Partial<Record<string, string>>,
resolved: MakeRequired<ContextOptionsResolved>,
resolved: MakeRequired<ContextOptionsResolved>
) => Awaitable<{
errors: {
name: string;

View File

@ -1,8 +1,9 @@
export * from './it/constants';
export * from './it/utils';
//
export * from './it/colors';
export * from './it/logger';
export * from './it/formatter';
//
export * from './shorters/channels';
export * from './shorters/emojis';
export * from './shorters/guilds';
@ -15,6 +16,7 @@ export * from './shorters/users';
export * from './shorters/threads';
export * from './shorters/webhook';
export * from './shorters/interaction';
//
export * from './types/options';
export * from './types/resolvables';
export * from './types/util';

203
src/common/it/formatter.ts Normal file
View File

@ -0,0 +1,203 @@
/**
* Represents heading levels.
*/
export enum HeadingLevel {
/**
* Represents a level 1 heading. (#)
*/
H1 = 1,
/**
* Represents a level 2 heading. (##)
*/
H2 = 2,
/**
* Represents a level 3 heading. (###)
*/
H3 = 3,
}
/**
* Represents timestamp styles.
*/
export enum TimestampStyle {
/**
* Represents a short timestamp style.
*/
ShortTime = 't',
/**
* Represents a long timestamp style.
*/
LongTime = 'T',
/**
* Represents a short date style.
*/
ShortDate = 'd',
/**
* Represents a long date style.
*/
LongDate = 'D',
/**
* Represents a short time style.
*/
ShortDateTime = 'f',
/**
* Represents a long time style.
*/
LongDateTime = 'F',
/**
* Represents a relative time style.
*/
RelativeTime = 'R',
}
/**
* Represents a message link.
*/
type MessageLink = `https://discord.com/channels/${string}/${string}/${string}`;
/**
* Represents a timestamp.
*/
type Timestamp = `<t:${number}:${TimestampStyle}>`;
/**
* Represents a formatter utility for formatting content.
*/
export class Formatter {
/**
* Formats a code block.
* @param content The content of the code block.
* @param language The language of the code block. Defaults to 'txt'.
* @returns The formatted code block.
*/
static codeBlock(content: string, language = 'txt'): string {
return `\`\`\`${language}\n${content}\n\`\`\``;
}
/**
* Formats content into inline code.
* @param content The content to format.
* @returns The formatted content.
*/
static inlineCode(content: string): `\`${string}\`` {
return `\`${content}\``;
}
/**
* Formats content into bold text.
* @param content The content to format.
* @returns The formatted content.
*/
static bold(content: string): `**${string}**` {
return `**${content}**`;
}
/**
* Formats content into italic text.
* @param content The content to format.
* @returns The formatted content.
*/
static italic(content: string): `*${string}*` {
return `*${content}*`;
}
/**
* Formats content into underlined text.
* @param content The content to format.
* @returns The formatted content.
*/
static underline(content: string): `__${string}__` {
return `__${content}__`;
}
/**
* Formats content into strikethrough text.
* @param content The content to format.
* @returns The formatted content.
*/
static strikeThrough(content: string): `~~${string}~~` {
return `~~${content}~~`;
}
/**
* Formats content into a hyperlink.
* @param content The content to format.
* @param url The URL to hyperlink to.
* @returns The formatted content.
*/
static hyperlink(content: string, url: string): `[${string}](${string})` {
return `[${content}](${url})`;
}
/**
* Formats content into a spoiler.
* @param content The content to format.
* @returns The formatted content.
*/
static spoiler(content: string): `||${string}||` {
return `||${content}||`;
}
/**
* Formats content into a quote.
* @param content The content to format.
* @returns The formatted content.
*/
static blockQuote(content: string): string {
return `>>> ${content}`;
}
/**
* Formats content into a quote.
* @param content The content to format.
* @returns The formatted content.
*/
static quote(content: string): string {
return `> ${content}`;
}
/**
* Formats a message link.
* @param guildId The ID of the guild.
* @param channelId The ID of the channel.
* @param messageId The ID of the message.
* @returns The formatted message link.
*/
static messageLink(guildId: string, channelId: string, messageId: string): MessageLink {
return `https://discord.com/channels/${guildId}/${channelId}/${messageId}`;
}
/**
* Formats a header.
* @param content The content of the header.
* @param level The level of the header. Defaults to 1.
* @returns The formatted header.
*/
static header(content: string, level: HeadingLevel = HeadingLevel.H1): string {
return `${'#'.repeat(level)} ${content}`;
}
/**
* Formats a list.
* @param items The items of the list.
* @param ordered Whether the list is ordered. Defaults to false.
* @returns The formatted list.
*/
static list(items: string[], ordered = false): string {
return items
.map((item, index) => {
return (ordered ? `${index + 1}. ` : '- ') + item;
})
.join('\n');
}
/**
* Formats the given timestamp into discord unix timestamp format.
* @param timestamp The timestamp to format.
* @param style The style of the timestamp. Defaults to 't'.
* @returns The formatted timestamp.
*/
static timestamp(timestamp: Date, style: TimestampStyle = TimestampStyle.RelativeTime): Timestamp {
return `<t:${Math.floor(timestamp.getTime() / 1000)}:${style}>`;
}
}

View File

@ -0,0 +1,86 @@
import type {
APIBan,
RESTGetAPIGuildBansQuery,
RESTPostAPIGuildBulkBanJSONBody,
RESTPutAPIGuildBanJSONBody,
} from 'discord-api-types/v10';
import { BaseShorter } from './base';
import { GuildBan } from '../../structures/GuildBan';
export class BanShorter extends BaseShorter {
/**
* Bulk creates bans in the guild.
* @param guildId The ID of the guild.
* @param body The request body for bulk banning members.
* @param reason The reason for bulk banning members.
*/
async bulkCreate(guildId: string, body: RESTPostAPIGuildBulkBanJSONBody, reason?: string) {
const bans = await this.client.proxy.guilds(guildId)['bulk-bans'].post({ reason, body });
for (const id of bans.banned_users) this.client.cache.members?.removeIfNI('GuildBans', id, guildId);
return bans;
}
/**
* Unbans a member from the guild.
* @param guildId The ID of the guild.
* @param memberId The ID of the member to unban.
* @param reason The reason for unbanning the member.
*/
async remove(guildId: string, memberId: string, reason?: string) {
await this.client.proxy.guilds(guildId).bans(memberId).delete({ reason });
}
/**
* Bans a member from the guild.
* @param guildId The ID of the guild.
* @param memberId The ID of the member to ban.
* @param body The request body for banning the member.
* @param reason The reason for banning the member.
*/
async create(guildId: string, memberId: string, body?: RESTPutAPIGuildBanJSONBody, reason?: string) {
await this.client.proxy.guilds(guildId).bans(memberId).put({ reason, body });
await this.client.cache.members?.removeIfNI('GuildBans', memberId, guildId);
}
/**
* Fetches a ban from the guild.
* @param guildId The ID of the guild.
* @param userId The ID of the user to fetch.
* @param force Whether to force fetching the ban from the API even if it exists in the cache.
* @returns A Promise that resolves to the fetched ban.
*/
async fetch(guildId: string, userId: string, force = false) {
let ban;
if (!force) {
ban = await this.client.cache.bans?.get(userId, guildId);
if (ban) return ban;
}
ban = await this.client.proxy.guilds(guildId).bans(userId).get();
await this.client.cache.members?.set(ban.user!.id, guildId, ban);
return new GuildBan(this.client, ban, guildId);
}
/**
* Lists bans in the guild based on the provided query.
* @param guildId The ID of the guild.
* @param query The query parameters for listing bans.
* @param force Whether to force listing bans from the API even if they exist in the cache.
* @returns A Promise that resolves to an array of listed bans.
*/
async list(guildId: string, query?: RESTGetAPIGuildBansQuery, force = false) {
let bans;
if (!force) {
bans = (await this.client.cache.bans?.values(guildId)) ?? [];
if (bans.length) return bans;
}
bans = await this.client.proxy.guilds(guildId).bans.get({
query,
});
await this.client.cache.bans?.set(
bans.map<[string, APIBan]>(x => [x.user!.id, x]),
guildId,
);
return bans.map(m => new GuildBan(this.client, m, guildId));
}
}

View File

@ -1,112 +1,112 @@
import { GatewayIntentBits } from 'discord-api-types/gateway/v10';
import {
BaseClient,
type BaseClientOptions,
type InternalRuntimeConfig,
type InternalRuntimeConfigHTTP,
type RuntimeConfig,
type RuntimeConfigHTTP,
} from './client/base';
import type { CustomEventsKeys, ClientNameEvents, EventContext } from './events';
import { isCloudfareWorker } from './common';
export { Logger, PermissionStrings, Watcher } from './common';
//
export { Collection, LimitedCollection } from './collection';
//
export * from './api';
export * from './builders';
export * from './cache';
export * from './commands';
export * from './components';
export * from './events';
export * from './langs';
//
export { ShardManager, WorkerManager } from './websocket/discord';
//
export * from './structures';
//
export * from './client';
//
export function throwError(msg: string): never {
throw new Error(msg);
}
/**
* Creates an event with the specified data and run function.
*
* @param data - The event data.
* @returns The created event.
*
* @example
* const myEvent = createEvent({
* data: { name: 'ready', once: true },
* run: (user, client, shard) => {
* client.logger.info(`Start ${user.username} on shard #${shard}`);
* }
* });
*/
export function createEvent<E extends ClientNameEvents | CustomEventsKeys>(data: {
data: { name: E; once?: boolean };
run: (...args: EventContext<{ data: { name: E } }>) => any;
}) {
data.data.once ??= false;
return data;
}
export const config = {
/**
* Configurations for the bot.
*
* @param data - The runtime configuration data for gateway connections.
* @returns The internal runtime configuration.
*/
bot(data: RuntimeConfig) {
return {
...data,
intents:
'intents' in data
? typeof data.intents === 'number'
? data.intents
: data.intents?.reduce<number>(
(pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]),
0,
) ?? 0
: 0,
} as InternalRuntimeConfig;
},
/**
* Configurations for the HTTP server.
*
* @param data - The runtime configuration data for http server.
* @returns The internal runtime configuration for HTTP.
*/
http(data: RuntimeConfigHTTP) {
const obj = {
port: 8080,
...data,
} as InternalRuntimeConfigHTTP;
if (isCloudfareWorker()) BaseClient._seyfertConfig = obj;
return obj;
},
};
/**
* Extends the context of a command interaction.
*
* @param cb - The callback function to extend the context.
* @returns The extended context.
*
* @example
* const customContext = extendContext((interaction) => {
* return {
* owner: '123456789012345678',
* // Add your custom properties here
* };
* });
*/
export function extendContext<T extends {}>(
cb: (interaction: Parameters<NonNullable<BaseClientOptions['context']>>[0]) => T,
) {
return cb;
}
import { GatewayIntentBits } from 'discord-api-types/gateway/v10';
import {
BaseClient,
type BaseClientOptions,
type InternalRuntimeConfig,
type InternalRuntimeConfigHTTP,
type RuntimeConfig,
type RuntimeConfigHTTP,
} from './client/base';
import type { CustomEventsKeys, ClientNameEvents, EventContext } from './events';
import { isCloudfareWorker } from './common';
export { Logger, PermissionStrings, Watcher, Formatter } from './common';
//
export { Collection, LimitedCollection } from './collection';
//
export * from './api';
export * from './builders';
export * from './cache';
export * from './commands';
export * from './components';
export * from './events';
export * from './langs';
//
export { ShardManager, WorkerManager } from './websocket/discord';
//
export * from './structures';
//
export * from './client';
//
export function throwError(msg: string): never {
throw new Error(msg);
}
/**
* Creates an event with the specified data and run function.
*
* @param data - The event data.
* @returns The created event.
*
* @example
* const myEvent = createEvent({
* data: { name: 'ready', once: true },
* run: (user, client, shard) => {
* client.logger.info(`Start ${user.username} on shard #${shard}`);
* }
* });
*/
export function createEvent<E extends ClientNameEvents | CustomEventsKeys>(data: {
data: { name: E; once?: boolean };
run: (...args: EventContext<{ data: { name: E } }>) => any;
}) {
data.data.once ??= false;
return data;
}
export const config = {
/**
* Configurations for the bot.
*
* @param data - The runtime configuration data for gateway connections.
* @returns The internal runtime configuration.
*/
bot(data: RuntimeConfig) {
return {
...data,
intents:
'intents' in data
? typeof data.intents === 'number'
? data.intents
: data.intents?.reduce<number>(
(pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]),
0,
) ?? 0
: 0,
} as InternalRuntimeConfig;
},
/**
* Configurations for the HTTP server.
*
* @param data - The runtime configuration data for http server.
* @returns The internal runtime configuration for HTTP.
*/
http(data: RuntimeConfigHTTP) {
const obj = {
port: 8080,
...data,
} as InternalRuntimeConfigHTTP;
if (isCloudfareWorker()) BaseClient._seyfertConfig = obj;
return obj;
},
};
/**
* Extends the context of a command interaction.
*
* @param cb - The callback function to extend the context.
* @returns The extended context.
*
* @example
* const customContext = extendContext((interaction) => {
* return {
* owner: '123456789012345678',
* // Add your custom properties here
* };
* });
*/
export function extendContext<T extends {}>(
cb: (interaction: Parameters<NonNullable<BaseClientOptions['context']>>[0]) => T,
) {
return cb;
}

View File

@ -10,6 +10,7 @@ import { Sticker } from './Sticker';
import { BaseChannel, WebhookGuildMethods } from './channels';
import { BaseGuild } from './extra/BaseGuild';
import type { DiscordBase } from './extra/DiscordBase';
import { GuildBan } from './GuildBan';
export interface Guild extends ObjectToLower<Omit<APIGuild, 'stickers' | 'emojis' | 'roles'>>, DiscordBase {}
export class Guild<State extends StructStates = 'api'> extends (BaseGuild as unknown as ToClass<
@ -75,6 +76,7 @@ export class Guild<State extends StructStates = 'api'> extends (BaseGuild as unk
roles = GuildRole.methods({ client: this.client, guildId: this.id });
channels = BaseChannel.allMethods({ client: this.client, guildId: this.id });
emojis = GuildEmoji.methods({ client: this.client, guildId: this.id });
bans = GuildBan.methods({ client: this.client, guildId: this.id });
}
/** Maximun custom guild emojis per level */

View File

@ -0,0 +1,49 @@
import type { APIBan, RESTGetAPIGuildBansQuery } from 'discord-api-types/v10';
import type { UsingClient } from '../commands';
import type { MethodContext, ObjectToLower } from '../common';
import { DiscordBase } from './extra/DiscordBase';
import type { BanShorter } from '../common/shorters/bans';
export interface GuildBan extends DiscordBase, ObjectToLower<Omit<APIBan, 'id'>> {}
export class GuildBan extends DiscordBase {
constructor(
client: UsingClient,
data: APIBan,
readonly guildId: string,
) {
super(client, { ...data, id: data.user.id });
}
create(body?: Parameters<BanShorter['create']>[2], reason?: string) {
return this.client.bans.create(this.guildId, this.id, body, reason);
}
remove(reason?: string) {
return this.client.bans.remove(this.guildId, this.id, reason);
}
guild(force = false) {
return this.client.guilds.fetch(this.guildId, force);
}
fetch(force = false) {
return this.client.bans.fetch(this.guildId, this.id, force);
}
toString() {
return `<@${this.id}>`;
}
static methods({ client, guildId }: MethodContext<{ guildId: string }>) {
return {
fetch: (userId: string, force = false) => client.bans.fetch(guildId, userId, force),
list: (query?: RESTGetAPIGuildBansQuery, force = false) => client.bans.list(guildId, query, force),
create: (memberId: string, body?: Parameters<BanShorter['create']>[2], reason?: string) =>
client.bans.create(guildId, memberId, body, reason),
remove: (memberId: string, reason?: string) => client.bans.remove(guildId, memberId, reason),
bulkCreate: (body: Parameters<BanShorter['bulkCreate']>[1], reason?: string) =>
client.bans.bulkCreate(guildId, body, reason),
};
}
}

View File

@ -41,7 +41,7 @@ import type { GuildMember } from './GuildMember';
import type { GuildRole } from './GuildRole';
import { DiscordBase } from './extra/DiscordBase';
import { channelLink } from './extra/functions';
import type { RawFile } from '..';
import { Collection, type RawFile } from '..';
export class BaseChannel<T extends ChannelType> extends DiscordBase<APIChannelBase<ChannelType>> {
declare type: T;
@ -216,9 +216,15 @@ export class MessagesMethods extends DiscordBase {
return this.client.channels.typing(this.id);
}
messages = MessagesMethods.messages({ client: this.client, channelId: this.id });
messages = MessagesMethods.messages({
client: this.client,
channelId: this.id,
});
pins = MessagesMethods.pins({ client: this.client, channelId: this.id });
reactions = MessagesMethods.reactions({ client: this.client, channelId: this.id });
reactions = MessagesMethods.reactions({
client: this.client,
channelId: this.id,
});
static messages(ctx: MethodContext<{ channelId: string }>) {
return {
@ -265,7 +271,10 @@ export class MessagesMethods extends DiscordBase {
embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined,
attachments:
'attachments' in body
? body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) ?? undefined
? body.attachments?.map((x, i) => ({
id: i,
...resolveAttachment(x),
})) ?? undefined
: (files?.map((x, id) => ({
id,
filename: x.name,
@ -378,10 +387,26 @@ export class VoiceChannelMethods extends DiscordBase {
const filter = states.filter(state => state.channelId === this.id);
return filter;
}
public async members(force?: boolean) {
const collection = new Collection<string, GuildMember>();
const states = await this.states();
for (const state of states) {
const member = await state.member(force);
collection.set(member.id, member);
}
return collection;
}
}
export class WebhookGuildMethods extends DiscordBase {
webhooks = WebhookGuildMethods.guild({ client: this.client, guildId: this.id });
webhooks = WebhookGuildMethods.guild({
client: this.client,
guildId: this.id,
});
static guild(ctx: MethodContext<{ guildId: string }>) {
return {
@ -391,7 +416,10 @@ export class WebhookGuildMethods extends DiscordBase {
}
export class WebhookChannelMethods extends DiscordBase {
webhooks = WebhookChannelMethods.channel({ client: this.client, channelId: this.id });
webhooks = WebhookChannelMethods.channel({
client: this.client,
channelId: this.id,
});
static channel(ctx: MethodContext<{ channelId: string }>) {
return {
@ -555,6 +583,7 @@ export type AllGuildChannels =
export type AllTextableChannels = TextGuildChannel | VoiceChannel | DMChannel | NewsChannel | ThreadChannel;
export type AllGuildTextableChannels = TextGuildChannel | VoiceChannel | NewsChannel | ThreadChannel;
export type AllGuildVoiceChannels = VoiceChannel | StageChannel;
export type AllChannels =
| BaseChannel<ChannelType>