This commit is contained in:
Skillz
2023-08-20 11:10:14 -05:00
31 changed files with 265 additions and 174 deletions
+38 -22
View File
@@ -3,16 +3,16 @@ name: Rest proxy
on:
push:
branches:
- "main"
- 'main'
paths:
- ".github/workflows/rest-proxy.yml"
- "proxies/rest/**"
- '.github/workflows/rest-proxy.yml'
- 'proxies/rest/**'
pull_request:
paths:
- ".github/workflows/rest-proxy.yml"
- "proxies/rest/**"
- '.github/workflows/rest-proxy.yml'
- 'proxies/rest/**'
schedule:
- cron: "0 0 * * *"
- cron: '0 0 * * *'
jobs:
build:
@@ -59,6 +59,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download artifact
@@ -71,27 +72,42 @@ jobs:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: "discordeno/rest-proxy:latest"
format: "table"
exit-code: "0"
image-ref: 'discordeno/rest-proxy:latest'
format: 'table'
exit-code: '0'
ignore-unfixed: true
vuln-type: "os,library"
severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
vuln-type: 'os,library'
severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL'
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
if: ${{ github.event_name == 'schedule' }}
if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }}
with:
image-ref: "discordeno/rest-proxy:latest"
exit-code: "0"
vuln-type: "os,library"
severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
format: "sarif"
output: "trivy-results.sarif"
image-ref: 'discordeno/rest-proxy:latest'
exit-code: '0'
vuln-type: 'os,library'
severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: ${{ github.event_name == 'schedule' }}
if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }}
with:
sarif_file: "trivy-results.sarif"
sarif_file: 'trivy-results.sarif'
- name: Run Snyk to check Docker image for vulnerabilities
if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }}
continue-on-error: true
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: 'discordeno/rest-proxy:latest'
args: --file=proxies/rest/Dockerfile
- name: Upload result to GitHub Code Scanning
if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }}
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: snyk.sarif
build-all-arch:
name: Build image for all architectures
@@ -117,7 +133,7 @@ jobs:
with:
context: proxies/rest
push: false
tags: "discordeno/rest-proxy:latest"
tags: 'discordeno/rest-proxy:latest'
# linux/s390x stuck at yarn install, remove it for now
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le
target: runner
@@ -157,7 +173,7 @@ jobs:
with:
context: proxies/rest
push: true
tags: "ghcr.io/discordeno/rest-proxy:latest"
tags: 'ghcr.io/discordeno/rest-proxy:latest'
# linux/s390x stuck at yarn install, remove it for now
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le
target: runner
+1 -1
View File
@@ -5,6 +5,6 @@
This repo is meant as a template which you can use to create a Discord bot very easily using the
[Discordeno library](https://github.com/discordeno/discordeno).
[Website/Guide](https://discordeno.js.org//)
[Website/Guide](https://discordeno.js.org/)
[Discord Server](https://discord.com/invite/5vBgXk3UcZ)
+15 -10
View File
@@ -67,18 +67,21 @@ export function createBot(options: CreateBotOptions): Bot {
// Set up helpers below.
helpers: {} as BotHelpers,
async start() {
// @ts-expect-error should this work
if (typeof Deno !== 'undefined') {
// @ts-expect-error should this work
const katsura = await import('https://x.nest.land/katsura@1.3.9/src/discordenoFixes/gatewaySocket.ts')
await katsura(bot.gateway)
}
if (!options.gateway?.connection) {
bot.gateway.connection = await bot.rest.getSessionInfo()
// Check for overrides in the configuration
if (!options.gateway?.url)
bot.gateway.url = bot.gateway.connection.url;
if (!options.gateway?.totalShards)
bot.gateway.totalShards = bot.gateway.connection.shards;
if (!options.gateway?.lastShardId)
bot.gateway.lastShardId = bot.gateway.connection.shards - 1;
}
return await bot.gateway.spawnShards()
await bot.gateway.spawnShards()
},
async shutdown() {
@@ -184,7 +187,8 @@ export interface EventHandlers {
guildId?: bigint
member?: Member
user?: User
emoji: Emoji
emoji: Emoji,
messageAuthorId?: bigint
}) => unknown
reactionRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; emoji: Emoji }) => unknown
reactionRemoveEmoji: (payload: { channelId: bigint; messageId: bigint; guildId?: bigint; emoji: Emoji }) => unknown
@@ -205,6 +209,7 @@ export interface EventHandlers {
guildBanRemove: (user: User, guildId: bigint) => unknown
guildCreate: (guild: Guild) => unknown
guildDelete: (id: bigint, shardId: number) => unknown
guildUnavailable: (id: bigint, shardId: number) => unknown
guildUpdate: (guild: Guild) => unknown
raw: (data: DiscordGatewayPayload, shardId: number) => unknown
roleCreate: (role: Role) => unknown
+1 -1
View File
@@ -41,7 +41,7 @@ export function createBotGatewayHandlers(
INTEGRATION_UPDATE: options.INTEGRATION_UPDATE ?? handlers.handleIntegrationUpdate,
INTEGRATION_DELETE: options.INTEGRATION_DELETE ?? handlers.handleIntegrationDelete,
INVITE_CREATE: options.INVITE_CREATE ?? handlers.handleInviteCreate,
INVITE_DELETE: options.INVITE_DELETE ?? handlers.handleInviteCreate,
INVITE_DELETE: options.INVITE_DELETE ?? handlers.handleInviteDelete,
MESSAGE_CREATE: options.MESSAGE_CREATE ?? handlers.handleMessageCreate,
MESSAGE_DELETE_BULK: options.MESSAGE_DELETE_BULK ?? handlers.handleMessageDeleteBulk,
MESSAGE_DELETE: options.MESSAGE_DELETE ?? handlers.handleMessageDelete,
@@ -3,5 +3,11 @@ import type { Bot } from '../../bot.js'
export async function handleGuildDelete(bot: Bot, data: DiscordGatewayPayload, shardId: number): Promise<void> {
const payload = data.d as DiscordUnavailableGuild
if (payload.unavailable) {
bot.events.guildUnavailable?.(bot.transformers.snowflake(payload.id), shardId)
return
}
bot.events.guildDelete?.(bot.transformers.snowflake(payload.id), shardId)
}
@@ -14,5 +14,6 @@ export async function handleMessageReactionAdd(bot: Bot, data: DiscordGatewayPay
member: payload.member && guildId ? bot.transformers.member(bot, payload.member, guildId, userId) : undefined,
user: payload.member ? bot.transformers.user(bot, payload.member.user) : undefined,
emoji: bot.transformers.emoji(bot, payload.emoji),
messageAuthorId: payload.message_author_id ? bot.transformers.snowflake(payload.message_author_id) : undefined,
})
}
+2
View File
@@ -334,6 +334,7 @@ export interface Transformers {
}
user: {
username: boolean
globalName: boolean
locale: boolean
flags: boolean
premiumType: boolean
@@ -672,6 +673,7 @@ export function createTransformers(options: Partial<Transformers>): Transformers
},
user: {
username: false,
globalName: false,
locale: false,
flags: false,
premiumType: false,
+12 -7
View File
@@ -1,5 +1,5 @@
import type { DiscordAuditLogEntry } from '@discordeno/types'
import type { Bot } from '../index.js'
import { iconHashToBigInt, type Bot } from '../index.js'
import type { Optionalize } from '../optionalize.js'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -21,13 +21,8 @@ export function transformAuditLogEntry(bot: Bot, payload: DiscordAuditLogEntry)
name: val?.name,
})),
}
case 'discovery_splash_hash':
case 'banner_hash':
case 'rules_channel_id':
case 'public_updates_channel_id':
case 'icon_hash':
case 'image_hash':
case 'splash_hash':
case 'owner_id':
case 'widget_channel_id':
case 'system_channel_id':
@@ -37,13 +32,23 @@ export function transformAuditLogEntry(bot: Bot, payload: DiscordAuditLogEntry)
case 'deny':
case 'channel_id':
case 'inviter_id':
case 'avatar_hash':
case 'id':
return {
key: change.key,
old: change.old_value ? bot.transformers.snowflake(change.old_value) : undefined,
new: change.new_value ? bot.transformers.snowflake(change.new_value) : undefined,
}
case 'discovery_splash_hash':
case 'banner_hash':
case 'icon_hash':
case 'image_hash':
case 'splash_hash':
case 'avatar_hash':
return {
key: change.key,
old: change.old_value ? iconHashToBigInt(change.old_value) : undefined,
new: change.new_value ? iconHashToBigInt(change.new_value) : undefined,
}
case 'name':
case 'description':
case 'preferred_locale':
+1 -1
View File
@@ -73,7 +73,7 @@ export function transformChannel(bot: Bot, payload: { channel: DiscordChannel }
if (payload.channel.name && props.name) channel.name = payload.channel.name
if (payload.channel.topic && props.topic) channel.topic = payload.channel.topic
if (payload.channel.last_message_id && props.lastMessageId) channel.lastMessageId = bot.transformers.snowflake(payload.channel.last_message_id)
if (payload.channel.bitrate && props.bitrate) channel.bitrate = props.bitrate
if (payload.channel.bitrate && props.bitrate) channel.bitrate = payload.channel.bitrate
if (props.userLimit) channel.userLimit = payload.channel.user_limit
if (props.rateLimitPerUser) channel.rateLimitPerUser = payload.channel.rate_limit_per_user
if (payload.channel.owner_id && props.ownerId) channel.ownerId = bot.transformers.snowflake(payload.channel.owner_id)
-1
View File
@@ -16,7 +16,6 @@ const baseMessage: Partial<Message> & MessageBase = {
return this.flags?.contains(MessageFlags.Crossposted) ?? false
},
set crossposted(value: boolean) {
if (!this.flags) return
if (!this.flags) return
if (value) this.flags.add(MessageFlags.Crossposted)
else this.flags.remove(MessageFlags.Crossposted)
@@ -1,5 +1,5 @@
import type { DiscordAuditLogEntry } from '@discordeno/types'
import type { Bot } from '../../index.js'
import { iconBigintToHash, type Bot } from '../../index.js'
import type { AuditLogEntry } from '../auditLogEntry.js'
export function transformAuditLogEntryToDiscordAuditLogEntry(bot: Bot, payload: AuditLogEntry): DiscordAuditLogEntry {
@@ -33,13 +33,8 @@ export function transformAuditLogEntryToDiscordAuditLogEntry(bot: Bot, payload:
name: val?.name,
})),
}
case 'discovery_splash_hash':
case 'banner_hash':
case 'rules_channel_id':
case 'public_updates_channel_id':
case 'icon_hash':
case 'image_hash':
case 'splash_hash':
case 'owner_id':
case 'widget_channel_id':
case 'system_channel_id':
@@ -49,13 +44,23 @@ export function transformAuditLogEntryToDiscordAuditLogEntry(bot: Bot, payload:
case 'deny':
case 'channel_id':
case 'inviter_id':
case 'avatar_hash':
case 'id':
return {
key: change.key,
old_value: change.old ? bot.transformers.reverse.snowflake(change.old as bigint) : '',
new_value: change.new ? bot.transformers.reverse.snowflake(change.new as bigint) : '',
}
case 'discovery_splash_hash':
case 'banner_hash':
case 'icon_hash':
case 'image_hash':
case 'splash_hash':
case 'avatar_hash':
return {
key: change.key,
old_value: change.old ? iconBigintToHash(change.old as bigint) : '',
new_value: change.new ? iconBigintToHash(change.new as bigint) : '',
}
case 'name':
case 'description':
case 'preferred_locale':
@@ -8,6 +8,7 @@ export function transformUserToDiscordUser(bot: Bot, payload: User): DiscordUser
return {
id: payload.id.toString(),
username: payload.username,
global_name: payload.globalName ?? null,
discriminator: payload.discriminator,
avatar: payload.avatar ? iconBigintToHash(payload.avatar) : null,
locale: payload.locale,
+4 -1
View File
@@ -32,6 +32,7 @@ export function transformUser(bot: Bot, payload: DiscordUser): User {
if (props.publicFlags) user.publicFlags = new ToggleBitfield(payload.public_flags)
if (payload.id && props.id) user.id = bot.transformers.snowflake(payload.id)
if (payload.username && props.username) user.username = payload.username
if (payload.global_name && props.globalName) user.globalName = payload.global_name
if (payload.discriminator && props.discriminator) user.discriminator = payload.discriminator
if (payload.locale && props.locale) user.locale = payload.locale
if (payload.email && props.email) user.email = payload.email
@@ -61,6 +62,8 @@ export interface User extends BaseUser {
toggles?: UserToggles
/** The user's username, not unique across the platform */
username: string
/** The user's display name, if it is set. For bots, this is the application name */
globalName?: string
/** The user's chosen language option */
locale?: string
/** The flags on a user's account */
@@ -73,7 +76,7 @@ export interface User extends BaseUser {
accentColor?: number
/** The user's id */
id: bigint
/** The user's 4-digit discord-tag */
/** The user's discord-tag */
discriminator: string
/** The user's avatar hash */
avatar?: bigint
+14 -10
View File
@@ -12,11 +12,13 @@ import type {
import { GatewayCloseEventCodes, GatewayIntents, GatewayOpcodes } from '@discordeno/types'
import { Collection, LeakyBucket, camelize, delay, logger } from '@discordeno/utils'
import { inflateSync } from 'node:zlib'
import WebSocket from 'ws'
import NodeWebSocket from 'ws'
import type { RequestMemberRequest } from './manager.js'
import type { BotStatusUpdate, ShardEvents, ShardGatewayConfig, ShardHeart, ShardSocketRequest, StatusUpdate, UpdateVoiceState } from './types.js'
import { ShardSocketCloseCodes, ShardState } from './types.js'
declare let WebSocket: any
export class DiscordenoShard {
/** The id of the shard */
id: number
@@ -33,7 +35,7 @@ export class DiscordenoShard {
/** Current session id of the shard if present. */
sessionId?: string
/** This contains the WebSocket connection to Discord, if currently connected. */
socket?: WebSocket
socket?: NodeWebSocket
/** Current internal state of the this. */
state = ShardState.Offline
/** The url provided by discord to use when resuming a connection for this this. */
@@ -111,7 +113,7 @@ export class DiscordenoShard {
/** Close the socket connection to discord if present. */
close(code: number, reason: string): void {
if (this.socket?.readyState !== WebSocket.OPEN) return
if (this.socket?.readyState !== NodeWebSocket.OPEN) return
this.socket?.close(code, reason)
}
@@ -129,13 +131,15 @@ export class DiscordenoShard {
url.searchParams.set('v', this.gatewayConfig.version.toString())
url.searchParams.set('encoding', 'json')
const socket = new WebSocket(url.toString())
const socket: NodeWebSocket =
// @ts-expect-error Deno
globalThis.Deno !== undefined && Reflect.has(globalThis, 'Deno') ? new WebSocket(url.toString()) : new NodeWebSocket(url.toString())
this.socket = socket
// TODO: proper event handling
socket.onerror = (event) => console.log({ error: event, shardId: this.id })
socket.onclose = async (event) => await this.handleClose(event)
socket.onmessage = async (message) => await this.handleMessage(message)
socket.onerror = (event: NodeWebSocket.ErrorEvent) => console.log({ error: event, shardId: this.id })
socket.onclose = async (event: NodeWebSocket.CloseEvent) => await this.handleClose(event)
socket.onmessage = async (message: NodeWebSocket.MessageEvent) => await this.handleMessage(message)
return await new Promise((resolve) => {
socket.onopen = () => {
@@ -204,7 +208,7 @@ export class DiscordenoShard {
/** Check whether the connection to Discord is currently open. */
isOpen(): boolean {
return this.socket?.readyState === WebSocket.OPEN
return this.socket?.readyState === NodeWebSocket.OPEN
}
/** Attempt to resume the previous shards session with the gateway. */
@@ -282,7 +286,7 @@ export class DiscordenoShard {
}
/** Handle a gateway connection close. */
async handleClose(close: WebSocket.CloseEvent): Promise<void> {
async handleClose(close: NodeWebSocket.CloseEvent): Promise<void> {
// gateway.debug("GW CLOSED", { shardId, payload: event });
this.stopHeartbeating()
@@ -485,7 +489,7 @@ export class DiscordenoShard {
}
/** Handle an incoming gateway message. */
async handleMessage(message: WebSocket.MessageEvent): Promise<void> {
async handleMessage(message: NodeWebSocket.MessageEvent): Promise<void> {
let preProcessMessage = message.data
// If message compression is enabled,
+13 -15
View File
@@ -5,17 +5,15 @@ import Shard from './Shard.js'
import type { ShardEvents, StatusUpdate, UpdateVoiceState } from './types.js'
export function createGatewayManager(options: CreateGatewayManagerOptions): GatewayManager {
if (!options.connection) {
options.connection = {
url: 'wss://gateway.discord.gg',
shards: 1,
sessionStartLimit: {
maxConcurrency: 1,
remaining: 1000,
total: 1000,
resetAfter: 1000 * 60 * 60 * 24,
},
}
const connectionOptions = options.connection ?? {
url: 'wss://gateway.discord.gg',
shards: 1,
sessionStartLimit: {
maxConcurrency: 1,
remaining: 1000,
total: 1000,
resetAfter: 1000 * 60 * 60 * 24,
},
}
const gateway: GatewayManager = {
@@ -28,11 +26,11 @@ export function createGatewayManager(options: CreateGatewayManagerOptions): Gate
device: options.properties?.device ?? 'Discordeno',
},
token: options.token,
url: options.url ?? options.connection.url ?? 'wss://gateway.discord.gg',
url: options.url ?? connectionOptions.url ?? 'wss://gateway.discord.gg',
version: options.version ?? 10,
connection: options.connection,
totalShards: options.totalShards ?? options.connection.shards ?? 1,
lastShardId: options.lastShardId ?? (options.totalShards ? options.totalShards - 1 : (options.connection ? options.connection.shards - 1 : 0)),
connection: connectionOptions,
totalShards: options.totalShards ?? connectionOptions.shards ?? 1,
lastShardId: options.lastShardId ?? (options.totalShards ? options.totalShards - 1 : (connectionOptions ? connectionOptions.shards - 1 : 0)),
firstShardId: options.firstShardId ?? 0,
totalWorkers: options.totalWorkers ?? 4,
shardsPerWorker: options.shardsPerWorker ?? 25,
+31 -5
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable no-const-assign */
import { calculateBits, camelToSnakeCase, camelize, delay, getBotIdFromToken, logger, processReactionString, urlToBase64 } from '@discordeno/utils'
import { calculateBits, camelToSnakeCase, camelize, delay, getBotIdFromToken, logger, processReactionString, urlToBase64, snakelize } from '@discordeno/utils'
import { createInvalidRequestBucket } from './invalidBucket.js'
import { Queue } from './queue.js'
@@ -126,10 +126,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
case 'permissions':
case 'allow':
case 'deny':
newObj[key] = calculateBits(value)
newObj[key] = typeof value === 'string' ? value : calculateBits(value)
continue
case 'defaultMemberPermissions':
newObj.default_member_permissions = calculateBits(value)
newObj.default_member_permissions = typeof value === 'string' ? value : calculateBits(value)
continue
case 'nameLocalizations':
newObj.name_localizations = value
@@ -175,7 +175,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
form.append(`file${i}`, options.files[i].blob, options.files[i].name)
}
form.append('payload_json', JSON.stringify({ ...options.body, files: undefined }))
form.append('payload_json', JSON.stringify(snakelize({ ...options.body, files: undefined })))
body = form
@@ -777,7 +777,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
},
async editMessage(channelId, messageId, body) {
return await rest.patch<DiscordMessage>(rest.routes.channels.message(channelId, messageId), { body })
return await rest.patch<DiscordMessage>(rest.routes.channels.message(channelId, messageId), { body, files: body.files })
},
async editOriginalInteractionResponse(token, body) {
@@ -1226,6 +1226,32 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
async upsertGuildApplicationCommands(guildId, body) {
return await rest.put<DiscordApplicationCommand[]>(rest.routes.interactions.commands.guilds.all(rest.applicationId, guildId), { body })
},
preferSnakeCase(enabled: boolean) {
const camelizer = enabled ? (x: any) => x : camelize
rest.get = async (url, options) => {
return camelizer(await rest.makeRequest('GET', url, options))
}
rest.post = async (url: string, options?: Omit<CreateRequestBodyOptions, 'body' | 'method'>) => {
return camelizer(await rest.makeRequest('POST', url, options))
}
rest.delete = async (url: string, options?: Omit<CreateRequestBodyOptions, 'body' | 'method'>) => {
camelizer(await rest.makeRequest('DELETE', url, options))
}
rest.patch = async (url: string, options?: Omit<CreateRequestBodyOptions, 'body' | 'method'>) => {
return camelizer(await rest.makeRequest('PATCH', url, options))
}
rest.put = async (url: string, options?: Omit<CreateRequestBodyOptions, 'body' | 'method'>) => {
return camelizer(await rest.makeRequest('PUT', url, options))
}
return rest
},
}
return rest
+2
View File
@@ -160,6 +160,8 @@ export interface RestManager {
invalidBucket: InvalidRequestBucket
/** The routes that are available for this manager. */
routes: RestRoutes
/** Whether or not the rest manager should keep objects in raw snake case from discord. */
preferSnakeCase: (enabled: boolean) => RestManager;
/** Check the rate limits for a url or a bucket. */
checkRateLimits: (url: string) => number | false
/** Reshapes and modifies the obj as needed to make it ready for discords api. */
+20
View File
@@ -59,6 +59,26 @@ describe('Send a message', () => {
expect(fileMsg.id).not.equals(undefined);
expect(fileMsg.content).equals('222');
expect(fileMsg.attachments.length).equals(1);
expect(fileMsg.attachments.at(0)?.filename).equals('application.txt');
expect(fileMsg.attachments.at(0)?.size).equals(11);
const txtFile2 = new Blob(['hello world edit'], { type: 'text/plain' });
const edited = await rest.editMessage(e2ecache.channel.id, fileMsg.id, {
content: '222 edit',
files: [
{
name: 'application_edit.txt',
blob: txtFile2,
},
],
});
expect(edited.id).not.equals(undefined);
expect(edited.content).equals('222 edit');
expect(edited.attachments.length).equals(1);
expect(edited.attachments.at(0)?.filename).equals('application_edit.txt');
expect(edited.attachments.at(0)?.size).equals(16);
})
})
+2 -1
View File
@@ -1,4 +1,3 @@
import { expect } from 'chai'
import { describe } from 'mocha'
import { e2ecache, rest } from './utils.js'
@@ -9,6 +8,7 @@ describe('Typings', () => {
})
})
/* TODO: Add this back when bot's name is changed (https://discord.com/channels/785384884197392384/785384884197392387/1142474846811459776)
describe('Commands', () => {
it('Upsert global commands', async () => {
await rest.upsertGlobalApplicationCommands([
@@ -37,3 +37,4 @@ describe('Commands', () => {
expect(rest.getGlobalApplicationCommand(created!.id)).to.throw
})
})
*/
+1 -1
View File
@@ -32,7 +32,7 @@ describe('Get a user from the api', () => {
})
it('Has a valid discriminator', () => {
expect(user.discriminator.length).to.be.equal(4)
expect(user.discriminator.length).to.be.oneOf([1,4])
})
it('Has been camelized', () => {
+7 -3
View File
@@ -46,6 +46,8 @@ import type {
export interface DiscordUser {
/** The user's username, not unique across the platform */
username: string
/** The user's display name, if it is set. For bots, this is the application name */
global_name: string | null
/** The user's chosen language option */
locale?: string
/** The flags on a user's account */
@@ -58,7 +60,7 @@ export interface DiscordUser {
accent_color?: number
/** The user's id */
id: string
/** The user's 4-digit discord-tag */
/** The user's discord-tag */
discriminator: string
/** The user's avatar hash */
avatar: string | null
@@ -264,8 +266,8 @@ export interface DiscordTeamMember {
permissions: Array<'*'>
/** The id of the parent team of which they are a member */
team_id: string
/** The avatar, discriminator, id, and username of the user */
user: Partial<DiscordUser> & Pick<DiscordUser, 'avatar' | 'discriminator' | 'id' | 'username'>
/** The avatar, discriminator, id, username, and global_name of the user */
user: Partial<DiscordUser> & Pick<DiscordUser, 'avatar' | 'discriminator' | 'id' | 'username' | 'global_name'>
}
/** https://discord.com/developers/docs/topics/gateway#webhooks-update-webhook-update-event-fields */
@@ -2020,6 +2022,8 @@ export interface DiscordMessageReactionAdd {
member?: DiscordMemberWithUser
/** The emoji used to react */
emoji: Partial<DiscordEmoji>
/** The id of the author of this message */
message_author_id?: string
}
/** https://discord.com/developers/docs/topics/gateway#voice-server-update */
+2 -2
View File
@@ -348,7 +348,7 @@ export interface GetBans {
/** https://discord.com/developers/docs/resources/guild#list-guild-members */
export interface ListGuildMembers {
/** Max number of members to return (1-1000). Default: 1000 */
/** Max number of members to return (1-1000). Default: 1 */
limit?: number
/** The highest user id in the previous page. Default: 0 */
after?: string
@@ -853,7 +853,7 @@ export interface EditMessage {
/** Edit the flags of the message (only `SUPPRESS_EMBEDS` can currently be set/unset) */
flags?: 4 | null
/** The contents of the files being sent/edited */
files?: FileContent[] | null
files?: FileContent[]
/** Allowed mentions for the message */
allowedMentions?: AllowedMentions
/** When specified (adding new attachments), attachments which are not provided in this list will be removed. */
+3 -1
View File
@@ -35,9 +35,11 @@ export class LeakyBucket implements LeakyBucketOptions {
this.used = this.refillAmount > this.used ? 0 : this.used - this.refillAmount
// Reset the refillsAt timestamp since it just got refilled
this.refillsAt = undefined
// Reset the timeoutId
clearTimeout(this.timeoutId)
this.timeoutId = undefined;
if (this.used > 0) {
if (this.timeoutId) clearTimeout(this.timeoutId)
this.timeoutId = setTimeout(() => {
this.refillBucket()
}, this.refillInterval)
+1 -1
View File
@@ -41,7 +41,7 @@ export function avatarUrl(
options?.size ?? 128,
options?.format,
)
: `https://cdn.discordapp.com/embed/avatars/${Number(discriminator) % 5}.png`
: `https://cdn.discordapp.com/embed/avatars/${discriminator === '0' ? (BigInt(userId) >> BigInt(22)) % BigInt(6) : Number(discriminator) % 5}.png`
}
/**
+4 -2
View File
@@ -25,8 +25,8 @@ describe('Convert interaction to args', () => {
version: 1,
app_permissions: '',
data: {
id: "",
name: "",
id: '',
name: '',
type: ApplicationCommandTypes.ChatInput,
},
})
@@ -91,12 +91,14 @@ describe('Convert interaction to args', () => {
'123': {
id: '123',
username: '123',
global_name: '123',
discriminator: '1234',
avatar: null,
},
'456': {
id: '456',
username: '456',
global_name: '456',
discriminator: '4567',
avatar: null,
},
+5 -5
View File
@@ -1,4 +1,4 @@
# we node 18 alpine 3.17 as base image
# we node 18 alpine 3.18 as base image
# we use multi stage build in this file
# deps: contain all dependencies including dev dependencies
# builder: contains all compiled files
@@ -6,7 +6,7 @@
# runner: the final image, with only the dependencies and compiled files
# build only with the platform of the host machine, since it only uses for dev purposes
FROM --platform=$BUILDPLATFORM node:18.15.0-alpine3.17 AS deps
FROM --platform=$BUILDPLATFORM node:18.16.0-alpine3.18 AS deps
WORKDIR /app
# copy necessary for install dependencies
COPY package.json yarn.lock ./
@@ -14,7 +14,7 @@ COPY package.json yarn.lock ./
RUN yarn install
# build only with the platform of the host machine, since we just need its files
FROM --platform=$BUILDPLATFORM node:18.15.0-alpine3.17 AS builder
FROM --platform=$BUILDPLATFORM node:18.16.0-alpine3.18 AS builder
# copy the dependencies (node_modules) from the deps image
COPY --from=deps /app /app
WORKDIR /app
@@ -25,7 +25,7 @@ COPY .swcrc ./
# compile the files
RUN yarn build
FROM node:18.15.0-alpine3.17 AS prod-deps
FROM node:18.16.0-alpine3.18 AS prod-deps
WORKDIR /app
# copy necessary files for install dependencies
COPY package.json yarn.lock .yarnrc.yml ./
@@ -35,7 +35,7 @@ RUN yarn plugin import workspace-tools
# install prod dependencies
RUN yarn workspaces focus --all --production
FROM node:18.15.0-alpine3.17 AS runner
FROM node:18.16.0-alpine3.18 AS runner
# copy the compiled files from the builder image
COPY --from=builder /app/dist /app/dist
# copy the prod dependencies (node_modules) from the prod-deps image
+4 -4
View File
@@ -1527,11 +1527,11 @@ __metadata:
linkType: hard
"semver@npm:^6.0.0":
version: 6.3.0
resolution: "semver@npm:6.3.0"
version: 6.3.1
resolution: "semver@npm:6.3.1"
bin:
semver: ./bin/semver.js
checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9
semver: bin/semver.js
checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2
languageName: node
linkType: hard
+45 -45
View File
@@ -2102,9 +2102,9 @@
}
},
"node_modules/@docusaurus/core/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -3033,9 +3033,9 @@
}
},
"node_modules/@docusaurus/theme-common/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"peer": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -3645,9 +3645,9 @@
}
},
"node_modules/@mdx-js/mdx/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"bin": {
"semver": "bin/semver"
}
@@ -4633,9 +4633,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -4777,9 +4777,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -4836,9 +4836,9 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -5808,9 +5808,9 @@
}
},
"node_modules/builtins/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -6680,9 +6680,9 @@
}
},
"node_modules/css-loader/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -7786,9 +7786,9 @@
}
},
"node_modules/eslint-plugin-n/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -8818,9 +8818,9 @@
}
},
"node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -11917,9 +11917,9 @@
}
},
"node_modules/postcss-loader/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -13237,9 +13237,9 @@
}
},
"node_modules/remark-mdx/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"bin": {
"semver": "bin/semver"
}
@@ -13702,9 +13702,9 @@
}
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
@@ -15246,9 +15246,9 @@
}
},
"node_modules/update-notifier/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -16205,9 +16205,9 @@
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw=="
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"devOptional": true,
"engines": {
"node": ">=0.10.0"
@@ -44,7 +44,7 @@ const reviewList: IReview[] = [
'https://cdn.discordapp.com/avatars/1032699319368814652/8abbc5911333d3df288ff962a2120d75.png',
invite_url:
'https://discord.com/oauth2/authorize?client_id=1032699319368814652&permissions=274877959232&scope=bot%20applications.commands',
guild_count: 50000,
guild_count: 64000,
},
developer: {
username: '8au',
@@ -66,7 +66,7 @@ const reviewList: IReview[] = [
'https://cdn.discordapp.com/avatars/853327905357561948/a394518e742643f05cc201249d3ae75e.png',
invite_url:
'https://discord.com/oauth2/authorize?client_id=853327905357561948&scope=bot&permissions=534925798592',
guild_count: 82000,
guild_count: 100000,
},
developer: {
username: '8au',
+8 -19
View File
@@ -10737,11 +10737,11 @@ __metadata:
linkType: hard
"semver@npm:^5.4.1":
version: 5.7.1
resolution: "semver@npm:5.7.1"
version: 5.7.2
resolution: "semver@npm:5.7.2"
bin:
semver: ./bin/semver
checksum: 57fd0acfd0bac382ee87cd52cd0aaa5af086a7dc8d60379dfe65fea491fb2489b6016400813930ecd61fd0952dae75c115287a1b16c234b1550887117744dfaf
semver: bin/semver
checksum: fb4ab5e0dd1c22ce0c937ea390b4a822147a9c53dbd2a9a0132f12fe382902beef4fbf12cf51bb955248d8d15874ce8cd89532569756384f994309825f10b686
languageName: node
linkType: hard
@@ -10754,18 +10754,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.0.0, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.7, semver@npm:^7.3.8":
version: 7.3.8
resolution: "semver@npm:7.3.8"
dependencies:
lru-cache: ^6.0.0
bin:
semver: bin/semver.js
checksum: ba9c7cbbf2b7884696523450a61fee1a09930d888b7a8d7579025ad93d459b2d1949ee5bbfeb188b2be5f4ac163544c5e98491ad6152df34154feebc2cc337c1
languageName: node
linkType: hard
"semver@npm:^7.3.5":
"semver@npm:^7.0.0, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8":
version: 7.4.0
resolution: "semver@npm:7.4.0"
dependencies:
@@ -12419,9 +12408,9 @@ __metadata:
linkType: hard
"word-wrap@npm:^1.2.3":
version: 1.2.3
resolution: "word-wrap@npm:1.2.3"
checksum: 30b48f91fcf12106ed3186ae4fa86a6a1842416df425be7b60485de14bec665a54a68e4b5156647dec3a70f25e84d270ca8bc8cd23182ed095f5c7206a938c1f
version: 1.2.4
resolution: "word-wrap@npm:1.2.4"
checksum: 8f1f2e0a397c0e074ca225ba9f67baa23f99293bc064e31355d426ae91b8b3f6b5f6c1fc9ae5e9141178bb362d563f55e62fd8d5c31f2a77e3ade56cb3e35bd1
languageName: node
linkType: hard
+7 -7
View File
@@ -4489,11 +4489,11 @@ __metadata:
linkType: hard
"semver@npm:^6.0.0, semver@npm:^6.3.0":
version: 6.3.0
resolution: "semver@npm:6.3.0"
version: 6.3.1
resolution: "semver@npm:6.3.1"
bin:
semver: ./bin/semver.js
checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9
semver: bin/semver.js
checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2
languageName: node
linkType: hard
@@ -5383,9 +5383,9 @@ __metadata:
linkType: hard
"word-wrap@npm:^1.2.3":
version: 1.2.3
resolution: "word-wrap@npm:1.2.3"
checksum: 30b48f91fcf12106ed3186ae4fa86a6a1842416df425be7b60485de14bec665a54a68e4b5156647dec3a70f25e84d270ca8bc8cd23182ed095f5c7206a938c1f
version: 1.2.4
resolution: "word-wrap@npm:1.2.4"
checksum: 8f1f2e0a397c0e074ca225ba9f67baa23f99293bc064e31355d426ae91b8b3f6b5f6c1fc9ae5e9141178bb362d563f55e62fd8d5c31f2a77e3ade56cb3e35bd1
languageName: node
linkType: hard