commit fee8ab1a0c068fa4cd46686f7df3230215471229 Author: Yorgei Date: Tue Jan 13 17:52:57 2026 +1000 init diff --git a/.gitea/.DS_Store b/.gitea/.DS_Store new file mode 100644 index 0000000..1f2c43e Binary files /dev/null and b/.gitea/.DS_Store differ diff --git a/.gitea/workflows/auto-release.yml b/.gitea/workflows/auto-release.yml new file mode 100644 index 0000000..b4fb8e6 --- /dev/null +++ b/.gitea/workflows/auto-release.yml @@ -0,0 +1,112 @@ +name: Auto Release + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + update-and-release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install requests + + - name: Configure Git + run: | + git config user.name "Gitea Actions" + git config user.email "actions@gitea.local" + + - name: Get file hash before update + id: before + run: | + if [ -f discord-quest.js ]; then + echo "hash=$(sha256sum discord-quest.js | cut -d' ' -f1)" >> $GITHUB_OUTPUT + else + echo "hash=" >> $GITHUB_OUTPUT + fi + + - name: Run update script + id: update + env: + GITEA_ACTIONS: true + run: python main.py || true + + - name: Get file hash after update + id: after + run: | + if [ -f discord-quest.js ]; then + echo "hash=$(sha256sum discord-quest.js | cut -d' ' -f1)" >> $GITHUB_OUTPUT + else + echo "hash=" >> $GITHUB_OUTPUT + fi + + - name: Check for changes + id: changes + run: | + if [ "${{ steps.before.outputs.hash }}" != "${{ steps.after.outputs.hash }}" ]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.changes.outputs.changed == 'true' + run: | + if [ -n "$(git status --porcelain discord-quest.js)" ]; then + git add discord-quest.js + git commit -m "Update discord-quest.js from upstream gist" + git push origin main + fi + + - name: Get latest tag + if: steps.changes.outputs.changed == 'true' + id: tag + run: | + latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo "latest=$latest_tag" >> $GITHUB_OUTPUT + + version=$(echo $latest_tag | sed 's/v//') + IFS='.' read -r major minor patch <<< "$version" + patch=$((patch + 1)) + new_tag="v${major}.${minor}.${patch}" + echo "new=$new_tag" >> $GITHUB_OUTPUT + + - name: Create and push tag + if: steps.changes.outputs.changed == 'true' + run: | + git tag ${{ steps.tag.outputs.new }} + git push origin ${{ steps.tag.outputs.new }} + + - name: Create Gitea release + if: steps.changes.outputs.changed == 'true' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_URL: ${{ secrets.GITEA_URL || github.server_url }} + run: | + REPO_OWNER=$(echo ${{ github.repository }} | cut -d'/' -f1) + REPO_NAME=$(echo ${{ github.repository }} | cut -d'/' -f2) + + API_URL="${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" + + curl -X POST "${API_URL}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"${{ steps.tag.outputs.new }}\", + \"name\": \"Release ${{ steps.tag.outputs.new }}\", + \"body\": \"Automated release: Updated discord-quest.js from upstream gist\", + \"draft\": false, + \"prerelease\": false + }" + diff --git a/discord-quest.js b/discord-quest.js new file mode 100644 index 0000000..09afbc4 --- /dev/null +++ b/discord-quest.js @@ -0,0 +1,166 @@ +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 = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getRunningGames).exports.ZP; +let QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getQuest).exports.Z; +let ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getAllThreadsForParent).exports.Z; +let GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getSFWDefaultChannel).exports.ZP; +let FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.flushWaitQueue).exports.Z; +let 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() +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..86b29a0 --- /dev/null +++ b/main.py @@ -0,0 +1,93 @@ +import requests +import re +import os +import subprocess +import sys + +# Configuration +URL = "https://gist.githubusercontent.com/aamiaa/204cd9d42013ded9faf646fae7f89fbb/raw" +OUTPUT_FILENAME = "discord-quest.js" +REPO_REMOTE = "origin" # Change if your remote is named differently +REPO_BRANCH = "main" # Change if your branch is named differently + +def fetch_and_extract(): + print(f"Fetching content from {URL}...") + try: + response = requests.get(URL) + response.raise_for_status() + content = response.text + except Exception as e: + print(f"Error downloading content: {e}") + sys.exit(1) + + # Regex to find markdown code blocks (``` ... ```) + # This looks for blocks optionally specified as javascript/js + code_blocks = re.findall(r'```(?:javascript|js)?\s*(.*?)```', content, re.DOTALL) + + if not code_blocks: + print("No code blocks found in the fetched text.") + sys.exit(1) + + # Heuristic: Find the block that looks like the actual script. + # Based on the known content, the script usually contains 'webpackChunkdiscord_app' + # If we can't find that specific keyword, we default to the longest block. + target_code = None + for block in code_blocks: + if "webpackChunkdiscord_app" in block: + target_code = block + break + + if not target_code: + print("Could not identify the specific Discord script block. Using the longest code block found.") + target_code = max(code_blocks, key=len) + + return target_code.strip() + +def has_changes(filename): + # Check if the file is modified in git + result = subprocess.run(["git", "status", "--porcelain", filename], capture_output=True, text=True) + return bool(result.stdout.strip()) + +def git_commit_and_push(filename): + try: + print("Changes detected. Committing...") + subprocess.run(["git", "add", filename], check=True) + subprocess.run(["git", "commit", "-m", "Update discord-quest.js from upstream gist"], check=True) + + print("Pushing to Gitea...") + subprocess.run(["git", "push", REPO_REMOTE, REPO_BRANCH], check=True) + print("Successfully pushed changes.") + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + sys.exit(1) + +def main(): + # 1. Extract new code + new_code = fetch_and_extract() + + # 2. Read existing file to compare (avoids git modifying timestamp if identical) + if os.path.exists(OUTPUT_FILENAME): + with open(OUTPUT_FILENAME, 'r', encoding='utf-8') as f: + current_code = f.read().strip() + else: + current_code = None + + # 3. Write file only if content is different + if new_code != current_code: + print(f"Updating {OUTPUT_FILENAME}...") + with open(OUTPUT_FILENAME, 'w', encoding='utf-8') as f: + f.write(new_code + '\n') # Add newline at end of file + else: + print("No changes in content detected. Exiting.") + sys.exit(0) + + # 4. Check Git status and Push (skip in CI - workflow handles it) + if os.getenv("CI") or os.getenv("GITHUB_ACTIONS") or os.getenv("GITEA_ACTIONS"): + print("Running in CI environment. Skipping git operations - workflow will handle commit/push.") + elif has_changes(OUTPUT_FILENAME): + git_commit_and_push(OUTPUT_FILENAME) + else: + print("File updated but git status shows no changes (clean).") + +if __name__ == "__main__": + main() \ No newline at end of file