Slack: refine scopes and onboarding

This commit is contained in:
Shadow
2026-01-03 23:12:11 -06:00
committed by Peter Steinberger
parent bf3d120f8c
commit 0085b2e0a9
17 changed files with 2484 additions and 1 deletions

View File

@@ -235,6 +235,92 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
typeof slash.ephemeral === "boolean" ? slash.ephemeral : true,
};
const slackDm = (slack.dm ?? {}) as Record<string, unknown>;
const slackChannels = slack.channels;
const slackSlash = (slack.slashCommand ?? {}) as Record<string, unknown>;
const slackActions =
(slack.actions ?? {}) as Partial<Record<keyof typeof defaultSlackActions, unknown>>;
state.slackForm = {
enabled: typeof slack.enabled === "boolean" ? slack.enabled : true,
botToken: typeof slack.botToken === "string" ? slack.botToken : "",
appToken: typeof slack.appToken === "string" ? slack.appToken : "",
dmEnabled: typeof slackDm.enabled === "boolean" ? slackDm.enabled : true,
allowFrom: toList(slackDm.allowFrom),
groupEnabled:
typeof slackDm.groupEnabled === "boolean" ? slackDm.groupEnabled : false,
groupChannels: toList(slackDm.groupChannels),
mediaMaxMb:
typeof slack.mediaMaxMb === "number" ? String(slack.mediaMaxMb) : "",
textChunkLimit:
typeof slack.textChunkLimit === "number"
? String(slack.textChunkLimit)
: "",
replyToMode:
slack.replyToMode === "first" || slack.replyToMode === "all"
? slack.replyToMode
: "off",
reactionNotifications:
slack.reactionNotifications === "off" ||
slack.reactionNotifications === "all" ||
slack.reactionNotifications === "allowlist"
? slack.reactionNotifications
: "own",
reactionAllowlist: toList(slack.reactionAllowlist),
slashEnabled:
typeof slackSlash.enabled === "boolean" ? slackSlash.enabled : false,
slashName: typeof slackSlash.name === "string" ? slackSlash.name : "",
slashSessionPrefix:
typeof slackSlash.sessionPrefix === "string"
? slackSlash.sessionPrefix
: "",
slashEphemeral:
typeof slackSlash.ephemeral === "boolean" ? slackSlash.ephemeral : true,
actions: {
...defaultSlackActions,
reactions:
typeof slackActions.reactions === "boolean"
? slackActions.reactions
: defaultSlackActions.reactions,
messages:
typeof slackActions.messages === "boolean"
? slackActions.messages
: defaultSlackActions.messages,
pins:
typeof slackActions.pins === "boolean"
? slackActions.pins
: defaultSlackActions.pins,
memberInfo:
typeof slackActions.memberInfo === "boolean"
? slackActions.memberInfo
: defaultSlackActions.memberInfo,
emojiList:
typeof slackActions.emojiList === "boolean"
? slackActions.emojiList
: defaultSlackActions.emojiList,
},
channels: Array.isArray(slackChannels)
? []
: typeof slackChannels === "object" && slackChannels
? Object.entries(slackChannels as Record<string, unknown>).map(
([key, value]): SlackChannelForm => {
const entry =
value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
return {
key,
allow:
typeof entry.allow === "boolean" ? entry.allow : true,
requireMention:
typeof entry.requireMention === "boolean"
? entry.requireMention
: false,
};
},
)
: [],
};
state.signalForm = {
enabled: typeof signal.enabled === "boolean" ? signal.enabled : true,
account: typeof signal.account === "string" ? signal.account : "",
@@ -281,6 +367,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
const configInvalid = snapshot.valid === false ? "Config invalid." : null;
state.telegramConfigStatus = configInvalid;
state.discordConfigStatus = configInvalid;
state.slackConfigStatus = configInvalid;
state.signalConfigStatus = configInvalid;
state.imessageConfigStatus = configInvalid;
@@ -405,3 +492,4 @@ function removePathValue(
delete (current as Record<string, unknown>)[lastKey];
}
}

View File

