mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-03 01:10:07 +00:00
fix: handle 429 rate limit
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordeno/transformer": "18.0.0-alpha.1",
|
||||
"@discordeno/utils": "18.0.0-alpha.1",
|
||||
"dotenv": "^16.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import TRANSFORMERS from '@discordeno/transformer'
|
||||
import type { BigString, Camelize, DiscordUser } from '@discordeno/types'
|
||||
import type { BigString, Camelize, CreateMessageOptions, DiscordCreateMessage, DiscordMessage, DiscordUser, GetMessagesOptions } from '@discordeno/types'
|
||||
import { delay } from '@discordeno/utils'
|
||||
|
||||
// TODO: make dynamic based on package.json file
|
||||
const version = '18.0.0-alpha.1'
|
||||
@@ -12,10 +13,39 @@ export function createRestManager (options: CreateRestManagerOptions): RestManag
|
||||
baseUrl: options.baseUrl ?? 'https://discord.com/api',
|
||||
|
||||
routes: {
|
||||
// Channel Endpoints
|
||||
channels: {
|
||||
message: (channelId, messageId) => {
|
||||
return `/channels/${channelId}/messages/${messageId}`
|
||||
},
|
||||
|
||||
messages: (channelId, options?: GetMessagesOptions) => {
|
||||
const url = `/channels/${channelId}/messages?`
|
||||
|
||||
if (options) {
|
||||
// if (isGetMessagesAfter(options) && options.after) {
|
||||
// url += `after=${options.after}`
|
||||
// }
|
||||
// if (isGetMessagesBefore(options) && options.before) {
|
||||
// url += `&before=${options.before}`
|
||||
// }
|
||||
// if (isGetMessagesAround(options) && options.around) {
|
||||
// url += `&around=${options.around}`
|
||||
// }
|
||||
// if (isGetMessagesLimit(options) && options.limit) {
|
||||
// url += `&limit=${options.limit}`
|
||||
// }
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
},
|
||||
|
||||
// User endpoints
|
||||
user (userId: BigString) {
|
||||
return `/users/${userId}`
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
createRequest (options) {
|
||||
@@ -45,6 +75,10 @@ export function createRestManager (options: CreateRestManagerOptions): RestManag
|
||||
options.body.reason = undefined
|
||||
}
|
||||
|
||||
if (options.body && !['GET', 'DELETE'].includes(options.method)) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
body: (options.body?.file ?? JSON.stringify(options.body)) as
|
||||
@@ -55,31 +89,72 @@ export function createRestManager (options: CreateRestManagerOptions): RestManag
|
||||
},
|
||||
|
||||
async sendRequest (options) {
|
||||
const response = await fetch(`${rest.baseUrl}/v${rest.version}/${options.url}`, rest.createRequest({ method: options.method, url: options.url }))
|
||||
const response = await fetch(
|
||||
`${rest.baseUrl}/v${rest.version}/${options.url}`,
|
||||
rest.createRequest({ method: options.method, url: options.url, body: options.body })
|
||||
)
|
||||
|
||||
if (response.status < 200 || response.status >= 400) {
|
||||
// If NOT rate limited remove from queue
|
||||
if (response.status === 429) {
|
||||
// TODO: RATELIMITED HANDLING
|
||||
return options.reject('RATELIMITED 429')
|
||||
} else {
|
||||
// INVALID REQUEST
|
||||
const body = JSON.stringify(await response.json())
|
||||
return options.reject({
|
||||
ok: false,
|
||||
status: response.status,
|
||||
body
|
||||
})
|
||||
// const json = await response.json()
|
||||
|
||||
// TOO MANY ATTEMPTS, GET RID OF REQUEST FROM QUEUE.
|
||||
// if (
|
||||
// options.retryCount !== undefined &&
|
||||
// options.retryCount++ >= rest.maxRetryCount
|
||||
// ) {
|
||||
// rest.debug(`[REST - RetriesMaxed] ${JSON.stringify(options)}`)
|
||||
// // REMOVE ITEM FROM QUEUE TO PREVENT RETRY
|
||||
// options.reject?.({
|
||||
// ok: false,
|
||||
// status: response.status,
|
||||
// error:
|
||||
// 'The options was rate limited and it maxed out the retries limit.'
|
||||
// })
|
||||
|
||||
// // @ts-expect-error Code should never reach here
|
||||
// return
|
||||
// }
|
||||
|
||||
// RATE LIMITED, ADD BACK TO QUEUE
|
||||
|
||||
// rest.invalidBucket.handleCompletedRequest(
|
||||
// response.status,
|
||||
// response.headers.get('X-RateLimit-Scope') === 'shared'
|
||||
// )
|
||||
|
||||
// console.log('rate limited', json.retry_after, response.headers)
|
||||
const resetAfter = response.headers.get('x-ratelimit-reset-after')
|
||||
if (resetAfter) await delay(Number(resetAfter) * 1000)
|
||||
options.retryCount++
|
||||
|
||||
return await options.retryRequest?.(options)
|
||||
}
|
||||
|
||||
// INVALID REQUEST
|
||||
const body = JSON.stringify(await response.json())
|
||||
return options.reject({
|
||||
ok: false,
|
||||
status: response.status,
|
||||
body
|
||||
})
|
||||
}
|
||||
|
||||
options.resolve(await response.json())
|
||||
},
|
||||
|
||||
async makeRequest (method, url) {
|
||||
async makeRequest (method, url, body) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
rest.sendRequest({
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
retryCount: 0,
|
||||
retryRequest: async function (options: SendRequestOptions) {
|
||||
// TODO: should change to reprocess queue item
|
||||
await rest.sendRequest(options)
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
})
|
||||
@@ -90,9 +165,51 @@ export function createRestManager (options: CreateRestManagerOptions): RestManag
|
||||
return await rest.makeRequest('GET', url)
|
||||
},
|
||||
|
||||
async post (url, body) {
|
||||
return await rest.makeRequest('POST', url, body)
|
||||
},
|
||||
|
||||
async getUser (id) {
|
||||
const result = await rest.get<DiscordUser>(rest.routes.user(id))
|
||||
return TRANSFORMERS.user(result)
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message to a channel.
|
||||
*
|
||||
* @param channelId - The ID of the channel to send the message in.
|
||||
* @param options - The parameters for the creation of the message.
|
||||
* @returns An instance of the created {@link DiscordMessage}.
|
||||
*
|
||||
* @remarks
|
||||
* Requires that the bot user be able to see the contents of the channel the message is to be sent in.
|
||||
*
|
||||
* If sending a message to a guild channel:
|
||||
* - Requires the `SEND_MESSAGES` permission.
|
||||
*
|
||||
* If sending a TTS message:
|
||||
* - Requires the `SEND_TTS_MESSAGES` permission.
|
||||
*
|
||||
* If sending a message as a reply to another message:
|
||||
* - Requires the `READ_MESSAGE_HISTORY` permission.
|
||||
* - The message being replied to cannot be a system message.
|
||||
*
|
||||
* ⚠️ The maximum size of a request (accounting for any attachments and message content) for bot users is _8 MiB_.
|
||||
*
|
||||
* Fires a _Message Create_ gateway event.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#create-message}
|
||||
*/
|
||||
async sendMessage (channelId: BigString, options: CreateMessageOptions) {
|
||||
const result = await rest.post<DiscordMessage>(
|
||||
rest.routes.channels.messages(channelId),
|
||||
{
|
||||
content: options.content
|
||||
// TODO: other options
|
||||
} as DiscordCreateMessage
|
||||
)
|
||||
|
||||
return TRANSFORMERS.message(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,25 +247,40 @@ export interface RestManager {
|
||||
routes: {
|
||||
/** A specific user route. */
|
||||
user: (id: BigString) => string
|
||||
/** Routes for channel related endpoints. */
|
||||
channels: {
|
||||
/** Route for a specific message */
|
||||
message: (channelId: BigString, id: BigString) => string
|
||||
/** Route for handling non-specific messages. */
|
||||
messages: (channelId: BigString, options?: GetMessagesOptions) => string
|
||||
}
|
||||
}
|
||||
/** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */
|
||||
createRequest: (options: CreateRequestBodyOptions) => RequestBody
|
||||
/** Sends a request to the api. */
|
||||
sendRequest: (options: SendRequestOptions) => Promise<void>
|
||||
/** Make a request to be sent to the api. */
|
||||
makeRequest: <T = unknown>(method: RequestMethods, url: string) => Promise<T>
|
||||
makeRequest: <T = unknown>(method: RequestMethods, url: string, body?: Record<string, any>) => Promise<T>
|
||||
/** Make a get request to the api */
|
||||
get: <T = unknown>(url: string) => Promise<T>
|
||||
/** Make a post request to the api. */
|
||||
post: <T = unknown>(url: string, body?: Record<string, any>) => Promise<T>
|
||||
/**
|
||||
* Get a user's data from the api
|
||||
*
|
||||
* @param id The user's id
|
||||
* @returns {DiscordUser}
|
||||
* @returns {Camelize<DiscordUser>}
|
||||
*/
|
||||
getUser: (id: BigString) => Promise<Camelize<DiscordUser>>
|
||||
/**
|
||||
* Send a message to a channel.
|
||||
*
|
||||
* @returns {Message}
|
||||
*/
|
||||
sendMessage: (channelId: BigString, options: CreateMessageOptions) => Promise<Camelize<DiscordMessage>>
|
||||
}
|
||||
|
||||
export type RequestMethods = 'GET'
|
||||
export type RequestMethods = 'GET' | 'POST'
|
||||
export type ApiVersions = 9 | 10
|
||||
|
||||
export interface CreateRequestBodyOptions {
|
||||
@@ -170,6 +302,12 @@ export interface SendRequestOptions {
|
||||
url: string
|
||||
/** The method to use when sending the request. */
|
||||
method: RequestMethods
|
||||
/** The body to be sent in the request. */
|
||||
body?: Record<string, any>
|
||||
/** The amount of times this request has been retried. */
|
||||
retryCount: number
|
||||
/** Handler to retry a request should it be rate limited. */
|
||||
retryRequest?: (options: SendRequestOptions) => Promise<void>
|
||||
/** Resolve handler when a request succeeds. */
|
||||
resolve: (value: any | PromiseLike<any>) => void
|
||||
/** Reject handler when a request fails. */
|
||||
|
||||
20
packages/rest/tests/e2e/message.spec.ts
Normal file
20
packages/rest/tests/e2e/message.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect } from 'chai'
|
||||
import { describe, it } from 'mocha'
|
||||
import { rest } from '../utils.js'
|
||||
|
||||
describe('[rest] Message related tests', () => {
|
||||
describe('Send a message', () => {
|
||||
it('With content', async () => {
|
||||
const message = await rest.sendMessage('1057524844712964146', { content: 'testing rate limit manager' })
|
||||
expect(message.content).to.be.equal('testing rate limit manager')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rate limit manager testing', () => {
|
||||
it('Send 10 messages to 1 channel', async () => {
|
||||
await Promise.all([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(async (i) => {
|
||||
await rest.sendMessage('1057524844712964146', { content: `testing rate limit manager ${i}` })
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { expect } from 'chai'
|
||||
import { describe, it } from 'mocha'
|
||||
import { rest } from '../utils.js'
|
||||
|
||||
describe('[rest] manager', () => {
|
||||
describe('[rest] User related tests', () => {
|
||||
describe('Get a user from the api', () => {
|
||||
it('With a valid user id', async () => {
|
||||
const user = await rest.getUser('130136895395987456')
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './message.js'
|
||||
export * from './user.js'
|
||||
|
||||
15
packages/transformer/src/casing/message.ts
Normal file
15
packages/transformer/src/casing/message.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Camelize, DiscordMessage } from '@discordeno/types'
|
||||
|
||||
export function c1amelize1Message (payload: DiscordMessage): Camelize<DiscordMessage> {
|
||||
return {
|
||||
id: payload.id,
|
||||
content: payload.content
|
||||
}
|
||||
}
|
||||
|
||||
export function s1nakelize1Message (payload: Camelize<DiscordMessage>): DiscordMessage {
|
||||
return {
|
||||
id: payload.id,
|
||||
content: payload.content
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { c1amelize1Message, s1nakelize1Message } from './casing/message.js'
|
||||
import { c1amelize1User, s1nakelize1User } from './casing/user.js'
|
||||
|
||||
export * from './casing/index.js'
|
||||
|
||||
export const TRANSFORMERS = {
|
||||
message: c1amelize1Message,
|
||||
user: c1amelize1User,
|
||||
|
||||
reverse: {
|
||||
message: s1nakelize1Message,
|
||||
user: s1nakelize1User
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
// WebhookTypes
|
||||
// } from './shared.js'
|
||||
|
||||
import type { UserFlags, PremiumTypes } from './shared'
|
||||
import type { PremiumTypes, UserFlags } from './shared'
|
||||
|
||||
/** https://discord.com/developers/docs/resources/user#user-object */
|
||||
export interface DiscordUser {
|
||||
@@ -1006,29 +1006,29 @@ export interface DiscordUser {
|
||||
// user: DiscordUser
|
||||
// }
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#message-object */
|
||||
// export interface DiscordMessage {
|
||||
// /** id of the message */
|
||||
// id: string
|
||||
// /** id of the channel the message was sent in */
|
||||
// channel_id: string
|
||||
// /**
|
||||
// * id of the guild the message was sent in
|
||||
// * Note: For MESSAGE_CREATE and MESSAGE_UPDATE events, the message object may not contain a guild_id or member field since the events are sent directly to the receiving user and the bot who sent the message, rather than being sent through the guild like non-ephemeral messages.
|
||||
// */
|
||||
// guild_id?: string
|
||||
// /**
|
||||
// * The author of this message (not guaranteed to be a valid user)
|
||||
// * Note: The author object follows the structure of the user object, but is only a valid user in the case where the message is generated by a user or bot user. If the message is generated by a webhook, the author object corresponds to the webhook's id, username, and avatar. You can tell if a message is generated by a webhook by checking for the webhook_id on the message object.
|
||||
// */
|
||||
// author: DiscordUser
|
||||
// /**
|
||||
// * Member properties for this message's author
|
||||
// * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory.
|
||||
// */
|
||||
// member?: DiscordMember
|
||||
// /** Contents of the message */
|
||||
// content?: string
|
||||
/** https://discord.com/developers/docs/resources/channel#message-object */
|
||||
export interface DiscordMessage {
|
||||
/** id of the message */
|
||||
id: string
|
||||
// /** id of the channel the message was sent in */
|
||||
// channel_id: string
|
||||
// /**
|
||||
// * id of the guild the message was sent in
|
||||
// * Note: For MESSAGE_CREATE and MESSAGE_UPDATE events, the message object may not contain a guild_id or member field since the events are sent directly to the receiving user and the bot who sent the message, rather than being sent through the guild like non-ephemeral messages.
|
||||
// */
|
||||
// guild_id?: string
|
||||
// /**
|
||||
// * The author of this message (not guaranteed to be a valid user)
|
||||
// * Note: The author object follows the structure of the user object, but is only a valid user in the case where the message is generated by a user or bot user. If the message is generated by a webhook, the author object corresponds to the webhook's id, username, and avatar. You can tell if a message is generated by a webhook by checking for the webhook_id on the message object.
|
||||
// */
|
||||
// author: DiscordUser
|
||||
// /**
|
||||
// * Member properties for this message's author
|
||||
// * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory.
|
||||
// */
|
||||
// member?: DiscordMember
|
||||
/** Contents of the message */
|
||||
content?: string
|
||||
// /** When this message was sent */
|
||||
// timestamp: string
|
||||
// /** When this message was edited (or null if never) */
|
||||
@@ -1093,7 +1093,7 @@ export interface DiscordUser {
|
||||
// sticker_items?: DiscordStickerItem[]
|
||||
// /** A generally increasing integer (there may be gaps or duplicates) that represents the approximate position of the message in a thread, it can be used to estimate the relative position of the message in a thread in company with `total_message_sent` on parent thread */
|
||||
// position?: number
|
||||
// }
|
||||
}
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#channel-mention-object */
|
||||
// export interface DiscordChannelMention {
|
||||
@@ -2765,36 +2765,36 @@ export interface DiscordUser {
|
||||
// components?: DiscordMessageComponents
|
||||
// }
|
||||
|
||||
// export interface DiscordCreateMessage {
|
||||
// /** The message contents (up to 2000 characters) */
|
||||
// content?: string
|
||||
// /** Can be used to verify a message was sent (up to 25 characters). Value will appear in the Message Create event. */
|
||||
// nonce?: string | number
|
||||
// /** true if this is a TTS message */
|
||||
// tts?: boolean
|
||||
// /** Embedded `rich` content (up to 6000 characters) */
|
||||
// embeds?: DiscordEmbed[]
|
||||
// /** Allowed mentions for the message */
|
||||
// allowed_mentions?: DiscordAllowedMentions
|
||||
// /** Include to make your message a reply */
|
||||
// message_reference?: {
|
||||
// /** id of the originating message */
|
||||
// message_id?: string
|
||||
// /**
|
||||
// * id of the originating message's channel
|
||||
// * Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model.
|
||||
// */
|
||||
// channel_id?: string
|
||||
// /** id of the originating message's guild */
|
||||
// guild_id?: string
|
||||
// /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */
|
||||
// fail_if_not_exists: boolean
|
||||
// }
|
||||
// /** The components you would like to have sent in this message */
|
||||
// components?: DiscordMessageComponents
|
||||
// /** IDs of up to 3 stickers in the server to send in the message */
|
||||
// stickerIds?: [string] | [string, string] | [string, string, string]
|
||||
// }
|
||||
export interface DiscordCreateMessage {
|
||||
/** The message contents (up to 2000 characters) */
|
||||
content?: string
|
||||
/** Can be used to verify a message was sent (up to 25 characters). Value will appear in the Message Create event. */
|
||||
nonce?: string | number
|
||||
/** true if this is a TTS message */
|
||||
tts?: boolean
|
||||
/** Embedded `rich` content (up to 6000 characters) */
|
||||
// embeds?: DiscordEmbed[]
|
||||
/** Allowed mentions for the message */
|
||||
// allowed_mentions?: DiscordAllowedMentions
|
||||
/** Include to make your message a reply */
|
||||
message_reference?: {
|
||||
/** id of the originating message */
|
||||
message_id?: string
|
||||
/**
|
||||
* id of the originating message's channel
|
||||
* Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model.
|
||||
*/
|
||||
channel_id?: string
|
||||
/** id of the originating message's guild */
|
||||
guild_id?: string
|
||||
/** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */
|
||||
fail_if_not_exists: boolean
|
||||
}
|
||||
/** The components you would like to have sent in this message */
|
||||
// components?: DiscordMessageComponents
|
||||
/** IDs of up to 3 stickers in the server to send in the message */
|
||||
stickerIds?: [string] | [string, string] | [string, string, string]
|
||||
}
|
||||
|
||||
// export interface DiscordCreateScheduledEvent {
|
||||
// /** the channel id of the scheduled event. */
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
export {}
|
||||
import type { BigString } from "./shared"
|
||||
|
||||
export interface CreateMessageOptions {
|
||||
/** The message contents (up to 2000 characters) */
|
||||
content?: string
|
||||
// /** Can be used to verify a message was sent (up to 25 characters). Value will appear in the Message Create event. */
|
||||
// nonce?: string | number
|
||||
// /** true if this is a TTS message */
|
||||
// tts?: boolean
|
||||
/** Embedded `rich` content (up to 6000 characters) */
|
||||
// embeds?: Embed[]
|
||||
/** Allowed mentions for the message */
|
||||
// allowedMentions?: AllowedMentions
|
||||
/** Include to make your message a reply */
|
||||
// messageReference?: {
|
||||
// /** id of the originating message */
|
||||
// messageId?: BigString
|
||||
// /**
|
||||
// * id of the originating message's channel
|
||||
// * Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model.
|
||||
// */
|
||||
// channelId?: BigString
|
||||
// /** id of the originating message's guild */
|
||||
// guildId?: BigString
|
||||
// /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */
|
||||
// failIfNotExists: boolean
|
||||
// }
|
||||
/** The contents of the file being sent */
|
||||
// file?: FileContent | FileContent[]
|
||||
// /** The components you would like to have sent in this message */
|
||||
// components?: MessageComponents
|
||||
// /** IDs of up to 3 stickers in the server to send in the message */
|
||||
// stickerIds?:
|
||||
// | [BigString]
|
||||
// | [BigString, BigString]
|
||||
// | [BigString, BigString, BigString]
|
||||
}
|
||||
// import type {
|
||||
// AllowedMentionsTypes,
|
||||
// ApplicationCommandTypes,
|
||||
@@ -244,35 +280,35 @@ export {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
// export interface GetMessagesLimit {
|
||||
// /** Max number of messages to return (1-100) default 50 */
|
||||
// limit?: number
|
||||
// }
|
||||
/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
export interface GetMessagesLimit {
|
||||
/** Max number of messages to return (1-100) default 50 */
|
||||
limit?: number
|
||||
}
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
// export interface GetMessagesAround extends GetMessagesLimit {
|
||||
// /** Get messages around this message id */
|
||||
// around?: BigString
|
||||
// }
|
||||
/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
export interface GetMessagesAround extends GetMessagesLimit {
|
||||
/** Get messages around this message id */
|
||||
around?: BigString
|
||||
}
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
// export interface GetMessagesBefore extends GetMessagesLimit {
|
||||
// /** Get messages before this message id */
|
||||
// before?: BigString
|
||||
// }
|
||||
/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
export interface GetMessagesBefore extends GetMessagesLimit {
|
||||
/** Get messages before this message id */
|
||||
before?: BigString
|
||||
}
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
// export interface GetMessagesAfter extends GetMessagesLimit {
|
||||
// /** Get messages after this message id */
|
||||
// after?: BigString
|
||||
// }
|
||||
/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */
|
||||
export interface GetMessagesAfter extends GetMessagesLimit {
|
||||
/** Get messages after this message id */
|
||||
after?: BigString
|
||||
}
|
||||
|
||||
// export type GetMessagesOptions =
|
||||
// | GetMessagesAfter
|
||||
// | GetMessagesBefore
|
||||
// | GetMessagesAround
|
||||
// | GetMessagesLimit
|
||||
export type GetMessagesOptions =
|
||||
| GetMessagesAfter
|
||||
| GetMessagesBefore
|
||||
| GetMessagesAround
|
||||
| GetMessagesLimit
|
||||
|
||||
// /** https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params */
|
||||
// export interface GetReactions {
|
||||
|
||||
7
packages/utils/.c8rc.json
Normal file
7
packages/utils/.c8rc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"all": true,
|
||||
"src": "src",
|
||||
"reporter": ["text", "lcov"],
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["tests"]
|
||||
}
|
||||
10
packages/utils/.mocharc.json
Normal file
10
packages/utils/.mocharc.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"require": "ts-node/register",
|
||||
"loader": "ts-node/esm",
|
||||
"recursive": true,
|
||||
"timeout": 2000,
|
||||
"watch-extensions": "ts",
|
||||
"watch-files": ["src", "tests"],
|
||||
"enable-source-maps": true,
|
||||
"parallel": false
|
||||
}
|
||||
31
packages/utils/.swcrc
Normal file
31
packages/utils/.swcrc
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"minify": true,
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2022",
|
||||
"keepClassNames": true,
|
||||
"loose": true,
|
||||
"minify": {
|
||||
"compress": {
|
||||
"unused": true
|
||||
},
|
||||
"mangle": true
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": false,
|
||||
"strictMode": true,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
},
|
||||
"sourceMaps": "inline"
|
||||
}
|
||||
158
packages/utils/README.md
Normal file
158
packages/utils/README.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Discordeno
|
||||
|
||||
<img align="right" src="https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png" height="150px">
|
||||
|
||||
Discord API library for [Deno](https://deno.land)
|
||||
|
||||
Discordeno follows [semantic versioning](https://semver.org/)
|
||||
|
||||
[](https://discord.com/invite/5vBgXk3UcZ)
|
||||

|
||||
[](https://codecov.io/gh/discordeno/discordeno)
|
||||
|
||||
## Features
|
||||
|
||||
Discordeno is actively maintained to guarantee **excellent performance and ease.**
|
||||
|
||||
- **Simple, Efficient, and Lightweight**: Discordeno is lightweight, simple to use, and adaptable. By default, no
|
||||
caching.
|
||||
- **Functional API**: The functional API eliminates the challenges of extending built-in classes and inheritance while
|
||||
ensuring overall simple but performant code.
|
||||
- **Cross Runtime**: Supports the Node.js and Deno runtimes.
|
||||
- **Standalone components**: Discordeno offers the option to have practically any component of a bot as a separate
|
||||
piece, including standalone REST, gateways, custom caches, and more.
|
||||
- **Plugins:** Designed to allow you to overwrite any portion of the code with your own code. Never go through the
|
||||
hassle of maintaining your fork in order to acquire something that is specifically tailored to your requirements.
|
||||
Plugins may be used for nearly anything; for instance, we have a few authorised plugins.
|
||||
|
||||
- A caching plugin that makes anything cacheable.
|
||||
- A plugin for sweepers that allows them to periodically clear the cache.
|
||||
- The permission plugin internally verifies any missing permissions before sending a call to the Discord API to
|
||||
prevent the client from receiving a Discord global ban.
|
||||
|
||||
- **Flexibility:** You may easily delete an object's attributes if your bot doesn't require them. For instance, you
|
||||
shouldn't be required to keep `Channel.topic` if your bot doesn't require it. You may save GBs of RAM in this way. A
|
||||
few lines of code are all that are needed to accomplish this for any property on any object.
|
||||
|
||||
### REST
|
||||
|
||||
- Freedom from 1 hour downtimes due to invalid requests
|
||||
- By lowering the maximum downtime to 10 minutes, Discordeno will prevent your bot from being down for an hour.
|
||||
- Freedom from global rate limit errors
|
||||
- As your bot grows, you want to handle global rate limits better. Shards don't communicate fast enough to truly
|
||||
handle it properly so this allows 1 rest handler across the entire bot.
|
||||
- You may really run numerous instances of your bot on different hosts, all of which will connect to the same REST
|
||||
server.
|
||||
- REST does not rest!
|
||||
- Separate rest guarantees that your queued requests will continue to be processed even if your bot breaks for
|
||||
whatever reason.
|
||||
- Seamless updates! There's a chance you'll lose a lot of messages or replies that are waiting to be given when you
|
||||
wish to update and restart the bot. You may restart your bot using this technique and never have to worry about
|
||||
losing any answers.
|
||||
- Single source of contact to Discord API
|
||||
- As a result, you will be able to send requests to Discord from any location, even a bot dashboard. You are no longer
|
||||
need to interact with your bot processes in order to submit a request or do anything else. Your bot process should
|
||||
be freed up to handle bot events.
|
||||
- Scalability! Scalability! Scalability!
|
||||
|
||||
### Gateway
|
||||
|
||||
- **Zero Downtime Updates:**
|
||||
- A few seconds are needed to update your bot. When using conventional sharding, you must restart in addition to going
|
||||
through a 1/5s rate-limited process of identifying all of your shards. As WS processing has been relocated to a
|
||||
proxy process, you may resume the bot code right away without worrying about any delays. Normally, if you had a bot
|
||||
that was spread across 200,000 servers, restarting it after making a simple modification would take 20 minutes.
|
||||
- **Zero Downtime Resharding:**
|
||||
- At various periods in time, Discord stops allowing your bot to be added to new servers. Consider 150 shards
|
||||
operating on 150,000 servers, for instance. Your shards may support a maximum of 150 \* 2500 = 375,000 servers. Your
|
||||
bot will be unable to join new servers once it reaches this point until it re-shards.
|
||||
- DD proxy provides 2 types of re-sharding. Automated and manual. You can also have both.
|
||||
- Automated: This system will automatically begin a Zero-downtime resharding process behind the scenes when you
|
||||
reach 80% of your maximum servers allowed by your shards. For example, since 375,000 was the max, at 300,000 we
|
||||
would begin re-sharding behind the scenes with ZERO DOWNTIME.
|
||||
- 80% of maximum servers reached (The % of 80% is customizable.)
|
||||
- Identify limits have room to allow re-sharding. (Also customizable)
|
||||
- Manual: You can also trigger this manually should you choose.
|
||||
- **Horizontal Scaling:**
|
||||
- The bot may be scaled horizontally thanks to the proxy mechanism. When your business grows significantly, you have
|
||||
two options: you can either keep investing money to upgrade your server or you may expand horizontally by purchasing
|
||||
numerous more affordable servers. The proxy enables WS handling on a totally other system.
|
||||
- **No Loss Restarts:**
|
||||
- Without the proxy mechanism, you would typically lose numerous events while restarting a bot. Users could issue
|
||||
instructions or send messages that are not screened. As your bot population increases, this amount grows sharply.
|
||||
Users who don't receive the automatic roles or any other activities your bot should do may join. You may keep
|
||||
restarting your bot thanks to the proxy technology without ever losing any events. While your bot is unavailable,
|
||||
events will be added to a queue (the maximum size of the queue is configurable), and once the bot is back online,
|
||||
the queue will start processing all of the events.
|
||||
- **Controllers:**
|
||||
- You have complete control over everything inside the proxy thanks to the controller aspect. To simply override the
|
||||
handler, you may supply a function. For instance, you may simply give a method to override a specific function if
|
||||
you want it to behave differently rather than forking and maintaining your fork.
|
||||
- **Clustering With Workers:**
|
||||
- Utilize all of your CPU cores to their greatest potential by distributing the workload across employees. To enhance
|
||||
efficiency, manage how many employees and shards there are each worker!
|
||||
|
||||
### Custom Cache
|
||||
|
||||
Have your cache setup in any way you like. Redis, PGSQL or any cache layer you would like.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Minimal Example
|
||||
|
||||
Here is a minimal example to get started with:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createBot,
|
||||
Intents,
|
||||
startBot,
|
||||
} from "https://deno.land/x/discordeno@13.0.0/mod.ts";
|
||||
|
||||
const bot = createBot({
|
||||
token: process.env.DISCORD_TOKEN,
|
||||
intents: Intents.Guilds | Intents.GuildMessages,
|
||||
events: {
|
||||
ready() {
|
||||
console.log("Successfully connected to gateway");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Another way to do events
|
||||
bot.events.messageCreate = function (b, message) {
|
||||
// Process the message here with your command handler.
|
||||
};
|
||||
|
||||
await startBot(bot);
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
This library is not intended for beginners, however if you still want to utilise it, check out these excellent official
|
||||
and unofficial templates:
|
||||
|
||||
**Templates**
|
||||
|
||||
- [Discordeno Template (official)](https://github.com/discordeno/discordeno/tree/main/template)
|
||||
- [Serverless Slash Commands Template (official)](https://github.com/discordeno/serverless-deno-deploy-template)
|
||||
- [`create-discordeno-bot` (WIP, unoffical)](https://github.com/Reboot-Codes/create-discordeno-bot/)
|
||||
- [Add Your Own!](https://github.com/discordeno/discordeno/pulls)
|
||||
|
||||
**Frameworks**
|
||||
|
||||
- [Amethyst Framework](https://github.com/AmethystFramework/framework)
|
||||
- [Add Your Own!](https://github.com/discordeno/discordeno/pulls)
|
||||
|
||||
**Plugins**
|
||||
|
||||
- [Cache Plugin](plugins/cache)
|
||||
- [Fileloader Plugin](plugins/fileloader)
|
||||
- [Helpers Plugin](plugins/helpers)
|
||||
- [Permissions Plugin](plugins/permissions)
|
||||
|
||||
## Links
|
||||
|
||||
- [Website](https://discordeno.mod.land)
|
||||
- [Documentation](https://doc.deno.land/https/deno.land/x/discordeno/mod.ts)
|
||||
- [Discord](https://discord.com/invite/5vBgXk3UcZ)
|
||||
46
packages/utils/package.json
Normal file
46
packages/utils/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@discordeno/utils",
|
||||
"version": "18.0.0-alpha.1",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/discordeno/discordeno.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "swc --delete-dir-on-start src --out-dir dist",
|
||||
"build:type": "tsc --skipDefaultLibCheck --declaration --emitDeclarationOnly --declarationDir dist",
|
||||
"release-build": "yarn build && yarn build:type",
|
||||
"fmt": "eslint --fix \"src/**/*.ts*\"",
|
||||
"lint": "eslint \"src/**/*.ts*\"",
|
||||
"test:unit-coverage": "c8 mocha --no-warnings 'tests/**/*.spec.ts'",
|
||||
"test:unit": "c8 --r lcov mocha --no-warnings 'tests/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js utils",
|
||||
"test:deno-unit": "swc tests --delete-dir-on-start --out-dir denoTestsDist && node ../../scripts/fixDenoTestExtension.js && deno test -A --import-map ../../denoImportMap.json denoTestsDist",
|
||||
"test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/**/*.spec.ts'",
|
||||
"test:type": "tsc --noEmit",
|
||||
"test:test-type": "tsc --project tsconfig.test.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordeno/types": "18.0.0-alpha.1",
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.3.21",
|
||||
"@types/chai": "^4",
|
||||
"@types/mocha": "^10",
|
||||
"@types/node": "^18.11.15",
|
||||
"@types/sinon": "^10.0.13",
|
||||
"c8": "^7.12.0",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-discordeno": "*",
|
||||
"mocha": "^10.1.0",
|
||||
"sinon": "^15.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig": "*",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
}
|
||||
1
packages/utils/src/index.ts
Normal file
1
packages/utils/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './utils.js'
|
||||
36
packages/utils/src/utils.ts
Normal file
36
packages/utils/src/utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// import type { ImageFormat, ImageSize } from '@discordeno/types'
|
||||
|
||||
/** Pause the execution for a given amount of milliseconds. */
|
||||
export async function delay (ms: number): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return new Promise(
|
||||
(resolve): NodeJS.Timeout =>
|
||||
setTimeout((): void => {
|
||||
resolve()
|
||||
}, ms)
|
||||
)
|
||||
}
|
||||
|
||||
// /** Help format an image url. */
|
||||
// export function formatImageURL (
|
||||
// url: string,
|
||||
// size: ImageSize = 128,
|
||||
// format?: ImageFormat
|
||||
// ): string {
|
||||
// return `${url}.${
|
||||
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
// format ?? (url.includes('/a_') ? 'gif' : 'jpg')
|
||||
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
// }?size=${size}`
|
||||
// }
|
||||
|
||||
// // Typescript is not so good as we developers so we need this little utility function to help it out
|
||||
// // Taken from https://fettblog.eu/typescript-hasownproperty/
|
||||
// /** TS save way to check if a property exists in an object */
|
||||
// export function hasProperty<T extends {}, Y extends PropertyKey = string> (
|
||||
// obj: T,
|
||||
// prop: Y
|
||||
// ): obj is T & Record<Y, unknown> {
|
||||
// // eslint-disable-next-line no-prototype-builtins
|
||||
// return obj.hasOwnProperty(prop)
|
||||
// }
|
||||
54
packages/utils/tests/base64.spec.ts
Normal file
54
packages/utils/tests/base64.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { expect } from 'chai'
|
||||
import { describe, it } from 'mocha'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { decode, encode } from '../src/base64.js'
|
||||
|
||||
describe('base64.ts', () => {
|
||||
describe('encode', () => {
|
||||
it('can encode string to base64', () => {
|
||||
expect(encode('Man Ё𤭢')).to.be.equal('TWFuINCB8KStog==')
|
||||
})
|
||||
it('can encode Uint8Array to base64', () => {
|
||||
expect(
|
||||
encode(new Uint8Array([77, 97, 110, 32, 208, 129, 240, 164, 173, 162]))
|
||||
).to.be.equal('TWFuINCB8KStog==')
|
||||
})
|
||||
it('can encode Buffer to base64', () => {
|
||||
expect(
|
||||
encode(Buffer.from([77, 97, 110, 32, 208, 129, 240, 164, 173, 162]))
|
||||
).to.be.equal('TWFuINCB8KStog==')
|
||||
})
|
||||
})
|
||||
|
||||
describe('decode', () => {
|
||||
it('can dencode string to Uint8Array', () => {
|
||||
expect(new TextDecoder().decode(decode('TWFuINCB8KStog=='))).to.be.equal(
|
||||
'Man Ё𤭢'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/** Old test */
|
||||
it('[utils] encode some bytes to base64', () => {
|
||||
expect(
|
||||
encode(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
|
||||
).to.be.deep.equal('AQIDBAUGBwgJCg==')
|
||||
})
|
||||
|
||||
it('[utils] decode some base64 to bytes', () => {
|
||||
expect(decode('AQIDBAUGBwgJCg==')).to.be.deep.equal(
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
)
|
||||
})
|
||||
|
||||
it('[utils] encode/decode base64 roundtrip should work', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const bytes: number[] = []
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
bytes.push(Math.floor(Math.random() * 256))
|
||||
}
|
||||
const data = new Uint8Array(bytes)
|
||||
expect(decode(encode(data))).to.be.deep.equal(data)
|
||||
}
|
||||
})
|
||||
21
packages/utils/tests/bigint.spec.ts
Normal file
21
packages/utils/tests/bigint.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { expect } from 'chai'
|
||||
import { it } from 'mocha'
|
||||
import { bigintToSnowflake, snowflakeToBigint } from '../src/bigint.js'
|
||||
|
||||
it('[bigint] - Transform a snowflake string to bigint', () => {
|
||||
const text = '130136895395987456'
|
||||
const big = 130136895395987456n
|
||||
const result = snowflakeToBigint(text)
|
||||
|
||||
expect(big).to.be.equal(result)
|
||||
expect(text).to.be.not.equal(result)
|
||||
})
|
||||
|
||||
it('[bigint] - Transform a bigint to a string', () => {
|
||||
const text = '130136895395987456'
|
||||
const big = 130136895395987456n
|
||||
const result = bigintToSnowflake(big)
|
||||
|
||||
expect(text).to.be.equal(result)
|
||||
expect(big).to.be.not.equal(result)
|
||||
})
|
||||
146
packages/utils/tests/collection.spec.ts
Normal file
146
packages/utils/tests/collection.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { expect } from 'chai'
|
||||
import { beforeEach, describe, it } from 'mocha'
|
||||
import sinon from 'sinon'
|
||||
import { Collection } from '../src/collection.js'
|
||||
|
||||
describe('[collection]', () => {
|
||||
let collection: Collection<any, any>
|
||||
|
||||
beforeEach(() => {
|
||||
collection = new Collection()
|
||||
})
|
||||
|
||||
it('[collection] collection values to array', () => {
|
||||
const testCollection = new Collection([
|
||||
['best', 'tri'],
|
||||
['proficient', 'yui']
|
||||
])
|
||||
expect(testCollection.array()).to.be.deep.equal(['tri', 'yui'])
|
||||
})
|
||||
|
||||
it('[collection] get a random value', () => {
|
||||
const testCollection = new Collection([['best', 'tri']])
|
||||
|
||||
expect(testCollection.random() ?? '').to.be.oneOf(['best', 'tri'])
|
||||
expect(collection.random()).to.be.undefined
|
||||
})
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
collection.set('best developer', 'triformine')
|
||||
})
|
||||
it('[collection] Set a value without maxSize', () => {
|
||||
expect(collection.size).to.be.equal(1)
|
||||
expect(collection.get('best developer')).to.be.equal('triformine')
|
||||
})
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
collection.set('deno', 'yes')
|
||||
})
|
||||
it('[collection] get the value of the first element', () => {
|
||||
expect(collection.first()).to.be.equal('triformine')
|
||||
})
|
||||
|
||||
it('[collection] get the value of the last element', () => {
|
||||
expect(collection.last()).to.be.equal('yes')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('[collection] Create a collection with maxSize', () => {
|
||||
const maxSize = 2
|
||||
|
||||
const maxCollection = new Collection([], {
|
||||
maxSize
|
||||
})
|
||||
|
||||
expect(maxCollection).to.exist
|
||||
expect(maxCollection.maxSize).to.exist
|
||||
expect(maxCollection.maxSize).to.be.equal(maxSize)
|
||||
|
||||
describe('[collection] Test if maxSize works properly', () => {
|
||||
maxCollection.set('foo', 'bar')
|
||||
maxCollection.set('me', 'you')
|
||||
|
||||
expect(maxCollection.size).to.be.equal(2)
|
||||
|
||||
maxCollection.set('this', 'not')
|
||||
|
||||
expect(maxCollection.size).to.be.equal(2)
|
||||
|
||||
it('[collection] Test if forceSet ignore maxSize', () => {
|
||||
maxCollection.forceSet('this', 'not')
|
||||
|
||||
expect(maxCollection.size).to.be.equal(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const testCollection = new Collection([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
['c', 3]
|
||||
])
|
||||
|
||||
it('[collection] find by key or value', () => {
|
||||
expect(testCollection.find((v, k) => v === 2)).to.be.equal(2)
|
||||
expect(testCollection.find((v, k) => k === 'b')).to.be.equal(2)
|
||||
})
|
||||
|
||||
it('[collection] filter by key or value', () => {
|
||||
expect(testCollection.filter((v, k) => v === 3).size).to.be.equal(1)
|
||||
expect(testCollection.filter((v, k) => k === 'd').size).to.be.equal(0)
|
||||
})
|
||||
|
||||
it('[collection] map', () => {
|
||||
expect(testCollection.map((k, v) => `${v}${k}`)).to.be.deep.equal([
|
||||
'a1',
|
||||
'b2',
|
||||
'c3'
|
||||
])
|
||||
})
|
||||
|
||||
it('[collection] some', () => {
|
||||
expect(testCollection.some((v, _) => v === 1)).to.be.equal(true)
|
||||
expect(testCollection.some((v, _) => v === 4)).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('[collection] every', () => {
|
||||
expect(testCollection.every((v, _) => v !== 0)).to.be.equal(true)
|
||||
expect(testCollection.every((v, _) => v === 1)).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('[collection] reduce', () => {
|
||||
expect(testCollection.reduce((acc, val) => acc + val, 0)).to.be.equal(6)
|
||||
})
|
||||
|
||||
it('[collection] start sweeper', async () => {
|
||||
const clock = sinon.useFakeTimers()
|
||||
const sweeperCollection = new Collection(
|
||||
[
|
||||
['a', 1],
|
||||
['b', 2]
|
||||
],
|
||||
{
|
||||
sweeper: {
|
||||
filter: (v, _) => v === 1,
|
||||
interval: 50
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
await clock.tickAsync(49)
|
||||
expect(sweeperCollection.size).to.be.equal(2)
|
||||
await clock.tickAsync(1)
|
||||
expect(sweeperCollection.size).to.be.equal(1)
|
||||
} catch (err) {
|
||||
sweeperCollection.stopSweeper()
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
sweeperCollection.stopSweeper()
|
||||
clock.restore()
|
||||
})
|
||||
})
|
||||
25
packages/utils/tests/hash.spec.ts
Normal file
25
packages/utils/tests/hash.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { expect } from 'chai'
|
||||
import { it } from 'mocha'
|
||||
import { iconBigintToHash, iconHashToBigInt } from '../src/hash.js'
|
||||
|
||||
const iconHash = '4bbb271a13f7195031adcc06a2d867ce'
|
||||
const iconBigInt = 3843769888406823508519992434416504301518n
|
||||
const a_iconHash = 'a_4bbb271a13f7195031adcc06a2d867ce'
|
||||
const a_iconBigInt = 3503487521485885045056617826984736090062n
|
||||
|
||||
it('[utils] icon hash to bigint', () => {
|
||||
expect(iconHashToBigInt(iconHash)).to.be.equal(iconBigInt)
|
||||
})
|
||||
|
||||
it('[utils] icon bigint to hash', () => {
|
||||
expect(iconBigintToHash(iconBigInt)).to.be.equal(iconHash)
|
||||
})
|
||||
|
||||
it('[utils] icon hash to bigint a_ (animated)', () => {
|
||||
expect(iconHashToBigInt(a_iconHash)).to.be.equal(a_iconBigInt)
|
||||
})
|
||||
|
||||
it('[utils] icon bigint to hash a_ (animated)', () => {
|
||||
expect(iconBigintToHash(a_iconBigInt)).to.be.equal(a_iconHash)
|
||||
})
|
||||
15
packages/utils/tests/token.spec.ts
Normal file
15
packages/utils/tests/token.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { expect } from 'chai'
|
||||
import { it } from 'mocha'
|
||||
import { removeTokenPrefix } from '../src/token.js'
|
||||
|
||||
it('[token] Remove token prefix when Bot is prefixed.', () => {
|
||||
expect(removeTokenPrefix('Bot discordeno is best lib')).to.be.equal(
|
||||
'discordeno is best lib'
|
||||
)
|
||||
})
|
||||
|
||||
it('[token] Remove token prefix when Bot is NOT prefixed.', () => {
|
||||
expect(removeTokenPrefix('discordeno is best lib')).to.be.equal(
|
||||
'discordeno is best lib'
|
||||
)
|
||||
})
|
||||
54
packages/utils/tests/utils.spec.ts
Normal file
54
packages/utils/tests/utils.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { expect } from 'chai'
|
||||
import { afterEach, beforeEach, describe, it } from 'mocha'
|
||||
import sinon from 'sinon'
|
||||
import { delay, formatImageURL, hasProperty } from '../src/utils.js'
|
||||
|
||||
describe('utils.ts', () => {
|
||||
let clock: sinon.SinonFakeTimers
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('will', async () => {
|
||||
let delayEnded = false
|
||||
delay(31).then(() => {
|
||||
delayEnded = true
|
||||
})
|
||||
expect(delayEnded).to.be.false
|
||||
await clock.tickAsync(30)
|
||||
expect(delayEnded).to.be.false
|
||||
await clock.tickAsync(31)
|
||||
expect(delayEnded).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
const obj = { prop: 'lts372005' }
|
||||
|
||||
it('[utils] hasProperty does HAVE property', () => {
|
||||
expect(hasProperty(obj, 'prop')).to.be.equal(true)
|
||||
})
|
||||
|
||||
it('[utils] hasProperty does NOT HAVE property', () => {
|
||||
expect(hasProperty(obj, 'lts372005')).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('[utils] format image url', () => {
|
||||
expect(formatImageURL('https://skillz.is.pro')).to.be.equal(
|
||||
'https://skillz.is.pro.jpg?size=128'
|
||||
)
|
||||
expect(formatImageURL('https://skillz.is.pro', 1024)).to.be.equal(
|
||||
'https://skillz.is.pro.jpg?size=1024'
|
||||
)
|
||||
expect(formatImageURL('https://skillz.is.pro', 1024, 'gif')).to.be.equal(
|
||||
'https://skillz.is.pro.gif?size=1024'
|
||||
)
|
||||
expect(formatImageURL('https://skillz.is.pro', undefined, 'gif')).to.be.equal(
|
||||
'https://skillz.is.pro.gif?size=128'
|
||||
)
|
||||
})
|
||||
27
packages/utils/tests/validateLength.spec.ts
Normal file
27
packages/utils/tests/validateLength.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect } from 'chai'
|
||||
import { it } from 'mocha'
|
||||
import { validateLength } from '../src/validateLength.js'
|
||||
|
||||
it('[utils] Validate length is too low', () => {
|
||||
expect(validateLength('test', { min: 5 })).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('[utils] Validate length is too high', () => {
|
||||
expect(validateLength('test', { max: 3 })).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('[utils] Validate length is NOT just right in between.', () => {
|
||||
expect(validateLength('test', { min: 5, max: 3 })).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('[utils] Validate length is NOT too low', () => {
|
||||
expect(validateLength('test', { min: 3 })).to.be.equal(true)
|
||||
})
|
||||
|
||||
it('[utils] Validate length is NOT too high', () => {
|
||||
expect(validateLength('test', { max: 5 })).to.be.equal(true)
|
||||
})
|
||||
|
||||
it('[utils] Validate length is just right in between.', () => {
|
||||
expect(validateLength('test', { min: 3, max: 6 })).to.be.equal(true)
|
||||
})
|
||||
42
packages/utils/tests/verifySignature.spec.ts
Normal file
42
packages/utils/tests/verifySignature.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect } from 'chai'
|
||||
import { afterEach, beforeEach, describe, it } from 'mocha'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import Sinon from 'sinon'
|
||||
import nacl from 'tweetnacl'
|
||||
import { verifySignature } from '../src/verifySignature.js'
|
||||
|
||||
describe('VerifySignature', () => {
|
||||
let publicKey: Uint8Array
|
||||
let secretKey: Uint8Array
|
||||
let clock: Sinon.SinonFakeTimers
|
||||
|
||||
beforeEach(() => {
|
||||
clock = Sinon.useFakeTimers();
|
||||
|
||||
({ publicKey, secretKey } = nacl.sign.keyPair())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Sinon.restore()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('reutrn true if signature is verified', () => {
|
||||
const timestamp = Date.now().toString()
|
||||
const body = 'test body'
|
||||
const signature = nacl.sign.detached(
|
||||
Buffer.from(timestamp + body),
|
||||
secretKey
|
||||
)
|
||||
|
||||
const verifiedSignature = verifySignature({
|
||||
publicKey: Buffer.from(publicKey).toString('hex'),
|
||||
signature: Buffer.from(signature).toString('hex'),
|
||||
timestamp,
|
||||
body
|
||||
})
|
||||
|
||||
expect(verifiedSignature.body).equal(body)
|
||||
expect(verifiedSignature.isValid).true
|
||||
})
|
||||
})
|
||||
16
packages/utils/tsconfig.json
Normal file
16
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"test",
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
11
packages/utils/tsconfig.test.json
Normal file
11
packages/utils/tsconfig.test.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "tsconfig/test.json",
|
||||
"include": [
|
||||
"tests",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"src"
|
||||
]
|
||||
}
|
||||
34
yarn.lock
34
yarn.lock
@@ -27,6 +27,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@discordeno/transformer": 18.0.0-alpha.1
|
||||
"@discordeno/types": 18.0.0-alpha.1
|
||||
"@discordeno/utils": 18.0.0-alpha.1
|
||||
"@swc/cli": ^0.1.57
|
||||
"@swc/core": ^1.3.21
|
||||
"@types/chai": ^4
|
||||
@@ -87,6 +88,30 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@discordeno/utils@18.0.0-alpha.1, @discordeno/utils@workspace:packages/utils":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@discordeno/utils@workspace:packages/utils"
|
||||
dependencies:
|
||||
"@discordeno/types": 18.0.0-alpha.1
|
||||
"@swc/cli": ^0.1.57
|
||||
"@swc/core": ^1.3.21
|
||||
"@types/chai": ^4
|
||||
"@types/mocha": ^10
|
||||
"@types/node": ^18.11.15
|
||||
"@types/sinon": ^10.0.13
|
||||
c8: ^7.12.0
|
||||
chai: ^4.3.7
|
||||
eslint: ^8.0.1
|
||||
eslint-config-discordeno: "*"
|
||||
mocha: ^10.1.0
|
||||
sinon: ^15.0.0
|
||||
ts-node: ^10.9.1
|
||||
tsconfig: "*"
|
||||
tweetnacl: ^1.0.3
|
||||
typescript: ^4.9.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@eslint/eslintrc@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "@eslint/eslintrc@npm:1.4.0"
|
||||
@@ -497,7 +522,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:^18.11.9":
|
||||
"@types/node@npm:*, @types/node@npm:^18.11.15, @types/node@npm:^18.11.9":
|
||||
version: 18.11.18
|
||||
resolution: "@types/node@npm:18.11.18"
|
||||
checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d
|
||||
@@ -5194,6 +5219,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tweetnacl@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "tweetnacl@npm:1.0.3"
|
||||
checksum: e4a57cac188f0c53f24c7a33279e223618a2bfb5fea426231991652a13247bea06b081fd745d71291fcae0f4428d29beba1b984b1f1ce6f66b06a6d1ab90645c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "type-check@npm:0.4.0"
|
||||
|
||||
Reference in New Issue
Block a user