@hera-al/server 1.6.6 → 1.6.11
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/dist/agent/agent-service.d.ts +1 -0
- package/dist/agent/agent-service.js +1 -1
- package/dist/agent/session-agent.d.ts +4 -2
- package/dist/agent/session-agent.js +1 -1
- package/dist/agent/session-db.d.ts +2 -0
- package/dist/agent/session-db.js +1 -1
- package/dist/commands/command.d.ts +7 -0
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -1
- package/dist/config.d.ts +170 -1
- package/dist/config.js +1 -1
- package/dist/cron/cron-service.d.ts +36 -2
- package/dist/cron/cron-service.js +1 -1
- package/dist/cron/types.d.ts +6 -0
- package/dist/gateway/bridge.d.ts +1 -1
- package/dist/gateway/channel-manager.d.ts +1 -1
- package/dist/gateway/channel-manager.js +1 -1
- package/dist/gateway/channels/telegram/config-types.d.ts +93 -0
- package/dist/gateway/channels/telegram/config-types.js +1 -0
- package/dist/gateway/channels/telegram/edit-delete.d.ts +73 -0
- package/dist/gateway/channels/telegram/edit-delete.js +1 -0
- package/dist/gateway/channels/telegram/error-handling.d.ts +63 -0
- package/dist/gateway/channels/telegram/error-handling.js +1 -0
- package/dist/gateway/channels/{telegram.d.ts → telegram/index.d.ts} +23 -11
- package/dist/gateway/channels/telegram/index.js +1 -0
- package/dist/gateway/channels/telegram/inline-buttons.d.ts +60 -0
- package/dist/gateway/channels/telegram/inline-buttons.js +1 -0
- package/dist/gateway/channels/telegram/polls.d.ts +50 -0
- package/dist/gateway/channels/telegram/polls.js +1 -0
- package/dist/gateway/channels/telegram/reactions.d.ts +56 -0
- package/dist/gateway/channels/telegram/reactions.js +1 -0
- package/dist/gateway/channels/telegram/retry-policy.d.ts +28 -0
- package/dist/gateway/channels/telegram/retry-policy.js +1 -0
- package/dist/gateway/channels/telegram/send.d.ts +55 -0
- package/dist/gateway/channels/telegram/send.js +1 -0
- package/dist/gateway/channels/telegram/stickers.d.ts +96 -0
- package/dist/gateway/channels/telegram/stickers.js +1 -0
- package/dist/gateway/channels/telegram/thread-support.d.ts +99 -0
- package/dist/gateway/channels/telegram/thread-support.js +1 -0
- package/dist/gateway/channels/telegram/utils.d.ts +69 -0
- package/dist/gateway/channels/telegram/utils.js +1 -0
- package/dist/gateway/channels/webchat.d.ts +1 -1
- package/dist/gateway/channels/webchat.js +1 -1
- package/dist/nostromo/ui-js-agent.js +1 -1
- package/dist/pi-agent-provider/pi-query.js +1 -1
- package/dist/pi-agent-provider/pi-types.d.ts +4 -0
- package/dist/server.js +1 -1
- package/dist/tools/cron-tools.js +1 -1
- package/dist/tools/message-tools.js +1 -1
- package/dist/tools/telegram-actions-tools.d.ts +13 -0
- package/dist/tools/telegram-actions-tools.js +1 -0
- package/installationPkg/config.example.yaml +55 -1
- package/package.json +1 -1
- package/dist/gateway/channels/telegram.js +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram retry policy
|
|
3
|
+
*
|
|
4
|
+
* This module provides retry logic with exponential backoff for Telegram API calls,
|
|
5
|
+
* including network error detection and rate limit handling.
|
|
6
|
+
*/
|
|
7
|
+
import type { TelegramRetryConfig } from "./config-types.js";
|
|
8
|
+
/** Retry runner function type */
|
|
9
|
+
export type RetryRunner = <T>(fn: () => Promise<T>, label?: string) => Promise<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Check if an error is a recoverable network error that should trigger retry.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRecoverableNetworkError(err: unknown): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Create a retry runner for Telegram API calls.
|
|
16
|
+
*
|
|
17
|
+
* The retry runner wraps async functions and automatically retries on:
|
|
18
|
+
* - Network errors (ECONNRESET, ETIMEDOUT, etc.)
|
|
19
|
+
* - Rate limits (429)
|
|
20
|
+
* - Telegram temporary unavailability
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - Exponential backoff with configurable delays
|
|
24
|
+
* - Respects Telegram's retry_after parameter
|
|
25
|
+
* - Verbose logging option
|
|
26
|
+
*/
|
|
27
|
+
export declare function createRetryRunner(config?: TelegramRetryConfig, verbose?: boolean): RetryRunner;
|
|
28
|
+
//# sourceMappingURL=retry-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createLogger as e}from"../../../utils/logger.js";const r=e("telegram:retry"),t=3,o=400,n=3e4,s=/429|timeout|connect|reset|closed|unavailable|temporarily/i,a=new Set(["ECONNRESET","ECONNREFUSED","EPIPE","ETIMEDOUT","ESOCKETTIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN","UND_ERR_CONNECT_TIMEOUT","UND_ERR_HEADERS_TIMEOUT","UND_ERR_BODY_TIMEOUT","UND_ERR_SOCKET","UND_ERR_ABORTED","ECONNABORTED","ERR_NETWORK"]),i=new Set(["AbortError","TimeoutError","ConnectTimeoutError","HeadersTimeoutError","BodyTimeoutError"]),c=["fetch failed","typeerror: fetch failed","undici","network error","network request","client network socket disconnected","socket hang up","getaddrinfo","timeout","timed out"];function u(e){if(!e||"object"!=typeof e)return;if("code"in e&&"string"==typeof e.code)return e.code;const r=e.errno;return"string"==typeof r?r:"number"==typeof r?String(r):void 0}function f(e){return e&&"object"==typeof e&&"name"in e?String(e.name):""}function E(e){return e instanceof Error?e.message:String(e)}export function isRecoverableNetworkError(e){if(!e)return!1;for(const r of function(e){const r=[e],t=new Set,o=[];for(;r.length>0;){const e=r.shift();if(null!=e&&!t.has(e)&&(t.add(e),o.push(e),"object"==typeof e)){const o=e.cause;o&&!t.has(o)&&r.push(o);const n=e.reason;n&&!t.has(n)&&r.push(n);const s=e.errors;if(Array.isArray(s))for(const e of s)e&&!t.has(e)&&r.push(e);if("HttpError"===f(e)){const o=e.error;o&&!t.has(o)&&r.push(o)}}}return o}(e)){const e=u(r);if(e&&a.has(e.toUpperCase().trim()))return!0;const t=f(r);if(t&&i.has(t))return!0;const o=E(r).toLowerCase();if(o&&c.some(e=>o.includes(e)))return!0}return!1}function m(e){if(!e||"object"!=typeof e)return;const r="parameters"in e&&e.parameters&&"object"==typeof e.parameters?e.parameters.retry_after:"response"in e&&e.response&&"object"==typeof e.response&&"parameters"in e.response?e.response.parameters?.retry_after:"error"in e&&e.error&&"object"==typeof e.error&&"parameters"in e.error?e.error.parameters?.retry_after:void 0;return"number"==typeof r&&Number.isFinite(r)?1e3*r:void 0}function p(e){return new Promise(r=>setTimeout(r,e))}function y(e){return isRecoverableNetworkError(e)||s.test(E(e))}export function createRetryRunner(e,s=!1){const a=(i=e,{maxAttempts:i?.maxAttempts??t,baseDelayMs:i?.baseDelayMs??o,maxDelayMs:i?.maxDelayMs??n});var i;return async(e,t)=>{const o=a.maxAttempts,n=a.baseDelayMs,i=a.maxDelayMs;let c;for(let a=1;a<=o;a++)try{return await e()}catch(e){if(c=e,a>=o||!y(e))break;const u=m(e),f="number"==typeof u&&Number.isFinite(u)?Math.max(u,n):n*2**(a-1),l=Math.min(f,i);if(s){const n=Math.max(1,o-1);r.warn(`Telegram ${t??"request"} retry ${a}/${n} in ${l}ms: ${E(e)}`)}await p(l)}throw c??new Error("Retry failed")}}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram send module
|
|
3
|
+
*
|
|
4
|
+
* Core sending functionality for Telegram with:
|
|
5
|
+
* - Text and media message sending
|
|
6
|
+
* - HTML parse fallback to plain text
|
|
7
|
+
* - Thread fallback for forum topics
|
|
8
|
+
* - Retry policy integration
|
|
9
|
+
* - Inline button support
|
|
10
|
+
* - Chat not found error enrichment
|
|
11
|
+
*/
|
|
12
|
+
import type { TelegramSendOptions, TelegramSendResult } from "./config-types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Send a message to Telegram.
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Text and media support
|
|
18
|
+
* - Inline buttons with scope validation
|
|
19
|
+
* - Thread/reply parameters
|
|
20
|
+
* - HTML parse fallback to plain text
|
|
21
|
+
* - Thread fallback for forum topics
|
|
22
|
+
* - Retry policy with exponential backoff
|
|
23
|
+
* - Chat not found error enrichment
|
|
24
|
+
*
|
|
25
|
+
* @param to Target chat ID (with optional thread: "chatId:topic:topicId")
|
|
26
|
+
* @param text Message text (markdown format)
|
|
27
|
+
* @param opts Send options (media, buttons, thread, retry, etc.)
|
|
28
|
+
* @returns Message ID and chat ID
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Simple text message
|
|
33
|
+
* await sendMessageTelegram("123456", "Hello world", { token: "..." });
|
|
34
|
+
*
|
|
35
|
+
* // Message with buttons
|
|
36
|
+
* await sendMessageTelegram("123456", "Choose:", {
|
|
37
|
+
* token: "...",
|
|
38
|
+
* buttons: [[
|
|
39
|
+
* { text: "Yes", callback_data: "yes" },
|
|
40
|
+
* { text: "No", callback_data: "no" },
|
|
41
|
+
* ]],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Forum topic message
|
|
45
|
+
* await sendMessageTelegram("-100123456:topic:42", "Hello topic", { token: "..." });
|
|
46
|
+
*
|
|
47
|
+
* // Media message
|
|
48
|
+
* await sendMessageTelegram("123456", "Check this out", {
|
|
49
|
+
* token: "...",
|
|
50
|
+
* mediaUrl: "https://example.com/image.jpg",
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function sendMessageTelegram(to: string, text: string, opts?: TelegramSendOptions): Promise<TelegramSendResult>;
|
|
55
|
+
//# sourceMappingURL=send.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Bot as e,InputFile as t}from"grammy";import{createLogger as s}from"../../../utils/logger.js";import{parseTelegramTarget as a,normalizeChatId as n}from"./utils.js";import{buildThreadReplyParams as r}from"./thread-support.js";import{buildInlineKeyboard as i}from"./inline-buttons.js";import{createRetryRunner as o}from"./retry-policy.js";import{withHtmlParseFallback as d,withThreadFallback as c,wrapChatNotFoundError as g}from"./error-handling.js";const l=s("telegram:send");export async function sendMessageTelegram(s,p,u={}){const m=u.token;if(!m)throw new Error("Telegram bot token is required");const w=a(s),h=n(w.chatId),f=u.mediaUrl?.trim(),_=new e(m).api,b=i(u.buttons),M=r({targetMessageThreadId:w.messageThreadId,messageThreadId:u.messageThreadId,chatType:w.chatType,replyToMessageId:u.replyToMessageId,quoteText:u.quoteText}),v=Object.keys(M).length>0,y=o(u.retry,!1),k=function(e){let t=function(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}(e);return t=t.replace(/```([\s\S]*?)```/g,"<pre><code>$1</code></pre>"),t=t.replace(/`([^`]+)`/g,"<code>$1</code>"),t=t.replace(/\*\*([^*]+)\*\*/g,"<b>$1</b>"),t=t.replace(/\*([^*]+)\*/g,"<i>$1</i>"),t=t.replace(/__([^_]+)__/g,"<u>$1</u>"),t=t.replace(/~~([^~]+)~~/g,"<s>$1</s>"),t}(p),T=async(e,t)=>{try{return await y(e,t)}catch(e){throw g(e,h,s)}};if(f){const e=u.asVoice?"voice":function(e){const t=e.toLowerCase();return/\.(jpg|jpeg|png|gif|webp)($|\?)/.test(t)?"photo":/\.(mp4|mov|avi|mkv|webm)($|\?)/.test(t)?"video":/\.(mp3|m4a|ogg|wav|flac)($|\?)/.test(t)?"audio":/\.(pdf|doc|docx|txt|zip)($|\?)/.test(t)?"document":null}(f);e||l.warn(`Unknown media type for URL: ${f}, sending as document`);const s=e||"document",a=k.length>0?k.substring(0,1024):void 0,n={...M,...a?{caption:a,parse_mode:"HTML"}:{},...b?{reply_markup:b}:{},...u.silent?{disable_notification:!0}:{},...!1===u.linkPreview?{disable_web_page_preview:!0}:{}},r=async e=>{const a=new t(new URL(f));switch(s){case"photo":return await _.sendPhoto(h,a,e);case"video":return await _.sendVideo(h,a,e);case"audio":return await _.sendAudio(h,a,e);case"voice":return await _.sendVoice(h,a,e);case"video_note":return await _.sendVideoNote(h,a,e);default:return await _.sendDocument(h,a,e)}};let i;return i=v?await c(n,"sendMedia",e=>T(()=>r(e||{}),"sendMedia")):await T(()=>r(n),"sendMedia"),{messageId:String(i.message_id),chatId:String(i.chat.id)}}const $=function(e,t){if(e.length<=t)return[e];const s=[];let a=e;for(;a.length>0;){if(a.length<=t){s.push(a);break}let e=a.lastIndexOf("\n",t);-1===e&&(e=a.lastIndexOf(" ",t)),-1===e&&(e=t),s.push(a.substring(0,e)),a=a.substring(e+1)}return s}(k,u.textChunkLimit??4e3);for(let e=0;e<$.length-1;e++){const t=$[e],s={...M,parse_mode:"HTML",...!1===u.linkPreview?{disable_web_page_preview:!0}:{}},a=async s=>await d({label:`sendMessage-chunk-${e}`,requestHtml:()=>_.sendMessage(h,t,s),requestPlain:()=>_.sendMessage(h,t,{...s,parse_mode:void 0})});v?await c(s,`sendMessage-chunk-${e}`,t=>T(()=>a(t||{}),`sendMessage-chunk-${e}`)):await T(()=>a(s),`sendMessage-chunk-${e}`)}const I=$[$.length-1],j={...M,parse_mode:"HTML",...b?{reply_markup:b}:{},...u.silent?{disable_notification:!0}:{},...!1===u.linkPreview?{disable_web_page_preview:!0}:{}},x=async e=>await d({label:"sendMessage-last",requestHtml:()=>_.sendMessage(h,I,e),requestPlain:()=>_.sendMessage(h,I,{...e,parse_mode:void 0})});let q;return q=v?await c(j,"sendMessage-last",e=>T(()=>x(e||{}),"sendMessage-last")):await T(()=>x(j),"sendMessage-last"),{messageId:String(q.message_id),chatId:String(q.chat.id)}}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram stickers module
|
|
3
|
+
*
|
|
4
|
+
* Provides sticker caching, fuzzy search, and sending functionality.
|
|
5
|
+
*/
|
|
6
|
+
import type { CachedSticker, TelegramSendResult, TelegramRetryConfig } from "./config-types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Initialize sticker cache with data directory path.
|
|
9
|
+
*
|
|
10
|
+
* Call this once at startup before using any sticker functions.
|
|
11
|
+
*/
|
|
12
|
+
export declare function initStickerCache(dataDir: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Get a cached sticker by its unique ID.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCachedSticker(fileUniqueId: string): CachedSticker | null;
|
|
17
|
+
/**
|
|
18
|
+
* Add or update a sticker in the cache.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* cacheSticker({
|
|
23
|
+
* fileId: "CAACAgIAAxk...",
|
|
24
|
+
* fileUniqueId: "AgAD...",
|
|
25
|
+
* emoji: "👍",
|
|
26
|
+
* setName: "AnimatedEmojies",
|
|
27
|
+
* description: "Thumbs up animated emoji",
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function cacheSticker(sticker: CachedSticker): void;
|
|
32
|
+
/**
|
|
33
|
+
* Search cached stickers by text query.
|
|
34
|
+
*
|
|
35
|
+
* Uses fuzzy matching on description, emoji, and set name with scoring:
|
|
36
|
+
* - Exact substring in description: +10 points
|
|
37
|
+
* - Word-level match in description: +5 points per word
|
|
38
|
+
* - Emoji match: +8 points
|
|
39
|
+
* - Set name match: +3 points
|
|
40
|
+
*
|
|
41
|
+
* @param query Search query (e.g., "happy", "👍", "cat")
|
|
42
|
+
* @param limit Maximum number of results (default: 10)
|
|
43
|
+
* @returns Array of matching stickers, sorted by relevance score
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const results = searchStickers("happy cat", 5);
|
|
48
|
+
* for (const sticker of results) {
|
|
49
|
+
* console.log(`${sticker.emoji} ${sticker.description}`);
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function searchStickers(query: string, limit?: number): CachedSticker[];
|
|
54
|
+
/**
|
|
55
|
+
* Get all cached stickers (for debugging/listing).
|
|
56
|
+
*/
|
|
57
|
+
export declare function getAllCachedStickers(): CachedSticker[];
|
|
58
|
+
/**
|
|
59
|
+
* Get cache statistics.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getCacheStats(): {
|
|
62
|
+
count: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Send a sticker to a Telegram chat.
|
|
66
|
+
*
|
|
67
|
+
* @param to Target chat ID (with optional thread: "chatId:topic:topicId")
|
|
68
|
+
* @param fileId Telegram sticker file ID
|
|
69
|
+
* @param token Bot token
|
|
70
|
+
* @param opts Send options (replyToMessageId, messageThreadId)
|
|
71
|
+
* @returns Message ID and chat ID
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* // Simple sticker send
|
|
76
|
+
* await sendStickerTelegram(
|
|
77
|
+
* "123456",
|
|
78
|
+
* "CAACAgIAAxk...",
|
|
79
|
+
* "bot-token",
|
|
80
|
+
* );
|
|
81
|
+
*
|
|
82
|
+
* // Sticker with reply
|
|
83
|
+
* await sendStickerTelegram(
|
|
84
|
+
* "123456",
|
|
85
|
+
* "CAACAgIAAxk...",
|
|
86
|
+
* "bot-token",
|
|
87
|
+
* { replyToMessageId: 42 },
|
|
88
|
+
* );
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function sendStickerTelegram(to: string, fileId: string, token: string, opts?: {
|
|
92
|
+
replyToMessageId?: number;
|
|
93
|
+
messageThreadId?: number;
|
|
94
|
+
retry?: TelegramRetryConfig;
|
|
95
|
+
}): Promise<TelegramSendResult>;
|
|
96
|
+
//# sourceMappingURL=stickers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as e,writeFileSync as t,existsSync as r,mkdirSync as s}from"node:fs";import{join as i,dirname as c}from"node:path";import{Bot as o}from"grammy";import{createLogger as n}from"../../../utils/logger.js";import{parseTelegramTarget as a,normalizeChatId as l}from"./utils.js";import{buildThreadReplyParams as d}from"./thread-support.js";import{createRetryRunner as h}from"./retry-policy.js";import{withThreadFallback as u,wrapChatNotFoundError as f}from"./error-handling.js";const k=n("telegram:stickers");let m=null;export function initStickerCache(e){m=i(e,"telegram","sticker-cache.json");const t=c(m);r(t)||s(t,{recursive:!0})}function p(){if(!m)throw new Error("Sticker cache not initialized. Call initStickerCache() first.");if(!r(m))return{version:1,stickers:{}};try{const t=JSON.parse(e(m,"utf-8"));if(!t||"object"!=typeof t)return{version:1,stickers:{}};const r=t;return 1!==r.version?(k.warn(`Sticker cache version mismatch (found: ${r.version}, expected: 1), resetting cache`),{version:1,stickers:{}}):r}catch(e){return k.warn(`Failed to load sticker cache: ${e}, starting with empty cache`),{version:1,stickers:{}}}}export function getCachedSticker(e){return p().stickers[e]??null}export function cacheSticker(e){const r=p();r.stickers[e.fileUniqueId]=e,function(e){if(!m)throw new Error("Sticker cache not initialized. Call initStickerCache() first.");try{t(m,JSON.stringify(e,null,2),"utf-8")}catch(e){k.error(`Failed to save sticker cache: ${e}`)}}(r),k.debug(`Cached sticker: ${e.fileUniqueId} (${e.description})`)}export function searchStickers(e,t=10){const r=p(),s=e.toLowerCase(),i=[];for(const t of Object.values(r.stickers)){let r=0;const c=t.description.toLowerCase();c.includes(s)&&(r+=10);const o=s.split(/\s+/).filter(Boolean),n=c.split(/\s+/);for(const e of o)n.some(t=>t.includes(e))&&(r+=5);t.emoji&&e.includes(t.emoji)&&(r+=8),t.setName?.toLowerCase().includes(s)&&(r+=3),r>0&&i.push({sticker:t,score:r})}return i.sort((e,t)=>t.score-e.score).slice(0,t).map(e=>e.sticker)}export function getAllCachedStickers(){const e=p();return Object.values(e.stickers)}export function getCacheStats(){const e=p();return{count:Object.keys(e.stickers).length}}export async function sendStickerTelegram(e,t,r,s={}){if(!r)throw new Error("Telegram bot token is required");const i=a(e),c=l(i.chatId),n=new o(r).api,k=h(s.retry,!1),m=d({targetMessageThreadId:i.messageThreadId,messageThreadId:s.messageThreadId,chatType:i.chatType,replyToMessageId:s.replyToMessageId}),p=async(t,r)=>{try{return await k(t,r)}catch(t){throw f(t,c,e)}};let g;return g=Object.keys(m).length>0?await u(m,"sendSticker",e=>p(()=>n.sendSticker(c,t,e||{}),"sendSticker")):await p(()=>n.sendSticker(c,t),"sendSticker"),{messageId:String(g.message_id),chatId:String(g.chat.id)}}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram thread and forum topic support
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for working with Telegram's threading features:
|
|
5
|
+
* - Forum topics (supergroups with topics enabled)
|
|
6
|
+
* - DM topics (direct message threads)
|
|
7
|
+
* - Reply parameters with quote support
|
|
8
|
+
*/
|
|
9
|
+
import type { TelegramChatType } from "./config-types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build thread reply parameters for Telegram API calls.
|
|
12
|
+
*
|
|
13
|
+
* Handles:
|
|
14
|
+
* - Forum topic message_thread_id
|
|
15
|
+
* - DM topic message_thread_id
|
|
16
|
+
* - Reply-to message IDs with quote support
|
|
17
|
+
*
|
|
18
|
+
* IMPORTANT: Thread IDs behave differently based on chat type:
|
|
19
|
+
* - DMs (private chats): Include message_thread_id when present (DM topics)
|
|
20
|
+
* - Forum topics: Skip thread_id=1 (General topic), include others
|
|
21
|
+
* - Regular groups: Thread IDs are ignored by Telegram
|
|
22
|
+
*
|
|
23
|
+
* General forum topic (id=1) must be treated like a regular supergroup send:
|
|
24
|
+
* Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
|
|
25
|
+
*
|
|
26
|
+
* @param params Thread and reply configuration
|
|
27
|
+
* @returns API params object with thread/reply fields
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // Forum topic (non-General)
|
|
32
|
+
* buildThreadReplyParams({
|
|
33
|
+
* messageThreadId: 42,
|
|
34
|
+
* chatType: "group",
|
|
35
|
+
* });
|
|
36
|
+
* // → { message_thread_id: 42 }
|
|
37
|
+
*
|
|
38
|
+
* // General forum topic
|
|
39
|
+
* buildThreadReplyParams({
|
|
40
|
+
* messageThreadId: 1,
|
|
41
|
+
* chatType: "group",
|
|
42
|
+
* });
|
|
43
|
+
* // → {} (thread_id=1 is omitted)
|
|
44
|
+
*
|
|
45
|
+
* // DM topic
|
|
46
|
+
* buildThreadReplyParams({
|
|
47
|
+
* messageThreadId: 5,
|
|
48
|
+
* chatType: "direct",
|
|
49
|
+
* });
|
|
50
|
+
* // → { message_thread_id: 5 }
|
|
51
|
+
*
|
|
52
|
+
* // Reply with quote
|
|
53
|
+
* buildThreadReplyParams({
|
|
54
|
+
* replyToMessageId: 123,
|
|
55
|
+
* quoteText: "Original message text",
|
|
56
|
+
* });
|
|
57
|
+
* // → { reply_parameters: { message_id: 123, quote: "Original message text" } }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildThreadReplyParams(params: {
|
|
61
|
+
/** Forum topic or DM thread ID (overrides targetMessageThreadId) */
|
|
62
|
+
messageThreadId?: number;
|
|
63
|
+
/** Alternative thread ID (used if messageThreadId is not set) */
|
|
64
|
+
targetMessageThreadId?: number;
|
|
65
|
+
/** Chat type for thread ID resolution */
|
|
66
|
+
chatType?: TelegramChatType;
|
|
67
|
+
/** Message ID to reply to */
|
|
68
|
+
replyToMessageId?: number;
|
|
69
|
+
/** Quote text for reply_parameters */
|
|
70
|
+
quoteText?: string;
|
|
71
|
+
}): Record<string, unknown>;
|
|
72
|
+
/**
|
|
73
|
+
* Build thread params for typing indicators (sendChatAction).
|
|
74
|
+
*
|
|
75
|
+
* Empirically, General topic (id=1) needs message_thread_id for typing to appear.
|
|
76
|
+
* Unlike message sending, typing indicators work with thread_id=1.
|
|
77
|
+
*
|
|
78
|
+
* @param messageThreadId Thread ID for typing indicator
|
|
79
|
+
* @returns API params object or undefined
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildTypingThreadParams(messageThreadId?: number): {
|
|
82
|
+
message_thread_id: number;
|
|
83
|
+
} | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the thread ID for Telegram forum topics.
|
|
86
|
+
*
|
|
87
|
+
* For non-forum groups, returns undefined even if messageThreadId is present
|
|
88
|
+
* (reply threads in regular groups should not create separate sessions).
|
|
89
|
+
*
|
|
90
|
+
* For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
|
|
91
|
+
*
|
|
92
|
+
* @param params Forum detection and thread ID
|
|
93
|
+
* @returns Resolved thread ID or undefined
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveTelegramForumThreadId(params: {
|
|
96
|
+
isForum?: boolean;
|
|
97
|
+
messageThreadId?: number | null;
|
|
98
|
+
}): number | undefined;
|
|
99
|
+
//# sourceMappingURL=thread-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function buildThreadReplyParams(e){const r={},a=null!=e.messageThreadId?e.messageThreadId:e.targetMessageThreadId;if(null!=a){const t=Math.trunc(a),s=e.chatType??"unknown";"direct"===s?t>0&&(r.message_thread_id=t):"group"===s?1!==t&&(r.message_thread_id=t):t>0&&(r.message_thread_id=t)}if(null!=e.replyToMessageId){const a=Math.trunc(e.replyToMessageId);e.quoteText?.trim()?r.reply_parameters={message_id:a,quote:e.quoteText.trim()}:r.reply_to_message_id=a}return r}export function buildTypingThreadParams(e){if(null!=e)return{message_thread_id:Math.trunc(e)}}export function resolveTelegramForumThreadId(e){if(e.isForum)return null==e.messageThreadId?1:e.messageThreadId}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram utility functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides utility functions for normalizing chat IDs,
|
|
5
|
+
* parsing targets, and determining chat types.
|
|
6
|
+
*/
|
|
7
|
+
import type { TelegramChatType } from "./config-types.js";
|
|
8
|
+
/** Telegram target parse result */
|
|
9
|
+
export interface TelegramTarget {
|
|
10
|
+
chatId: string;
|
|
11
|
+
messageThreadId?: number;
|
|
12
|
+
chatType: TelegramChatType;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Strip internal prefixes from Telegram target strings.
|
|
16
|
+
*
|
|
17
|
+
* Removes prefixes like:
|
|
18
|
+
* - `telegram:` or `tg:`
|
|
19
|
+
* - `group:` (when following telegram prefix)
|
|
20
|
+
*
|
|
21
|
+
* Examples:
|
|
22
|
+
* - `telegram:123456` → `123456`
|
|
23
|
+
* - `tg:group:-100123456` → `-100123456`
|
|
24
|
+
* - `123456` → `123456` (unchanged)
|
|
25
|
+
*/
|
|
26
|
+
export declare function stripTelegramInternalPrefixes(to: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve chat type from a Telegram chat ID.
|
|
29
|
+
*
|
|
30
|
+
* Rules:
|
|
31
|
+
* - Negative numeric IDs → "group"
|
|
32
|
+
* - Positive numeric IDs → "direct"
|
|
33
|
+
* - Non-numeric → "unknown"
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveChatType(chatId: string): TelegramChatType;
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a chat ID for use with Telegram API.
|
|
38
|
+
*
|
|
39
|
+
* Strips internal prefixes and returns the clean chat ID.
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeChatId(to: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Normalize a message ID to a number.
|
|
44
|
+
*
|
|
45
|
+
* Handles both string and number inputs.
|
|
46
|
+
*/
|
|
47
|
+
export declare function normalizeMessageId(raw: string | number): number;
|
|
48
|
+
/**
|
|
49
|
+
* Parse a Telegram delivery target into chatId and optional topic/thread ID.
|
|
50
|
+
*
|
|
51
|
+
* Supported formats:
|
|
52
|
+
* - `chatId` (plain chat ID, t.me link, @username, or internal prefixes)
|
|
53
|
+
* - `chatId:topicId` (numeric topic/thread ID)
|
|
54
|
+
* - `chatId:topic:topicId` (explicit topic marker; preferred)
|
|
55
|
+
*
|
|
56
|
+
* Examples:
|
|
57
|
+
* - `123456` → `{chatId: "123456", chatType: "direct"}`
|
|
58
|
+
* - `-100123456` → `{chatId: "-100123456", chatType: "group"}`
|
|
59
|
+
* - `123456:topic:42` → `{chatId: "123456", messageThreadId: 42, chatType: "direct"}`
|
|
60
|
+
* - `telegram:-100123456:789` → `{chatId: "-100123456", messageThreadId: 789, chatType: "group"}`
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseTelegramTarget(to: string): TelegramTarget;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the chat type for a target string.
|
|
65
|
+
*
|
|
66
|
+
* This is a convenience wrapper around parseTelegramTarget.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveTelegramTargetChatType(target: string): TelegramChatType;
|
|
69
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function stripTelegramInternalPrefixes(e){let r=e.trim(),t=!1;for(;;){const e=(()=>/^(telegram|tg):/i.test(r)?(t=!0,r.replace(/^(telegram|tg):/i,"").trim()):t&&/^group:/i.test(r)?r.replace(/^group:/i,"").trim():r)();if(e===r)return r;r=e}}export function resolveChatType(e){const r=e.trim();return r&&/^-?\d+$/.test(r)?r.startsWith("-")?"group":"direct":"unknown"}export function normalizeChatId(e){return stripTelegramInternalPrefixes(e)}export function normalizeMessageId(e){return"number"==typeof e?e:Number.parseInt(e,10)}export function parseTelegramTarget(e){const r=stripTelegramInternalPrefixes(e),t=/^(.+?):topic:(\d+)$/.exec(r);if(t)return{chatId:t[1],messageThreadId:Number.parseInt(t[2],10),chatType:resolveChatType(t[1])};const a=/^(.+):(\d+)$/.exec(r);return a?{chatId:a[1],messageThreadId:Number.parseInt(a[2],10),chatType:resolveChatType(a[1])}:{chatId:r,chatType:resolveChatType(r)}}export function resolveTelegramTargetChatType(e){return parseTelegramTarget(e).chatType}
|
|
@@ -28,7 +28,7 @@ export declare class WebChatChannel implements ChannelAdapter {
|
|
|
28
28
|
attachments?: RawAttachment[];
|
|
29
29
|
}): Promise<void>;
|
|
30
30
|
sendText(chatId: string, text: string): Promise<void>;
|
|
31
|
-
sendButtons(chatId: string, text: string, buttons: InlineButton[]): Promise<void>;
|
|
31
|
+
sendButtons(chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
32
32
|
setTyping(chatId: string): Promise<void>;
|
|
33
33
|
clearTyping(chatId: string): Promise<void>;
|
|
34
34
|
releaseTyping(chatId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:
|
|
1
|
+
import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){const s=n.flat();this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:s.map(t=>({text:t.text,callbackData:t.callbackData??t.text,...t.url?{url:t.url}:{}}))}),this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?this.sendWs(t,{type:"typing_indicator",typing:!0}):this.startTypingInterval(t)}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}async releaseTyping(t){this.stopTypingInterval(t)}async sendAudio(s,i,o){try{const a=t(i),r=e(i),c=n(i).toLowerCase().replace(".",""),h={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[c]||"audio/mpeg";this.sendWs(s,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:h,fileName:r,data:a.toString("base64"),asVoice:o??!1}),this.resendTypingIfActive(s)}catch(t){a.error(`WebChat: failed to read audio file ${i}: ${t}`)}}async stop(){for(const[t,e]of this.typingIntervals)clearInterval(e),this.sendWs(t,{type:"typing_indicator",typing:!1});this.typingIntervals.clear(),this.inflightCount.clear(),this.onMessage=null,a.info("WebChat channel stopped")}startTypingInterval(t){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return;const n=this.typingIntervals.get(t);n&&(clearInterval(n),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!0});const s=setInterval(()=>{(this.inflightCount.get(t)??0)>0?this.sendWs(t,{type:"typing_indicator",typing:!0}):(clearInterval(s),this.typingIntervals.delete(t),this.sendWs(t,{type:"typing_indicator",typing:!1}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){const e=(this.inflightCount.get(t)??1)-1;if(e>0)this.inflightCount.set(t,e);else{this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}}resendTypingIfActive(t){this.typingIntervals.has(t)&&this.sendWs(t,{type:"typing_indicator",typing:!0})}sendWs(t,e){const n=this.connections.get(t);if(n&&n.readyState===n.OPEN)try{n.send(JSON.stringify({...e,chatId:t}))}catch(e){a.error(`WebChat: failed to send to ${t}: ${e}`)}else a.warn(`WebChat: no active connection for chatId ${t}`)}}
|