diff --git a/lib/commands/pinglock.js b/lib/commands/pinglock.js new file mode 100644 index 0000000..c4cae4e --- /dev/null +++ b/lib/commands/pinglock.js @@ -0,0 +1,65 @@ +import { addPingLock, removePingLock, listPingLocks } from "../db.js"; +import { canManage } from "../perms.js"; + +const parseGroup = (s) => + s?.match(/@!]/g, ""); +const eph = (text) => ({ response_type: "ephemeral", text }); + +export default { + name: "pinglock", + description: "Manage automatic thread locking for user group pings", + async execute({ command, args, respond, client }) { + const u = command.user_id, + ch = command.channel_id; + const [action, target] = args; + + if (!(await canManage(client, u, ch))) { + console.log(`[pinglock] ${u} denied in ${ch}`); + return respond(eph(":loll: You do not have permission! :P")); + } + + switch (action) { + case "add": { + const groupId = parseGroup(target); + if (!groupId) + return respond(eph("Usage: `/pro pinglock add @usergroup`")); + addPingLock(ch, groupId, u); + console.log( + `[pinglock] ${u} added ping lock for group ${groupId} in ${ch}`, + ); + return respond( + eph( + `:white_check_mark: Threads containing pings in <#${ch}> will now be auto-locked.`, + ), + ); + } + case "remove": { + const groupId = parseGroup(target); + if (!groupId) + return respond(eph("Usage: `/pro pinglock remove @usergroup`")); + removePingLock(ch, groupId); + console.log( + `[pinglock] ${u} removed ping lock for group ${groupId} in ${ch}`, + ); + return respond( + eph( + `:white_check_mark: Removed auto-lock for in <#${ch}>.`, + ), + ); + } + case "list": { + const locks = listPingLocks(ch); + if (!locks.length) + return respond(eph("No ping locks configured for this channel.")); + const lines = locks.map( + (l) => `• — added by <@${l.added_by}>`, + ); + return respond(eph(`*Ping locks for <#${ch}>:*\n${lines.join("\n")}`)); + } + default: + return respond( + eph("Usage: `/pro pinglock [add|remove|list] [@usergroup]`"), + ); + } + }, +}; diff --git a/lib/db.js b/lib/db.js index 0980349..03e4f1d 100644 --- a/lib/db.js +++ b/lib/db.js @@ -51,6 +51,16 @@ db.run(` ) `); +db.run(` + CREATE TABLE IF NOT EXISTS ping_locks ( + channel_id TEXT NOT NULL, + group_id TEXT NOT NULL, + added_by TEXT NOT NULL, + added_at INTEGER NOT NULL DEFAULT (unixepoch()), + PRIMARY KEY (channel_id, group_id) + ) +`); + db.run(` CREATE TABLE IF NOT EXISTS embed_blocks ( channel_id TEXT NOT NULL, @@ -62,6 +72,21 @@ db.run(` ) `); +// seed SUPERADMINS into global_admins table +const superadmins = (process.env.SUPERADMINS || "").split(",").filter(Boolean); +for (const uid of superadmins) { + db.run( + "INSERT OR IGNORE INTO global_admins (user_id, added_by) VALUES (?, 'SUPERADMINS')", + [uid], + ); +} +if (superadmins.length) { + console.log( + `[db] seeded ${superadmins.length} superadmins into global_admins`, + ); +} + + const statements = { isAdmin: db.query('SELECT 1 FROM global_admins WHERE user_id = $userId'), addAdmin: db.query('INSERT OR IGNORE INTO global_admins (user_id, added_by) VALUES ($userId, $addedBy)'), @@ -87,6 +112,12 @@ const statements = { setwelcome: db.query('INSERT OR REPLACE INTO join_messages (channel_id, message, mode, set_by) VALUES ($channelId, $message, $mode, $setBy)'), removewelcome: db.query('DELETE FROM join_messages WHERE channel_id = $channelId'), + getPingLock: db.query('SELECT 1 FROM ping_locks WHERE channel_id = $channelId AND group_id = $groupId'), + addPingLock: db.query('INSERT OR IGNORE INTO ping_locks (channel_id, group_id, added_by) VALUES ($channelId, $groupId, $addedBy)'), + removePingLock: db.query('DELETE FROM ping_locks WHERE channel_id = $channelId AND group_id = $groupId'), + listPingLocks: db.query('SELECT group_id, added_by, added_at FROM ping_locks WHERE channel_id = $channelId'), + getPingLockChannels: db.query('SELECT channel_id FROM ping_locks WHERE group_id = $groupId'), + addEmbedBlock: db.query('INSERT OR REPLACE INTO embed_blocks (channel_id, type, target, blocked_by) VALUES ($channelId, $type, $target, $blockedBy)'), removeEmbedBlock: db.query('DELETE FROM embed_blocks WHERE channel_id = $channelId AND type = $type AND target = $target'), listEmbedBlocks: db.query('SELECT channel_id, type, target, blocked_by, blocked_at FROM embed_blocks WHERE channel_id = $channelId'), @@ -179,6 +210,33 @@ export function removewelcome(channelId) { statements.removewelcome.run({ $channelId: channelId }); } +export function getPingLock(channelId, groupId) { + return !!statements.getPingLock.get({ + $channelId: channelId, + $groupId: groupId, + }); +} + +export function addPingLock(channelId, groupId, addedBy) { + statements.addPingLock.run({ + $channelId: channelId, + $groupId: groupId, + $addedBy: addedBy, + }); +} + +export function removePingLock(channelId, groupId) { + statements.removePingLock.run({ $channelId: channelId, $groupId: groupId }); +} + +export function listPingLocks(channelId) { + return statements.listPingLocks.all({ $channelId: channelId }); +} + +export function getPingLockChannels(groupId) { + return statements.getPingLockChannels.all({ $groupId: groupId }); +} + export function addEmbedBlock(channelId, type, target, blockedBy) { statements.addEmbedBlock.run({ $channelId: channelId, diff --git a/lib/listeners/pingLock.js b/lib/listeners/pingLock.js new file mode 100644 index 0000000..e68cc7a --- /dev/null +++ b/lib/listeners/pingLock.js @@ -0,0 +1,54 @@ +import { listPingLocks } from "../db.js"; +import { canManage } from "../perms.js"; +import { lockThread, areWeEnterprise } from "../moderation.js"; + +const SUBTEAM_RE = / m[1]); + if (!mentioned.length) return; + + const locks = listPingLocks(event.channel); + if (!locks.length) return; + + const lockedGroups = new Set(locks.map((l) => l.group_id)); + const matched = mentioned.filter((g) => lockedGroups.has(g)); + if (!matched.length) return; + + // triggered only by cms + if (!(await canManage(client, event.user, event.channel))) return; + + console.log( + `[pinglock] auto-locking thread ${event.ts} in ${event.channel} (groups: ${matched.join(", ")})`, + ); + + try { + // you have to put a message before locking to create the thread + await client.chat.postMessage({ + channel: event.channel, + thread_ts: event.ts, + text: ":lock: This thread has been locked.", + }); + + await lockThread(event.channel, event.ts); + + console.log( + `[pinglock] successfully locked thread ${event.ts} in ${event.channel}`, + ); + } catch (e) { + logger?.error( + `[pinglock] failed to auto-lock thread ${event.ts}: ${e.message}`, + ); + } +}