halbot 1995.1.52 → 1995.1.54

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "halbot",
3
3
  "description": "Just another AI powered Telegram bot, which is simple design, easy to use, extendable and fun.",
4
- "version": "1995.1.52",
4
+ "version": "1995.1.54",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/halbot",
7
7
  "type": "module",
@@ -34,6 +34,7 @@
34
34
  "@google-cloud/discoveryengine": "^2.5.2",
35
35
  "@google/genai": "^1.42.0",
36
36
  "@mozilla/readability": "^0.6.0",
37
+ "@resvg/resvg-js": "^2.6.2",
37
38
  "fluent-ffmpeg": "^2.1.3",
38
39
  "google-gax": "^5.0.6",
39
40
  "ioredis": "^5.9.3",
@@ -43,8 +44,10 @@
43
44
  "mysql2": "^3.17.3",
44
45
  "office-text-extractor": "^4.0.0",
45
46
  "openai": "^6.22.0",
47
+ "parse-numeric-range": "^1.3.0",
46
48
  "pg": "^8.18.0",
47
49
  "pgvector": "^0.2.1",
50
+ "satori": "^0.19.2",
48
51
  "telegraf": "^4.16.3",
49
52
  "tellegram": "^1.1.18",
50
53
  "tesseract.js": "^7.0.0",
