@adriangalilea/utils 0.12.0 → 0.14.0
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 +28 -8
- package/dist/bot/access-control.d.ts +1 -1
- package/dist/bot/access-control.js +1 -1
- package/dist/bot/index.d.ts +2 -3
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +2 -3
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/llm.d.ts +520 -0
- package/dist/bot/llm.d.ts.map +1 -0
- package/dist/bot/llm.js +589 -0
- package/dist/bot/llm.js.map +1 -0
- package/dist/bot/menu.d.ts +99 -6
- package/dist/bot/menu.d.ts.map +1 -1
- package/dist/bot/menu.js +54 -6
- package/dist/bot/menu.js.map +1 -1
- package/package.json +4 -8
- package/dist/bot/llm-stream.d.ts +0 -142
- package/dist/bot/llm-stream.d.ts.map +0 -1
- package/dist/bot/llm-stream.js +0 -285
- package/dist/bot/llm-stream.js.map +0 -1
- package/dist/bot/message-history.d.ts +0 -279
- package/dist/bot/message-history.d.ts.map +0 -1
- package/dist/bot/message-history.js +0 -142
- package/dist/bot/message-history.js.map +0 -1
package/dist/bot/llm-stream.js
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM streaming for GramIO bots — both halves of the pipeline:
|
|
3
|
-
*
|
|
4
|
-
* `streamChat(response)` — INPUT: parse OpenAI-compatible SSE
|
|
5
|
-
* (OpenAI, vllm, mlx-lm, llama.cpp, Together,
|
|
6
|
-
* Groq, …) into a typed `AsyncGenerator` of
|
|
7
|
-
* `{ type: 'content' | 'reasoning', text }`.
|
|
8
|
-
*
|
|
9
|
-
* `ctx.startStream()` — OUTPUT: debounced `editMessageText` to
|
|
10
|
-
* Telegram. Local Markdown parse via
|
|
11
|
-
* `@gramio/format` so malformed mid-stream
|
|
12
|
-
* markup degrades to plain text instead of
|
|
13
|
-
* failing. Splits at 4000 chars on
|
|
14
|
-
* paragraph / line / word boundary.
|
|
15
|
-
*
|
|
16
|
-
* The two compose into the canonical bot pipeline:
|
|
17
|
-
*
|
|
18
|
-
* fetch(...) ──streamChat──> {content, reasoning} chunks ──ctx.startStream──> Telegram
|
|
19
|
-
*
|
|
20
|
-
* Peer deps: `gramio`, `@gramio/format`, `marked`.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* import { Bot } from 'gramio'
|
|
24
|
-
* import { llmStream, streamChat } from '@adriangalilea/utils/bot/llm-stream'
|
|
25
|
-
*
|
|
26
|
-
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
27
|
-
* .extend(llmStream())
|
|
28
|
-
* .on('message', async (ctx) => {
|
|
29
|
-
* const response = await fetch(process.env.LLM_URL!, {
|
|
30
|
-
* method: 'POST',
|
|
31
|
-
* headers: { 'Content-Type': 'application/json' },
|
|
32
|
-
* body: JSON.stringify({
|
|
33
|
-
* model: process.env.LLM_MODEL,
|
|
34
|
-
* messages: [{ role: 'user', content: ctx.text ?? '' }],
|
|
35
|
-
* stream: true,
|
|
36
|
-
* }),
|
|
37
|
-
* })
|
|
38
|
-
* const stream = ctx.startStream()
|
|
39
|
-
* for await (const chunk of streamChat(response)) {
|
|
40
|
-
* if (chunk.type === 'content') await stream.append(chunk.text)
|
|
41
|
-
* // chunk.type === 'reasoning' is also yielded for thinking models
|
|
42
|
-
* }
|
|
43
|
-
* await stream.end()
|
|
44
|
-
* })
|
|
45
|
-
*
|
|
46
|
-
* bot.start()
|
|
47
|
-
*/
|
|
48
|
-
import { Plugin } from 'gramio';
|
|
49
|
-
import { markdownToFormattable } from '@gramio/format/markdown';
|
|
50
|
-
/**
|
|
51
|
-
* Parse an OpenAI-compatible chat-completions SSE response into a
|
|
52
|
-
* typed `AsyncGenerator<LLMChunk>`. Reads `response.body` once.
|
|
53
|
-
*
|
|
54
|
-
* Recognised reasoning aliases (as of 2026): `reasoning_content`
|
|
55
|
-
* (vllm, qwen3, DeepSeek-R1, gpt-oss harmony) and `reasoning` (some
|
|
56
|
-
* mlx-lm forks, gemma builds). If a model surfaces a new key, add it
|
|
57
|
-
* here — single source of truth for the field.
|
|
58
|
-
*
|
|
59
|
-
* Constrained-SSE assumption: lines are `\n`-delimited and each event
|
|
60
|
-
* is `data: <json>` or `data: [DONE]`. This matches every OpenAI-compat
|
|
61
|
-
* server in the wild but is NOT the full SSE spec (no comments, no
|
|
62
|
-
* multi-line `data:`, no `retry`/`id` fields). Swap to
|
|
63
|
-
* `eventsource-parser` if you hit a producer that needs them.
|
|
64
|
-
*
|
|
65
|
-
* Malformed JSON lines are silently skipped; the generator ends when
|
|
66
|
-
* the stream closes.
|
|
67
|
-
*
|
|
68
|
-
* @param response the `fetch` `Response` from a `stream: true` chat
|
|
69
|
-
* completion call. Must not be already consumed.
|
|
70
|
-
* @throws if `response.body` is null (non-streaming response).
|
|
71
|
-
*
|
|
72
|
-
* @example framework-agnostic — parser doesn't know about Telegram
|
|
73
|
-
* const res = await fetch(url, { method: 'POST', body })
|
|
74
|
-
* for await (const chunk of streamChat(res)) {
|
|
75
|
-
* if (chunk.type === 'content') process.stdout.write(chunk.text)
|
|
76
|
-
* }
|
|
77
|
-
*/
|
|
78
|
-
export async function* streamChat(response) {
|
|
79
|
-
if (!response.body)
|
|
80
|
-
throw new Error('streamChat: response.body is null');
|
|
81
|
-
const reader = response.body.getReader();
|
|
82
|
-
const decoder = new TextDecoder();
|
|
83
|
-
let buffer = '';
|
|
84
|
-
while (true) {
|
|
85
|
-
const { done, value } = await reader.read();
|
|
86
|
-
if (done)
|
|
87
|
-
break;
|
|
88
|
-
buffer += decoder.decode(value, { stream: true });
|
|
89
|
-
const lines = buffer.split('\n');
|
|
90
|
-
buffer = lines.pop() ?? '';
|
|
91
|
-
for (const line of lines) {
|
|
92
|
-
if (!line.startsWith('data: '))
|
|
93
|
-
continue;
|
|
94
|
-
const data = line.slice(6).trim();
|
|
95
|
-
if (data === '[DONE]')
|
|
96
|
-
continue;
|
|
97
|
-
let parsed;
|
|
98
|
-
try {
|
|
99
|
-
parsed = JSON.parse(data);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const delta = parsed.choices?.[0]?.delta;
|
|
105
|
-
if (!delta)
|
|
106
|
-
continue;
|
|
107
|
-
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
108
|
-
if (typeof reasoning === 'string' && reasoning.length > 0) {
|
|
109
|
-
yield { type: 'reasoning', text: reasoning };
|
|
110
|
-
}
|
|
111
|
-
if (typeof delta.content === 'string' && delta.content.length > 0) {
|
|
112
|
-
yield { type: 'content', text: delta.content };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// ─── OUTPUT: Telegram streamer ─────────────────────────────────────
|
|
118
|
-
const MAX_LEN = 4000; // Telegram caps at 4096; leave headroom for entity offsets
|
|
119
|
-
const DEFAULT_DEBOUNCE_MS = 800;
|
|
120
|
-
export class MarkdownStreamer {
|
|
121
|
-
buffer = '';
|
|
122
|
-
currentMessageId;
|
|
123
|
-
firstSendPromise;
|
|
124
|
-
debounceTimer;
|
|
125
|
-
inFlight = false;
|
|
126
|
-
dirty = false;
|
|
127
|
-
ended = false;
|
|
128
|
-
chatId;
|
|
129
|
-
threadId;
|
|
130
|
-
// Match gramio's `bot.api` shape structurally. `text` accepts string or any
|
|
131
|
-
// `Formattable` (from `@gramio/format`) — both stringify safely. We don't
|
|
132
|
-
// import gramio's full Bot type to keep the streamer testable in isolation.
|
|
133
|
-
bot;
|
|
134
|
-
opts;
|
|
135
|
-
constructor(ctx, opts) {
|
|
136
|
-
this.chatId = ctx.chat.id;
|
|
137
|
-
// Captured so the streamed reply stays in the same thread.
|
|
138
|
-
// We call bot.api.sendMessage directly (no ctx.send), so the
|
|
139
|
-
// SendMixin's auto-thread doesn't help us here.
|
|
140
|
-
this.threadId = ctx.threadId;
|
|
141
|
-
this.bot = ctx.bot;
|
|
142
|
-
this.opts = {
|
|
143
|
-
debounceMs: opts.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
144
|
-
placeholder: opts.placeholder ?? '…',
|
|
145
|
-
markdown: opts.markdown ?? true,
|
|
146
|
-
onError: opts.onError ?? ((e) => console.error('[llm-stream]', e)),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
/** Append a chunk. Schedules a debounced edit. */
|
|
150
|
-
async append(text) {
|
|
151
|
-
if (this.ended)
|
|
152
|
-
throw new Error('stream already ended');
|
|
153
|
-
if (!text)
|
|
154
|
-
return;
|
|
155
|
-
// first chunk: send the placeholder so we have a message_id to edit.
|
|
156
|
-
// Serialized via firstSendPromise so concurrent appends don't double-send.
|
|
157
|
-
if (this.currentMessageId === undefined && !this.firstSendPromise) {
|
|
158
|
-
this.firstSendPromise = (async () => {
|
|
159
|
-
const sent = await this.bot.api.sendMessage({
|
|
160
|
-
chat_id: this.chatId,
|
|
161
|
-
...(this.threadId !== undefined && { message_thread_id: this.threadId }),
|
|
162
|
-
text: this.opts.placeholder,
|
|
163
|
-
});
|
|
164
|
-
this.currentMessageId = sent.message_id;
|
|
165
|
-
})();
|
|
166
|
-
}
|
|
167
|
-
if (this.firstSendPromise)
|
|
168
|
-
await this.firstSendPromise;
|
|
169
|
-
// overflow: freeze current message at last good split, start a new one.
|
|
170
|
-
const next = this.buffer + text;
|
|
171
|
-
if (next.length > MAX_LEN) {
|
|
172
|
-
const splitAt = findSplit(next, MAX_LEN);
|
|
173
|
-
const head = next.slice(0, splitAt);
|
|
174
|
-
const tail = next.slice(splitAt).trimStart();
|
|
175
|
-
this.buffer = head;
|
|
176
|
-
this.dirty = true;
|
|
177
|
-
await this.flushNow(); // commits head into current message
|
|
178
|
-
this.buffer = '';
|
|
179
|
-
this.currentMessageId = undefined;
|
|
180
|
-
this.firstSendPromise = undefined;
|
|
181
|
-
this.dirty = false;
|
|
182
|
-
if (this.debounceTimer) {
|
|
183
|
-
clearTimeout(this.debounceTimer);
|
|
184
|
-
this.debounceTimer = undefined;
|
|
185
|
-
}
|
|
186
|
-
if (tail)
|
|
187
|
-
await this.append(tail);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
this.buffer = next;
|
|
191
|
-
this.dirty = true;
|
|
192
|
-
this.scheduleFlush();
|
|
193
|
-
}
|
|
194
|
-
/** Flush any pending edit and close the stream. Idempotent. */
|
|
195
|
-
async end() {
|
|
196
|
-
if (this.ended)
|
|
197
|
-
return;
|
|
198
|
-
this.ended = true;
|
|
199
|
-
if (this.debounceTimer) {
|
|
200
|
-
clearTimeout(this.debounceTimer);
|
|
201
|
-
this.debounceTimer = undefined;
|
|
202
|
-
}
|
|
203
|
-
while (this.inFlight)
|
|
204
|
-
await sleep(50);
|
|
205
|
-
if (this.dirty)
|
|
206
|
-
await this.flushNow();
|
|
207
|
-
}
|
|
208
|
-
scheduleFlush() {
|
|
209
|
-
if (this.debounceTimer)
|
|
210
|
-
return;
|
|
211
|
-
this.debounceTimer = setTimeout(async () => {
|
|
212
|
-
this.debounceTimer = undefined;
|
|
213
|
-
if (this.inFlight) {
|
|
214
|
-
this.scheduleFlush();
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
await this.flushNow();
|
|
218
|
-
if (this.dirty && !this.ended)
|
|
219
|
-
this.scheduleFlush();
|
|
220
|
-
}, this.opts.debounceMs);
|
|
221
|
-
}
|
|
222
|
-
async flushNow() {
|
|
223
|
-
if (!this.dirty)
|
|
224
|
-
return;
|
|
225
|
-
if (this.currentMessageId === undefined) {
|
|
226
|
-
// No active message yet — likely the first send is still in flight
|
|
227
|
-
// or the recovery path cleared it. Caller's next append() will
|
|
228
|
-
// re-create one.
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
this.inFlight = true;
|
|
232
|
-
this.dirty = false;
|
|
233
|
-
const snapshot = this.buffer;
|
|
234
|
-
try {
|
|
235
|
-
const payload = this.opts.markdown ? markdownToFormattable(snapshot) : snapshot;
|
|
236
|
-
await this.bot.api.editMessageText({
|
|
237
|
-
chat_id: this.chatId,
|
|
238
|
-
message_id: this.currentMessageId,
|
|
239
|
-
text: payload,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
catch (e) {
|
|
243
|
-
const msg = String(e?.message ?? e);
|
|
244
|
-
if (msg.includes('message is not modified')) {
|
|
245
|
-
// identical content — fine
|
|
246
|
-
}
|
|
247
|
-
else if (msg.includes('message to edit not found')) {
|
|
248
|
-
// message gone (deleted by user, etc.) — restart with a fresh send
|
|
249
|
-
this.currentMessageId = undefined;
|
|
250
|
-
this.firstSendPromise = undefined;
|
|
251
|
-
this.dirty = true;
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
this.dirty = true;
|
|
255
|
-
this.opts.onError(e);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
finally {
|
|
259
|
-
this.inFlight = false;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* GramIO plugin. Adds `ctx.startStream(opts?)` on every message context.
|
|
265
|
-
*
|
|
266
|
-
* Defaults set here apply to every stream; per-call options in
|
|
267
|
-
* `ctx.startStream({...})` override them.
|
|
268
|
-
*/
|
|
269
|
-
export const llmStream = (defaults = {}) => new Plugin('@adriangalilea/utils/bot/llm-stream').derive('message', (ctx) => ({
|
|
270
|
-
// gramio's `message` scope guarantees `ctx.chat`. `ctx.bot.api` is on
|
|
271
|
-
// every Context. Structural compat → no cast needed.
|
|
272
|
-
startStream: (opts = {}) => new MarkdownStreamer(ctx, { ...defaults, ...opts }),
|
|
273
|
-
}));
|
|
274
|
-
function findSplit(text, maxLen) {
|
|
275
|
-
// Prefer paragraph break, then line, then space. Reject splits in the
|
|
276
|
-
// first half — better to truncate at maxLen than to leave a stub.
|
|
277
|
-
for (const sep of ['\n\n', '\n', ' ']) {
|
|
278
|
-
const idx = text.lastIndexOf(sep, maxLen);
|
|
279
|
-
if (idx > maxLen / 2)
|
|
280
|
-
return idx;
|
|
281
|
-
}
|
|
282
|
-
return maxLen;
|
|
283
|
-
}
|
|
284
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
285
|
-
//# sourceMappingURL=llm-stream.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"llm-stream.js","sourceRoot":"","sources":["../../src/bot/llm-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-user rolling message history for GramIO bots — opt-in, with
|
|
3
|
-
* retention, **sharded by thread**.
|
|
4
|
-
*
|
|
5
|
-
* Each user has one session record. Inside that record, `history` keeps
|
|
6
|
-
* a separate ring buffer per thread:
|
|
7
|
-
*
|
|
8
|
-
* storage[userId].history.shards = {
|
|
9
|
-
* 'general': [ ...entries from no-thread messages ],
|
|
10
|
-
* '12345': [ ...entries from message_thread_id=12345 ],
|
|
11
|
-
* '98765': [ ...entries from message_thread_id=98765 ],
|
|
12
|
-
* }
|
|
13
|
-
*
|
|
14
|
-
* This makes the plugin compatible with both forum-supergroup topics
|
|
15
|
-
* and BotFather's Threaded Mode for private chats (which is the whole
|
|
16
|
-
* point of those features: parallel conversations with separate
|
|
17
|
-
* context). `ctx.history` returns ONLY the slice for the current
|
|
18
|
-
* thread; `ctx.allHistory` returns the full sharded map when you need
|
|
19
|
-
* a cross-thread view (e.g. for `/export`).
|
|
20
|
-
*
|
|
21
|
-
* Thread key: `String(ctx.threadId)`, or `'general'` when no thread.
|
|
22
|
-
*
|
|
23
|
-
* ## What this plugin owns
|
|
24
|
-
*
|
|
25
|
-
* - Appends each incoming user message to the shard for its thread
|
|
26
|
-
* - Prunes per-shard by `retentionDays` and `maxMessages`
|
|
27
|
-
* - Exposes `ctx.history` (current thread's pruned slice) and
|
|
28
|
-
* `ctx.allHistory` (full map) to handlers
|
|
29
|
-
*
|
|
30
|
-
* ## GDPR caveat — retention IS personal data
|
|
31
|
-
*
|
|
32
|
-
* Unlike `bot/language` (preference data, covered by Telegram's standard
|
|
33
|
-
* privacy policy), retaining user **message content** is the one thing
|
|
34
|
-
* the [standard policy](https://telegram.org/privacy-tpa) does NOT cover
|
|
35
|
-
* by default. If you extend `messageHistory`, you should:
|
|
36
|
-
*
|
|
37
|
-
* 1. Set a custom `privacy` URL on your `botMenu` describing what you
|
|
38
|
-
* retain and for how long.
|
|
39
|
-
* 2. Pass `personalData: { storage }` to your `botMenu` so 🗑 Forget
|
|
40
|
-
* and 📥 Export buttons appear — letting users see and delete
|
|
41
|
-
* the data you keep about them.
|
|
42
|
-
*
|
|
43
|
-
* These are not enforced by this plugin (would couple it to menu); they
|
|
44
|
-
* are documented as the bot author's legal responsibility.
|
|
45
|
-
*
|
|
46
|
-
* Peer deps: `gramio`, `@gramio/session`.
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* import { Bot } from 'gramio'
|
|
50
|
-
* import { session } from '@gramio/session'
|
|
51
|
-
* import { redisStorage } from '@gramio/storage-redis'
|
|
52
|
-
* import { messageHistory } from '@adriangalilea/utils/bot/message-history'
|
|
53
|
-
*
|
|
54
|
-
* const userSession = session({ storage: redisStorage(), key: 'session', initial: () => ({}) })
|
|
55
|
-
*
|
|
56
|
-
* const history = messageHistory({
|
|
57
|
-
* session: userSession,
|
|
58
|
-
* maxMessages: 100, // cap per-thread
|
|
59
|
-
* retentionDays: 7,
|
|
60
|
-
* })
|
|
61
|
-
*
|
|
62
|
-
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
63
|
-
* .extend(userSession)
|
|
64
|
-
* .extend(history.plugin)
|
|
65
|
-
* .command('replay', (ctx) => {
|
|
66
|
-
* const last = ctx.history.slice(-3).map((e) => e.text).join('\n---\n')
|
|
67
|
-
* return ctx.send(last || '(no history in this thread)')
|
|
68
|
-
* })
|
|
69
|
-
*/
|
|
70
|
-
import { type DeriveDefinitions, Plugin } from 'gramio';
|
|
71
|
-
import { session } from '@gramio/session';
|
|
72
|
-
export type HistoryEntry = {
|
|
73
|
-
/** Telegram message id. */
|
|
74
|
-
messageId: number;
|
|
75
|
-
/** Unix seconds (Telegram's `message.date`). */
|
|
76
|
-
date: number;
|
|
77
|
-
/** Message text, or empty string if non-text. */
|
|
78
|
-
text: string;
|
|
79
|
-
};
|
|
80
|
-
/**
|
|
81
|
-
* Per-thread shards. Key is `String(ctx.threadId)` or `'general'`
|
|
82
|
-
* when no thread. Each shard is an independently capped + pruned
|
|
83
|
-
* ring buffer.
|
|
84
|
-
*/
|
|
85
|
-
export type HistoryRecord = {
|
|
86
|
-
shards: {
|
|
87
|
-
[threadKey: string]: HistoryEntry[];
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
/** Loose session shape — this plugin only touches the `history` field. */
|
|
91
|
-
type SessionLike = {
|
|
92
|
-
history?: HistoryRecord;
|
|
93
|
-
};
|
|
94
|
-
/** @internal — kept unexported so it doesn't clash with peers' refs. */
|
|
95
|
-
type HistorySessionPluginRef = ReturnType<typeof session<SessionLike, 'session'>>;
|
|
96
|
-
export type MessageHistoryOptions = {
|
|
97
|
-
/**
|
|
98
|
-
* Shared session plugin. This plugin extends it for the type flow;
|
|
99
|
-
* gramio's runtime dedup ensures it only runs once per update.
|
|
100
|
-
*/
|
|
101
|
-
session: HistorySessionPluginRef;
|
|
102
|
-
/** Ring buffer cap **per thread**. Oldest entries dropped when exceeded. */
|
|
103
|
-
maxMessages: number;
|
|
104
|
-
/** Entries older than this (in days) are dropped on read. */
|
|
105
|
-
retentionDays: number;
|
|
106
|
-
};
|
|
107
|
-
export type MessageHistoryFeature = {
|
|
108
|
-
plugin: ReturnType<typeof buildHistoryPlugin>;
|
|
109
|
-
};
|
|
110
|
-
type HistoryDerives = {
|
|
111
|
-
/** Pruned snapshot for the CURRENT thread only. */
|
|
112
|
-
history: ReadonlyArray<HistoryEntry>;
|
|
113
|
-
/**
|
|
114
|
-
* Pruned snapshot of all threads, keyed by thread (`'general'` or
|
|
115
|
-
* stringified threadId). Use for cross-thread views (e.g. /export).
|
|
116
|
-
*/
|
|
117
|
-
allHistory: Readonly<{
|
|
118
|
-
[threadKey: string]: ReadonlyArray<HistoryEntry>;
|
|
119
|
-
}>;
|
|
120
|
-
};
|
|
121
|
-
export declare const messageHistory: (opts: MessageHistoryOptions) => MessageHistoryFeature;
|
|
122
|
-
declare const buildHistoryPlugin: (args: {
|
|
123
|
-
sessionPlugin: HistorySessionPluginRef;
|
|
124
|
-
maxMessages: number;
|
|
125
|
-
retentionDays: number;
|
|
126
|
-
}) => Plugin<{}, DeriveDefinitions & {
|
|
127
|
-
global: HistoryDerives;
|
|
128
|
-
} & {
|
|
129
|
-
message: {
|
|
130
|
-
session: SessionLike & {
|
|
131
|
-
$clear: () => Promise<void>;
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
channel_post: {
|
|
135
|
-
session: SessionLike & {
|
|
136
|
-
$clear: () => Promise<void>;
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
inline_query: {
|
|
140
|
-
session: SessionLike & {
|
|
141
|
-
$clear: () => Promise<void>;
|
|
142
|
-
};
|
|
143
|
-
};
|
|
144
|
-
chosen_inline_result: {
|
|
145
|
-
session: SessionLike & {
|
|
146
|
-
$clear: () => Promise<void>;
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
callback_query: {
|
|
150
|
-
session: SessionLike & {
|
|
151
|
-
$clear: () => Promise<void>;
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
shipping_query: {
|
|
155
|
-
session: SessionLike & {
|
|
156
|
-
$clear: () => Promise<void>;
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
pre_checkout_query: {
|
|
160
|
-
session: SessionLike & {
|
|
161
|
-
$clear: () => Promise<void>;
|
|
162
|
-
};
|
|
163
|
-
};
|
|
164
|
-
poll_answer: {
|
|
165
|
-
session: SessionLike & {
|
|
166
|
-
$clear: () => Promise<void>;
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
chat_join_request: {
|
|
170
|
-
session: SessionLike & {
|
|
171
|
-
$clear: () => Promise<void>;
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
new_chat_members: {
|
|
175
|
-
session: SessionLike & {
|
|
176
|
-
$clear: () => Promise<void>;
|
|
177
|
-
};
|
|
178
|
-
};
|
|
179
|
-
new_chat_title: {
|
|
180
|
-
session: SessionLike & {
|
|
181
|
-
$clear: () => Promise<void>;
|
|
182
|
-
};
|
|
183
|
-
};
|
|
184
|
-
new_chat_photo: {
|
|
185
|
-
session: SessionLike & {
|
|
186
|
-
$clear: () => Promise<void>;
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
delete_chat_photo: {
|
|
190
|
-
session: SessionLike & {
|
|
191
|
-
$clear: () => Promise<void>;
|
|
192
|
-
};
|
|
193
|
-
};
|
|
194
|
-
group_chat_created: {
|
|
195
|
-
session: SessionLike & {
|
|
196
|
-
$clear: () => Promise<void>;
|
|
197
|
-
};
|
|
198
|
-
};
|
|
199
|
-
message_auto_delete_timer_changed: {
|
|
200
|
-
session: SessionLike & {
|
|
201
|
-
$clear: () => Promise<void>;
|
|
202
|
-
};
|
|
203
|
-
};
|
|
204
|
-
migrate_to_chat_id: {
|
|
205
|
-
session: SessionLike & {
|
|
206
|
-
$clear: () => Promise<void>;
|
|
207
|
-
};
|
|
208
|
-
};
|
|
209
|
-
migrate_from_chat_id: {
|
|
210
|
-
session: SessionLike & {
|
|
211
|
-
$clear: () => Promise<void>;
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
pinned_message: {
|
|
215
|
-
session: SessionLike & {
|
|
216
|
-
$clear: () => Promise<void>;
|
|
217
|
-
};
|
|
218
|
-
};
|
|
219
|
-
invoice: {
|
|
220
|
-
session: SessionLike & {
|
|
221
|
-
$clear: () => Promise<void>;
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
successful_payment: {
|
|
225
|
-
session: SessionLike & {
|
|
226
|
-
$clear: () => Promise<void>;
|
|
227
|
-
};
|
|
228
|
-
};
|
|
229
|
-
chat_shared: {
|
|
230
|
-
session: SessionLike & {
|
|
231
|
-
$clear: () => Promise<void>;
|
|
232
|
-
};
|
|
233
|
-
};
|
|
234
|
-
proximity_alert_triggered: {
|
|
235
|
-
session: SessionLike & {
|
|
236
|
-
$clear: () => Promise<void>;
|
|
237
|
-
};
|
|
238
|
-
};
|
|
239
|
-
video_chat_scheduled: {
|
|
240
|
-
session: SessionLike & {
|
|
241
|
-
$clear: () => Promise<void>;
|
|
242
|
-
};
|
|
243
|
-
};
|
|
244
|
-
video_chat_started: {
|
|
245
|
-
session: SessionLike & {
|
|
246
|
-
$clear: () => Promise<void>;
|
|
247
|
-
};
|
|
248
|
-
};
|
|
249
|
-
video_chat_ended: {
|
|
250
|
-
session: SessionLike & {
|
|
251
|
-
$clear: () => Promise<void>;
|
|
252
|
-
};
|
|
253
|
-
};
|
|
254
|
-
video_chat_participants_invited: {
|
|
255
|
-
session: SessionLike & {
|
|
256
|
-
$clear: () => Promise<void>;
|
|
257
|
-
};
|
|
258
|
-
};
|
|
259
|
-
web_app_data: {
|
|
260
|
-
session: SessionLike & {
|
|
261
|
-
$clear: () => Promise<void>;
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
location: {
|
|
265
|
-
session: SessionLike & {
|
|
266
|
-
$clear: () => Promise<void>;
|
|
267
|
-
};
|
|
268
|
-
};
|
|
269
|
-
passport_data: {
|
|
270
|
-
session: SessionLike & {
|
|
271
|
-
$clear: () => Promise<void>;
|
|
272
|
-
};
|
|
273
|
-
};
|
|
274
|
-
} & {
|
|
275
|
-
message: HistoryDerives;
|
|
276
|
-
callback_query: HistoryDerives;
|
|
277
|
-
}, {}>;
|
|
278
|
-
export {};
|
|
279
|
-
//# sourceMappingURL=message-history.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"message-history.d.ts","sourceRoot":"","sources":["../../src/bot/message-history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAIzC,MAAM,MAAM,YAAY,GAAG;IACzB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE;QAAE,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CAAA;KAAE,CAAA;CAChD,CAAA;AAaD,0EAA0E;AAC1E,KAAK,WAAW,GAAG;IAAE,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,CAAA;AAE9C,wEAAwE;AACxE,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAA;AAEjF,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,OAAO,EAAE,uBAAuB,CAAA;IAChC,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;IACnB,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAA;CAC9C,CAAA;AAID,KAAK,cAAc,GAAG;IACpB,mDAAmD;IACnD,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IACpC;;;OAGG;IACH,UAAU,EAAE,QAAQ,CAAC;QAAE,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,CAAA;KAAE,CAAC,CAAA;CAC3E,CAAA;AA6BD,eAAO,MAAM,cAAc,GAAI,MAAM,qBAAqB,KAAG,qBAW5D,CAAA;AAID,QAAA,MAAM,kBAAkB,GAAI,MAAM;IAChC,aAAa,EAAE,uBAAuB,CAAA;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;CACtB;YAGqD,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAqCnE,CAAA"}
|