@yaebal/panel 0.0.2 → 0.0.5
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 +156 -96
- package/lib/index.d.ts +43 -12
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +253 -20
- package/lib/index.js.map +1 -1
- package/lib/index.test.js +81 -3
- 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 +412 -115
- package/lib/panel-html.js.map +1 -1
- package/lib/serve.d.ts.map +1 -1
- package/lib/serve.js.map +1 -1
- package/lib/sqlite.d.ts +2 -5
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +76 -18
- package/lib/sqlite.js.map +1 -1
- package/lib/sqlite.test.js +41 -8
- package/lib/sqlite.test.js.map +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +104 -4
- package/src/index.ts +327 -30
- package/src/panel-html.ts +412 -115
- package/src/serve.ts +1 -1
- package/src/sqlite.test.ts +47 -9
- package/src/sqlite.ts +94 -21
package/src/serve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type IncomingMessage, type Server, type ServerResponse
|
|
1
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
2
|
|
|
3
3
|
/** options for the node {@link serve} helper. */
|
|
4
4
|
export interface ServeOptions {
|
package/src/sqlite.test.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { SqlitePanelStore } from "./sqlite.js";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// `node:sqlite` (and therefore `SqlitePanelStore`) is only available on node 22.5+.
|
|
5
|
+
// on older runtimes a static import of "./sqlite.js" throws ERR_UNKNOWN_BUILTIN_MODULE
|
|
6
|
+
// at module-evaluation time, which would fail the whole file. import it lazily and
|
|
7
|
+
// skip the suite when the builtin is missing instead.
|
|
8
|
+
let SqlitePanelStore: typeof import("./sqlite.js").SqlitePanelStore | undefined;
|
|
9
|
+
try {
|
|
10
|
+
({ SqlitePanelStore } = await import("./sqlite.js"));
|
|
11
|
+
} catch {
|
|
12
|
+
SqlitePanelStore = undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const skip = SqlitePanelStore ? false : "node:sqlite unavailable (requires node 22.5+)";
|
|
16
|
+
// non-undefined alias for the test bodies (only reached when not skipped)
|
|
17
|
+
const Store = SqlitePanelStore as NonNullable<typeof SqlitePanelStore>;
|
|
18
|
+
|
|
19
|
+
test("SqlitePanelStore: records, lists newest-first, paginates and emits events", { skip }, () => {
|
|
20
|
+
const store = new Store(); // :memory:
|
|
7
21
|
const events: number[] = [];
|
|
8
22
|
store.subscribe((e) => events.push(e.chatId));
|
|
9
23
|
|
|
@@ -33,26 +47,50 @@ test("SqlitePanelStore: records, lists newest-first, paginates and emits events"
|
|
|
33
47
|
store.close();
|
|
34
48
|
});
|
|
35
49
|
|
|
36
|
-
test("SqlitePanelStore persists attachments and media_group_id round-trip", () => {
|
|
37
|
-
const store = new
|
|
50
|
+
test("SqlitePanelStore persists attachments and media_group_id round-trip", { skip }, () => {
|
|
51
|
+
const store = new Store();
|
|
38
52
|
|
|
39
53
|
store.record(
|
|
40
|
-
{ id: 1, name: "@u" },
|
|
54
|
+
{ id: 1, name: "@u", firstName: "Uma", lastName: "Ray", username: "u" },
|
|
41
55
|
{
|
|
42
56
|
direction: "in",
|
|
43
57
|
text: "[photo]",
|
|
44
58
|
date: 1,
|
|
45
59
|
attachments: [{ type: "photo", fileId: "f1" }],
|
|
46
60
|
mediaGroupId: "G1",
|
|
61
|
+
keyboard: {
|
|
62
|
+
type: "inline",
|
|
63
|
+
rows: [[{ text: "Open", kind: "callback", callbackData: "open" }]],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
store.record(
|
|
68
|
+
{ id: 1, name: "@u" },
|
|
69
|
+
{
|
|
70
|
+
direction: "in",
|
|
71
|
+
text: "button clicked: open",
|
|
72
|
+
date: 2,
|
|
73
|
+
event: { type: "callback", title: "button clicked", detail: "open", data: "open" },
|
|
47
74
|
},
|
|
48
75
|
);
|
|
49
|
-
|
|
76
|
+
|
|
77
|
+
const chat = store.chats()[0];
|
|
78
|
+
assert.equal(chat?.firstName, "Uma");
|
|
79
|
+
assert.equal(chat?.lastName, "Ray");
|
|
80
|
+
assert.equal(chat?.username, "u");
|
|
81
|
+
assert.equal(chat?.lastAttachmentType, undefined);
|
|
82
|
+
assert.equal(chat?.lastEventType, "callback");
|
|
50
83
|
|
|
51
84
|
const hist = store.history(1);
|
|
52
85
|
assert.deepEqual(hist[0]?.attachments, [{ type: "photo", fileId: "f1" }]);
|
|
53
86
|
assert.equal(hist[0]?.mediaGroupId, "G1");
|
|
54
|
-
|
|
87
|
+
assert.deepEqual(hist[0]?.keyboard, {
|
|
88
|
+
type: "inline",
|
|
89
|
+
rows: [[{ text: "Open", kind: "callback", callbackData: "open" }]],
|
|
90
|
+
});
|
|
91
|
+
// event message has no attachments/group keys
|
|
55
92
|
assert.equal(hist[1]?.attachments, undefined);
|
|
56
93
|
assert.equal(hist[1]?.mediaGroupId, undefined);
|
|
94
|
+
assert.equal(hist[1]?.event?.type, "callback");
|
|
57
95
|
store.close();
|
|
58
|
-
});
|
|
96
|
+
});
|
package/src/sqlite.ts
CHANGED
|
@@ -3,11 +3,22 @@ import type {
|
|
|
3
3
|
HistoryOptions,
|
|
4
4
|
PanelAttachment,
|
|
5
5
|
PanelChat,
|
|
6
|
+
PanelChatRecord,
|
|
6
7
|
PanelEvent,
|
|
8
|
+
PanelKeyboard,
|
|
7
9
|
PanelMessage,
|
|
10
|
+
PanelMessageEvent,
|
|
8
11
|
PanelStore,
|
|
9
12
|
} from "./index.js";
|
|
10
13
|
|
|
14
|
+
function addColumn(db: DatabaseSync, table: string, definition: string): void {
|
|
15
|
+
try {
|
|
16
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition}`);
|
|
17
|
+
} catch {
|
|
18
|
+
// Existing sqlite stores from older panel versions simply already have the column.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/** options for {@link SqlitePanelStore}. */
|
|
12
23
|
export interface SqlitePanelStoreOptions {
|
|
13
24
|
/** sqlite file path, or `":memory:"` (default). ignored when `db` is provided. */
|
|
@@ -33,10 +44,15 @@ export class SqlitePanelStore implements PanelStore {
|
|
|
33
44
|
this.#db = options.db ?? new DatabaseSync(options.path ?? ":memory:");
|
|
34
45
|
this.#db.exec(`
|
|
35
46
|
CREATE TABLE IF NOT EXISTS panel_chats (
|
|
36
|
-
id
|
|
37
|
-
name
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
id INTEGER PRIMARY KEY,
|
|
48
|
+
name TEXT,
|
|
49
|
+
first_name TEXT,
|
|
50
|
+
last_name TEXT,
|
|
51
|
+
username TEXT,
|
|
52
|
+
last_text TEXT NOT NULL,
|
|
53
|
+
last_date INTEGER NOT NULL,
|
|
54
|
+
last_attachment TEXT,
|
|
55
|
+
last_event TEXT
|
|
40
56
|
);
|
|
41
57
|
CREATE TABLE IF NOT EXISTS panel_messages (
|
|
42
58
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -45,16 +61,26 @@ export class SqlitePanelStore implements PanelStore {
|
|
|
45
61
|
text TEXT NOT NULL,
|
|
46
62
|
date INTEGER NOT NULL,
|
|
47
63
|
attachments TEXT,
|
|
48
|
-
media_group TEXT
|
|
64
|
+
media_group TEXT,
|
|
65
|
+
keyboard TEXT,
|
|
66
|
+
event TEXT
|
|
49
67
|
);
|
|
50
68
|
CREATE INDEX IF NOT EXISTS panel_messages_chat ON panel_messages (chat_id, date);
|
|
51
69
|
`);
|
|
70
|
+
|
|
71
|
+
addColumn(this.#db, "panel_chats", "first_name TEXT");
|
|
72
|
+
addColumn(this.#db, "panel_chats", "last_name TEXT");
|
|
73
|
+
addColumn(this.#db, "panel_chats", "username TEXT");
|
|
74
|
+
addColumn(this.#db, "panel_chats", "last_attachment TEXT");
|
|
75
|
+
addColumn(this.#db, "panel_chats", "last_event TEXT");
|
|
76
|
+
addColumn(this.#db, "panel_messages", "keyboard TEXT");
|
|
77
|
+
addColumn(this.#db, "panel_messages", "event TEXT");
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
record(chat:
|
|
80
|
+
record(chat: PanelChatRecord, message: PanelMessage): void {
|
|
55
81
|
this.#db
|
|
56
82
|
.prepare(
|
|
57
|
-
"INSERT INTO panel_messages (chat_id, direction, text, date, attachments, media_group) VALUES (?, ?, ?, ?, ?, ?)",
|
|
83
|
+
"INSERT INTO panel_messages (chat_id, direction, text, date, attachments, media_group, keyboard, event) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
58
84
|
)
|
|
59
85
|
.run(
|
|
60
86
|
chat.id,
|
|
@@ -63,18 +89,39 @@ export class SqlitePanelStore implements PanelStore {
|
|
|
63
89
|
message.date,
|
|
64
90
|
message.attachments ? JSON.stringify(message.attachments) : null,
|
|
65
91
|
message.mediaGroupId ?? null,
|
|
92
|
+
message.keyboard ? JSON.stringify(message.keyboard) : null,
|
|
93
|
+
message.event ? JSON.stringify(message.event) : null,
|
|
66
94
|
);
|
|
67
95
|
|
|
68
96
|
// keep the name when an outgoing message omits it; COALESCE the existing row's value
|
|
69
97
|
this.#db
|
|
70
98
|
.prepare(`
|
|
71
|
-
INSERT INTO panel_chats (
|
|
99
|
+
INSERT INTO panel_chats (
|
|
100
|
+
id, name, first_name, last_name, username, last_text, last_date, last_attachment, last_event
|
|
101
|
+
) VALUES (
|
|
102
|
+
:id, :name, :firstName, :lastName, :username, :text, :date, :lastAttachment, :lastEvent
|
|
103
|
+
)
|
|
72
104
|
ON CONFLICT(id) DO UPDATE SET
|
|
73
105
|
name = COALESCE(:name, panel_chats.name),
|
|
106
|
+
first_name = COALESCE(:firstName, panel_chats.first_name),
|
|
107
|
+
last_name = COALESCE(:lastName, panel_chats.last_name),
|
|
108
|
+
username = COALESCE(:username, panel_chats.username),
|
|
74
109
|
last_text = :text,
|
|
75
|
-
last_date = :date
|
|
110
|
+
last_date = :date,
|
|
111
|
+
last_attachment = :lastAttachment,
|
|
112
|
+
last_event = :lastEvent
|
|
76
113
|
`)
|
|
77
|
-
.run({
|
|
114
|
+
.run({
|
|
115
|
+
id: chat.id,
|
|
116
|
+
name: chat.name ?? null,
|
|
117
|
+
firstName: chat.firstName ?? null,
|
|
118
|
+
lastName: chat.lastName ?? null,
|
|
119
|
+
username: chat.username ?? null,
|
|
120
|
+
text: message.text,
|
|
121
|
+
date: message.date,
|
|
122
|
+
lastAttachment: message.attachments?.[0]?.type ?? null,
|
|
123
|
+
lastEvent: message.event?.type ?? null,
|
|
124
|
+
});
|
|
78
125
|
|
|
79
126
|
// brand-new chat with no name → give it a stable fallback label
|
|
80
127
|
this.#db
|
|
@@ -88,15 +135,37 @@ export class SqlitePanelStore implements PanelStore {
|
|
|
88
135
|
|
|
89
136
|
chats(): PanelChat[] {
|
|
90
137
|
const rows = this.#db
|
|
91
|
-
.prepare(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id:
|
|
96
|
-
name:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
.prepare(
|
|
139
|
+
"SELECT id, name, first_name, last_name, username, last_text, last_date, last_attachment, last_event FROM panel_chats ORDER BY last_date DESC",
|
|
140
|
+
)
|
|
141
|
+
.all() as Array<{
|
|
142
|
+
id: number;
|
|
143
|
+
name: string | null;
|
|
144
|
+
first_name: string | null;
|
|
145
|
+
last_name: string | null;
|
|
146
|
+
username: string | null;
|
|
147
|
+
last_text: string;
|
|
148
|
+
last_date: number;
|
|
149
|
+
last_attachment: PanelChat["lastAttachmentType"] | null;
|
|
150
|
+
last_event: PanelChat["lastEventType"] | null;
|
|
151
|
+
}>;
|
|
152
|
+
|
|
153
|
+
return rows.map((r) => {
|
|
154
|
+
const chat: PanelChat = {
|
|
155
|
+
id: r.id,
|
|
156
|
+
name: r.name ?? `chat ${r.id}`,
|
|
157
|
+
lastText: r.last_text,
|
|
158
|
+
lastDate: r.last_date,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (r.first_name) chat.firstName = r.first_name;
|
|
162
|
+
if (r.last_name) chat.lastName = r.last_name;
|
|
163
|
+
if (r.username) chat.username = r.username;
|
|
164
|
+
if (r.last_attachment) chat.lastAttachmentType = r.last_attachment;
|
|
165
|
+
if (r.last_event) chat.lastEventType = r.last_event;
|
|
166
|
+
|
|
167
|
+
return chat;
|
|
168
|
+
});
|
|
100
169
|
}
|
|
101
170
|
|
|
102
171
|
history(chatId: number, options?: HistoryOptions): PanelMessage[] {
|
|
@@ -106,7 +175,7 @@ export class SqlitePanelStore implements PanelStore {
|
|
|
106
175
|
// grab the most recent `limit` rows older than `before`, then return ascending
|
|
107
176
|
const rows = this.#db
|
|
108
177
|
.prepare(`
|
|
109
|
-
SELECT direction, text, date, attachments, media_group FROM panel_messages
|
|
178
|
+
SELECT direction, text, date, attachments, media_group, keyboard, event FROM panel_messages
|
|
110
179
|
WHERE chat_id = ? AND date < ?
|
|
111
180
|
ORDER BY date DESC, id DESC LIMIT ?
|
|
112
181
|
`)
|
|
@@ -116,13 +185,17 @@ export class SqlitePanelStore implements PanelStore {
|
|
|
116
185
|
date: number;
|
|
117
186
|
attachments: string | null;
|
|
118
187
|
media_group: string | null;
|
|
188
|
+
keyboard: string | null;
|
|
189
|
+
event: string | null;
|
|
119
190
|
}>;
|
|
120
191
|
|
|
121
192
|
return rows.reverse().map((r) => {
|
|
122
193
|
const msg: PanelMessage = { direction: r.direction, text: r.text, date: r.date };
|
|
123
|
-
|
|
194
|
+
|
|
124
195
|
if (r.attachments) msg.attachments = JSON.parse(r.attachments) as PanelAttachment[];
|
|
125
196
|
if (r.media_group) msg.mediaGroupId = r.media_group;
|
|
197
|
+
if (r.keyboard) msg.keyboard = JSON.parse(r.keyboard) as PanelKeyboard;
|
|
198
|
+
if (r.event) msg.event = JSON.parse(r.event) as PanelMessageEvent;
|
|
126
199
|
|
|
127
200
|
return msg;
|
|
128
201
|
});
|