auramaxx 1.0.0-alpha.4
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 +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram adapter — full bidirectional approval via Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Uses long-polling (getUpdates) with raw fetch calls — no npm dependencies.
|
|
5
|
+
* Sends inline keyboard buttons for Approve/Reject, edits messages on resolution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ApprovalAdapter,
|
|
10
|
+
AdapterContext,
|
|
11
|
+
ActionNotification,
|
|
12
|
+
ActionResolution,
|
|
13
|
+
ChatMessage,
|
|
14
|
+
ChatReply,
|
|
15
|
+
} from './types';
|
|
16
|
+
import { getErrorMessage } from '../error';
|
|
17
|
+
|
|
18
|
+
/** Flavor text rotation for generic thinking time between tool calls */
|
|
19
|
+
const FLAVOR_TEXTS = [
|
|
20
|
+
'auramaxxing...',
|
|
21
|
+
'mogging prompt peasants...',
|
|
22
|
+
'thinking with my big brain...',
|
|
23
|
+
'controlling cortisol spike...',
|
|
24
|
+
'channeling main character energy...',
|
|
25
|
+
'ascending beyond mortal comprehension...',
|
|
26
|
+
'i will not double text...',
|
|
27
|
+
'consulting the blockchain elders...',
|
|
28
|
+
'asking the smart contracts nicely...',
|
|
29
|
+
'in my bag rn...',
|
|
30
|
+
'downloading more RAM...',
|
|
31
|
+
'mainnet mindset activated...',
|
|
32
|
+
'reading the whitepaper backwards...',
|
|
33
|
+
'gas fees for my thoughts...',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export interface TelegramAdapterConfig {
|
|
37
|
+
/** Telegram Bot API token (from @BotFather) */
|
|
38
|
+
botToken: string;
|
|
39
|
+
/** Chat ID to send notifications to and accept callbacks from */
|
|
40
|
+
chatId: string | number;
|
|
41
|
+
/** Chat configuration — opt-in for agent chat */
|
|
42
|
+
chat?: { enabled?: boolean };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class TelegramAdapter implements ApprovalAdapter {
|
|
46
|
+
readonly name = 'telegram';
|
|
47
|
+
private config: TelegramAdapterConfig;
|
|
48
|
+
private ctx: AdapterContext | null = null;
|
|
49
|
+
private pollAbort: AbortController | null = null;
|
|
50
|
+
private isRunning = false;
|
|
51
|
+
private updateOffset = 0;
|
|
52
|
+
|
|
53
|
+
/** Maps actionId → Telegram messageId for editing on resolution */
|
|
54
|
+
private actionMessages = new Map<string, number>();
|
|
55
|
+
|
|
56
|
+
constructor(config: TelegramAdapterConfig) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async start(ctx: AdapterContext): Promise<void> {
|
|
61
|
+
this.ctx = ctx;
|
|
62
|
+
|
|
63
|
+
// Verify bot token by calling getMe
|
|
64
|
+
const me = await this.apiCall('getMe', {});
|
|
65
|
+
if (!me?.result) {
|
|
66
|
+
console.error('[adapters] telegram: invalid bot token or network error — not starting');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const username = (me.result as unknown as { username?: string }).username;
|
|
70
|
+
console.log(`[adapters] telegram: connected as @${username}`);
|
|
71
|
+
|
|
72
|
+
// Clear any existing webhook so long-polling works
|
|
73
|
+
await this.apiCall('deleteWebhook', { drop_pending_updates: false });
|
|
74
|
+
|
|
75
|
+
this.isRunning = true;
|
|
76
|
+
this.startPolling();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async notify(action: ActionNotification): Promise<void> {
|
|
80
|
+
// Notification-only: send plain message without inline keyboard
|
|
81
|
+
if (action.type === 'notify') {
|
|
82
|
+
const text = this.formatInfoNotification(action);
|
|
83
|
+
try {
|
|
84
|
+
await this.apiCall('sendMessage', {
|
|
85
|
+
chat_id: this.config.chatId,
|
|
86
|
+
text,
|
|
87
|
+
parse_mode: 'HTML',
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const msg = getErrorMessage(err);
|
|
91
|
+
console.error(`[adapters] telegram notify error:`, msg);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const text = this.formatNotification(action);
|
|
97
|
+
const inlineKeyboard = {
|
|
98
|
+
inline_keyboard: [[
|
|
99
|
+
{ text: 'Approve', callback_data: `approve:${action.id}` },
|
|
100
|
+
{ text: 'Reject', callback_data: `reject:${action.id}` },
|
|
101
|
+
]],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await this.apiCall('sendMessage', {
|
|
106
|
+
chat_id: this.config.chatId,
|
|
107
|
+
text,
|
|
108
|
+
parse_mode: 'HTML',
|
|
109
|
+
reply_markup: JSON.stringify(inlineKeyboard),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (result?.result?.message_id) {
|
|
113
|
+
this.actionMessages.set(action.id, result.result.message_id);
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const msg = getErrorMessage(err);
|
|
117
|
+
console.error(`[adapters] telegram notify error:`, msg);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async resolved(resolution: ActionResolution): Promise<void> {
|
|
122
|
+
const messageId = this.actionMessages.get(resolution.id);
|
|
123
|
+
if (!messageId) return;
|
|
124
|
+
|
|
125
|
+
const status = resolution.approved ? 'APPROVED' : 'REJECTED';
|
|
126
|
+
const text = `${status} by ${resolution.resolvedBy}\nAction: ${resolution.id.slice(0, 8)}... (${resolution.type})`;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await this.apiCall('editMessageText', {
|
|
130
|
+
chat_id: this.config.chatId,
|
|
131
|
+
message_id: messageId,
|
|
132
|
+
text,
|
|
133
|
+
parse_mode: 'HTML',
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.debug('[adapters] telegram: failed to edit resolved message:', getErrorMessage(err));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.actionMessages.delete(resolution.id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async stop(): Promise<void> {
|
|
143
|
+
console.log('[adapters] telegram: stopping');
|
|
144
|
+
this.isRunning = false;
|
|
145
|
+
if (this.pollAbort) {
|
|
146
|
+
this.pollAbort.abort();
|
|
147
|
+
this.pollAbort = null;
|
|
148
|
+
}
|
|
149
|
+
this.actionMessages.clear();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private formatInfoNotification(action: ActionNotification): string {
|
|
153
|
+
const meta = (action.metadata || {}) as Record<string, unknown>;
|
|
154
|
+
const lines: string[] = [
|
|
155
|
+
`<b>${escapeHtml(action.summary)}</b>`,
|
|
156
|
+
`<b>Source:</b> ${escapeHtml(action.source)}`,
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
if (meta.contractAddress) {
|
|
160
|
+
lines.push(`<b>DexScreener:</b> <a href="https://dexscreener.com/base/${meta.contractAddress}">View Chart</a>`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (meta.symbol) {
|
|
164
|
+
lines.push(`<b>Symbol:</b> ${escapeHtml(String(meta.symbol))}`);
|
|
165
|
+
}
|
|
166
|
+
if (meta.marketCap) {
|
|
167
|
+
const mc = Number(meta.marketCap);
|
|
168
|
+
const formatted = mc >= 1_000_000
|
|
169
|
+
? `$${(mc / 1_000_000).toFixed(1)}M`
|
|
170
|
+
: mc >= 1000
|
|
171
|
+
? `$${(mc / 1000).toFixed(1)}K`
|
|
172
|
+
: `$${mc.toFixed(0)}`;
|
|
173
|
+
lines.push(`<b>Market Cap:</b> ${formatted}`);
|
|
174
|
+
}
|
|
175
|
+
if (meta.risk != null) {
|
|
176
|
+
lines.push(`<b>Risk:</b> ${meta.risk}/10`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (Array.isArray(meta.socialLinks)) {
|
|
180
|
+
const linkStrs: string[] = [];
|
|
181
|
+
for (const link of meta.socialLinks as Array<{ name?: string; link?: string }>) {
|
|
182
|
+
if (link.link) {
|
|
183
|
+
linkStrs.push(`<a href="${escapeHtml(link.link)}">${escapeHtml(link.name || 'Link')}</a>`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (linkStrs.length > 0) {
|
|
187
|
+
lines.push(`<b>Links:</b> ${linkStrs.join(' | ')}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (meta.evaluation && typeof meta.evaluation === 'string') {
|
|
192
|
+
// Truncate very long evaluations for Telegram (max ~3500 chars to stay under 4096 limit)
|
|
193
|
+
let evalText = meta.evaluation as string;
|
|
194
|
+
if (evalText.length > 3000) {
|
|
195
|
+
evalText = evalText.slice(0, 3000) + '...';
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push(escapeHtml(evalText));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return lines.join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private formatNotification(action: ActionNotification): string {
|
|
205
|
+
const vs = action.verifiedSummary || (action.metadata?.verifiedSummary as ActionNotification['verifiedSummary']);
|
|
206
|
+
|
|
207
|
+
const lines = [
|
|
208
|
+
`<b>New Action Request</b>`,
|
|
209
|
+
`<b>Type:</b> ${escapeHtml(action.type)}`,
|
|
210
|
+
`<b>Source:</b> ${escapeHtml(action.source)}`,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
// Use verified one-liner as primary summary when available
|
|
214
|
+
if (vs?.oneLiner) {
|
|
215
|
+
lines.push(`<b>Action:</b> ${escapeHtml(vs.oneLiner)}`);
|
|
216
|
+
if (action.summary !== vs.oneLiner) {
|
|
217
|
+
lines.push(`<b>Agent says:</b> <i>${escapeHtml(action.summary)}</i>`);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
lines.push(`<b>Summary:</b> ${escapeHtml(action.summary)}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
lines.push(`<b>ID:</b> <code>${action.id.slice(0, 12)}</code>`);
|
|
224
|
+
|
|
225
|
+
if (action.expiresAt) {
|
|
226
|
+
const expires = new Date(action.expiresAt).toISOString();
|
|
227
|
+
lines.push(`<b>Expires:</b> ${expires}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Show discrepancy warnings
|
|
231
|
+
if (vs?.discrepancies && vs.discrepancies.length > 0) {
|
|
232
|
+
lines.push('');
|
|
233
|
+
for (const d of vs.discrepancies) {
|
|
234
|
+
const icon = d.severity === 'critical' ? '🚨' : d.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
235
|
+
lines.push(`${icon} <b>${escapeHtml(d.severity.toUpperCase())}</b>: ${escapeHtml(d.field)} — agent says "${escapeHtml(d.agentClaim)}", actual: ${escapeHtml(d.actual)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Start long-polling loop for Telegram updates */
|
|
243
|
+
private startPolling(): void {
|
|
244
|
+
console.log('[adapters] telegram: polling started');
|
|
245
|
+
const poll = async () => {
|
|
246
|
+
while (this.isRunning) {
|
|
247
|
+
try {
|
|
248
|
+
this.pollAbort = new AbortController();
|
|
249
|
+
const result = await this.apiCall('getUpdates', {
|
|
250
|
+
offset: this.updateOffset,
|
|
251
|
+
timeout: 30,
|
|
252
|
+
}, this.pollAbort.signal);
|
|
253
|
+
|
|
254
|
+
if (!result?.result) continue;
|
|
255
|
+
|
|
256
|
+
for (const update of result.result) {
|
|
257
|
+
this.updateOffset = update.update_id + 1;
|
|
258
|
+
|
|
259
|
+
if (update.callback_query) {
|
|
260
|
+
await this.handleCallback(update.callback_query);
|
|
261
|
+
} else if (update.message?.text) {
|
|
262
|
+
await this.handleChatMessage(update.message as TelegramMessage);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if (err instanceof Error && err.name === 'AbortError') break;
|
|
267
|
+
const msg = getErrorMessage(err);
|
|
268
|
+
console.error('[adapters] telegram: polling error:', msg);
|
|
269
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log('[adapters] telegram: polling stopped');
|
|
273
|
+
};
|
|
274
|
+
poll();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Handle an inline keyboard callback */
|
|
278
|
+
private async handleCallback(query: TelegramCallbackQuery): Promise<void> {
|
|
279
|
+
// Security: only accept callbacks from the configured chat
|
|
280
|
+
const chatId = String(query.message?.chat?.id);
|
|
281
|
+
if (chatId !== String(this.config.chatId)) return;
|
|
282
|
+
|
|
283
|
+
const data = query.data;
|
|
284
|
+
if (!data) return;
|
|
285
|
+
|
|
286
|
+
const match = data.match(/^(approve|reject):(.+)$/);
|
|
287
|
+
if (!match) return;
|
|
288
|
+
|
|
289
|
+
const [, action, actionId] = match;
|
|
290
|
+
const approved = action === 'approve';
|
|
291
|
+
|
|
292
|
+
// Answer the callback to remove loading state
|
|
293
|
+
await this.apiCall('answerCallbackQuery', {
|
|
294
|
+
callback_query_id: query.id,
|
|
295
|
+
text: approved ? 'Approving...' : 'Rejecting...',
|
|
296
|
+
}).catch(() => {});
|
|
297
|
+
|
|
298
|
+
// Resolve the action
|
|
299
|
+
if (this.ctx) {
|
|
300
|
+
const result = await this.ctx.resolve(actionId, approved);
|
|
301
|
+
if (!result.success) {
|
|
302
|
+
// Notify the user of the error
|
|
303
|
+
await this.apiCall('sendMessage', {
|
|
304
|
+
chat_id: this.config.chatId,
|
|
305
|
+
text: `Failed to ${action}: ${result.error}`,
|
|
306
|
+
}).catch(() => {});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Handle incoming chat messages (adapter interface) */
|
|
312
|
+
async onMessage(message: ChatMessage): Promise<ChatReply | null> {
|
|
313
|
+
if (this.config.chat?.enabled !== true) return null;
|
|
314
|
+
if (!this.ctx) return null;
|
|
315
|
+
|
|
316
|
+
const appId = await this.ctx.resolveApp(message.targetApp);
|
|
317
|
+
if (!appId) {
|
|
318
|
+
return { text: 'No AI app configured. Set a default app in adapter settings.' };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = await this.ctx.sendMessage(appId, message.text, undefined, 'telegram');
|
|
322
|
+
if (result.error) {
|
|
323
|
+
return { text: `Error: ${result.error}` };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return result.reply ? { text: result.reply } : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Handle a text message from the Telegram polling loop */
|
|
330
|
+
private async handleChatMessage(message: TelegramMessage): Promise<void> {
|
|
331
|
+
// Chat must be opted in
|
|
332
|
+
if (this.config.chat?.enabled !== true) return;
|
|
333
|
+
|
|
334
|
+
// Security: only accept messages from the configured chat
|
|
335
|
+
if (!this.config.chatId) {
|
|
336
|
+
console.error('[adapters] telegram: chatId not configured — cannot process messages. Re-save adapter config with your chat ID.');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const chatId = String(message.chat?.id);
|
|
340
|
+
if (chatId !== String(this.config.chatId)) {
|
|
341
|
+
console.warn(`[adapters] telegram: ignoring message from chat ${chatId} (expected ${this.config.chatId})`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const text = message.text || '';
|
|
346
|
+
if (!text.trim()) return;
|
|
347
|
+
|
|
348
|
+
console.log(`[adapters] telegram: chat message from ${chatId}: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`);
|
|
349
|
+
|
|
350
|
+
// Show "typing..." indicator while AI processes (re-send every 4s since it expires after ~5s)
|
|
351
|
+
const sendTyping = () => this.apiCall('sendChatAction', {
|
|
352
|
+
chat_id: this.config.chatId,
|
|
353
|
+
action: 'typing',
|
|
354
|
+
}).catch(() => { /* non-fatal */ });
|
|
355
|
+
await sendTyping();
|
|
356
|
+
const typingInterval = setInterval(sendTyping, 4000);
|
|
357
|
+
|
|
358
|
+
// Progress status message — editable in place.
|
|
359
|
+
// Delayed: wait a few seconds before showing the first flavor text so
|
|
360
|
+
// quick replies don't flash a status message at all.
|
|
361
|
+
let statusMessageId: number | null = null;
|
|
362
|
+
let creatingStatusMessage = false;
|
|
363
|
+
let flavorIndex = 0;
|
|
364
|
+
|
|
365
|
+
const createStatusMessage = (text: string) => {
|
|
366
|
+
if (statusMessageId || creatingStatusMessage) return;
|
|
367
|
+
creatingStatusMessage = true;
|
|
368
|
+
this.apiCall('sendMessage', {
|
|
369
|
+
chat_id: this.config.chatId,
|
|
370
|
+
text,
|
|
371
|
+
}).then((res) => {
|
|
372
|
+
statusMessageId = res?.result?.message_id ?? null;
|
|
373
|
+
}).catch(() => {
|
|
374
|
+
// Non-fatal — proceed without status message
|
|
375
|
+
}).finally(() => {
|
|
376
|
+
creatingStatusMessage = false;
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const onProgress = (status: string) => {
|
|
381
|
+
const progressText = status || FLAVOR_TEXTS[flavorIndex++ % FLAVOR_TEXTS.length];
|
|
382
|
+
if (statusMessageId) {
|
|
383
|
+
this.apiCall('editMessageText', {
|
|
384
|
+
chat_id: this.config.chatId,
|
|
385
|
+
message_id: statusMessageId,
|
|
386
|
+
text: progressText,
|
|
387
|
+
}).catch(() => {});
|
|
388
|
+
} else if (status) {
|
|
389
|
+
// Explicit progress means work is ongoing; create status message immediately.
|
|
390
|
+
createStatusMessage(progressText);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Delay the initial status message — no need to spam on quick replies
|
|
395
|
+
const statusDelay = setTimeout(async () => {
|
|
396
|
+
if (statusMessageId || creatingStatusMessage) return;
|
|
397
|
+
const initialText = FLAVOR_TEXTS[flavorIndex++ % FLAVOR_TEXTS.length];
|
|
398
|
+
createStatusMessage(initialText);
|
|
399
|
+
}, 3000);
|
|
400
|
+
|
|
401
|
+
// Rotate flavor text on a timer when no tool-call progress fires
|
|
402
|
+
const flavorInterval = setInterval(() => {
|
|
403
|
+
if (statusMessageId) onProgress('');
|
|
404
|
+
}, 10_000);
|
|
405
|
+
|
|
406
|
+
const cleanupTimers = () => {
|
|
407
|
+
clearTimeout(statusDelay);
|
|
408
|
+
clearInterval(typingInterval);
|
|
409
|
+
clearInterval(flavorInterval);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const deleteStatusMessage = async () => {
|
|
413
|
+
if (statusMessageId) {
|
|
414
|
+
await this.apiCall('deleteMessage', {
|
|
415
|
+
chat_id: this.config.chatId,
|
|
416
|
+
message_id: statusMessageId,
|
|
417
|
+
}).catch(() => {});
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Route through the message chain with the progress callback
|
|
422
|
+
if (!this.ctx) {
|
|
423
|
+
cleanupTimers();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const appId = await this.ctx.resolveApp();
|
|
428
|
+
if (!appId) {
|
|
429
|
+
cleanupTimers();
|
|
430
|
+
await deleteStatusMessage();
|
|
431
|
+
await this.apiCall('sendMessage', {
|
|
432
|
+
chat_id: this.config.chatId,
|
|
433
|
+
text: 'No AI app configured. Set a default app in adapter settings.',
|
|
434
|
+
}).catch(() => {});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let result: { reply: string | null; error?: string };
|
|
439
|
+
try {
|
|
440
|
+
result = await this.ctx.sendMessage(appId, text, onProgress, 'telegram');
|
|
441
|
+
} finally {
|
|
442
|
+
cleanupTimers();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Delete status message before sending real reply (await to prevent double-texting)
|
|
446
|
+
await deleteStatusMessage();
|
|
447
|
+
|
|
448
|
+
if (result.error) {
|
|
449
|
+
await this.apiCall('sendMessage', {
|
|
450
|
+
chat_id: this.config.chatId,
|
|
451
|
+
text: `Error: ${result.error}`,
|
|
452
|
+
}).catch(() => {});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!result.reply) {
|
|
457
|
+
console.log('[adapters] telegram: no reply generated for chat message');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
await this.sendLongMessage(result.reply);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Send a long message, converting markdown to Telegram HTML and splitting
|
|
466
|
+
* into chunks that respect the 4096-char limit. Falls back to plain text
|
|
467
|
+
* if HTML parsing fails on a chunk.
|
|
468
|
+
*/
|
|
469
|
+
private async sendLongMessage(reply: string): Promise<void> {
|
|
470
|
+
const html = markdownToTelegramHtml(reply);
|
|
471
|
+
const chunks = splitMessage(html, 4000); // leave headroom under 4096
|
|
472
|
+
for (const chunk of chunks) {
|
|
473
|
+
// Try HTML first, fall back to plain text if Telegram rejects the markup
|
|
474
|
+
const sent = await this.apiCall('sendMessage', {
|
|
475
|
+
chat_id: this.config.chatId,
|
|
476
|
+
text: chunk,
|
|
477
|
+
parse_mode: 'HTML',
|
|
478
|
+
});
|
|
479
|
+
if (!sent) {
|
|
480
|
+
// HTML parse failed (unclosed tags from split) — retry as plain text
|
|
481
|
+
const plain = chunk.replace(/<[^>]+>/g, '');
|
|
482
|
+
const fallback = await this.apiCall('sendMessage', {
|
|
483
|
+
chat_id: this.config.chatId,
|
|
484
|
+
text: plain || '(message could not be rendered)',
|
|
485
|
+
});
|
|
486
|
+
if (!fallback) {
|
|
487
|
+
console.error('[adapters] telegram: failed to send chat reply chunk even as plain text');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Call a Telegram Bot API method */
|
|
494
|
+
private async apiCall(
|
|
495
|
+
method: string,
|
|
496
|
+
params: Record<string, unknown>,
|
|
497
|
+
signal?: AbortSignal,
|
|
498
|
+
): Promise<TelegramApiResponse | null> {
|
|
499
|
+
const url = `https://api.telegram.org/bot${this.config.botToken}/${method}`;
|
|
500
|
+
|
|
501
|
+
const response = await fetch(url, {
|
|
502
|
+
method: 'POST',
|
|
503
|
+
headers: { 'Content-Type': 'application/json' },
|
|
504
|
+
body: JSON.stringify(params),
|
|
505
|
+
signal,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
const text = await response.text().catch(() => '');
|
|
510
|
+
console.error(`[adapters] telegram API ${method} failed: ${response.status} ${text}`);
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return response.json() as Promise<TelegramApiResponse>;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Minimal Telegram types (no npm dependency)
|
|
519
|
+
interface TelegramCallbackQuery {
|
|
520
|
+
id: string;
|
|
521
|
+
data?: string;
|
|
522
|
+
message?: { chat?: { id: number }; message_id?: number };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
interface TelegramMessage {
|
|
526
|
+
message_id: number;
|
|
527
|
+
chat: { id: number };
|
|
528
|
+
text?: string;
|
|
529
|
+
from?: { id: number; first_name?: string };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
interface TelegramApiResponse {
|
|
533
|
+
ok: boolean;
|
|
534
|
+
result?: { message_id?: number; update_id?: number; callback_query?: TelegramCallbackQuery; message?: TelegramMessage }[] & { message_id?: number };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Split a message into chunks that fit within a max length.
|
|
539
|
+
* HTML-tag-aware: closes open tags at the end of each chunk and reopens
|
|
540
|
+
* them at the start of the next, so Telegram's HTML parser doesn't choke.
|
|
541
|
+
*/
|
|
542
|
+
function splitMessage(text: string, maxLength: number): string[] {
|
|
543
|
+
if (text.length <= maxLength) return [text];
|
|
544
|
+
|
|
545
|
+
const chunks: string[] = [];
|
|
546
|
+
let remaining = text;
|
|
547
|
+
|
|
548
|
+
while (remaining.length > 0) {
|
|
549
|
+
if (remaining.length <= maxLength) {
|
|
550
|
+
chunks.push(remaining);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Find a split point: prefer newline near the limit, avoid splitting inside a tag
|
|
555
|
+
let splitAt = remaining.lastIndexOf('\n', maxLength);
|
|
556
|
+
if (splitAt <= 0) splitAt = remaining.lastIndexOf(' ', maxLength);
|
|
557
|
+
if (splitAt <= 0) splitAt = maxLength;
|
|
558
|
+
|
|
559
|
+
// Don't split inside an HTML tag — back up to before the '<'
|
|
560
|
+
const lastOpen = remaining.lastIndexOf('<', splitAt);
|
|
561
|
+
const lastClose = remaining.lastIndexOf('>', splitAt);
|
|
562
|
+
if (lastOpen > lastClose) {
|
|
563
|
+
// We're inside a tag — split before it
|
|
564
|
+
splitAt = lastOpen;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let chunk = remaining.slice(0, splitAt);
|
|
568
|
+
remaining = remaining.slice(splitAt).replace(/^\n/, '');
|
|
569
|
+
|
|
570
|
+
// Find unclosed tags in this chunk and close them
|
|
571
|
+
const openTags: string[] = [];
|
|
572
|
+
const tagRegex = /<\/?([a-z]+)[^>]*>/gi;
|
|
573
|
+
let m: RegExpExecArray | null;
|
|
574
|
+
while ((m = tagRegex.exec(chunk)) !== null) {
|
|
575
|
+
const tagName = m[1].toLowerCase();
|
|
576
|
+
if (m[0].startsWith('</')) {
|
|
577
|
+
// Closing tag — pop the last matching open tag
|
|
578
|
+
const idx = openTags.lastIndexOf(tagName);
|
|
579
|
+
if (idx !== -1) openTags.splice(idx, 1);
|
|
580
|
+
} else if (!m[0].endsWith('/>')) {
|
|
581
|
+
openTags.push(tagName);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Close remaining open tags at end of chunk (reverse order)
|
|
586
|
+
for (let i = openTags.length - 1; i >= 0; i--) {
|
|
587
|
+
chunk += `</${openTags[i]}>`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Reopen them at the start of the next chunk
|
|
591
|
+
if (openTags.length > 0 && remaining.length > 0) {
|
|
592
|
+
remaining = openTags.map(t => `<${t}>`).join('') + remaining;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
chunks.push(chunk);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return chunks;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function escapeHtml(text: string): string {
|
|
602
|
+
return text
|
|
603
|
+
.replace(/&/g, '&')
|
|
604
|
+
.replace(/</g, '<')
|
|
605
|
+
.replace(/>/g, '>');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Convert markdown to Telegram-compatible HTML */
|
|
609
|
+
export function markdownToTelegramHtml(md: string): string {
|
|
610
|
+
// 1. Extract fenced code blocks — preserve content, no markdown processing inside
|
|
611
|
+
const codeBlocks: string[] = [];
|
|
612
|
+
let text = md.replace(/```\w*\n?([\s\S]*?)```/g, (_, content: string) => {
|
|
613
|
+
const idx = codeBlocks.length;
|
|
614
|
+
codeBlocks.push(`<pre>${escapeHtml(content.replace(/\n$/, ''))}</pre>`);
|
|
615
|
+
return `\x00CB${idx}\x00`;
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// 2. Extract inline code
|
|
619
|
+
const inlineCodes: string[] = [];
|
|
620
|
+
text = text.replace(/`([^`\n]+)`/g, (_, content: string) => {
|
|
621
|
+
const idx = inlineCodes.length;
|
|
622
|
+
inlineCodes.push(`<code>${escapeHtml(content)}</code>`);
|
|
623
|
+
return `\x00IC${idx}\x00`;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// 3. Escape HTML in remaining text
|
|
627
|
+
text = escapeHtml(text);
|
|
628
|
+
|
|
629
|
+
// 4. Convert markdown patterns
|
|
630
|
+
// Bold: **text** or __text__
|
|
631
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
632
|
+
text = text.replace(/__(.+?)__/g, '<b>$1</b>');
|
|
633
|
+
// Strikethrough: ~~text~~
|
|
634
|
+
text = text.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
635
|
+
// Headers: # text → bold
|
|
636
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
|
|
637
|
+
// Markdown links: [text](url)
|
|
638
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
639
|
+
|
|
640
|
+
// 5. Re-insert preserved blocks
|
|
641
|
+
text = text.replace(/\x00CB(\d+)\x00/g, (_, idx) => codeBlocks[Number(idx)]);
|
|
642
|
+
text = text.replace(/\x00IC(\d+)\x00/g, (_, idx) => inlineCodes[Number(idx)]);
|
|
643
|
+
|
|
644
|
+
return text;
|
|
645
|
+
}
|