# discord-quests

<div align="center">

[![npm version](https://img.shields.io/npm/v/discord-quests?color=5865F2&style=flat-square)](https://www.npmjs.com/package/discord-quests)
[![npm downloads](https://img.shields.io/npm/dm/discord-quests?color=5865F2&style=flat-square)](https://www.npmjs.com/package/discord-quests)
[![License: MIT](https://img.shields.io/badge/license-MIT-5865F2?style=flat-square)](LICENSE)
[![Node.js](https://img.shields.io/badge/node-%3E%3D18-5865F2?style=flat-square)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-ready-5865F2?style=flat-square)](https://www.typescriptlang.org)

**A Node.js library for automating Discord Quests.**  
Supports video watching, game heartbeats, and activity quests — with built-in  
rate-limit handling, automatic enroll, proxy support, and a clean event-driven API.

</div>

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Getting Your Token](#getting-your-token)
- [API Reference](#api-reference)
  - [Constructor](#constructor)
  - [Static `run()`](#static-run--one-liner)
  - [validateToken](#validatetoken)
  - [fetchQuests](#fetchquests)
  - [fetchQuest](#fetchquestquestid)
  - [enroll](#enrollquestid)
  - [enrollAll](#enrollall)
  - [solve](#solvequestid-options)
  - [solveAll](#solveall-options)
  - [getStatus](#getstatus)
  - [createSolver](#createsolverquest-taskid)
  - [registerSolver](#registersolvertaskid-solverclass)
  - [Events & Callbacks](#events--callbacks)
- [Return Types](#return-types)
  - [QuestInfo](#questinfo)
  - [QuestTask](#questtask)
  - [EnrollResult](#enrollresult)
  - [SolveResult](#solveresult)
- [Custom Solvers](#custom-solvers)
- [Supported Quest Types](#supported-quest-types)
- [Proxy Support](#proxy-support)
- [Examples](#examples)
- [FAQ](#faq)

---

## Features

- **Core quest types supported** — `WATCH_VIDEO`, `WATCH_VIDEO_ON_MOBILE`, `PLAY_ON_DESKTOP`, `STREAM_ON_DESKTOP`, `PLAY_ACTIVITY`
- **Extensible** — register your own solver for any new quest type Discord adds, no library update needed
- **Auto enroll** — enrolls in quests automatically before solving
- **Multi-account safe** — every instance has its own isolated rate-limit tracker and unique request fingerprint
- **Unique fingerprints** — `client_launch_id`, `launch_signature`, `client_heartbeat_session_id`, and `x-installation-id` are randomly generated per instance so no two accounts look identical
- **Auto rate-limit handling** — per-instance bucket tracking respects Discord's `x-ratelimit-*` headers, retries on 429/5xx automatically
- **Proxy support** — HTTP and SOCKS5, with or without authentication
- **Event-driven progress** — real-time `onProgress`, `onCompleted`, `onError` callbacks
- **Status dashboard** — check enrollment and progress for all quests at a glance
- **Structured results** — every `solve()` call returns a typed result, never throws silently
- **TypeScript-first** — full type definitions included, works in plain JavaScript too
- **Zero Discord.js dependency** — pure HTTP, minimal footprint (`axios` + `socks-proxy-agent`)

---

## Installation

```bash
npm install discord-quests
```
```bash
yarn add discord-quests
```
```bash
pnpm add discord-quests
```

**Requirements:** Node.js 18 or higher.

---

## Quick Start

```js
import { DiscordQuests } from "discord-quests";

const dq = new DiscordQuests("YOUR_DISCORD_TOKEN");

const results = await dq.solveAll({
  onProgress: ({ taskId, percent }) => console.log(`[${taskId}] ${percent}%`),
  onCompleted: (questId) => console.log(`Quest ${questId} completed!`),
  onError: (questId, err) => console.error(`Quest ${questId} failed:`, err.message),
});

console.log(results);
```

The library automatically:
1. Fetches all available quests
2. Enrolls in any you aren't enrolled in yet
3. Runs the correct solver for each quest type
4. Reports progress in real-time
5. Returns a result for every quest

---

## Getting Your Token

> [!WARNING]
> **Use a real Discord user account token, not a bot token.**  
> Quest rewards are given to user accounts only.

1. Open Discord in your **browser** at [discord.com/app](https://discord.com/app)
2. Press `F12` to open DevTools → click the **Console** tab
3. Paste the following and press `Enter`:

```js
window.webpackChunkdiscord_app.push([[Symbol()],{},o=>{
    let token = "";
    for(let e of Object.values(o.c)) {
        try {
            if(!e.exports||e.exports===window) continue;
            if(e.exports?.getToken) token = e.exports.getToken();
            for(let o in e.exports) {
                if(e.exports?.[o]?.getToken && "IntlMessagesProxy"!==e.exports[o][Symbol.toStringTag]) {
                    token = e.exports[o].getToken();
                }
            }
        } catch {}
    }
    if(token) copy(token);
}]);
window.webpackChunkdiscord_app.pop();
```

4. It will be copied to the clipboard

> **Keep your token private.** Anyone who has it has full access to your Discord account.

---

## API Reference

### Constructor

```ts
new DiscordQuests(token: string, options?: DiscordQuestsOptions)
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `token` | `string` | ✅ | Discord user account token |
| `options.proxy` | `string \| ProxyOptions` | — | Proxy server (see [Proxy Support](#proxy-support)) |
| `options.proxyType` | `"http" \| "socks5"` | — | Proxy protocol (default: `"http"`) |

Throws immediately if the token format is invalid.

```js
const dq = new DiscordQuests("YOUR_TOKEN");

// With proxy
const dq = new DiscordQuests("YOUR_TOKEN", {
  proxy: "1.2.3.4:8080",
});
```

---

### Static `run()` — One-liner

```ts
static async run(token, options?): Promise<SolveResult[]>
```

Creates an instance and calls `solveAll()` in one line. Perfect for simple scripts.

```js
import { DiscordQuests } from "discord-quests";

await DiscordQuests.run("YOUR_TOKEN", {
  onCompleted: (id) => console.log("Done:", id),
});
```

---

### `validateToken()`

```ts
async validateToken(): Promise<boolean>
```

Checks whether the token is working by calling Discord's `/users/@me`. Returns `false` if the token is invalid, expired, or the account is banned.

```js
const valid = await dq.validateToken();
if (!valid) {
  console.log("Token is invalid!");
  process.exit(1);
}
```

---

### `fetchQuests()`

```ts
async fetchQuests(): Promise<QuestInfo[]>
```

Returns all **active** (non-expired) quests for this account. Expired quests are automatically filtered out.

```js
const quests = await dq.fetchQuests();

quests.forEach(quest => {
  console.log(`${quest.name}`);
  console.log(`  Enrolled:  ${quest.enrolled}`);
  console.log(`  Completed: ${quest.completed}`);
  console.log(`  Expires:   ${quest.expiresAt?.toLocaleString() ?? "N/A"}`);
});
```

---

### `fetchQuest(questId)`

```ts
async fetchQuest(questId: string): Promise<QuestInfo>
```

Fetch a single quest by its ID.

```js
const quest = await dq.fetchQuest("1234567890");
console.log(quest.name, quest.tasks);
```

---

### `enroll(questId)`

```ts
async enroll(questId: string): Promise<EnrollResult>
```

Enroll this account in a quest. If already enrolled, returns immediately without making an extra request.

```js
const result = await dq.enroll("1234567890");

if (result.success) {
  if (result.alreadyEnrolled) {
    console.log("Was already enrolled.");
  } else {
    console.log("Enrolled in:", result.quest.name);
  }
} else {
  console.log("Enrollment failed.");
}
```

---

### `enrollAll()`

```ts
async enrollAll(): Promise<EnrollResult[]>
```

Enroll in **all** available quests at once.

```js
const results = await dq.enrollAll();

const newEnrollments = results.filter(r => r.success && !r.alreadyEnrolled);
console.log(`Enrolled in ${newEnrollments.length} new quest(s).`);
```

---

### `solve(questId, options?)`

```ts
async solve(questId: string, options?: SolverOptions): Promise<SolveResult>
```

The main solving method. Handles the full lifecycle:

1. Fetches fresh quest data
2. Skips if already completed
3. Enrolls automatically if not enrolled
4. Picks the best task to solve
5. Detects unsupported quest types gracefully
6. Runs the solver until completion or error
7. Returns a [`SolveResult`](#solveresult)

```js
const result = await dq.solve("1234567890", {
  onProgress: ({ taskId, current, target, percent }) => {
    process.stdout.write(`\r[${taskId}] ${current}/${target}s — ${percent}%   `);
  },
  onCompleted: (questId) => {
    console.log(`\nQuest ${questId} completed!`);
  },
});

console.log(result.status);
// "completed" | "already_completed" | "enroll_failed" | "unsupported" | "error"
```

---

### `solveAll(options?)`

```ts
async solveAll(options?: SolverOptions & { parallel?: boolean }): Promise<SolveResult[]>
```

Fetch all quests and solve every unsolved one.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `parallel` | `boolean` | `false` | Run all quests at the same time (default: one by one) |
| `onProgress` | `function` | — | Progress callback |
| `onCompleted` | `function` | — | Completion callback |
| `onError` | `function` | — | Error callback (does not stop other quests) |
| `onLog` | `function` | — | Internal debug messages |

```js
// Sequential — safe, recommended
const results = await dq.solveAll({
  onProgress: ({ taskId, percent }) => console.log(`${taskId}: ${percent}%`),
  onCompleted: (id) => console.log(`Done: ${id}`),
  onError: (id, err) => console.error(`Error on ${id}:`, err.message),
});

// Parallel — faster, uses more connections
const results = await dq.solveAll({ parallel: true });
```

---

### `getStatus()`

```ts
async getStatus(): Promise<Array<{
  id: string;
  name: string;
  enrolled: boolean;
  completed: boolean;
  solvable: boolean;
  tasks: Array<{ id: string; percent: number; completed: boolean }>;
  expiresAt: Date | null;
}>>
```

Returns a summary for all active quests — enrollment state, completion, solvability, and per-task progress.

```js
const status = await dq.getStatus();

status.forEach(s => {
  console.log(`${s.name}`);
  console.log(`  Enrolled: ${s.enrolled} | Completed: ${s.completed} | Solvable: ${s.solvable}`);
  s.tasks.forEach(t => {
    console.log(`  [${t.id}] ${t.percent}%`);
  });
});
```

---

### `createSolver(quest, taskId?)`

```ts
createSolver(quest: QuestInfo, taskId?: string): BaseSolver
```

Creates a solver instance without running it. Gives you full manual control over execution, events, and timing. The solver is an `EventEmitter`.

```js
const quest = await dq.fetchQuest("1234567890");
const solver = dq.createSolver(quest);

solver.on("progress", ({ current, target, percent }) => {
  process.stdout.write(`\rProgress: ${percent}% (${current}/${target}s)   `);
});
solver.on("completed", () => console.log("\nDone!"));
solver.on("error", (id, err) => console.error(`\n${err.message}`));

// Emergency stop after 2 hours
setTimeout(() => solver.stop(), 2 * 60 * 60 * 1000);

await solver.start();
```

Pass a `taskId` to force a specific task:

```js
const solver = dq.createSolver(quest, "WATCH_VIDEO");
```

---

### `registerSolver(taskId, SolverClass)`

```ts
registerSolver(taskId: string, SolverClass: SolverConstructor): void
```

Register a custom solver for any task ID — including ones not built into the library. Once registered, that solver is used automatically by `solve()`, `solveAll()`, and `createSolver()`.

Registering a solver for a task ID that the library already supports will **replace** the built-in one.

See [Custom Solvers](#custom-solvers) for the full guide.

```js
dq.registerSolver("NEW_QUEST_TYPE", MyCustomSolver);

// From this point on, solveAll() handles NEW_QUEST_TYPE automatically
await dq.solveAll();
```

---

### Events & Callbacks

All callbacks are available as `onX` options in `solve()` / `solveAll()`, and as EventEmitter events when using `createSolver()` directly.

#### `onProgress` / `"progress"`

Fired every time the solver sends a heartbeat and receives updated progress.

```ts
interface ProgressEvent {
  questId: string;  // Quest ID (Snowflake)
  taskId: string;   // e.g. "WATCH_VIDEO", "PLAY_ON_DESKTOP"
  current: number;  // Current value (seconds for duration quests)
  target: number;   // Target value to complete the task
  percent: number;  // 0–100
}
```

#### `onCompleted` / `"completed"`

```ts
(questId: string) => void
```

Fired once when the solver confirms the task is finished.

#### `onError` / `"error"`

```ts
(questId: string, error: Error) => void
```

Fired if the solver encounters a fatal error. In `solveAll()`, this does **not** stop other quests.

#### `onLog` / `"log"`

```ts
(message: string) => void
```

Internal status messages. Useful for debugging.

---

## Return Types

### `QuestInfo`

```ts
interface QuestInfo {
  id: string;
  name: string;
  application: {
    id: string;
    name: string;
    link: string;
  };
  tasks: QuestTask[];     // sorted by progress descending
  rewards: QuestReward[];
  enrolled: boolean;
  completed: boolean;
  expiresAt: Date | null;
  startsAt: string;
  raw: RawQuest;          // full Discord API response
}
```

### `QuestTask`

```ts
interface QuestTask {
  id: string;            // "WATCH_VIDEO" | "PLAY_ON_DESKTOP" | etc.
  target: number;        // seconds for duration quests
  current: number;
  percent: number;       // 0–100
  completed: boolean;
  enrolled: boolean;
  type: "duration" | "count";
  eventName?: string;    // raw event name from Discord API, if present (useful in custom solvers)
}
```

### `EnrollResult`

```ts
interface EnrollResult {
  success: boolean;
  alreadyEnrolled: boolean;
  quest: QuestInfo;
}
```

### `SolveResult`

```ts
interface SolveResult {
  questId: string;
  questName: string;
  status:
    | "completed"          // solver ran and the quest is now done
    | "already_completed"  // quest was already done before we started
    | "enroll_failed"      // could not enroll in the quest
    | "unsupported"        // no built-in or registered solver for this type
    | "error";             // solver threw an error
  error?: string;          // only present when status === "error"
}
```

---

## Custom Solvers

If Discord adds a new quest type that the library doesn't support yet, you can write your own solver and plug it in — no need to wait for a library update.

### How to write a custom solver

Extend `BaseSolver` and implement the `run()` method:

```ts
import { BaseSolver, type SolverConstructorArgs } from "discord-quests";

class MyCustomSolver extends BaseSolver {
  constructor(config: SolverConstructorArgs) {
    super(config);
    // config.task  — the QuestTask being solved
    // config.quest — the full QuestInfo
    // config.api   — pre-configured axios instance (with token + headers)
  }

  async run(): Promise<void> {
    while (!this.stopped) {
      const res = await this.api
        .post(`/quests/${this.questId}/some-endpoint`, { /* ... */ })
        .catch(err => err?.response);

      if (!res?.data?.user_id) {
        this.stop();
        throw new Error("Request failed");
      }

      const { value, completed } = this.extractProgress(res.data);
      this.emitProgress(value, this.target);

      if (completed || value >= this.target) {
        this.emitCompleted();
        break;
      }

      await this.delay(30000); // wait 30s before next heartbeat
    }
  }
}
```

### Register and use it

```ts
import { DiscordQuests } from "discord-quests";

const dq = new DiscordQuests("YOUR_TOKEN");

// Register once — applies to solve(), solveAll(), and createSolver()
dq.registerSolver("NEW_QUEST_TYPE", MyCustomSolver);

// Everything works normally from here
await dq.solveAll({
  onProgress: ({ taskId, percent }) => console.log(`[${taskId}] ${percent}%`),
  onCompleted: (id) => console.log(`Done: ${id}`),
});
```

### Available in `BaseSolver`

| Property / Method | Description |
|-------------------|-------------|
| `this.api` | Axios instance with token, headers, and rate-limit handling |
| `this.questId` | Current quest ID |
| `this.taskId` | Current task ID (e.g. `"NEW_QUEST_TYPE"`) |
| `this.current` | Starting progress value |
| `this.target` | Target value to reach |
| `this.userId` | Discord user ID |
| `this.task` | Full `QuestTask` object |
| `this.quest` | Full `QuestInfo` object |
| `this.stopped` | `true` if `stop()` was called |
| `this.emitProgress(current, target)` | Fires the `"progress"` event |
| `this.emitCompleted()` | Fires the `"completed"` event |
| `this.emitLog(message)` | Fires the `"log"` event |
| `this.extractProgress(responseData)` | Extracts `{ value, completed }` from a Discord heartbeat response |
| `this.delay(ms)` | Promise-based sleep that respects `stop()` |

### Override a built-in solver

Registering a solver for a task ID that already exists in the library **replaces** the built-in one:

```js
dq.registerSolver("WATCH_VIDEO", MyFasterVideoSolver);
```

---

## Supported Quest Types

| Task ID | Solver | Interval | Endpoint |
|---------|--------|----------|----------|
| `WATCH_VIDEO` | `WatchVideoSolver` | 5 seconds | `POST /quests/{id}/video-progress` |
| `WATCH_VIDEO_ON_MOBILE` | `WatchVideoSolver` | 5 seconds | `POST /quests/{id}/video-progress` |
| `PLAY_ON_DESKTOP` | `PlayOnDesktopSolver` | 30 seconds | `POST /quests/{id}/heartbeat` — `stream_key: "call:{questId}:1"` |
| `STREAM_ON_DESKTOP` | `PlayOnDesktopSolver` | 30 seconds | `POST /quests/{id}/heartbeat` — `stream_key: "call:{questId}:1"` |
| `PLAY_ACTIVITY` | `PlayActivitySolver` | 30 seconds | `POST /quests/{id}/heartbeat` — `stream_key: "call:{userId}:1"` |

`WATCH_VIDEO_ON_MOBILE` and `WATCH_VIDEO` share the same solver. Same for `STREAM_ON_DESKTOP` and `PLAY_ON_DESKTOP` — Discord uses the same endpoint for both, so one solver handles both task IDs.

> If Discord introduces a new quest type, use [`registerSolver()`](#registersolvertaskid-solverclass) to add support for it without modifying the library.

---

## Proxy Support

Proxies are passed through the constructor options.

**String format:**
```js
// http proxy
new DiscordQuests("TOKEN", { proxy: "1.2.3.4:8080" });
new DiscordQuests("TOKEN", { proxy: "user:pass@1.2.3.4:8080" });

// socks5 proxy
new DiscordQuests("TOKEN", { proxy: "1.2.3.4:1080", proxyType: "socks5" });
```

**Object format:**
```js
new DiscordQuests("TOKEN", {
  proxy: {
    host: "1.2.3.4",
    port: 8080,
    auth: { username: "user", password: "pass" },
    type: "http", // or "socks5"
  },
});
```

---

## Examples

**Solve all quests with live progress:**
```js
import { DiscordQuests } from "discord-quests";

const dq = new DiscordQuests("YOUR_TOKEN");

const results = await dq.solveAll({
  onProgress: ({ taskId, current, target, percent }) => {
    process.stdout.write(`\r[${taskId}] ${current}/${target}s — ${percent}%   `);
  },
  onCompleted: (id) => console.log(`\nDone: ${id}`),
  onError: (id, err) => console.error(`\nFailed ${id}: ${err.message}`),
});

console.table(results.map(r => ({ name: r.questName, status: r.status })));
```

**Check quest status without solving:**
```js
const status = await dq.getStatus();

status.forEach(s => {
  const icon = s.completed ? "✓" : s.solvable ? "→" : "✗";
  console.log(`${icon} ${s.name}`);
  s.tasks.forEach(t => console.log(`   [${t.id}] ${t.percent}%`));
});
```

**Add a custom solver for a new quest type:**
```js
import { DiscordQuests, BaseSolver } from "discord-quests";

class NewTypeSolver extends BaseSolver {
  async run() {
    // implement heartbeat/progress logic here
    this.emitProgress(this.target, this.target);
    this.emitCompleted();
  }
}

const dq = new DiscordQuests("YOUR_TOKEN");
dq.registerSolver("NEW_TYPE", NewTypeSolver);

await dq.solveAll(); // NEW_TYPE is now handled automatically
```

---

## FAQ

**Does this work with bot tokens?**  
No. Discord Quests are only available to user accounts. Bot tokens will be rejected by the constructor.

**Will this get my account banned?**  
Automating Discord accounts violates Discord's Terms of Service. Use at your own risk.

**A quest shows `status: "unsupported"` — what do I do?**  
Discord added a new quest type that the library doesn't have a built-in solver for yet. Write a custom solver using `registerSolver()` — see the [Custom Solvers](#custom-solvers) section.

**Can I use this with multiple accounts at once?**  
Yes. Create one `DiscordQuests` instance per token and run them in parallel or sequentially.

**How do I stop a running solver mid-way?**  
Use `createSolver()` to get the solver instance and call `solver.stop()` whenever needed.
