├── .gitignore ├── package.json ├── README.md ├── .github └── workflows │ └── mine.yml └── src ├── index.js └── download.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | 4 | tmp 5 | out -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | {"type":"module","dependencies":{"@electron/asar":"^3.2.3"}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discord-desktop-datamining 2 | 3 | ## branches 4 | 5 | - [stable](https://github.com/OpenAsar/discord-desktop-datamining/tree/stable) 6 | - [ptb](https://github.com/OpenAsar/discord-desktop-datamining/tree/ptb) 7 | - [canary](https://github.com/OpenAsar/discord-desktop-datamining/tree/canary) 8 | - [development](https://github.com/OpenAsar/discord-desktop-datamining/tree/development) 9 | -------------------------------------------------------------------------------- /.github/workflows/mine.yml: -------------------------------------------------------------------------------- 1 | name: Mine 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 * * * *' 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | mine: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | channel: ['stable', 'ptb', 'canary', 'development'] 16 | 17 | env: 18 | CHECKOUT_DIR: ${{ github.workspace }}/checkout 19 | 20 | steps: 21 | - name: Checkout script 22 | uses: actions/checkout@v3 23 | 24 | - name: Checkout result 25 | uses: actions/checkout@v3 26 | with: 27 | ref: ${{ matrix.channel }} 28 | path: ${{ env.CHECKOUT_DIR }} 29 | continue-on-error: true 30 | 31 | - name: Setup Node 32 | uses: actions/setup-node@v2 33 | with: 34 | node-version: '18' 35 | 36 | - name: Run 37 | run: | 38 | npm install 39 | node src ${{ matrix.channel }} "${{ env.CHECKOUT_DIR }}/manifest.json" 40 | 41 | - name: Publish 42 | run: | 43 | rm -rf ${{ env.CHECKOUT_DIR }}/* 44 | cp -rf out/. ${{ env.CHECKOUT_DIR }} 45 | 46 | echo "" > /tmp/changes.txt 47 | cp -rf changes.txt /tmp/changes.txt || : 48 | 49 | cd ${{ env.CHECKOUT_DIR }} 50 | 51 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 52 | git config --local user.name "github-actions[bot]" 53 | git config --local core.symlinks true 54 | 55 | echo "*.exe" > .gitignore 56 | 57 | git add . 58 | git diff-index --quiet HEAD || git commit -m "${{ matrix.channel }} update" -m "$(cat /tmp/changes.txt)" 59 | git push origin ${{ matrix.channel }} 60 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import download, { _cache, baseOutDir } from './download.js'; 2 | import { join } from 'path'; 3 | import { cpSync, writeFileSync, readFileSync, existsSync } from 'fs'; 4 | 5 | global.LATEST_ONLY = true; 6 | 7 | const modules = [ 8 | 'host', 9 | 'desktop_core', 10 | 'desktop_overlay', 11 | 'krisp', 12 | 'dispatch', 13 | 'utils', 14 | 'media', 15 | 'sekrit', 16 | 'spellcheck', 17 | 'hook', 18 | 'modules', 19 | 'ml', 20 | 'notifications', 21 | 'overlay2', 22 | 'game_utils', 23 | 'voice', 24 | 'voice_filters', 25 | 'vigilante', 26 | 'rpc', 27 | 'erlpack', 28 | 'cloudsync', 29 | 'zstd', 30 | ]; 31 | 32 | let [ channel = 'canary', oldManifest = '' ] = process.argv.slice(2); 33 | if (oldManifest && existsSync(oldManifest)) oldManifest = JSON.parse(readFileSync(oldManifest)); 34 | else oldManifest = false; 35 | 36 | for (const mod of modules) { 37 | await download(channel, mod); 38 | } 39 | 40 | const manifestToVersions = manifest => ({ 41 | host: manifest.full.host_version.join('.'), 42 | ...Object.keys(manifest.modules).reduce((acc, x) => { 43 | acc[x.replace('discord_', '')] = manifest.modules[x].full.module_version; 44 | return acc; 45 | }, {}) 46 | }); 47 | 48 | // hack to get manifest from download 49 | const manifest = Object.values(_cache)[0]; 50 | const versions = manifestToVersions(manifest); 51 | writeFileSync(join(baseOutDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); 52 | 53 | if (oldManifest) { 54 | const oldVersions = manifestToVersions(oldManifest); 55 | 56 | const updated = []; 57 | for (const x in oldVersions) { 58 | if (oldVersions[x] !== versions[x]) updated.push([ x, oldVersions[x], versions[x] ]); 59 | } 60 | 61 | writeFileSync('changes.txt', updated.map(([ name, o, n ]) => `${name}: ${o} -> ${n}`).join('\n')); 62 | } 63 | 64 | writeFileSync(join(baseOutDir, 'README.md'), `# discord-desktop-datamining 65 | 66 | ## ${channel} versions 67 | 68 | **host: ${versions.host}** 69 | 70 | | module | version | 71 | | ------ | :-----: | 72 | ${Object.keys(versions).filter(x => x !== 'host').sort().map(x => `| ${x} | ${versions[x]} |`).join('\n')} 73 | 74 | ## branches 75 | 76 | - [stable](https://github.com/OpenAsar/discord-desktop-datamining/tree/stable) 77 | - [ptb](https://github.com/OpenAsar/discord-desktop-datamining/tree/ptb) 78 | - [canary](https://github.com/OpenAsar/discord-desktop-datamining/tree/canary) 79 | - [development](https://github.com/OpenAsar/discord-desktop-datamining/tree/development)`) 80 | 81 | const hostVersion = manifest.full.host_version.join('.'); 82 | if (!LATEST_ONLY) cpSync(join(baseOutDir, hostVersion), join(baseOutDir, 'latest'), { recursive: true }); 83 | -------------------------------------------------------------------------------- /src/download.js: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { get } from 'https'; 4 | import zlib from 'zlib'; 5 | import cp from 'child_process'; 6 | import fs from 'fs'; 7 | import asar from '@electron/asar'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | export const baseOutDir = join(__dirname, '..', 'out'); 13 | 14 | export const _cache = {}; 15 | const fetchJson = async url => { 16 | if (_cache[url]) return _cache[url]; 17 | return _cache[url] = await (await fetch(url)).json(); 18 | }; 19 | 20 | const extractAsars = dir => { 21 | if (!fs.existsSync(dir)) return; 22 | 23 | for (const f of fs.readdirSync(dir)) { 24 | const p = join(dir, f); 25 | if (f.endsWith('.asar')) { 26 | console.log('extracting', f); 27 | 28 | asar.extractAll(p, p.replace('.asar', '')); 29 | } 30 | } 31 | }; 32 | 33 | 34 | export default async (channel, mod, version) => { 35 | const manifest = await fetchJson(`https://discord.com/api/updates/distributions/app/manifests/latest?platform=win&channel=${channel}&arch=x64`); 36 | 37 | const hostVersion = manifest.full.host_version.join('.'); 38 | version = version ?? (mod === 'host' ? manifest.full.host_version[2] : manifest.modules['discord_' + mod]?.full?.module_version); 39 | if (!version) return; 40 | 41 | const domain = `https://dl${channel === 'stable' ? '' : `-${channel}`}.discordapp.net`; 42 | 43 | // const downloadUrl = (mod === 'host' ? manifest : manifest.modules['discord_' + mod]).full.url; 44 | const downloadUrl = mod === 'host' ? `${domain}/distro/app/${channel}/win/x64/1.0.${version}/full.distro` : `${domain}/distro/app/${channel}/win/x64/${hostVersion}/discord_${mod}/${version}/full.distro`; 45 | console.log('DOWNLOADING', mod, version, '|', downloadUrl); 46 | 47 | const outDir = join(baseOutDir, hostVersion, mod === 'host' ? 'host' : `modules`, mod === 'host' ? '' : mod); 48 | 49 | const tarPath = join(__dirname, '..', 'tmp', mod + '-' + version + '.tar'); 50 | let finalPath = join(outDir, mod === 'host' ? '' : `${version}`); 51 | if (global.LATEST_ONLY) finalPath = join(baseOutDir, mod === 'host' ? 'host' : `modules`, mod === 'host' ? '' : mod); 52 | 53 | fs.rmSync(tarPath, { force: true }); 54 | fs.rmSync(finalPath, { recursive: true, force: true }); 55 | 56 | await fs.promises.mkdir(dirname(tarPath)).catch(_ => {}); 57 | 58 | const stream = zlib.createBrotliDecompress(); 59 | stream.pipe(fs.createWriteStream(tarPath)); 60 | 61 | let downloadTotal = 0, downloadCurrent = 0; 62 | get(downloadUrl, res => { // query for caching 63 | res.pipe(stream); 64 | 65 | downloadTotal = parseInt(res.headers['content-length'] ?? 1, 10); 66 | 67 | res.on('data', c => { 68 | downloadCurrent += c.length; 69 | 70 | // console.log((downloadCurrent / downloadTotal) * 100); 71 | }); 72 | }); 73 | 74 | await new Promise(res => stream.on('end', res)); 75 | 76 | await fs.promises.mkdir(finalPath, { recursive: true }).catch(_ => {}); 77 | 78 | const proc = cp.execFile('tar', [ '--strip-components', '1', '-xf', tarPath, '-C', finalPath]); 79 | await new Promise(res => proc.on('close', res)); 80 | 81 | console.log('DOWNLOADED', finalPath); 82 | 83 | for (const p of [ 84 | finalPath, 85 | join(finalPath, 'resources') 86 | ]) extractAsars(p); 87 | 88 | if (mod !== 'host' && !global.LATEST_ONLY) { 89 | fs.cpSync(finalPath, join(outDir, 'latest'), { recursive: true }); 90 | } 91 | }; 92 | --------------------------------------------------------------------------------