@moxxy/plugin-telegram 0.27.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/LICENSE +21 -0
- package/dist/approval.d.ts +37 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +75 -0
- package/dist/approval.js.map +1 -0
- package/dist/channel/approval-prompt.d.ts +20 -0
- package/dist/channel/approval-prompt.d.ts.map +1 -0
- package/dist/channel/approval-prompt.js +34 -0
- package/dist/channel/approval-prompt.js.map +1 -0
- package/dist/channel/callback-handler.d.ts +32 -0
- package/dist/channel/callback-handler.d.ts.map +1 -0
- package/dist/channel/callback-handler.js +212 -0
- package/dist/channel/callback-handler.js.map +1 -0
- package/dist/channel/frame-pump.d.ts +58 -0
- package/dist/channel/frame-pump.d.ts.map +1 -0
- package/dist/channel/frame-pump.js +159 -0
- package/dist/channel/frame-pump.js.map +1 -0
- package/dist/channel/html.d.ts +24 -0
- package/dist/channel/html.d.ts.map +1 -0
- package/dist/channel/html.js +62 -0
- package/dist/channel/html.js.map +1 -0
- package/dist/channel/pairing-handler.d.ts +66 -0
- package/dist/channel/pairing-handler.d.ts.map +1 -0
- package/dist/channel/pairing-handler.js +147 -0
- package/dist/channel/pairing-handler.js.map +1 -0
- package/dist/channel/permission-prompt.d.ts +21 -0
- package/dist/channel/permission-prompt.d.ts.map +1 -0
- package/dist/channel/permission-prompt.js +36 -0
- package/dist/channel/permission-prompt.js.map +1 -0
- package/dist/channel/slash-handler.d.ts +47 -0
- package/dist/channel/slash-handler.d.ts.map +1 -0
- package/dist/channel/slash-handler.js +214 -0
- package/dist/channel/slash-handler.js.map +1 -0
- package/dist/channel/text-handler.d.ts +43 -0
- package/dist/channel/text-handler.d.ts.map +1 -0
- package/dist/channel/text-handler.js +116 -0
- package/dist/channel/text-handler.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +40 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +67 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel/typing-indicator.d.ts +14 -0
- package/dist/channel/typing-indicator.d.ts.map +1 -0
- package/dist/channel/typing-indicator.js +29 -0
- package/dist/channel/typing-indicator.js.map +1 -0
- package/dist/channel/voice-handler.d.ts +42 -0
- package/dist/channel/voice-handler.d.ts.map +1 -0
- package/dist/channel/voice-handler.js +138 -0
- package/dist/channel/voice-handler.js.map +1 -0
- package/dist/channel.d.ts +118 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +478 -0
- package/dist/channel.js.map +1 -0
- package/dist/channel.test-d.d.ts +2 -0
- package/dist/channel.test-d.d.ts.map +1 -0
- package/dist/channel.test-d.js +5 -0
- package/dist/channel.test-d.js.map +1 -0
- package/dist/format.d.ts +38 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +237 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +37 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +42 -0
- package/dist/keys.js.map +1 -0
- package/dist/pair-flow.d.ts +18 -0
- package/dist/pair-flow.d.ts.map +1 -0
- package/dist/pair-flow.js +119 -0
- package/dist/pair-flow.js.map +1 -0
- package/dist/pairing.d.ts +90 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +97 -0
- package/dist/pairing.js.map +1 -0
- package/dist/permission.d.ts +25 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +49 -0
- package/dist/permission.js.map +1 -0
- package/dist/render.d.ts +68 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +439 -0
- package/dist/render.js.map +1 -0
- package/dist/setup-wizard.d.ts +21 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +140 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +91 -0
- package/src/approval.test.ts +103 -0
- package/src/approval.ts +86 -0
- package/src/channel/approval-prompt.ts +46 -0
- package/src/channel/callback-handler.test.ts +188 -0
- package/src/channel/callback-handler.ts +256 -0
- package/src/channel/frame-pump.ts +176 -0
- package/src/channel/html.test.ts +40 -0
- package/src/channel/html.ts +60 -0
- package/src/channel/pairing-handler.ts +181 -0
- package/src/channel/permission-prompt.ts +51 -0
- package/src/channel/slash-handler.ts +259 -0
- package/src/channel/text-handler.test.ts +222 -0
- package/src/channel/text-handler.ts +174 -0
- package/src/channel/turn-runner.ts +102 -0
- package/src/channel/typing-indicator.ts +31 -0
- package/src/channel/voice-handler.test.ts +328 -0
- package/src/channel/voice-handler.ts +198 -0
- package/src/channel/voice-reply.test.ts +223 -0
- package/src/channel.test-d.ts +7 -0
- package/src/channel.ts +564 -0
- package/src/format.test.ts +159 -0
- package/src/format.ts +257 -0
- package/src/index.ts +347 -0
- package/src/keys.ts +52 -0
- package/src/pair-flow.ts +131 -0
- package/src/pairing.test.ts +113 -0
- package/src/pairing.ts +184 -0
- package/src/permission.test.ts +56 -0
- package/src/permission.ts +61 -0
- package/src/render.test.ts +350 -0
- package/src/render.ts +522 -0
- package/src/setup-wizard.ts +178 -0
- package/src/subcommands.test.ts +153 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard cap on an inbound audio file we will buffer into memory before
|
|
3
|
+
* transcribing. Telegram allows ~20MB uploads via getFile, and the handler
|
|
4
|
+
* accepts arbitrary uploaded `message:audio` (not just short voice notes), so
|
|
5
|
+
* an authorized-but-hostile chat could otherwise push a large file we fully
|
|
6
|
+
* buffer. 20MB matches Telegram's own getFile ceiling.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_AUDIO_BYTES = 20 * 1024 * 1024;
|
|
9
|
+
/** Abort the file download if Telegram's CDN hasn't responded in this window. */
|
|
10
|
+
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
11
|
+
/**
|
|
12
|
+
* Handle inbound voice notes and uploaded audio files. Authorization
|
|
13
|
+
* gate matches the text path. If no Transcriber is registered on the
|
|
14
|
+
* session, reply with a one-time guidance message instead of silently
|
|
15
|
+
* swallowing the audio. Otherwise: download via Bot-API file URL,
|
|
16
|
+
* transcribe through the active Transcriber, echo a short "heard:"
|
|
17
|
+
* confirmation, then run a normal user turn with the transcript.
|
|
18
|
+
*/
|
|
19
|
+
export async function handleVoiceMessage(ctx, state, deps, cb) {
|
|
20
|
+
const chatId = ctx.chat?.id;
|
|
21
|
+
if (!chatId)
|
|
22
|
+
return;
|
|
23
|
+
// Pull whichever audio variant Telegram delivered. `voice` is a press-
|
|
24
|
+
// and-hold voice note; `audio` is an uploaded audio file. Both expose
|
|
25
|
+
// a `file_id` and (for voice) a mime_type — we trust voice's
|
|
26
|
+
// `audio/ogg` default when missing.
|
|
27
|
+
const voice = ctx.message?.voice;
|
|
28
|
+
const audio = ctx.message?.audio;
|
|
29
|
+
const media = voice ?? audio;
|
|
30
|
+
if (!media)
|
|
31
|
+
return;
|
|
32
|
+
if (!deps.pairing.isAuthorized(chatId)) {
|
|
33
|
+
await ctx.reply('This bot is paired with a different chat (or not paired yet). Run `moxxy telegram pair` to (re-)pair.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (state.busy) {
|
|
37
|
+
await ctx.reply('I am still working on the previous prompt. Send /cancel to abort it.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!state.session) {
|
|
41
|
+
await ctx.reply('Session is not ready yet.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const transcriber = state.session.transcribers.tryGetActive();
|
|
45
|
+
if (!transcriber) {
|
|
46
|
+
await ctx.reply("Heard a voice note, but no speech-to-text backend is configured. Install @moxxy/plugin-stt-whisper and run `moxxy login openai` (or set OPENAI_API_KEY) to enable voice input.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Reject oversized uploads up-front using the size Telegram reports on the
|
|
50
|
+
// voice/audio object, before we spend a download or any memory on it.
|
|
51
|
+
const declaredSize = media.file_size;
|
|
52
|
+
if (typeof declaredSize === 'number' && declaredSize > MAX_AUDIO_BYTES) {
|
|
53
|
+
await ctx.reply(`That audio is too large (${Math.round(declaredSize / (1024 * 1024))}MB). The limit is ${MAX_AUDIO_BYTES / (1024 * 1024)}MB.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let fileInfo;
|
|
57
|
+
try {
|
|
58
|
+
fileInfo = await ctx.api.getFile(media.file_id);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
deps.logger?.warn('telegram getFile failed', {
|
|
62
|
+
err: err instanceof Error ? err.message : String(err),
|
|
63
|
+
});
|
|
64
|
+
await ctx.reply('Could not look up that voice note with Telegram.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!fileInfo.file_path) {
|
|
68
|
+
await ctx.reply('Telegram did not return a downloadable file path for that voice note.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const url = `https://api.telegram.org/file/bot${deps.token}/${fileInfo.file_path}`;
|
|
72
|
+
const fetcher = cb.fetchAudio ?? ((u, init) => fetch(u, init));
|
|
73
|
+
// Bound the download with an AbortSignal timeout so a hung/slow Telegram CDN
|
|
74
|
+
// connection can't block the handler indefinitely.
|
|
75
|
+
const ac = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => ac.abort(), DOWNLOAD_TIMEOUT_MS);
|
|
77
|
+
let bytes;
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetcher(url, { signal: ac.signal });
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
deps.logger?.warn('telegram voice download failed', {
|
|
82
|
+
...(response.status !== undefined ? { status: response.status } : {}),
|
|
83
|
+
...(response.statusText ? { statusText: response.statusText } : {}),
|
|
84
|
+
});
|
|
85
|
+
await ctx.reply('Failed to download the voice note from Telegram.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Trust the Content-Length header (when present) to bail before buffering a
|
|
89
|
+
// body that lies about its size on the voice/audio object.
|
|
90
|
+
const declaredLen = Number(response.headers?.get('content-length') ?? '');
|
|
91
|
+
if (Number.isFinite(declaredLen) && declaredLen > MAX_AUDIO_BYTES) {
|
|
92
|
+
await ctx.reply('That audio is too large to transcribe.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const buf = await response.arrayBuffer();
|
|
96
|
+
if (buf.byteLength > MAX_AUDIO_BYTES) {
|
|
97
|
+
await ctx.reply('That audio is too large to transcribe.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
bytes = new Uint8Array(buf);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
deps.logger?.warn('telegram voice fetch failed', {
|
|
104
|
+
err: err instanceof Error ? err.message : String(err),
|
|
105
|
+
});
|
|
106
|
+
await ctx.reply(ac.signal.aborted
|
|
107
|
+
? 'Downloading the voice note timed out.'
|
|
108
|
+
: 'Failed to download the voice note from Telegram.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
114
|
+
// Voice notes are OGG/Opus by default; uploaded audio carries its own mime.
|
|
115
|
+
const mimeType = media.mime_type ?? (voice ? 'audio/ogg' : 'audio/mpeg');
|
|
116
|
+
let transcript;
|
|
117
|
+
try {
|
|
118
|
+
const result = await transcriber.transcribe(bytes, { mimeType });
|
|
119
|
+
transcript = result.text.trim();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
deps.logger?.warn('telegram voice transcription failed', {
|
|
123
|
+
err: err instanceof Error ? err.message : String(err),
|
|
124
|
+
});
|
|
125
|
+
await ctx.reply(`Transcription failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!transcript) {
|
|
129
|
+
await ctx.reply('Could not transcribe the voice note (got empty text).');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Echo what we heard so the user can spot misrecognitions before the
|
|
133
|
+
// agent acts on it. Italics keep it visually distinct from a normal
|
|
134
|
+
// reply.
|
|
135
|
+
await ctx.reply(`_heard:_ ${transcript}`, { parse_mode: 'Markdown' });
|
|
136
|
+
await cb.runUserTurn(ctx, chatId, transcript);
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=voice-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-handler.js","sourceRoot":"","sources":["../../src/channel/voice-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
2
|
+
import type { Channel, ChannelHandle, ChannelStartOptsBase } from '@moxxy/sdk';
|
|
3
|
+
import type { VaultStore } from '@moxxy/plugin-vault';
|
|
4
|
+
import { TelegramPermissionResolver } from './permission.js';
|
|
5
|
+
import { TelegramApprovalResolver } from './approval.js';
|
|
6
|
+
import { PairingHandler } from './channel/pairing-handler.js';
|
|
7
|
+
export type { PairingConfirmResult } from './channel/pairing-handler.js';
|
|
8
|
+
export interface TelegramStartOpts extends ChannelStartOptsBase {
|
|
9
|
+
readonly session: Session;
|
|
10
|
+
/**
|
|
11
|
+
* If true (and no chat is paired yet), open a host-issued QR pairing window on
|
|
12
|
+
* startup: mint a code, publish a `t.me/<bot>?start=<code>` deep link as the
|
|
13
|
+
* connect value, and pair whichever chat presents that code back (deep-link tap
|
|
14
|
+
* or a plain 6-digit message). Set by the `moxxy channels telegram pair` command
|
|
15
|
+
* (which renders the deep link as a terminal QR and waits via `onPaired`).
|
|
16
|
+
*/
|
|
17
|
+
readonly pair?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The channel is running on its own dedicated runner under a GUI control
|
|
20
|
+
* surface (the desktop Channels panel) rather than a terminal. Equivalent to
|
|
21
|
+
* `pair` for the unpaired case — `start()` opens the same host-issued QR
|
|
22
|
+
* pairing window instead of throwing — so the desktop pairs with the identical
|
|
23
|
+
* mechanism. Threaded by the CLI from `ChannelDef.dedicatedRunner` /
|
|
24
|
+
* `--dedicated` / `MOXXY_DEDICATED_RUNNER`.
|
|
25
|
+
*/
|
|
26
|
+
readonly dedicated?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface TelegramChannelOptions {
|
|
29
|
+
readonly vault: VaultStore;
|
|
30
|
+
readonly token?: string;
|
|
31
|
+
readonly logger?: {
|
|
32
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
33
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
34
|
+
};
|
|
35
|
+
readonly editFrameMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare class TelegramChannel implements Channel<TelegramStartOpts> {
|
|
38
|
+
readonly name = "telegram";
|
|
39
|
+
readonly permissionResolver: TelegramPermissionResolver;
|
|
40
|
+
readonly approvalResolver: TelegramApprovalResolver;
|
|
41
|
+
private readonly opts;
|
|
42
|
+
private bot;
|
|
43
|
+
private botLink;
|
|
44
|
+
private hostPairingCode;
|
|
45
|
+
private readonly connectListeners;
|
|
46
|
+
private currentChatId;
|
|
47
|
+
private lastChatId;
|
|
48
|
+
private logUnsub;
|
|
49
|
+
private session;
|
|
50
|
+
private model;
|
|
51
|
+
private activeModelOverride;
|
|
52
|
+
private yolo;
|
|
53
|
+
private voiceReplies;
|
|
54
|
+
private readonly turns;
|
|
55
|
+
private awaitingApprovalText;
|
|
56
|
+
private handle;
|
|
57
|
+
private readonly framePump;
|
|
58
|
+
private readonly typing;
|
|
59
|
+
private readonly pairing;
|
|
60
|
+
constructor(opts: TelegramChannelOptions);
|
|
61
|
+
/** This channel's connect value (see {@link Channel.requestUrl}): the resolved
|
|
62
|
+
* bot's `t.me` link, surfaced by control surfaces as a QR / open-bot step.
|
|
63
|
+
* While a host pairing window is open it includes the `?start=<code>` payload. */
|
|
64
|
+
get requestUrl(): string | null;
|
|
65
|
+
/** Whether the "connect the other side" step is satisfied — i.e. a chat is
|
|
66
|
+
* paired (see {@link Channel.connected}). A host shows "✓ Connected" instead
|
|
67
|
+
* of the pairing QR once this is true. */
|
|
68
|
+
get connected(): boolean;
|
|
69
|
+
private notifyConnectChange;
|
|
70
|
+
start(startOpts: TelegramStartOpts): Promise<ChannelHandle>;
|
|
71
|
+
pairingPhase(): ReturnType<PairingHandler['phase']>;
|
|
72
|
+
unpair(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Subscribe to "a chat just paired" — fires once when the host-issued QR
|
|
75
|
+
* pairing completes. The `moxxy channels telegram pair` command uses this to
|
|
76
|
+
* print success and stop waiting; returns an unsubscribe function.
|
|
77
|
+
*/
|
|
78
|
+
onPaired(listener: (chatId: number) => void): () => void;
|
|
79
|
+
private handleVoice;
|
|
80
|
+
private handleText;
|
|
81
|
+
/** Persist + apply the voice-replies preference (the `/voice` toggle). */
|
|
82
|
+
private setVoiceReplies;
|
|
83
|
+
/**
|
|
84
|
+
* Speak the final assistant reply as a voice note, when enabled. Best-effort
|
|
85
|
+
* and fully isolated (never throws): synthesize via the session's active
|
|
86
|
+
* Synthesizer, transcode to OGG/Opus (or send plain audio when ffmpeg is
|
|
87
|
+
* unavailable), and deliver via grammy. The text reply already went out.
|
|
88
|
+
*/
|
|
89
|
+
private sendVoiceReply;
|
|
90
|
+
/**
|
|
91
|
+
* Plain-message fallback for host-issued pairing: if a host window is open and
|
|
92
|
+
* an unauthorized chat sends exactly the 6-digit code, pair it (covers clients
|
|
93
|
+
* that don't auto-deliver the `?start=` deep-link payload). Returns true when
|
|
94
|
+
* the message was a pairing attempt we handled (paired, or a wrong-code reply)
|
|
95
|
+
* so the caller doesn't also emit the generic "not paired" rejection.
|
|
96
|
+
*/
|
|
97
|
+
private tryHostPair;
|
|
98
|
+
/**
|
|
99
|
+
* Run a handler promise detached from the grammy poll loop. grammy awaits the
|
|
100
|
+
* value a middleware returns before fetching the next update batch; returning
|
|
101
|
+
* void here (instead of the handler promise) lets the loop keep delivering
|
|
102
|
+
* callback_query / `/cancel` while a turn runs. Errors are logged here because
|
|
103
|
+
* `bot.catch` only sees rejections of the AWAITED middleware chain.
|
|
104
|
+
*/
|
|
105
|
+
private dispatchInBackground;
|
|
106
|
+
private runUserTurn;
|
|
107
|
+
/**
|
|
108
|
+
* Post the assistant's prose for a turn this channel did not initiate. Gated
|
|
109
|
+
* by `!busy` (our own turns are rendered by the FramePump from the runUserTurn
|
|
110
|
+
* iterator) and by having served a chat at least once. Sent as plain text to
|
|
111
|
+
* avoid parse-mode pitfalls; the view itself lives on the web surface.
|
|
112
|
+
*/
|
|
113
|
+
private mirrorForeignTurn;
|
|
114
|
+
private askForPermission;
|
|
115
|
+
private askForApproval;
|
|
116
|
+
private dispatchCallback;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|