refactor: split monitor runtime helpers

This commit is contained in:
Peter Steinberger 2026-03-17 20:51:00 -07:00
parent fb5ab95e03
commit a2518a16ac
No known key found for this signature in database
7 changed files with 1475 additions and 1192 deletions

File diff suppressed because it is too large Load Diff

View File

@ -332,7 +332,7 @@ async function expectBoundStatusCommandDispatch(params: {
describe("Discord native plugin command dispatch", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
clearPluginCommands();
persistentBindingMocks.resolveConfiguredAcpBindingRecord.mockReset();
persistentBindingMocks.resolveConfiguredAcpBindingRecord.mockImplementation((params) => ({

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
import {
listSkillCommandsForAgents,
parseStrictPositiveInteger,
type OpenClawConfig,
type RuntimeEnv,
} from "../runtime-api.js";
import type { ResolvedMattermostAccount } from "./accounts.js";
import {
fetchMattermostUserTeams,
normalizeMattermostBaseUrl,
type MattermostClient,
} from "./client.js";
import {
DEFAULT_COMMAND_SPECS,
isSlashCommandsEnabled,
registerSlashCommands,
resolveCallbackUrl,
resolveSlashCommandConfig,
type MattermostCommandSpec,
type MattermostRegisteredCommand,
type MattermostSlashCommandConfig,
} from "./slash-commands.js";
import { activateSlashCommands } from "./slash-state.js";
function isLoopbackHost(hostname: string): boolean {
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
}
function buildSlashCommands(params: {
cfg: OpenClawConfig;
runtime: RuntimeEnv;
nativeSkills: boolean;
}): MattermostCommandSpec[] {
const commandsToRegister: MattermostCommandSpec[] = [...DEFAULT_COMMAND_SPECS];
if (!params.nativeSkills) {
return commandsToRegister;
}
try {
const skillCommands = listSkillCommandsForAgents({ cfg: params.cfg as any });
for (const spec of skillCommands) {
const name = typeof spec.name === "string" ? spec.name.trim() : "";
if (!name) continue;
const trigger = name.startsWith("oc_") ? name : `oc_${name}`;
commandsToRegister.push({
trigger,
description: spec.description || `Run skill ${name}`,
autoComplete: true,
autoCompleteHint: "[args]",
originalName: name,
});
}
} catch (err) {
params.runtime.error?.(`mattermost: failed to list skill commands: ${String(err)}`);
}
return commandsToRegister;
}
function dedupeSlashCommands(commands: MattermostCommandSpec[]): MattermostCommandSpec[] {
const seen = new Set<string>();
return commands.filter((cmd) => {
const key = cmd.trigger.trim();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function buildTriggerMap(commands: MattermostCommandSpec[]): Map<string, string> {
const triggerMap = new Map<string, string>();
for (const cmd of commands) {
if (cmd.originalName) {
triggerMap.set(cmd.trigger, cmd.originalName);
}
}
return triggerMap;
}
function warnOnSuspiciousCallbackUrl(params: {
runtime: RuntimeEnv;
baseUrl: string;
callbackUrl: string;
}) {
try {
const mmHost = new URL(normalizeMattermostBaseUrl(params.baseUrl) ?? params.baseUrl).hostname;
const callbackHost = new URL(params.callbackUrl).hostname;
if (isLoopbackHost(callbackHost) && !isLoopbackHost(mmHost)) {
params.runtime.error?.(
`mattermost: slash commands callbackUrl resolved to ${params.callbackUrl} (loopback) while baseUrl is ${params.baseUrl}. This MAY be unreachable depending on your deployment. If native slash commands don't work, set channels.mattermost.commands.callbackUrl to a URL reachable from the Mattermost server (e.g. your public reverse proxy URL).`,
);
}
} catch {
// Ignore malformed URLs and let the downstream registration fail naturally.
}
}
async function registerSlashCommandsAcrossTeams(params: {
client: MattermostClient;
teams: Array<{ id: string }>;
botUserId: string;
callbackUrl: string;
commands: MattermostCommandSpec[];
runtime: RuntimeEnv;
}): Promise<{
registered: MattermostRegisteredCommand[];
teamRegistrationFailures: number;
}> {
const registered: MattermostRegisteredCommand[] = [];
let teamRegistrationFailures = 0;
for (const team of params.teams) {
try {
const created = await registerSlashCommands({
client: params.client,
teamId: team.id,
creatorUserId: params.botUserId,
callbackUrl: params.callbackUrl,
commands: params.commands,
log: (msg) => params.runtime.log?.(msg),
});
registered.push(...created);
} catch (err) {
teamRegistrationFailures += 1;
params.runtime.error?.(
`mattermost: failed to register slash commands for team ${team.id}: ${String(err)}`,
);
}
}
return { registered, teamRegistrationFailures };
}
export async function registerMattermostMonitorSlashCommands(params: {
client: MattermostClient;
cfg: OpenClawConfig;
runtime: RuntimeEnv;
account: ResolvedMattermostAccount;
baseUrl: string;
botUserId: string;
}) {
const commandsRaw = params.account.config.commands as
| Partial<MattermostSlashCommandConfig>
| undefined;
const slashConfig = resolveSlashCommandConfig(commandsRaw);
if (!isSlashCommandsEnabled(slashConfig)) {
return;
}
try {
const teams = await fetchMattermostUserTeams(params.client, params.botUserId);
const envPort = parseStrictPositiveInteger(process.env.OPENCLAW_GATEWAY_PORT?.trim());
const slashGatewayPort = envPort ?? params.cfg.gateway?.port ?? 18789;
const slashCallbackUrl = resolveCallbackUrl({
config: slashConfig,
gatewayPort: slashGatewayPort,
gatewayHost: params.cfg.gateway?.customBindHost ?? undefined,
});
warnOnSuspiciousCallbackUrl({
runtime: params.runtime,
baseUrl: params.baseUrl,
callbackUrl: slashCallbackUrl,
});
const dedupedCommands = dedupeSlashCommands(
buildSlashCommands({
cfg: params.cfg,
runtime: params.runtime,
nativeSkills: slashConfig.nativeSkills === true,
}),
);
const { registered, teamRegistrationFailures } = await registerSlashCommandsAcrossTeams({
client: params.client,
teams,
botUserId: params.botUserId,
callbackUrl: slashCallbackUrl,
commands: dedupedCommands,
runtime: params.runtime,
});
if (registered.length === 0) {
params.runtime.error?.(
"mattermost: native slash commands enabled but no commands could be registered; keeping slash callbacks inactive",
);
return;
}
if (teamRegistrationFailures > 0) {
params.runtime.error?.(
`mattermost: slash command registration completed with ${teamRegistrationFailures} team error(s)`,
);
}
activateSlashCommands({
account: params.account,
commandTokens: registered.map((cmd) => cmd.token).filter(Boolean),
registeredCommands: registered,
triggerMap: buildTriggerMap(dedupedCommands),
api: { cfg: params.cfg, runtime: params.runtime },
log: (msg) => params.runtime.log?.(msg),
});
params.runtime.log?.(
`mattermost: slash commands registered (${registered.length} commands across ${teams.length} teams, callback=${slashCallbackUrl})`,
);
} catch (err) {
params.runtime.error?.(`mattermost: failed to register slash commands: ${String(err)}`);
}
}

View File

@ -19,7 +19,6 @@ import {
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntryIfEnabled,
isDangerousNameMatchingEnabled,
parseStrictPositiveInteger,
registerPluginHttpRoute,
resolveControlCommandGate,
readStoreAllowFromForDmPolicy,
@ -28,7 +27,6 @@ import {
resolveDefaultGroupPolicy,
resolveChannelMediaMaxBytes,
warnMissingProviderGroupPolicyFallbackOnce,
listSkillCommandsForAgents,
type HistoryEntry,
} from "../runtime-api.js";
import { getMattermostRuntime } from "../runtime.js";
@ -38,7 +36,6 @@ import {
fetchMattermostChannel,
fetchMattermostMe,
fetchMattermostUser,
fetchMattermostUserTeams,
normalizeMattermostBaseUrl,
sendMattermostTyping,
updateMattermostPost,
@ -78,6 +75,7 @@ import {
resolveThreadSessionKeys,
} from "./monitor-helpers.js";
import { resolveOncharPrefixes, stripOncharPrefix } from "./monitor-onchar.js";
import { registerMattermostMonitorSlashCommands } from "./monitor-slash.js";
import {
createMattermostConnectOnce,
type MattermostEventPayload,
@ -86,19 +84,8 @@ import {
import { runWithReconnect } from "./reconnect.js";
import { deliverMattermostReplyPayload } from "./reply-delivery.js";
import { sendMessageMattermost } from "./send.js";
import {
DEFAULT_COMMAND_SPECS,
cleanupSlashCommands,
isSlashCommandsEnabled,
registerSlashCommands,
resolveCallbackUrl,
resolveSlashCommandConfig,
} from "./slash-commands.js";
import {
activateSlashCommands,
deactivateSlashCommands,
getSlashCommandState,
} from "./slash-state.js";
import { cleanupSlashCommands } from "./slash-commands.js";
import { deactivateSlashCommands, getSlashCommandState } from "./slash-state.js";
export {
evaluateMattermostMentionGate,
@ -291,139 +278,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const botUsername = botUser.username?.trim() || undefined;
runtime.log?.(`mattermost connected as ${botUsername ? `@${botUsername}` : botUserId}`);
// ─── Slash command registration ──────────────────────────────────────────
const commandsRaw = account.config.commands as
| Partial<import("./slash-commands.js").MattermostSlashCommandConfig>
| undefined;
const slashConfig = resolveSlashCommandConfig(commandsRaw);
const slashEnabled = isSlashCommandsEnabled(slashConfig);
if (slashEnabled) {
try {
const teams = await fetchMattermostUserTeams(client, botUserId);
// Use the *runtime* listener port when available (e.g. `openclaw gateway run --port <port>`).
// The gateway sets OPENCLAW_GATEWAY_PORT when it boots, but the config file may still contain
// a different port.
const envPortRaw = process.env.OPENCLAW_GATEWAY_PORT?.trim();
const envPort = parseStrictPositiveInteger(envPortRaw);
const slashGatewayPort = envPort ?? cfg.gateway?.port ?? 18789;
const slashCallbackUrl = resolveCallbackUrl({
config: slashConfig,
gatewayPort: slashGatewayPort,
gatewayHost: cfg.gateway?.customBindHost ?? undefined,
});
try {
const mmHost = new URL(baseUrl).hostname;
const callbackHost = new URL(slashCallbackUrl).hostname;
// NOTE: We cannot infer network reachability from hostnames alone.
// Mattermost might be accessed via a public domain while still running on the same
// machine as the gateway (where http://localhost:<port> is valid).
// So treat loopback callback URLs as an advisory warning only.
if (isLoopbackHost(callbackHost) && !isLoopbackHost(mmHost)) {
runtime.error?.(
`mattermost: slash commands callbackUrl resolved to ${slashCallbackUrl} (loopback) while baseUrl is ${baseUrl}. This MAY be unreachable depending on your deployment. If native slash commands don't work, set channels.mattermost.commands.callbackUrl to a URL reachable from the Mattermost server (e.g. your public reverse proxy URL).`,
);
}
} catch {
// URL parse failed; ignore and continue (we'll fail naturally if registration requests break).
}
const commandsToRegister: import("./slash-commands.js").MattermostCommandSpec[] = [
...DEFAULT_COMMAND_SPECS,
];
if (slashConfig.nativeSkills === true) {
try {
const skillCommands = listSkillCommandsForAgents({ cfg: cfg as any });
for (const spec of skillCommands) {
const name = typeof spec.name === "string" ? spec.name.trim() : "";
if (!name) continue;
const trigger = name.startsWith("oc_") ? name : `oc_${name}`;
commandsToRegister.push({
trigger,
description: spec.description || `Run skill ${name}`,
autoComplete: true,
autoCompleteHint: "[args]",
originalName: name,
});
}
} catch (err) {
runtime.error?.(`mattermost: failed to list skill commands: ${String(err)}`);
}
}
// Deduplicate by trigger
const seen = new Set<string>();
const dedupedCommands = commandsToRegister.filter((cmd) => {
const key = cmd.trigger.trim();
if (!key) return false;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
const allRegistered: import("./slash-commands.js").MattermostRegisteredCommand[] = [];
let teamRegistrationFailures = 0;
for (const team of teams) {
try {
const registered = await registerSlashCommands({
client,
teamId: team.id,
creatorUserId: botUserId,
callbackUrl: slashCallbackUrl,
commands: dedupedCommands,
log: (msg) => runtime.log?.(msg),
});
allRegistered.push(...registered);
} catch (err) {
teamRegistrationFailures += 1;
runtime.error?.(
`mattermost: failed to register slash commands for team ${team.id}: ${String(err)}`,
);
}
}
if (allRegistered.length === 0) {
runtime.error?.(
"mattermost: native slash commands enabled but no commands could be registered; keeping slash callbacks inactive",
);
} else {
if (teamRegistrationFailures > 0) {
runtime.error?.(
`mattermost: slash command registration completed with ${teamRegistrationFailures} team error(s)`,
);
}
// Build trigger→originalName map for accurate command name resolution
const triggerMap = new Map<string, string>();
for (const cmd of dedupedCommands) {
if (cmd.originalName) {
triggerMap.set(cmd.trigger, cmd.originalName);
}
}
activateSlashCommands({
account,
commandTokens: allRegistered.map((cmd) => cmd.token).filter(Boolean),
registeredCommands: allRegistered,
triggerMap,
api: { cfg, runtime },
log: (msg) => runtime.log?.(msg),
});
runtime.log?.(
`mattermost: slash commands registered (${allRegistered.length} commands across ${teams.length} teams, callback=${slashCallbackUrl})`,
);
}
} catch (err) {
runtime.error?.(`mattermost: failed to register slash commands: ${String(err)}`);
}
}
await registerMattermostMonitorSlashCommands({
client,
cfg,
runtime,
account,
baseUrl,
botUserId,
});
// ─── Interactive buttons registration ──────────────────────────────────────
// Derive a stable HMAC secret from the bot token so CLI and gateway share it.

View File

@ -29,6 +29,11 @@ import { fetchAllChannels, fetchInitData } from "./discovery.js";
import { cacheMessage, getChannelHistory, fetchThreadHistory } from "./history.js";
import { downloadMessageImages } from "./media.js";
import { createProcessedMessageTracker } from "./processed-messages.js";
import {
applyTlonSettingsOverrides,
buildTlonSettingsMigrations,
mergeUniqueStrings,
} from "./settings-helpers.js";
import {
extractMessageText,
extractCites,
@ -177,48 +182,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
// Migrate file config to settings store (seed on first run)
async function migrateConfigToSettings() {
const migrations: Array<{ key: string; fileValue: unknown; settingsValue: unknown }> = [
{
key: "dmAllowlist",
fileValue: account.dmAllowlist,
settingsValue: currentSettings.dmAllowlist,
},
{
key: "groupInviteAllowlist",
fileValue: account.groupInviteAllowlist,
settingsValue: currentSettings.groupInviteAllowlist,
},
{
key: "groupChannels",
fileValue: account.groupChannels,
settingsValue: currentSettings.groupChannels,
},
{
key: "defaultAuthorizedShips",
fileValue: account.defaultAuthorizedShips,
settingsValue: currentSettings.defaultAuthorizedShips,
},
{
key: "autoDiscoverChannels",
fileValue: account.autoDiscoverChannels,
settingsValue: currentSettings.autoDiscoverChannels,
},
{
key: "autoAcceptDmInvites",
fileValue: account.autoAcceptDmInvites,
settingsValue: currentSettings.autoAcceptDmInvites,
},
{
key: "autoAcceptGroupInvites",
fileValue: account.autoAcceptGroupInvites,
settingsValue: currentSettings.autoAcceptGroupInvites,
},
{
key: "showModelSig",
fileValue: account.showModelSignature,
settingsValue: currentSettings.showModelSig,
},
];
const migrations = buildTlonSettingsMigrations(account, currentSettings);
for (const { key, fileValue, settingsValue } of migrations) {
// Only migrate if file has a value and settings store doesn't
@ -255,55 +219,21 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
// Migrate file config to settings store if not already present
await migrateConfigToSettings();
// Apply settings overrides
// Note: groupChannels from settings store are merged AFTER discovery runs (below)
if (currentSettings.defaultAuthorizedShips?.length) {
runtime.log?.(
`[tlon] Using defaultAuthorizedShips from settings store: ${currentSettings.defaultAuthorizedShips.join(", ")}`,
);
}
if (currentSettings.autoDiscoverChannels !== undefined) {
effectiveAutoDiscoverChannels = currentSettings.autoDiscoverChannels;
runtime.log?.(
`[tlon] Using autoDiscoverChannels from settings store: ${effectiveAutoDiscoverChannels}`,
);
}
if (currentSettings.dmAllowlist !== undefined) {
effectiveDmAllowlist = currentSettings.dmAllowlist;
runtime.log?.(
`[tlon] Using dmAllowlist from settings store: ${effectiveDmAllowlist.join(", ")}`,
);
}
if (currentSettings.showModelSig !== undefined) {
effectiveShowModelSig = currentSettings.showModelSig;
}
if (currentSettings.autoAcceptDmInvites !== undefined) {
effectiveAutoAcceptDmInvites = currentSettings.autoAcceptDmInvites;
runtime.log?.(
`[tlon] Using autoAcceptDmInvites from settings store: ${effectiveAutoAcceptDmInvites}`,
);
}
if (currentSettings.autoAcceptGroupInvites !== undefined) {
effectiveAutoAcceptGroupInvites = currentSettings.autoAcceptGroupInvites;
runtime.log?.(
`[tlon] Using autoAcceptGroupInvites from settings store: ${effectiveAutoAcceptGroupInvites}`,
);
}
if (currentSettings.groupInviteAllowlist !== undefined) {
effectiveGroupInviteAllowlist = currentSettings.groupInviteAllowlist;
runtime.log?.(
`[tlon] Using groupInviteAllowlist from settings store: ${effectiveGroupInviteAllowlist.join(", ")}`,
);
}
if (currentSettings.ownerShip) {
effectiveOwnerShip = normalizeShip(currentSettings.ownerShip);
runtime.log?.(`[tlon] Using ownerShip from settings store: ${effectiveOwnerShip}`);
}
if (currentSettings.pendingApprovals?.length) {
pendingApprovals = currentSettings.pendingApprovals;
runtime.log?.(`[tlon] Loaded ${pendingApprovals.length} pending approval(s) from settings`);
}
({
effectiveDmAllowlist,
effectiveShowModelSig,
effectiveAutoAcceptDmInvites,
effectiveAutoAcceptGroupInvites,
effectiveGroupInviteAllowlist,
effectiveAutoDiscoverChannels,
effectiveOwnerShip,
pendingApprovals,
currentSettings,
} = applyTlonSettingsOverrides({
account,
currentSettings,
log: (message) => runtime.log?.(message),
}));
} catch (err) {
runtime.log?.(`[tlon] Settings store not available, using file config: ${String(err)}`);
}
@ -323,24 +253,14 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
// Merge manual config with auto-discovered channels
if (account.groupChannels.length > 0) {
for (const ch of account.groupChannels) {
if (!groupChannels.includes(ch)) {
groupChannels.push(ch);
}
}
groupChannels = mergeUniqueStrings(groupChannels, account.groupChannels);
runtime.log?.(
`[tlon] Added ${account.groupChannels.length} manual groupChannels to monitoring`,
);
}
// Also merge settings store groupChannels (may have been set via tlon settings command)
if (currentSettings.groupChannels?.length) {
for (const ch of currentSettings.groupChannels) {
if (!groupChannels.includes(ch)) {
groupChannels.push(ch);
}
}
}
groupChannels = mergeUniqueStrings(groupChannels, currentSettings.groupChannels);
if (groupChannels.length > 0) {
runtime.log?.(

View File

@ -0,0 +1,152 @@
import type { PendingApproval, TlonSettingsStore } from "../settings.js";
import { normalizeShip } from "../targets.js";
import type { TlonResolvedAccount } from "../types.js";
export type TlonMonitorSettingsState = {
effectiveDmAllowlist: string[];
effectiveShowModelSig: boolean;
effectiveAutoAcceptDmInvites: boolean;
effectiveAutoAcceptGroupInvites: boolean;
effectiveGroupInviteAllowlist: string[];
effectiveAutoDiscoverChannels: boolean;
effectiveOwnerShip: string | null;
pendingApprovals: PendingApproval[];
currentSettings: TlonSettingsStore;
};
export function buildTlonSettingsMigrations(
account: TlonResolvedAccount,
currentSettings: TlonSettingsStore,
): Array<{ key: string; fileValue: unknown; settingsValue: unknown }> {
return [
{
key: "dmAllowlist",
fileValue: account.dmAllowlist,
settingsValue: currentSettings.dmAllowlist,
},
{
key: "groupInviteAllowlist",
fileValue: account.groupInviteAllowlist,
settingsValue: currentSettings.groupInviteAllowlist,
},
{
key: "groupChannels",
fileValue: account.groupChannels,
settingsValue: currentSettings.groupChannels,
},
{
key: "defaultAuthorizedShips",
fileValue: account.defaultAuthorizedShips,
settingsValue: currentSettings.defaultAuthorizedShips,
},
{
key: "autoDiscoverChannels",
fileValue: account.autoDiscoverChannels,
settingsValue: currentSettings.autoDiscoverChannels,
},
{
key: "autoAcceptDmInvites",
fileValue: account.autoAcceptDmInvites,
settingsValue: currentSettings.autoAcceptDmInvites,
},
{
key: "autoAcceptGroupInvites",
fileValue: account.autoAcceptGroupInvites,
settingsValue: currentSettings.autoAcceptGroupInvites,
},
{
key: "showModelSig",
fileValue: account.showModelSignature,
settingsValue: currentSettings.showModelSig,
},
];
}
export function applyTlonSettingsOverrides(params: {
account: TlonResolvedAccount;
currentSettings: TlonSettingsStore;
log?: (message: string) => void;
}): TlonMonitorSettingsState {
let effectiveDmAllowlist = params.account.dmAllowlist;
let effectiveShowModelSig = params.account.showModelSignature ?? false;
let effectiveAutoAcceptDmInvites = params.account.autoAcceptDmInvites ?? false;
let effectiveAutoAcceptGroupInvites = params.account.autoAcceptGroupInvites ?? false;
let effectiveGroupInviteAllowlist = params.account.groupInviteAllowlist;
let effectiveAutoDiscoverChannels = params.account.autoDiscoverChannels ?? false;
let effectiveOwnerShip = params.account.ownerShip
? normalizeShip(params.account.ownerShip)
: null;
let pendingApprovals: PendingApproval[] = [];
if (params.currentSettings.defaultAuthorizedShips?.length) {
params.log?.(
`[tlon] Using defaultAuthorizedShips from settings store: ${params.currentSettings.defaultAuthorizedShips.join(", ")}`,
);
}
if (params.currentSettings.autoDiscoverChannels !== undefined) {
effectiveAutoDiscoverChannels = params.currentSettings.autoDiscoverChannels;
params.log?.(
`[tlon] Using autoDiscoverChannels from settings store: ${effectiveAutoDiscoverChannels}`,
);
}
if (params.currentSettings.dmAllowlist !== undefined) {
effectiveDmAllowlist = params.currentSettings.dmAllowlist;
params.log?.(
`[tlon] Using dmAllowlist from settings store: ${effectiveDmAllowlist.join(", ")}`,
);
}
if (params.currentSettings.showModelSig !== undefined) {
effectiveShowModelSig = params.currentSettings.showModelSig;
}
if (params.currentSettings.autoAcceptDmInvites !== undefined) {
effectiveAutoAcceptDmInvites = params.currentSettings.autoAcceptDmInvites;
params.log?.(
`[tlon] Using autoAcceptDmInvites from settings store: ${effectiveAutoAcceptDmInvites}`,
);
}
if (params.currentSettings.autoAcceptGroupInvites !== undefined) {
effectiveAutoAcceptGroupInvites = params.currentSettings.autoAcceptGroupInvites;
params.log?.(
`[tlon] Using autoAcceptGroupInvites from settings store: ${effectiveAutoAcceptGroupInvites}`,
);
}
if (params.currentSettings.groupInviteAllowlist !== undefined) {
effectiveGroupInviteAllowlist = params.currentSettings.groupInviteAllowlist;
params.log?.(
`[tlon] Using groupInviteAllowlist from settings store: ${effectiveGroupInviteAllowlist.join(", ")}`,
);
}
if (params.currentSettings.ownerShip) {
effectiveOwnerShip = normalizeShip(params.currentSettings.ownerShip);
params.log?.(`[tlon] Using ownerShip from settings store: ${effectiveOwnerShip}`);
}
if (params.currentSettings.pendingApprovals?.length) {
pendingApprovals = params.currentSettings.pendingApprovals;
params.log?.(`[tlon] Loaded ${pendingApprovals.length} pending approval(s) from settings`);
}
return {
effectiveDmAllowlist,
effectiveShowModelSig,
effectiveAutoAcceptDmInvites,
effectiveAutoAcceptGroupInvites,
effectiveGroupInviteAllowlist,
effectiveAutoDiscoverChannels,
effectiveOwnerShip,
pendingApprovals,
currentSettings: params.currentSettings,
};
}
export function mergeUniqueStrings(base: string[], next?: string[]): string[] {
if (!next?.length) {
return [...base];
}
const merged = [...base];
for (const value of next) {
if (!merged.includes(value)) {
merged.push(value);
}
}
return merged;
}