본문으로 건너뛰기

Discord Orbs Quest 자동 완료하기

· 약 14분
L4N14KE4
Student

Discord Orbs 퀘스트를 영상 시청/게임플레이 없이 Orbs 보상받는 방법을 알아보자.

discord-orbs-quest.jpg

TL;DR

Discord Quest 자동 완료 스크립트를 사용하면 영상 시청이나 게임 플레이 없이 Orbs를 받을 수 있다.

개발자 도구 실행 -> 스크립트 붙여넣기 - > 퀘스트 완료


Discord Orbs 퀘스트란?

Discord에서는 Orbs라는 가상 화폐를 통해 다양한 아이템을 구매할 수 있다. Orbs는 Discord Quest를 완료하면 보상으로 받을 수 있는데, 이 퀘스트는 영상 시청이나 게임 플레이를 요구한다.

Discord Orbs 퀘스트의 유형과 보상

Discord Quest는 크게 두 가지 유형으로 나뉜다:

영상 시청형: 홍보 영상을 시청하는 퀘스트 (WATCH_VIDEO, WATCH_VIDEO_ON_MOBILE)

게임/프로그램 실행형: 특정 게임을 플레이하거나 스트리밍하는 퀘스트 (PLAY_ON_DESKTOP, STREAM_ON_DESKTOP, PLAY_ACTIVITY)

보상

  1. Discord Orbs: 대부분의 퀘스트는 700 Orbs를 보상으로 준다.
  2. 아바타 장식 / 프로필 효과
  3. 게임 코드 및 아이템

퀘스트를 클리어하지 않고 Orbs 받기

앞서 말했듯 Discord Orbs Quest는 영상 시청이나 게임 플레이를 요구한다. 하지만 두가지 방법으로 퀘스트를 클리어하지 않고도 Orbs를 받을 수 있다.

  1. 스크립트 이용
  2. 임의의 .exe 파일을 게임 실행 파일 이름으로 변경1

이 글에서는 스크립트 이용방법을 설명한다.

스크립트 이용하기

경고

스크립트를 통한 퀘스트 수행은 Discord의 서비스 약관에 위배될 수 있으며, 계정 정지 등의 불이익을 받을 수 있다. 본인의 책임 하에 진행해야 한다.

인터넷을 검색해보면 이미 만들어진 훌륭한 해결책이 있다. Discord Orbs Quest를 자동으로 완료해주는 스크립트를 사용할 수 있는데, 이 스크립트는 퀘스트 시스템을 우회하여 자동으로 완료하고 Orbs를 지급받도록 도와준다. 모든 유형의 퀘스트를 지원한다.

스크립트 작동원리

  1. Discord 클라이언트의 내부 모듈 시스템에 접근
  2. 게임 실행 상태를 조작하거나 API 직접 호출
  3. 서버에 정상적인 진행 상태로 위장하여 전송

사전 설정

브라우저에서는 게임 실행 감지 기능이 제한되기 때문에 게임/프로그램 실행형 퀘스트를 완료하려면 Discord 데스크톱 앱이 필요하다.

그리고 스크립트 실행을 위해 개발자도구(DevTools)를 열어야 하는데, 보안 설정 때문에 Discord 데스크톱 앱에서는 기본적으로 DevTools를 열 수 없다. 따라서 다음 단계를 따라 설정해야 한다.

귀찮다면 방법2를 사용하자.

방법 1: Discord setting.json 파일 수정

  1. OS에 맞춰 아래 경로에 있는 settings.json 파일을 연다.

macOS: ~/Library/Application Support/discord/settings.json

windows: %appdata%/discord/settings.json

  1. 다음 내용을 추가한다.
{
// 기존 설정들...
"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true
}

discord-setting.png

  1. Discord 앱을 재시작한다.

방법2: Canary 또는 PTB 버전 사용

Discord의 Canary 또는 PTB(공개 테스트 빌드) 버전은 기본적으로 DevTools가 활성화되어있다. 이 버전을 설치하여 사용한다.

  • canary
  • PTB

