Skip to content

Commit

Permalink
Add anonymous message reporting mechanism (#199)
Browse files Browse the repository at this point in the history
* Command handler setup

* Move emojiMod helper out into helpers/modLog

* Change emojiMod to use cached log channels

* Add anonymous report command

* Use interactions

* Fix ssh action name
  • Loading branch information
vcarl committed Mar 24, 2022
1 parent ae99527 commit f81732a
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 95 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
cd reactibot
sudo docker build -t reactiflux/reactibot:latest .
- name: Deploy Discord commands
uses: applyboy/ssh-action@master
uses: appleboy/ssh-action@master
env:
DISCORD_HASH: ${{ secrets.DISCORD_HASH }}
with:
Expand Down
4 changes: 3 additions & 1 deletion scripts/deploy-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { applicationId, discordToken, guildId } from "../src/constants";
import { logger } from "../src/features/log";
import { difference } from "../src/helpers/sets";

import * as report from "../src/commands/report";

// TODO: make this a global command in production
const upsertUrl = () => Routes.applicationGuildCommands(applicationId, guildId);
const deleteUrl = (commandId: string) =>
Expand All @@ -22,7 +24,7 @@ interface CommandConfig {
description: string;
type: ApplicationCommandType;
}
const cmds: CommandConfig[] = [];
const cmds: CommandConfig[] = [report];

const commands = [
...cmds
Expand Down
21 changes: 21 additions & 0 deletions src/commands/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApplicationCommandType } from "discord-api-types/v9";
import { Message, MessageContextMenuInteraction } from "discord.js";
import { ReportReasons } from "../constants";
import { constructLog, reportUser } from "../helpers/modLog";

export const name = "report-message";
export const description = "Anonymously report this message";
export const type = ApplicationCommandType.Message;
export const handler = async (interaction: MessageContextMenuInteraction) => {
const message = interaction.targetMessage;
if (!(message instanceof Message)) {
return;
}

reportUser(message, constructLog(ReportReasons.anonReport, [], [], message));

await interaction.reply({
ephemeral: true,
content: "This message has been reported anonymously",
});
};
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const gitHubToken = process.env.GITHUB_TOKEN ?? "";
export const amplitudeKey = process.env.AMPLITUDE_KEY ?? "";

export const enum ReportReasons {
anonReport = "anonReport",
userWarn = "userWarn",
userDelete = "userDelete",
mod = "mod",
Expand Down
15 changes: 14 additions & 1 deletion src/features/commands.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import fetch from "node-fetch";
import { Message, TextChannel } from "discord.js";
import { Client, Message, TextChannel } from "discord.js";
import cooldown from "./cooldown";
import { ChannelHandlers } from "../types";
import { isStaff } from "../helpers/discord";

import * as report from "../commands/report";

export const setupInteractions = (bot: Client) => {
bot.on("interactionCreate", (interaction) => {
if (interaction.isMessageContextMenu()) {
switch (interaction.commandName) {
case report.name:
return report.handler(interaction);
}
}
});
};

export const EMBED_COLOR = 7506394;

type Categories = "Reactiflux" | "Communication" | "Web" | "React/Redux";
Expand Down
103 changes: 13 additions & 90 deletions src/features/emojiMod.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import {
MessageReaction,
Message,
GuildMember,
TextChannel,
Guild,
} from "discord.js";
import { MessageReaction, Message, GuildMember, Guild } from "discord.js";
import cooldown from "./cooldown";
import { ChannelHandlers } from "../types";
import { ReportReasons } from "../constants";
import { CHANNELS } from "../constants/channels";
import { constructLog } from "../helpers/modLog";
import { simplifyString } from "../helpers/string";
import { CHANNELS, getChannel } from "../constants/channels";
import { constructLog, reportUser } from "../helpers/modLog";
import { fetchReactionMembers, isStaff } from "../helpers/discord";
import { partition } from "../helpers/array";

const modLog = getChannel(CHANNELS.modLog);

const AUTO_SPAM_THRESHOLD = 5;
const config = {
// This is how many ️️warning reactions a post must get until it's considered an official warning
Expand All @@ -24,17 +19,11 @@ const config = {
deletionThreshold: Infinity,
};

const warningMessages = new Map<
string,
{ warnings: number; message: Message }
>();

const thumbsDownEmojis = ["👎", "👎🏻", "👎🏼", "👎🏽", "👎🏾", "👎🏿"];

type ReactionHandlers = {
[emoji: string]: (args: {
guild: Guild;
logChannel: TextChannel;
reaction: MessageReaction;
message: Message;
reactor: GuildMember;
Expand All @@ -43,52 +32,8 @@ type ReactionHandlers = {
}) => void;
};

const handleReport = (
reason: ReportReasons,
channelInstance: TextChannel,
reportedMessage: Message,
logBody: string,
) => {
const simplifiedContent = `${reportedMessage.author.id}${simplifyString(
reportedMessage.content,
)}`;
const cached = warningMessages.get(simplifiedContent);

if (cached) {
// If we already logged for ~ this message, edit the log
const { message, warnings: oldWarnings } = cached;
const warnings = oldWarnings + 1;

let finalLog = logBody;
// If this was a mod report, increment the warning count
if (reason === ReportReasons.mod || reason === ReportReasons.spam) {
finalLog = logBody.replace(/warned \d times/, `warned ${warnings} times`);
}

message.edit(finalLog);
warningMessages.set(simplifiedContent, { warnings, message });
return warnings;
} else {
// If this is new, send a new message
channelInstance.send(logBody).then((warningMessage) => {
warningMessages.set(simplifiedContent, {
warnings: 1,
message: warningMessage,
});
});
return 1;
}
};

const reactionHandlers: ReactionHandlers = {
"⚠️": async ({
author,
reactor,
message,
reaction,
usersWhoReacted,
logChannel,
}) => {
export const reactionHandlers: ReactionHandlers = {
"⚠️": async ({ author, reactor, message, reaction, usersWhoReacted }) => {
// Skip if the post is from someone from the staff
if (isStaff(author)) {
return;
Expand All @@ -112,21 +57,9 @@ Thanks!
return;
}

handleReport(
ReportReasons.mod,
logChannel,
message,
constructLog(ReportReasons.mod, [], staff, message),
);
reportUser(message, constructLog(ReportReasons.mod, [], staff, message));
},
"💩": async ({
guild,
author,
reactor,
message,
usersWhoReacted,
logChannel,
}) => {
"💩": async ({ guild, author, reactor, message, usersWhoReacted }) => {
// Skip if the post is from someone from the staff or reactor is not staff
if (isStaff(author) || !isStaff(reactor)) {
return;
Expand All @@ -135,9 +68,7 @@ Thanks!
const [members, staff] = partition(isStaff, usersWhoReacted);

message.delete();
const warnings = handleReport(
ReportReasons.spam,
logChannel,
const warnings = reportUser(
message,
constructLog(
ReportReasons.spam,
Expand All @@ -150,13 +81,11 @@ Thanks!
if (warnings >= AUTO_SPAM_THRESHOLD) {
guild.members.fetch(message.author.id).then((member) => {
member.kick("Autokicked for spamming");
logChannel.send(
`Automatically kicked <@${message.author.id}> for spam`,
);
modLog.send(`Automatically kicked <@${message.author.id}> for spam`);
});
}
},
"👎": async ({ message, reactor, usersWhoReacted, logChannel }) => {
"👎": async ({ message, reactor, usersWhoReacted }) => {
if (cooldown.hasCooldown(reactor.id, "thumbsdown")) {
return;
}
Expand All @@ -181,9 +110,7 @@ Thanks!
message.delete();
}

handleReport(
meetsDeletion ? ReportReasons.userDelete : ReportReasons.userWarn,
logChannel,
reportUser(
message,
constructLog(
trigger,
Expand Down Expand Up @@ -237,10 +164,6 @@ const emojiMod: ChannelHandlers = {
usersWhoReacted: usersWhoReacted.filter((x): x is GuildMember =>
Boolean(x),
),
logChannel: guild.channels.cache.find(
(channel) =>
channel.name === "mod-log" || channel.id === CHANNELS.modLog,
) as TextChannel,
});
},
};
Expand Down
47 changes: 46 additions & 1 deletion src/helpers/modLog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
import { Message } from "discord.js";
import { ReportReasons, modRoleId } from "../constants";
import { constructDiscordLink } from "./discord";
import { simplifyString } from "../helpers/string";
import { CHANNELS, getChannel } from "../constants/channels";

const warningMessages = new Map<
string,
{ warnings: number; message: Message }
>();
export const reportUser = (reportedMessage: Message, logBody: string) => {
const simplifiedContent = `${reportedMessage.author.id}${simplifyString(
reportedMessage.content,
)}`;
const cached = warningMessages.get(simplifiedContent);

if (cached) {
// If we already logged for ~ this message, edit the log
const { message, warnings: oldWarnings } = cached;
const warnings = oldWarnings + 1;

const finalLog = logBody.replace(
/warned \d times/,
`warned ${warnings} times`,
);

message.edit(finalLog);
warningMessages.set(simplifiedContent, { warnings, message });
return warnings;
} else {
// If this is new, send a new message
getChannel(CHANNELS.modLog)
.send(logBody)
.then((warningMessage) => {
warningMessages.set(simplifiedContent, {
warnings: 1,
message: warningMessage,
});
});
return 1;
}
};

// Discord's limit for message length
const maxMessageLength = 2000;
Expand All @@ -18,7 +57,7 @@ export const constructLog = (
members: string[],
staff: string[],
message: Message,
) => {
): string => {
const modAlert = `<@${modRoleId}>`;
const preface = `<@${message.author.id}> in <#${message.channel.id}> warned 1 times`;
const postfix = `Link: ${constructDiscordLink(message)}
Expand Down Expand Up @@ -54,6 +93,12 @@ ${postfix}`;
\`${reportedMessage}\`
${postfix}`;
case ReportReasons.anonReport:
return `${preface}, reported anonymously:
\`${reportedMessage}\`
${postfix}`;
}
};
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { logger, channelLog } from "./features/log";
// import codeblock from './features/codeblock';
import jobsMod from "./features/jobs-moderation";
import autoban from "./features/autoban";
import commands from "./features/commands";
import commands, { setupInteractions } from "./features/commands";
import setupStats from "./features/stats";
import emojiMod from "./features/emojiMod";
import autodelete from "./features/autodelete-spam";
Expand Down Expand Up @@ -150,6 +150,9 @@ logger.add(channelLog(bot, CHANNELS.botLog));
// Amplitude metrics
setupStats(bot);

// Discord commands
setupInteractions(bot);

// common
addHandler("*", [
commands,
Expand Down

0 comments on commit f81732a

Please sign in to comment.