Skip to content

Commit 4c7a95d

Browse files
Merge branch 'main' into main
2 parents 6ae0b6b + ac64508 commit 4c7a95d

File tree

12 files changed

+279
-47
lines changed

12 files changed

+279
-47
lines changed

packages/collection/__tests__/collection.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,3 +908,73 @@ describe('random thisArg tests', () => {
908908
}, array);
909909
});
910910
});
911+
912+
describe('findLast() tests', () => {
913+
const coll = createTestCollection();
914+
test('it returns last matched element', () => {
915+
expect(coll.findLast((value) => value % 2 === 1)).toStrictEqual(3);
916+
});
917+
918+
test('throws if fn is not a function', () => {
919+
// @ts-expect-error: Invalid function
920+
expectInvalidFunctionError(() => createCollection().findLast());
921+
// @ts-expect-error: Invalid function
922+
expectInvalidFunctionError(() => createCollection().findLast(123), 123);
923+
});
924+
925+
test('binds the thisArg', () => {
926+
coll.findLast(function findLast() {
927+
expect(this).toBeNull();
928+
return true;
929+
}, null);
930+
});
931+
});
932+
933+
describe('findLastKey() tests', () => {
934+
const coll = createTestCollection();
935+
test('it returns last matched element', () => {
936+
expect(coll.findLastKey((value) => value % 2 === 1)).toStrictEqual('c');
937+
});
938+
939+
test('throws if fn is not a function', () => {
940+
// @ts-expect-error: Invalid function
941+
expectInvalidFunctionError(() => createCollection().findLastKey());
942+
// @ts-expect-error: Invalid function
943+
expectInvalidFunctionError(() => createCollection().findLastKey(123), 123);
944+
});
945+
946+
test('binds the thisArg', () => {
947+
coll.findLastKey(function findLastKey() {
948+
expect(this).toBeNull();
949+
return true;
950+
}, null);
951+
});
952+
});
953+
954+
describe('reduceRight() tests', () => {
955+
const coll = createTestCollection();
956+
957+
test('throws if fn is not a function', () => {
958+
// @ts-expect-error: Invalid function
959+
expectInvalidFunctionError(() => coll.reduceRight());
960+
// @ts-expect-error: Invalid function
961+
expectInvalidFunctionError(() => coll.reduceRight(123), 123);
962+
});
963+
964+
test('reduce collection into a single value with initial value', () => {
965+
const sum = coll.reduceRight((a, x) => a + x, 0);
966+
expect(sum).toStrictEqual(6);
967+
});
968+
969+
test('reduce collection into a single value without initial value', () => {
970+
const sum = coll.reduceRight<number>((a, x) => a + x);
971+
expect(sum).toStrictEqual(6);
972+
});
973+
974+
test('reduce empty collection without initial value', () => {
975+
const coll = createCollection();
976+
expect(() => coll.reduceRight((a: number, x) => a + x)).toThrowError(
977+
new TypeError('Reduce of empty collection with no initial value'),
978+
);
979+
});
980+
});

packages/collection/src/collection.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,64 @@ export class Collection<K, V> extends Map<K, V> {
274274
return undefined;
275275
}
276276

