@yaebal/panel 0.0.1 → 0.0.4

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/lib/serve.js ADDED
@@ -0,0 +1,47 @@
1
+ import { createServer } from "node:http";
2
+ /** translate a node request into a whatwg `Request` (body streamed for non-GET/HEAD). */
3
+ function toRequest(req) {
4
+ const host = req.headers.host ?? "localhost";
5
+ const url = `http://${host}${req.url ?? "/"}`;
6
+ const method = req.method ?? "GET";
7
+ const hasBody = method !== "GET" && method !== "HEAD";
8
+ return new Request(url, {
9
+ method,
10
+ headers: req.headers,
11
+ // node streams are async-iterable, which `Request` accepts as a body source
12
+ body: hasBody ? req : undefined,
13
+ // required by undici when streaming a request body
14
+ duplex: "half",
15
+ });
16
+ }
17
+ /** pipe a whatwg `Response` back out through a node `ServerResponse`. */
18
+ async function writeResponse(res, response) {
19
+ res.writeHead(response.status, Object.fromEntries(response.headers));
20
+ res.end(Buffer.from(await response.arrayBuffer()));
21
+ }
22
+ /**
23
+ * start a native node `http` server for a fetch-style handler (e.g. {@link panelHandler}).
24
+ * zero third-party deps — just `node:http`. on bun/deno use their built-in `serve` instead.
25
+ *
26
+ * ```ts
27
+ * import { panelHandler } from "@yaebal/panel";
28
+ * import { serve } from "@yaebal/panel/serve";
29
+ * serve(panelHandler(bot.api, store, { token }), { port: 8080 });
30
+ * ```
31
+ */
32
+ export function serve(handler, options) {
33
+ const server = createServer((req, res) => {
34
+ Promise.resolve(handler(toRequest(req)))
35
+ .then((response) => writeResponse(res, response))
36
+ .catch(() => {
37
+ if (!res.headersSent)
38
+ res.writeHead(500);
39
+ res.end("internal error");
40
+ });
41
+ });
42
+ server.listen(options.port, options.host, () => {
43
+ options.onListen?.({ port: options.port, host: options.host });
44
+ });
45
+ return server;
46
+ }
47
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0D,MAAM,WAAW,CAAC;AAYjG,yFAAyF;AACzF,SAAS,SAAS,CAAC,GAAoB;IACtC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;IAC7C,MAAM,GAAG,GAAG,UAAU,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;IAE9C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC;IACnC,MAAM,OAAO,GAAG,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC;IAEtD,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE;QACvB,MAAM;QACN,OAAO,EAAE,GAAG,CAAC,OAAiC;QAC9C,4EAA4E;QAC5E,IAAI,EAAE,OAAO,CAAC,CAAC,CAAE,GAAiC,CAAC,CAAC,CAAC,SAAS;QAC9D,mDAAmD;QACnD,MAAM,EAAE,MAAM;KACC,CAAC,CAAC;AACnB,CAAC;AAED,yEAAyE;AACzE,KAAK,UAAU,aAAa,CAAC,GAAmB,EAAE,QAAkB;IACnE,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACrE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,KAAK,CACpB,OAA2D,EAC3D,OAAqB;IAErB,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACxC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;aACtC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;aAChD,KAAK,CAAC,GAAG,EAAE;YACX,IAAI,CAAC,GAAG,CAAC,WAAW;gBAAE,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACzC,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE;QAC9C,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AACf,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import type { HistoryOptions, PanelChat, PanelChatRecord, PanelEvent, PanelMessage, PanelStore } from "./index.js";
3
+ /** options for {@link SqlitePanelStore}. */
4
+ export interface SqlitePanelStoreOptions {
5
+ /** sqlite file path, or `":memory:"` (default). ignored when `db` is provided. */
6
+ path?: string;
7
+ /** bring your own `node:sqlite` database instead of opening one. */
8
+ db?: DatabaseSync;
9
+ }
10
+ /**
11
+ * a persistent {@link PanelStore} backed by node's built-in `node:sqlite` (node 22.5+).
12
+ * zero third-party deps. import from `@yaebal/panel/sqlite`.
13
+ *
14
+ * ```ts
15
+ * import { SqlitePanelStore } from "@yaebal/panel/sqlite";
16
+ * const store = new SqlitePanelStore({ path: "./panel.db" });
17
+ * ```
18
+ */
19
+ export declare class SqlitePanelStore implements PanelStore {
20
+ #private;
21
+ constructor(options?: SqlitePanelStoreOptions);
22
+ record(chat: PanelChatRecord, message: PanelMessage): void;
23
+ chats(): PanelChat[];
24
+ history(chatId: number, options?: HistoryOptions): PanelMessage[];
25
+ subscribe(listener: (event: PanelEvent) => void): () => void;
26
+ /** close the underlying database (no-op if you passed your own `db`). */
27
+ close(): void;
28
+ }
29
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EACX,cAAc,EAEd,SAAS,EACT,eAAe,EACf,UAAU,EAEV,YAAY,EAEZ,UAAU,EACV,MAAM,YAAY,CAAC;AAUpB,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACvC,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,EAAE,CAAC,EAAE,YAAY,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,YAAW,UAAU;;gBAItC,OAAO,GAAE,uBAA4B;IAqCjD,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAwD1D,KAAK,IAAI,SAAS,EAAE;IAmCpB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,YAAY,EAAE;IAiCjE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAK5D,yEAAyE;IACzE,KAAK,IAAI,IAAI;CAGb"}
package/lib/sqlite.js ADDED
@@ -0,0 +1,155 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ function addColumn(db, table, definition) {
3
+ try {
4
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition}`);
5
+ }
6
+ catch {
7
+ // Existing sqlite stores from older panel versions simply already have the column.
8
+ }
9
+ }
10
+ /**
11
+ * a persistent {@link PanelStore} backed by node's built-in `node:sqlite` (node 22.5+).
12
+ * zero third-party deps. import from `@yaebal/panel/sqlite`.
13
+ *
14
+ * ```ts
15
+ * import { SqlitePanelStore } from "@yaebal/panel/sqlite";
16
+ * const store = new SqlitePanelStore({ path: "./panel.db" });
17
+ * ```
18
+ */
19
+ export class SqlitePanelStore {
20
+ #db;
21
+ #listeners = new Set();
22
+ constructor(options = {}) {
23
+ this.#db = options.db ?? new DatabaseSync(options.path ?? ":memory:");
24
+ this.#db.exec(`
25
+ CREATE TABLE IF NOT EXISTS panel_chats (
26
+ id INTEGER PRIMARY KEY,
27
+ name TEXT,
28
+ first_name TEXT,
29
+ last_name TEXT,
30
+ username TEXT,
31
+ last_text TEXT NOT NULL,
32
+ last_date INTEGER NOT NULL,
33
+ last_attachment TEXT,
34
+ last_event TEXT
35
+ );
36
+ CREATE TABLE IF NOT EXISTS panel_messages (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ chat_id INTEGER NOT NULL,
39
+ direction TEXT NOT NULL,
40
+ text TEXT NOT NULL,
41
+ date INTEGER NOT NULL,
42
+ attachments TEXT,
43
+ media_group TEXT,
44
+ keyboard TEXT,
45
+ event TEXT
46
+ );
47
+ CREATE INDEX IF NOT EXISTS panel_messages_chat ON panel_messages (chat_id, date);
48
+ `);
49
+ addColumn(this.#db, "panel_chats", "first_name TEXT");
50
+ addColumn(this.#db, "panel_chats", "last_name TEXT");
51
+ addColumn(this.#db, "panel_chats", "username TEXT");
52
+ addColumn(this.#db, "panel_chats", "last_attachment TEXT");
53
+ addColumn(this.#db, "panel_chats", "last_event TEXT");
54
+ addColumn(this.#db, "panel_messages", "keyboard TEXT");
55
+ addColumn(this.#db, "panel_messages", "event TEXT");
56
+ }
57
+ record(chat, message) {
58
+ this.#db
59
+ .prepare("INSERT INTO panel_messages (chat_id, direction, text, date, attachments, media_group, keyboard, event) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
60
+ .run(chat.id, message.direction, message.text, message.date, message.attachments ? JSON.stringify(message.attachments) : null, message.mediaGroupId ?? null, message.keyboard ? JSON.stringify(message.keyboard) : null, message.event ? JSON.stringify(message.event) : null);
61
+ // keep the name when an outgoing message omits it; COALESCE the existing row's value
62
+ this.#db
63
+ .prepare(`
64
+ INSERT INTO panel_chats (
65
+ id, name, first_name, last_name, username, last_text, last_date, last_attachment, last_event
66
+ ) VALUES (
67
+ :id, :name, :firstName, :lastName, :username, :text, :date, :lastAttachment, :lastEvent
68
+ )
69
+ ON CONFLICT(id) DO UPDATE SET
70
+ name = COALESCE(:name, panel_chats.name),
71
+ first_name = COALESCE(:firstName, panel_chats.first_name),
72
+ last_name = COALESCE(:lastName, panel_chats.last_name),
73
+ username = COALESCE(:username, panel_chats.username),
74
+ last_text = :text,
75
+ last_date = :date,
76
+ last_attachment = :lastAttachment,
77
+ last_event = :lastEvent
78
+ `)
79
+ .run({
80
+ id: chat.id,
81
+ name: chat.name ?? null,
82
+ firstName: chat.firstName ?? null,
83
+ lastName: chat.lastName ?? null,
84
+ username: chat.username ?? null,
85
+ text: message.text,
86
+ date: message.date,
87
+ lastAttachment: message.attachments?.[0]?.type ?? null,
88
+ lastEvent: message.event?.type ?? null,
89
+ });
90
+ // brand-new chat with no name → give it a stable fallback label
91
+ this.#db
92
+ .prepare("UPDATE panel_chats SET name = ? WHERE id = ? AND name IS NULL")
93
+ .run(`chat ${chat.id}`, chat.id);
94
+ for (const fn of this.#listeners) {
95
+ fn({ type: "record", chatId: chat.id, direction: message.direction });
96
+ }
97
+ }
98
+ chats() {
99
+ const rows = this.#db
100
+ .prepare("SELECT id, name, first_name, last_name, username, last_text, last_date, last_attachment, last_event FROM panel_chats ORDER BY last_date DESC")
101
+ .all();
102
+ return rows.map((r) => {
103
+ const chat = {
104
+ id: r.id,
105
+ name: r.name ?? `chat ${r.id}`,
106
+ lastText: r.last_text,
107
+ lastDate: r.last_date,
108
+ };
109
+ if (r.first_name)
110
+ chat.firstName = r.first_name;
111
+ if (r.last_name)
112
+ chat.lastName = r.last_name;
113
+ if (r.username)
114
+ chat.username = r.username;
115
+ if (r.last_attachment)
116
+ chat.lastAttachmentType = r.last_attachment;
117
+ if (r.last_event)
118
+ chat.lastEventType = r.last_event;
119
+ return chat;
120
+ });
121
+ }
122
+ history(chatId, options) {
123
+ const before = options?.before ?? Number.MAX_SAFE_INTEGER;
124
+ const limit = options?.limit ?? -1; // sqlite: negative LIMIT = no limit
125
+ // grab the most recent `limit` rows older than `before`, then return ascending
126
+ const rows = this.#db
127
+ .prepare(`
128
+ SELECT direction, text, date, attachments, media_group, keyboard, event FROM panel_messages
129
+ WHERE chat_id = ? AND date < ?
130
+ ORDER BY date DESC, id DESC LIMIT ?
131
+ `)
132
+ .all(chatId, before, limit);
133
+ return rows.reverse().map((r) => {
134
+ const msg = { direction: r.direction, text: r.text, date: r.date };
135
+ if (r.attachments)
136
+ msg.attachments = JSON.parse(r.attachments);
137
+ if (r.media_group)
138
+ msg.mediaGroupId = r.media_group;
139
+ if (r.keyboard)
140
+ msg.keyboard = JSON.parse(r.keyboard);
141
+ if (r.event)
142
+ msg.event = JSON.parse(r.event);
143
+ return msg;
144
+ });
145
+ }
146
+ subscribe(listener) {
147
+ this.#listeners.add(listener);
148
+ return () => this.#listeners.delete(listener);
149
+ }
150
+ /** close the underlying database (no-op if you passed your own `db`). */
151
+ close() {
152
+ this.#db.close();
153
+ }
154
+ }
155
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sqlite.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.test.d.ts","sourceRoot":"","sources":["../src/sqlite.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ // `node:sqlite` (and therefore `SqlitePanelStore`) is only available on node 22.5+.
4
+ // on older runtimes a static import of "./sqlite.js" throws ERR_UNKNOWN_BUILTIN_MODULE
5
+ // at module-evaluation time, which would fail the whole file. import it lazily and
6
+ // skip the suite when the builtin is missing instead.
7
+ let SqlitePanelStore;
8
+ try {
9
+ ({ SqlitePanelStore } = await import("./sqlite.js"));
10
+ }
11
+ catch {
12
+ SqlitePanelStore = undefined;
13
+ }
14
+ const skip = SqlitePanelStore ? false : "node:sqlite unavailable (requires node 22.5+)";
15
+ // non-undefined alias for the test bodies (only reached when not skipped)
16
+ const Store = SqlitePanelStore;
17
+ test("SqlitePanelStore: records, lists newest-first, paginates and emits events", { skip }, () => {
18
+ const store = new Store(); // :memory:
19
+ const events = [];
20
+ store.subscribe((e) => events.push(e.chatId));
21
+ store.record({ id: 1, name: "@sam" }, { direction: "in", text: "hi", date: 10 });
22
+ store.record({ id: 2, name: "@lee" }, { direction: "in", text: "yo", date: 20 });
23
+ for (let i = 1; i <= 5; i++) {
24
+ store.record({ id: 1 }, { direction: "in", text: `m${i}`, date: 100 + i });
25
+ }
26
+ // chats sorted by last_date desc; name preserved across nameless records
27
+ const chats = store.chats();
28
+ assert.equal(chats[0]?.id, 1);
29
+ assert.equal(chats[0]?.name, "@sam");
30
+ assert.equal(chats[0]?.lastText, "m5");
31
+ // pagination
32
+ assert.deepEqual(store.history(1, { limit: 2 }).map((m) => m.text), ["m4", "m5"]);
33
+ assert.deepEqual(store.history(1, { before: 103, limit: 2 }).map((m) => m.text), ["m1", "m2"]);
34
+ assert.ok(events.length >= 2);
35
+ store.close();
36
+ });
37
+ test("SqlitePanelStore persists attachments and media_group_id round-trip", { skip }, () => {
38
+ const store = new Store();
39
+ store.record({ id: 1, name: "@u", firstName: "Uma", lastName: "Ray", username: "u" }, {
40
+ direction: "in",
41
+ text: "[photo]",
42
+ date: 1,
43
+ attachments: [{ type: "photo", fileId: "f1" }],
44
+ mediaGroupId: "G1",
45
+ keyboard: {
46
+ type: "inline",
47
+ rows: [[{ text: "Open", kind: "callback", callbackData: "open" }]],
48
+ },
49
+ });
50
+ store.record({ id: 1, name: "@u" }, {
51
+ direction: "in",
52
+ text: "button clicked: open",
53
+ date: 2,
54
+ event: { type: "callback", title: "button clicked", detail: "open", data: "open" },
55
+ });
56
+ const chat = store.chats()[0];
57
+ assert.equal(chat?.firstName, "Uma");
58
+ assert.equal(chat?.lastName, "Ray");
59
+ assert.equal(chat?.username, "u");
60
+ assert.equal(chat?.lastAttachmentType, undefined);
61
+ assert.equal(chat?.lastEventType, "callback");
62
+ const hist = store.history(1);
63
+ assert.deepEqual(hist[0]?.attachments, [{ type: "photo", fileId: "f1" }]);
64
+ assert.equal(hist[0]?.mediaGroupId, "G1");
65
+ assert.deepEqual(hist[0]?.keyboard, {
66
+ type: "inline",
67
+ rows: [[{ text: "Open", kind: "callback", callbackData: "open" }]],
68
+ });
69
+ // event message has no attachments/group keys
70
+ assert.equal(hist[1]?.attachments, undefined);
71
+ assert.equal(hist[1]?.mediaGroupId, undefined);
72
+ assert.equal(hist[1]?.event?.type, "callback");
73
+ store.close();
74
+ });
75
+ //# sourceMappingURL=sqlite.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.test.js","sourceRoot":"","sources":["../src/sqlite.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaebal/panel",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "yaebal panel — an operator panel: view chats and reply from the browser.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -9,6 +9,14 @@
9
9
  ".": {
10
10
  "types": "./lib/index.d.ts",
11
11
  "import": "./lib/index.js"
12
+ },
13
+ "./serve": {
14
+ "types": "./lib/serve.d.ts",
15
+ "import": "./lib/serve.js"
16
+ },
17
+ "./sqlite": {
18
+ "types": "./lib/sqlite.d.ts",
19
+ "import": "./lib/sqlite.js"
12
20
  }
13
21
  },
14
22
  "files": [
@@ -16,7 +24,7 @@
16
24
  "src"
17
25
  ],
18
26
  "dependencies": {
19
- "@yaebal/core": "0.0.1"
27
+ "@yaebal/core": "0.0.5"
20
28
  },
21
29
  "devDependencies": {
22
30
  "@types/node": "latest"