djs-selfbot-v13 3.7.1 → 3.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ 'use strict';
2
+
3
+ const { Collection } = require('@discordjs/collection');
4
+ const BaseManager = require('./BaseManager');
5
+
6
+ /**
7
+ * Represents a single quest
8
+ */
9
+ class Quest {
10
+ constructor(data) {
11
+ this.id = data.id;
12
+ this.config = data.config;
13
+ this.userStatus = data.user_status;
14
+ }
15
+
16
+ /**
17
+ * Check if quest is expired
18
+ * @param {Date} [date=new Date()] Date to check against
19
+ * @returns {boolean}
20
+ */
21
+ isExpired(date = new Date()) {
22
+ if (!this.config.expires_at) return false;
23
+ return new Date(this.config.expires_at) < date;
24
+ }
25
+
26
+ /**
27
+ * Check if quest is completed
28
+ * @returns {boolean}
29
+ */
30
+ isCompleted() {
31
+ return this.userStatus?.completed_at != null;
32
+ }
33
+
34
+ /**
35
+ * Check if quest rewards have been claimed
36
+ * @returns {boolean}
37
+ */
38
+ hasClaimedRewards() {
39
+ return this.userStatus?.claimed_at != null;
40
+ }
41
+
42
+ /**
43
+ * Check if user is enrolled in quest
44
+ * @returns {boolean}
45
+ */
46
+ isEnrolledQuest() {
47
+ return this.userStatus?.enrolled_at != null;
48
+ }
49
+
50
+ /**
51
+ * Update user status for this quest
52
+ * @param {Object} status New status data
53
+ */
54
+ updateUserStatus(status) {
55
+ this.userStatus = { ...this.userStatus, ...status };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Manages API methods for Discord quests
61
+ * @extends {BaseManager}
62
+ */
63
+ class QuestManager extends BaseManager {
64
+ constructor(client) {
65
+ super(client);
66
+
67
+ /**
68
+ * Collection of cached quests
69
+ * @type {Collection<string, Quest>}
70
+ */
71
+ this.cache = new Collection();
72
+ }
73
+
74
+ /**
75
+ * Get all available quests for the user
76
+ * @returns {Promise<Object>} Quest data
77
+ */
78
+ async get() {
79
+ const data = await this.client.api.users('@me').quests.get();
80
+
81
+ // Cache quests
82
+ if (data.quests) {
83
+ this.cache.clear();
84
+ data.quests.forEach(questData => {
85
+ const quest = new Quest(questData);
86
+ this.cache.set(quest.id, quest);
87
+ });
88
+ }
89
+
90
+ return data;
91
+ }
92
+
93
+ /**
94
+ * Get user's orb balance (virtual currency)
95
+ * @returns {Promise<Object>} Balance data
96
+ */
97
+ async orbs() {
98
+ const data = await this.client.api.users['@me']['virtual-currency'].balance.get();
99
+ return data;
100
+ }
101
+
102
+ /**
103
+ * Get quest by ID from cache
104
+ * @param {string} id Quest ID
105
+ * @returns {Quest|undefined}
106
+ */
107
+ getQuest(id) {
108
+ return this.cache.get(id);
109
+ }
110
+
111
+ /**
112
+ * Get all cached quests as array
113
+ * @returns {Quest[]}
114
+ */
115
+ list() {
116
+ return Array.from(this.cache.values());
117
+ }
118
+
119
+ /**
120
+ * Get expired quests
121
+ * @param {Date} [date=new Date()] Date to check against
122
+ * @returns {Quest[]}
123
+ */
124
+ getExpired(date = new Date()) {
125
+ return this.list().filter(quest => quest.isExpired(date));
126
+ }
127
+
128
+ /**
129
+ * Get completed quests
130
+ * @returns {Quest[]}
131
+ */
132
+ getCompleted() {
133
+ return this.list().filter(quest => quest.isCompleted());
134
+ }
135
+
136
+ /**
137
+ * Get claimable quests (completed but not claimed)
138
+ * @returns {Quest[]}
139
+ */
140
+ getClaimable() {
141
+ return this.list().filter(quest => quest.isCompleted() && !quest.hasClaimedRewards());
142
+ }
143
+
144
+ /**
145
+ * Get valid quests (not completed, not expired, not blacklisted)
146
+ * @returns {Quest[]}
147
+ */
148
+ filterQuestsValid() {
149
+ return this.list().filter(quest =>
150
+ quest.id !== '1412491570820812933' &&
151
+ !quest.isCompleted() &&
152
+ !quest.isExpired()
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Check if quest exists in cache
158
+ * @param {string} id Quest ID
159
+ * @returns {boolean}
160
+ */
161
+ hasQuest(id) {
162
+ return this.cache.has(id);
163
+ }
164
+
165
+ /**
166
+ * Get application data for given IDs
167
+ * @param {string[]} ids Application IDs
168
+ * @returns {Promise<Object[]>}
169
+ */
170
+ async getApplicationData(ids) {
171
+ const query = new URLSearchParams();
172
+ ids.forEach(id => query.append('application_ids', id));
173
+
174
+ return this.client.api.applications.public.get({ query: query.toString() });
175
+ }
176
+
177
+ /**
178
+ * Enroll in a specific quest
179
+ * @param {string} questId The quest ID to enroll in
180
+ * @param {Object} [options] Enrollment options
181
+ * @param {number} [options.location=11] Location parameter
182
+ * @param {boolean} [options.isTargeted=false] Whether the quest is targeted
183
+ * @param {*} [options.metadataRaw=null] Raw metadata
184
+ * @returns {Promise<Quest|undefined>} Updated quest or undefined
185
+ */
186
+ async acceptQuest(questId, options = {}) {
187
+ const { location = 11, isTargeted = false, metadataRaw = null } = options;
188
+
189
+ const data = await this.client.api.quests(questId).enroll.post({
190
+ data: {
191
+ location,
192
+ is_targeted: isTargeted,
193
+ metadata_raw: metadataRaw
194
+ }
195
+ });
196
+
197
+ const quest = this.getQuest(questId);
198
+ if (quest) {
199
+ quest.updateUserStatus(data);
200
+ }
201
+
202
+ return quest;
203
+ }
204
+
205
+ /**
206
+ * Update progress for a video quest
207
+ * @param {string} questId The quest ID
208
+ * @param {number} timestamp Current progress timestamp
209
+ * @returns {Promise<Object>} Progress update result
210
+ */
211
+ async videoProgress(questId, timestamp) {
212
+ const data = await this.client.api.quests(questId)['video-progress'].post({
213
+ data: { timestamp }
214
+ });
215
+ return data;
216
+ }
217
+
218
+ /**
219
+ * Send heartbeat for desktop quests
220
+ * @param {string} questId The quest ID
221
+ * @param {string} applicationId Application ID
222
+ * @param {boolean} [terminal=false] Whether this is a terminal heartbeat
223
+ * @returns {Promise<Object>} Heartbeat result
224
+ */
225
+ async heartbeat(questId, applicationId, terminal = false) {
226
+ const data = await this.client.api.quests(questId).heartbeat.post({
227
+ data: {
228
+ application_id: applicationId,
229
+ terminal
230
+ }
231
+ });
232
+ return data;
233
+ }
234
+
235
+ /**
236
+ * Helper function for timeout
237
+ * @param {number} ms Milliseconds to wait
238
+ * @returns {Promise<void>}
239
+ * @private
240
+ */
241
+ async timeout(ms) {
242
+ return new Promise(resolve => setTimeout(resolve, ms));
243
+ }
244
+
245
+ /**
246
+ * Complete a quest automatically
247
+ * @param {Quest} quest Quest to complete
248
+ * @returns {Promise<void>}
249
+ */
250
+ async doingQuest(quest) {
251
+ const questName = quest.config.messages?.quest_name || 'Unknown Quest';
252
+
253
+ if (!quest.isEnrolledQuest()) {
254
+ console.log(`Enrolling in quest "${questName}"...`);
255
+ await this.acceptQuest(quest.id);
256
+ }
257
+
258
+ const applicationName = quest.config.application?.name || 'Unknown App';
259
+ const taskConfig = quest.config.task_config;
260
+
261
+ const taskName = [
262
+ 'WATCH_VIDEO',
263
+ 'PLAY_ON_DESKTOP',
264
+ 'STREAM_ON_DESKTOP',
265
+ 'PLAY_ACTIVITY',
266
+ 'WATCH_VIDEO_ON_MOBILE'
267
+ ].find(x => taskConfig.tasks?.[x] != null);
268
+
269
+ if (!taskName) {
270
+ console.log(`Unknown task type for quest "${questName}"`);
271
+ return;
272
+ }
273
+
274
+ const secondsNeeded = taskConfig.tasks[taskName].target;
275
+ let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
276
+
277
+ if (taskName === 'WATCH_VIDEO' || taskName === 'WATCH_VIDEO_ON_MOBILE') {
278
+ const maxFuture = 10;
279
+ const speed = 7;
280
+ const interval = 1;
281
+ const enrolledAt = new Date(quest.userStatus?.enrolled_at).getTime();
282
+ let completed = false;
283
+
284
+ console.log(`Spoofing video for ${questName}.`);
285
+
286
+ while (true) {
287
+ const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
288
+ const diff = maxAllowed - secondsDone;
289
+ const timestamp = secondsDone + speed;
290
+
291
+ if (diff >= speed) {
292
+ const res = await this.videoProgress(quest.id, Math.min(secondsNeeded, timestamp + Math.random()));
293
+ completed = res.completed_at != null;
294
+ secondsDone = Math.min(secondsNeeded, timestamp);
295
+ }
296
+
297
+ if (timestamp >= secondsNeeded) {
298
+ break;
299
+ }
300
+
301
+ await this.timeout(interval * 1000);
302
+ }
303
+
304
+ if (!completed) {
305
+ await this.videoProgress(quest.id, secondsNeeded);
306
+ }
307
+
308
+ console.log(`Quest "${questName}" completed!`);
309
+
310
+ } else if (taskName === 'PLAY_ON_DESKTOP') {
311
+ const interval = 60;
312
+
313
+ while (!quest.isCompleted()) {
314
+ const secondsDone = quest.userStatus?.progress?.[taskName]?.value || 0;
315
+ const res = await this.heartbeat(quest.id, quest.config.application.id, false);
316
+ quest.updateUserStatus(res);
317
+
318
+ console.log(`Spoofed your game to ${applicationName}. Wait for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`);
319
+ await this.timeout(interval * 1000);
320
+ }
321
+
322
+ const res = await this.heartbeat(quest.id, quest.config.application.id, true);
323
+ quest.updateUserStatus(res);
324
+ console.log(`Quest "${questName}" completed!`);
325
+
326
+ } else if (taskName === 'STREAM_ON_DESKTOP') {
327
+ console.log('This no longer works in node for non-video quests. Use the discord desktop app to complete the', questName, 'quest!');
328
+ } else if (taskName === 'PLAY_ACTIVITY') {
329
+ console.log('This quest not supported. Use the discord desktop app to complete the', questName, 'quest!');
330
+ } else {
331
+ console.log('Unknown quest type. Use the discord desktop app to complete the', questName, 'quest!');
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Auto-complete all valid quests
337
+ * @returns {Promise<void>}
338
+ */
339
+ async autoCompleteAll() {
340
+ await this.get(); // Refresh quest data
341
+ const validQuests = this.filterQuestsValid();
342
+
343
+ for (const quest of validQuests) {
344
+ try {
345
+ await this.doingQuest(quest);
346
+ } catch (error) {
347
+ console.error(`Failed to complete quest ${quest.id}:`, error);
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Get cache size
354
+ * @returns {number}
355
+ */
356
+ get size() {
357
+ return this.cache.size;
358
+ }
359
+
360
+ /**
361
+ * Clear quest cache
362
+ */
363
+ clear() {
364
+ this.cache.clear();
365
+ }
366
+
367
+ /**
368
+ * Make QuestManager iterable
369
+ * @returns {IterableIterator<Quest>}
370
+ */
371
+ [Symbol.iterator]() {
372
+ return this.cache.values();
373
+ }
374
+ }
375
+
376
+ module.exports = QuestManager;
@@ -161,7 +161,7 @@ class ThreadManager extends CachedManager {
161
161
  // Discord sends the thread id as id in this object
162
162
  for (const rawMember of rawThreads.members) client.channels.cache.get(rawMember.id)?.members._add(rawMember);
163
163
  // Patch firstMessage
164
- // According to https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues/1502, rawThreads.first_messages could be null.
164
+ // According to https://github.com/aiko-chan-ai/djs-selfbot-v13/issues/1502, rawThreads.first_messages could be null.
165
165
  for (const rawMessage of rawThreads?.first_messages || []) {
166
166
  client.channels.cache.get(rawMessage.id)?.messages._add(rawMessage);
167
167
  }
@@ -203,7 +203,7 @@ class ClientUser extends User {
203
203
  * @example
204
204
  * // Set the client user's presence
205
205
  * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' });
206
- * @see {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/RichPresence.md}
206
+ * @see {@link https://github.com/aiko-chan-ai/djs-selfbot-v13/blob/main/Document/RichPresence.md}
207
207
  */
208
208
  setPresence(data) {
209
209
  return this.client.presence.set(data);
@@ -248,7 +248,7 @@ class ClientUser extends User {
248
248
  * @example
249
249
  * // Set the client user's activity
250
250
  * client.user.setActivity('discord.js', { type: 'WATCHING' });
251
- * @see {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/RichPresence.md}
251
+ * @see {@link https://github.com/aiko-chan-ai/djs-selfbot-v13/blob/main/Document/RichPresence.md}
252
252
  */
253
253
  setActivity(name, options = {}) {
254
254
  if (!name) return this.setPresence({ activities: [], shardId: options.shardId });
@@ -447,12 +447,186 @@ class ClientUser extends User {
447
447
  }
448
448
 
449
449
  /**
450
- * Set pronouns
451
- * @param {?string} pronouns Your pronouns
450
+ * Add a widget to the user's profile
451
+ * @param {string} type Widget type (favorite_games, current_games, played_games, want_to_play_games)
452
+ * @param {string} gameId The game ID to add
453
+ * @param {string} [comment] Optional comment for the game
454
+ * @param {string[]} [tags] Optional tags for the game
455
+ * @returns {Promise<Object>}
456
+ */
457
+ async addWidget(type, gameId, comment = null, tags = []) {
458
+ if (!type || !gameId) {
459
+ throw new TypeError('Widget type and game ID are required');
460
+ }
461
+
462
+ const validTypes = ['favorite_games', 'current_games', 'played_games', 'want_to_play_games'];
463
+ if (!validTypes.includes(type)) {
464
+ throw new TypeError(`Invalid widget type. Must be one of: ${validTypes.join(', ')}`);
465
+ }
466
+
467
+ // Get current widgets first
468
+ const currentWidgets = await this.widgetsList();
469
+
470
+ // Find existing widget of this type or create new one
471
+ let targetWidget = currentWidgets.widgets.find(w => w.data.type === type);
472
+
473
+ if (!targetWidget) {
474
+ // Create new widget if it doesn't exist
475
+ targetWidget = {
476
+ id: Date.now().toString(), // Generate temporary ID
477
+ data: {
478
+ type: type,
479
+ games: []
480
+ }
481
+ };
482
+ currentWidgets.widgets.push(targetWidget);
483
+ }
484
+
485
+ // Add the game if it doesn't already exist
486
+ const existingGame = targetWidget.data.games.find(g => g.game_id === gameId);
487
+ if (!existingGame) {
488
+ const gameData = { game_id: gameId };
489
+ if (comment !== null) gameData.comment = comment;
490
+ if (tags.length > 0) gameData.tags = tags;
491
+
492
+ targetWidget.data.games.push(gameData);
493
+ }
494
+
495
+ // Update widgets via API
496
+ return this.client.api.users['@me'].profile.patch({
497
+ data: { widgets: currentWidgets.widgets }
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Delete a widget or remove a game from a widget
503
+ * @param {string} type Widget type to modify
504
+ * @param {string} [gameId] Optional game ID to remove (if not provided, removes entire widget)
505
+ * @returns {Promise<Object>}
506
+ */
507
+ async delWidget(type, gameId = null) {
508
+ if (!type) {
509
+ throw new TypeError('Widget type is required');
510
+ }
511
+
512
+ const validTypes = ['favorite_games', 'current_games', 'played_games', 'want_to_play_games'];
513
+ if (!validTypes.includes(type)) {
514
+ throw new TypeError(`Invalid widget type. Must be one of: ${validTypes.join(', ')}`);
515
+ }
516
+
517
+ // Get current widgets
518
+ const currentWidgets = await this.widgetsList();
519
+
520
+ if (gameId) {
521
+ // Remove specific game from widget
522
+ const targetWidget = currentWidgets.widgets.find(w => w.data.type === type);
523
+ if (targetWidget) {
524
+ targetWidget.data.games = targetWidget.data.games.filter(g => g.game_id !== gameId);
525
+ }
526
+ } else {
527
+ // Remove entire widget
528
+ currentWidgets.widgets = currentWidgets.widgets.filter(w => w.data.type !== type);
529
+ }
530
+
531
+ // Update widgets via API
532
+ return this.client.api.users['@me'].profile.patch({
533
+ data: { widgets: currentWidgets.widgets }
534
+ });
535
+ }
536
+
537
+ /**
538
+ * Get the list of all widgets for the user
539
+ * @returns {Promise<Object>} Object containing widgets array
540
+ */
541
+ async widgetsList() {
542
+ try {
543
+ const data = await this.client.api.users['@me'].profile.get();
544
+ return data.widgets ? { widgets: data.widgets } : { widgets: [] };
545
+ } catch (error) {
546
+ // If profile endpoint doesn't exist or fails, return empty widgets
547
+ return { widgets: [] };
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Set display name style with font, effect, and colors
553
+ * @param {string|number} fontName Font name or ID
554
+ * @param {string|number} effectName Effect name or ID
555
+ * @param {number|string} color1 Primary color (hex or decimal)
556
+ * @param {number|string} [color2] Secondary color for gradient effects (hex or decimal)
452
557
  * @returns {Promise<ClientUser>}
558
+ * @example
559
+ * // Set Sans font with gradient effect
560
+ * client.user.setNameStyle('Sans', 'Gradient', 7183099, 6082490);
561
+ * // Set Tempo font with solid effect
562
+ * client.user.setNameStyle('Tempo', 'Solid', 7183099);
563
+ * // Using IDs directly
564
+ * client.user.setNameStyle(11, 2, 7183099, 6082490);
453
565
  */
454
- setPronouns(pronouns = '') {
455
- return this.edit({ pronouns });
566
+ async setNameStyle(fontName, effectName, color1, color2 = null) {
567
+ // Font name/ID mapping
568
+ const fontMap = {
569
+ 'Sans': 11,
570
+ 'Tempo': 12,
571
+ 'Sakura': 3,
572
+ 'JellyBean': 4,
573
+ 'Modern': 6,
574
+ 'Medieval': 7,
575
+ '8Bit': 8,
576
+ 'Vampire': 10
577
+ };
578
+
579
+ // Effect name/ID mapping
580
+ const effectMap = {
581
+ 'Solid': 1,
582
+ 'Gradient': 2,
583
+ 'Neon': 3,
584
+ 'Toon': 4,
585
+ 'Pop': 5
586
+ };
587
+
588
+ // Resolve font ID
589
+ let fontId = typeof fontName === 'string' ? fontMap[fontName] : fontName;
590
+ if (!fontId) {
591
+ throw new TypeError(`Invalid font name. Must be one of: ${Object.keys(fontMap).join(', ')} or a valid font ID`);
592
+ }
593
+
594
+ // Resolve effect ID
595
+ let effectId = typeof effectName === 'string' ? effectMap[effectName] : effectName;
596
+ if (!effectId) {
597
+ throw new TypeError(`Invalid effect name. Must be one of: ${Object.keys(effectMap).join(', ')} or a valid effect ID`);
598
+ }
599
+
600
+ // Resolve colors
601
+ const resolveColor = (color) => {
602
+ if (typeof color === 'string') {
603
+ // Handle hex colors
604
+ if (color.startsWith('#')) {
605
+ return parseInt(color.slice(1), 16);
606
+ }
607
+ return parseInt(color, 16);
608
+ }
609
+ return color;
610
+ };
611
+
612
+ const primaryColor = resolveColor(color1);
613
+ const colors = [primaryColor];
614
+
615
+ if (color2 !== null) {
616
+ const secondaryColor = resolveColor(color2);
617
+ colors.push(secondaryColor);
618
+ }
619
+
620
+ // Build the data object
621
+ const data = {
622
+ display_name_font_id: fontId,
623
+ display_name_effect_id: effectId,
624
+ display_name_colors: colors
625
+ };
626
+
627
+ // Send PATCH request to Discord API
628
+ await this.client.api.users('@me').patch({ data });
629
+ return this;
456
630
  }
457
631
 
458
632
  /**
@@ -1605,6 +1605,60 @@ class Guild extends AnonymousGuild {
1605
1605
  };
1606
1606
  }
1607
1607
 
1608
+ /**
1609
+ * Mute this guild
1610
+ * @param {GuildMuteOptions} [options] Options for muting the guild
1611
+ * @returns {Promise<Object>} The updated guild settings
1612
+ * @example
1613
+ * // Mute the guild with default settings
1614
+ * guild.mute();
1615
+ * @example
1616
+ * // Mute the guild with custom options
1617
+ * guild.mute({
1618
+ * muted: true,
1619
+ * suppressRoles: false,
1620
+ * suppressEveryone: true,
1621
+ * muteScheduledEvents: false
1622
+ * });
1623
+ */
1624
+ async mute(options = {}) {
1625
+ const {
1626
+ muted = true,
1627
+ suppressRoles = true,
1628
+ suppressEveryone = true,
1629
+ muteScheduledEvents = true
1630
+ } = options;
1631
+
1632
+ const data = await this.client.api.users('@me').guilds(this.id).settings.patch({
1633
+ data: {
1634
+ muted,
1635
+ suppress_roles: suppressRoles,
1636
+ suppress_everyone: suppressEveryone,
1637
+ mute_scheduled_events: muteScheduledEvents
1638
+ }
1639
+ });
1640
+ return data;
1641
+ }
1642
+
1643
+ /**
1644
+ * Unmute this guild
1645
+ * @returns {Promise<Object>} The updated guild settings
1646
+ * @example
1647
+ * // Unmute the guild
1648
+ * guild.unmute();
1649
+ */
1650
+ async unmute() {
1651
+ const data = await this.client.api.users('@me').guilds(this.id).settings.patch({
1652
+ data: {
1653
+ muted: false,
1654
+ suppress_roles: false,
1655
+ suppress_everyone: false,
1656
+ mute_scheduled_events: false
1657
+ }
1658
+ });
1659
+ return data;
1660
+ }
1661
+
1608
1662
  /**
1609
1663
  * Creates a collection of this guild's roles, sorted by their position and ids.
1610
1664
  * @returns {Collection<Snowflake, Role>}
@@ -800,6 +800,22 @@ class RichPresence extends Activity {
800
800
  return this;
801
801
  }
802
802
 
803
+ /**
804
+ * Set the URL of the state of the activity
805
+ * @param {?string} url The url of the state
806
+ * @returns {RichPresence}
807
+ */
808
+ setStateURL(url){
809
+ if (!url)
810
+ throw new Error('Detail URL must be a url');
811
+
812
+ if (typeof url !== 'string') throw new Error('Detail URL must be a url');
813
+ if (!URL.canParse(url)) throw new Error('Detail URL must be a valid url');
814
+
815
+ this.state_url = url;
816
+ return this;
817
+ }
818
+
803
819
  /**
804
820
  * Set the details of the activity
805
821
  * @param {?string} details The details of the activity
@@ -810,6 +826,22 @@ class RichPresence extends Activity {
810
826
  return this;
811
827
  }
812
828
 
829
+ /**
830
+ * Set the URL of the details of the activity
831
+ * @param {?string} url The url of the details
832
+ * @returns {RichPresence}
833
+ */
834
+ setDetailsURL(url){
835
+ if (!url)
836
+ throw new Error('Detail URL must be a url');
837
+
838
+ if (typeof url !== 'string') throw new Error('Detail URL must be a url');
839
+ if (!URL.canParse(url)) throw new Error('Detail URL must be a valid url');
840
+
841
+ this.details_url = url;
842
+ return this;
843
+ }
844
+
813
845
  /**
814
846
  * @typedef {Object} RichParty
815
847
  * @property {string} id The id of the party