Skip to content

fix(telegram): improve error message for 403 bot-not-member errors#48296

Open
Jimmy-xuzimo wants to merge 18 commits intoopenclaw:mainfrom
Jimmy-xuzimo:fix/telegram-bot-not-member-error
Open

fix(telegram): improve error message for 403 bot-not-member errors#48296
Jimmy-xuzimo wants to merge 18 commits intoopenclaw:mainfrom
Jimmy-xuzimo:fix/telegram-bot-not-member-error

Conversation

@Jimmy-xuzimo
Copy link
Copy Markdown
Contributor

Summary

Fixes #48273

When Telegram returns a 403 "bot is not a member of the channel chat" error, users now receive a clear, actionable error message instead of the raw API error.

Problem

Users configuring Telegram outbound messaging were seeing confusing errors like:

403: Forbidden: bot is not a member of the channel chat

This error occurs when:

  1. Bot tries to send to a channel but isn't a member/admin
  2. Bot was removed from a group
  3. User hasn't started a DM conversation with the bot

The raw API error doesn't explain how to fix the issue.

Solution

Added error handling to detect the "bot not member" error and provide actionable guidance:

Changes

  • src/telegram/send.ts:
    • Added BOT_NOT_MEMBER_RE regex pattern to detect bot membership errors
    • Added wrapTelegramBotNotMemberError() function with helpful context
    • Updated createRequestWithChatNotFound() to also handle bot-not-member errors

New Error Message

Instead of the raw API error, users now see:

Telegram send failed: bot is not a member of the chat (chat_id=-1001234567890).
For channels: add the bot as an administrator or member first.
For groups: the bot may have been removed; re-add it to the group.
For DMs: ensure the user has started a conversation with the bot (/start).
Input was: "-1001234567890".

Testing

  • Added 2 new test cases in src/telegram/send.test.ts:
    • Message send to channel with bot-not-member error
    • Message send to group with bot-not-member error
  • Verified regex matches various error message formats (case-insensitive)
  • Confirmed error messages include chat_id and actionable guidance

Impact

  • Improves DX (Developer Experience) for Telegram integration setup
  • Reduces support burden by providing self-service error resolution
  • No breaking changes - only error message formatting is affected

Related

The 🥱 (yawn) emoji has a negative connotation of boredom, which can be
interpreted as the AI being bored with the user's request. Changing it to
⏳ (hourglass) better represents the 'operation in progress but taking longer
than expected' meaning of the stallSoft state.

This also aligns the implementation with the documented default in the
StatusReactionEmojis type definition.

Fixes openclaw#28602
When memory.backend is set to "qmd" but the qmd binary is not installed,
the system now:

1. Shows a clear warning during 'openclaw doctor' with actionable fix steps
2. Logs a warning at startup when QMD fallback activates
3. Provides helpful error messages pointing to installation docs

This prevents silent failures where users think QMD is working but
it's actually falling back to builtin backend without notice.

Fixes openclaw#25910
- Remove unused 'version' variable in checkQmdBinaryAvailable()
- Fix logic bug: skip QMD creation when binary is unavailable
- Restore QMD cache fast path before binary probe to avoid latency regression
…oad 'spawn' error pattern that masks real errors - Revert unrelated emoji change (stallSoft back to 🥱)
- Add cwd parameter to checkQmdBinaryAvailable() to probe from agent workspace
  This ensures relative paths (e.g., ./bin/qmd) resolve correctly against
  the agent workspace instead of the gateway process directory.

- Limit Windows .cmd extension to known shim names (qmd, npm, npx)
  This prevents breaking custom executables that rely on PATHEXT
  resolution (e.g., my-qmd-wrapper -> my-qmd-wrapper.exe).

- Update callers in search-manager.ts and doctor-memory-search.ts to
  pass the appropriate working directory.
Fix unescaped dollar signs in Brave Search documentation that were
causing incorrect rendering in Mintlify/MDX.

- docs/brave-search.md: Escape /month as \/month
- docs/tools/web.md: Escape /month as \/month
- docs/reference/api-usage-costs.md: Escape /month as \/month

Fixes openclaw#44979
- Fix Windows executable resolution to use PATH-only search (avoid CWD)
- Fix explicit path handling for commands like C:\tools\qmd
- Fix timeout test to actually exercise timeout mechanism using mocks
- Import resolveWindowsExecutablePath from plugin-sdk/windows-spawn
- Use resolveCliSpawnInvocation to handle Windows npm shims consistently
  with runtime spawn semantics