스크립트 실행 방법

  1. 아래 코드 블록 / gist 페이지에서 스크립트 코드를 복사한다.
스크립트 코드 보기
delete window.$;
let wpRequire = webpackChunkdiscord_app.push([[Symbol()], {}, r => r]);
webpackChunkdiscord_app.pop();

let ApplicationStreamingStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getStreamerActiveStreamMetadata)?.exports?.Z;
let RunningGameStore, QuestsStore, ChannelStore, GuildChannelStore, FluxDispatcher, api
if(!ApplicationStreamingStore) {
ApplicationStreamingStore = Object.values(wpRequire.c).find(x => x?.exports?.A?.__proto__?.getStreamerActiveStreamMetadata).exports.A;
RunningGameStore = Object.values(wpRequire.c).find(x => x?.exports?.Ay?.getRunningGames).exports.Ay;
QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.A?.__proto__?.getQuest).exports.A;
ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.A?.__proto__?.getAllThreadsForParent).exports.A;
GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Ay?.getSFWDefaultChannel).exports.Ay;
FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.h?.__proto__?.flushWaitQueue).exports.h;
api = Object.values(wpRequire.c).find(x => x?.exports?.Bo?.get).exports.Bo;
} else {
RunningGameStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getRunningGames).exports.ZP;
QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getQuest).exports.Z;
ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getAllThreadsForParent).exports.Z;
GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getSFWDefaultChannel).exports.ZP;
FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.flushWaitQueue).exports.Z;
api = Object.values(wpRequire.c).find(x => x?.exports?.tn?.get).exports.tn;
}

const supportedTasks = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE"]
let quests = [...QuestsStore.quests.values()].filter(x => x.userStatus?.enrolledAt && !x.userStatus?.completedAt && new Date(x.config.expiresAt).getTime() > Date.now() && supportedTasks.find(y => Object.keys((x.config.taskConfig ?? x.config.taskConfigV2).tasks).includes(y)))
let isApp = typeof DiscordNative !== "undefined"
if(quests.length === 0) {
console.log("You don't have any uncompleted quests!")
} else {
let doJob = function() {
const quest = quests.pop()
if(!quest) return

const pid = Math.floor(Math.random() * 30000) + 1000

const applicationId = quest.config.application.id
const applicationName = quest.config.application.name
const questName = quest.config.messages.questName
const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2
const taskName = supportedTasks.find(x => taskConfig.tasks[x] != null)
const secondsNeeded = taskConfig.tasks[taskName].target
let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0

if(taskName === "WATCH_VIDEO" || taskName === "WATCH_VIDEO_ON_MOBILE") {
const maxFuture = 10, speed = 7, interval = 1
const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime()
let completed = false
let fn = async () => {
while(true) {
const maxAllowed = Math.floor((Date.now() - enrolledAt)/1000) + maxFuture
const diff = maxAllowed - secondsDone
const timestamp = secondsDone + speed
if(diff >= speed) {
const res = await api.post({url: `/quests/${quest.id}/video-progress`, body: {timestamp: Math.min(secondsNeeded, timestamp + Math.random())}})
completed = res.body.completed_at != null
secondsDone = Math.min(secondsNeeded, timestamp)
}

if(timestamp >= secondsNeeded) {
break
}
await new Promise(resolve => setTimeout(resolve, interval * 1000))
}
if(!completed) {
await api.post({url: `/quests/${quest.id}/video-progress`, body: {timestamp: secondsNeeded}})
}
console.log("Quest completed!")
doJob()
}
fn()
console.log(`Spoofing video for ${questName}.`)
} else if(taskName === "PLAY_ON_DESKTOP") {
if(!isApp) {
console.log("This no longer works in browser for non-video quests. Use the discord desktop app to complete the", questName, "quest!")
} else {
api.get({url: `/applications/public?application_ids=${applicationId}`}).then(res => {
const appData = res.body[0]
const exeName = appData.executables.find(x => x.os === "win32").name.replace(">","")

const fakeGame = {
cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`,
exeName,
exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`,
hidden: false,
isLauncher: false,
id: applicationId,
name: appData.name,
pid: pid,
pidPath: [pid],
processName: appData.name,
start: Date.now(),
}
const realGames = RunningGameStore.getRunningGames()
const fakeGames = [fakeGame]
const realGetRunningGames = RunningGameStore.getRunningGames
const realGetGameForPID = RunningGameStore.getGameForPID
RunningGameStore.getRunningGames = () => fakeGames
RunningGameStore.getGameForPID = (pid) => fakeGames.find(x => x.pid === pid)
FluxDispatcher.dispatch({type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: fakeGames})

let fn = data => {
let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value)
console.log(`Quest progress: ${progress}/${secondsNeeded}`)

if(progress >= secondsNeeded) {
console.log("Quest completed!")

RunningGameStore.getRunningGames = realGetRunningGames
RunningGameStore.getGameForPID = realGetGameForPID
FluxDispatcher.dispatch({type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: []})
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn)

doJob()
}
}
FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn)

console.log(`Spoofed your game to ${applicationName}. Wait for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`)
})
}
} else if(taskName === "STREAM_ON_DESKTOP") {
if(!isApp) {
console.log("This no longer works in browser for non-video quests. Use the discord desktop app to complete the", questName, "quest!")
} else {
let realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata
ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({
id: applicationId,
pid,
sourceName: null
})

let fn = data => {
let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value)
console.log(`Quest progress: ${progress}/${secondsNeeded}`)

if(progress >= secondsNeeded) {
console.log("Quest completed!")

ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn)

doJob()
}
}
FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn)

