@s0nderlabs/anima-plugin-telegram 0.19.9 → 0.19.10

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/listener.ts +56 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s0nderlabs/anima-plugin-telegram",
3
- "version": "0.19.9",
3
+ "version": "0.19.10",
4
4
  "type": "module",
5
5
  "description": "Telegram gateway plugin for anima — long-poll bot, debounced dispatch, reactions, allowlist",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "test": "bun test"
29
29
  },
30
30
  "dependencies": {
31
- "@s0nderlabs/anima-core": "0.19.9",
31
+ "@s0nderlabs/anima-core": "0.19.10",
32
32
  "grammy": "^1.42.0",
33
33
  "zod": "^3.23.8"
34
34
  }
package/src/listener.ts CHANGED
@@ -49,6 +49,9 @@ export class TelegramListener {
49
49
  private running = false
50
50
  private tokenLock: TokenLock | null = null
51
51
  private refreshTimer: ReturnType<typeof setInterval> | null = null
52
+ private approvalResolver:
53
+ | ((approvalId: string, choice: ApprovalChoice, fromUserId: number) => void)
54
+ | null = null
52
55
 
53
56
  constructor(opts: TelegramListenerOpts) {
54
57
  this.opts = opts
@@ -58,12 +61,54 @@ export class TelegramListener {
58
61
  quietPeriodMs: opts.debounceMs,
59
62
  })
60
63
  this.bot.on('message', ctx => this.onMessage(ctx))
64
+ // Register callback_query handler at construction time. grammY rejects
65
+ // late `bot.on()` registration once polling starts, so any approval
66
+ // resolver wiring must happen via the `approvalResolver` slot, not by
67
+ // calling `bot.on()` again. See approvalBridge.installCallbackHandler.
68
+ this.bot.on('callback_query:data', ctx => this.handleCallbackQuery(ctx))
61
69
  this.bot.catch(err => {
62
70
  const msg = err instanceof Error ? err.message : String(err)
63
71
  this.log(`grammy.catch: ${msg.slice(0, 200)}`)
64
72
  })
65
73
  }
66
74
 
75
+ private async handleCallbackQuery(ctx: Context): Promise<void> {
76
+ const q = ctx.callbackQuery
77
+ if (!q) return
78
+ const parsed = parseCallbackData(q.data)
79
+ if (!parsed) {
80
+ try {
81
+ await ctx.answerCallbackQuery({ text: 'malformed approval callback' })
82
+ } catch {
83
+ /* ignore */
84
+ }
85
+ return
86
+ }
87
+ if (this.opts.allowedUserIds.length > 0 && !this.opts.allowedUserIds.includes(q.from.id)) {
88
+ try {
89
+ await ctx.answerCallbackQuery({ text: '⛔ You are not authorized to approve commands.' })
90
+ } catch {
91
+ /* ignore */
92
+ }
93
+ return
94
+ }
95
+ const resolver = this.approvalResolver
96
+ if (!resolver) {
97
+ try {
98
+ await ctx.answerCallbackQuery({ text: 'no approval pending' })
99
+ } catch {
100
+ /* ignore */
101
+ }
102
+ return
103
+ }
104
+ resolver(parsed.approvalId, parsed.choice, q.from.id)
105
+ try {
106
+ await ctx.answerCallbackQuery({ text: `✓ ${parsed.choice}` })
107
+ } catch {
108
+ /* ignore */
109
+ }
110
+ }
111
+
67
112
  async start(): Promise<void> {
68
113
  if (this.running) return
69
114
 
@@ -169,44 +214,20 @@ export class TelegramListener {
169
214
  }
170
215
 
171
216
  /**
172
- * Register a single callback_query dispatcher. Parses + re-validates the
173
- * clicker against `allowedUserIds`, then forwards to the caller's resolver
174
- * which owns the pending-approval Map. grammy doesn't expose middleware
175
- * unregister, so we no-op the returned function and rely on bot.stop()
176
- * for teardown.
217
+ * Register the caller's approval resolver. The actual `bot.on('callback_query:data', ...)`
218
+ * middleware is installed once in the constructor (grammY rejects late
219
+ * registration after polling starts, so we cannot wire the handler lazily
220
+ * inside a dispatch turn). This method just swaps the resolver slot the
221
+ * pre-installed handler reads from. Returns a no-op uninstaller for
222
+ * back-compat with the previous API; teardown happens via `bot.stop()`.
177
223
  */
178
224
  private installCallbackHandler(
179
225
  onResolve: (approvalId: string, choice: ApprovalChoice, fromUserId: number) => void,
180
226
  ): () => void {
181
- const handler = async (ctx: Context): Promise<void> => {
182
- const q = ctx.callbackQuery
183
- if (!q) return
184
- const parsed = parseCallbackData(q.data)
185
- if (!parsed) {
186
- try {
187
- await ctx.answerCallbackQuery({ text: 'malformed approval callback' })
188
- } catch {
189
- /* ignore */
190
- }
191
- return
192
- }
193
- if (this.opts.allowedUserIds.length > 0 && !this.opts.allowedUserIds.includes(q.from.id)) {
194
- try {
195
- await ctx.answerCallbackQuery({ text: '⛔ You are not authorized to approve commands.' })
196
- } catch {
197
- /* ignore */
198
- }
199
- return
200
- }
201
- onResolve(parsed.approvalId, parsed.choice, q.from.id)
202
- try {
203
- await ctx.answerCallbackQuery({ text: `✓ ${parsed.choice}` })
204
- } catch {
205
- /* ignore */
206
- }
227
+ this.approvalResolver = onResolve
228
+ return () => {
229
+ this.approvalResolver = null
207
230
  }
208
- this.bot.on('callback_query:data', handler)
209
- return () => {}
210
231
  }
211
232
 
212
233
  /**
@@ -316,7 +337,8 @@ export class TelegramListener {
316
337
  } catch (err) {
317
338
  ok = false
318
339
  const msg = err instanceof Error ? err.message : String(err)
319
- this.log(`dispatch failed: ${msg.slice(0, 200)}`)
340
+ const stack = err instanceof Error && err.stack ? `\n${err.stack}` : ''
341
+ console.error(`[telegram] dispatch failed: ${msg.slice(0, 500)}${stack}`)
320
342
  void reactError(this.bot, chatId, messageId)
321
343
  try {
322
344
  await this.bot.api.sendMessage(