@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,269 @@
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 { MarkdownError, parseMarkdownV2, } from '../format/markdown_v2.js';
6
+ import { botUser } from './bots.js';
7
+ import { channelSchema, usernameKey } from './channels.js';
8
+ /** Telegram's limit on message text, in UTF-16 code units after parsing. */
9
+ export 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 class SendError extends Error {
15
+ status;
16
+ description;
17
+ constructor(status, description) {
18
+ super(description);
19
+ this.status = status;
20
+ this.description = description;
21
+ }
22
+ }
23
+ const badRequest = (reason) => new SendError(400, `Bad Request: ${reason}`);
24
+ export const sendErrors = {
25
+ parse: () => badRequest("can't parse entities"),
26
+ tooLong: () => badRequest('message is too long'),
27
+ empty: () => badRequest('message text is empty'),
28
+ chatEmpty: () => badRequest('chat_id is empty'),
29
+ chatNotFound: () => badRequest('chat not found'),
30
+ parseMode: () => badRequest('unsupported parse_mode'),
31
+ invalid: () => badRequest('invalid parameter value'),
32
+ unsupported: () => new SendError(501, 'Not Implemented: parameter is not emulated'),
33
+ unsupportedMode: () => new SendError(501, 'Not Implemented: parse mode is not emulated'),
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 function render(source, parseMode) {
40
+ let rendered;
41
+ if (parseMode === 'MarkdownV2') {
42
+ try {
43
+ rendered = parseMarkdownV2(source);
44
+ }
45
+ catch (error) {
46
+ if (error instanceof MarkdownError) {
47
+ throw sendErrors.parse();
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ else {
53
+ rendered = { text: source, entities: [] };
54
+ }
55
+ if (rendered.text.length === 0) {
56
+ throw sendErrors.empty();
57
+ }
58
+ if (rendered.text.length > maxTextLength) {
59
+ throw sendErrors.tooLong();
60
+ }
61
+ return rendered;
62
+ }
63
+ export function chatReference(value) {
64
+ if (typeof value === 'number') {
65
+ return Number.isSafeInteger(value) ? { id: value } : null;
66
+ }
67
+ if (typeof value !== 'string') {
68
+ return null;
69
+ }
70
+ if (/^-?[1-9][0-9]*$/.test(value)) {
71
+ const id = Number(value);
72
+ return Number.isSafeInteger(id) ? { id } : null;
73
+ }
74
+ return value.startsWith('@') && value.length > 1
75
+ ? { username: usernameKey(value.slice(1)) }
76
+ : null;
77
+ }
78
+ const accepted = new Set([
79
+ 'chat_id',
80
+ 'text',
81
+ 'parse_mode',
82
+ 'disable_notification',
83
+ 'link_preview_options',
84
+ ]);
85
+ /**
86
+ * Only the fields the declared consumer sends are emulated. Any other field,
87
+ * or a value that would change behavior the emulator does not model, is
88
+ * refused instead of being silently ignored.
89
+ */
90
+ export function sendRequest(params) {
91
+ if (Object.keys(params).some((key) => !accepted.has(key))) {
92
+ throw sendErrors.unsupported();
93
+ }
94
+ const { chat_id, text, parse_mode, disable_notification, link_preview_options, } = params;
95
+ if (disable_notification !== undefined) {
96
+ if (typeof disable_notification !== 'boolean') {
97
+ throw sendErrors.invalid();
98
+ }
99
+ }
100
+ if (link_preview_options !== undefined) {
101
+ if (typeof link_preview_options !== 'object' ||
102
+ link_preview_options === null || Array.isArray(link_preview_options)) {
103
+ throw sendErrors.invalid();
104
+ }
105
+ const options = link_preview_options;
106
+ if (Object.keys(options).some((key) => key !== 'is_disabled') ||
107
+ (options.is_disabled !== undefined && options.is_disabled !== false)) {
108
+ throw sendErrors.unsupported();
109
+ }
110
+ }
111
+ let parseMode = null;
112
+ if (parse_mode !== undefined && parse_mode !== '') {
113
+ if (typeof parse_mode !== 'string') {
114
+ throw sendErrors.invalid();
115
+ }
116
+ // The Bot API compares parse modes without regard to case.
117
+ const mode = parse_mode.toLowerCase();
118
+ if (mode === 'html' || mode === 'markdown') {
119
+ throw sendErrors.unsupportedMode();
120
+ }
121
+ if (mode !== 'markdownv2') {
122
+ throw sendErrors.parseMode();
123
+ }
124
+ parseMode = 'MarkdownV2';
125
+ }
126
+ if (chat_id === undefined || chat_id === '') {
127
+ throw sendErrors.chatEmpty();
128
+ }
129
+ const chat = chatReference(chat_id);
130
+ if (!chat) {
131
+ throw sendErrors.chatNotFound();
132
+ }
133
+ if (text === undefined || text === '') {
134
+ throw sendErrors.empty();
135
+ }
136
+ if (typeof text !== 'string') {
137
+ throw sendErrors.invalid();
138
+ }
139
+ return { chat, source: text, parseMode };
140
+ }
141
+ export const entitySchema = z
142
+ .strictObject({
143
+ type: z.enum([
144
+ 'bold',
145
+ 'italic',
146
+ 'underline',
147
+ 'strikethrough',
148
+ 'spoiler',
149
+ 'code',
150
+ 'pre',
151
+ 'text_link',
152
+ 'blockquote',
153
+ 'expandable_blockquote',
154
+ ]),
155
+ offset: z.number().int().nonnegative(),
156
+ length: z.number().int().positive(),
157
+ url: z.string().optional(),
158
+ language: z.string().optional(),
159
+ });
160
+ export const storedSchema = z
161
+ .strictObject({
162
+ chatId: z.number().int(),
163
+ messageId: z.number().int().positive(),
164
+ botId: z.number().int().positive(),
165
+ date: z.number().int().nonnegative(),
166
+ parseMode: z.literal('MarkdownV2').nullable(),
167
+ source: z.string(),
168
+ text: z.string(),
169
+ entities: z.array(entitySchema),
170
+ });
171
+ export function chatView(channel) {
172
+ return {
173
+ id: channel.id,
174
+ title: channel.title,
175
+ username: channel.username,
176
+ type: 'channel',
177
+ };
178
+ }
179
+ export function messageView(message, channel, bot) {
180
+ const user = botUser(bot);
181
+ return {
182
+ message_id: message.messageId,
183
+ from: {
184
+ id: user.id,
185
+ is_bot: true,
186
+ first_name: user.first_name,
187
+ username: user.username,
188
+ },
189
+ sender_chat: chatView(channel),
190
+ chat: chatView(channel),
191
+ date: message.date,
192
+ text: message.text,
193
+ ...(message.entities.length > 0 ? { entities: message.entities } : {}),
194
+ };
195
+ }
196
+ export async function findChannel(tx, chat) {
197
+ if ('id' in chat) {
198
+ const row = await tx.get('channels', String(chat.id));
199
+ return row === undefined ? null : channelSchema.parse(row);
200
+ }
201
+ const channels = (await tx.list('channels')).map((row) => channelSchema.parse(row.value));
202
+ return channels.find((channel) => usernameKey(channel.username) === chat.username) ?? null;
203
+ }
204
+ /** Message IDs sort as text in store order and as numbers in the key. */
205
+ export const messageKey = (chatId, messageId) => `${chatId}:${String(messageId).padStart(16, '0')}`;
206
+ const sequenceSchema = z.strictObject({
207
+ next: z.number().int().positive(),
208
+ });
209
+ /**
210
+ * The chat lookup, rendering, ID allocation and message write share one
211
+ * transaction: a rejected send leaves no trace and allocates no ID, and
212
+ * concurrent sends to one chat never share or skip an ID.
213
+ */
214
+ export async function sendMessage(store, bot, request, now) {
215
+ return await store.transaction(async (tx) => {
216
+ const channel = await findChannel(tx, request.chat);
217
+ if (!channel) {
218
+ throw sendErrors.chatNotFound();
219
+ }
220
+ const rendered = render(request.source, request.parseMode);
221
+ const sequence = await tx.get('messageSequences', String(channel.id));
222
+ const messageId = sequence === undefined
223
+ ? 1
224
+ : sequenceSchema.parse(sequence).next;
225
+ const message = {
226
+ chatId: channel.id,
227
+ messageId,
228
+ botId: bot.id,
229
+ date: Math.floor(now / 1000),
230
+ parseMode: request.parseMode,
231
+ source: request.source,
232
+ text: rendered.text,
233
+ entities: rendered.entities,
234
+ };
235
+ await tx.put({
236
+ collection: 'messageSequences',
237
+ id: String(channel.id),
238
+ value: { next: messageId + 1 },
239
+ });
240
+ await tx.put({
241
+ collection: 'messages',
242
+ id: messageKey(channel.id, messageId),
243
+ value: message,
244
+ });
245
+ return messageView(message, channel, bot);
246
+ });
247
+ }
248
+ export const listInput = z.strictObject({
249
+ chatId: z.number().int(),
250
+ limit: z.number().int().min(1).max(100).optional(),
251
+ });
252
+ /**
253
+ * The most recent `limit` messages (default 100) of one channel, oldest
254
+ * first, so a multipart post reads in the order it was sent.
255
+ */
256
+ export async function listMessages(store, raw) {
257
+ const input = listInput.parse(raw);
258
+ return await store.transaction(async (tx) => {
259
+ const channel = await findChannel(tx, { id: input.chatId });
260
+ if (!channel) {
261
+ throw new DomainError('CHAT_NOT_FOUND', 'No such channel.');
262
+ }
263
+ const messages = (await tx.list('messages'))
264
+ .map((row) => storedSchema.parse(row.value))
265
+ .filter((message) => message.chatId === channel.id)
266
+ .sort((a, b) => a.messageId - b.messageId);
267
+ return messages.slice(-(input.limit ?? 100));
268
+ });
269
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ import { type PluginContext } from 'emulon';
3
+ import { type ReactionCount } from './updates.js';
4
+ type Store = PluginContext['store'];
5
+ export declare const reactionsInput: z.ZodType<ReactionsInput, ReactionsInput>;
6
+ export interface ReactionsInput {
7
+ chatId: number;
8
+ messageId: number;
9
+ reactions: ReactionCount[];
10
+ }
11
+ export interface ReactionsResult {
12
+ chatId: number;
13
+ messageId: number;
14
+ reactions: ReactionCount[];
15
+ changed: boolean;
16
+ queued: number;
17
+ }
18
+ export declare const reactionsResultSchema: z.ZodType<ReactionsResult, ReactionsResult>;
19
+ /**
20
+ * The stored form of an absolute snapshot: zero counts are dropped, and the
21
+ * rest are ordered by count, then paid before emoji before custom emoji, then
22
+ * by emoji or ID, so equal snapshots compare equal. A type listed twice is
23
+ * ambiguous and refused.
24
+ */
25
+ export declare function normalizeReactions(reactions: readonly ReactionCount[]): ReactionCount[];
26
+ /**
27
+ * Replaces a message's aggregate counts. The check of the target, the counts
28
+ * and the update of every subscribed bot commit together, so a poll either
29
+ * sees the new counts in its queue or not at all. An unchanged snapshot queues
30
+ * nothing.
31
+ */
32
+ export declare function setReactions(store: Store, raw: ReactionsInput, now: number): Promise<ReactionsResult>;
33
+ export {};
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { DomainError } from 'emulon';
3
+ import { botSchema } from './bots.js';
4
+ import { chatView, findChannel, messageKey } from './messages.js';
5
+ import { enqueue, queueOf, reactionCountSchema, receives, } from './updates.js';
6
+ export const reactionsInput = z
7
+ .strictObject({
8
+ chatId: z.number().int(),
9
+ messageId: z.number().int().positive().safe(),
10
+ reactions: z.array(reactionCountSchema).max(100),
11
+ });
12
+ export const reactionsResultSchema = z.strictObject({
13
+ chatId: z.number().int(),
14
+ messageId: z.number().int().positive(),
15
+ reactions: z.array(reactionCountSchema),
16
+ changed: z.boolean(),
17
+ queued: z.number().int().nonnegative(),
18
+ });
19
+ const kinds = ['paid', 'emoji', 'custom_emoji'];
20
+ function canonicalType(type) {
21
+ switch (type.type) {
22
+ case 'emoji':
23
+ return { type: 'emoji', emoji: type.emoji };
24
+ case 'custom_emoji':
25
+ return { type: 'custom_emoji', custom_emoji_id: type.custom_emoji_id };
26
+ case 'paid':
27
+ return { type: 'paid' };
28
+ }
29
+ }
30
+ const reactionKey = (type) => `${type.type}:${reactionId(type)}`;
31
+ function reactionId(type) {
32
+ switch (type.type) {
33
+ case 'emoji':
34
+ return type.emoji;
35
+ case 'custom_emoji':
36
+ return type.custom_emoji_id;
37
+ case 'paid':
38
+ return '';
39
+ }
40
+ }
41
+ /**
42
+ * The stored form of an absolute snapshot: zero counts are dropped, and the
43
+ * rest are ordered by count, then paid before emoji before custom emoji, then
44
+ * by emoji or ID, so equal snapshots compare equal. A type listed twice is
45
+ * ambiguous and refused.
46
+ */
47
+ export function normalizeReactions(reactions) {
48
+ const seen = new Set();
49
+ for (const { type } of reactions) {
50
+ const key = reactionKey(type);
51
+ if (seen.has(key)) {
52
+ throw new DomainError('DUPLICATE_REACTION', 'Each reaction type may appear only once.');
53
+ }
54
+ seen.add(key);
55
+ }
56
+ const id = (reaction) => reactionId(reaction.type);
57
+ return reactions
58
+ .filter((reaction) => reaction.total_count > 0)
59
+ .map((reaction) => ({
60
+ type: canonicalType(reaction.type),
61
+ total_count: reaction.total_count,
62
+ }))
63
+ .sort((a, b) => b.total_count - a.total_count ||
64
+ kinds.indexOf(a.type.type) - kinds.indexOf(b.type.type) ||
65
+ (id(a) < id(b) ? -1 : id(a) > id(b) ? 1 : 0));
66
+ }
67
+ function sameReactions(a, b) {
68
+ return a.length === b.length &&
69
+ a.every((reaction, index) => reactionKey(reaction.type) === reactionKey(b[index].type) &&
70
+ reaction.total_count === b[index].total_count);
71
+ }
72
+ const storedSchema = z.strictObject({
73
+ reactions: z.array(reactionCountSchema),
74
+ });
75
+ /**
76
+ * Replaces a message's aggregate counts. The check of the target, the counts
77
+ * and the update of every subscribed bot commit together, so a poll either
78
+ * sees the new counts in its queue or not at all. An unchanged snapshot queues
79
+ * nothing.
80
+ */
81
+ export async function setReactions(store, raw, now) {
82
+ const input = reactionsInput.parse(raw);
83
+ const reactions = normalizeReactions(input.reactions);
84
+ return await store.transaction(async (tx) => {
85
+ const channel = await findChannel(tx, { id: input.chatId });
86
+ if (!channel) {
87
+ throw new DomainError('CHAT_NOT_FOUND', 'No such channel.');
88
+ }
89
+ const key = messageKey(channel.id, input.messageId);
90
+ if (await tx.get('messages', key) === undefined) {
91
+ throw new DomainError('MESSAGE_NOT_FOUND', 'No such message.');
92
+ }
93
+ const stored = await tx.get('reactions', key);
94
+ const previous = stored === undefined
95
+ ? []
96
+ : storedSchema.parse(stored).reactions;
97
+ const result = {
98
+ chatId: channel.id,
99
+ messageId: input.messageId,
100
+ reactions,
101
+ changed: !sameReactions(previous, reactions),
102
+ queued: 0,
103
+ };
104
+ if (!result.changed) {
105
+ return result;
106
+ }
107
+ await tx.put({ collection: 'reactions', id: key, value: { reactions } });
108
+ const bots = (await tx.list('bots'))
109
+ .map((row) => botSchema.parse(row.value))
110
+ .sort((a, b) => a.id - b.id);
111
+ for (const bot of bots) {
112
+ const queue = await queueOf(tx, bot.id);
113
+ if (!receives(queue, 'message_reaction_count')) {
114
+ continue;
115
+ }
116
+ await enqueue(tx, bot.id, queue, (updateId) => ({
117
+ update_id: updateId,
118
+ message_reaction_count: {
119
+ chat: chatView(channel),
120
+ message_id: input.messageId,
121
+ date: Math.floor(now / 1000),
122
+ reactions,
123
+ },
124
+ }));
125
+ result.queued++;
126
+ }
127
+ return result;
128
+ });
129
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod';
2
+ import { type PluginContext } from 'emulon';
3
+ import type { ChatView } from './messages.js';
4
+ type Store = PluginContext['store'];
5
+ type Transaction = Parameters<Parameters<Store['transaction']>[0]>[0];
6
+ /**
7
+ * Every update type typed by `@grammyjs/types@3.28.0`; a test keeps this list
8
+ * equal to those types. Only `message_reaction_count` is ever generated.
9
+ */
10
+ export declare const updateTypes: readonly ["message", "edited_message", "channel_post", "edited_channel_post", "business_connection", "business_message", "edited_business_message", "deleted_business_messages", "guest_message", "message_reaction", "message_reaction_count", "inline_query", "chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer", "my_chat_member", "chat_member", "managed_bot", "chat_join_request", "chat_boost", "removed_chat_boost", "purchased_paid_media"];
11
+ export type UpdateType = typeof updateTypes[number];
12
+ /** The reaction emoji typed by `@grammyjs/types@3.28.0`. */
13
+ export declare const reactionEmoji: readonly ["๐Ÿ‘", "๐Ÿ‘Ž", "โค", "๐Ÿ”ฅ", "๐Ÿฅฐ", "๐Ÿ‘", "๐Ÿ˜", "๐Ÿค”", "๐Ÿคฏ", "๐Ÿ˜ฑ", "๐Ÿคฌ", "๐Ÿ˜ข", "๐ŸŽ‰", "๐Ÿคฉ", "๐Ÿคฎ", "๐Ÿ’ฉ", "๐Ÿ™", "๐Ÿ‘Œ", "๐Ÿ•Š", "๐Ÿคก", "๐Ÿฅฑ", "๐Ÿฅด", "๐Ÿ˜", "๐Ÿณ", "โคโ€๐Ÿ”ฅ", "๐ŸŒš", "๐ŸŒญ", "๐Ÿ’ฏ", "๐Ÿคฃ", "โšก", "๐ŸŒ", "๐Ÿ†", "๐Ÿ’”", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ“", "๐Ÿพ", "๐Ÿ’‹", "๐Ÿ–•", "๐Ÿ˜ˆ", "๐Ÿ˜ด", "๐Ÿ˜ญ", "๐Ÿค“", "๐Ÿ‘ป", "๐Ÿ‘จโ€๐Ÿ’ป", "๐Ÿ‘€", "๐ŸŽƒ", "๐Ÿ™ˆ", "๐Ÿ˜‡", "๐Ÿ˜จ", "๐Ÿค", "โœ", "๐Ÿค—", "๐Ÿซก", "๐ŸŽ…", "๐ŸŽ„", "โ˜ƒ", "๐Ÿ’…", "๐Ÿคช", "๐Ÿ—ฟ", "๐Ÿ†’", "๐Ÿ’˜", "๐Ÿ™‰", "๐Ÿฆ„", "๐Ÿ˜˜", "๐Ÿ’Š", "๐Ÿ™Š", "๐Ÿ˜Ž", "๐Ÿ‘พ", "๐Ÿคทโ€โ™‚", "๐Ÿคท", "๐Ÿคทโ€โ™€", "๐Ÿ˜ก"];
14
+ export type ReactionType = {
15
+ type: 'emoji';
16
+ emoji: typeof reactionEmoji[number];
17
+ } | {
18
+ type: 'custom_emoji';
19
+ custom_emoji_id: string;
20
+ } | {
21
+ type: 'paid';
22
+ };
23
+ export interface ReactionCount {
24
+ type: ReactionType;
25
+ total_count: number;
26
+ }
27
+ export declare const reactionTypeSchema: z.ZodType<ReactionType, ReactionType>;
28
+ export declare const reactionCountSchema: z.ZodType<ReactionCount, ReactionCount>;
29
+ export interface ReactionCountUpdate {
30
+ update_id: number;
31
+ message_reaction_count: {
32
+ chat: ChatView;
33
+ message_id: number;
34
+ date: number;
35
+ reactions: ReactionCount[];
36
+ };
37
+ }
38
+ export declare const updateSchema: z.ZodType<ReactionCountUpdate, ReactionCountUpdate>;
39
+ /** A rejected `getUpdates`; descriptions are fixed and never echo input. */
40
+ export declare class UpdatesError extends Error {
41
+ readonly status: 400 | 409 | 501 | 503;
42
+ readonly description: string;
43
+ constructor(status: 400 | 409 | 501 | 503, description: string);
44
+ }
45
+ export declare const updatesErrors: {
46
+ readonly invalid: () => UpdatesError;
47
+ readonly limit: () => UpdatesError;
48
+ readonly allowedUpdates: () => UpdatesError;
49
+ readonly unsupported: () => UpdatesError;
50
+ readonly conflict: () => UpdatesError;
51
+ readonly cancelled: () => UpdatesError;
52
+ };
53
+ export interface UpdatesRequest {
54
+ offset?: number;
55
+ limit: number;
56
+ timeout: number;
57
+ allowedUpdates?: UpdateType[];
58
+ }
59
+ /** Only the documented polling fields; anything else is refused, not ignored. */
60
+ export declare function updatesRequest(params: Record<string, unknown>): UpdatesRequest;
61
+ /**
62
+ * Per-bot queue state. An empty `allowedUpdates` is Telegram's default, which
63
+ * leaves out `message_reaction_count`.
64
+ */
65
+ export interface Queue {
66
+ nextUpdateId: number;
67
+ allowedUpdates: UpdateType[];
68
+ }
69
+ export declare function queueOf(tx: Transaction, botId: number): Promise<Queue>;
70
+ export declare function receives(queue: Queue, type: UpdateType): boolean;
71
+ /** The bot's queued updates in ID order. */
72
+ export declare function queued(tx: Transaction, botId: number): Promise<ReactionCountUpdate[]>;
73
+ /** Allocates the next update ID of one bot and queues the update it builds. */
74
+ export declare function enqueue(tx: Transaction, botId: number, queue: Queue, build: (updateId: number) => ReactionCountUpdate): Promise<void>;
75
+ /**
76
+ * The first read of a `getUpdates` call: it confirms by offset, replaces the
77
+ * subscription when one is given, then returns the head of the queue without
78
+ * confirming it.
79
+ */
80
+ export declare function takeUpdates(tx: Transaction, botId: number, request: Pick<UpdatesRequest, 'offset' | 'limit' | 'allowedUpdates'>): Promise<ReactionCountUpdate[]>;
81
+ /**
82
+ * Waits on real time for committed updates. The store listener is attached
83
+ * before every read, so a commit between the read and the wait is not lost.
84
+ * Cancellation is an error, never an empty batch: after a reset the caller
85
+ * must not mistake a cancelled poll for a drained queue.
86
+ */
87
+ export declare function pollUpdates(store: Store, botId: number, request: UpdatesRequest, signal: AbortSignal): Promise<ReactionCountUpdate[]>;
88
+ export declare const inspectInput: z.ZodType<InspectInput, InspectInput>;
89
+ export interface InspectInput {
90
+ botId: number;
91
+ limit?: number | undefined;
92
+ }
93
+ export interface QueueView {
94
+ botId: number;
95
+ allowedUpdates: UpdateType[];
96
+ pending: number;
97
+ updates: ReactionCountUpdate[];
98
+ }
99
+ export declare const queueViewSchema: z.ZodType<QueueView, QueueView>;
100
+ /**
101
+ * The oldest `limit` pending updates (default 100) and the subscription of one
102
+ * bot. Reading confirms nothing and shows no credential.
103
+ */
104
+ export declare function inspectUpdates(store: Store, raw: InspectInput): Promise<QueueView>;
105
+ export {};