@yaebal/panel 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -5
- package/lib/index.d.ts +90 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +378 -28
- package/lib/index.js.map +1 -1
- package/lib/index.test.js +322 -1
- package/lib/index.test.js.map +1 -1
- package/lib/panel-html.d.ts +2 -2
- package/lib/panel-html.d.ts.map +1 -1
- package/lib/panel-html.js +185 -22
- package/lib/panel-html.js.map +1 -1
- package/lib/serve.d.ts +25 -0
- package/lib/serve.d.ts.map +1 -0
- package/lib/serve.js +47 -0
- package/lib/serve.js.map +1 -0
- package/lib/sqlite.d.ts +32 -0
- package/lib/sqlite.d.ts.map +1 -0
- package/lib/sqlite.js +97 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/sqlite.test.d.ts +2 -0
- package/lib/sqlite.test.d.ts.map +1 -0
- package/lib/sqlite.test.js +42 -0
- package/lib/sqlite.test.js.map +1 -0
- package/package.json +9 -1
- package/src/index.test.ts +413 -1
- package/src/index.ts +494 -41
- package/src/panel-html.ts +185 -22
- package/src/serve.ts +65 -0
- package/src/sqlite.test.ts +58 -0
- package/src/sqlite.ts +140 -0
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# @yaebal/panel
|
|
2
2
|
|
|
3
3
|
an operator panel for [yaebal](https://github.com/neverlane/yaebal) bots: view
|
|
4
|
-
incoming private-chat messages and reply from the browser. ships as a
|
|
4
|
+
incoming private-chat messages and reply from the browser, live. ships as a
|
|
5
5
|
self-contained `fetch` handler — mount it on any HTTP framework.
|
|
6
6
|
|
|
7
|
+
- **login page** on the panel root — paste your token, no secrets in the url
|
|
8
|
+
- **realtime** updates over server-sent events, with a polling safety net
|
|
9
|
+
- **media**: photos, docs, voice, video and albums — both directions, in the browser
|
|
10
|
+
- **persistence** via a pluggable `PanelStore` (in-memory + sqlite included)
|
|
11
|
+
- CORS, basePath mounting and failed-auth rate limiting
|
|
12
|
+
|
|
7
13
|
## install
|
|
8
14
|
|
|
9
15
|
```sh
|
|
@@ -15,6 +21,7 @@ pnpm add @yaebal/panel
|
|
|
15
21
|
```ts
|
|
16
22
|
import { Bot } from "@yaebal/core";
|
|
17
23
|
import { MemoryPanelStore, recorder, panelHandler } from "@yaebal/panel";
|
|
24
|
+
import { serve } from "@yaebal/panel/serve";
|
|
18
25
|
|
|
19
26
|
const bot = new Bot(token);
|
|
20
27
|
const store = new MemoryPanelStore();
|
|
@@ -22,13 +29,137 @@ const store = new MemoryPanelStore();
|
|
|
22
29
|
bot.install(recorder(store)); // log incoming private messages
|
|
23
30
|
bot.start();
|
|
24
31
|
|
|
25
|
-
// serve the panel
|
|
32
|
+
// serve the panel — a fetch handler: (Request) => Promise<Response>
|
|
33
|
+
const handler = panelHandler(bot.api, store, { token: process.env.PANEL_TOKEN! });
|
|
34
|
+
serve(handler, { port: 8080 });
|
|
35
|
+
// open http://localhost:8080 and paste your token on the login screen
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The panel root serves a small SPA: a centered login (token input + **authorize**
|
|
39
|
+
button), then the live chat view. The token is kept in `sessionStorage` and sent as
|
|
40
|
+
`Authorization: Bearer …` — it never rides in the page url.
|
|
41
|
+
|
|
42
|
+
## mounting
|
|
43
|
+
|
|
44
|
+
`panelHandler` returns a plain `(Request) => Promise<Response>` — it binds no port of
|
|
45
|
+
its own. pick a port when you wire it into a server.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// node 20+ — `serve` ships in the box (native `node:http`, no deps)
|
|
49
|
+
import { serve } from "@yaebal/panel/serve";
|
|
50
|
+
|
|
51
|
+
serve(handler, { port: 8080, onListen: ({ port }) => console.log(`panel on :${port}`) });
|
|
52
|
+
|
|
53
|
+
// bun
|
|
54
|
+
Bun.serve({ port: 8080, fetch: handler });
|
|
55
|
+
|
|
56
|
+
// deno
|
|
57
|
+
Deno.serve({ port: 8080 }, handler);
|
|
58
|
+
|
|
59
|
+
// hono / any fetch framework
|
|
60
|
+
app.all("/panel/*", (c) => handler(c.req.raw)); // pair with basePath: "/panel"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`serve` is a separate entry (`@yaebal/panel/serve`) so the main module stays free of
|
|
64
|
+
`node:` imports for edge bundles. on cloudflare workers, deno deploy or vercel edge
|
|
65
|
+
just export the handler as the `fetch` entry — same handler, no port:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export default { fetch: handler };
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## options
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
panelHandler(bot.api, store, {
|
|
75
|
+
token: process.env.PANEL_TOKEN!, // required shared secret
|
|
76
|
+
basePath: "/panel", // mount under a sub-path (default: root)
|
|
77
|
+
cors: "https://ops.example", // allow a browser origin (or a list, or "*")
|
|
78
|
+
rateLimit: { max: 10, windowMs: 60_000 }, // throttle failed auth (default); false to disable
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **`basePath`** — the UI builds its api urls from this, so no extra rewriting is
|
|
83
|
+
needed when you mount under a prefix.
|
|
84
|
+
- **`rateLimit`** — after `max` bad tokens within `windowMs`, a client gets `429`
|
|
85
|
+
with `Retry-After` until the window passes. keyed by `x-forwarded-for` / `x-real-ip`
|
|
86
|
+
by default; override with `clientKey`.
|
|
87
|
+
|
|
88
|
+
## persistence
|
|
89
|
+
|
|
90
|
+
implement `PanelStore` (`record` / `chats` / `history`, optional `subscribe` for
|
|
91
|
+
realtime) to keep conversations in redis, postgres, etc. a sqlite-backed store built
|
|
92
|
+
on node's native `node:sqlite` ships in the box:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { SqlitePanelStore } from "@yaebal/panel/sqlite";
|
|
96
|
+
|
|
97
|
+
const store = new SqlitePanelStore({ path: "./panel.db" }); // or ":memory:"
|
|
98
|
+
bot.install(recorder(store));
|
|
26
99
|
const handler = panelHandler(bot.api, store, { token: process.env.PANEL_TOKEN! });
|
|
27
|
-
// handler: (Request) => Promise<Response> — open /?token=<PANEL_TOKEN>
|
|
28
100
|
```
|
|
29
101
|
|
|
30
|
-
|
|
31
|
-
|
|
102
|
+
`subscribe` is what powers the SSE stream — a store without it still works, the UI
|
|
103
|
+
just falls back to polling.
|
|
104
|
+
|
|
105
|
+
## media
|
|
106
|
+
|
|
107
|
+
photos, documents, voice notes, video and **albums** flow both ways:
|
|
108
|
+
|
|
109
|
+
- **incoming** — the recorder stores each attachment's `file_id` (and album id). the
|
|
110
|
+
browser renders them inline: images, `<video>`, `<audio>`, or a download link for
|
|
111
|
+
documents. consecutive messages sharing a `media_group_id` are shown as one album.
|
|
112
|
+
- **outgoing** — the 📎 button in the composer uploads a file; the panel picks
|
|
113
|
+
`sendPhoto` / `sendVideo` / `sendVoice` / `sendDocument` / … from its mime type
|
|
114
|
+
(the text box becomes the caption).
|
|
115
|
+
|
|
116
|
+
media bytes are **proxied** through `GET /api/file?id=<file_id>` (the panel calls
|
|
117
|
+
`getFile` and streams the result) so the bot token never reaches the browser. this
|
|
118
|
+
needs an api with `call()` / `fileUrl()` — the real `@yaebal/core` `Api` has both;
|
|
119
|
+
without them, media routes answer `501` and text still works.
|
|
120
|
+
|
|
121
|
+
## what the recorder captures
|
|
122
|
+
|
|
123
|
+
incoming **private-chat** messages only. text and captions are stored verbatim;
|
|
124
|
+
a media-only message is previewed in the chat list as a `[photo]` / `[document]` /
|
|
125
|
+
`[voice]` / … placeholder. when you reply with text from the panel, the api accepts
|
|
126
|
+
`text` plus optional `parse_mode`, `reply_to_message_id` and `reply_parameters`,
|
|
127
|
+
forwarded to `sendMessage`.
|
|
128
|
+
|
|
129
|
+
`recorder` only sees **incoming** updates. to also capture replies the bot sends
|
|
130
|
+
*elsewhere* (e.g. `ctx.reply(...)` in your own handlers), hook the api with
|
|
131
|
+
`recordOutgoing` — and tell the panel to stop recording its own sends so they
|
|
132
|
+
aren't logged twice:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { recordOutgoing } from "@yaebal/panel";
|
|
136
|
+
|
|
137
|
+
recordOutgoing(bot.api, store); // logs every outgoing sendMessage to a private chat
|
|
138
|
+
const handler = panelHandler(bot.api, store, {
|
|
139
|
+
token: process.env.PANEL_TOKEN!,
|
|
140
|
+
recordSends: false, // recordOutgoing already covers panel replies
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## api routes
|
|
145
|
+
|
|
146
|
+
mounted relative to `basePath` (default root). all but the page require the token.
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
GET / → login + chat SPA (public)
|
|
150
|
+
GET /api/chats → PanelChat[] (sorted by lastDate desc)
|
|
151
|
+
GET /api/chats/:id → PanelMessage[] (?before=&limit= to page)
|
|
152
|
+
GET /api/stream → text/event-stream of record events
|
|
153
|
+
GET /api/file?id=<file_id> → proxied file bytes (getFile + stream)
|
|
154
|
+
POST /api/chats/:id/send → json { text, … } → sendMessage
|
|
155
|
+
multipart { file, caption?, type? } → sendPhoto/Document/…
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## example
|
|
159
|
+
|
|
160
|
+
a runnable, full-featured bot lives in
|
|
161
|
+
[`examples/panel`](https://github.com/neverlane/yaebal/tree/master/examples/panel) —
|
|
162
|
+
media both ways, `recordOutgoing`, login page and SSE, served on node.
|
|
32
163
|
|
|
33
164
|
---
|
|
34
165
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import type { Context, Plugin } from "@yaebal/core";
|
|
2
2
|
export { PANEL_HTML } from "./panel-html.js";
|
|
3
|
+
/** downloadable file kinds carried by a message (each maps to one `file_id`). */
|
|
4
|
+
export type AttachmentType = "photo" | "video" | "animation" | "audio" | "voice" | "video_note" | "document" | "sticker";
|
|
5
|
+
/** a downloadable attachment, referenced by telegram `file_id`. */
|
|
6
|
+
export interface PanelAttachment {
|
|
7
|
+
type: AttachmentType;
|
|
8
|
+
fileId: string;
|
|
9
|
+
fileName?: string;
|
|
10
|
+
mimeType?: string;
|
|
11
|
+
}
|
|
3
12
|
export interface PanelMessage {
|
|
4
13
|
direction: "in" | "out";
|
|
14
|
+
/** caption / text, or a `[kind]` placeholder when the message is media-only. */
|
|
5
15
|
text: string;
|
|
6
16
|
date: number;
|
|
17
|
+
/** downloadable attachments, fetched lazily through `GET /api/file?id=…`. */
|
|
18
|
+
attachments?: PanelAttachment[];
|
|
19
|
+
/** telegram album id — consecutive messages sharing it are one media group. */
|
|
20
|
+
mediaGroupId?: string;
|
|
7
21
|
}
|
|
8
22
|
export interface PanelChat {
|
|
9
23
|
id: number;
|
|
@@ -11,6 +25,19 @@ export interface PanelChat {
|
|
|
11
25
|
lastText: string;
|
|
12
26
|
lastDate: number;
|
|
13
27
|
}
|
|
28
|
+
/** options for reading a slice of a conversation. */
|
|
29
|
+
export interface HistoryOptions {
|
|
30
|
+
/** return only messages strictly older than this unix timestamp (for "load earlier"). */
|
|
31
|
+
before?: number;
|
|
32
|
+
/** cap the result to the most recent N messages within the window. */
|
|
33
|
+
limit?: number;
|
|
34
|
+
}
|
|
35
|
+
/** a change the panel may want to react to in real time. */
|
|
36
|
+
export interface PanelEvent {
|
|
37
|
+
type: "record";
|
|
38
|
+
chatId: number;
|
|
39
|
+
direction: "in" | "out";
|
|
40
|
+
}
|
|
14
41
|
/** where conversations are kept for the panel to read. implement for persistence. */
|
|
15
42
|
export interface PanelStore {
|
|
16
43
|
record(chat: {
|
|
@@ -18,7 +45,9 @@ export interface PanelStore {
|
|
|
18
45
|
name?: string;
|
|
19
46
|
}, message: PanelMessage): void | Promise<void>;
|
|
20
47
|
chats(): PanelChat[] | Promise<PanelChat[]>;
|
|
21
|
-
history(chatId: number): PanelMessage[] | Promise<PanelMessage[]>;
|
|
48
|
+
history(chatId: number, options?: HistoryOptions): PanelMessage[] | Promise<PanelMessage[]>;
|
|
49
|
+
/** optional realtime hook — return an unsubscribe fn. enables the panel's SSE stream. */
|
|
50
|
+
subscribe?(listener: (event: PanelEvent) => void): () => void;
|
|
22
51
|
}
|
|
23
52
|
/** defaults to in-memory store. Lost on restart — swap for a persistent one in production. */
|
|
24
53
|
export declare class MemoryPanelStore implements PanelStore {
|
|
@@ -28,21 +57,75 @@ export declare class MemoryPanelStore implements PanelStore {
|
|
|
28
57
|
name?: string;
|
|
29
58
|
}, message: PanelMessage): void;
|
|
30
59
|
chats(): PanelChat[];
|
|
31
|
-
history(chatId: number): PanelMessage[];
|
|
60
|
+
history(chatId: number, options?: HistoryOptions): PanelMessage[];
|
|
61
|
+
subscribe(listener: (event: PanelEvent) => void): () => void;
|
|
32
62
|
}
|
|
33
63
|
/** records incoming private-chat text into the store so the panel can show it. */
|
|
34
64
|
export declare function recorder(store: PanelStore): Plugin<Context, Record<never, never>>;
|
|
35
|
-
|
|
65
|
+
/**
|
|
66
|
+
* what {@link panelHandler} needs from the api. `sendMessage` is required; `call` and
|
|
67
|
+
* `fileUrl` unlock media (file proxying + operator uploads) and are present on the real
|
|
68
|
+
* `@yaebal/core` `Api`. without them, media routes answer `501`.
|
|
69
|
+
*/
|
|
70
|
+
interface PanelApi {
|
|
36
71
|
sendMessage(params: Record<string, unknown>): Promise<unknown>;
|
|
72
|
+
call?<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
73
|
+
fileUrl?(filePath: string): string;
|
|
74
|
+
}
|
|
75
|
+
/** the slice of `@yaebal/core`'s `Api` that {@link recordOutgoing} needs. */
|
|
76
|
+
interface AfterHookApi {
|
|
77
|
+
after(hook: (method: string, result: unknown) => unknown): unknown;
|
|
37
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* record replies the bot sends *outside* the panel (e.g. `ctx.reply(...)` or `ctx.replyWithPhoto(...)`
|
|
81
|
+
* in your handlers) so they show up in the conversation too. hooks the api's `after` stage and logs
|
|
82
|
+
* every successful `send*` call (including `sendMediaGroup`, which returns an array) to a private chat.
|
|
83
|
+
*
|
|
84
|
+
* pairs with `panelHandler(..., { recordSends: false })` — the panel records its own sends,
|
|
85
|
+
* so disable that to avoid double entries when this is installed.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* recordOutgoing(bot.api, store);
|
|
89
|
+
* const handler = panelHandler(bot.api, store, { token, recordSends: false });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function recordOutgoing<A extends AfterHookApi>(api: A, store: PanelStore): A;
|
|
38
93
|
export interface PanelOptions {
|
|
39
94
|
/** shared secret required to open the panel and call its api. */
|
|
40
95
|
token: string;
|
|
96
|
+
/**
|
|
97
|
+
* allowed CORS origin(s) for the panel api. omit for same-origin only (default).
|
|
98
|
+
* pass `"*"` to allow any origin, or an explicit list to echo a matching `Origin` back.
|
|
99
|
+
*/
|
|
100
|
+
cors?: string | string[];
|
|
101
|
+
/**
|
|
102
|
+
* mount prefix when the handler does not live at the server root, e.g. `"/panel"`.
|
|
103
|
+
* the UI builds its api urls from this, so no extra rewriting is needed. default `""`.
|
|
104
|
+
*/
|
|
105
|
+
basePath?: string;
|
|
106
|
+
/**
|
|
107
|
+
* throttle failed auth attempts per client. defaults to 10 failures / 60s, then `429`
|
|
108
|
+
* until the window passes. pass `false` to disable.
|
|
109
|
+
*/
|
|
110
|
+
rateLimit?: {
|
|
111
|
+
max?: number;
|
|
112
|
+
windowMs?: number;
|
|
113
|
+
} | false;
|
|
114
|
+
/**
|
|
115
|
+
* derive a client key for rate limiting. defaults to `x-forwarded-for` / `x-real-ip`,
|
|
116
|
+
* falling back to a single shared bucket when no proxy header is present.
|
|
117
|
+
*/
|
|
118
|
+
clientKey?: (request: Request) => string;
|
|
119
|
+
/**
|
|
120
|
+
* record replies sent from the panel into the store. default `true`. set to `false`
|
|
121
|
+
* when you use {@link recordOutgoing}, which already captures every outgoing message.
|
|
122
|
+
*/
|
|
123
|
+
recordSends?: boolean;
|
|
41
124
|
}
|
|
42
125
|
/**
|
|
43
|
-
* a fetch-style handler for the operator panel: serves the
|
|
44
|
-
* api to list chats, read a conversation, and send a
|
|
45
|
-
*
|
|
126
|
+
* a fetch-style handler for the operator panel: serves the login + chat UI at the mount
|
|
127
|
+
* root, and a small api to list chats, read a conversation, stream updates, and send a
|
|
128
|
+
* reply. mount it on any fetch-compatible server.
|
|
46
129
|
*/
|
|
47
|
-
export declare function panelHandler(api:
|
|
130
|
+
export declare function panelHandler(api: PanelApi, store: PanelStore, options: PanelOptions): (request: Request) => Promise<Response>;
|
|
48
131
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|