djs-selfbot-v13 3.7.29 → 3.7.32

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.
@@ -1,8 +1,21 @@
1
1
  'use strict';
2
2
 
3
+ const { randomUUID } = require('node:crypto');
4
+ const { fetch } = require('undici');
3
5
  const { Collection } = require('@discordjs/collection');
4
6
  const BaseManager = require('./BaseManager');
5
7
 
8
+ const TASK_TYPES = [
9
+ 'WATCH_VIDEO',
10
+ 'PLAY_ON_DESKTOP',
11
+ 'PLAY_ON_XBOX',
12
+ 'PLAY_ON_PLAYSTATION',
13
+ 'STREAM_ON_DESKTOP',
14
+ 'PLAY_ACTIVITY',
15
+ 'WATCH_VIDEO_ON_MOBILE',
16
+ 'ACHIEVEMENT_IN_ACTIVITY',
17
+ ];
18
+
6
19
  /**
7
20
  * Represents a single quest
8
21
  */
@@ -11,6 +24,15 @@ class Quest {
11
24
  this.id = data.id;
12
25
  this.config = data.config;
13
26
  this.userStatus = data.user_status;
27
+ this._raw = data;
28
+ }
29
+
30
+ /**
31
+ * Raw quest data from the API
32
+ * @returns {Object}
33
+ */
34
+ get raw() {
35
+ return this._raw;
14
36
  }
15
37
 
16
38
  /**
@@ -52,7 +74,24 @@ class Quest {
52
74
  * @param {Object} status New status data
53
75
  */
54
76
  updateUserStatus(status) {
55
- this.userStatus = { ...this.userStatus, ...status };
77
+ if (!status) return;
78
+
79
+ if (status.user_status) {
80
+ this.updateUserStatus(status.user_status);
81
+ return;
82
+ }
83
+
84
+ const previous = this.userStatus ?? {};
85
+ this.userStatus = { ...previous, ...status };
86
+
87
+ if (status.progress) {
88
+ this.userStatus.progress = { ...previous.progress };
89
+ for (const [task, data] of Object.entries(status.progress)) {
90
+ this.userStatus.progress[task] = { ...previous.progress?.[task], ...data };
91
+ }
92
+ }
93
+
94
+ this._raw.user_status = this.userStatus;
56
95
  }
57
96
  }
58
97
 