@@ -0,0 +1,207 @@
1
+ import { dbio, hal } from '../index.mjs';
2
+ import satori from 'satori';
3
+ import { Resvg } from '@resvg/resvg-js';
4
+
5
+ // Pre-load font
6
+ let interFontBuffer;
7
+ let notoFontBuffer;
8
+
9
+ const getFonts = async () => {
10
+ if (!interFontBuffer) {
11
+ try {
12
+ const url = 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZJhjp-Ek-_EeA.woff';
13
+ const res = await fetch(url);
14
+ interFontBuffer = await res.arrayBuffer();
15
+ } catch (e) {
16
+ console.error('Failed to load Inter font:', e);
17
+ throw new Error('Inter Font loading failed');
18
+ }
19
+ }
20
+ if (!notoFontBuffer) {
21
+ try {
22
+ // Fetching a complete Noto Sans TC OTF from JSDelivr to ensure all CJK glyphs load in Satori
23
+ const url = 'https://cdn.jsdelivr.net/gh/googlefonts/noto-cjk@main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf';
24
+ const res = await fetch(url);
25
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
26
+ notoFontBuffer = await res.arrayBuffer();
27
+ } catch (e) {
28
+ console.error('Failed to load Noto font:', e);
29
+ throw new Error('Noto Font loading failed');
30
+ }
31
+ }
32
+ return { inter: interFontBuffer, noto: notoFontBuffer };
33
+ };
34
+
35
+ const process = async (ctx, next) => {
36
+ const token = ctx.params.token;
37
+
38
+ // 1. Fetch Chat Data
39
+ const result = await dbio.queryOne(
40
+ `SELECT * FROM ${hal.table} WHERE token = $1`,
41
+ [token]
42
+ );
43
+
44
+ if (!result) {
45
+ ctx.status = 404;
46
+ ctx.body = 'Not Found';
47
+ return;
48
+ }
49
+
50
+ result.received = JSON.parse(result.received);
51
+ const msg = result.received.message;
52
+ const userText = result.received_text || '';
53
+ const username = msg.from.username || msg.from.first_name || 'User';
54
+ const chatId = result.chat_id || 'Unknown';
55
+
56
+ // Allow CSS to handle text clamping, but give a safe upper limit to prevent memory overloads
57
+ let previewText = userText.replace(/<[^>]+>/g, '').trim();
58
+ if (previewText.length > 500) {
59
+ previewText = previewText.substring(0, 500) + '...';
60
+ }
61
+
62
+ // 2. Build Satori VDOM
63
+ const fonts = await getFonts();
64
+
65
+ const svg = await satori(
66
+ {
67
+ type: 'div',
68
+ props: {
69
+ style: {
70
+ height: '100%',
71
+ width: '100%',
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ alignItems: 'flex-start',
75
+ justifyContent: 'center',
76
+ backgroundColor: '#0a0a0c', // HAL9000 Dark bg
77
+ backgroundImage: 'radial-gradient(circle at 25px 25px, rgba(255, 255, 255, 0.05) 2%, transparent 0%), radial-gradient(circle at 75px 75px, rgba(255, 255, 255, 0.05) 2%, transparent 0%)',
78
+ backgroundSize: '100px 100px',
79
+ padding: '60px',
80
+ fontFamily: '"Inter"',
81
+ color: '#ffffff',
82
+ },
83
+ children: [
84
+ // Brand / Logo area
85
+ {
86
+ type: 'div',
87
+ props: {
88
+ style: {
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ marginBottom: '40px',
92
+ },
93
+ children: [
94
+ {
95
+ type: 'img',
96
+ props: {
97
+ src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3CradialGradient id='lens-glow' cx='50%25' cy='50%25' r='50%25' fx='50%25' fy='50%25'%3E%3Cstop offset='0%25' style='stop-color:%23ffeb3b;stop-opacity:1' /%3E%3Cstop offset='20%25' style='stop-color:%23ff9800;stop-opacity:1' /%3E%3Cstop offset='60%25' style='stop-color:%23f44336;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23b71c1c;stop-opacity:1' /%3E%3C/radialGradient%3E%3ClinearGradient id='metal-rim' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23e0e0e0;stop-opacity:1' /%3E%3Cstop offset='50%25' style='stop-color:%239e9e9e;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23616161;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='48' style='fill:url(%23metal-rim);stroke:%23333;stroke-width:2' /%3E%3Ccircle cx='50' cy='50' r='42' style='fill:%23111' /%3E%3Ccircle cx='50' cy='50' r='28' style='fill:url(%23lens-glow);stroke:%23800000;stroke-width:1' /%3E%3Ccircle cx='35' cy='35' r='5' style='fill:%23fff;opacity:0.6;' /%3E%3C/svg%3E",
98
+ style: {
99
+ width: '64px',
100
+ height: '64px',
101
+ marginRight: '20px'
102
+ }
103
+ }
104
+ },
105
+ {
106
+ type: 'div',
107
+ props: {
108
+ style: { fontSize: '48px', fontWeight: 'bold', color: '#00f0ff', letterSpacing: '2px' },
109
+ children: 'HAL9000'
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ },
115
+ // Chat ID Info
116
+ {
117
+ type: 'div',
118
+ props: {
119
+ style: { fontSize: '36px', color: '#888', marginBottom: '20px' },
120
+ children: `Chat ID: ${chatId}`
121
+ }
122
+ },
123
+ // Preview text
124
+ {
125
+ type: 'div',
126
+ props: {
127
+ style: {
128
+ display: '-webkit-box',
129
+ WebkitLineClamp: 4,
130
+ WebkitBoxOrient: 'vertical',
131
+ fontSize: '38px',
132
+ color: '#eee',
133
+ lineHeight: '1.5',
134
+ background: 'rgba(255,255,255,0.05)',
135
+ padding: '30px',
136
+ borderLeft: '8px solid #00f0ff',
137
+ maxWidth: '1080px',
138
+ wordBreak: 'break-all',
139
+ overflow: 'hidden',
140
+ textOverflow: 'ellipsis'
141
+ },
142
+ children: previewText
143
+ }
144
+ },
145
+ // Footer Username
146
+ {
147
+ type: 'div',
148
+ props: {
149
+ style: { marginTop: 'auto', fontSize: '28px', color: '#aaa', display: 'flex', alignItems: 'center' },
150
+ children: [
151
+ {
152
+ type: 'span',
153
+ props: { style: { color: '#00f0ff', marginRight: '10px' }, children: '●' }
154
+ },
155
+ `Prompt by @${username}`
156
+ ]
157
+ }
158
+ }
159
+ ]
160
+ }
161
+ },
162
+ {
163
+ width: 1200,
164
+ height: 630,
165
+ fonts: [
166
+ {
167
+ name: 'Inter',
168
+ data: fonts.inter,
169
+ weight: 400,
170
+ style: 'normal',
171
+ },
172
+ {
173
+ name: 'Noto Sans TC',
174
+ data: fonts.noto,
175
+ weight: 400,
176
+ style: 'normal',
177
+ },
178
+ ],
179
+ }
180
+ );
181
+
182
+ // 3. Render SVG to PNG using Resvg
183
+ const resvg = new Resvg(svg, {
184
+ background: '#0a0a0c',
185
+ fitTo: { mode: 'width', value: 1200 },
186
+ });
187
+
188
+ const pngData = resvg.render();
189
+ const pngBuffer = pngData.asPng();
190
+
191
+ // 4. Send response
192
+ ctx.set('Content-Type', 'image/png');
193
+ ctx.set('Cache-Control', 'public, max-age=86400'); // Cache for 1 day
194
+ ctx.body = pngBuffer;
195
+ };
196
+
197
+ export const { actions } = {
198
+ actions: [
199
+ {
200
+ path: 'og-image/:token',
201
+ method: 'GET',
202
+ process,
203
+ }
204
+ ]
205
+ };
206
+
207
+ export { process };
package/web/turn.html CHANGED
@@ -7,6 +7,7 @@
7
7
  <title>HAL9000 Chat</title>
8
8
  <link rel="icon" type="image/svg+xml"
9
9
  href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3CradialGradient id='a' cx='50%25' cy='50%25' r='50%25' fx='50%25' fy='50%25'%3E%3Cstop offset='0%25' stop-color='%23ffeb3b'/%3E%3Cstop offset='20%25' stop-color='%23ff9800'/%3E%3Cstop offset='60%25' stop-color='%23f44336'/%3E%3Cstop offset='100%25' stop-color='%23b71c1c'/%3E%3C/radialGradient%3E%3ClinearGradient id='b' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23e0e0e0'/%3E%3Cstop offset='50%25' stop-color='%239e9e9e'/%3E%3Cstop offset='100%25' stop-color='%23616161'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='48' fill='url(%23b)' stroke='%23333' stroke-width='2'/%3E%3Ccircle cx='50' cy='50' r='42' fill='%23111'/%3E%3Ccircle cx='50' cy='50' r='28' fill='url(%23a)' stroke='%23800000' stroke-width='1'/%3E%3Cellipse cx='60' cy='40' rx='10' ry='6' fill='rgba(255,255,255,0.4)' transform='rotate(-45 60 40)'/%3E%3C/svg%3E">
10
+ {{meta_tags}}
10
11
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
11
12
  <link rel="stylesheet"
12
13
  href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.6.1/github-markdown-light.min.css">
@@ -497,7 +498,7 @@
497
498
  // Set Chat ID from data
498
499
  if (chatData.chat_id) {
499
500
  chatId.textContent = `Chat ID: ${chatData.chat_id}`;
500
- document.title = `HAL 9000 Chat: ${chatData.chat_id}`;
501
+ document.title = `HAL9000 Chat: ${chatData.chat_id}`;
501
502
  } else {
502
503
  chatId.textContent = `Chat ID: UNKNOWN`;
503
504
  }
package/web/turn.mjs CHANGED
@@ -9,7 +9,8 @@ const safeStringify = (obj) => JSON.stringify(obj)
9
9
  .replace(/&/g, '\\u0026')
10
10
  .replace(/\u2028/g, '\\u2028')
11
11
  .replace(/\u2029/g, '\\u2029');
12
- const renderHtml = async (data) => await getHtml().then((html) => html.replace("'{{data}}'", () => data));
12
+ const renderHtml = async (data, metaTags = '') => await getHtml()
13
+ .then((html) => html.replace("'{{data}}'", () => data).replace('{{meta_tags}}', () => metaTags));
13
14
 
14
15
  const file = async (ctx) => {
15
16
  try {
@@ -106,10 +107,38 @@ const process = async (ctx, next) => {
106
107
  });
107
108
  }
108
109
  });
110
+ let previewText = messages.length ? messages[0].text : 'A conversation with HAL9000';
111
+ previewText = previewText.replace(/<[^>]+>/g, '').substring(0, 160).trim() + '...';
112
+
113
+ const ogImageUrl = `https://${ctx.host}/og-image/${ctx.params.token}`;
114
+
115
+ // Construct dynamic meta tags
116
+ const pageTitle = `Chat ID: ${result.chat_id}`;
117
+ const escapedDesc = previewText.replace(/"/g, '&quot;');
118
+ const escapedTitle = pageTitle.replace(/"/g, '&quot;');
119
+
120
+ const metaTags = `
121
+ <!-- Standard Meta -->
122
+ <meta name="description" content="${escapedDesc}">
123
+
124
+ <!-- Open Graph / Facebook / Meta -->
125
+ <meta property="og:type" content="website">
126
+ <meta property="og:title" content="${escapedTitle}">
127
+ <meta property="og:description" content="${escapedDesc}">
128
+ <meta property="og:site_name" content="HAL9000">
129
+ <meta property="og:image" content="${ogImageUrl}">
130
+
131
+ <!-- Twitter / Telegram -->
132
+ <meta name="twitter:card" content="summary_large_image">
133
+ <meta name="twitter:title" content="${escapedTitle}">
134
+ <meta name="twitter:description" content="${escapedDesc}">
135
+ <meta name="twitter:image" content="${ogImageUrl}">
136
+ `.trim();
137
+
109
138
  ctx.body = await renderHtml(safeStringify({
110
139
  bot_id: result.bot_id, chat_id: result.chat_id,
111
140
  chat_type: result.chat_type, messages, prompt_count,
112
- }));
141
+ }), metaTags);
113
142
  };
114
143
 
115
144
  export const { actions } = {