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 +4 -1
- package/web/ogimage.mjs +207 -0
- package/web/turn.html +2 -1
- package/web/turn.mjs +31 -2
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.
|
|
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",
|
package/web/ogimage.mjs
ADDED
|
@@ -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 = `
|
|
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
|
|
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, '"');
|
|
118
|
+
const escapedTitle = pageTitle.replace(/"/g, '"');
|
|
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 } = {
|