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.
- package/package.json +1 -1
- package/src/client/Client.js +7 -0
- package/src/index.js +3 -0
- package/src/managers/BackupManager.js +141 -0
- package/src/managers/QuestManager.js +488 -100
- package/src/managers/backup/create.js +167 -0
- package/src/managers/backup/index.js +144 -0
- package/src/managers/backup/load.js +293 -0
- package/src/managers/backup/util.js +400 -0
- package/typings/index.d.ts +219 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
287
|
+
* Get valid quests (not completed, not expired)
|
|
146
288
|
* @returns {Quest[]}
|
|
147
289
|
*/
|
|
148
290
|
filterQuestsValid() {
|
|
149
|
-
return this.list().filter(quest =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
327
|
+
* @param {number} [options.location] Location parameter
|
|
182
328
|
* @param {boolean} [options.isTargeted=false] Whether the quest is targeted
|
|
183
|
-
* @param {
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
213
|
-
data: { timestamp }
|
|
214
|
-
}
|
|
215
|
-
|
|
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}
|
|
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,
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
680
|
+
case 'PLAY_ACTIVITY':
|
|
681
|
+
await this._doingPlayActivityQuest(quest, taskName, secondsNeeded);
|
|
682
|
+
break;
|
|
302
683
|
|
|
303
|
-
|
|
304
|
-
await this.
|
|
305
|
-
|
|
684
|
+
case 'ACHIEVEMENT_IN_ACTIVITY':
|
|
685
|
+
await this._doingAchievementInActivityQuest(quest);
|
|
686
|
+
break;
|
|
306
687
|
|
|
307
|
-
|
|
308
|
-
|
|
688
|
+
case 'STREAM_ON_DESKTOP':
|
|
689
|
+
console.log(`Quest "${questName}" requires the Discord desktop app to complete streaming tasks.`);
|
|
690
|
+
break;
|
|
309
691
|
|
|
310
|
-
|
|
311
|
-
|
|
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();
|
|
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;
|