// Line plugin module implements accounts behavior. import { DEFAULT_ACCOUNT_ID, normalizeAccountId as normalizeSharedAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution"; import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution"; import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core"; import type { LineAccountConfig, LineConfig, LineTokenSource, ResolvedLineAccount, } from "./types.js"; export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; function readFileIfExists(filePath: string | undefined): string | undefined { return tryReadSecretFileSync(filePath, "LINE credential file", { rejectSymlink: true }); } function resolveToken(params: { accountId: string; baseConfig?: LineConfig; accountConfig?: LineAccountConfig; }): { token: string; tokenSource: LineTokenSource } { const { accountId, baseConfig, accountConfig } = params; if (accountConfig?.channelAccessToken?.trim()) { return { token: accountConfig.channelAccessToken.trim(), tokenSource: "config" }; } const accountFileToken = readFileIfExists(accountConfig?.tokenFile); if (accountFileToken) { return { token: accountFileToken, tokenSource: "file" }; } if (accountId === DEFAULT_ACCOUNT_ID) { if (baseConfig?.channelAccessToken?.trim()) { return { token: baseConfig.channelAccessToken.trim(), tokenSource: "config" }; } const baseFileToken = readFileIfExists(baseConfig?.tokenFile); if (baseFileToken) { return { token: baseFileToken, tokenSource: "file" }; } const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim(); if (envToken) { return { token: envToken, tokenSource: "env" }; } } return { token: "", tokenSource: "none" }; } function resolveSecret(params: { accountId: string; baseConfig?: LineConfig; accountConfig?: LineAccountConfig; }): string { const { accountId, baseConfig, accountConfig } = params; if (accountConfig?.channelSecret?.trim()) { return accountConfig.channelSecret.trim(); } const accountFileSecret = readFileIfExists(accountConfig?.secretFile); if (accountFileSecret) { return accountFileSecret; } if (accountId === DEFAULT_ACCOUNT_ID) { if (baseConfig?.channelSecret?.trim()) { return baseConfig.channelSecret.trim(); } const baseFileSecret = readFileIfExists(baseConfig?.secretFile); if (baseFileSecret) { return baseFileSecret; } const envSecret = process.env.LINE_CHANNEL_SECRET?.trim(); if (envSecret) { return envSecret; } } return ""; } export function resolveLineAccount(params: { cfg: OpenClawConfig; accountId?: string; }): ResolvedLineAccount { const cfg = params.cfg; const accountId = normalizeSharedAccountId(params.accountId ?? resolveDefaultLineAccountId(cfg)); const lineConfig = cfg.channels?.line as LineConfig | undefined; const accounts = lineConfig?.accounts; const accountConfig = resolveAccountEntry(accounts, accountId); const { token, tokenSource } = resolveToken({ accountId, baseConfig: lineConfig, accountConfig, }); const secret = resolveSecret({ accountId, baseConfig: lineConfig, accountConfig, }); const { accounts: _ignoredAccounts, defaultAccount: _ignoredDefaultAccount, ...lineBase } = (lineConfig ?? {}) as LineConfig & { accounts?: unknown; defaultAccount?: unknown; }; const mergedConfig: LineConfig & LineAccountConfig = { ...lineBase, ...accountConfig, }; const baseEnabled = lineConfig?.enabled !== false; const accountEnabled = accountConfig?.enabled !== false; const enabled = baseEnabled && accountEnabled; const name = accountConfig?.name ?? (accountId === DEFAULT_ACCOUNT_ID ? lineConfig?.name : undefined); return { accountId, name, enabled, channelAccessToken: token, channelSecret: secret, tokenSource, config: mergedConfig, }; } export function listLineAccountIds(cfg: OpenClawConfig): string[] { const lineConfig = cfg.channels?.line as LineConfig | undefined; const accounts = lineConfig?.accounts; const ids = new Set(); if ( lineConfig?.channelAccessToken?.trim() || lineConfig?.tokenFile || process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim() ) { ids.add(DEFAULT_ACCOUNT_ID); } if (accounts) { for (const id of Object.keys(accounts)) { ids.add(id); } } return Array.from(ids); } export function resolveDefaultLineAccountId(cfg: OpenClawConfig): string { const preferred = normalizeOptionalAccountId( (cfg.channels?.line as LineConfig | undefined)?.defaultAccount, ); if ( preferred && listLineAccountIds(cfg).some((accountId) => normalizeSharedAccountId(accountId) === preferred) ) { return preferred; } const ids = listLineAccountIds(cfg); if (ids.includes(DEFAULT_ACCOUNT_ID)) { return DEFAULT_ACCOUNT_ID; } return ids[0] ?? DEFAULT_ACCOUNT_ID; } export function normalizeAccountId(accountId: string | undefined): string { return normalizeSharedAccountId(accountId); }