* fix(telegram): auto-wrap file references with TLD extensions to prevent URL previews
Telegram's auto-linker aggressively treats filenames like HEARTBEAT.md,
README.md, main.go, script.py as URLs and generates domain registrar previews.
This fix adds comprehensive protection for file extensions that share TLDs:
- High priority: .md, .go, .py, .pl, .ai, .sh
- Medium priority: .io, .tv, .fm, .am, .at, .be, .cc, .co
Implementation:
- Added wrapFileReferencesInHtml() in format.ts
- Runs AFTER markdown→HTML conversion
- Tokenizes HTML to respect tag boundaries
- Skips content inside <code>, <pre>, <a> tags (no nesting issues)
- Applied to all rendering paths: renderTelegramHtmlText, markdownToTelegramHtml,
markdownToTelegramChunks, and delivery.ts fallback
Addresses review comments:
- P1: Now handles chunked rendering paths correctly
- P2: No longer wraps inside existing code blocks (token-based parsing)
- No lookbehinds used (broad Node compatibility)
Includes comprehensive test suite in format.wrap-md.test.ts
AI-assisted: true
* fix(telegram): prevent URL previews for file refs with TLD extensions
Two layers were causing spurious link previews for file references like
`README.md`, `backup.sh`, `main.go`:
1. **markdown-it linkify** converts `README.md` to
`<a href="http://README.md">README.md</a>` (.md = Moldova TLD)
2. **Telegram auto-linker** treats remaining bare text as URLs
## Changes
### Primary fix: suppress auto-linkified file refs in buildTelegramLink
- Added `isAutoLinkedFileRef()` helper that detects when linkify auto-
generated a link from a bare filename (href = "http://" + label)
- Rejects paths with domain-like segments (dots in non-final path parts)
- Modified `buildTelegramLink()` to return null for these, so file refs
stay as plain text and get wrapped in `<code>` by the wrapper
### Safety-net: de-linkify in wrapFileReferencesInHtml
- Added pre-pass that catches auto-linkified anchors in pre-rendered HTML
- Handles edge cases where HTML is passed directly (textMode: "html")
- Reuses `isAutoLinkedFileRef()` logic — no duplication
### Bug fixes discovered during review
- **Fixed `isClosing` bug (line 169)**: the check `match[1] === "/"`
was wrong — the regex `(<\/?)}` captures `<` or `</`, so closing
tags were never detected. Changed to `match[1] === "</"`. This was
causing `inCode/inPre/inAnchor` to stay stuck at true after any
opening tag, breaking file ref wrapping after closing tags.
- **Removed double `wrapFileReferencesInHtml` call**: `renderTelegramHtmlText`
was calling `markdownToTelegramHtml` (which wraps) then wrapping again.
### Test coverage (+12 tests, 26 total)
- `.sh` filenames (original issue #6932 mentioned backup.sh)
- Auto-linkified anchor replacement
- Auto-linkified path anchor replacement
- Explicit link preservation (different label)
- File ref after closing anchor tag (exercises isClosing fix)
- Multiple file types in single message
- Real URL preservation
- Explicit markdown link preservation
- File ref after real URL in same message
- Chunked output file ref wrapping
Closes#6932
* test(telegram): add comprehensive edge case coverage for file ref wrapping
Add 16 edge case tests covering:
- File refs inside bold/italic tags
- Fenced code blocks (no double-wrap)
- Domain-like paths preserved as links (example.com/README.md)
- GitHub URLs with file paths
- wrapFileRefs: false behavior
- All TLD extensions (.ai, .io, .tv, .fm)
- Non-TLD extensions not wrapped (.png, .css, .js)
- File ref position (start, end, multiple in sequence)
- Nested paths without domain segments
- Version-like paths (v1.0/README.md wraps, example.com/v1.0/README.md links)
- Hyphens and underscores in filenames
- Uppercase extensions
* fix(telegram): use regex literal and depth counters for tag tracking
Code review fixes:
1. Replace RegExp constructor with regex literal for autoLinkedAnchor
- Avoids double-escaping issues with \s
- Uses backreference \1 to match href=label pattern directly
2. Replace boolean toggles with depth counters for tag nesting
- codeDepth, preDepth, anchorDepth track nesting levels
- Correctly handles nested tags like <pre><code>...</code></pre>
- Prevents wrapping inside any level of protected tags
Add 4 tests for edge cases:
- Nested code tags (depth tracking)
- Multiple anchor tags in sequence
- Auto-linked anchor with backreference match
- Anchor with different href/label (no match)
* fix(telegram): add escapeHtml and escapeRegex for defense in depth
Code review fixes:
1. Escape filename with escapeHtml() before inserting into <code> tags
- Prevents HTML injection if regex ever matches unsafe chars
- Defense in depth (current regex already limits to safe chars)
2. Escape extensions with escapeRegex() before joining into pattern
- Prevents regex breakage if extensions contain metacharacters
- Future-proofs against extensions like 'c++' or 'd.ts'
Add tests documenting regex safety boundaries:
- Filenames with special chars (&, <, >) don't match
- Only [a-zA-Z0-9_.\-./] chars are captured
* fix(telegram): catch orphaned single-letter TLD patterns
When text like 'R&D.md' doesn't match the main file pattern (because &
breaks the character class), the 'D.md' part can still be auto-linked
by Telegram as a domain (https://d.md/).
Add second pass to catch orphaned TLD patterns like 'D.md', 'R.io', 'X.ai'
that follow non-alphanumeric characters and wrap them in <code> tags.
Pattern: ([^a-zA-Z0-9]|^)([A-Za-z]\.(?:extensions))(?=[^a-zA-Z0-9/]|$)
Tests added:
- 'wraps orphaned TLD pattern after special character' (R&D.md → R&<code>D.md</code>)
- 'wraps orphaned single-letter TLD patterns' (X.ai, R.io)
* refactor(telegram): remove popular domain TLDs from file extension list
Remove .ai, .io, .tv, .fm from FILE_EXTENSIONS_WITH_TLD because:
- These are commonly used as real domains (x.ai, vercel.io, github.io)
- Rarely used as actual file extensions
- Users are more likely referring to websites than files
Keep: md, sh, py, go, pl (common file extensions, rarely intentional domains)
Keep: am, at, be, cc, co (less common as intentional domain references)
Update tests to reflect the change:
- Add test for supported extensions (.am, .at, .be, .cc, .co)
- Add test verifying popular TLDs stay as links
* fix(telegram): prevent orphaned TLD wrapping inside HTML tags
Code review fixes:
1. Orphaned TLD pass now checks if match is inside HTML tag
- Uses lastIndexOf('<') vs lastIndexOf('>') to detect tag context
- Skips wrapping when between < and > (inside attributes)
- Prevents invalid HTML like <a href="...&<code>D.md</code>">
2. textMode: 'html' now trusts caller markup
- Returns text unchanged instead of wrapping
- Caller owns HTML structure in this mode
Tests added:
- 'does not wrap orphaned TLD inside href attributes'
- 'does not wrap orphaned TLD inside any HTML attribute'
- 'does not wrap in HTML mode (trusts caller markup)'
* refactor(telegram): use snapshot for orphaned TLD offset clarity
Use explicit snapshot variable when checking tag positions in orphaned
TLD pass. While JavaScript's replace() doesn't mutate during iteration,
this makes intent explicit and adds test coverage for multi-TLD HTML.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(telegram): prevent orphaned TLD wrapping inside code/pre tags
- Add depth tracking for code/pre tags in orphaned TLD pass
- Fix test to expect valid HTML output
- 55 tests now covering nested tag scenarios
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(telegram): clamp depth counters and add anchor tracking to orphaned pass
- Clamp depth counters at 0 for malformed HTML with stray closing tags
- Add anchor depth tracking to orphaned TLD pass to prevent wrapping
inside link text (e.g., <a href="...">R&D.md</a>)
- 57 tests covering all edge cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(telegram): keep .co domains linked and wrap punctuated file refs
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(security): default standalone servers to loopback bind (#4)
Change canvas host and telegram webhook default bind from 0.0.0.0
(all interfaces) to 127.0.0.1 (loopback only) to prevent unintended
network exposure when no explicit host is configured.
* fix: restore telegram webhook host override while keeping loopback defaults (openclaw#13184) thanks @davidrudduck
* style: format telegram docs after rebase (openclaw#13184) thanks @davidrudduck
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(pairing): show actual code in approval command instead of placeholder
The pairing reply shown to new users included the approval command with
a literal '<code>' placeholder. Users had to manually copy the code
from one line and substitute it into the command.
Now shows the ready-to-copy command with the real pairing code:
Before: openclaw pairing approve telegram <code>
After: openclaw pairing approve telegram abc123
Fixed in both the shared pairing message builder and the Telegram
inline pairing reply.
* test(pairing): update test to expect actual code instead of placeholder
---------
Co-authored-by: Echo Ito <echoito@MacBook-Air.local>
Telegram DM access-control incorrectly used chatId as the allowFrom match key.
For DMs, allowFrom entries are typically Telegram user ids (msg.from.id) and/or @usernames. Using chatId causes legitimate DMs to be rejected or silently dropped even when dmPolicy is open/allowlist.
This change matches allowFrom against the sender's user id when available, falling back to chatId only if msg.from.id is missing.
Tests: existing telegram DM/thread routing tests pass.
Closes#4515
Telegram Bot API limits setMyCommands to 100 commands per scope. When
users have many skills installed (~15+), the combined native + plugin +
custom commands can exceed this limit, causing a 400 error on every
gateway restart.
Truncate the command list to 100 (native commands first, then plugins,
then custom) and log a warning instead of failing the registration.
Fixes#11567
* feat: Implement Telegram video note support with tests and docs
* fixing lint
* feat: add doctor-state-integrity command, Telegram messaging, and PowerShell Docker setup scripts.
* Update src/telegram/send.video-note.test.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix: Set video note follow-up text to undefined for empty input and adjust caption test expectation.
* test: add assertion for `sendMessage` with reply markup and HTML parse mode in `send.video-note` test.
* docs: add changelog entry for Telegram video notes
---------
Co-authored-by: Evgenii Utkin <thewulf7@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: CLAWDINATOR Bot <clawdinator[bot]@users.noreply.github.com>
* fix: use .js extension for ESM imports of RoutePeerKind
The imports incorrectly used .ts extension which doesn't resolve
with moduleResolution: NodeNext. Changed to .js and added 'type'
import modifier.
* fix tsconfig
* refactor: unify peer kind to ChatType, rename dm to direct
- Replace RoutePeerKind with ChatType throughout codebase
- Change 'dm' literal values to 'direct' in routing/session keys
- Keep backward compat: normalizeChatType accepts 'dm' -> 'direct'
- Add ChatType export to plugin-sdk, deprecate RoutePeerKind
- Update session key parsing to accept both 'dm' and 'direct' markers
- Update all channel monitors and extensions to use ChatType
BREAKING CHANGE: Session keys now use 'direct' instead of 'dm'.
Existing 'dm' keys still work via backward compat layer.
* fix tests
* test: update session key expectations for dmdirect migration
- Fix test expectations to expect :direct: in generated output
- Add explicit backward compat test for normalizeChatType('dm')
- Keep input test data with :dm: keys to verify backward compat
* fix: accept legacy 'dm' in session key parsing for backward compat
getDmHistoryLimitFromSessionKey now accepts both :dm: and :direct:
to ensure old session keys continue to work correctly.
* test: add explicit backward compat tests for dmdirect migration
- session-key.test.ts: verify both :dm: and :direct: keys are valid
- getDmHistoryLimitFromSessionKey: verify both formats work
* feat: backward compat for resetByType.dm config key
* test: skip unix-path Nix tests on Windows
* feat(telegram): add spoiler tag support
Render markdown ||spoiler|| syntax as <tg-spoiler> tags in Telegram HTML output.
The markdown IR already parses spoiler syntax, but the Telegram renderer was
missing the style marker. This adds the spoiler marker to renderTelegramHtml().
Fixes spoiler text appearing as raw ||text|| instead of hidden text.
* fix: enable Telegram spoiler rendering (#11543) (thanks @ezhikkk)
---------
Co-authored-by: Параша <parasha@openclaw.local>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
* feat(bluebubbles): auto-strip markdown from outbound messages (#7402)
* fix(security): add timeout to webhook body reading (#6762)
Adds 30-second timeout to readBody() in voice-call, bluebubbles, and nostr
webhook handlers. Prevents Slow-Loris DoS (CWE-400, CVSS 7.5).
Merged with existing maxBytes protection in voice-call.
* fix(security): unify Error objects and lint fixes in webhook timeouts (#6762)
* fix: prevent plugins from auto-enabling without user consent (#3961)
Changes default plugin enabled state from true to false in enablePluginEntry().
Preserves existing enabled:true values. Fixes#3932.
* fix: apply hierarchical mediaMaxMb config to all channels (#8749)
Generalizes resolveAttachmentMaxBytes() to use account → channel → global
config resolution for all channels, not just BlueBubbles. Fixes#7847.
* fix(bluebubbles): sanitize attachment filenames against header injection (#10333)
Strip ", \r, \n, and \\ from filenames after path.basename() to prevent
multipart Content-Disposition header injection (CWE-93, CVSS 5.4).
Also adds sanitization to setGroupIconBlueBubbles which had zero filename
sanitization.
* fix(lint): exclude extensions/ from Oxlint preflight check (#9313)
Extensions use PluginRuntime|null patterns that trigger
no-redundant-type-constituents because PluginRuntime resolves to any.
Excluding extensions/ from Oxlint unblocks user upgrades.
Re-applies the approach from closed PR #10087.
* fix(bluebubbles): add tempGuid to createNewChatWithMessage payload (#7745)
Non-Private-API mode (AppleScript) requires tempGuid in send payloads.
The main sendMessageBlueBubbles already had it, but createNewChatWithMessage
was missing it, causing 400 errors for new chat creation without Private API.
* fix: send stop-typing signal when run ends with NO_REPLY (#8785)
Adds onCleanup callback to the typing controller that fires when the
controller is cleaned up while typing was active (e.g., after NO_REPLY).
Channels using createTypingCallbacks automatically get stop-typing on
cleanup. This prevents the typing indicator from lingering in group chats
when the agent decides not to reply.
* fix(telegram): deduplicate skill commands in multi-agent setup (#5717)
Two fixes:
1. Skip duplicate workspace dirs when listing skill commands across agents.
Multiple agents sharing the same workspace would produce duplicate commands
with _2, _3 suffixes.
2. Clear stale commands via deleteMyCommands before registering new ones.
Commands from deleted skills now get cleaned up on restart.
* fix: add size limits to unbounded in-memory caches (#4948)
Adds max-size caps with oldest-entry eviction to prevent OOM in
long-running deployments:
- BlueBubbles serverInfoCache: 64 entries (already has TTL)
- Google Chat authCache: 32 entries
- Matrix directRoomCache: 1024 entries
- Discord presenceCache: 5000 entries per account
* fix: address review concerns (#11093)
- Chain deleteMyCommands → setMyCommands to prevent race condition (#5717)
- Rename enablePluginEntry to registerPluginEntry (now sets enabled: false)
- Add Slow-Loris timeout test for readJsonBody (#6023)
* fix(telegram): accept messages from group members in allowlisted groups
Issue #4559: Telegram bot was silently dropping messages from non-paired users
in allowlisted group chats due to overly strict sender filtering.
The fix adds a check to distinguish between:
1. Group itself is allowlisted → accept messages from any member
2. Group is NOT allowlisted → only accept from allowlisted senders
Changes:
- Check if group ID is in the allowlist (or allowlist is wildcard)
- Only reject sender if they're not in allowlist AND group is not allowlisted
- Improved logging to indicate the actual reason for rejection
This preserves security controls while fixing the UX issue where group members
couldn't participate unless individually allowlisted.
Backwards compatible: existing allowlists continue to work as before.
* style: format telegram fix for oxfmt compliance
* refactor(telegram): clarify group allowlist semantics in fix for #4559
Changes:
- Rename 'isGroupInAllowlist' to 'isGroupChatIdInAllowlist' for clarity
- Expand comments to explain the semantic distinction:
* Group chat ID in allowlist -> accept any group member (fixes#4559)
* Group chat ID NOT in allowlist -> enforce sender allowlist (preserves security)
- This addresses concerns about config semantics raised in code review
The fix maintains backward compatibility:
- 'groupAllowFrom' with group chat IDs now correctly acts as group enablement
- 'groupAllowFrom' with sender IDs continues to work as sender allowlist
- Operators should use group chat IDs for group enablement, sender IDs for sender control
Note: If operators were using 'groupAllowFrom' with group IDs expecting sender-level
filtering, they should migrate to a separate sender allowlist config. This is the
intended behavior per issue #4559.
* Telegram: allow per-group groupPolicy overrides
* Telegram: support per-group groupPolicy overrides (#9775) (thanks @nicolasstanley)
---------
Co-authored-by: George Pickett <gpickett00@gmail.com>
* feat: add Claude Opus 4.6 to built-in model catalog
- Update default model from claude-opus-4-5 to claude-opus-4-6
- Add opus-4.6 model ID normalization
- Add claude-opus-4-6 to live model filter prefixes
- Update image tool to prefer claude-opus-4-6 for vision
- Add CLI backend alias for opus-4.6
- Update onboard auth default selections to include opus-4.6
- Update model picker placeholder
Closes#9811
* test: update tests for claude-opus-4-6 default
- Fix model-alias-defaults test to use claude-opus-4-6
- Fix image-tool test to expect claude-opus-4-6 in fallbacks
* feat: support claude-opus-4-6
* docs: update changelog for opus 4.6 (#9853) (thanks @TinyTb)
* chore: bump pi to 0.52.0
---------
Co-authored-by: Slurpy <slurpy@openclaw.ai>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Fixes#9545 and #9351.
When a message comes from a Telegram forum topic, the peer ID includes
the topic suffix (e.g., `-1001234567890:topic:99`). Users configure
bindings with the base group ID, which previously did not match.
This adds `parentPeer` to `resolveAgentRoute()` calls for forum groups,
enabling binding inheritance from the parent group to all topics.
- Extract `buildTelegramParentPeer()` helper in bot/helpers.ts
- Pass parentPeer in bot-message-context.ts, bot-handlers.ts,
bot-native-commands.ts, and bot.ts (reaction handler)
- Add tests for forum topic routing and topic precedence
When receiving messages in Telegram DM topics (Topics in Private Chats),
the threadId was not saved in the session's deliveryContext, causing
replies to go to General chat instead of the topic.
Now we pass threadId to updateLastRoute for DM topics.
Fixes#8891
* Telegram: remove @ts-nocheck from bot-handlers.ts, use Grammy types directly, deduplicate StickerMetadata
* Telegram: remove last @ts-nocheck from bot-handlers.ts (#9206)
* Telegram: remove @ts-nocheck from bot-message.ts, type deps via Omit<BuildTelegramMessageContextParams>
* Telegram: widen allMedia to TelegramMediaRef[] so stickerMetadata flows through
* Telegram: remove @ts-nocheck from bot-message.ts (#9180)
* Telegram: remove @ts-nocheck from bot.ts and bot-message-dispatch.ts
- bot/types.ts: TelegramContext.me uses UserFromGetMe (Grammy) instead of manual inline type
- bot.ts: remove 6 unsafe casts (as any, as unknown, as object), use Grammy types directly
- bot.ts: remove dead message_thread_id access on reactions (not in Telegram Bot API)
- bot.ts: remove resolveThreadSessionKeys import (no longer needed for reactions)
- bot-message-dispatch.ts: replace ': any' with DispatchTelegramMessageParams type
- bot-message-dispatch.ts: add sticker.fileId guard before cache access
- bot.test.ts: update reaction tests, remove dead DM thread-reaction test
* Telegram: remove duplicate bot.catch handler (only the last one runs in Grammy)
* Telegram: remove @ts-nocheck from bot.ts, fix duplicate error handler, harden sticker caching (#9077)