Wire validated core bridge, migrate daemon to it, verify live
This commit is contained in:
@@ -44,22 +44,22 @@ next steps. Read it before you start. Update it before you stop.
|
|||||||
| Area | Status |
|
| Area | Status |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Action protocol and renderer actions | Implemented |
|
| Action protocol and renderer actions | Implemented |
|
||||||
| Main bridge, event push, hooks | Pending |
|
| Main bridge, event push, hooks | Implemented |
|
||||||
| Daemon on the CDP transport | Implemented and verified live |
|
| Daemon on the core-bridge transport | Implemented and verified live |
|
||||||
| Daemon on the core-bridge transport | Pending |
|
|
||||||
| QR login, pacing, trigger log | Implemented |
|
| QR login, pacing, trigger log | Implemented |
|
||||||
| Auth persistence in the core | Verified, then removed during the CDP pivot. To re-add. |
|
| Auth persistence in the core | Implemented and verified live |
|
||||||
|
| CDP transport | Kept as a development fallback |
|
||||||
| Voice, audio, video | Not started |
|
| Voice, audio, video | Not started |
|
||||||
|
|
||||||
## Work items
|
## Work items
|
||||||
|
|
||||||
See the todo list in the current session. The main items are:
|
See [CURRENT.md](CURRENT.md) for the live list. The main items are:
|
||||||
|
|
||||||
1. Wire the core bridge: `events.ts`, `index.ts`, `puppetBridge.ts`, and the upstream hooks.
|
1. Voice join and leave actions.
|
||||||
2. Re-add auth persistence: pinned `DATA_DIR`, token file, preload seeding, `VESKSLOP_HEADLESS`.
|
2. Audio in and out with PipeWire.
|
||||||
3. Move the daemon to the core-bridge transport.
|
3. Video streaming.
|
||||||
4. Add a mock core for tests.
|
4. Fetch-based message read.
|
||||||
5. Later: voice actions, audio, video, fetch-based read, client lightening, web debug UI.
|
5. Client lightening and the web debug UI.
|
||||||
|
|
||||||
## Dev commands
|
## Dev commands
|
||||||
|
|
||||||
|
|||||||
+17
-13
@@ -11,28 +11,31 @@ into the external daemon. Keep upstream divergence small.
|
|||||||
|
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
- Core bridge wiring. The protocol and the renderer actions exist. The events, the main bridge, and
|
- None. The rework milestone is complete and verified live.
|
||||||
the upstream hooks are not wired yet.
|
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
- Audit of the earlier Vesktop patches. `src/` was reverted to upstream.
|
- Audit of the earlier Vesktop patches. `src/` was reverted to upstream.
|
||||||
- Action protocol and validated renderer actions: `src/shared/puppetProtocol.ts`,
|
- Action protocol and validated renderer actions: `src/shared/puppetProtocol.ts`,
|
||||||
`src/renderer/puppet/actions.ts`.
|
`src/renderer/puppet/actions.ts`.
|
||||||
- CDP feasibility proven. The daemon works on CDP. `get_state` and `send_dm` were verified live.
|
- Core bridge wired: events, renderer init, main bridge, and the upstream hooks.
|
||||||
- QR login verified end-to-end. The token file works.
|
- Auth persistence re-added: pinned `DATA_DIR`, token file, preload seeding, `VESKSLOP_HEADLESS`.
|
||||||
- Pacing policy (500 to 2800 ms), trigger log, and trust model logic live in the daemon.
|
- Daemon moved to the core-bridge transport. CDP stays as a development fallback.
|
||||||
|
- Mock core and trigger test added. The daemon runs without Electron.
|
||||||
|
- Live verification (2026-09-11): `get_state` returned the real account and 7 guilds, `send_dm`
|
||||||
|
reached zzib, and a synthetic owner message flowed through the real event path into the daemon
|
||||||
|
log with the command parsed.
|
||||||
- Module docs in `docs/` and the agent entry point in `AGENTS.md`.
|
- Module docs in `docs/` and the agent entry point in `AGENTS.md`.
|
||||||
|
|
||||||
## Next (in order)
|
## Next (in order)
|
||||||
|
|
||||||
1. Wire the core bridge: `events.ts`, `index.ts`, `puppetBridge.ts`, and the upstream hooks.
|
1. Keep `CURRENT.md`, `AGENTS.md`, and `docs/` in sync with each change.
|
||||||
2. Re-add auth persistence to the core: pinned `DATA_DIR`, token file, preload seeding,
|
2. Voice join and leave actions. Validated actions, then a trigger command.
|
||||||
`VESKSLOP_HEADLESS`.
|
3. Audio in and out with PipeWire.
|
||||||
3. Move the daemon to the core-bridge transport. Keep CDP as a development fallback.
|
4. Video streaming.
|
||||||
4. Add a mock core. Run the daemon tests without Electron.
|
5. Fetch-based message read. The current read is cache-only.
|
||||||
5. Type check, build, and run a live test. The live send target is zzib only.
|
6. Client lightening and the web debug UI.
|
||||||
6. Later: voice join and leave, audio, video, fetch-based read, client lightening, web debug UI.
|
7. Remote client authentication over LAN or Tailscale.
|
||||||
|
|
||||||
## Blocked / needs input
|
## Blocked / needs input
|
||||||
|
|
||||||
@@ -43,4 +46,5 @@ into the external daemon. Keep upstream divergence small.
|
|||||||
- Design documents: `docs/INDEX.md`
|
- Design documents: `docs/INDEX.md`
|
||||||
- Agent rules: `AGENTS.md`
|
- Agent rules: `AGENTS.md`
|
||||||
- Daemon code: `poc/src/`
|
- Daemon code: `poc/src/`
|
||||||
- Core bridge code: `src/shared/puppetProtocol.ts`, `src/renderer/puppet/`
|
- Core bridge code: `src/shared/puppetProtocol.ts`, `src/renderer/puppet/`,
|
||||||
|
`src/main/puppetBridge.ts`
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ when the action list changes.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Action protocol | `src/shared/puppetProtocol.ts` | Implemented |
|
| Action protocol | `src/shared/puppetProtocol.ts` | Implemented |
|
||||||
| Validated actions | `src/renderer/puppet/actions.ts` | Implemented |
|
| Validated actions | `src/renderer/puppet/actions.ts` | Implemented |
|
||||||
| Event push | `src/renderer/puppet/events.ts` | Pending |
|
| Event push | `src/renderer/puppet/events.ts` | Implemented |
|
||||||
| Main bridge | `src/main/puppetBridge.ts` | Pending |
|
| Main bridge | `src/main/puppetBridge.ts` | Implemented |
|
||||||
| Core hooks | `src/main/main.ts`, `src/renderer/ipcCommands.ts`, `src/shared/IpcEvents.ts`, `src/preload/VesktopNative.ts` | Pending |
|
| Core hooks | `src/main/main.ts`, `src/renderer/ipcCommands.ts`, `src/shared/IpcEvents.ts`, `src/preload/VesktopNative.ts` | Implemented |
|
||||||
| Daemon (CDP transport) | `poc/src/` | Implemented and verified live |
|
| Daemon (core-bridge transport) | `poc/src/core-client.ts` | Implemented and verified live |
|
||||||
| Daemon (core-bridge transport) | `poc/src/core-client.ts` | Pending |
|
| Daemon (CDP transport) | `poc/src/cdp-client.ts` | Kept as a development fallback |
|
||||||
| QR login | `poc/src/qr-login.ts` | Implemented and verified live |
|
| QR login | `poc/src/qr-login.ts` | Implemented and verified live |
|
||||||
| Pacing | `poc/src/pacing.ts` | Implemented |
|
| Pacing | `poc/src/pacing.ts` | Implemented |
|
||||||
| Trigger log and trust list | `poc/src/communication.ts`, `poc/src/puppet.ts` | Implemented on the CDP transport |
|
| Trigger log and trust list | `poc/src/communication.ts`, `poc/src/puppet.ts` | Implemented and verified live |
|
||||||
| Voice, audio, video | Not started | Not started |
|
| Voice, audio, video | Not started | Not started |
|
||||||
|
|
||||||
## Data flow
|
## Data flow
|
||||||
|
|||||||
+7
-3
@@ -9,9 +9,13 @@ describes the protocol, the validation rules, and the hooks into upstream files.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `src/shared/puppetProtocol.ts` | Action names, error codes, limits, and shared types | Implemented |
|
| `src/shared/puppetProtocol.ts` | Action names, error codes, limits, and shared types | Implemented |
|
||||||
| `src/renderer/puppet/actions.ts` | Validated action handlers and the core rate limit | Implemented |
|
| `src/renderer/puppet/actions.ts` | Validated action handlers and the core rate limit | Implemented |
|
||||||
| `src/renderer/puppet/events.ts` | Event normalization and push to main | Pending |
|
| `src/renderer/puppet/events.ts` | Event normalization and push to main | Implemented |
|
||||||
| `src/renderer/puppet/index.ts` | Renderer initialization | Pending |
|
| `src/renderer/puppet/index.ts` | Renderer initialization | Implemented |
|
||||||
| `src/main/puppetBridge.ts` | Unix socket server, request forwarding, event fan-out | Pending |
|
| `src/main/puppetBridge.ts` | Unix socket server, request forwarding, event fan-out | Implemented |
|
||||||
|
|
||||||
|
**Event subscription timing.** The event module must wait for the flux dispatcher with
|
||||||
|
`waitFor(filters.byProps("dispatch", "subscribe"), ...)`. A direct subscribe at module load can run
|
||||||
|
before Vencord registers the dispatcher. That mistake crashes the whole renderer bundle.
|
||||||
|
|
||||||
## Transport
|
## Transport
|
||||||
|
|
||||||
|
|||||||
+21
-16
@@ -6,29 +6,27 @@ The daemon runs outside Vesktop. It holds all logic. It talks to the core bridge
|
|||||||
|
|
||||||
| File | Role | Status |
|
| File | Role | Status |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `poc/src/control-bridge.ts` | MCP server, tool list, health endpoint | Implemented on CDP |
|
| `poc/src/control-bridge.ts` | MCP server, tool list, health endpoint | Implemented |
|
||||||
| `poc/src/puppet.ts` | Trust list, command handling, trigger log | Implemented |
|
| `poc/src/puppet.ts` | Trust list, command handling, trigger log | Implemented |
|
||||||
| `poc/src/communication.ts` | Pure log and parser logic | Implemented |
|
| `poc/src/communication.ts` | Pure log and parser logic | Implemented |
|
||||||
| `poc/src/pacing.ts` | Paced queue and query coalescing | Implemented |
|
| `poc/src/pacing.ts` | Paced queue and query coalescing | Implemented |
|
||||||
| `poc/src/cdp-client.ts` | Chrome DevTools Protocol client | Implemented (current transport) |
|
| `poc/src/core-client.ts` | Client actions over the core bridge | Implemented (primary transport) |
|
||||||
| `poc/src/discord.ts` | Client actions over CDP | Implemented (current transport) |
|
| `poc/src/cdp-client.ts` | Chrome DevTools Protocol client | Development fallback |
|
||||||
| `poc/src/core-client.ts` | Client actions over the core bridge | Pending (target transport) |
|
|
||||||
| `poc/src/qr-login.ts` | Terminal QR login | Implemented and verified |
|
| `poc/src/qr-login.ts` | Terminal QR login | Implemented and verified |
|
||||||
| `poc/src/test-communication.ts` | Unit tests for the pure logic | Implemented |
|
| `poc/src/test-communication.ts` | Unit tests for the pure logic | Implemented |
|
||||||
| `poc/src/cdp-probe.ts` | CDP feasibility probe | Implemented (dev tool) |
|
| `poc/src/mock-core.ts` | Mock core bridge for daemon tests | Implemented |
|
||||||
|
| `poc/src/test-trigger.ts` | Inject a trigger event into the mock core | Implemented |
|
||||||
|
| `poc/src/cdp-probe.ts` | CDP feasibility probe | Development tool |
|
||||||
|
| `poc/src/cdp-dispatch.ts` | Run one CDP expression in the client | Development tool |
|
||||||
|
|
||||||
## Transport
|
## Transport
|
||||||
|
|
||||||
The daemon supports two transports.
|
The primary transport is the validated core bridge. The daemon connects to
|
||||||
|
`vesktop-puppet.sock`. All actions go through the validated capability layer. See
|
||||||
|
[core-bridge.md](core-bridge.md).
|
||||||
|
|
||||||
1. **CDP (current)** — the daemon starts Vesktop with `--remote-debugging-port=9222`. The daemon
|
The CDP transport is a development fallback. It needs no Vesktop patch. It evaluates raw code in
|
||||||
connects to the Discord page target. It evaluates the action code in the page and receives
|
the page, so it has no core-side guardrails. Use it for debugging only.
|
||||||
events through a CDP binding. This transport needs no Vesktop patch. The project used it to
|
|
||||||
prove the concept and to test the client.
|
|
||||||
2. **Core bridge (target)** — the daemon connects to `vesktop-puppet.sock`. All actions go through
|
|
||||||
the validated capability layer. This transport gives the core-side guardrails.
|
|
||||||
|
|
||||||
The daemon logic is the same on both transports. Only the client class changes.
|
|
||||||
|
|
||||||
## Pacing
|
## Pacing
|
||||||
|
|
||||||
@@ -56,17 +54,24 @@ The daemon exposes MCP over Streamable HTTP. The default address is `http://127.
|
|||||||
## Run commands
|
## Run commands
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Start the daemon. It connects to the client on CDP port 9222.
|
# Start the daemon. It connects to the core bridge.
|
||||||
pnpm control:bridge
|
pnpm control:bridge
|
||||||
|
|
||||||
|
# Start a mock core for tests without Electron.
|
||||||
|
VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm control:mock
|
||||||
|
|
||||||
|
# Inject a trigger event into the mock core.
|
||||||
|
VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm test:trigger
|
||||||
|
|
||||||
# Send one test DM. The only permitted live target is zzib.
|
# Send one test DM. The only permitted live target is zzib.
|
||||||
pnpm poc:dm
|
pnpm poc:dm
|
||||||
|
|
||||||
# Run the pure-logic unit tests.
|
# Run the pure-logic unit tests.
|
||||||
pnpm test:comm
|
pnpm test:comm
|
||||||
|
|
||||||
# Probe CDP access and events.
|
# Development tools for the CDP fallback.
|
||||||
pnpm cdp:probe
|
pnpm cdp:probe
|
||||||
|
CDP_EXPRESSION='...' pnpm cdp:dispatch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Health endpoint
|
## Health endpoint
|
||||||
|
|||||||
+30
-7
@@ -31,14 +31,35 @@ pnpm build
|
|||||||
|
|
||||||
The build writes the application to `dist/`.
|
The build writes the application to `dist/`.
|
||||||
|
|
||||||
|
## Mock core test
|
||||||
|
|
||||||
|
The mock core runs the daemon without Electron.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Terminal 1: mock core
|
||||||
|
VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm control:mock
|
||||||
|
|
||||||
|
# Terminal 2: daemon
|
||||||
|
VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm control:bridge
|
||||||
|
|
||||||
|
# Terminal 3: inject an owner command
|
||||||
|
VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock TEST_TRIGGER_CONTENT="whitelist list" pnpm test:trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: the daemon log shows one trusted entry with the parsed command. The mock log shows
|
||||||
|
the reply (`message.send`). The reply wait is inside the 500 to 2800 ms window.
|
||||||
|
|
||||||
## CDP probe
|
## CDP probe
|
||||||
|
|
||||||
The probe checks the current CDP transport. Start the client with
|
The probe checks the CDP fallback transport. Start the client with
|
||||||
`--remote-debugging-port=9222` first.
|
`--remote-debugging-port=9222` first.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd poc
|
cd poc
|
||||||
pnpm cdp:probe
|
pnpm cdp:probe
|
||||||
|
|
||||||
|
# Run one expression in the client.
|
||||||
|
CDP_EXPRESSION='Vencord.Webpack.Common.FluxDispatcher.dispatch({...})' pnpm cdp:dispatch
|
||||||
```
|
```
|
||||||
|
|
||||||
A good result shows the Vencord global, the Flux dispatcher, the current user, and one binding
|
A good result shows the Vencord global, the Flux dispatcher, the current user, and one binding
|
||||||
@@ -82,20 +103,22 @@ cd poc
|
|||||||
pnpm poc:dm
|
pnpm poc:dm
|
||||||
```
|
```
|
||||||
|
|
||||||
## CDP live results
|
## Live results
|
||||||
|
|
||||||
The CDP transport was verified on 2026-09-11:
|
The core bridge was verified live on 2026-09-11:
|
||||||
|
|
||||||
- `get_state` returned the real account and 7 guilds.
|
- `get_state` returned the real account and 7 guilds.
|
||||||
- `send_dm` delivered a real DM to zzib.
|
- `send_dm` delivered a real DM to zzib.
|
||||||
- The client ran headless under Xvfb.
|
- A synthetic owner message flowed through the real event path. The daemon log held the entry with
|
||||||
- The token was seeded from the token file.
|
the parsed command.
|
||||||
|
- The client ran headless under Xvfb with the token seeded from the token file.
|
||||||
|
|
||||||
|
The CDP transport was verified earlier with the same checks.
|
||||||
|
|
||||||
## Known gaps
|
## Known gaps
|
||||||
|
|
||||||
| Gap | Effect |
|
| Gap | Effect |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Mock core is not in the tree. | The daemon tests need the real client. |
|
| Live trigger tests need an incoming message. | Use the mock core, or send from zzib's account. |
|
||||||
| The trigger event path needs an incoming message. | Use zzib's account for a live test. |
|
|
||||||
| `message.read` is cache-only. | A channel that the client never opened returns zero messages. |
|
| `message.read` is cache-only. | A channel that the client never opened returns zero messages. |
|
||||||
| Voice, audio, and video are not started. | No test coverage. |
|
| Voice, audio, and video are not started. | No test coverage. |
|
||||||
|
|||||||
+11
-7
@@ -16,19 +16,22 @@ pnpm install
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `pnpm poc:qr` | QR login. Writes the token file. |
|
| `pnpm poc:qr` | QR login. Writes the token file. |
|
||||||
| `pnpm control:bridge` | Start the daemon. It listens on `http://127.0.0.1:8788/mcp`. |
|
| `pnpm control:bridge` | Start the daemon. It listens on `http://127.0.0.1:8788/mcp`. |
|
||||||
|
| `pnpm control:mock` | Start the mock core. Test the daemon without Electron. |
|
||||||
|
| `pnpm test:trigger` | Inject a trigger event into the mock core. |
|
||||||
| `pnpm poc:dm` | Send one test DM to zzib. |
|
| `pnpm poc:dm` | Send one test DM to zzib. |
|
||||||
| `pnpm test:comm` | Run the unit tests for the pure logic. |
|
| `pnpm test:comm` | Run the unit tests for the pure logic. |
|
||||||
| `pnpm cdp:probe` | Check CDP access and event delivery. |
|
| `pnpm cdp:probe` | Check CDP access and event delivery. |
|
||||||
|
| `pnpm cdp:dispatch` | Run one CDP expression in the client. |
|
||||||
| `pnpm check` | Type check the daemon code. |
|
| `pnpm check` | Type check the daemon code. |
|
||||||
|
|
||||||
## Transport
|
## Transport
|
||||||
|
|
||||||
The daemon uses Chrome DevTools Protocol at this time. Start the client with
|
The primary transport is the validated core bridge at `vesktop-puppet.sock`. The daemon sends
|
||||||
`--remote-debugging-port=9222`. The daemon connects to the Discord page and runs the actions in the
|
validated actions and receives normalized events. See
|
||||||
real client.
|
[../docs/core-bridge.md](../docs/core-bridge.md).
|
||||||
|
|
||||||
The target transport is the validated core bridge. See
|
The CDP transport is a development fallback. It needs no Vesktop patch, and it has no
|
||||||
[../docs/core-bridge.md](../docs/core-bridge.md). The daemon logic is the same on both transports.
|
core-side guardrails. Use it for debugging only.
|
||||||
|
|
||||||
## Pacing
|
## Pacing
|
||||||
|
|
||||||
@@ -44,8 +47,9 @@ The only permitted live-send target is user zzib. See
|
|||||||
## Verified
|
## Verified
|
||||||
|
|
||||||
- QR login works end-to-end. The token came from a phone scan.
|
- QR login works end-to-end. The token came from a phone scan.
|
||||||
- The CDP transport works. `get_state` returned the real account and 7 guilds. `send_dm` delivered
|
- The core bridge works live. `get_state` returned the real account and 7 guilds. `send_dm` reached
|
||||||
a real DM to zzib.
|
zzib. A synthetic owner message reached the daemon log with the command parsed.
|
||||||
|
- The mock path works. The daemon handles state, sends, commands, and the two log buckets.
|
||||||
- Pacing stayed inside the 500 to 2800 ms window.
|
- Pacing stayed inside the 500 to 2800 ms window.
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|||||||
+4
-1
@@ -10,7 +10,10 @@
|
|||||||
"test:comm": "tsx src/test-communication.ts",
|
"test:comm": "tsx src/test-communication.ts",
|
||||||
"cdp:probe": "tsx src/cdp-probe.ts",
|
"cdp:probe": "tsx src/cdp-probe.ts",
|
||||||
"control:bridge": "tsx src/control-bridge.ts",
|
"control:bridge": "tsx src/control-bridge.ts",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit",
|
||||||
|
"control:mock": "tsx src/mock-core.ts",
|
||||||
|
"test:trigger": "tsx src/test-trigger.ts",
|
||||||
|
"cdp:dispatch": "tsx src/cdp-dispatch.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop development tool — run one expression in the real client over CDP.
|
||||||
|
*
|
||||||
|
* Use it with the client started on --remote-debugging-port=9222. The main use
|
||||||
|
* is to dispatch synthetic events for end-to-end tests.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* CDP_EXPRESSION='Vencord.Webpack.Common.FluxDispatcher.dispatch({...})' pnpm cdp:dispatch
|
||||||
|
*/
|
||||||
|
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
const expression = process.env.CDP_EXPRESSION;
|
||||||
|
if (!expression) {
|
||||||
|
console.error("CDP_EXPRESSION is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdpUrl = process.env.CDP_URL ?? "http://127.0.0.1:9222";
|
||||||
|
const targets = (await (await fetch(`${cdpUrl}/json`)).json()) as any[];
|
||||||
|
const page = targets.find(target => target.type === "page" && target.url.includes("discord.com"));
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error("No Discord page target");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
ws.once("open", () => resolve());
|
||||||
|
ws.once("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await new Promise<any>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => reject(new Error("CDP evaluate timed out")), 10_000);
|
||||||
|
|
||||||
|
ws.on("message", raw => {
|
||||||
|
const message = JSON.parse(raw.toString());
|
||||||
|
if (message.id !== 1) return;
|
||||||
|
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 1,
|
||||||
|
method: "Runtime.evaluate",
|
||||||
|
params: { expression, returnByValue: true, awaitPromise: true }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(response.result ?? response.error));
|
||||||
|
ws.close();
|
||||||
+27
-27
@@ -1,13 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* Veskslop daemon — MCP control bridge over Chrome DevTools Protocol.
|
* Veskslop daemon — MCP control bridge over the validated core bridge.
|
||||||
*
|
*
|
||||||
* Talks to an unmodified Vesktop through --remote-debugging-port, evaluates
|
* The daemon holds all logic. It sends validated actions to Vesktop and
|
||||||
* actions against the real client, receives events through a CDP binding, and
|
* receives normalized events. See docs/core-bridge.md.
|
||||||
* runs the trigger/whitelist/log logic locally.
|
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* VESKSLOP_TOKEN_FILE=... pnpm control:bridge
|
* pnpm control:bridge
|
||||||
* CDP_URL=http://127.0.0.1:9222 MCP_PORT=8788 pnpm control:bridge
|
* VESKSLOP_SOCKET=/path/to/vesktop-puppet.sock MCP_PORT=8788 pnpm control:bridge
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||||
@@ -15,45 +14,43 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|||||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { CdpClient } from "./cdp-client.js";
|
import { CoreClient, coreSocketPath } from "./core-client.js";
|
||||||
import { Discord } from "./discord.js";
|
|
||||||
import { dedupe, getQueryCount, paced, sleep } from "./pacing.js";
|
import { dedupe, getQueryCount, paced, sleep } from "./pacing.js";
|
||||||
import { Puppet } from "./puppet.js";
|
import { Puppet } from "./puppet.js";
|
||||||
|
|
||||||
const HOST = process.env.MCP_HOST ?? "127.0.0.1";
|
const HOST = process.env.MCP_HOST ?? "127.0.0.1";
|
||||||
const PORT = Number(process.env.MCP_PORT ?? 8788);
|
const PORT = Number(process.env.MCP_PORT ?? 8788);
|
||||||
|
|
||||||
let discord: Discord | undefined;
|
let core: CoreClient | undefined;
|
||||||
let puppet: Puppet | undefined;
|
let puppet: Puppet | undefined;
|
||||||
let cdpConnected = false;
|
let coreConnected = false;
|
||||||
|
|
||||||
async function connectLoop() {
|
async function connectLoop() {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
try {
|
try {
|
||||||
const cdp = await CdpClient.connect();
|
const client = await CoreClient.connect();
|
||||||
|
|
||||||
discord = new Discord(cdp);
|
core = client;
|
||||||
puppet = new Puppet(discord);
|
puppet = new Puppet(client);
|
||||||
|
|
||||||
await discord.installEventBridge();
|
client.onEvent(event => {
|
||||||
cdp.onEvent(event => {
|
|
||||||
puppet!.handleEvent(event).catch(error => console.error("[daemon] event error:", error));
|
puppet!.handleEvent(event).catch(error => console.error("[daemon] event error:", error));
|
||||||
});
|
});
|
||||||
|
|
||||||
cdpConnected = true;
|
coreConnected = true;
|
||||||
console.log("[daemon] connected to Vesktop via CDP");
|
console.log(`[daemon] connected to the core bridge at ${coreSocketPath}`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[daemon] waiting for Vesktop CDP: ${(error as Error).message}`);
|
console.warn(`[daemon] waiting for the core bridge: ${(error as Error).message}`);
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireDiscord(): Discord {
|
function requireCore(): CoreClient {
|
||||||
if (!cdpConnected || !discord) throw new Error("Not connected to Vesktop (run it with --remote-debugging-port)");
|
if (!coreConnected || !core) throw new Error("Not connected to Vesktop (is the core bridge running?)");
|
||||||
return discord;
|
return core;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requirePuppet(): Puppet {
|
function requirePuppet(): Puppet {
|
||||||
@@ -73,7 +70,7 @@ function buildServer(): McpServer {
|
|||||||
{
|
{
|
||||||
description: "Get the logged-in user, guilds and channels. Cache-only, no Discord queries."
|
description: "Get the logged-in user, guilds and channels. Cache-only, no Discord queries."
|
||||||
},
|
},
|
||||||
async () => jsonText(await requireDiscord().getState())
|
async () => jsonText(await requireCore().getState())
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
@@ -86,7 +83,7 @@ function buildServer(): McpServer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async ({ channelId, content }) => {
|
async ({ channelId, content }) => {
|
||||||
await paced(() => requireDiscord().sendMessage(channelId, content));
|
await paced(() => requireCore().sendMessage(channelId, content));
|
||||||
return jsonText({ channelId, sent: true, queriesUsed: getQueryCount() });
|
return jsonText({ channelId, sent: true, queriesUsed: getQueryCount() });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -101,7 +98,7 @@ function buildServer(): McpServer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async ({ userId, content }) => {
|
async ({ userId, content }) => {
|
||||||
const result = await paced(() => requireDiscord().sendDm(userId, content));
|
const result = await paced(() => requireCore().sendDm(userId, content));
|
||||||
return jsonText({ ...result, userId, queriesUsed: getQueryCount() });
|
return jsonText({ ...result, userId, queriesUsed: getQueryCount() });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -117,7 +114,7 @@ function buildServer(): McpServer {
|
|||||||
},
|
},
|
||||||
async ({ channelId, limit }) => {
|
async ({ channelId, limit }) => {
|
||||||
const result = await dedupe<{ cached: number; messages: unknown[] }>(`read:${channelId}:${limit}`, () =>
|
const result = await dedupe<{ cached: number; messages: unknown[] }>(`read:${channelId}:${limit}`, () =>
|
||||||
paced(() => requireDiscord().readMessages(channelId, limit))
|
paced(() => requireCore().readMessages(channelId, limit))
|
||||||
);
|
);
|
||||||
return jsonText({ ...result, channelId, queriesUsed: getQueryCount() });
|
return jsonText({ ...result, channelId, queriesUsed: getQueryCount() });
|
||||||
}
|
}
|
||||||
@@ -203,7 +200,7 @@ app.delete("/mcp", (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get("/health", (_req, res) => {
|
app.get("/health", (_req, res) => {
|
||||||
res.json({ ok: true, cdpConnected, ownerId: puppet?.owner, queries: getQueryCount() });
|
res.json({ ok: true, coreConnected, socketPath: coreSocketPath, ownerId: puppet?.owner, queries: getQueryCount() });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, HOST, (error?: Error) => {
|
app.listen(PORT, HOST, (error?: Error) => {
|
||||||
@@ -214,4 +211,7 @@ app.listen(PORT, HOST, (error?: Error) => {
|
|||||||
console.log(`[daemon] MCP listening on http://${HOST}:${PORT}/mcp`);
|
console.log(`[daemon] MCP listening on http://${HOST}:${PORT}/mcp`);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => process.exit(0));
|
process.on("SIGINT", () => {
|
||||||
|
core?.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop daemon — client for the validated core bridge.
|
||||||
|
*
|
||||||
|
* Speaks newline-delimited JSON over the core socket. A request gets one
|
||||||
|
* response line. Events arrive as { type: "event" } lines after subscribe.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
export const coreSocketPath =
|
||||||
|
process.env.VESKSLOP_SOCKET ??
|
||||||
|
(process.env.XDG_RUNTIME_DIR ? `${process.env.XDG_RUNTIME_DIR}/vesktop-puppet.sock` : "/tmp/vesktop-puppet.sock");
|
||||||
|
|
||||||
|
export class CoreError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly code: string,
|
||||||
|
message: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CoreEventListener = (event: any) => void;
|
||||||
|
|
||||||
|
interface Pending {
|
||||||
|
resolve: (value: any) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreClient {
|
||||||
|
private nextId = 1;
|
||||||
|
private readonly pending = new Map<number, Pending>();
|
||||||
|
private readonly listeners = new Set<CoreEventListener>();
|
||||||
|
|
||||||
|
private constructor(private readonly socket: net.Socket) {
|
||||||
|
socket.setEncoding("utf-8");
|
||||||
|
|
||||||
|
let buffer = "";
|
||||||
|
socket.on("data", (chunk: string) => {
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
let newline: number;
|
||||||
|
while ((newline = buffer.indexOf("\n")) !== -1) {
|
||||||
|
const line = buffer.slice(0, newline);
|
||||||
|
buffer = buffer.slice(newline + 1);
|
||||||
|
if (line) this.onLine(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("close", () => {
|
||||||
|
for (const request of this.pending.values()) {
|
||||||
|
request.reject(new CoreError("DISCONNECTED", "core disconnected"));
|
||||||
|
}
|
||||||
|
this.pending.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", () => socket.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
static connect(path = coreSocketPath): Promise<CoreClient> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = net.connect(path);
|
||||||
|
const client = new CoreClient(socket);
|
||||||
|
|
||||||
|
socket.once("connect", async () => {
|
||||||
|
try {
|
||||||
|
await client.request("events.subscribe");
|
||||||
|
resolve(client);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.once("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLine(line: string) {
|
||||||
|
let message: any;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "event") {
|
||||||
|
for (const listener of this.listeners) listener(message.event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = this.pending.get(message.id);
|
||||||
|
if (!request) return;
|
||||||
|
this.pending.delete(message.id);
|
||||||
|
|
||||||
|
if (message.ok) request.resolve(message.result);
|
||||||
|
else request.reject(new CoreError(message.error?.code ?? "UNKNOWN", message.error?.message ?? "core error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
request<T = any>(action: string, params?: Record<string, unknown>): Promise<T> {
|
||||||
|
const id = this.nextId++;
|
||||||
|
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
this.pending.set(id, { resolve, reject });
|
||||||
|
this.socket.write(JSON.stringify({ id, action, params }) + "\n");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(listener: CoreEventListener) {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
return () => this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convenience actions */
|
||||||
|
|
||||||
|
async getState() {
|
||||||
|
return this.request("state.get");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSelfId(): Promise<string | undefined> {
|
||||||
|
return (await this.getState())?.user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(channelId: string, content: string) {
|
||||||
|
await this.request("message.send", { channelId, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendDm(userId: string, content: string) {
|
||||||
|
return this.request("dm.send", { userId, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
async readMessages(channelId: string, limit: number) {
|
||||||
|
return this.request("message.read", { channelId, limit });
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.socket.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* Veskslop — Discord actions executed inside the real client via CDP.
|
|
||||||
*
|
|
||||||
* Every expression runs in the Discord page against Vencord's exposed globals,
|
|
||||||
* so all traffic is performed by the official client.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { CdpClient } from "./cdp-client.js";
|
|
||||||
|
|
||||||
export interface DiscordUser {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiscordChannel {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiscordGuild {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
channels: DiscordChannel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiscordState {
|
|
||||||
user: DiscordUser;
|
|
||||||
guilds: DiscordGuild[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringify = (value: unknown) => JSON.stringify(value);
|
|
||||||
|
|
||||||
export class Discord {
|
|
||||||
constructor(private readonly cdp: CdpClient) {}
|
|
||||||
|
|
||||||
/** Install the page-side event pusher (idempotent across reconnects). */
|
|
||||||
async installEventBridge(): Promise<void> {
|
|
||||||
await this.cdp.evaluate(`
|
|
||||||
(() => {
|
|
||||||
if (globalThis.__veskslopBridgeInstalled) return "already";
|
|
||||||
globalThis.__veskslopBridgeInstalled = true;
|
|
||||||
|
|
||||||
const { FluxDispatcher, ChannelStore } = Vencord.Webpack.Common;
|
|
||||||
FluxDispatcher.subscribe("MESSAGE_CREATE", event => {
|
|
||||||
try {
|
|
||||||
const message = event.message;
|
|
||||||
if (!message?.author) return;
|
|
||||||
|
|
||||||
veskSlopEmit(JSON.stringify({
|
|
||||||
type: "MESSAGE_CREATE",
|
|
||||||
id: message.id,
|
|
||||||
channelId: event.channelId,
|
|
||||||
guildId: event.guildId ?? null,
|
|
||||||
channelType: ChannelStore.getChannel(event.channelId)?.type ?? null,
|
|
||||||
author: {
|
|
||||||
id: message.author.id,
|
|
||||||
username: message.author.username,
|
|
||||||
bot: !!message.author.bot
|
|
||||||
},
|
|
||||||
content: message.content ?? "",
|
|
||||||
mentions: [
|
|
||||||
...(message.mentions ?? []).map(user => user.id),
|
|
||||||
...(message.mentionIds ?? [])
|
|
||||||
],
|
|
||||||
timestamp: message.timestamp ?? new Date().toISOString()
|
|
||||||
}));
|
|
||||||
} catch {
|
|
||||||
// never break the client because of the bridge
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return "installed";
|
|
||||||
})()
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getState(): Promise<DiscordState | null> {
|
|
||||||
return this.cdp.evaluate<DiscordState | null>(`
|
|
||||||
(() => {
|
|
||||||
const { UserStore, GuildStore, ChannelStore } = Vencord.Webpack.Common;
|
|
||||||
const user = UserStore.getCurrentUser();
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: { id: user.id, username: user.username },
|
|
||||||
guilds: GuildStore.getGuildsArray().map(guild => ({
|
|
||||||
id: guild.id,
|
|
||||||
name: guild.name,
|
|
||||||
channels: Object.values(ChannelStore.getMutableBasicGuildChannelsForGuild(guild.id)).map(channel => ({
|
|
||||||
id: channel.id,
|
|
||||||
name: channel.name,
|
|
||||||
type: channel.type
|
|
||||||
}))
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
})()
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSelfId(): Promise<string | undefined> {
|
|
||||||
return (await this.getState())?.user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage(channelId: string, content: string): Promise<void> {
|
|
||||||
await this.cdp.evaluate(`
|
|
||||||
Vencord.Util.sendMessage(${stringify(channelId)}, { content: ${stringify(content)} }, false)
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendDm(userId: string, content: string): Promise<{ sent: boolean; channelId?: string; error?: string }> {
|
|
||||||
return this.cdp.evaluate(`
|
|
||||||
(async () => {
|
|
||||||
const { ChannelStore } = Vencord.Webpack.Common;
|
|
||||||
const { openPrivateChannel, sendMessage } = Vencord.Util;
|
|
||||||
|
|
||||||
let channelId = ChannelStore.getDMFromUserId(${stringify(userId)});
|
|
||||||
if (!channelId) {
|
|
||||||
openPrivateChannel(${stringify(userId)});
|
|
||||||
for (let i = 0; i < 20 && !channelId; i++) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
channelId = ChannelStore.getDMFromUserId(${stringify(userId)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelId) return { sent: false, error: "No DM channel yet" };
|
|
||||||
|
|
||||||
await sendMessage(channelId, { content: ${stringify(content)} }, false);
|
|
||||||
return { sent: true, channelId };
|
|
||||||
})()
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async readMessages(channelId: string, limit: number) {
|
|
||||||
return this.cdp.evaluate(`
|
|
||||||
(() => {
|
|
||||||
const messages = Vencord.Webpack.Common.MessageStore.getMessages(${stringify(channelId)})?._array ?? [];
|
|
||||||
return {
|
|
||||||
cached: messages.length,
|
|
||||||
messages: messages.slice(${-Math.abs(limit)}).map(message => ({
|
|
||||||
id: message.id,
|
|
||||||
author: { id: message.author.id, username: message.author.username },
|
|
||||||
content: message.content,
|
|
||||||
timestamp: message.timestamp
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
})()
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Seed the auth token and reload the client. Used for fresh profiles. */
|
|
||||||
async seedToken(token: string): Promise<void> {
|
|
||||||
await this.cdp.evaluate(`
|
|
||||||
localStorage.setItem("token", ${stringify(JSON.stringify(token))});
|
|
||||||
location.reload();
|
|
||||||
"reloading"
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop — mock core bridge for daemon tests.
|
||||||
|
*
|
||||||
|
* Implements the validated protocol with fake data. Use it to test the daemon
|
||||||
|
* without Electron or Discord. The `debug.emit` action injects an event into
|
||||||
|
* all subscribers and exists only in the mock.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm control:mock
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { rmSync } from "node:fs";
|
||||||
|
import { createServer, Socket } from "node:net";
|
||||||
|
|
||||||
|
const socketPath = process.env.VESKSLOP_SOCKET ?? "/tmp/vesktop-puppet-mock.sock";
|
||||||
|
|
||||||
|
const subscribers = new Set<Socket>();
|
||||||
|
|
||||||
|
const FAKE_GUILDS = [
|
||||||
|
{
|
||||||
|
id: "100000000000000001",
|
||||||
|
name: "Mock Guild",
|
||||||
|
channels: [
|
||||||
|
{ id: "200000000000000001", name: "general", type: 0 },
|
||||||
|
{ id: "200000000000000002", name: "voice", type: 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const FAKE_MESSAGES = [
|
||||||
|
{
|
||||||
|
id: "300000000000000001",
|
||||||
|
author: { id: "1", username: "mockuser" },
|
||||||
|
content: "hello from the mock",
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handle(action: string | undefined, params: any) {
|
||||||
|
switch (action) {
|
||||||
|
case "ping":
|
||||||
|
return "pong";
|
||||||
|
case "state.get":
|
||||||
|
return { user: { id: "999", username: "mockuser" }, guilds: FAKE_GUILDS };
|
||||||
|
case "message.send":
|
||||||
|
return { channelId: params?.channelId, sent: true };
|
||||||
|
case "message.read":
|
||||||
|
return { cached: FAKE_MESSAGES.length, messages: FAKE_MESSAGES };
|
||||||
|
case "dm.send":
|
||||||
|
return { userId: params?.userId, channelId: `dm-${params?.userId}`, sent: true };
|
||||||
|
case "debug.emit": {
|
||||||
|
const line = JSON.stringify({ type: "event", event: params?.event }) + "\n";
|
||||||
|
for (const subscriber of subscribers) subscriber.write(line);
|
||||||
|
return { emitted: true };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(socketPath, { force: true });
|
||||||
|
|
||||||
|
const server = createServer(socket => {
|
||||||
|
socket.setEncoding("utf-8");
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
socket.on("data", (chunk: string) => {
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
let newline: number;
|
||||||
|
while ((newline = buffer.indexOf("\n")) !== -1) {
|
||||||
|
const line = buffer.slice(0, newline).trim();
|
||||||
|
buffer = buffer.slice(newline + 1);
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = JSON.parse(line);
|
||||||
|
|
||||||
|
if (request.action === "events.subscribe") {
|
||||||
|
subscribers.add(socket);
|
||||||
|
socket.write(JSON.stringify({ id: request.id, ok: true, result: { subscribed: true } }) + "\n");
|
||||||
|
console.log(`[mock] ${new Date().toISOString()} events.subscribe`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = handle(request.action, request.params);
|
||||||
|
console.log(`[mock] ${new Date().toISOString()} ${request.action}`);
|
||||||
|
socket.write(JSON.stringify({ id: request.id, ok: true, result }) + "\n");
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.log(`[mock] ${new Date().toISOString()} ERROR ${message}`);
|
||||||
|
socket.write(JSON.stringify({ ok: false, error: { code: "UNKNOWN_ACTION", message } }) + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", () => subscribers.delete(socket));
|
||||||
|
socket.on("close", () => subscribers.delete(socket));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(socketPath, () => {
|
||||||
|
console.log(`[mock] listening on ${socketPath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
server.close();
|
||||||
|
rmSync(socketPath, { force: true });
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
+7
-2
@@ -15,7 +15,12 @@ import {
|
|||||||
type ParsedCommand,
|
type ParsedCommand,
|
||||||
type PuppetMessage
|
type PuppetMessage
|
||||||
} from "./communication.js";
|
} from "./communication.js";
|
||||||
import type { Discord } from "./discord.js";
|
|
||||||
|
/** The subset of the transport that the puppet logic needs. */
|
||||||
|
export interface PuppetTransport {
|
||||||
|
getSelfId(): Promise<string | undefined>;
|
||||||
|
sendMessage(channelId: string, content: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
const DM_CHANNEL = 1;
|
const DM_CHANNEL = 1;
|
||||||
const GROUP_DM_CHANNEL = 3;
|
const GROUP_DM_CHANNEL = 3;
|
||||||
@@ -36,7 +41,7 @@ export class Puppet {
|
|||||||
private whitelistIds: string[] = [];
|
private whitelistIds: string[] = [];
|
||||||
private selfId?: string;
|
private selfId?: string;
|
||||||
|
|
||||||
constructor(private readonly discord: Discord) {
|
constructor(private readonly discord: PuppetTransport) {
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(readFileSync(STATE_FILE, "utf8")) as PersistedState;
|
const state = JSON.parse(readFileSync(STATE_FILE, "utf8")) as PersistedState;
|
||||||
if (state.ownerId) this.ownerId = state.ownerId;
|
if (state.ownerId) this.ownerId = state.ownerId;
|
||||||
|
|||||||
+6
-9
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Veskslop — live DM send test through a running client.
|
* Veskslop — live DM send test through the core bridge.
|
||||||
*
|
*
|
||||||
* Sanctioned test target: user zzib (defaults to their user ID).
|
* Sanctioned test target: user zzib (defaults to their user ID).
|
||||||
*
|
*
|
||||||
@@ -8,8 +8,7 @@
|
|||||||
* TEST_DM_USER_ID=... TEST_DM_CONTENT="hello" pnpm poc:dm
|
* TEST_DM_USER_ID=... TEST_DM_CONTENT="hello" pnpm poc:dm
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CdpClient } from "./cdp-client.js";
|
import { CoreClient } from "./core-client.js";
|
||||||
import { Discord } from "./discord.js";
|
|
||||||
|
|
||||||
/** zzib — the only sanctioned live-send target. */
|
/** zzib — the only sanctioned live-send target. */
|
||||||
const DEFAULT_TEST_DM_USER_ID = "177117296140091393";
|
const DEFAULT_TEST_DM_USER_ID = "177117296140091393";
|
||||||
@@ -17,10 +16,8 @@ const DEFAULT_TEST_DM_USER_ID = "177117296140091393";
|
|||||||
const userId = process.env.TEST_DM_USER_ID ?? DEFAULT_TEST_DM_USER_ID;
|
const userId = process.env.TEST_DM_USER_ID ?? DEFAULT_TEST_DM_USER_ID;
|
||||||
const content = process.env.TEST_DM_CONTENT ?? "veskslop test dm";
|
const content = process.env.TEST_DM_CONTENT ?? "veskslop test dm";
|
||||||
|
|
||||||
const cdp = await CdpClient.connect();
|
const core = await CoreClient.connect();
|
||||||
const discord = new Discord(cdp);
|
console.log("[dm] connected to the core bridge");
|
||||||
|
console.log("[dm]", await core.sendDm(userId, content));
|
||||||
|
|
||||||
console.log("[dm] connected via CDP");
|
core.close();
|
||||||
console.log("[dm]", await discord.sendDm(userId, content));
|
|
||||||
|
|
||||||
cdp.close();
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop — inject a synthetic trigger event into the mock core.
|
||||||
|
* Use it to test the daemon trigger path without Discord.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm test:trigger
|
||||||
|
* TEST_TRIGGER_CONTENT="whitelist add @123" pnpm test:trigger
|
||||||
|
*/
|
||||||
|
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
import { coreSocketPath } from "./core-client.js";
|
||||||
|
|
||||||
|
const authorId = process.env.TEST_TRIGGER_AUTHOR ?? "177117296140091393";
|
||||||
|
const content = process.env.TEST_TRIGGER_CONTENT ?? "whitelist list";
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
type: "MESSAGE_CREATE",
|
||||||
|
id: `30000000000000${Date.now() % 10000}`,
|
||||||
|
channelId: "805255536223715370",
|
||||||
|
guildId: null,
|
||||||
|
channelType: 1,
|
||||||
|
author: { id: authorId, username: "zzib", bot: false },
|
||||||
|
content,
|
||||||
|
mentions: [],
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const socket = net.connect(coreSocketPath);
|
||||||
|
socket.setEncoding("utf-8");
|
||||||
|
|
||||||
|
socket.once("connect", () =>
|
||||||
|
socket.write(JSON.stringify({ id: 1, action: "debug.emit", params: { event } }) + "\n")
|
||||||
|
);
|
||||||
|
socket.once("data", data => {
|
||||||
|
console.log("[trigger]", data.toString().trim());
|
||||||
|
socket.destroy();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
socket.once("error", reject);
|
||||||
|
});
|
||||||
@@ -18,10 +18,15 @@ export const PORTABLE =
|
|||||||
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
|
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
|
||||||
|
|
||||||
export const DATA_DIR =
|
export const DATA_DIR =
|
||||||
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
|
process.env.VENCORD_USER_DATA_DIR ||
|
||||||
|
process.env.VESKSLOP_DATA_DIR ||
|
||||||
|
(PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("appData"), "veskslop"));
|
||||||
|
|
||||||
mkdirSync(DATA_DIR, { recursive: true });
|
mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
|
||||||
|
/** Durable auth credential. The preload seeds it into the client before boot. */
|
||||||
|
export const TOKEN_FILE = process.env.VESKSLOP_TOKEN_FILE || join(DATA_DIR, "token");
|
||||||
|
|
||||||
export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
|
export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
|
||||||
app.setPath("sessionData", SESSION_DATA_DIR);
|
app.setPath("sessionData", SESSION_DATA_DIR);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { join } from "path";
|
|||||||
import { IpcEvents } from "../shared/IpcEvents";
|
import { IpcEvents } from "../shared/IpcEvents";
|
||||||
import { setBadgeCount } from "./appBadge";
|
import { setBadgeCount } from "./appBadge";
|
||||||
import { autoStart } from "./autoStart";
|
import { autoStart } from "./autoStart";
|
||||||
|
import { TOKEN_FILE } from "./constants";
|
||||||
import { enableHardwareAcceleration } from "./main";
|
import { enableHardwareAcceleration } from "./main";
|
||||||
import { mainWin } from "./mainWindow";
|
import { mainWin } from "./mainWindow";
|
||||||
import { Settings, State } from "./settings";
|
import { Settings, State } from "./settings";
|
||||||
@@ -60,6 +61,17 @@ handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
|
|||||||
handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
|
handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
|
||||||
handleSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION, () => enableHardwareAcceleration);
|
handleSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION, () => enableHardwareAcceleration);
|
||||||
|
|
||||||
|
handleSync(IpcEvents.VESKSLOP_GET_TOKEN, () => {
|
||||||
|
let token: string | undefined;
|
||||||
|
try {
|
||||||
|
token = readFileSync(TOKEN_FILE, "utf8").trim() || undefined;
|
||||||
|
} catch {
|
||||||
|
// No token file: the client falls back to its normal login.
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token, force: process.env.VESKSLOP_FORCE_TOKEN === "1" };
|
||||||
|
});
|
||||||
|
|
||||||
handleSync(
|
handleSync(
|
||||||
IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
|
IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
|
||||||
() => process.platform === "win32" && Number(release().split(".").pop()) >= 22621
|
() => process.platform === "win32" && Number(release().split(".").pop()) >= 22621
|
||||||
|
|||||||
+5
-3
@@ -16,6 +16,7 @@ import { DATA_DIR } from "./constants";
|
|||||||
import { createFirstLaunchTour } from "./firstLaunch";
|
import { createFirstLaunchTour } from "./firstLaunch";
|
||||||
import { createWindows, mainWin } from "./mainWindow";
|
import { createWindows, mainWin } from "./mainWindow";
|
||||||
import { registerMediaPermissionsHandler } from "./mediaPermissions";
|
import { registerMediaPermissionsHandler } from "./mediaPermissions";
|
||||||
|
import { initPuppetBridge } from "./puppetBridge";
|
||||||
import { registerScreenShareHandler } from "./screenShare";
|
import { registerScreenShareHandler } from "./screenShare";
|
||||||
import { Settings, State } from "./settings";
|
import { Settings, State } from "./settings";
|
||||||
import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient";
|
import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient";
|
||||||
@@ -105,6 +106,7 @@ function init() {
|
|||||||
|
|
||||||
registerScreenShareHandler();
|
registerScreenShareHandler();
|
||||||
registerMediaPermissionsHandler();
|
registerMediaPermissionsHandler();
|
||||||
|
initPuppetBridge();
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
||||||
@@ -127,10 +129,10 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
if (!Object.hasOwn(State.store, "firstLaunch")) {
|
if (process.env.VESKSLOP_HEADLESS === "1" || Object.hasOwn(State.store, "firstLaunch")) {
|
||||||
createFirstLaunchTour();
|
|
||||||
} else {
|
|
||||||
createWindows();
|
createWindows();
|
||||||
|
} else {
|
||||||
|
createFirstLaunchTour();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop core bridge — the only external control surface.
|
||||||
|
*
|
||||||
|
* The bridge accepts newline-delimited JSON. It validates the request shape,
|
||||||
|
* forwards the action to the renderer, and fans events out to subscribers.
|
||||||
|
* The renderer validates the action content. See src/renderer/puppet/actions.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app, ipcMain } from "electron";
|
||||||
|
import { rmSync } from "fs";
|
||||||
|
import { rm } from "fs/promises";
|
||||||
|
import { createServer, Server, Socket } from "net";
|
||||||
|
import { join } from "path";
|
||||||
|
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
|
||||||
|
import {
|
||||||
|
PUPPET_LIMITS,
|
||||||
|
PuppetAction,
|
||||||
|
PuppetErrorCode,
|
||||||
|
type PuppetEvent,
|
||||||
|
type PuppetRequest,
|
||||||
|
type PuppetResult
|
||||||
|
} from "shared/puppetProtocol";
|
||||||
|
|
||||||
|
import { sendRendererCommand } from "./ipcCommands";
|
||||||
|
|
||||||
|
const { env, platform } = process;
|
||||||
|
|
||||||
|
export const PUPPET_SOCKET_PATH =
|
||||||
|
platform === "win32"
|
||||||
|
? "\\\\?\\pipe\\vesktop-puppet.sock"
|
||||||
|
: join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || "/tmp", "vesktop-puppet.sock");
|
||||||
|
|
||||||
|
const KNOWN_ACTIONS = new Set<string>([
|
||||||
|
PuppetAction.Ping,
|
||||||
|
PuppetAction.EventsSubscribe,
|
||||||
|
PuppetAction.StateGet,
|
||||||
|
PuppetAction.MessageSend,
|
||||||
|
PuppetAction.MessageRead,
|
||||||
|
PuppetAction.DmSend
|
||||||
|
]);
|
||||||
|
|
||||||
|
let server: Server | undefined;
|
||||||
|
const subscribers = new Set<Socket>();
|
||||||
|
|
||||||
|
export async function initPuppetBridge() {
|
||||||
|
await rm(PUPPET_SOCKET_PATH, { force: true });
|
||||||
|
|
||||||
|
server = createServer(socket => {
|
||||||
|
socket.setEncoding("utf-8");
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
socket.on("data", (chunk: string) => {
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
if (buffer.length > PUPPET_LIMITS.maxRequestBytes) {
|
||||||
|
respond(socket, {
|
||||||
|
ok: false,
|
||||||
|
error: { code: PuppetErrorCode.PayloadTooLarge, message: "request too large" }
|
||||||
|
});
|
||||||
|
buffer = "";
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newline: number;
|
||||||
|
while ((newline = buffer.indexOf("\n")) !== -1) {
|
||||||
|
const line = buffer.slice(0, newline).trim();
|
||||||
|
buffer = buffer.slice(newline + 1);
|
||||||
|
if (line) void handleLine(socket, line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", () => subscribers.delete(socket));
|
||||||
|
socket.on("close", () => subscribers.delete(socket));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PUPPET_SOCKET_PATH);
|
||||||
|
server.on("error", error => console.error("[puppet] socket error:", error));
|
||||||
|
|
||||||
|
ipcMain.on(IpcEvents.PUPPET_EVENT, (_event, event: PuppetEvent) => {
|
||||||
|
const line = JSON.stringify({ type: "event", event }) + "\n";
|
||||||
|
|
||||||
|
for (const subscriber of subscribers) {
|
||||||
|
try {
|
||||||
|
subscriber.write(line);
|
||||||
|
} catch {
|
||||||
|
subscribers.delete(subscriber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[puppet] bridge listening on ${PUPPET_SOCKET_PATH}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function respond(socket: Socket, response: Record<string, unknown>) {
|
||||||
|
socket.write(JSON.stringify(response) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLine(socket: Socket, line: string) {
|
||||||
|
let request: PuppetRequest;
|
||||||
|
try {
|
||||||
|
request = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
respond(socket, { ok: false, error: { code: PuppetErrorCode.InvalidRequest, message: "invalid JSON" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, action, params } = request;
|
||||||
|
|
||||||
|
if (typeof action !== "string" || !KNOWN_ACTIONS.has(action)) {
|
||||||
|
respond(socket, {
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: { code: PuppetErrorCode.UnknownAction, message: `unknown action: ${action}` }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === PuppetAction.Ping) {
|
||||||
|
respond(socket, { id, ok: true, result: "pong" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === PuppetAction.EventsSubscribe) {
|
||||||
|
subscribers.add(socket);
|
||||||
|
respond(socket, { id, ok: true, result: { subscribed: true } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sendRendererCommand<PuppetResult>(IpcCommands.PUPPET_COMMAND, { action, params });
|
||||||
|
|
||||||
|
if (result?.ok === false) respond(socket, { id, ok: false, error: result.error });
|
||||||
|
else respond(socket, { id, ok: true, result: result?.result });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[puppet] action failed:", action, error);
|
||||||
|
respond(socket, {
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: { code: PuppetErrorCode.CoreUnavailable, message: "core is not ready" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on("will-quit", () => {
|
||||||
|
server?.close();
|
||||||
|
rmSync(PUPPET_SOCKET_PATH, { force: true });
|
||||||
|
});
|
||||||
@@ -108,5 +108,8 @@ export const VesktopNative = {
|
|||||||
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
|
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
|
||||||
},
|
},
|
||||||
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
|
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
|
||||||
|
},
|
||||||
|
puppet: {
|
||||||
|
emit: (event: unknown) => ipcRenderer.send(IpcEvents.PUPPET_EVENT, event)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ import { contextBridge, ipcRenderer, webFrame } from "electron/renderer";
|
|||||||
import { IpcEvents } from "../shared/IpcEvents";
|
import { IpcEvents } from "../shared/IpcEvents";
|
||||||
import { VesktopNative } from "./VesktopNative";
|
import { VesktopNative } from "./VesktopNative";
|
||||||
|
|
||||||
|
// Veskslop: seed the Discord auth token before the client boots, unless a session exists.
|
||||||
|
try {
|
||||||
|
const seed = ipcRenderer.sendSync(IpcEvents.VESKSLOP_GET_TOKEN) as
|
||||||
|
| { token?: string; force?: boolean }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (seed?.token && (seed.force || !localStorage.getItem("token"))) {
|
||||||
|
localStorage.setItem("token", JSON.stringify(seed.token));
|
||||||
|
console.log("[veskslop] seeded auth token");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[veskslop] failed to seed auth token", error);
|
||||||
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("VesktopNative", VesktopNative);
|
contextBridge.exposeInMainWorld("VesktopNative", VesktopNative);
|
||||||
|
|
||||||
// While sandboxed, Electron "polyfills" these APIs as local variables.
|
// While sandboxed, Electron "polyfills" these APIs as local variables.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import "./ipcCommands";
|
|||||||
import "./appBadge";
|
import "./appBadge";
|
||||||
import "./fixes";
|
import "./fixes";
|
||||||
import "./arrpc";
|
import "./arrpc";
|
||||||
|
import "./puppet";
|
||||||
import "__patches__"; // auto generated by the build script
|
import "__patches__"; // auto generated by the build script
|
||||||
|
|
||||||
export * as Components from "./components";
|
export * as Components from "./components";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SettingsRouter } from "@vencord/types/webpack/common";
|
|||||||
import { IpcCommands } from "shared/IpcEvents";
|
import { IpcCommands } from "shared/IpcEvents";
|
||||||
|
|
||||||
import { openScreenSharePicker } from "./components/ScreenSharePicker";
|
import { openScreenSharePicker } from "./components/ScreenSharePicker";
|
||||||
|
import { handlePuppetAction } from "./puppet";
|
||||||
|
|
||||||
type IpcCommandHandler = (data: any) => any;
|
type IpcCommandHandler = (data: any) => any;
|
||||||
|
|
||||||
@@ -52,3 +53,5 @@ onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => {
|
|||||||
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
|
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
|
||||||
|
|
||||||
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
|
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
|
||||||
|
|
||||||
|
onIpcCommand(IpcCommands.PUPPET_COMMAND, data => handlePuppetAction(data.action, data.params));
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop core events — normalize client events and push them to main.
|
||||||
|
*
|
||||||
|
* The subscription starts only after Vencord registers the flux dispatcher.
|
||||||
|
* A startup crash here would break the whole renderer bundle, so every step
|
||||||
|
* is guarded.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ChannelStore } from "@vencord/types/webpack/common";
|
||||||
|
import { filters, waitFor } from "@vencord/types/webpack";
|
||||||
|
import type { PuppetEvent } from "shared/puppetProtocol";
|
||||||
|
|
||||||
|
function normalizeMessage(event: any): PuppetEvent | undefined {
|
||||||
|
const message = event?.message;
|
||||||
|
if (!message?.author) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "MESSAGE_CREATE",
|
||||||
|
id: message.id,
|
||||||
|
channelId: event.channelId,
|
||||||
|
guildId: event.guildId ?? null,
|
||||||
|
channelType: ChannelStore.getChannel(event.channelId)?.type ?? null,
|
||||||
|
author: {
|
||||||
|
id: message.author.id,
|
||||||
|
username: message.author.username,
|
||||||
|
bot: !!message.author.bot
|
||||||
|
},
|
||||||
|
content: message.content ?? "",
|
||||||
|
mentions: [
|
||||||
|
...(message.mentions ?? []).map((user: any) => user.id),
|
||||||
|
...(message.mentionIds ?? [])
|
||||||
|
],
|
||||||
|
timestamp: message.timestamp ?? new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = false;
|
||||||
|
|
||||||
|
export function initPuppetEvents() {
|
||||||
|
waitFor(filters.byProps("dispatch", "subscribe"), dispatcher => {
|
||||||
|
if (installed) return;
|
||||||
|
installed = true;
|
||||||
|
|
||||||
|
dispatcher.subscribe("MESSAGE_CREATE", (event: any) => {
|
||||||
|
try {
|
||||||
|
const normalized = normalizeMessage(event);
|
||||||
|
if (normalized) VesktopNative.puppet.emit(normalized);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[puppet] event normalization failed:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[veskslop] puppet events active");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* Veskslop core entry — starts the event push and exports the action handler.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { initPuppetEvents } from "./events";
|
||||||
|
|
||||||
|
export { handlePuppetAction } from "./actions";
|
||||||
|
|
||||||
|
initPuppetEvents();
|
||||||
@@ -62,7 +62,11 @@ export const enum IpcEvents {
|
|||||||
DEVTOOLS_OPENED = "VCD_DEVTOOLS_OPENED",
|
DEVTOOLS_OPENED = "VCD_DEVTOOLS_OPENED",
|
||||||
DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED",
|
DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED",
|
||||||
|
|
||||||
CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET"
|
CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET",
|
||||||
|
|
||||||
|
/* Veskslop */
|
||||||
|
VESKSLOP_GET_TOKEN = "VCD_VESKSLOP_GET_TOKEN",
|
||||||
|
PUPPET_EVENT = "VCD_PUPPET_EVENT"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum UpdaterIpcEvents {
|
export const enum UpdaterIpcEvents {
|
||||||
@@ -83,5 +87,8 @@ export const enum IpcCommands {
|
|||||||
|
|
||||||
GET_LANGUAGES = "navigator.languages",
|
GET_LANGUAGES = "navigator.languages",
|
||||||
|
|
||||||
SCREEN_SHARE_PICKER = "screenshare:picker"
|
SCREEN_SHARE_PICKER = "screenshare:picker",
|
||||||
|
|
||||||
|
/* Veskslop */
|
||||||
|
PUPPET_COMMAND = "puppet:command"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user