- Remove unused resolveWindowsExecutablePath import
- Simplify timeout test to avoid mocking issues
Convert ANTHROPIC_MODEL_ALIASES from a const to a lazy-initialized function
getAnthropicModelAliases() to avoid Temporal Dead Zone (TDZ) issues during
module loading.

The error occurred when parseModelRef was called during config loading before
the ANTHROPIC_MODEL_ALIASES constant was initialized.

Fixes openclaw#45057
…x compatibility

Fixes openclaw#48279

ACP TypeScript SDK v0.16.0 renamed unstable_listSessions to listSessions.
This change updates the AcpGatewayAgent class to use the new method name,
restoring compatibility with the latest SDK version.

Changes:
- Rename unstable_listSessions method to listSessions in translator.ts
- Add comprehensive unit tests for listSessions functionality

Testing:
- All 6 new unit tests pass
- Verified default and custom limit handling
- Tested empty session list and metadata inclusion
Fixes openclaw#48273

When Telegram returns a 403 'bot is not a member of the channel chat' error,
users now receive a clear, actionable error message instead of the raw API error.

Changes:
- Add BOT_NOT_MEMBER_RE regex to detect bot membership errors
- Add wrapTelegramBotNotMemberError function with helpful context
- Update createRequestWithChatNotFound to also handle bot-not-member errors
- Add comprehensive unit tests for the new error handling

The new error message provides specific guidance based on chat type:
- For channels: add the bot as an administrator or member
- For groups: re-add the bot if it was removed
- For DMs: ensure the user started a conversation with /start

Testing:
- Added 2 new test cases for bot-not-member error handling
- Verified regex matches various error message formats
- Confirmed error messages include chat_id and actionable guidance
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 16, 2026

🔒 Aisle Security Analysis

We found 4 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium DoS risk: status requests spawn external qmd --version process on every call (no caching/concurrency control)
2 🟡 Medium Untrusted search path leads to unintended binary execution in checkQmdBinaryAvailable (CWE-426)
3 🔵 Low Unbounded limit in ACP listSessions can trigger large session enumeration (DoS)
4 🔵 Low Sensitive recipient target leaked via Telegram bot-not-member error message (logged/persisted)

1. 🟡 DoS risk: status requests spawn external qmd --version process on every call (no caching/concurrency control)

Property Value
Severity Medium
CWE CWE-400
Location src/memory/search-manager.ts:38-45

Description

getMemorySearchManager() now probes QMD availability by executing qmd --version via checkQmdBinaryAvailable(...) even for purpose: "status" calls, and status managers are explicitly not cached.

This creates a resource-exhaustion vector:

  • Any caller that can trigger a status check (e.g. the gateway method doctor.memory.status, which is in operator.read scope) can cause a new OS process spawn per request.
  • There is no memoization, TTL, or in-flight de-duplication of the probe; concurrent/polled status calls can accumulate child processes.
  • The probe uses a 5s timeout, so a slow/hung qmd binary (or misconfiguration) can tie up resources longer per request.

Vulnerable code:

const qmdCheck = await checkQmdBinaryAvailable(resolved.qmd.command, 5000, workspaceDir);

Recommendation

Avoid spawning a process per status request.

Mitigations (one or combine):

  1. Cache probe results per (agentId, resolved.qmd.command) with a TTL (e.g. 30–300s), and reuse the last result for purpose: "status".
  2. De-duplicate in-flight probes so concurrent calls await the same Promise.
  3. Consider a cheaper probe (e.g. which/where lookup or fs.access for explicit paths) and only run --version on first use.

Example (sketch):

const QMD_PROBE_CACHE = new Map<string, { ts: number; result: QmdProbeResult; inFlight?: Promise<QmdProbeResult> }>();

async function getQmdProbe(key: string, run: () => Promise<QmdProbeResult>) {
  const now = Date.now();
  const entry = QMD_PROBE_CACHE.get(key);
  if (entry?.result && now - entry.ts < 60_000) return entry.result;
  if (entry?.inFlight) return await entry.inFlight;
  const inFlight = run().finally(() => {
    const cur = QMD_PROBE_CACHE.get(key);
    if (cur) delete cur.inFlight;
  });
  QMD_PROBE_CACHE.set(key, { ts: now, result: entry?.result ?? { available: false, error: "" } as any, inFlight });
  const result = await inFlight;
  QMD_PROBE_CACHE.set(key, { ts: Date.now(), result });
  return result;
}

2. 🟡 Untrusted search path leads to unintended binary execution in checkQmdBinaryAvailable (CWE-426)

Property Value
Severity Medium
CWE CWE-426
Location src/memory/backend-config.ts:375-398

Description

