refactor: split monitor runtime helpers
This commit is contained in:
parent
fb5ab95e03
commit
a2518a16ac
1030
extensions/discord/src/monitor/native-command-ui.ts
Normal file
1030
extensions/discord/src/monitor/native-command-ui.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
211
extensions/mattermost/src/mattermost/monitor-slash.ts
Normal file
211
extensions/mattermost/src/mattermost/monitor-slash.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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?.(
|
||||
|
||||
152
extensions/tlon/src/monitor/settings-helpers.ts
Normal file
152
extensions/tlon/src/monitor/settings-helpers.ts
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user