@@ -63,7 +102,7 @@ class Quest {
63
102
  class QuestManager extends BaseManager {
64
103
  constructor(client) {
65
104
  super(client);
66
-
105
+
67
106
  /**
68
107
  * Collection of cached quests
69
108
  * @type {Collection<string, Quest>}
@@ -71,14 +110,118 @@ class QuestManager extends BaseManager {
71
110
  this.cache = new Collection();
72
111
  }
73
112
 
113
+ /**
114
+ * Get task configuration (prefers v2)
115
+ * @param {Quest} quest Quest instance
116
+ * @returns {Object|undefined}
117
+ * @private
118
+ */
119
+ _getTaskConfig(quest) {
120
+ return quest.config.task_config_v2 ?? quest.config.task_config;
121
+ }
122
+
123
+ /**
124
+ * Find the active task type for a quest
125
+ * @param {Object} taskConfig Task configuration
126
+ * @returns {string|null}
127
+ * @private
128
+ */
129
+ _findTaskName(taskConfig) {
130
+ if (!taskConfig?.tasks) return null;
131
+ return TASK_TYPES.find(taskName => taskConfig.tasks[taskName] != null) ?? null;
132
+ }
133
+
134
+ /**
135
+ * Check if a quest task has reached its target
136
+ * @param {Quest} quest Quest instance
137
+ * @param {string} taskName Task type
138
+ * @param {number} target Target value in seconds
139
+ * @returns {boolean}
140
+ * @private
141
+ */
142
+ _isTaskComplete(quest, taskName, target) {
143
+ if (quest.isCompleted()) return true;
144
+ const value = quest.userStatus?.progress?.[taskName]?.value ?? 0;
145
+ return value >= target;
146
+ }
147
+
148
+ /**
149
+ * Resolve a stream key for PLAY_ACTIVITY quests
150
+ * @returns {string|null}
151
+ * @private
152
+ */
153
+ _getActivityStreamKey() {
154
+ const { client } = this;
155
+
156
+ const dmChannel = client.channels.cache.find(c => c.type === 'DM' || c.type === 'GROUP_DM');
157
+ if (dmChannel) {
158
+ return `call:${dmChannel.id}:${client.user.id}`;
159
+ }
160
+
161
+ for (const guild of client.guilds.cache.values()) {
162
+ const voiceChannel = guild.channels.cache.find(
163
+ c => c.type === 'GUILD_VOICE' || c.type === 'GUILD_STAGE_VOICE',
164
+ );
165
+ if (voiceChannel) {
166
+ return `guild:${guild.id}:${voiceChannel.id}:${client.user.id}`;
167
+ }
168
+ }
169
+
170
+ return null;
171
+ }
172
+
173
+ /**
174
+ * Check if quest requires Android enrollment
175
+ * @param {Quest} quest Quest instance
176
+ * @returns {boolean}
177
+ * @private
178
+ */
179
+ _isAndroidQuest(quest) {
180
+ const taskConfig = this._getTaskConfig(quest);
181
+ return Boolean(taskConfig?.tasks?.WATCH_VIDEO_ON_MOBILE) && !Boolean(taskConfig?.tasks?.WATCH_VIDEO);
182
+ }
183
+
184
+ /**
185
+ * Build Android-specific request headers
186
+ * @returns {Object}
187
+ * @private
188
+ */
189
+ _getAndroidHeaders() {
190
+ const androidProperties = {
191
+ os: 'Android',
192
+ browser: 'Discord Android',
193
+ device: 'b0q',
194
+ system_locale: 'en-US',
195
+ has_client_mods: false,
196
+ client_version: '316.11 - rn',
197
+ release_channel: 'googleRelease',
198
+ device_vendor_id: randomUUID(),
199
+ design_id: 2,
200
+ browser_user_agent: '',
201
+ browser_version: '',
202
+ os_version: '28',
203
+ client_build_number: 5169,
204
+ client_event_source: null,
205
+ client_launch_id: randomUUID(),
206
+ launch_signature: Date.now().toString(),
207
+ client_app_state: 'active',
208
+ client_heartbeat_session_id: randomUUID(),
209
+ };
210
+
211
+ return {
212
+ 'User-Agent': 'Discord-Android/316011;RNA',
213
+ 'x-super-properties': Buffer.from(JSON.stringify(androidProperties)).toString('base64'),
214
+ 'sec-ch-ua-mobile': '?1',
215
+ };
216
+ }
217
+
74
218
  /**
75
219
  * Get all available quests for the user
76
220
  * @returns {Promise<Object>} Quest data
77
221
  */
78
222
  async get() {
79
223
  const data = await this.client.api.quests('@me').get();
80
-
81
- // Cache quests
224
+
82
225
  if (data.quests) {
83
226
  this.cache.clear();
84
227
  data.quests.forEach(questData => {
@@ -86,7 +229,7 @@ class QuestManager extends BaseManager {
86
229
  this.cache.set(quest.id, quest);
87
230
  });
88
231
  }
89
-
232
+
90
233
  return data;
91
234
  }
92
235
 
@@ -95,8 +238,7 @@ class QuestManager extends BaseManager {
95
238
  * @returns {Promise<Object>} Balance data
96
239
  */
97
240
  async orbs() {
98
- const data = await this.client.api.users['@me']['virtual-currency'].balance.get();
99
- return data;
241
+ return this.client.api.users['@me']['virtual-currency'].balance.get();
100
242
  }
101
243
 
102
244
  /**
@@ -142,15 +284,19 @@ class QuestManager extends BaseManager {
142
284
  }
143
285
 
144
286
  /**
145
- * Get valid quests (not completed, not expired, not blacklisted)
287
+ * Get valid quests (not completed, not expired)
146
288
  * @returns {Quest[]}
147
289
  */
148
290
  filterQuestsValid() {
149
- return this.list().filter(quest =>
150
- quest.id !== '1412491570820812933' &&
151
- !quest.isCompleted() &&
152
- !quest.isExpired()
153
- );
291
+ return this.list().filter(quest => !quest.isCompleted() && !quest.isExpired());
292
+ }
293
+
294
+ /**
295
+ * Get quests ready to redeem
296
+ * @returns {Quest[]}
297
+ */
298
+ filterQuestsValidToRedeem() {
299
+ return this.getClaimable();
154
300
  }
155
301
 
156
302
  /**
@@ -170,7 +316,7 @@ class QuestManager extends BaseManager {
170
316
  async getApplicationData(ids) {
171
317
  const query = new URLSearchParams();
172
318
  ids.forEach(id => query.append('application_ids', id));
173
-
319
+
174
320
  return this.client.api.applications.public.get({ query: query.toString() });
175
321
  }
176
322
 
@@ -178,27 +324,39 @@ class QuestManager extends BaseManager {
178
324
  * Enroll in a specific quest
179
325
  * @param {string} questId The quest ID to enroll in
180
326
  * @param {Object} [options] Enrollment options
181
- * @param {number} [options.location=11] Location parameter
327
+ * @param {number} [options.location] Location parameter
182
328
  * @param {boolean} [options.isTargeted=false] Whether the quest is targeted
183
- * @param {*} [options.metadataRaw=null] Raw metadata
329
+ * @param {boolean} [options.isAndroid] Whether to enroll as Android client
184
330
  * @returns {Promise<Quest|undefined>} Updated quest or undefined
185
331
  */
186
332
  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,
333
+ let quest = this.getQuest(questId);
334
+ const isAndroid = options.isAndroid ?? (quest ? this._isAndroidQuest(quest) : false);
335
+ const { isTargeted = false } = options;
336
+ const location = options.location ?? (isAndroid ? 12 : 11);
337
+
338
+ const requestOptions = {
339
+ data: {
340
+ location,
192
341
  is_targeted: isTargeted,
193
- metadata_raw: metadataRaw
194
- }
195
- });
196
-
197
- const quest = this.getQuest(questId);
198
- if (quest) {
199
- quest.updateUserStatus(data);
342
+ metadata_sealed: null,
343
+ traffic_metadata_raw: quest?.raw?.traffic_metadata_raw ?? null,
344
+ traffic_metadata_sealed: quest?.raw?.traffic_metadata_sealed ?? null,
345
+ },
346
+ };
347
+
348
+ if (isAndroid) {
349
+ requestOptions.headers = this._getAndroidHeaders();
200
350
  }
201
-
351
+
352
+ const data = await this.client.api.quests(questId).enroll.post(requestOptions);
353
+
354
+ if (!quest) {
355
+ quest = new Quest({ id: questId, config: {}, user_status: data });
356
+ this.cache.set(questId, quest);
357
+ }
358
+
359
+ quest.updateUserStatus(data);
202
360
  return quest;
203
361
  }
204
362
 
@@ -206,30 +364,80 @@ class QuestManager extends BaseManager {
206
364
  * Update progress for a video quest
207
365
  * @param {string} questId The quest ID
208
366
  * @param {number} timestamp Current progress timestamp
367
+ * @param {Object} [options] Request options
368
+ * @param {boolean} [options.isAndroid=false] Whether to send as Android client
209
369
  * @returns {Promise<Object>} Progress update result
210
370
  */
211
- async videoProgress(questId, timestamp) {
212
- const data = await this.client.api.quests(questId)['video-progress'].post({
213
- data: { timestamp }
214
- });
215
- return data;
371
+ async videoProgress(questId, timestamp, options = {}) {
372
+ const requestOptions = {
373
+ data: { timestamp },
374
+ };
375
+
376
+ if (options.isAndroid) {
377
+ requestOptions.headers = this._getAndroidHeaders();
378
+ }
379
+
380
+ return this.client.api.quests(questId)['video-progress'].post(requestOptions);
216
381
  }
217
382
 
218
383
  /**
219
- * Send heartbeat for desktop quests
384
+ * Send heartbeat for desktop/activity quests
220
385
  * @param {string} questId The quest ID
221
- * @param {string} applicationId Application ID
386
+ * @param {string|Object} applicationIdOrOptions Application ID or heartbeat options
222
387
  * @param {boolean} [terminal=false] Whether this is a terminal heartbeat
223
388
  * @returns {Promise<Object>} Heartbeat result
224
389
  */
225
- async heartbeat(questId, applicationId, terminal = false) {
226
- const data = await this.client.api.quests(questId).heartbeat.post({
390
+ async heartbeat(questId, applicationIdOrOptions, terminal = false) {
391
+ let body = { terminal };
392
+
393
+ if (typeof applicationIdOrOptions === 'object' && applicationIdOrOptions !== null) {
394
+ const { applicationId, streamKey, terminal: isTerminal = false } = applicationIdOrOptions;
395
+ body.terminal = isTerminal;
396
+ if (applicationId) body.application_id = applicationId;
397
+ if (streamKey) body.stream_key = streamKey;
398
+ } else {
399
+ body.application_id = applicationIdOrOptions;
400
+ body.terminal = terminal;
401
+ }
402
+
403
+ return this.client.api.quests(questId).heartbeat.post({ data: body });
404
+ }
405
+
406
+ /**
407
+ * Claim rewards for a completed quest
408
+ * @param {Quest|string} quest Quest instance or quest ID
409
+ * @returns {Promise<Quest|undefined>}
410
+ */
411
+ async redeemQuest(quest) {
412
+ if (typeof quest === 'string') {
413
+ quest = this.getQuest(quest);
414
+ }
415
+
416
+ if (!quest) return undefined;
417
+
418
+ if (!quest.isCompleted()) {
419
+ throw new Error('Cannot redeem rewards for an incomplete quest.');
420
+ }
421
+
422
+ if (quest.hasClaimedRewards()) {
423
+ throw new Error('Rewards for this quest have already been claimed.');
424
+ }
425
+
426
+ const platform = quest.config.rewards_config?.platforms?.[0] ?? null;
427
+ const data = await this.client.api.quests(quest.id)['claim-reward'].post({
227
428
  data: {
228
- application_id: applicationId,
229
- terminal
230
- }
429
+ platform,
430
+ location: 11,
431
+ is_targeted: false,
432
+ metadata_raw: null,
433
+ metadata_sealed: null,
434
+ traffic_metadata_raw: quest.raw?.traffic_metadata_raw ?? null,
435
+ traffic_metadata_sealed: quest.raw?.traffic_metadata_sealed ?? null,
436
+ },
231
437
  });
232
- return data;
438
+
439
+ quest.updateUserStatus(data);
440
+ return quest;
233
441
  }
234
442
 
235
443
  /**
@@ -242,92 +450,260 @@ class QuestManager extends BaseManager {
242
450
  return new Promise(resolve => setTimeout(resolve, ms));
243
451
  }
244
452
 
453
+ /**
454
+ * Complete a watch video quest
455
+ * @param {Quest} quest Quest instance
456
+ * @param {string} taskName Task type
457
+ * @param {number} secondsNeeded Target seconds
458
+ * @param {number} secondsDone Current progress
459
+ * @param {boolean} isAndroid Whether this is a mobile quest
460
+ * @returns {Promise<void>}
461
+ * @private
462
+ */
463
+ async _doingWatchVideoQuest(quest, taskName, secondsNeeded, secondsDone, isAndroid) {
464
+ const maxFuture = 10;
465
+ const speed = 7;
466
+ const interval = 7;
467
+ const enrolledAt = new Date(quest.userStatus?.enrolled_at).getTime();
468
+ let completed = false;
469
+
470
+ while (true) {
471
+ const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
472
+ const diff = maxAllowed - secondsDone;
473
+ const timestamp = secondsDone + speed;
474
+
475
+ if (diff >= speed) {
476
+ const res = await this.videoProgress(
477
+ quest.id,
478
+ Math.min(secondsNeeded, timestamp + Math.random()),
479
+ { isAndroid },
480
+ );
481
+ completed = res.completed_at != null;
482
+ quest.updateUserStatus(res);
483
+ secondsDone = Math.min(secondsNeeded, timestamp);
484
+ }
485
+
486
+ if (timestamp >= secondsNeeded) break;
487
+
488
+ await this.timeout(interval * 1000);
489
+ }
490
+
491
+ if (!completed) {
492
+ const res = await this.videoProgress(quest.id, secondsNeeded, { isAndroid });
493
+ quest.updateUserStatus(res);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Complete a play-on-platform quest
499
+ * @param {Quest} quest Quest instance
500
+ * @param {string} taskName Task type
501
+ * @param {number} secondsNeeded Target seconds
502
+ * @returns {Promise<void>}
503
+ * @private
504
+ */
505
+ async _doingPlayOnPlatformQuest(quest, taskName, secondsNeeded) {
506
+ const interval = 20;
507
+ const applicationId = quest.config.application?.id;
508
+
509
+ if (!applicationId) {
510
+ throw new Error(`Missing application ID for quest "${quest.config.messages?.quest_name ?? quest.id}".`);
511
+ }
512
+
513
+ while (!this._isTaskComplete(quest, taskName, secondsNeeded)) {
514
+ const res = await this.heartbeat(quest.id, { applicationId, terminal: false });
515
+ quest.updateUserStatus(res);
516
+
517
+ if (!this._isTaskComplete(quest, taskName, secondsNeeded)) {
518
+ await this.timeout(interval * 1000);
519
+ }
520
+ }
521
+
522
+ const res = await this.heartbeat(quest.id, { applicationId, terminal: true });
523
+ quest.updateUserStatus(res);
524
+ }
525
+
526
+ /**
527
+ * Complete a play activity quest
528
+ * @param {Quest} quest Quest instance
529
+ * @param {string} taskName Task type
530
+ * @param {number} secondsNeeded Target seconds
531
+ * @returns {Promise<void>}
532
+ * @private
533
+ */
534
+ async _doingPlayActivityQuest(quest, taskName, secondsNeeded) {
535
+ const interval = 20;
536
+ const streamKey = this._getActivityStreamKey();
537
+
538
+ if (!streamKey) {
539
+ throw new Error(
540
+ `No voice or DM channel available for PLAY_ACTIVITY quest "${quest.config.messages?.quest_name ?? quest.id}".`,
541
+ );
542
+ }
543
+
544
+ while (!this._isTaskComplete(quest, taskName, secondsNeeded)) {
545
+ const res = await this.heartbeat(quest.id, { streamKey, terminal: false });
546
+ quest.updateUserStatus(res);
547
+
548
+ if (!this._isTaskComplete(quest, taskName, secondsNeeded)) {
549
+ await this.timeout(interval * 1000);
550
+ }
551
+ }
552
+
553
+ const res = await this.heartbeat(quest.id, { streamKey, terminal: true });
554
+ quest.updateUserStatus(res);
555
+ }
556
+
557
+ /**
558
+ * Complete an achievement in activity quest
559
+ * @param {Quest} quest Quest instance
560
+ * @returns {Promise<void>}
561
+ * @private
562
+ */
563
+ async _doingAchievementInActivityQuest(quest) {
564
+ const applicationId = quest.config.application?.id;
565
+ const applicationName = quest.config.application?.name ?? applicationId;
566
+ const questTarget = this._getTaskConfig(quest)?.tasks?.ACHIEVEMENT_IN_ACTIVITY?.target;
567
+
568
+ if (!applicationId || !questTarget) {
569
+ throw new Error(`Invalid achievement quest configuration for "${applicationName}".`);
570
+ }
571
+
572
+ const query = new URLSearchParams({
573
+ response_type: 'code',
574
+ client_id: applicationId,
575
+ scope: 'identify applications.commands applications.entitlements',
576
+ state: '',
577
+ });
578
+
579
+ const authResponse = await this.client.api.oauth2.authorize.post({
580
+ query: Object.fromEntries(query),
581
+ data: {
582
+ permissions: '0',
583
+ authorize: true,
584
+ integration_type: 1,
585
+ location_context: {
586
+ guild_id: '10000',
587
+ channel_id: '10000',
588
+ channel_type: 10000,
589
+ },
590
+ },
591
+ });
592
+
593
+ let authCode = null;
594
+ if (authResponse?.location) {
595
+ authCode = new URL(authResponse.location).searchParams.get('code');
596
+ }
597
+
598
+ if (!authCode) {
599
+ throw new Error(`No auth code received for application ${applicationName}.`);
600
+ }
601
+
602
+ const tokenResponse = await fetch(`https://${applicationId}.discordsays.com/.proxy/acf/authorize`, {
603
+ method: 'POST',
604
+ headers: { 'Content-Type': 'application/json' },
605
+ body: JSON.stringify({ code: authCode }),
606
+ }).then(res => res.json());
607
+
608
+ if (!tokenResponse?.token) {
609
+ throw new Error(`Failed to authorize with Discord Says for application ${applicationName}.`);
610
+ }
611
+
612
+ const progressResponse = await fetch(`https://${applicationId}.discordsays.com/.proxy/acf/quest/progress`, {
613
+ method: 'POST',
614
+ headers: {
615
+ 'Content-Type': 'application/json',
616
+ 'x-auth-token': tokenResponse.token,
617
+ },
618
+ body: JSON.stringify({ progress: questTarget }),
619
+ });
620
+
621
+ if (!progressResponse.ok) {
622
+ throw new Error(`Failed to progress quest with Discord Says for application ${applicationName}.`);
623
+ }
624
+
625
+ const tokens = await this.client.api.oauth2.tokens.get();
626
+ const tokenInfo = tokens.find(token => token.application?.id === applicationId);
627
+
628
+ if (tokenInfo) {
629
+ await this.client.api.oauth2.tokens(tokenInfo.id).delete().catch(() => {});
630
+ }
631
+
632
+ await this.get();
633
+ }
634
+
245
635
  /**
246
636
  * Complete a quest automatically
247
637
  * @param {Quest} quest Quest to complete
248
638
  * @returns {Promise<void>}
249
639
  */
250
640
  async doingQuest(quest) {
251
- // Ensure quest is a proper Quest instance
252
641
  if (!(quest instanceof Quest)) {
253
642
  quest = new Quest(quest);
254
643
  }
255
-
256
- const questName = quest.config.messages?.quest_name || 'Unknown Quest';
257
-
258
- if (!quest.isEnrolledQuest())
259
- await this.acceptQuest(quest.id);
260
644
 
261
- const taskConfig = quest.config.task_config;
262
-
263
- const taskName = [
264
- 'WATCH_VIDEO',
265
- 'PLAY_ON_DESKTOP',
266
- 'STREAM_ON_DESKTOP',
267
- 'PLAY_ACTIVITY',
268
- 'WATCH_VIDEO_ON_MOBILE'
269
- ].find(x => taskConfig?.tasks?.[x] != null);
645
+ const questName = quest.config.messages?.quest_name || 'Unknown Quest';
646
+ const isAndroid = this._isAndroidQuest(quest);
647
+ const taskConfig = this._getTaskConfig(quest);
648
+ const taskName = this._findTaskName(taskConfig);
270
649
 
271
- if (!taskName)
272
- return console.log(`Unknown task type for quest "${questName}"`);
650
+ if (!taskName) {
651
+ console.log(`Unknown task type for quest "${questName}"`);
652
+ return;
653
+ }
273
654
 
655
+ if (!quest.isEnrolledQuest()) {
656
+ try {
657
+ const enrolledQuest = await this.acceptQuest(quest.id, { isAndroid });
658
+ if (enrolledQuest) quest = enrolledQuest;
659
+ } catch (error) {
660
+ console.error(`Failed to enroll in quest "${questName}":`, error);
661
+ return;
662
+ }
663
+ }
274
664
 
275
665
  const secondsNeeded = taskConfig.tasks[taskName].target;
276
666
  let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
277
667
 
278
- if (taskName === 'WATCH_VIDEO' || taskName === 'WATCH_VIDEO_ON_MOBILE') {
279
- const maxFuture = 10;
280
- const speed = 7;
281
- const interval = 1;
282
- const enrolledAt = new Date(quest.userStatus?.enrolled_at).getTime();
283
- let completed = false;
284
-
285
- while (true) {
286
- const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
287
- const diff = maxAllowed - secondsDone;
288
- const timestamp = secondsDone + speed;
289
-
290
- if (diff >= speed) {
291
- const res = await this.videoProgress(quest.id, Math.min(secondsNeeded, timestamp + Math.random()));
292
- completed = res.completed_at != null;
293
- secondsDone = Math.min(secondsNeeded, timestamp);
294
- }
668
+ switch (taskName) {
669
+ case 'WATCH_VIDEO':
670
+ case 'WATCH_VIDEO_ON_MOBILE':
671
+ await this._doingWatchVideoQuest(quest, taskName, secondsNeeded, secondsDone, isAndroid);
672
+ break;
295
673
 
296
- if (timestamp >= secondsNeeded) {
297
- break;
298
- }
674
+ case 'PLAY_ON_XBOX':
675
+ case 'PLAY_ON_PLAYSTATION':
676
+ case 'PLAY_ON_DESKTOP':
677
+ await this._doingPlayOnPlatformQuest(quest, taskName, secondsNeeded);
678
+ break;
299
679
 
300
- await this.timeout(interval * 1000);
301
- }
680
+ case 'PLAY_ACTIVITY':
681
+ await this._doingPlayActivityQuest(quest, taskName, secondsNeeded);
682
+ break;
302
683
 
303
- if (!completed) {
304
- await this.videoProgress(quest.id, secondsNeeded);
305
- }
684
+ case 'ACHIEVEMENT_IN_ACTIVITY':
685
+ await this._doingAchievementInActivityQuest(quest);
686
+ break;
306
687
 
307
- } else if (taskName === 'PLAY_ON_DESKTOP') {
308
- const interval = 60;
688
+ case 'STREAM_ON_DESKTOP':
689
+ console.log(`Quest "${questName}" requires the Discord desktop app to complete streaming tasks.`);
690
+ break;
309
691
 
310
- while (!quest.isCompleted()) {
311
- const secondsDone = quest.userStatus?.progress?.[taskName]?.value || 0;
312
- const res = await this.heartbeat(quest.id, quest.config.application.id, false);
313
- quest.updateUserStatus(res);
314
-
315
- await this.timeout(interval * 1000);
316
- }
317
-
318
- const res = await this.heartbeat(quest.id, quest.config.application.id, true);
319
- quest.updateUserStatus(res);
692
+ default:
693
+ console.log(`Unsupported task type "${taskName}" for quest "${questName}".`);
320
694
  }
321
695
  }
322
696
 
323
697
  /**
324
698
  * Auto-complete all valid quests
699
+ * @param {Object} [options] Options
700
+ * @param {boolean} [options.redeem=false] Whether to redeem rewards after completion
325
701
  * @returns {Promise<void>}
326
702
  */
327
- async autoCompleteAll() {
328
- await this.get(); // Refresh quest data
703
+ async autoCompleteAll(options = {}) {
704
+ await this.get();
329
705
  const validQuests = this.filterQuestsValid();
330
-
706
+
331
707
  for (const quest of validQuests) {
332
708
  try {
333
709
  await this.doingQuest(quest);
@@ -335,6 +711,17 @@ class QuestManager extends BaseManager {
335
711
  console.error(`Failed to complete quest ${quest.id}:`, error);
336
712
  }
337
713
  }
714
+
715
+ if (options.redeem) {
716
+ await this.get();
717
+ for (const quest of this.filterQuestsValidToRedeem()) {
718
+ try {
719
+ await this.redeemQuest(quest);
720
+ } catch (error) {
721
+ console.error(`Failed to redeem quest ${quest.id}:`, error);
722
+ }
723
+ }
724
+ }
338
725
  }
339
726
 
340
727
  /**
@@ -362,4 +749,5 @@ class QuestManager extends BaseManager {
362
749
  }
363
750
  }
364
751
 
365
- module.exports = QuestManager;
752
+ module.exports = QuestManager;
753
+ module.exports.Quest = Quest;