@s0nderlabs/anima-plugin-telegram 0.19.18 → 0.19.19
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 +2 -2
- package/src/approval-resolution.test.ts +25 -0
- package/src/index.ts +6 -1
- package/src/listener.ts +35 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s0nderlabs/anima-plugin-telegram",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.19",
|
|
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.
|
|
31
|
+
"@s0nderlabs/anima-core": "0.19.19",
|
|
32
32
|
"grammy": "^1.42.0",
|
|
33
33
|
"zod": "^3.23.8"
|
|
34
34
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { formatApprovalResolution } from './listener'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regression test for v0.19.19: every choice maps to a human-readable
|
|
6
|
+
* suffix so the post-click modal edit shows the operator what they tapped.
|
|
7
|
+
* v0.19.18 left modals visible after click because the listener only
|
|
8
|
+
* answered the popup but never edited the message body. v0.19.19 edits the
|
|
9
|
+
* text to append this suffix and removes the inline keyboard.
|
|
10
|
+
*/
|
|
11
|
+
describe('formatApprovalResolution', () => {
|
|
12
|
+
it('labels each choice distinctly with the clicker user id', () => {
|
|
13
|
+
expect(formatApprovalResolution('once', 42)).toBe('✅ Allowed once (by 42)')
|
|
14
|
+
expect(formatApprovalResolution('session', 42)).toBe('✅ Allowed for session (by 42)')
|
|
15
|
+
expect(formatApprovalResolution('always', 42)).toBe('✅ Always allowed (by 42)')
|
|
16
|
+
expect(formatApprovalResolution('deny', 42)).toBe('❌ Denied (by 42)')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('uses ✅ for permitting choices and ❌ only for deny', () => {
|
|
20
|
+
for (const choice of ['once', 'session', 'always'] as const) {
|
|
21
|
+
expect(formatApprovalResolution(choice, 1)).toMatch(/^✅/)
|
|
22
|
+
}
|
|
23
|
+
expect(formatApprovalResolution('deny', 1)).toMatch(/^❌/)
|
|
24
|
+
})
|
|
25
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,12 @@ export type {
|
|
|
27
27
|
TelegramToolEvent,
|
|
28
28
|
} from './types'
|
|
29
29
|
export { ProgressTracker, PROGRESS_EDIT_INTERVAL } from './progress'
|
|
30
|
-
export {
|
|
30
|
+
export {
|
|
31
|
+
TelegramListener,
|
|
32
|
+
TELEGRAM_ALLOWED_UPDATES,
|
|
33
|
+
capForTelegram,
|
|
34
|
+
formatApprovalResolution,
|
|
35
|
+
} from './listener'
|
|
31
36
|
export { buildSessionKey, sanitizeAgentName } from './session-key'
|
|
32
37
|
export { formatTelegramChannel, formatInboundPreview } from './format'
|
|
33
38
|
export { RateLimiter } from './limits'
|
package/src/listener.ts
CHANGED
|
@@ -38,6 +38,23 @@ import { startTypingLoop } from './typing'
|
|
|
38
38
|
const RETRY_INTERVAL_MS = 30_000
|
|
39
39
|
const MAX_LOCK_RETRY_ATTEMPTS = 12
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Map an `ApprovalChoice` to the human-readable resolution label appended
|
|
43
|
+
* to the approval message after a click. Mirrors the keyboard labels so
|
|
44
|
+
* operators see the same wording in the resolved message that they tapped.
|
|
45
|
+
*/
|
|
46
|
+
export function formatApprovalResolution(choice: ApprovalChoice, byUserId: number): string {
|
|
47
|
+
const label =
|
|
48
|
+
choice === 'once'
|
|
49
|
+
? '✅ Allowed once'
|
|
50
|
+
: choice === 'session'
|
|
51
|
+
? '✅ Allowed for session'
|
|
52
|
+
: choice === 'always'
|
|
53
|
+
? '✅ Always allowed'
|
|
54
|
+
: '❌ Denied'
|
|
55
|
+
return `${label} (by ${byUserId})`
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
/**
|
|
42
59
|
* Update kinds we ask Telegram to deliver via long-poll.
|
|
43
60
|
*
|
|
@@ -136,6 +153,24 @@ export class TelegramListener {
|
|
|
136
153
|
} catch {
|
|
137
154
|
/* ignore */
|
|
138
155
|
}
|
|
156
|
+
// Resolve the modal visually: append the choice + drop the inline
|
|
157
|
+
// keyboard. Without this, every clicked approval message stays on
|
|
158
|
+
// screen with all four buttons, leaving operators unsure whether
|
|
159
|
+
// their tap registered. Best-effort — if the edit fails (rate
|
|
160
|
+
// limit, message age, deleted), the underlying approval is still
|
|
161
|
+
// resolved at the runtime level, so we swallow the error.
|
|
162
|
+
const originalText =
|
|
163
|
+
typeof q.message?.text === 'string' && q.message.text.length > 0 ? q.message.text : null
|
|
164
|
+
const suffix = formatApprovalResolution(parsed.choice, q.from.id)
|
|
165
|
+
try {
|
|
166
|
+
if (originalText) {
|
|
167
|
+
await ctx.editMessageText(`${originalText}\n\n${suffix}`, { reply_markup: undefined })
|
|
168
|
+
} else {
|
|
169
|
+
await ctx.editMessageReplyMarkup({ reply_markup: undefined })
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore — modal was clicked, runtime already resolved; keyboard cleanup is cosmetic */
|
|
173
|
+
}
|
|
139
174
|
}
|
|
140
175
|
|
|
141
176
|
async start(): Promise<void> {
|