The new checkQmdBinaryAvailable() helper executes a user-supplied/bare command name via execFile() to probe availability.

On Windows (and in some PATH configurations), if the requested command is not found on PATH, resolveCliSpawnInvocation() can leave spawnInvocation.command as a bare name (e.g. "qmd"). execFile()/spawn() then falls back to OS search rules which include the current working directory. This can lead to execution of an attacker-planted qmd.exe/qmd.cmd in the process CWD when the user runs openclaw from an untrusted directory.

Key points:

  • Input: command ultimately comes from config (memory.qmd.command) or callers.
  • The function intends to check “on PATH”, but it may execute a same-named binary from the working directory when PATH resolution fails.
  • The provided cwd is ignored for bare command names (effectiveCwd becomes undefined), which means Node uses process.cwd().

Vulnerable code:

const effectiveCwd = hasPath ? cwd : undefined;

await execFileAsync(spawnInvocation.command, spawnInvocation.argv, {
  cwd: effectiveCwd,
  shell: spawnInvocation.shell,
});

Attack scenario (Windows):

  • User configures memory.backend=qmd but does not have QMD installed / not on PATH.
  • User runs openclaw from an untrusted folder containing a malicious qmd.cmd/qmd.exe.
  • checkQmdBinaryAvailable("qmd") executes the planted binary during “doctor/status” flows, causing local code execution with the privileges of the OpenClaw process.

Recommendation

Harden the probe so it does not execute a bare command that failed PATH resolution.

Recommended changes (Windows-focused):

  1. Avoid executing when PATH resolution did not yield a path (prevents CWD hijacking):
const spawnInvocation = resolveCliSpawnInvocation({ /* ... */ });// If the user provided a bare name, require resolution to an explicit path.
if (!hasPath && process.platform === "win32" && !/[\\/]/.test(spawnInvocation.command)) {
  return {
    available: false,
    error: `QMD binary "${command}" not found on PATH. Please install QMD or check your configuration.`,
  };
}
  1. Consider setting allowShellFallback: false for the probe (or exposing it via resolveCliSpawnInvocation) so the availability check does not rely on shell execution.

  2. Optionally, resolve executables without running them by iterating PATH entries and checking fs.access() (and PATHEXT on Windows) instead of spawning.


3. 🔵 Unbounded limit in ACP listSessions can trigger large session enumeration (DoS)

Property Value
Severity Low
CWE CWE-400
Location src/acp/translator.ts:219-222

Description

The ACP listSessions method forwards a client-controlled limit from params._meta to the gateway without any upper bound.

  • Input (user-controlled): params._meta.limit in the ACP request
  • Weak validation: readNumber only checks typeof value === "number" && Number.isFinite(value) (no min/max/integer enforcement)
  • Sink: forwarded to this.gateway.request("sessions.list", { limit })
  • Downstream: gateway sessions.list schema enforces only minimum: 1 for limit (no maximum), so extremely large values are accepted

Impact:

  • A malicious/buggy ACP client can request an excessively large limit, causing the gateway to return all sessions from disk-backed store.
  • This can create a very large JSON response and high CPU/memory usage in both gateway and ACP process, potentially leading to degraded performance or denial of service (CWE-400).

Vulnerable code:

async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
  const limit = readNumber(params._meta, ["limit"]) ?? 100;
  const result = await this.gateway.request<SessionsListResult>("sessions.list", { limit });// ...
}

Recommendation

Enforce a reasonable upper bound for limit (and coerce to an integer) either at the ACP layer, the gateway layer, or both.

Example fix in src/acp/translator.ts:

const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 200; // choose based on UX/perf

const raw = readNumber(params._meta, ["limit"]);
const limit = Math.min(
  MAX_LIMIT,
  Math.max(1, Math.floor(raw ?? DEFAULT_LIMIT)),
);

Additionally, update the gateway schema to reject overly large limits:

// src/gateway/protocol/schema/sessions.ts
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),

This prevents resource exhaustion even if another caller bypasses ACP and calls sessions.list directly.


4. 🔵 Sensitive recipient target leaked via Telegram bot-not-member error message (logged/persisted)

Property Value
Severity Low
CWE CWE-532
Location src/telegram/send.ts:399-411

Description

The new wrapTelegramBotNotMemberError wraps Telegram API errors by creating a new Error whose message includes the unredacted input parameter:

  • params.input is ultimately the caller-provided to target (chat id / @​username / t.me link / internal prefixes), passed in createRequestWithChatNotFound({ input: to }).
  • The wrapped error message is then:
    • logged through withTelegramApiErrorLogging() (which logs formatErrorMessage(err)), and
    • persisted into the outbound delivery queue as lastError (via failDelivery(..., err.message, ...)).