console.log(`Spoofed your stream to ${applicationName}. Stream any window in vc for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`)
console.log("Remember that you need at least 1 other person to be in the vc!")
}
} else if(taskName === "PLAY_ACTIVITY") {
const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id ?? Object.values(GuildChannelStore.getAllGuilds()).find(x => x != null && x.VOCAL.length > 0).VOCAL[0].channel.id
const streamKey = `call:${channelId}:1`

let fn = async () => {
console.log("Completing quest", questName, "-", quest.config.messages.questName)

while(true) {
const res = await api.post({url: `/quests/${quest.id}/heartbeat`, body: {stream_key: streamKey, terminal: false}})
const progress = res.body.progress.PLAY_ACTIVITY.value
console.log(`Quest progress: ${progress}/${secondsNeeded}`)

await new Promise(resolve => setTimeout(resolve, 20 * 1000))

if(progress >= secondsNeeded) {
await api.post({url: `/quests/${quest.id}/heartbeat`, body: {stream_key: streamKey, terminal: true}})
break
}
}

console.log("Quest completed!")
doJob()
}
fn()
}
}
doJob()
}
코드 이해용으로 단 주석 버전(by Claude)
/*
============================================================
* 기능: Discord의 Quest 시스템을 우회하여 자동 완료
* 지원 Quest 유형:
* - WATCH_VIDEO / WATCH_VIDEO_ON_MOBILE: 비디오 시청
* - PLAY_ON_DESKTOP: 게임 실행
* - STREAM_ON_DESKTOP: 화면 공유
* - PLAY_ACTIVITY: 음성 채널 액티비티
*
* 작동 원리:
* 1. Discord 클라이언트의 내부 모듈 시스템에 접근
* 2. 게임 실행 상태를 조작하거나 API 직접 호출
* 3. 서버에 정상적인 진행 상태로 위장하여 전송
* ============================================================
*/

// ============================================================
// 0. 환경 초기화
// ============================================================
// jQuery 등 다른 라이브러리와의 충돌 방지를 위해 전역 변수 $ 제거
delete window.$;


