openclaw/extensions/line/src/gateway.ts

118 lines
3.6 KiB
TypeScript

import {
clearAccountEntryFields,
DEFAULT_ACCOUNT_ID,
type ChannelPlugin,
type LineConfig,
type OpenClawConfig,
type ResolvedLineAccount,
} from "../api.js";
import { getLineRuntime } from "./runtime.js";
export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>["gateway"]> = {
startAccount: async (ctx) => {
const account = ctx.account;
const token = account.channelAccessToken.trim();
const secret = account.channelSecret.trim();
if (!token) {
throw new Error(
`LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`,
);
}
if (!secret) {
throw new Error(
`LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`,
);
}
let lineBotLabel = "";
try {
const probe = await getLineRuntime().channel.line.probeLineBot(token, 2500);
const displayName = probe.ok ? probe.bot?.displayName?.trim() : null;
if (displayName) {
lineBotLabel = ` (${displayName})`;
}
} catch (err) {
if (getLineRuntime().logging.shouldLogVerbose()) {
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
}
}
ctx.log?.info(`[${account.accountId}] starting LINE provider${lineBotLabel}`);
return await getLineRuntime().channel.line.monitorLineProvider({
channelAccessToken: token,
channelSecret: secret,
accountId: account.accountId,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
webhookPath: account.config.webhookPath,
});
},
logoutAccount: async ({ accountId, cfg }) => {
const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim() ?? "";
const nextCfg = { ...cfg } as OpenClawConfig;
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
const nextLine = { ...lineConfig };
let cleared = false;
let changed = false;
if (accountId === DEFAULT_ACCOUNT_ID) {
if (
nextLine.channelAccessToken ||
nextLine.channelSecret ||
nextLine.tokenFile ||
nextLine.secretFile
) {
delete nextLine.channelAccessToken;
delete nextLine.channelSecret;
delete nextLine.tokenFile;
delete nextLine.secretFile;
cleared = true;
changed = true;
}
}
const accountCleanup = clearAccountEntryFields({
accounts: nextLine.accounts,
accountId,
fields: ["channelAccessToken", "channelSecret", "tokenFile", "secretFile"],
markClearedOnFieldPresence: true,
});
if (accountCleanup.changed) {
changed = true;
if (accountCleanup.cleared) {
cleared = true;
}
if (accountCleanup.nextAccounts) {
nextLine.accounts = accountCleanup.nextAccounts;
} else {
delete nextLine.accounts;
}
}
if (changed) {
if (Object.keys(nextLine).length > 0) {
nextCfg.channels = { ...nextCfg.channels, line: nextLine };
} else {
const nextChannels = { ...nextCfg.channels };
delete (nextChannels as Record<string, unknown>).line;
if (Object.keys(nextChannels).length > 0) {
nextCfg.channels = nextChannels;
} else {
delete nextCfg.channels;
}
}
await getLineRuntime().config.writeConfigFile(nextCfg);
}
const resolved = getLineRuntime().channel.line.resolveLineAccount({
cfg: changed ? nextCfg : cfg,
accountId,
});
const loggedOut = resolved.tokenSource === "none";
return { cleared, envToken: Boolean(envToken), loggedOut };
},
};