@@ -379,6 +379,147 @@ export async function saveDiscordConfig(state: ConnectionsState) {
}
}
export async function saveSlackConfig(state: ConnectionsState) {
if (!state.client || !state.connected) return;
if (state.slackSaving) return;
state.slackSaving = true;
state.slackConfigStatus = null;
try {
const base = state.configSnapshot?.config ?? {};
const config = { ...base } as Record<string, unknown>;
const slack = { ...(config.slack ?? {}) } as Record<string, unknown>;
const form = state.slackForm;
if (form.enabled) {
delete slack.enabled;
} else {
slack.enabled = false;
}
if (!state.slackTokenLocked) {
const token = form.botToken.trim();
if (token) slack.botToken = token;
else delete slack.botToken;
}
if (!state.slackAppTokenLocked) {
const token = form.appToken.trim();
if (token) slack.appToken = token;
else delete slack.appToken;
}
const dm = { ...(slack.dm ?? {}) } as Record<string, unknown>;
dm.enabled = form.dmEnabled;
const allowFrom = parseList(form.allowFrom);
if (allowFrom.length > 0) dm.allowFrom = allowFrom;
else delete dm.allowFrom;
if (form.groupEnabled) {
dm.groupEnabled = true;
} else {
delete dm.groupEnabled;
}
const groupChannels = parseList(form.groupChannels);
if (groupChannels.length > 0) dm.groupChannels = groupChannels;
else delete dm.groupChannels;
if (Object.keys(dm).length > 0) slack.dm = dm;
else delete slack.dm;
const mediaMaxMb = Number.parseFloat(form.mediaMaxMb);
if (Number.isFinite(mediaMaxMb) && mediaMaxMb > 0) {
slack.mediaMaxMb = mediaMaxMb;
} else {
delete slack.mediaMaxMb;
}
const textChunkLimit = Number.parseInt(form.textChunkLimit, 10);
if (Number.isFinite(textChunkLimit) && textChunkLimit > 0) {
slack.textChunkLimit = textChunkLimit;
} else {
delete slack.textChunkLimit;
}
if (form.replyToMode === "off") delete slack.replyToMode;
else slack.replyToMode = form.replyToMode;
if (form.reactionNotifications === "own") {
delete slack.reactionNotifications;
} else {
slack.reactionNotifications = form.reactionNotifications;
}
const reactionAllowlist = parseList(form.reactionAllowlist);
if (reactionAllowlist.length > 0) {
slack.reactionAllowlist = reactionAllowlist;
} else {
delete slack.reactionAllowlist;
}
const slash = { ...(slack.slashCommand ?? {}) } as Record<string, unknown>;
if (form.slashEnabled) {
slash.enabled = true;
} else {
delete slash.enabled;
}
if (form.slashName.trim()) slash.name = form.slashName.trim();
else delete slash.name;
if (form.slashSessionPrefix.trim())
slash.sessionPrefix = form.slashSessionPrefix.trim();
else delete slash.sessionPrefix;
if (form.slashEphemeral) {
delete slash.ephemeral;
} else {
slash.ephemeral = false;
}
if (Object.keys(slash).length > 0) slack.slashCommand = slash;
else delete slack.slashCommand;
const actions: Partial<SlackActionForm> = {};
const applyAction = (key: keyof SlackActionForm) => {
const value = form.actions[key];
if (value !== defaultSlackActions[key]) actions[key] = value;
};
applyAction("reactions");
applyAction("messages");
applyAction("pins");
applyAction("memberInfo");
applyAction("emojiList");
if (Object.keys(actions).length > 0) {
slack.actions = actions;
} else {
delete slack.actions;
}
const channels = form.channels
.map((entry): [string, Record<string, unknown>] | null => {
const key = entry.key.trim();
if (!key) return null;
const record: Record<string, unknown> = {
allow: entry.allow,
requireMention: entry.requireMention,
};
return [key, record];
})
.filter((value): value is [string, Record<string, unknown>] => Boolean(value));
if (channels.length > 0) {
slack.channels = Object.fromEntries(channels);
} else {
delete slack.channels;
}
if (Object.keys(slack).length > 0) {
config.slack = slack;
} else {
delete config.slack;
}
const raw = `${JSON.stringify(config, null, 2).trimEnd()}\n`;
await state.client.request("config.set", { raw });
state.slackConfigStatus = "Saved. Restart gateway if needed.";
} catch (err) {
state.slackConfigStatus = String(err);
} finally {
state.slackSaving = false;
}
}
export async function saveSignalConfig(state: ConnectionsState) {
if (!state.client || !state.connected) return;
if (state.signalSaving) return;
@@ -529,3 +670,4 @@ export async function saveIMessageConfig(state: ConnectionsState) {
state.imessageSaving = false;
}
}

View File

@@ -60,6 +60,41 @@ export type DiscordActionForm = {
moderation: boolean;
};
export type SlackChannelForm = {
key: string;
allow: boolean;
requireMention: boolean;
};
export type SlackActionForm = {
reactions: boolean;
messages: boolean;
pins: boolean;
memberInfo: boolean;
emojiList: boolean;
};
export type SlackForm = {
enabled: boolean;
botToken: string;
appToken: string;
dmEnabled: boolean;
allowFrom: string;
groupEnabled: boolean;
groupChannels: string;
mediaMaxMb: string;
textChunkLimit: string;
replyToMode: "off" | "first" | "all";
reactionNotifications: "off" | "own" | "all" | "allowlist";
reactionAllowlist: string;
slashEnabled: boolean;
slashName: string;
slashSessionPrefix: string;
slashEphemeral: boolean;
actions: SlackActionForm;
channels: SlackChannelForm[];
};
export const defaultDiscordActions: DiscordActionForm = {
reactions: true,
stickers: true,
@@ -78,6 +113,14 @@ export const defaultDiscordActions: DiscordActionForm = {
moderation: false,
};
export const defaultSlackActions: SlackActionForm = {
reactions: true,
messages: true,
pins: true,
memberInfo: true,
emojiList: true,
};
export type SignalForm = {
enabled: boolean;
account: string;
@@ -125,3 +168,4 @@ export type CronFormState = {
timeoutSeconds: string;
postToMainPrefix: string;
};

View File

@@ -28,6 +28,14 @@ const discordActionOptions = [
{ key: "moderation", label: "Moderation" },
] satisfies Array<{ key: keyof DiscordActionForm; label: string }>;
const slackActionOptions = [
{ key: "reactions", label: "Reactions" },
{ key: "messages", label: "Messages" },
{ key: "pins", label: "Pins" },
{ key: "memberInfo", label: "Member info" },
{ key: "emojiList", label: "Emoji list" },
] satisfies Array<{ key: keyof SlackActionForm; label: string }>;
export type ConnectionsProps = {
connected: boolean;
loading: boolean;
@@ -1347,3 +1355,4 @@ function renderProvider(
return nothing;
}
}