mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:28:37 +00:00
fix: repair Telegram allowlist DM migrations (#27936) (thanks @widingmarcus-cyber)
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Typing/Cross-channel leakage: unify run-scoped typing suppression for cross-channel/internal-webchat routes, preserve current inbound origin as embedded run message channel context, harden shared typing keepalive with consecutive-failure circuit breaker edge-case handling, and enforce dispatcher completion/idle waits in extension dispatcher callsites (Feishu, Matrix, Mattermost, MSTeams) so typing indicators always clean up on success/error paths. Related: #27647, #27493, #27598. Supersedes/replaces draft PRs: #27640, #27593, #27540.
|
- Typing/Cross-channel leakage: unify run-scoped typing suppression for cross-channel/internal-webchat routes, preserve current inbound origin as embedded run message channel context, harden shared typing keepalive with consecutive-failure circuit breaker edge-case handling, and enforce dispatcher completion/idle waits in extension dispatcher callsites (Feishu, Matrix, Mattermost, MSTeams) so typing indicators always clean up on success/error paths. Related: #27647, #27493, #27598. Supersedes/replaces draft PRs: #27640, #27593, #27540.
|
||||||
- Telegram/sendChatAction 401 handling: add bounded exponential backoff + temporary local typing suppression after repeated unauthorized failures to stop unbounded `sendChatAction` retry loops that can trigger Telegram abuse enforcement and bot deletion. (#27415) Thanks @widingmarcus-cyber.
|
- Telegram/sendChatAction 401 handling: add bounded exponential backoff + temporary local typing suppression after repeated unauthorized failures to stop unbounded `sendChatAction` retry loops that can trigger Telegram abuse enforcement and bot deletion. (#27415) Thanks @widingmarcus-cyber.
|
||||||
- Telegram/Webhook startup: clarify webhook config guidance, allow `channels.telegram.webhookPort: 0` for ephemeral listener binding, and log both the local listener URL and Telegram-advertised webhook URL with the bound port. (#25732) thanks @huntharo.
|
- Telegram/Webhook startup: clarify webhook config guidance, allow `channels.telegram.webhookPort: 0` for ephemeral listener binding, and log both the local listener URL and Telegram-advertised webhook URL with the bound port. (#25732) thanks @huntharo.
|
||||||
|
- Config/Doctor allowlist safety: reject `dmPolicy: "allowlist"` configs with empty `allowFrom`, add Telegram account-level inheritance-aware validation, and teach `openclaw doctor --fix` to restore missing `allowFrom` entries from pairing-store files when present, preventing silent DM drops after upgrades. (#27936) Thanks @widingmarcus-cyber.
|
||||||
- Browser/Chrome extension handshake: bind relay WS message handling before `onopen` and add non-blocking `connect.challenge` response handling for gateway-style handshake frames, avoiding stuck `…` badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553)
|
- Browser/Chrome extension handshake: bind relay WS message handling before `onopen` and add non-blocking `connect.challenge` response handling for gateway-style handshake frames, avoiding stuck `…` badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553)
|
||||||
- Browser/Extension relay init: dedupe concurrent same-port relay startup with shared in-flight initialization promises so callers await one startup lifecycle and receive consistent success/failure results. Landed from contributor PR #21277 by @HOYALIM. (Related #20688)
|
- Browser/Extension relay init: dedupe concurrent same-port relay startup with shared in-flight initialization promises so callers await one startup lifecycle and receive consistent success/failure results. Landed from contributor PR #21277 by @HOYALIM. (Related #20688)
|
||||||
- Browser/Fill relay + CLI parity: accept `act.fill` fields without explicit `type` by defaulting missing/empty `type` to `text` in both browser relay route parsing and `openclaw browser fill` CLI field parsing, so relay calls no longer fail when the model omits field type metadata. Landed from contributor PR #27662 by @Uface11. (#27296) Thanks @Uface11.
|
- Browser/Fill relay + CLI parity: accept `act.fill` fields without explicit `type` by defaulting missing/empty `type` to `text` in both browser relay route parsing and `openclaw browser fill` CLI field parsing, so relay calls no longer fail when the model omits field type metadata. Landed from contributor PR #27662 by @Uface11. (#27296) Thanks @Uface11.
|
||||||
|
|||||||
@@ -109,13 +109,15 @@ Token resolution order is account-aware. In practice, config values win over env
|
|||||||
`channels.telegram.dmPolicy` controls direct message access:
|
`channels.telegram.dmPolicy` controls direct message access:
|
||||||
|
|
||||||
- `pairing` (default)
|
- `pairing` (default)
|
||||||
- `allowlist`
|
- `allowlist` (requires at least one sender ID in `allowFrom`)
|
||||||
- `open` (requires `allowFrom` to include `"*"`)
|
- `open` (requires `allowFrom` to include `"*"`)
|
||||||
- `disabled`
|
- `disabled`
|
||||||
|
|
||||||
`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
|
`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
|
||||||
|
`dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation.
|
||||||
The onboarding wizard accepts `@username` input and resolves it to numeric IDs.
|
The onboarding wizard accepts `@username` input and resolves it to numeric IDs.
|
||||||
If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token).
|
If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token).
|
||||||
|
If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can auto-migrate recovered entries into `channels.telegram.allowFrom`.
|
||||||
|
|
||||||
### Finding your Telegram user ID
|
### Finding your Telegram user ID
|
||||||
|
|
||||||
@@ -716,7 +718,7 @@ Primary reference:
|
|||||||
- `channels.telegram.botToken`: bot token (BotFather).
|
- `channels.telegram.botToken`: bot token (BotFather).
|
||||||
- `channels.telegram.tokenFile`: read token from file path.
|
- `channels.telegram.tokenFile`: read token from file path.
|
||||||
- `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
|
- `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
|
||||||
- `channels.telegram.allowFrom`: DM allowlist (numeric Telegram user IDs). `open` requires `"*"`. `openclaw doctor --fix` can resolve legacy `@username` entries to IDs.
|
- `channels.telegram.allowFrom`: DM allowlist (numeric Telegram user IDs). `allowlist` requires at least one sender ID. `open` requires `"*"`. `openclaw doctor --fix` can resolve legacy `@username` entries to IDs and can restore allowlist entries from pairing-store files when available.
|
||||||
- `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
|
- `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
|
||||||
- `channels.telegram.groupAllowFrom`: group sender allowlist (numeric Telegram user IDs). `openclaw doctor --fix` can resolve legacy `@username` entries to IDs.
|
- `channels.telegram.groupAllowFrom`: group sender allowlist (numeric Telegram user IDs). `openclaw doctor --fix` can resolve legacy `@username` entries to IDs.
|
||||||
- Multi-account precedence:
|
- Multi-account precedence:
|
||||||
|
|||||||
@@ -452,6 +452,50 @@ describe("doctor config flow", () => {
|
|||||||
expect(cfg.channels.discord.accounts.work.allowFrom).toEqual(["*"]);
|
expect(cfg.channels.discord.accounts.work.allowFrom).toEqual(["*"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('repairs dmPolicy="allowlist" by restoring allowFrom from pairing store on repair', async () => {
|
||||||
|
const result = await withTempHome(async (home) => {
|
||||||
|
const configDir = path.join(home, ".openclaw");
|
||||||
|
const credentialsDir = path.join(configDir, "credentials");
|
||||||
|
await fs.mkdir(credentialsDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir, "openclaw.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
botToken: "fake-token",
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(credentialsDir, "telegram-allowFrom.json"),
|
||||||
|
JSON.stringify({ version: 1, allowFrom: ["12345"] }, null, 2),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
return await loadAndMaybeMigrateDoctorConfig({
|
||||||
|
options: { nonInteractive: true, repair: true },
|
||||||
|
confirm: async () => false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = result.cfg as {
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
dmPolicy: string;
|
||||||
|
allowFrom: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expect(cfg.channels.telegram.dmPolicy).toBe("allowlist");
|
||||||
|
expect(cfg.channels.telegram.allowFrom).toEqual(["12345"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("migrates legacy toolsBySender keys to typed id entries on repair", async () => {
|
it("migrates legacy toolsBySender keys to typed id entries on repair", async () => {
|
||||||
const result = await runDoctorConfigWithInput({
|
const result = await runDoctorConfigWithInput({
|
||||||
repair: true,
|
repair: true,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
isTrustedSafeBinPath,
|
isTrustedSafeBinPath,
|
||||||
normalizeTrustedSafeBinDirs,
|
normalizeTrustedSafeBinDirs,
|
||||||
} from "../infra/exec-safe-bin-trust.js";
|
} from "../infra/exec-safe-bin-trust.js";
|
||||||
|
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||||
import {
|
import {
|
||||||
isDiscordMutableAllowEntry,
|
isDiscordMutableAllowEntry,
|
||||||
@@ -1095,10 +1096,167 @@ function maybeRepairOpenPolicyAllowFrom(cfg: OpenClawConfig): {
|
|||||||
return { config: next, changes };
|
return { config: next, changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAllowFromEntries(list?: Array<string | number>) {
|
||||||
|
return Array.isArray(list) && list.map((v) => String(v).trim()).filter(Boolean).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeRepairAllowlistPolicyAllowFrom(cfg: OpenClawConfig): Promise<{
|
||||||
|
config: OpenClawConfig;
|
||||||
|
changes: string[];
|
||||||
|
}> {
|
||||||
|
const channels = cfg.channels;
|
||||||
|
if (!channels || typeof channels !== "object") {
|
||||||
|
return { config: cfg, changes: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllowFromMode = "topOnly" | "topOrNested" | "nestedOnly";
|
||||||
|
|
||||||
|
const resolveAllowFromMode = (channelName: string): AllowFromMode => {
|
||||||
|
if (channelName === "googlechat") {
|
||||||
|
return "nestedOnly";
|
||||||
|
}
|
||||||
|
if (channelName === "discord" || channelName === "slack") {
|
||||||
|
return "topOrNested";
|
||||||
|
}
|
||||||
|
return "topOnly";
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = structuredClone(cfg);
|
||||||
|
const changes: string[] = [];
|
||||||
|
|
||||||
|
const applyRecoveredAllowFrom = (params: {
|
||||||
|
account: Record<string, unknown>;
|
||||||
|
allowFrom: string[];
|
||||||
|
mode: AllowFromMode;
|
||||||
|
prefix: string;
|
||||||
|
}) => {
|
||||||
|
const count = params.allowFrom.length;
|
||||||
|
const noun = count === 1 ? "entry" : "entries";
|
||||||
|
|
||||||
|
if (params.mode === "nestedOnly") {
|
||||||
|
const dmEntry = params.account.dm;
|
||||||
|
const dm =
|
||||||
|
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||||
|
? (dmEntry as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
dm.allowFrom = params.allowFrom;
|
||||||
|
params.account.dm = dm;
|
||||||
|
changes.push(
|
||||||
|
`- ${params.prefix}.dm.allowFrom: restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.mode === "topOrNested") {
|
||||||
|
const dmEntry = params.account.dm;
|
||||||
|
const dm =
|
||||||
|
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||||
|
? (dmEntry as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const nestedAllowFrom = dm?.allowFrom as Array<string | number> | undefined;
|
||||||
|
if (dm && !Array.isArray(params.account.allowFrom) && Array.isArray(nestedAllowFrom)) {
|
||||||
|
dm.allowFrom = params.allowFrom;
|
||||||
|
changes.push(
|
||||||
|
`- ${params.prefix}.dm.allowFrom: restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.account.allowFrom = params.allowFrom;
|
||||||
|
changes.push(
|
||||||
|
`- ${params.prefix}.allowFrom: restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const recoverAllowFromForAccount = async (params: {
|
||||||
|
channelName: string;
|
||||||
|
account: Record<string, unknown>;
|
||||||
|
accountId?: string;
|
||||||
|
prefix: string;
|
||||||
|
}) => {
|
||||||
|
const dmEntry = params.account.dm;
|
||||||
|
const dm =
|
||||||
|
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||||
|
? (dmEntry as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const dmPolicy =
|
||||||
|
(params.account.dmPolicy as string | undefined) ?? (dm?.policy as string | undefined);
|
||||||
|
if (dmPolicy !== "allowlist") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topAllowFrom = params.account.allowFrom as Array<string | number> | undefined;
|
||||||
|
const nestedAllowFrom = dm?.allowFrom as Array<string | number> | undefined;
|
||||||
|
if (hasAllowFromEntries(topAllowFrom) || hasAllowFromEntries(nestedAllowFrom)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedChannelId = (normalizeChatChannelId(params.channelName) ?? params.channelName)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!normalizedChannelId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalizedAccountId = normalizeAccountId(params.accountId) || DEFAULT_ACCOUNT_ID;
|
||||||
|
const fromStore = await readChannelAllowFromStore(
|
||||||
|
normalizedChannelId,
|
||||||
|
process.env,
|
||||||
|
normalizedAccountId,
|
||||||
|
).catch(() => []);
|
||||||
|
const recovered = Array.from(new Set(fromStore.map((entry) => String(entry).trim()))).filter(
|
||||||
|
Boolean,
|
||||||
|
);
|
||||||
|
if (recovered.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRecoveredAllowFrom({
|
||||||
|
account: params.account,
|
||||||
|
allowFrom: recovered,
|
||||||
|
mode: resolveAllowFromMode(params.channelName),
|
||||||
|
prefix: params.prefix,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextChannels = next.channels as Record<string, Record<string, unknown>>;
|
||||||
|
for (const [channelName, channelConfig] of Object.entries(nextChannels)) {
|
||||||
|
if (!channelConfig || typeof channelConfig !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await recoverAllowFromForAccount({
|
||||||
|
channelName,
|
||||||
|
account: channelConfig,
|
||||||
|
prefix: `channels.${channelName}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const accounts = channelConfig.accounts as Record<string, Record<string, unknown>> | undefined;
|
||||||
|
if (!accounts || typeof accounts !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const [accountId, accountConfig] of Object.entries(accounts)) {
|
||||||
|
if (!accountConfig || typeof accountConfig !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await recoverAllowFromForAccount({
|
||||||
|
channelName,
|
||||||
|
account: accountConfig,
|
||||||
|
accountId,
|
||||||
|
prefix: `channels.${channelName}.accounts.${accountId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
return { config: cfg, changes: [] };
|
||||||
|
}
|
||||||
|
return { config: next, changes };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan all channel configs for dmPolicy="allowlist" without any allowFrom entries.
|
* Scan all channel configs for dmPolicy="allowlist" without any allowFrom entries.
|
||||||
* This configuration causes all DMs to be silently dropped because no sender can
|
* This configuration blocks all DMs because no sender can match the empty
|
||||||
* match the empty allowlist. Common after upgrades that remove external allowlist
|
* allowlist. Common after upgrades that remove external allowlist
|
||||||
* file support.
|
* file support.
|
||||||
*/
|
*/
|
||||||
function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
||||||
@@ -1109,9 +1267,6 @@ function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
|||||||
|
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|
||||||
const hasEntries = (list?: Array<string | number>) =>
|
|
||||||
Array.isArray(list) && list.map((v) => String(v).trim()).filter(Boolean).length > 0;
|
|
||||||
|
|
||||||
const checkAccount = (
|
const checkAccount = (
|
||||||
account: Record<string, unknown>,
|
account: Record<string, unknown>,
|
||||||
prefix: string,
|
prefix: string,
|
||||||
@@ -1145,12 +1300,12 @@ function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
|||||||
const parentNestedAllowFrom = parentDm?.allowFrom as Array<string | number> | undefined;
|
const parentNestedAllowFrom = parentDm?.allowFrom as Array<string | number> | undefined;
|
||||||
const effectiveAllowFrom = topAllowFrom ?? nestedAllowFrom ?? parentNestedAllowFrom;
|
const effectiveAllowFrom = topAllowFrom ?? nestedAllowFrom ?? parentNestedAllowFrom;
|
||||||
|
|
||||||
if (hasEntries(effectiveAllowFrom)) {
|
if (hasAllowFromEntries(effectiveAllowFrom)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`- ${prefix}.dmPolicy is "allowlist" but allowFrom is empty — all DMs will be silently dropped. Add sender IDs to ${prefix}.allowFrom or change dmPolicy to "pairing".`,
|
`- ${prefix}.dmPolicy is "allowlist" but allowFrom is empty — all DMs will be blocked. Add sender IDs to ${prefix}.allowFrom, or run "${formatCliCommand("openclaw doctor --fix")}" to auto-migrate from pairing store when entries exist.`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1634,6 +1789,14 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
|||||||
cfg = allowFromRepair.config;
|
cfg = allowFromRepair.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowlistRepair = await maybeRepairAllowlistPolicyAllowFrom(candidate);
|
||||||
|
if (allowlistRepair.changes.length > 0) {
|
||||||
|
note(allowlistRepair.changes.join("\n"), "Doctor changes");
|
||||||
|
candidate = allowlistRepair.config;
|
||||||
|
pendingChanges = true;
|
||||||
|
cfg = allowlistRepair.config;
|
||||||
|
}
|
||||||
|
|
||||||
const emptyAllowlistWarnings = detectEmptyAllowlistPolicy(candidate);
|
const emptyAllowlistWarnings = detectEmptyAllowlistPolicy(candidate);
|
||||||
if (emptyAllowlistWarnings.length > 0) {
|
if (emptyAllowlistWarnings.length > 0) {
|
||||||
note(emptyAllowlistWarnings.join("\n"), "Doctor warnings");
|
note(emptyAllowlistWarnings.join("\n"), "Doctor warnings");
|
||||||
|
|||||||
@@ -295,6 +295,27 @@ export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
|
|||||||
if (account.enabled === false) {
|
if (account.enabled === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const effectiveDmPolicy = account.dmPolicy ?? value.dmPolicy;
|
||||||
|
const effectiveAllowFrom = Array.isArray(account.allowFrom)
|
||||||
|
? account.allowFrom
|
||||||
|
: value.allowFrom;
|
||||||
|
requireOpenAllowFrom({
|
||||||
|
policy: effectiveDmPolicy,
|
||||||
|
allowFrom: effectiveAllowFrom,
|
||||||
|
ctx,
|
||||||
|
path: ["accounts", accountId, "allowFrom"],
|
||||||
|
message:
|
||||||
|
'channels.telegram.accounts.*.dmPolicy="open" requires channels.telegram.allowFrom or channels.telegram.accounts.*.allowFrom to include "*"',
|
||||||
|
});
|
||||||
|
requireAllowlistAllowFrom({
|
||||||
|
policy: effectiveDmPolicy,
|
||||||
|
allowFrom: effectiveAllowFrom,
|
||||||
|
ctx,
|
||||||
|
path: ["accounts", accountId, "allowFrom"],
|
||||||
|
message:
|
||||||
|
'channels.telegram.accounts.*.dmPolicy="allowlist" requires channels.telegram.allowFrom or channels.telegram.accounts.*.allowFrom to contain at least one sender ID',
|
||||||
|
});
|
||||||
|
|
||||||
const accountWebhookUrl =
|
const accountWebhookUrl =
|
||||||
typeof account.webhookUrl === "string" ? account.webhookUrl.trim() : "";
|
typeof account.webhookUrl === "string" ? account.webhookUrl.trim() : "";
|
||||||
if (!accountWebhookUrl) {
|
if (!accountWebhookUrl) {
|
||||||
|
|||||||
Reference in New Issue
Block a user