This can cause information disclosure of recipient identifiers (chat IDs/usernames/links) into logs and on-disk queue state, and can also enable log amplification (very long to strings) because there is no truncation/size bound.

Vulnerable code:

`Input was: ${JSON.stringify(params.input)}.`

While JSON.stringify helps avoid newline/ANSI escape log-forging (it escapes control chars), it does not address privacy (PII) or log-size concerns.

Recommendation

Avoid embedding raw request/target input directly in error messages that may be logged/persisted.

Recommended changes:

  • Redact + truncate the input before adding it to the message, or omit it by default.
  • Prefer structured error metadata (fields) over concatenating into the human message.

Example (redact + truncate):

function safePreview(value: string, max = 128): string {
  const truncated = value.length > max ? value.slice(0, max) + "…" : value;
  return redactSensitiveText(truncated);
}// ...
`Input was: ${JSON.stringify(safePreview(params.input))}.`

Or: remove Input was: from the thrown error message and attach it as non-logged diagnostic data when verbose/diagnostic flags are enabled.


Analyzed PR: #48296 at commit 311ad68

Last updated on: 2026-03-16T16:08:22Z

@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation app: web-ui App: web-ui commands Command implementations agents Agent runtime and tooling size: L labels Mar 16, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR adds actionable error handling for Telegram 403 "bot is not a member" errors, following the same pattern as the existing wrapTelegramChatNotFoundError. The regex (BOT_NOT_MEMBER_RE) correctly covers all chat types without anchoring to a specific type suffix. The change is accompanied by several unrelated but clean improvements: a QMD binary availability probe before manager creation, a listSessions API stabilization for ACP SDK v0.16.x, a TDZ fix for model alias initialization, and a form coercion fix for Discord-style numeric IDs.

  • src/telegram/send.ts — New BOT_NOT_MEMBER_RE and wrapTelegramBotNotMemberError() are correctly chained in createRequestWithChatNotFound; regex covers channels, supergroups, and groups.
  • src/telegram/send.test.ts — The "message send to group" test uses the same "channel chat" error string as the channel test instead of a group-specific variant (e.g. "bot was kicked from the group chat"), leaving the group-specific regex path untested.
  • src/memory/backend-config.tscheckQmdBinaryAvailable() is solid; the hasPath guard intentionally prevents workspace cwd from being used for bare command names, which is a good security boundary.
  • src/memory/search-manager.ts — Switches from the manager-runtime.ts re-export shim to ./manager.js directly; functionally identical since manager-runtime.ts only re-exports manager.ts.
  • ui/src/ui/controllers/config/form-coerce.ts — The hasStringVariant short-circuit intentionally skips number coercion for string | number unions to prevent numeric IDs from being coerced; behavior is intentional and well-documented.

Confidence Score: 4/5

  • Safe to merge; the Telegram error improvement is correct and the unrelated refactors are clean, with only a minor test coverage gap in the group scenario.
  • The core Telegram change is implemented correctly with a well-scoped regex and proper chaining. The only substantive concern is that the "message send to group" test uses the wrong error string (reuses the "channel chat" error), meaning the regex is never exercised against group-specific error variants in the test suite. All other changes are low-risk refactors or additions.
  • src/telegram/send.test.ts — the group test case should use a group-specific error string to actually cover that regex branch.

Comments Outside Diff (1)

  1. src/telegram/send.test.ts, line 810-824 (link)

    "Group" test case uses wrong error string

    The second test case is named "message send to group" but it throws the same error string as the channel test ("bot is not a member of the channel chat"), not a group-specific variant like "bot is not a member of the group chat" or "bot was kicked from the group chat".

    The BOT_NOT_MEMBER_RE regex is /403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i — both group-specific variants would match, but this test doesn't actually exercise them. If the regex were accidentally narrowed in a future refactor (e.g., someone adds of the channel to it), this test would not catch the regression.

    Consider using a group-appropriate error message to make the test actually verify coverage for the group scenario:

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/telegram/send.test.ts
Line: 810-824

Comment:
**"Group" test case uses wrong error string**

The second test case is named `"message send to group"` but it throws the same error string as the channel test (`"bot is not a member of the channel chat"`), not a group-specific variant like `"bot is not a member of the group chat"` or `"bot was kicked from the group chat"`.

The `BOT_NOT_MEMBER_RE` regex is `/403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i` — both group-specific variants would match, but this test doesn't actually exercise them. If the regex were accidentally narrowed in a future refactor (e.g., someone adds ` of the channel` to it), this test would not catch the regression.