// ============================================================
// 1. Webpack 모듈 로더(require 함수) 탈취
// ============================================================
// Discord는 Webpack으로 번들링되어 있으며, 모든 내부 모듈은
// webpackChunkdiscord_app 배열을 통해 관리됩니다.
// 이 배열에 임시 모듈을 push하여 require 함수를 추출합니다.
let wpRequire = webpackChunkdiscord_app.push([
[Symbol()], // 고유 식별자
{}, // 빈 모듈 정의
r => r // require 함수를 반환하는 콜백
]);
webpackChunkdiscord_app.pop(); // 주입했던 임시 모듈 제거 (흔적 제거)


// ============================================================
// 2. 필요한 내부 Store 및 유틸리티 모듈 탐색
// ============================================================
// Discord 내부 코드는 난독화되어 있고 버전마다 구조가 다를 수 있습니다.
// 특정 메서드 시그니처를 가진 모듈을 찾아 필요한 Store를 추출합니다.

// ApplicationStreamingStore: 화면 공유(스트리밍) 상태 관리
let ApplicationStreamingStore = Object.values(wpRequire.c).find(
x => x?.exports?.Z?.__proto__?.getStreamerActiveStreamMetadata
)?.exports?.Z;

// 나머지 Store들을 담을 변수 선언
let RunningGameStore, // 실행 중인 게임 목록 관리
QuestsStore, // Quest 정보 및 진행 상태 관리
ChannelStore, // 채널 정보 관리
GuildChannelStore, // 길드(서버) 채널 정보 관리
FluxDispatcher, // 이벤트 발행/구독 시스템
api; // HTTP API 클라이언트


// ============================================================
// 2-1. 모듈 Export 패턴 분기 처리
// ============================================================
// Discord 버전에 따라 모듈이 exports.A 또는 exports.Z 등에 위치할 수 있습니다.
// 두 가지 패턴을 모두 지원하여 호환성을 확보합니다.

if(!ApplicationStreamingStore) {
// Pattern A: exports.A, exports.Ay, exports.h, exports.Bo
// (일부 Discord 버전에서 사용)
ApplicationStreamingStore = Object.values(wpRequire.c).find(
x => x?.exports?.A?.__proto__?.getStreamerActiveStreamMetadata
).exports.A;

RunningGameStore = Object.values(wpRequire.c).find(
x => x?.exports?.Ay?.getRunningGames
).exports.Ay;

QuestsStore = Object.values(wpRequire.c).find(
x => x?.exports?.A?.__proto__?.getQuest
).exports.A;

ChannelStore = Object.values(wpRequire.c).find(
x => x?.exports?.A?.__proto__?.getAllThreadsForParent
).exports.A;

GuildChannelStore = Object.values(wpRequire.c).find(
x => x?.exports?.Ay?.getSFWDefaultChannel
).exports.Ay;

FluxDispatcher = Object.values(wpRequire.c).find(
x => x?.exports?.h?.__proto__?.flushWaitQueue
).exports.h;

api = Object.values(wpRequire.c).find(
x => x?.exports?.Bo?.get
).exports.Bo;

} else {
// Pattern B: exports.Z, exports.ZP, exports.tn
// (대부분의 Discord 버전에서 사용)
RunningGameStore = Object.values(wpRequire.c).find(
x => x?.exports?.ZP?.getRunningGames
).exports.ZP;

QuestsStore = Object.values(wpRequire.c).find(
x => x?.exports?.Z?.__proto__?.getQuest
).exports.Z;

ChannelStore = Object.values(wpRequire.c).find(
x => x?.exports?.Z?.__proto__?.getAllThreadsForParent
).exports.Z;

GuildChannelStore = Object.values(wpRequire.c).find(
x => x?.exports?.ZP?.getSFWDefaultChannel
).exports.ZP;

FluxDispatcher = Object.values(wpRequire.c).find(
x => x?.exports?.Z?.__proto__?.flushWaitQueue
).exports.Z;

api = Object.values(wpRequire.c).find(
x => x?.exports?.tn?.get
).exports.tn;
}


