Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions lib/commands/pinglock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { addPingLock, removePingLock, listPingLocks } from "../db.js";
import { canManage } from "../perms.js";

const parseGroup = (s) =>
s?.match(/<!subteam\^(\w+)/)?.[1] || s?.replace(/[<>@!]/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 <!subteam^${groupId}> 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 <!subteam^${groupId}> 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) => `• <!subteam^${l.group_id}> — 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]`"),
);
}
},
};
58 changes: 58 additions & 0 deletions lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)'),
Expand All @@ -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'),
Expand Down Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions lib/listeners/pingLock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { listPingLocks } from "../db.js";
import { canManage } from "../perms.js";
import { lockThread, areWeEnterprise } from "../moderation.js";

const SUBTEAM_RE = /<!subteam\^(\w+)/g;

export default async function pingLockListener({
event,
context: _context,
client,
logger,
}) {
if (!event || event.type !== "message" || !event.user) return;
if (event.subtype) return;
if (event.thread_ts) return;
if (!areWeEnterprise) return;

const text = event.text || "";
const mentioned = [...text.matchAll(SUBTEAM_RE)].map((m) => 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}`,
);
}
}