@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/lib/panel-html.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** the operator panel ui — a single static page
|
|
1
|
+
/** the operator panel ui — a single static page: token login, then the live chat view. */
|
|
2
2
|
export const PANEL_HTML = `<!doctype html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
@@ -6,37 +6,125 @@ export const PANEL_HTML = `<!doctype html>
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
7
|
<title>yaebal panel</title>
|
|
8
8
|
<style>
|
|
9
|
-
:root { color-scheme: light dark; --bg:#0f1115; --panel:#171a21; --line:#252a33; --muted:#8b93a1; --accent:#229ED9; --text:#e6e8eb; }
|
|
9
|
+
:root { color-scheme: light dark; --bg:#0f1115; --panel:#171a21; --line:#252a33; --muted:#8b93a1; --accent:#229ED9; --accent-2:#1b87ba; --text:#e6e8eb; --danger:#e5484d; }
|
|
10
10
|
* { box-sizing: border-box; }
|
|
11
|
-
body { margin:0; font:14px/1.5 system-ui,sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; }
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
body { margin:0; font:14px/1.5 system-ui,-apple-system,sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; }
|
|
12
|
+
|
|
13
|
+
/* ---- login ---- */
|
|
14
|
+
#login { margin:auto; width:300px; display:flex; flex-direction:column; gap:14px; padding:24px; }
|
|
15
|
+
#login .brand { text-align:center; font-size:20px; font-weight:600; letter-spacing:-.2px; }
|
|
16
|
+
#login .brand b { color:var(--accent); }
|
|
17
|
+
#login .sub { text-align:center; color:var(--muted); font-size:12px; margin-top:-8px; }
|
|
18
|
+
#login input, #login button { width:100%; height:44px; border-radius:10px; font:inherit; padding:0 14px; }
|
|
19
|
+
#login input { background:var(--panel); border:1px solid var(--line); color:var(--text); text-align:center; }
|
|
20
|
+
#login input:focus { outline:none; border-color:var(--accent); }
|
|
21
|
+
#login button { background:var(--accent); color:#fff; border:0; cursor:pointer; font-weight:600; transition:background .15s; }
|
|
22
|
+
#login button:hover { background:var(--accent-2); }
|
|
23
|
+
#login button:disabled { opacity:.6; cursor:default; }
|
|
24
|
+
#login .err { color:var(--danger); font-size:12px; text-align:center; min-height:16px; }
|
|
25
|
+
|
|
26
|
+
/* ---- app ---- */
|
|
27
|
+
#app { display:none; flex:1; }
|
|
28
|
+
body.authed #login { display:none; }
|
|
29
|
+
body.authed #app { display:flex; }
|
|
30
|
+
#chats { width:280px; border-right:1px solid var(--line); overflow-y:auto; flex:none; display:flex; flex-direction:column; }
|
|
31
|
+
#chats .top { display:flex; align-items:center; justify-content:space-between; padding:14px 16px; }
|
|
32
|
+
#chats .top h1 { font-size:13px; color:var(--muted); margin:0; letter-spacing:.5px; text-transform:lowercase; }
|
|
33
|
+
#chats .top button { background:none; border:0; color:var(--muted); cursor:pointer; font:inherit; font-size:12px; }
|
|
34
|
+
#chats .top button:hover { color:var(--text); }
|
|
35
|
+
.chat { padding:10px 16px; border-top:1px solid var(--line); cursor:pointer; }
|
|
15
36
|
.chat:hover, .chat.on { background:var(--panel); }
|
|
16
37
|
.chat .n { font-weight:500; }
|
|
17
38
|
.chat .l { color:var(--muted); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
18
39
|
#main { flex:1; display:flex; flex-direction:column; min-width:0; }
|
|
19
40
|
#log { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:8px; }
|
|
41
|
+
#log .more { align-self:center; background:none; border:1px solid var(--line); color:var(--muted); border-radius:999px; padding:4px 14px; cursor:pointer; font:inherit; font-size:12px; }
|
|
20
42
|
.msg { max-width:70%; padding:8px 12px; border-radius:12px; white-space:pre-wrap; word-break:break-word; }
|
|
21
43
|
.msg.in { background:var(--panel); align-self:flex-start; }
|
|
22
44
|
.msg.out { background:var(--accent); color:#fff; align-self:flex-end; }
|
|
45
|
+
.msg img, .msg video { max-width:280px; max-height:320px; border-radius:8px; display:block; }
|
|
46
|
+
.msg audio { width:260px; display:block; }
|
|
47
|
+
.msg .cap { margin-top:6px; }
|
|
48
|
+
.msg .media + .media { margin-top:6px; }
|
|
49
|
+
.msg .doc { display:inline-flex; align-items:center; gap:6px; color:inherit; text-decoration:none; border-bottom:1px dotted currentColor; }
|
|
50
|
+
.msg.album { display:flex; flex-wrap:wrap; gap:4px; max-width:300px; }
|
|
51
|
+
.msg.album img, .msg.album video { max-width:140px; max-height:140px; margin:0; }
|
|
52
|
+
.msg.album .cap { flex-basis:100%; }
|
|
23
53
|
#composer { display:flex; gap:8px; padding:12px; border-top:1px solid var(--line); }
|
|
24
|
-
#composer input { flex:1; background:var(--panel); border:1px solid var(--line); color:var(--text); border-radius:8px; padding:9px 12px; font:inherit; }
|
|
54
|
+
#composer input.text { flex:1; background:var(--panel); border:1px solid var(--line); color:var(--text); border-radius:8px; padding:9px 12px; font:inherit; }
|
|
25
55
|
#composer button { background:var(--accent); color:#fff; border:0; border-radius:8px; padding:0 16px; cursor:pointer; font:inherit; }
|
|
56
|
+
#composer .attach { background:var(--panel); border:1px solid var(--line); color:var(--muted); padding:0 12px; }
|
|
57
|
+
#composer .attach:hover { color:var(--text); }
|
|
26
58
|
#empty { margin:auto; color:var(--muted); }
|
|
27
59
|
</style>
|
|
28
60
|
</head>
|
|
29
61
|
<body>
|
|
30
|
-
<
|
|
31
|
-
<div
|
|
62
|
+
<form id="login">
|
|
63
|
+
<div class="brand"><b>@yaebal</b>/panel</div>
|
|
64
|
+
<div class="sub">operator panel</div>
|
|
65
|
+
<input id="token" type="password" placeholder="access token" autocomplete="off" autofocus />
|
|
66
|
+
<button id="go" type="submit">authorize</button>
|
|
67
|
+
<div class="err" id="err"></div>
|
|
68
|
+
</form>
|
|
69
|
+
|
|
70
|
+
<div id="app">
|
|
71
|
+
<div id="chats"><div class="top"><h1>chats</h1><button id="logout" type="button">log out</button></div></div>
|
|
72
|
+
<div id="main"><div id="empty">select a chat</div></div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
32
75
|
<script>
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
let
|
|
76
|
+
const BASE = "__BASE__";
|
|
77
|
+
const KEY = "yaebal-panel-token" + BASE;
|
|
78
|
+
let token = sessionStorage.getItem(KEY) || "";
|
|
79
|
+
let active = null, oldest = null, es = null;
|
|
80
|
+
|
|
36
81
|
const el = (t, c, x) => { const e = document.createElement(t); if (c) e.className = c; if (x != null) e.textContent = x; return e; };
|
|
82
|
+
const api = (p, opt = {}) => fetch(BASE + p, { ...opt, headers: { ...(opt.headers||{}), authorization: "Bearer " + token } });
|
|
83
|
+
|
|
84
|
+
/* ---- auth ---- */
|
|
85
|
+
const login = document.getElementById("login");
|
|
86
|
+
login.onsubmit = async (e) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const go = document.getElementById("go"), err = document.getElementById("err");
|
|
89
|
+
token = document.getElementById("token").value.trim();
|
|
90
|
+
if (!token) return;
|
|
91
|
+
go.disabled = true; err.textContent = "";
|
|
92
|
+
const res = await api("/api/chats").catch(() => null);
|
|
93
|
+
go.disabled = false;
|
|
94
|
+
if (!res || !res.ok) { err.textContent = "invalid token"; return; }
|
|
95
|
+
sessionStorage.setItem(KEY, token);
|
|
96
|
+
enter();
|
|
97
|
+
};
|
|
98
|
+
document.getElementById("logout").onclick = () => {
|
|
99
|
+
sessionStorage.removeItem(KEY); token = ""; active = null;
|
|
100
|
+
if (es) { es.close(); es = null; }
|
|
101
|
+
document.body.classList.remove("authed");
|
|
102
|
+
document.getElementById("token").value = "";
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function enter() {
|
|
106
|
+
document.body.classList.add("authed");
|
|
107
|
+
loadChats();
|
|
108
|
+
openStream();
|
|
109
|
+
}
|
|
37
110
|
|
|
111
|
+
/* ---- realtime: instant via SSE, with a slow polling safety net ---- */
|
|
112
|
+
function openStream() {
|
|
113
|
+
if (es || !window.EventSource) return;
|
|
114
|
+
es = new EventSource(BASE + "/api/stream?token=" + encodeURIComponent(token));
|
|
115
|
+
es.addEventListener("record", (ev) => {
|
|
116
|
+
const e = JSON.parse(ev.data);
|
|
117
|
+
if (active && e.chatId === active) openChat(active, true);
|
|
118
|
+
loadChats();
|
|
119
|
+
});
|
|
120
|
+
es.onerror = () => {}; // EventSource auto-reconnects
|
|
121
|
+
}
|
|
122
|
+
setInterval(() => { if (token) (active ? openChat(active, true) : loadChats()); }, 8000);
|
|
123
|
+
|
|
124
|
+
/* ---- chats ---- */
|
|
38
125
|
async function loadChats() {
|
|
39
|
-
const
|
|
126
|
+
const res = await api("/api/chats"); if (!res.ok) return;
|
|
127
|
+
const chats = await res.json();
|
|
40
128
|
const box = document.getElementById("chats");
|
|
41
129
|
box.querySelectorAll(".chat").forEach(n => n.remove());
|
|
42
130
|
for (const c of chats) {
|
|
@@ -46,18 +134,79 @@ async function loadChats() {
|
|
|
46
134
|
box.append(d);
|
|
47
135
|
}
|
|
48
136
|
}
|
|
49
|
-
|
|
137
|
+
|
|
138
|
+
/* ---- media rendering ---- */
|
|
139
|
+
const fileSrc = (att) => BASE + "/api/file?id=" + encodeURIComponent(att.fileId) + "&token=" + encodeURIComponent(token);
|
|
140
|
+
const isPlaceholder = (t) => /^\[[a-z_]+\]$/.test(t);
|
|
141
|
+
|
|
142
|
+
function attEl(att) {
|
|
143
|
+
const src = fileSrc(att);
|
|
144
|
+
if (att.type === "photo" || att.type === "sticker") { const i = el("img", "media"); i.src = src; i.loading = "lazy"; return i; }
|
|
145
|
+
if (att.type === "video" || att.type === "animation" || att.type === "video_note") { const v = el("video", "media"); v.src = src; v.controls = true; return v; }
|
|
146
|
+
if (att.type === "voice" || att.type === "audio") { const a = el("audio", "media"); a.src = src; a.controls = true; return a; }
|
|
147
|
+
const link = el("a", "media doc", "📎 " + (att.fileName || att.type)); link.href = src; link.target = "_blank"; return link;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function bubble(m) {
|
|
151
|
+
const b = el("div", "msg " + m.direction);
|
|
152
|
+
const atts = m.attachments || [];
|
|
153
|
+
for (const a of atts) b.append(attEl(a));
|
|
154
|
+
if (m.text && !(atts.length && isPlaceholder(m.text))) b.append(el("div", atts.length ? "cap" : null, m.text));
|
|
155
|
+
return b;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* merge consecutive messages sharing a media_group_id into one album bubble */
|
|
159
|
+
function renderMsgs(msgs) {
|
|
160
|
+
const out = [];
|
|
161
|
+
let group = null, groupId = null;
|
|
162
|
+
for (const m of msgs) {
|
|
163
|
+
if (m.mediaGroupId && m.mediaGroupId === groupId && group) {
|
|
164
|
+
for (const a of m.attachments || []) group.insertBefore(attEl(a), group.querySelector(".cap"));
|
|
165
|
+
if (m.text && !isPlaceholder(m.text)) group.append(el("div", "cap", m.text));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const b = bubble(m);
|
|
169
|
+
if (m.mediaGroupId) { b.classList.add("album"); group = b; groupId = m.mediaGroupId; }
|
|
170
|
+
else { group = null; groupId = null; }
|
|
171
|
+
out.push(b);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function openChat(id, keepScroll) {
|
|
50
177
|
active = id;
|
|
51
|
-
|
|
52
|
-
const
|
|
178
|
+
if (!keepScroll) loadChats();
|
|
179
|
+
const res = await api("/api/chats/" + id + "?limit=200"); if (!res.ok) return;
|
|
180
|
+
const msgs = await res.json();
|
|
181
|
+
oldest = msgs.length ? msgs[0].date : null;
|
|
182
|
+
|
|
53
183
|
const main = document.getElementById("main");
|
|
184
|
+
const prevTop = keepScroll ? (main.querySelector("#log")?.scrollTop ?? null) : null;
|
|
54
185
|
main.innerHTML = "";
|
|
186
|
+
|
|
55
187
|
const log = el("div"); log.id = "log";
|
|
56
|
-
|
|
188
|
+
if (msgs.length >= 200) {
|
|
189
|
+
const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log);
|
|
190
|
+
log.append(more);
|
|
191
|
+
}
|
|
192
|
+
for (const b of renderMsgs(msgs)) log.append(b);
|
|
193
|
+
|
|
57
194
|
const form = el("form"); form.id = "composer";
|
|
58
|
-
const
|
|
195
|
+
const fileInput = el("input"); fileInput.type = "file"; fileInput.style.display = "none";
|
|
196
|
+
const attach = el("button", "attach", "📎"); attach.type = "button"; attach.title = "send a file";
|
|
197
|
+
attach.onclick = () => fileInput.click();
|
|
198
|
+
const input = el("input", "text"); input.placeholder = "reply…"; input.autocomplete = "off";
|
|
59
199
|
const btn = el("button", null, "send"); btn.type = "submit";
|
|
60
|
-
form.append(input, btn);
|
|
200
|
+
form.append(attach, fileInput, input, btn);
|
|
201
|
+
|
|
202
|
+
fileInput.onchange = async () => {
|
|
203
|
+
const file = fileInput.files && fileInput.files[0]; if (!file) return;
|
|
204
|
+
const fd = new FormData(); fd.append("file", file);
|
|
205
|
+
if (input.value.trim()) fd.append("caption", input.value.trim());
|
|
206
|
+
input.value = ""; fileInput.value = "";
|
|
207
|
+
await api("/api/chats/" + id + "/send", { method: "POST", body: fd }); // browser sets multipart boundary
|
|
208
|
+
openChat(id);
|
|
209
|
+
};
|
|
61
210
|
form.onsubmit = async (e) => {
|
|
62
211
|
e.preventDefault();
|
|
63
212
|
const text = input.value.trim(); if (!text) return;
|
|
@@ -66,10 +215,24 @@ async function openChat(id) {
|
|
|
66
215
|
openChat(id);
|
|
67
216
|
};
|
|
68
217
|
main.append(log, form);
|
|
69
|
-
log.scrollTop = log.scrollHeight;
|
|
218
|
+
log.scrollTop = prevTop != null ? prevTop : log.scrollHeight;
|
|
70
219
|
}
|
|
71
|
-
|
|
72
|
-
|
|
220
|
+
|
|
221
|
+
async function loadEarlier(id, log) {
|
|
222
|
+
if (oldest == null) return;
|
|
223
|
+
const res = await api("/api/chats/" + id + "?limit=200&before=" + oldest); if (!res.ok) return;
|
|
224
|
+
const older = await res.json();
|
|
225
|
+
if (!older.length) { log.querySelector(".more")?.remove(); return; }
|
|
226
|
+
oldest = older[0].date;
|
|
227
|
+
const anchor = log.querySelector(".more")?.nextSibling ?? log.firstChild;
|
|
228
|
+
const frag = document.createDocumentFragment();
|
|
229
|
+
if (older.length >= 200) { const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log); frag.append(more); }
|
|
230
|
+
for (const b of renderMsgs(older)) frag.append(b);
|
|
231
|
+
log.querySelector(".more")?.remove();
|
|
232
|
+
log.insertBefore(frag, anchor);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (token) enter();
|
|
73
236
|
</script>
|
|
74
237
|
</body>
|
|
75
238
|
</html>`;
|
package/lib/panel-html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel-html.js","sourceRoot":"","sources":["../src/panel-html.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"panel-html.js","sourceRoot":"","sources":["../src/panel-html.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA4OlB,CAAC"}
|
package/lib/serve.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
/** options for the node {@link serve} helper. */
|
|
3
|
+
export interface ServeOptions {
|
|
4
|
+
/** port to listen on. */
|
|
5
|
+
port: number;
|
|
6
|
+
/** host/interface to bind. defaults to node's default (all interfaces). */
|
|
7
|
+
host?: string;
|
|
8
|
+
/** invoked once the server is listening. */
|
|
9
|
+
onListen?: (info: {
|
|
10
|
+
port: number;
|
|
11
|
+
host?: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* start a native node `http` server for a fetch-style handler (e.g. {@link panelHandler}).
|
|
16
|
+
* zero third-party deps — just `node:http`. on bun/deno use their built-in `serve` instead.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { panelHandler } from "@yaebal/panel";
|
|
20
|
+
* import { serve } from "@yaebal/panel/serve";
|
|
21
|
+
* serve(panelHandler(bot.api, store, { token }), { port: 8080 });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function serve(handler: (request: Request) => Promise<Response> | Response, options: ServeOptions): Server;
|
|
25
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,MAAM,EAAqC,MAAM,WAAW,CAAC;AAEjG,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC5B,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC3D;AA0BD;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CACpB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC3D,OAAO,EAAE,YAAY,GACnB,MAAM,CAeR"}
|
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
|
package/lib/serve.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0D,YAAY,EAAE,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"}
|
package/lib/sqlite.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { HistoryOptions, PanelChat, 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: {
|
|
23
|
+
id: number;
|
|
24
|
+
name?: string;
|
|
25
|
+
}, message: PanelMessage): void;
|
|
26
|
+
chats(): PanelChat[];
|
|
27
|
+
history(chatId: number, options?: HistoryOptions): PanelMessage[];
|
|
28
|
+
subscribe(listener: (event: PanelEvent) => void): () => void;
|
|
29
|
+
/** close the underlying database (no-op if you passed your own `db`). */
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
//# 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,UAAU,EACV,YAAY,EACZ,UAAU,EACV,MAAM,YAAY,CAAC;AAEpB,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;IAsBjD,MAAM,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAmCxE,KAAK,IAAI,SAAS,EAAE;IAapB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,YAAY,EAAE;IA6BjE,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,97 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
/**
|
|
3
|
+
* a persistent {@link PanelStore} backed by node's built-in `node:sqlite` (node 22.5+).
|
|
4
|
+
* zero third-party deps. import from `@yaebal/panel/sqlite`.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { SqlitePanelStore } from "@yaebal/panel/sqlite";
|
|
8
|
+
* const store = new SqlitePanelStore({ path: "./panel.db" });
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export class SqlitePanelStore {
|
|
12
|
+
#db;
|
|
13
|
+
#listeners = new Set();
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.#db = options.db ?? new DatabaseSync(options.path ?? ":memory:");
|
|
16
|
+
this.#db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS panel_chats (
|
|
18
|
+
id INTEGER PRIMARY KEY,
|
|
19
|
+
name TEXT,
|
|
20
|
+
last_text TEXT NOT NULL,
|
|
21
|
+
last_date INTEGER NOT NULL
|
|
22
|
+
);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS panel_messages (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
chat_id INTEGER NOT NULL,
|
|
26
|
+
direction TEXT NOT NULL,
|
|
27
|
+
text TEXT NOT NULL,
|
|
28
|
+
date INTEGER NOT NULL,
|
|
29
|
+
attachments TEXT,
|
|
30
|
+
media_group TEXT
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS panel_messages_chat ON panel_messages (chat_id, date);
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
record(chat, message) {
|
|
36
|
+
this.#db
|
|
37
|
+
.prepare("INSERT INTO panel_messages (chat_id, direction, text, date, attachments, media_group) VALUES (?, ?, ?, ?, ?, ?)")
|
|
38
|
+
.run(chat.id, message.direction, message.text, message.date, message.attachments ? JSON.stringify(message.attachments) : null, message.mediaGroupId ?? null);
|
|
39
|
+
// keep the name when an outgoing message omits it; COALESCE the existing row's value
|
|
40
|
+
this.#db
|
|
41
|
+
.prepare(`
|
|
42
|
+
INSERT INTO panel_chats (id, name, last_text, last_date) VALUES (:id, :name, :text, :date)
|
|
43
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
44
|
+
name = COALESCE(:name, panel_chats.name),
|
|
45
|
+
last_text = :text,
|
|
46
|
+
last_date = :date
|
|
47
|
+
`)
|
|
48
|
+
.run({ id: chat.id, name: chat.name ?? null, text: message.text, date: message.date });
|
|
49
|
+
// brand-new chat with no name → give it a stable fallback label
|
|
50
|
+
this.#db
|
|
51
|
+
.prepare("UPDATE panel_chats SET name = ? WHERE id = ? AND name IS NULL")
|
|
52
|
+
.run(`chat ${chat.id}`, chat.id);
|
|
53
|
+
for (const fn of this.#listeners) {
|
|
54
|
+
fn({ type: "record", chatId: chat.id, direction: message.direction });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
chats() {
|
|
58
|
+
const rows = this.#db
|
|
59
|
+
.prepare("SELECT id, name, last_text, last_date FROM panel_chats ORDER BY last_date DESC")
|
|
60
|
+
.all();
|
|
61
|
+
return rows.map((r) => ({
|
|
62
|
+
id: r.id,
|
|
63
|
+
name: r.name ?? `chat ${r.id}`,
|
|
64
|
+
lastText: r.last_text,
|
|
65
|
+
lastDate: r.last_date,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
history(chatId, options) {
|
|
69
|
+
const before = options?.before ?? Number.MAX_SAFE_INTEGER;
|
|
70
|
+
const limit = options?.limit ?? -1; // sqlite: negative LIMIT = no limit
|
|
71
|
+
// grab the most recent `limit` rows older than `before`, then return ascending
|
|
72
|
+
const rows = this.#db
|
|
73
|
+
.prepare(`
|
|
74
|
+
SELECT direction, text, date, attachments, media_group FROM panel_messages
|
|
75
|
+
WHERE chat_id = ? AND date < ?
|
|
76
|
+
ORDER BY date DESC, id DESC LIMIT ?
|
|
77
|
+
`)
|
|
78
|
+
.all(chatId, before, limit);
|
|
79
|
+
return rows.reverse().map((r) => {
|
|
80
|
+
const msg = { direction: r.direction, text: r.text, date: r.date };
|
|
81
|
+
if (r.attachments)
|
|
82
|
+
msg.attachments = JSON.parse(r.attachments);
|
|
83
|
+
if (r.media_group)
|
|
84
|
+
msg.mediaGroupId = r.media_group;
|
|
85
|
+
return msg;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
subscribe(listener) {
|
|
89
|
+
this.#listeners.add(listener);
|
|
90
|
+
return () => this.#listeners.delete(listener);
|
|
91
|
+
}
|
|
92
|
+
/** close the underlying database (no-op if you passed your own `db`). */
|
|
93
|
+
close() {
|
|
94
|
+
this.#db.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# 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 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.test.d.ts","sourceRoot":"","sources":["../src/sqlite.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { SqlitePanelStore } from "./sqlite.js";
|
|
4
|
+
test("SqlitePanelStore: records, lists newest-first, paginates and emits events", () => {
|
|
5
|
+
const store = new SqlitePanelStore(); // :memory:
|
|
6
|
+
const events = [];
|
|
7
|
+
store.subscribe((e) => events.push(e.chatId));
|
|
8
|
+
store.record({ id: 1, name: "@sam" }, { direction: "in", text: "hi", date: 10 });
|
|
9
|
+
store.record({ id: 2, name: "@lee" }, { direction: "in", text: "yo", date: 20 });
|
|
10
|
+
for (let i = 1; i <= 5; i++) {
|
|
11
|
+
store.record({ id: 1 }, { direction: "in", text: `m${i}`, date: 100 + i });
|
|
12
|
+
}
|
|
13
|
+
// chats sorted by last_date desc; name preserved across nameless records
|
|
14
|
+
const chats = store.chats();
|
|
15
|
+
assert.equal(chats[0]?.id, 1);
|
|
16
|
+
assert.equal(chats[0]?.name, "@sam");
|
|
17
|
+
assert.equal(chats[0]?.lastText, "m5");
|
|
18
|
+
// pagination
|
|
19
|
+
assert.deepEqual(store.history(1, { limit: 2 }).map((m) => m.text), ["m4", "m5"]);
|
|
20
|
+
assert.deepEqual(store.history(1, { before: 103, limit: 2 }).map((m) => m.text), ["m1", "m2"]);
|
|
21
|
+
assert.ok(events.length >= 2);
|
|
22
|
+
store.close();
|
|
23
|
+
});
|
|
24
|
+
test("SqlitePanelStore persists attachments and media_group_id round-trip", () => {
|
|
25
|
+
const store = new SqlitePanelStore();
|
|
26
|
+
store.record({ id: 1, name: "@u" }, {
|
|
27
|
+
direction: "in",
|
|
28
|
+
text: "[photo]",
|
|
29
|
+
date: 1,
|
|
30
|
+
attachments: [{ type: "photo", fileId: "f1" }],
|
|
31
|
+
mediaGroupId: "G1",
|
|
32
|
+
});
|
|
33
|
+
store.record({ id: 1, name: "@u" }, { direction: "out", text: "thanks", date: 2 });
|
|
34
|
+
const hist = store.history(1);
|
|
35
|
+
assert.deepEqual(hist[0]?.attachments, [{ type: "photo", fileId: "f1" }]);
|
|
36
|
+
assert.equal(hist[0]?.mediaGroupId, "G1");
|
|
37
|
+
// plain text message has no attachments/group keys
|
|
38
|
+
assert.equal(hist[1]?.attachments, undefined);
|
|
39
|
+
assert.equal(hist[1]?.mediaGroupId, undefined);
|
|
40
|
+
store.close();
|
|
41
|
+
});
|
|
42
|
+
//# 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.
|
|
3
|
+
"version": "0.0.2",
|
|
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": [
|