// ============================================================
// 3. 수행 가능한 Quest 목록 필터링
// ============================================================
// 이 스크립트가 처리할 수 있는 Quest 작업 유형 정의
const supportedTasks = [
"WATCH_VIDEO", // 비디오 시청 (브라우저/앱)
"PLAY_ON_DESKTOP", // 게임 실행 (앱 전용)
"STREAM_ON_DESKTOP", // 화면 공유 (앱 전용)
"PLAY_ACTIVITY", // 액티비티 참여
"WATCH_VIDEO_ON_MOBILE" // 모바일 비디오 시청
];

// QuestsStore에서 현재 진행 가능한 Quest 추출
// 필터링 조건:
// 1. 등록됨 (userStatus.enrolledAt 존재)
// 2. 미완료 (!userStatus.completedAt)
// 3. 기간 만료 전 (config.expiresAt > 현재 시각)
// 4. 지원하는 작업 유형 포함
let quests = [...QuestsStore.quests.values()].filter(x =>
x.userStatus?.enrolledAt &&
!x.userStatus?.completedAt &&
new Date(x.config.expiresAt).getTime() > Date.now() &&
supportedTasks.find(y =>
Object.keys((x.config.taskConfig ?? x.config.taskConfigV2).tasks).includes(y)
)
);

// 실행 환경 감지
// DiscordNative 객체 존재 여부로 데스크톱 앱/브라우저 구분
let isApp = typeof DiscordNative !== "undefined";