277+
/**
278+
* Searches for a last item where the given function returns a truthy value. This behaves like
279+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast | Array.findLast()}.
280+
*
281+
* @param fn - The function to test with (should return a boolean)
282+
* @param thisArg - Value to use as `this` when executing the function
283+
*/
284+
public findLast<V2 extends V>(fn: (value: V, key: K, collection: this) => value is V2): V2 | undefined;
285+
public findLast(fn: (value: V, key: K, collection: this) => unknown): V | undefined;
286+
public findLast<This, V2 extends V>(
287+
fn: (this: This, value: V, key: K, collection: this) => value is V2,
288+
thisArg: This,
289+
): V2 | undefined;
290+
public findLast<This>(fn: (this: This, value: V, key: K, collection: this) => unknown, thisArg: This): V | undefined;
291+
public findLast(fn: (value: V, key: K, collection: this) => unknown, thisArg?: unknown): V | undefined {
292+
if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`);
293+
if (thisArg !== undefined) fn = fn.bind(thisArg);
294+
const entries = [...this.entries()];
295+
for (let index = entries.length - 1; index >= 0; index--) {
296+
const val = entries[index]![1];
297+
const key = entries[index]![0];
298+
if (fn(val, key, this)) return val;
299+
}
300+
301+
return undefined;
302+
}
303+
304+
/**
305+
* Searches for the key of a last item where the given function returns a truthy value. This behaves like
306+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex | Array.findLastIndex()},
307+
* but returns the key rather than the positional index.
308+
*
309+
* @param fn - The function to test with (should return a boolean)
310+
* @param thisArg - Value to use as `this` when executing the function
311+
*/
312+
public findLastKey<K2 extends K>(fn: (value: V, key: K, collection: this) => key is K2): K2 | undefined;
313+
public findLastKey(fn: (value: V, key: K, collection: this) => unknown): K | undefined;
314+
public findLastKey<This, K2 extends K>(
315+
fn: (this: This, value: V, key: K, collection: this) => key is K2,
316+
thisArg: This,
317+
): K2 | undefined;
318+
public findLastKey<This>(
319+
fn: (this: This, value: V, key: K, collection: this) => unknown,
320+
thisArg: This,
321+
): K | undefined;
322+
public findLastKey(fn: (value: V, key: K, collection: this) => unknown, thisArg?: unknown): K | undefined {
323+
if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`);
324+
if (thisArg !== undefined) fn = fn.bind(thisArg);
325+
const entries = [...this.entries()];
326+
for (let index = entries.length - 1; index >= 0; index--) {
327+
const key = entries[index]![0];
328+
const val = entries[index]![1];
329+
if (fn(val, key, this)) return key;
330+
}
331+
332+
return undefined;
333+
}
334+
277335
/**
278336
* Removes items that satisfy the provided filter function.
279337
*
@@ -533,6 +591,37 @@ export class Collection<K, V> extends Map<K, V> {
533591
return accumulator;
534592
}
535593

594+
/**
595+
* Applies a function to produce a single value. Identical in behavior to
596+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight | Array.reduceRight()}.
597+
*
598+
* @param fn - Function used to reduce, taking four arguments; `accumulator`, `value`, `key`, and `collection`
599+
* @param initialValue - Starting value for the accumulator
600+
*/
601+
public reduceRight<T>(fn: (accumulator: T, value: V, key: K, collection: this) => T, initialValue?: T): T {
602+
if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`);
603+
const entries = [...this.entries()];
604+
let accumulator!: T;
605+
606+
let index: number;
607+
if (initialValue === undefined) {
608+
if (entries.length === 0) throw new TypeError('Reduce of empty collection with no initial value');
609+
accumulator = entries[entries.length - 1]![1] as unknown as T;
610+
index = entries.length - 1;
611+
} else {
612+
accumulator = initialValue;
613+
index = entries.length;
614+
}
615+
616+
while (--index >= 0) {
617+
const key = entries[index]![0];
618+
const val = entries[index]![1];
619+
accumulator = fn(accumulator, val, key, this);
620+
}
621+
622+
return accumulator;
623+
}
624+
536625
/**
537626
* Identical to
538627
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach | Map.forEach()},

packages/core/src/api/guild.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ import {
9292
type RESTPostAPIGuildsMFAResult,
9393
type RESTPostAPIGuildsResult,
9494
type RESTPutAPIGuildBanJSONBody,
95+
type RESTPutAPIGuildMemberJSONBody,
96+
type RESTPutAPIGuildMemberResult,
9597
type RESTPutAPIGuildOnboardingJSONBody,
9698
type RESTPutAPIGuildOnboardingResult,
9799
type RESTPutAPIGuildTemplateSyncResult,
@@ -167,6 +169,27 @@ export class GuildsAPI {
167169
await this.rest.delete(Routes.guild(guildId), { reason, signal });
168170
}
169171

172+
/**
173+
* Adds user to the guild
174+
*
175+
* @see {@link https://discord.com/developers/docs/resources/guild#add-guild-member}
176+
* @param guildId - The id of the guild to add the user to
177+
* @param userId - The id of the user to add
178+
* @param body - The data for adding users to the guild
179+
* @param options - The options for adding users to the guild
180+
*/
181+
public async addMember(
182+
guildId: Snowflake,
183+
userId: Snowflake,
184+
body: RESTPutAPIGuildMemberJSONBody,
185+
{ signal }: Pick<RequestData, 'signal'> = {},
186+
) {
187+
return this.rest.put(Routes.guildMember(guildId, userId), {
188+
body,
189+
signal,
190+
}) as Promise<RESTPutAPIGuildMemberResult>;
191+
}
192+
170193
/**
171194
* Fetches all the members of a guild
172195
*

packages/discord.js/src/client/BaseClient.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const EventEmitter = require('node:events');
44
const { REST } = require('@discordjs/rest');
5+
const { Routes } = require('discord-api-types/v10');
56
const { DiscordjsTypeError, ErrorCodes } = require('../errors');
67
const Options = require('../util/Options');
78
const { mergeDefault, flatten } = require('../util/Util');
@@ -48,6 +49,23 @@ class BaseClient extends EventEmitter {
4849
this.rest.clearHandlerSweeper();
4950
}
5051

52+
/**
53+
* Options used for deleting a webhook.
54+
* @typedef {Object} WebhookDeleteOptions
55+
* @property {string} [token] Token of the webhook
56+
* @property {string} [reason] The reason for deleting the webhook
57+
*/
58+
59+
/**
60+
* Deletes a webhook.
61+
* @param {Snowflake} id The webhook's id
62+
* @param {WebhookDeleteOptions} [options] Options for deleting the webhook
63+
* @returns {Promise<void>}
64+
*/
65+
async deleteWebhook(id, { token, reason } = {}) {
66+
await this.rest.delete(Routes.webhook(id, token), { auth: !token, reason });
67+
}
68+
5169
/**
5270
* Increments max listeners by one, if they are not zero.
5371
* @private

packages/discord.js/src/client/Client.js

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -255,23 +255,6 @@ class Client extends BaseClient {
255255
this.rest.setToken(null);
256256
}
257257

258-
/**
259-
* Options used for deleting a webhook.
260-
* @typedef {Object} WebhookDeleteOptions
261-
* @property {string} [token] Token of the webhook
262-
* @property {string} [reason] The reason for deleting the webhook
263-
*/
264-
265-
/**
266-
* Deletes a webhook.
267-
* @param {Snowflake} id The webhook's id
268-
* @param {WebhookDeleteOptions} [options] Options for deleting the webhook
269-
* @returns {Promise<void>}
270-
*/
271-
async deleteWebhook(id, { token, reason } = {}) {
272-
await this.rest.delete(Routes.webhook(id, token), { auth: !token, reason });
273-
}
274-
275258
/**
276259
* Options used when fetching an invite from Discord.
277260
* @typedef {Object} ClientFetchInviteOptions

packages/discord.js/src/managers/GuildManager.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { Collection } = require('@discordjs/collection');
66
const { makeURLSearchParams } = require('@discordjs/rest');
77
const { Routes, RouteBases } = require('discord-api-types/v10');
88
const CachedManager = require('./CachedManager');
9+
const ShardClientUtil = require('../sharding/ShardClientUtil');
910
const { Guild } = require('../structures/Guild');
1011
const GuildChannel = require('../structures/GuildChannel');
1112
const GuildEmoji = require('../structures/GuildEmoji');
@@ -272,6 +273,7 @@ class GuildManager extends CachedManager {
272273
const data = await this.client.rest.get(Routes.guild(id), {
273274
query: makeURLSearchParams({ with_counts: options.withCounts ?? true }),
274275
});
276+
data.shardId = ShardClientUtil.shardIdForGuildId(id, this.client.options.shardCount);
275277
return this._add(data, options.cache);
276278
}
277279

packages/discord.js/src/structures/BaseGuildEmoji.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,23 @@ class BaseGuildEmoji extends Emoji {
5353
}
5454
}
5555

56+
/**
57+
* Returns a URL for the emoji.
58+
* @method imageURL
59+
* @memberof BaseGuildEmoji
60+
* @instance
61+
* @param {BaseImageURLOptions} [options] Options for the image URL
62+
* @returns {string}
63+
*/
64+
65+
/**
66+
* Returns a URL for the emoji.
67+
* @name url
68+
* @memberof BaseGuildEmoji
69+
* @instance
70+
* @type {string}
71+
* @readonly
72+
* @deprecated Use {@link BaseGuildEmoji#imageURL} instead.
73+
*/
74+
5675
module.exports = BaseGuildEmoji;

packages/discord.js/src/structures/Emoji.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
'use strict';
22

3+
const process = require('node:process');
34
const { DiscordSnowflake } = require('@sapphire/snowflake');
45
const Base = require('./Base');
56

7+
let deprecationEmittedForURL = false;
8+
69
/**
710
* Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}.
811
* @extends {Base}
@@ -40,12 +43,27 @@ class Emoji extends Base {
4043
}
4144

4245
/**
43-
* The URL to the emoji file if it's a custom emoji
46+
* Returns a URL for the emoji or `null` if this is not a custom emoji.
47+
* @param {BaseImageURLOptions} [options] Options for the image URL
48+
* @returns {?string}
49+
*/
50+
imageURL(options) {
51+
return this.id && this.client.rest.cdn.emoji(this.id, options);
52+
}
53+
54+
/**
55+
* Returns a URL for the emoji or `null` if this is not a custom emoji.
4456
* @type {?string}
4557
* @readonly
58+
* @deprecated Use {@link Emoji#imageURL} instead.
4659
*/
4760
get url() {
48-
return this.id && this.client.rest.cdn.emoji(this.id, this.animated ? 'gif' : 'png');
61+
if (!deprecationEmittedForURL) {
62+
process.emitWarning('The Emoji#url getter is deprecated. Use Emoji#imageURL() instead.', 'DeprecationWarning');
63+
deprecationEmittedForURL = true;
64+
}
65+
66+
return this.imageURL({ extension: this.animated ? 'gif' : 'png' });
4967
}
5068

5169
/**

packages/discord.js/src/structures/Message.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class Message extends Base {
148148

149149
if ('components' in data) {
150150
/**
151-
* An array of of action rows in the message.
151+
* An array of action rows in the message.
152152
* <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
153153
* in a guild for messages that do not mention the client.</info>
154154
* @type {ActionRow[]}

0 commit comments

Comments
 (0)