mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix: stop channel runtime before WhatsApp removal
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Slack/setup: print the generated app manifest as plain JSON instead of embedding it inside the framed setup note, so it can be copied into Slack without deleting border characters. Fixes #65751. Thanks @theDanielJLewis.
|
||||
- Channels/WhatsApp: route CLI logout through the live Gateway and stop runtime-backed listeners before channel removal, so removing a WhatsApp account does not leave the old socket replying until restart. Fixes #67746. Thanks @123Mismail.
|
||||
- Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash.
|
||||
- Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme.
|
||||
- Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer.
|
||||
|
||||
@@ -492,6 +492,8 @@ Behavior notes:
|
||||
<Accordion title="Logout behavior">
|
||||
`openclaw channels logout --channel whatsapp [--account <id>]` clears WhatsApp auth state for that account.
|
||||
|
||||
When a Gateway is reachable, logout first stops the live WhatsApp listener for the selected account so the linked session does not keep receiving messages until the next restart. `openclaw channels remove --channel whatsapp` also stops the live listener before disabling or deleting account config.
|
||||
|
||||
In legacy auth directories, `oauth.json` is preserved while Baileys auth files are removed.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -52,6 +52,7 @@ openclaw channels remove --channel telegram --delete
|
||||
</Tip>
|
||||
|
||||
`channels remove` only operates on installed/configured channel plugins. Use `channels add` first for installable catalog channels.
|
||||
For runtime-backed channel plugins, `channels remove` also asks the running Gateway to stop the selected account before it updates config, so disabling or deleting an account does not leave the old listener active until restart.
|
||||
|
||||
Common non-interactive add surfaces include:
|
||||
|
||||
@@ -94,6 +95,7 @@ openclaw channels logout --channel whatsapp
|
||||
|
||||
- `channels login` supports `--verbose`.
|
||||
- `channels login` and `logout` can infer the channel when only one supported login target is configured.
|
||||
- `channels logout` prefers the live Gateway path when reachable, so logout stops any active listener before clearing channel auth state. If a local Gateway is not reachable, it falls back to local auth cleanup.
|
||||
- Run `channels login` from a terminal on the gateway host. Agent `exec` blocks this interactive login flow; channel-native agent login tools, such as `whatsapp_login`, should be used from chat when available.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -260,9 +260,10 @@ describe("channel-auth", () => {
|
||||
|
||||
await runChannelLogout({}, runtime);
|
||||
|
||||
expect(mocks.logoutAccount).toHaveBeenCalledWith(
|
||||
expect(mocks.callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: autoEnabledCfg,
|
||||
config: autoEnabledCfg,
|
||||
method: "channels.logout",
|
||||
}),
|
||||
);
|
||||
expect(mocks.replaceConfigFile).toHaveBeenCalledWith({
|
||||
@@ -518,7 +519,28 @@ describe("channel-auth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("runs logout with resolved account and explicit account id", async () => {
|
||||
it("runs logout through the live gateway with resolved account and explicit account id", async () => {
|
||||
await runChannelLogout({ channel: "whatsapp", account: " acct-2 " }, runtime);
|
||||
|
||||
expect(mocks.callGateway).toHaveBeenCalledWith({
|
||||
config: { channels: { whatsapp: {} } },
|
||||
method: "channels.logout",
|
||||
params: {
|
||||
channel: "whatsapp",
|
||||
accountId: "acct-2",
|
||||
},
|
||||
mode: "backend",
|
||||
clientName: "gateway-client",
|
||||
deviceIdentity: null,
|
||||
});
|
||||
expect(mocks.resolveAccount).not.toHaveBeenCalled();
|
||||
expect(mocks.logoutAccount).not.toHaveBeenCalled();
|
||||
expect(mocks.setVerbose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to local auth cleanup when a local gateway logout is unreachable", async () => {
|
||||
mocks.callGateway.mockRejectedValue(new Error("gateway unreachable"));
|
||||
|
||||
await runChannelLogout({ channel: "whatsapp", account: " acct-2 " }, runtime);
|
||||
|
||||
expect(mocks.resolveAccount).toHaveBeenCalledWith({ channels: { whatsapp: {} } }, "acct-2");
|
||||
@@ -528,6 +550,9 @@ describe("channel-auth", () => {
|
||||
account: { id: "resolved-account" },
|
||||
runtime,
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("running gateway did not stop it: gateway unreachable"),
|
||||
);
|
||||
expect(mocks.setVerbose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -168,6 +168,36 @@ async function reconcileGatewayRuntimeAfterLocalLogin(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function logoutViaGatewayRuntime(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelId: string;
|
||||
accountId: string;
|
||||
runtime: RuntimeEnv;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
await callGateway({
|
||||
config: params.cfg,
|
||||
method: "channels.logout",
|
||||
params: {
|
||||
channel: params.channelId,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
deviceIdentity: null,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (params.cfg.gateway?.mode === "remote") {
|
||||
throw error;
|
||||
}
|
||||
params.runtime.log(
|
||||
`Local logout will clear auth for ${params.channelId}/${params.accountId}, but the running gateway did not stop it: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runChannelLogin(
|
||||
opts: ChannelAuthOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
@@ -235,8 +265,18 @@ export async function runChannelLogout(
|
||||
if (!logoutAccount) {
|
||||
throw new Error(`Channel ${channelInput} does not support logout`);
|
||||
}
|
||||
// Auth-only flow: resolve account + clear session state only.
|
||||
// Prefer the live gateway so logout also stops any active channel runtime.
|
||||
const { accountId } = resolveAccountContext(plugin, opts, cfg);
|
||||
if (
|
||||
await logoutViaGatewayRuntime({
|
||||
cfg,
|
||||
channelId: plugin.id,
|
||||
accountId,
|
||||
runtime,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
await logoutAccount({
|
||||
cfg,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
@@ -23,6 +24,10 @@ const registryRefreshMocks = vi.hoisted(() => ({
|
||||
refreshPluginRegistryAfterConfigMutation: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
const gatewayMocks = vi.hoisted(() => ({
|
||||
callGateway: vi.fn(async () => ({ stopped: true })),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/catalog.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../channels/plugins/catalog.js")>(
|
||||
"../channels/plugins/catalog.js",
|
||||
@@ -54,6 +59,10 @@ vi.mock("./channel-setup/plugin-install.js", async () => {
|
||||
|
||||
vi.mock("../cli/plugins-registry-refresh.js", () => registryRefreshMocks);
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: gatewayMocks.callGateway,
|
||||
}));
|
||||
|
||||
const runtime = createTestRuntime();
|
||||
|
||||
describe("channelsRemoveCommand", () => {
|
||||
@@ -86,6 +95,8 @@ describe("channelsRemoveCommand", () => {
|
||||
createTestRegistry(),
|
||||
);
|
||||
registryRefreshMocks.refreshPluginRegistryAfterConfigMutation.mockClear();
|
||||
gatewayMocks.callGateway.mockClear();
|
||||
gatewayMocks.callGateway.mockResolvedValue({ stopped: true });
|
||||
setActivePluginRegistry(createTestRegistry());
|
||||
});
|
||||
|
||||
@@ -170,4 +181,71 @@ describe("channelsRemoveCommand", () => {
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops an active gateway channel runtime before deleting a runtime-backed account", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
"external-chat": {
|
||||
enabled: true,
|
||||
token: "token-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const catalogEntry: ChannelPluginCatalogEntry = createExternalChatCatalogEntry();
|
||||
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
|
||||
const scopedPlugin = {
|
||||
...createExternalChatDeletePlugin(),
|
||||
gateway: {
|
||||
startAccount: vi.fn(),
|
||||
},
|
||||
} as ChannelPlugin;
|
||||
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockReturnValue(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
plugin: scopedPlugin,
|
||||
source: "test",
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await channelsRemoveCommand(
|
||||
{
|
||||
channel: "external-chat",
|
||||
account: "default",
|
||||
delete: true,
|
||||
},
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
expect(gatewayMocks.callGateway).toHaveBeenCalledWith({
|
||||
config: {
|
||||
channels: {
|
||||
"external-chat": {
|
||||
enabled: true,
|
||||
token: "token-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
method: "channels.stop",
|
||||
params: {
|
||||
channel: "external-chat",
|
||||
accountId: "default",
|
||||
},
|
||||
mode: "backend",
|
||||
clientName: "gateway-client",
|
||||
deviceIdentity: null,
|
||||
});
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
channels: expect.objectContaining({
|
||||
"external-chat": expect.anything(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,9 +5,12 @@ import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
|
||||
import { commitConfigWithPendingPluginInstalls } from "../../cli/plugins-install-record-commit.js";
|
||||
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
|
||||
import { replaceConfigFile, type OpenClawConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import { channelLabel } from "./runtime-label.js";
|
||||
import { type ChatChannel, requireValidConfigFileSnapshot, shouldUseWizard } from "./shared.js";
|
||||
@@ -30,6 +33,35 @@ function listAccountIds(
|
||||
return plugin.config.listAccountIds(cfg);
|
||||
}
|
||||
|
||||
async function stopGatewayRuntimeBeforeRemove(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: ChatChannel;
|
||||
accountId: string;
|
||||
plugin: ChannelPlugin;
|
||||
runtime: RuntimeEnv;
|
||||
}) {
|
||||
if (!params.plugin.gateway?.startAccount && !params.plugin.gateway?.logoutAccount) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await callGateway({
|
||||
config: params.cfg,
|
||||
method: "channels.stop",
|
||||
params: {
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
deviceIdentity: null,
|
||||
});
|
||||
} catch (error) {
|
||||
params.runtime.log(
|
||||
`Could not stop running ${channelLabel(params.channel)} account "${params.accountId}" before removing it: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function channelsRemoveCommand(
|
||||
opts: ChannelsRemoveOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
@@ -147,6 +179,14 @@ export async function channelsRemoveCommand(
|
||||
normalizeAccountId(accountId) ?? resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
const accountKey = resolvedAccountId || DEFAULT_ACCOUNT_ID;
|
||||
|
||||
await stopGatewayRuntimeBeforeRemove({
|
||||
cfg,
|
||||
channel: resolvedChannelId,
|
||||
accountId: accountKey,
|
||||
plugin,
|
||||
runtime,
|
||||
});
|
||||
|
||||
let next = { ...cfg };
|
||||
const prevCfg = cfg;
|
||||
if (deleteConfig) {
|
||||
|
||||
@@ -169,6 +169,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
||||
],
|
||||
[ADMIN_SCOPE]: [
|
||||
"channels.start",
|
||||
"channels.stop",
|
||||
"channels.logout",
|
||||
"agents.create",
|
||||
"agents.update",
|
||||
|
||||
@@ -57,6 +57,8 @@ import {
|
||||
AgentWaitParamsSchema,
|
||||
type ChannelsStartParams,
|
||||
ChannelsStartParamsSchema,
|
||||
type ChannelsStopParams,
|
||||
ChannelsStopParamsSchema,
|
||||
type ChannelsLogoutParams,
|
||||
ChannelsLogoutParamsSchema,
|
||||
type TalkConfigParams,
|
||||
@@ -528,6 +530,7 @@ export const validateChannelsStatusParams = ajv.compile<ChannelsStatusParams>(
|
||||
);
|
||||
export const validateChannelsStartParams =
|
||||
ajv.compile<ChannelsStartParams>(ChannelsStartParamsSchema);
|
||||
export const validateChannelsStopParams = ajv.compile<ChannelsStopParams>(ChannelsStopParamsSchema);
|
||||
export const validateChannelsLogoutParams = ajv.compile<ChannelsLogoutParams>(
|
||||
ChannelsLogoutParamsSchema,
|
||||
);
|
||||
@@ -740,6 +743,7 @@ export {
|
||||
ChannelsStatusParamsSchema,
|
||||
ChannelsStatusResultSchema,
|
||||
ChannelsStartParamsSchema,
|
||||
ChannelsStopParamsSchema,
|
||||
ChannelsLogoutParamsSchema,
|
||||
WebLoginStartParamsSchema,
|
||||
WebLoginWaitParamsSchema,
|
||||
@@ -854,6 +858,7 @@ export type {
|
||||
ChannelsStatusParams,
|
||||
ChannelsStatusResult,
|
||||
ChannelsStartParams,
|
||||
ChannelsStopParams,
|
||||
ChannelsLogoutParams,
|
||||
WebLoginStartParams,
|
||||
WebLoginWaitParams,
|
||||
|
||||
@@ -310,6 +310,14 @@ export const ChannelsLogoutParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ChannelsStopParamsSchema = Type.Object(
|
||||
{
|
||||
channel: NonEmptyString,
|
||||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ChannelsStartParamsSchema = Type.Object(
|
||||
{
|
||||
channel: NonEmptyString,
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
} from "./artifacts.js";
|
||||
import {
|
||||
ChannelsStartParamsSchema,
|
||||
ChannelsStopParamsSchema,
|
||||
ChannelsLogoutParamsSchema,
|
||||
TalkConfigParamsSchema,
|
||||
TalkConfigResultSchema,
|
||||
@@ -328,6 +329,7 @@ export const ProtocolSchemas = {
|
||||
ChannelsStatusParams: ChannelsStatusParamsSchema,
|
||||
ChannelsStatusResult: ChannelsStatusResultSchema,
|
||||
ChannelsStartParams: ChannelsStartParamsSchema,
|
||||
ChannelsStopParams: ChannelsStopParamsSchema,
|
||||
ChannelsLogoutParams: ChannelsLogoutParamsSchema,
|
||||
WebLoginStartParams: WebLoginStartParamsSchema,
|
||||
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
||||
|
||||
@@ -99,6 +99,7 @@ export type TalkSpeakResult = SchemaType<"TalkSpeakResult">;
|
||||
export type ChannelsStatusParams = SchemaType<"ChannelsStatusParams">;
|
||||
export type ChannelsStatusResult = SchemaType<"ChannelsStatusResult">;
|
||||
export type ChannelsStartParams = SchemaType<"ChannelsStartParams">;
|
||||
export type ChannelsStopParams = SchemaType<"ChannelsStopParams">;
|
||||
export type ChannelsLogoutParams = SchemaType<"ChannelsLogoutParams">;
|
||||
export type WebLoginStartParams = SchemaType<"WebLoginStartParams">;
|
||||
export type WebLoginWaitParams = SchemaType<"WebLoginWaitParams">;
|
||||
|
||||
@@ -15,6 +15,7 @@ const BASE_METHODS = [
|
||||
"logs.tail",
|
||||
"channels.status",
|
||||
"channels.start",
|
||||
"channels.stop",
|
||||
"channels.logout",
|
||||
"status",
|
||||
"usage.status",
|
||||
|
||||
@@ -40,6 +40,7 @@ function createOptions(
|
||||
getRuntimeConfig: mocks.getRuntimeConfig,
|
||||
startChannel: vi.fn(),
|
||||
stopChannel: vi.fn(),
|
||||
markChannelLoggedOut: vi.fn(),
|
||||
getRuntimeSnapshot: vi.fn(
|
||||
(): ChannelRuntimeSnapshot => ({
|
||||
channels: {
|
||||
@@ -178,6 +179,63 @@ describe("channelsHandlers channels.start", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("channelsHandlers channels.stop", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.getRuntimeConfig.mockReturnValue({});
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
id: "whatsapp",
|
||||
config: {
|
||||
defaultAccountId: () => "default-account",
|
||||
listAccountIds: () => ["default-account"],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("stops a channel account without clearing auth state", async () => {
|
||||
const stopChannel = vi.fn(async () => undefined);
|
||||
const respond = vi.fn();
|
||||
|
||||
await channelsHandlers["channels.stop"](
|
||||
createOptions(
|
||||
{ channel: "whatsapp" },
|
||||
{
|
||||
respond,
|
||||
context: {
|
||||
getRuntimeConfig: mocks.getRuntimeConfig,
|
||||
stopChannel,
|
||||
getRuntimeSnapshot: vi.fn(
|
||||
(): ChannelRuntimeSnapshot => ({
|
||||
channels: {},
|
||||
channelAccounts: {
|
||||
whatsapp: {
|
||||
"default-account": {
|
||||
accountId: "default-account",
|
||||
running: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(stopChannel).toHaveBeenCalledWith("whatsapp", "default-account");
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
{
|
||||
channel: "whatsapp",
|
||||
accountId: "default-account",
|
||||
stopped: true,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("channelsHandlers channels.logout", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateChannelsStartParams,
|
||||
validateChannelsStopParams,
|
||||
validateChannelsLogoutParams,
|
||||
validateChannelsStatusParams,
|
||||
} from "../protocol/index.js";
|
||||
@@ -42,6 +43,12 @@ type ChannelStartPayload = {
|
||||
started: boolean;
|
||||
};
|
||||
|
||||
type ChannelStopPayload = {
|
||||
channel: ChannelId;
|
||||
accountId: string;
|
||||
stopped: boolean;
|
||||
};
|
||||
|
||||
const CHANNEL_STATUS_MAX_TIMEOUT_MS = 30_000;
|
||||
const CHANNEL_STATUS_PROBE_CONCURRENCY = 5;
|
||||
|
||||
@@ -138,6 +145,29 @@ export async function startChannelAccount(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function stopChannelAccount(params: {
|
||||
channelId: ChannelId;
|
||||
accountId?: string | null;
|
||||
cfg: OpenClawConfig;
|
||||
context: GatewayRequestContext;
|
||||
plugin: ChannelPlugin;
|
||||
}): Promise<ChannelStopPayload> {
|
||||
const resolvedAccountId = resolveChannelGatewayAccountId(params);
|
||||
await params.context.stopChannel(params.channelId, resolvedAccountId);
|
||||
const runtime = params.context.getRuntimeSnapshot();
|
||||
const stopped =
|
||||
resolveRuntimeAccountSnapshot({
|
||||
runtime,
|
||||
channelId: params.channelId,
|
||||
accountId: resolvedAccountId,
|
||||
})?.running !== true;
|
||||
return {
|
||||
channel: params.channelId,
|
||||
accountId: resolvedAccountId,
|
||||
stopped,
|
||||
};
|
||||
}
|
||||
|
||||
export const channelsHandlers: GatewayRequestHandlers = {
|
||||
"channels.status": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsStatusParams(params)) {
|
||||
@@ -391,6 +421,52 @@ export const channelsHandlers: GatewayRequestHandlers = {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(error)));
|
||||
}
|
||||
},
|
||||
"channels.stop": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsStopParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid channels.stop params: ${formatValidationErrors(validateChannelsStopParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rawChannel = (params as { channel?: unknown }).channel;
|
||||
const channelId = typeof rawChannel === "string" ? normalizeChannelId(rawChannel) : null;
|
||||
if (!channelId) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "invalid channels.stop channel"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
if (!plugin) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, `unknown channel ${channelId}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const accountIdRaw = (params as { accountId?: unknown }).accountId;
|
||||
const accountId = normalizeOptionalString(accountIdRaw);
|
||||
try {
|
||||
const payload = await stopChannelAccount({
|
||||
channelId,
|
||||
accountId,
|
||||
cfg: context.getRuntimeConfig(),
|
||||
context,
|
||||
plugin,
|
||||
});
|
||||
respond(true, payload, undefined);
|
||||
} catch (error) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(error)));
|
||||
}
|
||||
},
|
||||
"channels.logout": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsLogoutParams(params)) {
|
||||
respond(
|
||||
|
||||
Reference in New Issue
Block a user