Consider using a group-appropriate error message to make the test actually verify coverage for the group scenario:

```suggestion
        {
          name: "message send to group",
          run: async () => {
            const chatId = "-456";
            const err = new Error("403: Forbidden: bot was kicked from the group chat");
            const sendMessage = vi.fn().mockRejectedValue(err);
            const api = { sendMessage } as unknown as {
              sendMessage: typeof sendMessage;
            };
            await expectBotNotMemberWithChatId(
              sendMessageTelegram(chatId, "hi", { token: "tok", api }),
              chatId,
            );
          },
        },
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 311ad68

const MESSAGE_NOT_MODIFIED_RE =
/400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i;
const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot is not a member of the channel chat/i;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex only matches "channel chat", misses supergroup and group variants

The current regex anchors to the literal string "channel chat", but the Telegram Bot API returns different error messages depending on the chat type:

  • Channels → "bot is not a member of the channel chat"
  • Supergroups → "bot is not a member of the supergroup chat"
  • Regular groups → "bot is not a member of the group chat" (or "bot was kicked from the group chat")

The PR description explicitly claims to handle the "bot was removed from a group" scenario, but the current regex will silently miss those cases and fall through to the raw Telegram error.

Suggested change
const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot is not a member of the channel chat/i;
const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/send.ts
Line: 106

Comment:
**Regex only matches "channel chat", misses supergroup and group variants**

The current regex anchors to the literal string `"channel chat"`, but the Telegram Bot API returns different error messages depending on the chat type:
- Channels → `"bot is not a member of the channel chat"`
- Supergroups → `"bot is not a member of the supergroup chat"`
- Regular groups → `"bot is not a member of the group chat"` (or `"bot was kicked from the group chat"`)

The PR description explicitly claims to handle the "bot was removed from a group" scenario, but the current regex will silently miss those cases and fall through to the raw Telegram error.

```suggestion
const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i;
```

How can I resolve this? If you propose a fix, please make it concise.

Addresses review comment on PR openclaw#48296

The previous regex only matched 'channel chat' errors, but Telegram API
returns different messages for different chat types:
- Channels: 'bot is not a member of the channel chat'
- Supergroups: 'bot is not a member of the supergroup chat'
- Groups: 'bot is not a member of the group chat' or 'bot was kicked from the group chat'

Updated regex to match all variants:
/403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i

This ensures consistent error handling across all chat types.
@Jimmy-xuzimo
Copy link
Copy Markdown
Contributor Author

@greptile-apps 感谢审查评论!我已修复了正则表达式。

修复内容:

  • BOT_NOT_MEMBER_RE 从只匹配 "channel chat" 更新为匹配所有聊天类型
  • 新的正则表达式:/403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i

现在匹配的错误消息:

  • Channels: "bot is not a member of the channel chat"
  • Supergroups: "bot is not a member of the supergroup chat"
  • Groups: "bot is not a member of the group chat" 或 "bot was kicked from the group chat"

改进效果:

  • 确保所有聊天类型的一致性错误处理
  • 用户在所有场景下都能收到可操作的错误消息

请查看最新的提交 311ad686a

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e47f9b356

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Check if string type is an option - if so, preserve string values as-is
// This fixes issues like Discord IDs (numeric-looking strings) being coerced to numbers
const hasStringVariant = variants.some((v) => schemaType(v) === "string");
if (typeof value === "string" && hasStringVariant) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore numeric coercion for string/number unions

This condition short-circuits all string inputs whenever a union contains a string variant, so anyOf/oneOf schemas like [{type:"number"},{type:"string"}] now keep values such as "30" as strings and never reach the numeric coercion block below. That changes persisted types for mixed-union fields and regresses the previous behavior expected by the config-form coercion tests, so numeric union inputs can now be serialized with the wrong type.

Useful? React with 👍 / 👎.

}
// Binary exists but --version failed (non-zero exit) - still treat as available
// This handles older QMD versions that may not support --version flag
return { available: true, path: spawnInvocation.command };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark non-version qmd probe failures as unavailable

The catch-all fallback returns available: true for every non-"not found" error, which incorrectly treats execution failures like EACCES/EPERM (or other unusable binary states) as healthy. In that scenario the doctor/check path reports QMD available, but runtime initialization still fails and silently falls back to builtin memory, so users get misleading health diagnostics instead of actionable remediation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: web-ui App: web-ui commands Command implementations docs Improvements or additions to documentation size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Telegram outbound sendMessage fails with "403 bot is not a member" despite working bot token and inbound messages working

1 participant