@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 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 (auth via a required token)
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
- implement `PanelStore` (`record` / `chats` / `history`) to persist conversations
31
- in redis, postgres, etc. instead of the in-memory default.
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
- interface SendApi {
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 ui at `/`, and a small
44
- * api to list chats, read a conversation, and send a reply. mount it on any
45
- * fetch-compatible server. open it at `/?token=<your token>`.
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: SendApi, store: PanelStore, options: PanelOptions): (request: Request) => Promise<Response>;
130
+ export declare function panelHandler(api: PanelApi, store: PanelStore, options: PanelOptions): (request: Request) => Promise<Response>;
48
131
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAgBpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,IAAI,GAAG,KAAK,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,KAAK,IAAI,SAAS,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAClE;AAED,8FAA8F;AAC9F,qBAAa,gBAAiB,YAAW,UAAU;;IAIlD,MAAM,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAiBxE,KAAK,IAAI,SAAS,EAAE;IAIpB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE;CAGvC;AAED,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAqBjF;AAED,UAAU,OAAO;IAChB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,YAAY;IAC5B,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;CACd;AAQD;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,YAAY,GACnB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuDzC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}