134 lines
5.3 KiB
Markdown
134 lines
5.3 KiB
Markdown
# Core bridge
|
|
|
|
The core bridge is the only surface between the Vesktop core and the external daemon. This document
|
|
describes the protocol, the validation rules, and the hooks into upstream files.
|
|
|
|
## Files
|
|
|
|
| File | Role | Status |
|
|
| --- | --- | --- |
|
|
| `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/events.ts` | Event normalization and push to main | Implemented |
|
|
| `src/renderer/puppet/index.ts` | Renderer initialization | Implemented |
|
|
| `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
|
|
|
|
The core bridge uses a Unix socket. On Linux the path is `$XDG_RUNTIME_DIR/vesktop-puppet.sock`.
|
|
On Linux without `XDG_RUNTIME_DIR`, the path is `/tmp/vesktop-puppet.sock`. On Windows the path is
|
|
the named pipe `\\.\pipe\vesktop-puppet.sock`.
|
|
|
|
The protocol is newline-delimited JSON. Each request gets one response line. An event subscriber
|
|
gets event lines until the connection closes.
|
|
|
|
Request:
|
|
|
|
```json
|
|
{ "id": 1, "action": "message.send", "params": { "channelId": "123", "content": "hello" } }
|
|
```
|
|
|
|
Success response:
|
|
|
|
```json
|
|
{ "id": 1, "ok": true, "result": { "channelId": "123", "sent": true } }
|
|
```
|
|
|
|
Error response:
|
|
|
|
```json
|
|
{ "id": 1, "ok": false, "error": { "code": "CONTENT_EMPTY", "message": "content is empty" } }
|
|
```
|
|
|
|
Event line:
|
|
|
|
```json
|
|
{ "type": "event", "event": { "type": "MESSAGE_CREATE", "channelId": "123" } }
|
|
```
|
|
|
|
## Actions
|
|
|
|
| Action | Parameters | Result |
|
|
| --- | --- | --- |
|
|
| `ping` | none | `"pong"` |
|
|
| `events.subscribe` | none | `{ "subscribed": true }`. The socket becomes an event subscriber. |
|
|
| `state.get` | none | The current user, the guilds, and the channels. Cache-only. |
|
|
| `message.send` | `channelId`, `content` | `{ channelId, sent: true }` |
|
|
| `message.read` | `channelId`, `limit` | `{ cached, messages }`. Cache-only. |
|
|
| `dm.send` | `userId`, `content` | `{ userId, channelId, sent: true }` |
|
|
|
|
## Validation rules
|
|
|
|
The core validates all input. The daemon cannot bypass these rules.
|
|
|
|
- A channel ID and a user ID must match the snowflake format. The format is 17 to 20 digits.
|
|
- `content` must be a string. The content must not be empty after a trim.
|
|
- `content` must not contain control characters.
|
|
- `content` must not be longer than 2000 characters.
|
|
- The target channel must exist. The channel type must accept text. The allowed types are
|
|
`GUILD_TEXT`, `DM`, `GROUP_DM`, `GUILD_ANNOUNCEMENT`, `ANNOUNCEMENT_THREAD`, `PUBLIC_THREAD`,
|
|
and `PRIVATE_THREAD`.
|
|
- `limit` must be an integer from 1 to 100.
|
|
- A request line must not be larger than 64 KiB.
|
|
|
|
## Core rate limit
|
|
|
|
The core rate limit is a safety net. It does not replace the daemon pacing.
|
|
|
|
- The minimum interval between two outbound actions is 500 ms.
|
|
- The maximum is 30 outbound actions per minute.
|
|
- The limit applies to `message.send` and `dm.send`.
|
|
|
|
## Error codes
|
|
|
|
| Code | Meaning |
|
|
| --- | --- |
|
|
| `INVALID_REQUEST` | The line is not valid JSON. |
|
|
| `PAYLOAD_TOO_LARGE` | The request line is larger than 64 KiB. |
|
|
| `UNKNOWN_ACTION` | The action name is not in the action list. |
|
|
| `CORE_UNAVAILABLE` | The bridge cannot reach the renderer. |
|
|
| `NOT_LOGGED_IN` | No user is logged in. |
|
|
| `INVALID_SNOWFLAKE` | The ID is not a snowflake. |
|
|
| `INVALID_PARAM` | A parameter has the wrong type or value. |
|
|
| `CONTENT_EMPTY` | The content is empty. |
|
|
| `CONTENT_TOO_LONG` | The content is longer than 2000 characters. |
|
|
| `CHANNEL_NOT_FOUND` | The channel is not in the client cache. |
|
|
| `NOT_SENDABLE` | The channel type cannot receive messages. |
|
|
| `SEND_FAILED` | Discord rejected the message. |
|
|
| `DM_UNAVAILABLE` | The client cannot create a DM channel. |
|
|
| `RATE_LIMITED` | The core rate limit rejected the action. |
|
|
| `INTERNAL` | An unexpected error occurred. The details go to the local log only. |
|
|
|
|
## Hooks into upstream files
|
|
|
|
The bridge uses a small number of hooks. Keep these changes minimal.
|
|
|
|
| File | Change |
|
|
| --- | --- |
|
|
| `src/main/main.ts` | Import the bridge. Call `initPuppetBridge()` after `app.whenReady()`. |
|
|
| `src/shared/IpcEvents.ts` | Add `PUPPET_EVENT` and `IpcCommands.PUPPET_COMMAND`. |
|
|
| `src/renderer/index.ts` | Import `./puppet`. |
|
|
| `src/renderer/ipcCommands.ts` | Register `PUPPET_COMMAND` and call `handlePuppetAction`. |
|
|
| `src/preload/VesktopNative.ts` | Add `puppet.emit(event)` for event push. |
|
|
|
|
## How to add an action
|
|
|
|
1. Add the action name to `PuppetAction` in `src/shared/puppetProtocol.ts`.
|
|
2. Add a validation case in `src/renderer/puppet/actions.ts`.
|
|
3. Add the action to the known-action set in `src/main/puppetBridge.ts`.
|
|
4. Add a tool in the daemon (`poc/src/control-bridge.ts`).
|
|
5. Add a unit test for the validation path.
|
|
6. Run `pnpm testTypes`, `pnpm lint`, and `pnpm build`.
|
|
|
|
## Why this shape
|
|
|
|
The shape gives three properties:
|
|
|
|
- The daemon cannot run arbitrary code in the client.
|
|
- A daemon bug cannot crash the client or send malformed data to Discord.
|
|
- The core patch stays small, so an upstream rebase is easy.
|