Skip to content

Commit

Permalink
feat(Client): add request soundboard sounds
Browse files Browse the repository at this point in the history
  • Loading branch information
sdanialraza committed Nov 24, 2024
1 parent 0374079 commit af803db
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 12 deletions.
123 changes: 111 additions & 12 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clearTimeout, setTimeout } from 'node:timers';
import type { REST } from '@discordjs/rest';
import { calculateShardId } from '@discordjs/util';
import { calculateShardId, groupBy } from '@discordjs/util';
import { WebSocketShardEvents } from '@discordjs/ws';
import { DiscordSnowflake } from '@sapphire/snowflake';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
Expand Down Expand Up @@ -81,6 +81,8 @@ import {
type GatewayVoiceStateUpdateData,
type GatewayVoiceStateUpdateDispatchData,
type GatewayWebhooksUpdateDispatchData,
type GatewayRequestSoundboardSoundsData,
type GatewaySoundboardSoundsDispatchData,
} from 'discord-api-types/v10';
import type { Gateway } from './Gateway.js';
import { API } from './api/index.js';
Expand Down Expand Up @@ -142,6 +144,7 @@ export interface MappedEvents {
[GatewayDispatchEvents.GuildSoundboardSoundDelete]: [ToEventProps<GatewayGuildSoundboardSoundDeleteDispatch>];
[GatewayDispatchEvents.GuildSoundboardSoundUpdate]: [ToEventProps<GatewayGuildSoundboardSoundUpdateDispatch>];
[GatewayDispatchEvents.GuildSoundboardSoundsUpdate]: [ToEventProps<GatewayGuildSoundboardSoundsUpdateDispatch>];
[GatewayDispatchEvents.SoundboardSounds]: [ToEventProps<GatewaySoundboardSoundsDispatchData>];
[GatewayDispatchEvents.GuildStickersUpdate]: [ToEventProps<GatewayGuildStickersUpdateDispatchData>];
[GatewayDispatchEvents.GuildUpdate]: [ToEventProps<GatewayGuildUpdateDispatchData>];
[GatewayDispatchEvents.IntegrationCreate]: [ToEventProps<GatewayIntegrationCreateDispatchData>];
Expand Down Expand Up @@ -196,6 +199,10 @@ export interface RequestGuildMembersResult {
presences: NonNullable<GatewayGuildMembersChunkDispatchData['presences']>;
}

function createTimer(controller: AbortController, timeout: number) {
return setTimeout(() => controller.abort(), timeout);
}

export class Client extends AsyncEventEmitter<MappedEvents> {
public readonly rest: REST;

Expand Down Expand Up @@ -234,12 +241,7 @@ export class Client extends AsyncEventEmitter<MappedEvents> {

const controller = new AbortController();

const createTimer = () =>
setTimeout(() => {
controller.abort();
}, timeout);

let timer: NodeJS.Timeout | undefined = createTimer();
let timer: NodeJS.Timeout | undefined = createTimer(controller, timeout);

await this.gateway.send(shardId, {
op: GatewayOpcodes.RequestGuildMembers,
Expand Down Expand Up @@ -270,11 +272,9 @@ export class Client extends AsyncEventEmitter<MappedEvents> {
chunkCount: data.chunk_count,
};

if (data.chunk_index >= data.chunk_count - 1) {
break;
} else {
timer = createTimer();
}
if (data.chunk_index >= data.chunk_count - 1) break;

timer = createTimer(controller, timeout);
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
Expand Down Expand Up @@ -316,6 +316,105 @@ export class Client extends AsyncEventEmitter<MappedEvents> {
return { members, nonce, notFound, presences };
}

/**
* Requests soundboard sounds from the gateway and returns an async iterator that yields the data from each soundboard sounds event.
*
* @see {@link https://discord.com/developers/docs/topics/gateway-events#request-soundboard-sounds}
* @param options - The options for the request
* @param timeout - The timeout for waiting for each soundboard sounds
* @example
* Requesting soundboard sounds for specific guilds
* ```ts
* for await (const { soundboardSounds } of this.requestSoundboardSoundsIterator({
* guild_ids: ['1234567890', '9876543210'],
* })) {
* console.log(soundboardSounds);
*}
* ```
*/
public async *requestSoundboardSoundsIterator(options: GatewayRequestSoundboardSoundsData, timeout = 10_000) {
const shardCount = await this.gateway.getShardCount();
const shardIds = groupBy(options.guild_ids, (guildId) => calculateShardId(guildId, shardCount));

const controller = new AbortController();

let timer: NodeJS.Timeout | undefined = createTimer(controller, timeout);

for (const [shardId, guildIds] of shardIds) {
await this.gateway.send(shardId, {
op: GatewayOpcodes.RequestSoundboardSounds,
// eslint-disable-next-line id-length
d: {
...options,
guild_ids: guildIds,
},
});
}

try {
const iterator = AsyncEventEmitter.on(this, GatewayDispatchEvents.SoundboardSounds, {
signal: controller.signal,
});

const guildIds = new Set(options.guild_ids);

for await (const [{ data }] of iterator) {
clearTimeout(timer);
timer = undefined;

if (guildIds.has(data.guild_id)) {
yield {
guildId: data.guild_id,
soundboardSounds: data.soundboard_sounds,
};

guildIds.delete(data.guild_id);
}

if (guildIds.size === 0) break;

timer = createTimer(controller, timeout);
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Request timed out');
}

throw error;
} finally {
if (timer) {
clearTimeout(timer);
}
}
}

/**
* Requests soundboard sounds from the gateway.
*
* @see {@link https://discord.com/developers/docs/topics/gateway-events#request-soundboard-sounds}
* @param options - The options for the request
* @param timeout - The timeout for waiting for each soundboard sounds event
* @example
* Requesting soundboard sounds for specific guilds
* ```ts
* const soundboardSounds = await client.requestSoundboardSounds({ guild_ids: ['1234567890', '9876543210'], });
*
* console.log(soundboardSounds.get('1234567890'));
* ```
*/
public async requestSoundboardSounds(options: GatewayRequestSoundboardSoundsData, timeout = 10_000) {
const soundboardSounds = new Map<
GatewaySoundboardSoundsDispatchData['guild_id'],
GatewaySoundboardSoundsDispatchData['soundboard_sounds']
>();

for await (const data of this.requestSoundboardSoundsIterator(options, timeout)) {
soundboardSounds.set(data.guildId, data.soundboardSounds);
}

return soundboardSounds;
}

/**
* Updates the voice state of the bot user
*
Expand Down
27 changes: 27 additions & 0 deletions packages/util/__tests__/groupBy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, test, expect } from 'vitest';
import { groupBy } from '../src/index.js';

describe('groupBy', () => {
test('GIVEN an array of objects and a key THEN groups the objects by the key', () => {
const objects = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 20 },
{ name: 'Charlie', age: 30 },
];

const grouped = groupBy(objects, (object) => object.age);

expect(grouped).toEqual(
new Map([
[
20,
[
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 20 },
],
],
[30, [{ name: 'Charlie', age: 30 }]],
]),
);
});
});
40 changes: 40 additions & 0 deletions packages/util/src/functions/groupBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Groups members of an iterable according to the return value of the passed callback.
*
* @param items - An iterable.
* @param keySelector - A callback which will be invoked for each item in items.
* @typeParam Key - The type of the key.
* @typeParam Value - The type of the value.
* @example
* ```ts
* const items = [
* { name: 'Alice', age: 20 },
* { name: 'Bob', age: 20 },
* { name: 'Charlie', age: 30 },
* ];
*
* groupBy(items, item => item.age);
* // Map { 20 => [ { name: 'Alice', age: 20 }, { name: 'Bob', age: 20 } ], 30 => [ { name: 'Charlie', age: 30 } ] }
* ```
*/
export function groupBy<Key, Value>(
items: Iterable<Value>,
keySelector: (item: Value, index: number) => Key,
): Map<Key, Value[]> {
if (typeof Map.groupBy === 'function') {
return Map.groupBy(items, keySelector);
}

const map = new Map<Key, Value[]>();
let index = 0;

for (const item of items) {
const key = keySelector(item, index++);
const list = map.get(key);

if (list) list.push(item);
else map.set(key, [item]);
}

return map;
}
1 change: 1 addition & 0 deletions packages/util/src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './calculateShardId.js';
export * from './runtime.js';
export * from './userAgentAppendix.js';
export * from './polyfillDispose.js';
export * from './groupBy.js';

0 comments on commit af803db

Please sign in to comment.