From 92429f1e83895500f1fd938604f0c7fee4583944 Mon Sep 17 00:00:00 2001 From: Fisto Date: Wed, 16 Sep 2026 07:59:51 +0200 Subject: [PATCH] Add the slopbot scaffold and discord.js dependency, unused --- AGENTS.md | 3 + CURRENT.md | 4 +- docs/slopbot.md | 9 +- media-scrape/README.md | 3 +- media-scrape/package.json | 10 ++ media-scrape/pnpm-lock.yaml | 218 +++++++++++++++++++++++++++++++ media-scrape/pnpm-workspace.yaml | 2 + media-scrape/slopbot/config.ts | 46 +++++++ media-scrape/slopbot/index.ts | 24 ++++ poc/tsconfig.json | 2 +- 10 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 media-scrape/package.json create mode 100644 media-scrape/pnpm-lock.yaml create mode 100644 media-scrape/pnpm-workspace.yaml create mode 100644 media-scrape/slopbot/config.ts create mode 100644 media-scrape/slopbot/index.ts diff --git a/AGENTS.md b/AGENTS.md index 27ffba5..601e174 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,9 @@ before you stop. - Triggers come from trusted users only: one-to-one DMs and direct mentions. - Test with throwaway accounts and private test guilds only. - Never commit `media-scrape/db/`, scrape databases, downloaded media, or the slopbot token. +- Owner interjections become `TODO.md` items unless marked "add this now". Add the item, then + continue the current task. +- No sudo. Record apt or system-level installs in `TODO.md`; the owner runs them. - Live send tests: only DMs to user **zzib** (`177117296140091393`). - Run `pnpm testTypes` and `pnpm lint` before you report done. Run `pnpm build` to compile. diff --git a/CURRENT.md b/CURRENT.md index 017ab9b..4a0cc6e 100644 --- a/CURRENT.md +++ b/CURRENT.md @@ -19,7 +19,9 @@ mobile draft at 390px. bridge for group DMs; one self-contained SQLite + media dir per scrape under `media-scrape/db/` (gitignored); links indexed by default, downloads optional, third-party embeds never downloaded; 30 GiB default limit with a 90% warning and 5% no-delete overrun, usage bar and clean button in - Settings → Bot; Media and Bot top-level tabs; MCP hooks designed for C7. + Settings → Bot; Media and Bot top-level tabs; MCP hooks designed for C7. The slopbot scaffold + (config loader, invite helper) and its discord.js dependency were added 2026-09-16 and stay + unused until C4. Checkpoints, push after each: - [x] C1 design docs, AGENTS/INDEX pointers, `.gitignore` diff --git a/docs/slopbot.md b/docs/slopbot.md index 6ade421..0e1681c 100644 --- a/docs/slopbot.md +++ b/docs/slopbot.md @@ -29,7 +29,7 @@ libraries stands. `attachments`, and `embeds` in both gateway events and REST history responses. Unverified apps in under 100 servers can enable it in the portal. 3. Invite the bot with the `bot` and `applications.commands` scopes and the minimum permissions: - View Channels, Read Message History, Send Messages (for status replies), Embed Links. + View Channels and Read Message History. The bot never sends, so it needs no more. 4. Store the token in the daemon state file `slopbot.json` (mode 600) or `VESKSLOP_SLOPBOT_TOKEN`. Never commit it. The action log redacts token-shaped strings as before. @@ -41,7 +41,8 @@ channels. | Setting | Default | Purpose | | --- | --- | --- | | `VESKSLOP_SLOPBOT_ENABLED` | `0` | Kill switch, off by default | -| `VESKSLOP_SLOPBOT_TOKEN` / `slopbot.json` | unset | Bot token | +| `VESKSLOP_SLOPBOT_TOKEN` | unset | Bot token (preferred) | +| `VESKSLOP_SLOPBOT_STATE` | unset | JSON file with `token` and `clientId` | | `VESKSLOP_SLOPBOT_CLIENT_ID` | unset | For the invite URL helper | ## Code @@ -68,4 +69,6 @@ Slopbot runs with the faster bot budget; the puppet backend uses the slower user ## Status -Design approved 2026-09-16. Implementation at checkpoint C4 in [CURRENT.md](../CURRENT.md). +Design approved 2026-09-16. Scaffold added 2026-09-16: `media-scrape/slopbot/` (config loader and +invite helper) and the discord.js dependency in `media-scrape/package.json`. Unused until +checkpoint C4 in [CURRENT.md](../CURRENT.md). diff --git a/media-scrape/README.md b/media-scrape/README.md index 32b1f78..bb641e9 100644 --- a/media-scrape/README.md +++ b/media-scrape/README.md @@ -18,7 +18,8 @@ media-scrape/ configurable with `VESKSLOP_MEDIA_DB_DIR` and never committed. See [../docs/media.md](../docs/media.md). -The daemon imports this folder. discord.js is declared in `poc/package.json`, like the MCP SDK. +The daemon imports this folder. `media-scrape/` is its own pnpm workspace root (like `poc/`), so +`discord.js` is declared in `media-scrape/package.json` and resolves from `media-scrape/node_modules`. ## Docs diff --git a/media-scrape/package.json b/media-scrape/package.json new file mode 100644 index 0000000..8b96c1d --- /dev/null +++ b/media-scrape/package.json @@ -0,0 +1,10 @@ +{ + "name": "veskslop-media-scrape", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Media archive module for Veskslop: scraper engine, store, Web UI routes, and slopbot.", + "dependencies": { + "discord.js": "^14.27.0" + } +} diff --git a/media-scrape/pnpm-lock.yaml b/media-scrape/pnpm-lock.yaml new file mode 100644 index 0000000..b3db192 --- /dev/null +++ b/media-scrape/pnpm-lock.yaml @@ -0,0 +1,218 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + discord.js: + specifier: ^14.27.0 + version: 14.27.0 + +packages: + + '@discordjs/builders@1.14.1': + resolution: {integrity: sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.6.3': + resolution: {integrity: sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==} + engines: {node: '>=18'} + + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.3': + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.5': + resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@types/node@26.5.1': + resolution: {integrity: sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + discord-api-types@0.38.55: + resolution: {integrity: sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA==} + + discord.js@14.27.0: + resolution: {integrity: sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==} + engines: {node: '>=18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + magic-bytes.js@1.13.1: + resolution: {integrity: sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==} + + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@6.28.1: + resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} + engines: {node: '>=18.17'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@discordjs/builders@1.14.1': + dependencies: + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.55 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.2': + dependencies: + discord-api-types: 0.38.55 + + '@discordjs/rest@2.6.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.5 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.55 + magic-bytes.js: 1.13.1 + tslib: 2.8.1 + undici: 6.28.1 + + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.55 + + '@discordjs/ws@1.2.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.3 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.55 + tslib: 2.8.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.18.1 + + '@sapphire/snowflake@3.5.5': {} + + '@types/node@26.5.1': + dependencies: + undici-types: 8.9.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.5.1 + + '@vladfrangu/async_event_emitter@2.4.7': {} + + discord-api-types@0.38.55: {} + + discord.js@14.27.0: + dependencies: + '@discordjs/builders': 1.14.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.2 + '@discordjs/rest': 2.6.3 + '@discordjs/util': 1.2.0 + '@discordjs/ws': 1.2.3 + '@sapphire/snowflake': 3.5.5 + discord-api-types: 0.38.55 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.13.1 + tslib: 2.8.1 + undici: 6.28.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + fast-deep-equal@3.1.3: {} + + lodash.snakecase@4.1.1: {} + + lodash@4.18.1: {} + + magic-bytes.js@1.13.1: {} + + ts-mixer@6.0.4: {} + + tslib@2.8.1: {} + + undici-types@8.9.0: {} + + undici@6.28.1: {} + + ws@8.21.3: {} diff --git a/media-scrape/pnpm-workspace.yaml b/media-scrape/pnpm-workspace.yaml new file mode 100644 index 0000000..9eb9a68 --- /dev/null +++ b/media-scrape/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +# Keep media-scrape its own workspace root, separate from the Vesktop app. +packages: [] diff --git a/media-scrape/slopbot/config.ts b/media-scrape/slopbot/config.ts new file mode 100644 index 0000000..e8c49b7 --- /dev/null +++ b/media-scrape/slopbot/config.ts @@ -0,0 +1,46 @@ +/* + * Slopbot — the dedicated bot core for media scraping. + * + * Scaffold only: nothing imports this yet. The scrape backend lands at + * checkpoint C4 (see ../CURRENT.md). Setup, limits, and security: docs/slopbot.md. + */ + +import { readFileSync } from "node:fs"; + +export interface SlopbotConfig { + /** Kill switch. Off unless VESKSLOP_SLOPBOT_ENABLED=1 and a token exists. */ + enabled: boolean; + /** Bot token. Never logged, never committed. */ + token: string | null; + /** Application ID, used to render the invite URL. */ + clientId: string | null; +} + +interface SlopbotStateFile { + token?: string; + clientId?: string; +} + +function readStateFile(path: string): SlopbotStateFile { + try { + return JSON.parse(readFileSync(path, "utf8")) as SlopbotStateFile; + } catch { + return {}; + } +} + +/** Read the slopbot settings from the environment, then from the state file. */ +export function loadSlopbotConfig( + env: NodeJS.ProcessEnv = process.env, + stateFilePath = env.VESKSLOP_SLOPBOT_STATE +): SlopbotConfig { + const state = stateFilePath ? readStateFile(stateFilePath) : {}; + const token = env.VESKSLOP_SLOPBOT_TOKEN ?? state.token ?? null; + const clientId = env.VESKSLOP_SLOPBOT_CLIENT_ID ?? state.clientId ?? null; + + return { + enabled: env.VESKSLOP_SLOPBOT_ENABLED === "1" && token !== null, + token, + clientId + }; +} diff --git a/media-scrape/slopbot/index.ts b/media-scrape/slopbot/index.ts new file mode 100644 index 0000000..46e60e1 --- /dev/null +++ b/media-scrape/slopbot/index.ts @@ -0,0 +1,24 @@ +/* + * Slopbot entry point. + * + * Scaffold only: the daemon does not import this yet. Checkpoint C4 adds the + * scrape backend; docs/slopbot.md is the module contract. + */ + +import type { Client } from "discord.js"; +import { loadSlopbotConfig, type SlopbotConfig } from "./config.js"; + +/** Kept as a type so the discord.js dependency is type-checked before C4 uses it. */ +export type SlopbotClient = Client; + +export { loadSlopbotConfig, type SlopbotConfig }; + +/** View Channels + Read Message History. The bot reads only; it never sends. */ +const PERMISSIONS = String((1 << 10) | (1 << 16)); + +/** The invite URL the Bot tab renders once a client ID is configured. */ +export function slopbotInviteUrl(clientId: string): string { + return `https://discord.com/oauth2/authorize?client_id=${encodeURIComponent( + clientId + )}&scope=bot%20applications.commands&permissions=${PERMISSIONS}`; +} diff --git a/poc/tsconfig.json b/poc/tsconfig.json index f87becc..4a41f93 100644 --- a/poc/tsconfig.json +++ b/poc/tsconfig.json @@ -12,5 +12,5 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, - "include": ["src"] + "include": ["src", "../media-scrape/slopbot"] }