// ============================================================
// 4. Quest 수행 메인 로직
// ============================================================
if(quests.length === 0) {
// 수행할 Quest가 없는 경우
console.log("You don't have any uncompleted quests!");

} else {
// --------------------------------------------------------
// 4-1. Quest 순차 처리 함수 (재귀 호출)
// --------------------------------------------------------
let doJob = function() {
const quest = quests.pop(); // 배열에서 Quest 하나를 가져옴
if(!quest) return; // 모든 Quest 완료 시 종료

// 가짜 프로세스 ID 생성 (1000~30999 범위)
const pid = Math.floor(Math.random() * 30000) + 1000;

// Quest 설정값 추출
const applicationId = quest.config.application.id; // 대상 앱 ID
const applicationName = quest.config.application.name; // 대상 앱 이름
const questName = quest.config.messages.questName; // Quest 표시 이름
const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2; // 작업 설정
const taskName = supportedTasks.find(x => taskConfig.tasks[x] != null); // 실제 작업 유형
const secondsNeeded = taskConfig.tasks[taskName].target; // 목표 시간(초)
let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; // 현재 진행 시간


// ========================================================
// [CASE 1] 비디오 시청 Quest
// ========================================================
if(taskName === "WATCH_VIDEO" || taskName === "WATCH_VIDEO_ON_MOBILE") {
// 시청 진행 시뮬레이션 파라미터
const maxFuture = 10; // 서버가 허용하는 미래 타임스탬프 허용치 (초)
const speed = 7; // 각 요청당 진행시킬 시간 (초)
const interval = 1; // API 요청 간격 (초)

const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
let completed = false;

let fn = async () => {
while(true) {
// 서버 측 타임스탬프 검증 우회
// Quest 등록 후 경과 시간을 기준으로 "허용 가능한 최대 진행 시간" 계산
const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
const diff = maxAllowed - secondsDone;
const timestamp = secondsDone + speed;

if(diff >= speed) {
// Discord Quest API 호출
// POST /quests/{quest_id}/video-progress
// - body: { timestamp: number } - 현재 비디오 시청 시간(초)
// - response: { completed_at: string | null } - 완료 시각
const res = await api.post({
url: `/quests/${quest.id}/video-progress`,
body: {timestamp: Math.min(secondsNeeded, timestamp + Math.random())}
});
completed = res.body.completed_at != null;
secondsDone = Math.min(secondsNeeded, timestamp);
}

// 목표 시간 도달 시 루프 종료
if(timestamp >= secondsNeeded) {
break;
}

// 다음 요청까지 대기
await new Promise(resolve => setTimeout(resolve, interval * 1000));
}

// 완료 확정 요청 (completed_at이 null인 경우)
if(!completed) {
await api.post({
url: `/quests/${quest.id}/video-progress`,
body: {timestamp: secondsNeeded}
});
}

console.log("Quest completed!");
doJob(); // 다음 Quest 수행
};

fn();
console.log(`Spoofing video for ${questName}.`);


// ========================================================
// [CASE 2] 데스크톱 게임 실행 Quest
// ========================================================
} else if(taskName === "PLAY_ON_DESKTOP") {
if(!isApp) {
// 브라우저에서는 게임 감지 기능을 사용할 수 없음
console.log("This no longer works in browser for non-video quests. Use the discord desktop app to complete the", questName, "quest!");
} else {
// 대상 애플리케이션의 실행 파일 정보 조회
// GET /applications/public?application_ids={id}
api.get({url: `/applications/public?application_ids=${applicationId}`}).then(res => {
const appData = res.body[0];
const exeName = appData.executables.find(x => x.os === "win32").name.replace(">","");

// 가짜 게임 프로세스 데이터 생성
// Discord가 게임을 감지할 때 저장하는 형식과 동일하게 구성
const fakeGame = {
cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`,
exeName,
exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`,
hidden: false,
isLauncher: false,
id: applicationId,
name: appData.name,
pid: pid,
pidPath: [pid],
processName: appData.name,
start: Date.now(),
};

// 원본 함수 백업 (나중에 복구용)
const realGames = RunningGameStore.getRunningGames();
const fakeGames = [fakeGame];
const realGetRunningGames = RunningGameStore.getRunningGames;
const realGetGameForPID = RunningGameStore.getGameForPID;

// Monkey Patching (런타임 함수 대체)
// RunningGameStore의 메서드를 일시적으로 덮어씌워
// Discord가 실제로는 실행되지 않은 게임을 실행 중으로 인식하도록 속입니다.
RunningGameStore.getRunningGames = () => fakeGames;
RunningGameStore.getGameForPID = (pid) => fakeGames.find(x => x.pid === pid);

// Flux 이벤트 발행: 게임 상태 변경 알림
// Discord 클라이언트 전체에 "새 게임이 실행되었다"는 이벤트를 전파
FluxDispatcher.dispatch({
type: "RUNNING_GAMES_CHANGE",
removed: realGames,
added: [fakeGame],
games: fakeGames
});

// Quest 하트비트 성공 이벤트 구독
// Discord는 주기적으로 Quest 진행 상황을 서버에 전송(하트비트)하며,
// 성공 시 이 이벤트가 발생합니다.
let fn = data => {
// 진행률 추출 (config 버전에 따라 경로가 다름)
let progress = quest.config.configVersion === 1
? data.userStatus.streamProgressSeconds
: Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value);

console.log(`Quest progress: ${progress}/${secondsNeeded}`);

if(progress >= secondsNeeded) {
console.log("Quest completed!");

// Cleanup: 원본 함수 복구
RunningGameStore.getRunningGames = realGetRunningGames;
RunningGameStore.getGameForPID = realGetGameForPID;

// 게임 종료 상태로 이벤트 발행
FluxDispatcher.dispatch({
type: "RUNNING_GAMES_CHANGE",
removed: [fakeGame],
added: [],
games: []
});

// 이벤트 구독 해제
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);

doJob(); // 다음 Quest 수행
}
};

// 이벤트 구독 시작
FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);

console.log(`Spoofed your game to ${applicationName}. Wait for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`);
});
}


// ========================================================
// [CASE 3] 화면 공유(스트리밍) Quest
// ========================================================
} else if(taskName === "STREAM_ON_DESKTOP") {
if(!isApp) {
console.log("This no longer works in browser for non-video quests. Use the discord desktop app to complete the", questName, "quest!");
} else {
// 원본 함수 백업
let realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata;

// Monkey Patching: 스트리밍 메타데이터 위조
// Discord가 스트리밍 상태를 확인할 때 호출하는 함수를 덮어씌워
// 실제로는 스트리밍하지 않지만 특정 앱을 스트리밍 중인 것처럼 반환
ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({
id: applicationId, // 목표 애플리케이션 ID
pid, // 가짜 프로세스 ID
sourceName: null // 스트리밍 소스 이름
});

// Quest 하트비트 성공 이벤트 구독
let fn = data => {
let progress = quest.config.configVersion === 1
? data.userStatus.streamProgressSeconds
: Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value);

console.log(`Quest progress: ${progress}/${secondsNeeded}`);

if(progress >= secondsNeeded) {
console.log("Quest completed!");

// 원본 함수 복구
ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc;

// 이벤트 구독 해제
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);

doJob(); // 다음 Quest 수행
}
};

FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);

console.log(`Spoofed your stream to ${applicationName}. Stream any window in vc for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`);
console.log("Remember that you need at least 1 other person to be in the vc!");
}


// ========================================================
// [CASE 4] 액티비티 참여 Quest
// ========================================================
} else if(taskName === "PLAY_ACTIVITY") {
// 활성화된 음성 채널 찾기
// DM 채널 우선, 없으면 길드의 음성 채널 사용
const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id
?? Object.values(GuildChannelStore.getAllGuilds())
.find(x => x != null && x.VOCAL.length > 0)
.VOCAL[0].channel.id;

// 스트림 키 생성 (채널 ID 기반)
const streamKey = `call:${channelId}:1`;

let fn = async () => {
console.log("Completing quest", questName, "-", quest.config.messages.questName);

while(true) {
// Discord Quest API에 직접 하트비트 전송
// POST /quests/{quest_id}/heartbeat
// - body: { stream_key: string, terminal: boolean }
// - stream_key: 음성 채널 식별자
// - terminal: 완료 신호 (false=진행 중, true=완료)
const res = await api.post({
url: `/quests/${quest.id}/heartbeat`,
body: {stream_key: streamKey, terminal: false}
});

const progress = res.body.progress.PLAY_ACTIVITY.value;
console.log(`Quest progress: ${progress}/${secondsNeeded}`);

// 20초 대기 (Discord의 하트비트 주기와 유사)
await new Promise(resolve => setTimeout(resolve, 20 * 1000));

if(progress >= secondsNeeded) {
// 완료 신호 전송 (terminal: true)
await api.post({
url: `/quests/${quest.id}/heartbeat`,
body: {stream_key: streamKey, terminal: true}
});
break;
}
}

console.log("Quest completed!");
doJob(); // 다음 Quest 수행
};

fn();
}
};

// --------------------------------------------------------
// Quest 처리 시작
// --------------------------------------------------------
doJob();
}
  1. 원하는 퀘스트 수락하기

    • 스크립트를 입력하기 전 본인이 원하는 퀘스트를 수락한다.
    • 한번에 여러개의 퀘스트를 수락해도 된다.
  2. Discord 클라이언트에서 개발자 도구(DevTools)를 연다.

    • 단축키: Ctrl + Shift + I (Windows) / Cmd + Option + I (macOS)
  3. 콘솔 탭에서 복사한 스크립트를 붙여넣고 실행한다. script1.png

  4. 스크립트가 자동으로 퀘스트를 완료하고 Orbs를 지급받는다. script2.png

맥에서는 실행할 수 없는 PC 전용 퀘스트도 문제없이 완료되었다.

결론

이 스크립트를 활용하면 실제로 영상을 시청하거나 게임을 플레이하지 않고도 Discord Orbs Quest를 완료할 수 있다. 이미 누군가가 만들어둔 스크립트를 찾아 사용하는 것만으로도 시간을 크게 절약할 수 있었다. 역시 인터넷에는 없는 게 없다는 생각이 든다.

다만 이러한 방법은 Discord의 서비스 약관에 위배될 가능성이 있다는 점을 잊지 말아야 한다. 계정 정지 등의 위험을 감수할 수 있다면 본인의 책임 하에 사용하도록 하자.

Footnotes

  1. How can I do a Discord quest without having the game downloaded? - r/discordapp, 2025.10.1, https://www.reddit.com/r/discordapp/comments/1nufllj/ (접근: 2026.02.02) [아카이브]