@emulon/telegram 0.2.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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Telegram MarkdownV2 parsing, modelled on the TDLib parser the Bot API uses:
3
+ * one left-to-right pass over the source with a stack of open entities, so
4
+ * nesting, escaping and delimiter ambiguity resolve the way Telegram resolves
5
+ * them. Offsets and lengths are UTF-16 code units of the rendered text, which
6
+ * is exactly how a JavaScript string counts.
7
+ */
8
+ /**
9
+ * Malformed or unsupported markup. It carries no detail: Telegram's own
10
+ * message names the offending character, which would echo caller input.
11
+ */
12
+ export class MarkdownError extends Error {
13
+ constructor() {
14
+ super("can't parse entities");
15
+ }
16
+ }
17
+ /** Characters that are markup outside code and must otherwise be escaped. */
18
+ const reserved = new Set('_*[]()~`>#+-=|{}.!');
19
+ /** Any character from 1 to 126 may follow a backslash to stand for itself. */
20
+ function escapable(source, index) {
21
+ const code = source.charCodeAt(index);
22
+ return code >= 1 && code <= 126;
23
+ }
24
+ /**
25
+ * Link targets are unescaped verbatim. Telegram adds `http://` to a target
26
+ * without a scheme and silently drops a link it cannot read as a URL; mention,
27
+ * custom emoji, mail and other schemes are outside this emulator's scope and
28
+ * are refused rather than rendered differently.
29
+ */
30
+ export function linkTarget(raw) {
31
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
32
+ if (!/^https?:\/\//i.test(raw)) {
33
+ throw new MarkdownError();
34
+ }
35
+ }
36
+ else if (/^(tg|ton|tonsite|mailto|tel):/i.test(raw)) {
37
+ throw new MarkdownError();
38
+ }
39
+ else {
40
+ raw = `http://${raw}`;
41
+ }
42
+ if (/\s/.test(raw)) {
43
+ return null;
44
+ }
45
+ try {
46
+ const url = new URL(raw);
47
+ return url.hostname === '' ? null : url.href;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ export function parseMarkdownV2(source) {
54
+ let text = '';
55
+ const entities = [];
56
+ const open = [];
57
+ let quote = null;
58
+ let order = 0;
59
+ let lineStart = true;
60
+ const push = (entity, at) => {
61
+ // An empty entity is dropped, as Telegram drops `**` or `[](url)`.
62
+ if (entity.length > 0) {
63
+ entities.push({ ...entity, order: at });
64
+ }
65
+ };
66
+ // Entities opened inside a quotation must close inside it.
67
+ const closeQuote = () => {
68
+ if (open.length > 0) {
69
+ throw new MarkdownError();
70
+ }
71
+ push({
72
+ type: quote.expandable ? 'expandable_blockquote' : 'blockquote',
73
+ offset: quote.offset,
74
+ length: text.length - quote.offset,
75
+ }, quote.order);
76
+ quote = null;
77
+ };
78
+ for (let i = 0; i < source.length; i++) {
79
+ const c = source[i];
80
+ const top = open.at(-1);
81
+ const inCode = top?.type === 'code' || top?.type === 'pre';
82
+ const atLineStart = lineStart;
83
+ lineStart = false;
84
+ if (c === '\\' && escapable(source, i + 1)) {
85
+ text += source[++i];
86
+ continue;
87
+ }
88
+ if (atLineStart && !inCode) {
89
+ const marker = source.startsWith('**>', i)
90
+ ? 3
91
+ : source[i] === '>'
92
+ ? 1
93
+ : 0;
94
+ if (marker > 0) {
95
+ // Quotations cannot nest in other entities or in each other.
96
+ if (!quote && open.length > 0) {
97
+ throw new MarkdownError();
98
+ }
99
+ quote ??= { offset: text.length, order: order++, expandable: false };
100
+ i += marker - 1;
101
+ continue;
102
+ }
103
+ }
104
+ if (c === '\n') {
105
+ if (quote && inCode) {
106
+ // Code spanning quoted lines would need prefix rules Telegram
107
+ // does not document; refuse instead of guessing them.
108
+ throw new MarkdownError();
109
+ }
110
+ text += c;
111
+ lineStart = true;
112
+ // A quotation ends with the newline that ends its last line, so the
113
+ // newline belongs to it; `||` ends the line after it marked.
114
+ if (quote && (quote.expandable || source[i + 1] !== '>')) {
115
+ closeQuote();
116
+ }
117
+ continue;
118
+ }
119
+ if (inCode ? c !== '`' : !reserved.has(c)) {
120
+ text += c;
121
+ continue;
122
+ }
123
+ // The expandability mark: `||` ending a quoted line outside a spoiler.
124
+ if (quote && c === '|' && source[i + 1] === '|' && top?.type !== 'spoiler' &&
125
+ (i + 2 === source.length || source[i + 2] === '\n')) {
126
+ quote.expandable = true;
127
+ i++;
128
+ continue;
129
+ }
130
+ const ends = top !== undefined && (() => {
131
+ switch (top.type) {
132
+ case 'bold':
133
+ return c === '*';
134
+ case 'italic':
135
+ return c === '_' && source[i + 1] !== '_';
136
+ case 'underline':
137
+ return c === '_' && source[i + 1] === '_';
138
+ case 'strikethrough':
139
+ return c === '~';
140
+ case 'spoiler':
141
+ return c === '|' && source[i + 1] === '|';
142
+ case 'code':
143
+ return c === '`';
144
+ case 'pre':
145
+ return source.startsWith('```', i);
146
+ case 'text_link':
147
+ return c === ']';
148
+ }
149
+ })();
150
+ if (ends) {
151
+ const entity = open.pop();
152
+ const base = {
153
+ type: entity.type,
154
+ offset: entity.offset,
155
+ length: text.length - entity.offset,
156
+ };
157
+ // Skip the rest of a multi-character closing delimiter.
158
+ i += entity.type === 'underline' || entity.type === 'spoiler'
159
+ ? 1
160
+ : entity.type === 'pre'
161
+ ? 2
162
+ : 0;
163
+ if (entity.type === 'text_link') {
164
+ let raw = '';
165
+ if (source[i + 1] === '(') {
166
+ i += 2;
167
+ while (i < source.length && source[i] !== ')') {
168
+ if (source[i] === '\\' && escapable(source, i + 1)) {
169
+ i++;
170
+ }
171
+ raw += source[i++];
172
+ }
173
+ if (i >= source.length) {
174
+ throw new MarkdownError();
175
+ }
176
+ }
177
+ else {
178
+ raw = text.slice(entity.offset);
179
+ }
180
+ const url = linkTarget(raw);
181
+ if (url !== null) {
182
+ push({ ...base, url }, entity.order);
183
+ }
184
+ }
185
+ else if (entity.type === 'pre' && entity.language !== undefined) {
186
+ push({ ...base, language: entity.language }, entity.order);
187
+ }
188
+ else {
189
+ push(base, entity.order);
190
+ }
191
+ continue;
192
+ }
193
+ // A backtick inside code that does not close it cannot open anything.
194
+ if (inCode) {
195
+ throw new MarkdownError();
196
+ }
197
+ const start = (() => {
198
+ switch (c) {
199
+ case '_':
200
+ if (source[i + 1] === '_') {
201
+ i++;
202
+ return { type: 'underline' };
203
+ }
204
+ return { type: 'italic' };
205
+ case '*':
206
+ return { type: 'bold' };
207
+ case '~':
208
+ return { type: 'strikethrough' };
209
+ case '|':
210
+ if (source[i + 1] === '|') {
211
+ i++;
212
+ return { type: 'spoiler' };
213
+ }
214
+ throw new MarkdownError();
215
+ case '[':
216
+ // Links cannot contain links.
217
+ if (open.some((entity) => entity.type === 'text_link')) {
218
+ throw new MarkdownError();
219
+ }
220
+ return { type: 'text_link' };
221
+ case '`': {
222
+ if (!source.startsWith('```', i)) {
223
+ return { type: 'code' };
224
+ }
225
+ i += 3;
226
+ let end = i;
227
+ while (end < source.length && !/\s/.test(source[end]) &&
228
+ source[end] !== '`') {
229
+ end++;
230
+ }
231
+ let language;
232
+ if (end !== i && end < source.length && source[end] !== '`') {
233
+ language = source.slice(i, end);
234
+ i = end;
235
+ }
236
+ // One line break after the opening fence is not content.
237
+ if (source[i] === '\n' || source[i] === '\r') {
238
+ const pair = (source[i + 1] === '\n' || source[i + 1] === '\r') &&
239
+ source[i] !== source[i + 1];
240
+ i += pair ? 2 : 1;
241
+ }
242
+ i--;
243
+ return language === undefined
244
+ ? { type: 'pre' }
245
+ : { type: 'pre', language };
246
+ }
247
+ default:
248
+ // Includes `!` before `[`: custom emoji are not emulated.
249
+ throw new MarkdownError();
250
+ }
251
+ })();
252
+ open.push({ ...start, offset: text.length, order: order++ });
253
+ }
254
+ if (open.length > 0) {
255
+ throw new MarkdownError();
256
+ }
257
+ if (quote) {
258
+ closeQuote();
259
+ }
260
+ // Outer entities first: by offset, then longer first, then opening order.
261
+ entities.sort((a, b) => a.offset - b.offset || b.length - a.length || a.order - b.order);
262
+ return {
263
+ text,
264
+ entities: entities.map(({ order: _, ...entity }) => entity),
265
+ };
266
+ }
package/esm/mod.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { z } from 'zod';
2
+ import type { CompatibilityManifest, defineCommand } from 'emulon';
3
+ import { definePlugin } from 'emulon';
4
+ import { type Commands } from './commands/mod.js';
5
+ import { type Options } from './model/channels.js';
6
+ declare const telegram: ReturnType<typeof definePlugin<Options | undefined, Commands & {
7
+ 'compatibility.get': ReturnType<typeof defineCommand<z.ZodObject<Record<string, never>>, z.ZodType<CompatibilityManifest>>>;
8
+ }>>;
9
+ export default telegram;
package/esm/mod.js ADDED
@@ -0,0 +1,28 @@
1
+ import { definePlugin } from 'emulon';
2
+ import { commands } from './commands/mod.js';
3
+ import { compatibility } from './compatibility.js';
4
+ import { channelFixtures } from './model/channels.js';
5
+ import { routes } from './routes/api.js';
6
+ const telegram = definePlugin({
7
+ name: '@emulon/telegram',
8
+ apiVersion: 1,
9
+ compatibility,
10
+ capabilities: ['http', 'authorization', 'reset'],
11
+ commands,
12
+ state: {
13
+ pluginVersion: '0.1.0',
14
+ schemaVersion: 1,
15
+ // Bots are never fixtures: reset removes them and every issued token.
16
+ fixtures: channelFixtures,
17
+ },
18
+ async setup(ctx) {
19
+ routes(ctx, ctx.http.surface('api'));
20
+ const api = await ctx.http.listen('api');
21
+ return {
22
+ endpoints: { api },
23
+ ready: () => Promise.resolve(),
24
+ stop: () => Promise.resolve(),
25
+ };
26
+ },
27
+ });
28
+ export default telegram;
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { type PluginContext } from 'emulon';
3
+ import { type ParsedToken } from '../auth/tokens.js';
4
+ type Store = PluginContext['store'];
5
+ /** Telegram bot usernames: 5–32 characters that end in `bot`. */
6
+ export declare const botUsername: z.ZodType<string, string>;
7
+ export declare const createInput: z.ZodType<CreateInput, CreateInput>;
8
+ export interface CreateInput {
9
+ username: string;
10
+ firstName: string;
11
+ }
12
+ /** The only output that carries the token; it is never readable again. */
13
+ export interface IssuedBot {
14
+ id: number;
15
+ username: string;
16
+ token: string;
17
+ }
18
+ export declare const issuedSchema: z.ZodType<IssuedBot, IssuedBot>;
19
+ export declare const botSchema: z.ZodType<Bot, Bot>;
20
+ export interface Bot {
21
+ id: number;
22
+ username: string;
23
+ firstName: string;
24
+ verifier: string;
25
+ }
26
+ /** The `getMe` projection: a bot without optional capabilities. */
27
+ export interface BotUser {
28
+ id: number;
29
+ is_bot: true;
30
+ first_name: string;
31
+ username: string;
32
+ can_join_groups: boolean;
33
+ can_read_all_group_messages: boolean;
34
+ supports_inline_queries: boolean;
35
+ can_connect_to_business: boolean;
36
+ has_main_web_app: boolean;
37
+ has_topics_enabled: boolean;
38
+ allows_users_to_create_topics: boolean;
39
+ can_manage_bots: boolean;
40
+ supports_join_request_queries: boolean;
41
+ }
42
+ /** IDs look like issued Telegram bot IDs and stay far from channel IDs. */
43
+ export declare const firstBotId = 7000000001;
44
+ export declare function nextBotId(existing: readonly Bot[]): number;
45
+ export declare function botUser(bot: Bot): BotUser;
46
+ /**
47
+ * The username check, ID allocation and verifier write commit together, so
48
+ * two concurrent creations can never share a username or an ID.
49
+ */
50
+ export declare function createBot(store: Store, raw: CreateInput): Promise<IssuedBot>;
51
+ /** The bot a well-formed token names, or null when it names no issued bot. */
52
+ export declare function authenticate(store: Store, token: ParsedToken): Promise<Bot | null>;
53
+ export {};
@@ -0,0 +1,84 @@
1
+ // Bot API objects are snake_case on the wire.
2
+ // deno-lint-ignore-file camelcase
3
+ import { z } from 'zod';
4
+ import { DomainError } from 'emulon';
5
+ import { issueToken, sameVerifier, verifier, } from '../auth/tokens.js';
6
+ import { channelSchema, usernameKey } from './channels.js';
7
+ /** Telegram bot usernames: 5–32 characters that end in `bot`. */
8
+ export const botUsername = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{1,28}[Bb][Oo][Tt]$/);
9
+ export const createInput = z.strictObject({
10
+ username: botUsername,
11
+ firstName: z.string().min(1).max(64),
12
+ });
13
+ export const issuedSchema = z.strictObject({
14
+ id: z.number().int().positive().safe(),
15
+ username: z.string(),
16
+ token: z.string(),
17
+ });
18
+ export const botSchema = z.strictObject({
19
+ id: z.number().int().positive().safe(),
20
+ username: z.string(),
21
+ firstName: z.string(),
22
+ verifier: z.string().regex(/^[0-9a-f]{64}$/),
23
+ });
24
+ /** IDs look like issued Telegram bot IDs and stay far from channel IDs. */
25
+ export const firstBotId = 7_000_000_001;
26
+ export function nextBotId(existing) {
27
+ return existing.reduce((next, bot) => Math.max(next, bot.id + 1), firstBotId);
28
+ }
29
+ export function botUser(bot) {
30
+ return {
31
+ id: bot.id,
32
+ is_bot: true,
33
+ first_name: bot.firstName,
34
+ username: bot.username,
35
+ can_join_groups: true,
36
+ can_read_all_group_messages: false,
37
+ supports_inline_queries: false,
38
+ can_connect_to_business: false,
39
+ has_main_web_app: false,
40
+ has_topics_enabled: false,
41
+ allows_users_to_create_topics: false,
42
+ can_manage_bots: false,
43
+ supports_join_request_queries: false,
44
+ };
45
+ }
46
+ /**
47
+ * The username check, ID allocation and verifier write commit together, so
48
+ * two concurrent creations can never share a username or an ID.
49
+ */
50
+ export async function createBot(store, raw) {
51
+ const input = createInput.parse(raw);
52
+ return await store.transaction(async (tx) => {
53
+ const bots = (await tx.list('bots')).map((row) => botSchema.parse(row.value));
54
+ const taken = new Set([
55
+ ...bots.map((bot) => usernameKey(bot.username)),
56
+ ...(await tx.list('channels')).map((row) => usernameKey(channelSchema.parse(row.value).username)),
57
+ ]);
58
+ if (taken.has(usernameKey(input.username))) {
59
+ throw new DomainError('USERNAME_TAKEN', 'A bot or channel already uses this username.');
60
+ }
61
+ const id = nextBotId(bots);
62
+ const token = issueToken(id);
63
+ const bot = {
64
+ id,
65
+ username: input.username,
66
+ firstName: input.firstName,
67
+ verifier: await verifier(token),
68
+ };
69
+ await tx.put({ collection: 'bots', id: String(id), value: bot });
70
+ return { id, username: bot.username, token };
71
+ });
72
+ }
73
+ /** The bot a well-formed token names, or null when it names no issued bot. */
74
+ export async function authenticate(store, token) {
75
+ const presented = await verifier(token.token);
76
+ return await store.transaction(async (tx) => {
77
+ const row = await tx.get('bots', String(token.botId));
78
+ if (row === undefined) {
79
+ return null;
80
+ }
81
+ const bot = botSchema.parse(row);
82
+ return sameVerifier(presented, bot.verifier) ? bot : null;
83
+ });
84
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ /** Telegram public usernames: 5–32 characters, starting with a letter. */
3
+ export declare const channelUsername: z.ZodType<string, string>;
4
+ /** Channel and supergroup IDs are negative with a `-100` decimal prefix. */
5
+ export declare function isChannelId(id: number): boolean;
6
+ export declare const channelSchema: z.ZodType<Channel, Channel>;
7
+ export interface Channel {
8
+ id: number;
9
+ title: string;
10
+ username: string;
11
+ }
12
+ export interface Options {
13
+ fixtures?: {
14
+ channels?: Channel[] | undefined;
15
+ } | undefined;
16
+ }
17
+ export declare const optionsSchema: z.ZodType<Options, Options>;
18
+ /** Usernames of bots and channels share one case-insensitive namespace. */
19
+ export declare function usernameKey(username: string): string;
20
+ /** Fixture errors name the rule, never the configured value. */
21
+ export declare function channelFixtures(options?: Options): {
22
+ collection: string;
23
+ id: string;
24
+ value: Channel;
25
+ }[];
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ /** Telegram public usernames: 5–32 characters, starting with a letter. */
3
+ export const channelUsername = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{4,31}$/);
4
+ /** Channel and supergroup IDs are negative with a `-100` decimal prefix. */
5
+ export function isChannelId(id) {
6
+ return Number.isSafeInteger(id) && id < 0 && /^-100\d+$/.test(String(id));
7
+ }
8
+ export const channelSchema = z.strictObject({
9
+ id: z.number().refine(isChannelId),
10
+ title: z.string().min(1).max(128),
11
+ username: channelUsername,
12
+ });
13
+ export const optionsSchema = z.strictObject({
14
+ fixtures: z.strictObject({ channels: z.array(channelSchema).optional() })
15
+ .optional(),
16
+ });
17
+ /** Usernames of bots and channels share one case-insensitive namespace. */
18
+ export function usernameKey(username) {
19
+ return username.toLowerCase();
20
+ }
21
+ /** Fixture errors name the rule, never the configured value. */
22
+ export function channelFixtures(options) {
23
+ const parsed = optionsSchema.safeParse(options ?? {});
24
+ if (!parsed.success) {
25
+ throw new Error('Invalid Telegram options.');
26
+ }
27
+ const channels = parsed.data.fixtures?.channels ?? [];
28
+ const ids = new Set();
29
+ const usernames = new Set();
30
+ for (const channel of channels) {
31
+ if (ids.has(channel.id)) {
32
+ throw new Error('Duplicate fixture channel ID.');
33
+ }
34
+ if (usernames.has(usernameKey(channel.username))) {
35
+ throw new Error('Duplicate fixture channel username.');
36
+ }
37
+ ids.add(channel.id);
38
+ usernames.add(usernameKey(channel.username));
39
+ }
40
+ return channels.map((channel) => ({
41
+ collection: 'channels',
42
+ id: String(channel.id),
43
+ value: { ...channel },
44
+ }));
45
+ }
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod';
2
+ import { type PluginContext } from 'emulon';
3
+ import { type MessageEntity } from '../format/markdown_v2.js';
4
+ import { type Bot, type BotUser } from './bots.js';
5
+ import { type Channel } from './channels.js';
6
+ type Store = PluginContext['store'];
7
+ type Transaction = Parameters<Parameters<Store['transaction']>[0]>[0];
8
+ /** Telegram's limit on message text, in UTF-16 code units after parsing. */
9
+ export declare const maxTextLength = 4096;
10
+ /**
11
+ * A rejected send. The route turns it into a Bot API envelope; the reason is
12
+ * one of the fixed descriptions below and never contains caller input.
13
+ */
14
+ export declare class SendError extends Error {
15
+ readonly status: 400 | 501;
16
+ readonly description: string;
17
+ constructor(status: 400 | 501, description: string);
18
+ }
19
+ export declare const sendErrors: {
20
+ readonly parse: () => SendError;
21
+ readonly tooLong: () => SendError;
22
+ readonly empty: () => SendError;
23
+ readonly chatEmpty: () => SendError;
24
+ readonly chatNotFound: () => SendError;
25
+ readonly parseMode: () => SendError;
26
+ readonly invalid: () => SendError;
27
+ readonly unsupported: () => SendError;
28
+ readonly unsupportedMode: () => SendError;
29
+ };
30
+ export type ParseMode = 'MarkdownV2' | null;
31
+ export interface Rendered {
32
+ text: string;
33
+ entities: MessageEntity[];
34
+ }
35
+ /**
36
+ * Parsing comes before the length rule, so malformed markup is reported even
37
+ * when it is also too long; the limit counts rendered text only.
38
+ */
39
+ export declare function render(source: string, parseMode: ParseMode): Rendered;
40
+ /** A `chat_id` as a channel ID, or a lowercase username without `@`. */
41
+ export type ChatReference = {
42
+ id: number;
43
+ } | {
44
+ username: string;
45
+ };
46
+ export declare function chatReference(value: unknown): ChatReference | null;
47
+ export interface SendRequest {
48
+ chat: ChatReference;
49
+ source: string;
50
+ parseMode: ParseMode;
51
+ }
52
+ /**
53
+ * Only the fields the declared consumer sends are emulated. Any other field,
54
+ * or a value that would change behavior the emulator does not model, is
55
+ * refused instead of being silently ignored.
56
+ */
57
+ export declare function sendRequest(params: Record<string, unknown>): SendRequest;
58
+ export declare const entitySchema: z.ZodType<MessageEntity, MessageEntity>;
59
+ /** A stored channel post: what was submitted and what Telegram would show. */
60
+ export interface StoredMessage {
61
+ chatId: number;
62
+ messageId: number;
63
+ botId: number;
64
+ date: number;
65
+ parseMode: ParseMode;
66
+ source: string;
67
+ text: string;
68
+ entities: MessageEntity[];
69
+ }
70
+ export declare const storedSchema: z.ZodType<StoredMessage, StoredMessage>;
71
+ export interface ChatView {
72
+ id: number;
73
+ title: string;
74
+ username: string;
75
+ type: 'channel';
76
+ }
77
+ /** The Bot API `Message` returned by `sendMessage`. */
78
+ export interface MessageView {
79
+ message_id: number;
80
+ from: Pick<BotUser, 'id' | 'is_bot' | 'first_name' | 'username'>;
81
+ sender_chat: ChatView;
82
+ chat: ChatView;
83
+ date: number;
84
+ text: string;
85
+ entities?: MessageEntity[];
86
+ }
87
+ export declare function chatView(channel: Channel): ChatView;
88
+ export declare function messageView(message: StoredMessage, channel: Channel, bot: Bot): MessageView;
89
+ export declare function findChannel(tx: Transaction, chat: ChatReference): Promise<Channel | null>;
90
+ /** Message IDs sort as text in store order and as numbers in the key. */
91
+ export declare const messageKey: (chatId: number, messageId: number) => string;
92
+ /**
93
+ * The chat lookup, rendering, ID allocation and message write share one
94
+ * transaction: a rejected send leaves no trace and allocates no ID, and
95
+ * concurrent sends to one chat never share or skip an ID.
96
+ */
97
+ export declare function sendMessage(store: Store, bot: Bot, request: SendRequest, now: number): Promise<MessageView>;
98
+ export declare const listInput: z.ZodType<ListInput, ListInput>;
99
+ export interface ListInput {
100
+ chatId: number;
101
+ limit?: number | undefined;
102
+ }
103
+ /**
104
+ * The most recent `limit` messages (default 100) of one channel, oldest
105
+ * first, so a multipart post reads in the order it was sent.
106
+ */
107
+ export declare function listMessages(store: Store, raw: ListInput): Promise<StoredMessage[]>;
108
+ export {};