Discord: default reaction notifications to own
This commit is contained in:
@@ -170,7 +170,7 @@ struct DiscordGuildForm: Identifiable {
|
|||||||
key: String = "",
|
key: String = "",
|
||||||
slug: String = "",
|
slug: String = "",
|
||||||
requireMention: Bool = false,
|
requireMention: Bool = false,
|
||||||
reactionNotifications: String = "allowlist",
|
reactionNotifications: String = "own",
|
||||||
users: String = "",
|
users: String = "",
|
||||||
channels: [DiscordGuildChannelForm] = []
|
channels: [DiscordGuildChannelForm] = []
|
||||||
) {
|
) {
|
||||||
@@ -497,7 +497,7 @@ final class ConnectionsStore {
|
|||||||
let reactionModeRaw = entry["reactionNotifications"]?.stringValue ?? ""
|
let reactionModeRaw = entry["reactionNotifications"]?.stringValue ?? ""
|
||||||
let reactionNotifications = ["off", "own", "all", "allowlist"].contains(reactionModeRaw)
|
let reactionNotifications = ["off", "own", "all", "allowlist"].contains(reactionModeRaw)
|
||||||
? reactionModeRaw
|
? reactionModeRaw
|
||||||
: "allowlist"
|
: "own"
|
||||||
let users = entry["users"]?.arrayValue?
|
let users = entry["users"]?.arrayValue?
|
||||||
.compactMap { item -> String? in
|
.compactMap { item -> String? in
|
||||||
if let str = item.stringValue { return str }
|
if let str = item.stringValue { return str }
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ Configure the Discord bot by setting the bot token and optional gating:
|
|||||||
"123456789012345678": { // guild id (preferred) or slug
|
"123456789012345678": { // guild id (preferred) or slug
|
||||||
slug: "friends-of-clawd",
|
slug: "friends-of-clawd",
|
||||||
requireMention: false, // per-guild default
|
requireMention: false, // per-guild default
|
||||||
reactionNotifications: "allowlist", // off | own | all | allowlist
|
reactionNotifications: "own", // off | own | all | allowlist
|
||||||
users: ["987654321098765432"], // optional per-guild user allowlist
|
users: ["987654321098765432"], // optional per-guild user allowlist
|
||||||
channels: {
|
channels: {
|
||||||
general: { allow: true },
|
general: { allow: true },
|
||||||
@@ -283,9 +283,9 @@ Clawdis starts Discord only when a `discord` config section exists. The token is
|
|||||||
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
|
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
|
||||||
Reaction notification modes:
|
Reaction notification modes:
|
||||||
- `off`: no reaction events.
|
- `off`: no reaction events.
|
||||||
- `own`: all reactions on the bot's own messages.
|
- `own`: reactions on the bot's own messages (default).
|
||||||
- `all`: all reactions on all messages.
|
- `all`: all reactions on all messages.
|
||||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages.
|
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||||||
|
|
||||||
### `imessage` (imsg CLI)
|
### `imessage` (imsg CLI)
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-rea
|
|||||||
"123456789012345678": {
|
"123456789012345678": {
|
||||||
slug: "friends-of-clawd",
|
slug: "friends-of-clawd",
|
||||||
requireMention: false,
|
requireMention: false,
|
||||||
reactionNotifications: "allowlist",
|
reactionNotifications: "own",
|
||||||
users: ["987654321098765432", "steipete"],
|
users: ["987654321098765432", "steipete"],
|
||||||
channels: {
|
channels: {
|
||||||
general: { allow: true },
|
general: { allow: true },
|
||||||
@@ -121,9 +121,9 @@ Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-rea
|
|||||||
|
|
||||||
Reaction notifications use `guilds.<id>.reactionNotifications`:
|
Reaction notifications use `guilds.<id>.reactionNotifications`:
|
||||||
- `off`: no reaction events.
|
- `off`: no reaction events.
|
||||||
- `own`: all reactions on the bot's own messages.
|
- `own`: reactions on the bot's own messages (default).
|
||||||
- `all`: all reactions on all messages.
|
- `all`: all reactions on all messages.
|
||||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages.
|
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||||||
|
|
||||||
### Tool action defaults
|
### Tool action defaults
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
resolveDiscordGuildEntry,
|
resolveDiscordGuildEntry,
|
||||||
resolveDiscordReplyTarget,
|
resolveDiscordReplyTarget,
|
||||||
resolveGroupDmAllow,
|
resolveGroupDmAllow,
|
||||||
|
shouldEmitDiscordReactionNotification,
|
||||||
} from "./monitor.js";
|
} from "./monitor.js";
|
||||||
|
|
||||||
const fakeGuild = (id: string, name: string) =>
|
const fakeGuild = (id: string, name: string) =>
|
||||||
@@ -21,6 +22,7 @@ const makeEntries = (
|
|||||||
out[key] = {
|
out[key] = {
|
||||||
slug: value.slug,
|
slug: value.slug,
|
||||||
requireMention: value.requireMention,
|
requireMention: value.requireMention,
|
||||||
|
reactionNotifications: value.reactionNotifications,
|
||||||
users: value.users,
|
users: value.users,
|
||||||
channels: value.channels,
|
channels: value.channels,
|
||||||
};
|
};
|
||||||
@@ -207,3 +209,87 @@ describe("discord reply target selection", () => {
|
|||||||
).toBe("123");
|
).toBe("123");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("discord reaction notification gating", () => {
|
||||||
|
it("defaults to own when mode is unset", () => {
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: undefined,
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "bot-1",
|
||||||
|
userId: "user-1",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: undefined,
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "user-1",
|
||||||
|
userId: "user-2",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips when mode is off", () => {
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: "off",
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "bot-1",
|
||||||
|
userId: "user-1",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows all reactions when mode is all", () => {
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: "all",
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "user-1",
|
||||||
|
userId: "user-2",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires bot ownership when mode is own", () => {
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: "own",
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "bot-1",
|
||||||
|
userId: "user-2",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: "own",
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "user-2",
|
||||||
|
userId: "user-3",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires allowlist matches when mode is allowlist", () => {
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: "allowlist",
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "user-1",
|
||||||
|
userId: "user-2",
|
||||||
|
allowlist: [],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldEmitDiscordReactionNotification({
|
||||||
|
mode: "allowlist",
|
||||||
|
botId: "bot-1",
|
||||||
|
messageAuthorId: "user-1",
|
||||||
|
userId: "123",
|
||||||
|
userName: "steipete",
|
||||||
|
allowlist: ["123", "other"],
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -559,28 +559,17 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const botId = client.user?.id;
|
const botId = client.user?.id;
|
||||||
if (botId && user.id === botId) return;
|
if (botId && user.id === botId) return;
|
||||||
|
|
||||||
const reactionMode = guildInfo?.reactionNotifications ?? "allowlist";
|
const reactionMode = guildInfo?.reactionNotifications ?? "own";
|
||||||
if (reactionMode === "off") return;
|
const shouldNotify = shouldEmitDiscordReactionNotification({
|
||||||
if (reactionMode === "own") {
|
mode: reactionMode,
|
||||||
const authorId = message.author?.id;
|
botId,
|
||||||
if (!botId || authorId !== botId) return;
|
messageAuthorId: message.author?.id,
|
||||||
}
|
userId: user.id,
|
||||||
if (reactionMode === "allowlist") {
|
userName: user.username,
|
||||||
const userAllow = guildInfo?.users;
|
userTag: user.tag,
|
||||||
if (!Array.isArray(userAllow) || userAllow.length === 0) return;
|
allowlist: guildInfo?.users,
|
||||||
const users = normalizeDiscordAllowList(userAllow, [
|
});
|
||||||
"discord:",
|
if (!shouldNotify) return;
|
||||||
"user:",
|
|
||||||
]);
|
|
||||||
const userOk =
|
|
||||||
!!users &&
|
|
||||||
allowListMatches(users, {
|
|
||||||
id: user.id,
|
|
||||||
name: user.username,
|
|
||||||
tag: user.tag,
|
|
||||||
});
|
|
||||||
if (!userOk) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiLabel = formatDiscordReactionEmoji(resolvedReaction);
|
const emojiLabel = formatDiscordReactionEmoji(resolvedReaction);
|
||||||
const actorLabel = user.tag ?? user.username ?? user.id;
|
const actorLabel = user.tag ?? user.username ?? user.id;
|
||||||
@@ -985,6 +974,43 @@ export function allowListMatches(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldEmitDiscordReactionNotification(params: {
|
||||||
|
mode: "off" | "own" | "all" | "allowlist" | undefined;
|
||||||
|
botId?: string | null;
|
||||||
|
messageAuthorId?: string | null;
|
||||||
|
userId: string;
|
||||||
|
userName?: string | null;
|
||||||
|
userTag?: string | null;
|
||||||
|
allowlist?: Array<string | number> | null;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
botId,
|
||||||
|
messageAuthorId,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
userTag,
|
||||||
|
allowlist,
|
||||||
|
} = params;
|
||||||
|
const effectiveMode = mode ?? "own";
|
||||||
|
if (effectiveMode === "off") return false;
|
||||||
|
if (effectiveMode === "own") {
|
||||||
|
if (!botId || !messageAuthorId) return false;
|
||||||
|
return messageAuthorId === botId;
|
||||||
|
}
|
||||||
|
if (effectiveMode === "allowlist") {
|
||||||
|
if (!Array.isArray(allowlist) || allowlist.length === 0) return false;
|
||||||
|
const users = normalizeDiscordAllowList(allowlist, ["discord:", "user:"]);
|
||||||
|
if (!users) return false;
|
||||||
|
return allowListMatches(users, {
|
||||||
|
id: userId,
|
||||||
|
name: userName ?? undefined,
|
||||||
|
tag: userTag ?? undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveDiscordGuildEntry(params: {
|
export function resolveDiscordGuildEntry(params: {
|
||||||
guild: Guild | null;
|
guild: Guild | null;
|
||||||
guildEntries: Record<string, DiscordGuildEntryResolved> | undefined;
|
guildEntries: Record<string, DiscordGuildEntryResolved> | undefined;
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
|||||||
entry.reactionNotifications === "own" ||
|
entry.reactionNotifications === "own" ||
|
||||||
entry.reactionNotifications === "allowlist"
|
entry.reactionNotifications === "allowlist"
|
||||||
? entry.reactionNotifications
|
? entry.reactionNotifications
|
||||||
: "allowlist",
|
: "own",
|
||||||
users: toList(entry.users),
|
users: toList(entry.users),
|
||||||
channels,
|
channels,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -654,7 +654,7 @@ function renderProvider(
|
|||||||
next[guildIndex] = {
|
next[guildIndex] = {
|
||||||
...next[guildIndex],
|
...next[guildIndex],
|
||||||
reactionNotifications: (e.target as HTMLSelectElement)
|
reactionNotifications: (e.target as HTMLSelectElement)
|
||||||
.value as "off" | "own" | "all",
|
.value as "off" | "own" | "all" | "allowlist",
|
||||||
};
|
};
|
||||||
props.onDiscordChange({ guilds: next });
|
props.onDiscordChange({ guilds: next });
|
||||||
}}
|
}}
|
||||||
@@ -832,7 +832,7 @@ function renderProvider(
|
|||||||
key: "",
|
key: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
requireMention: false,
|
requireMention: false,
|
||||||
reactionNotifications: "allowlist",
|
reactionNotifications: "own",
|
||||||
users: "",
|
users: "",
|
||||||
channels: [],
|
channels: [],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user