Skip to content

Commit

Permalink
chore(extensions:urlfish): complete support for url scanning and exte…
Browse files Browse the repository at this point in the history
…nsive configuration
  • Loading branch information
virtual-designer committed Feb 16, 2024
1 parent ba804b4 commit 2f9d850
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import EventListener from "@sudobot/core/EventListener";
import { Events } from "@sudobot/types/ClientEvents";
import type URLFishService from "../../services/URLFishService";
import { Message } from "discord.js";
import type URLFishService from "../../services/URLFishService";

export default class NormalMessageCreateEventListener extends EventListener<Events.NormalMessageCreate> {
public readonly name = Events.NormalMessageCreate;

async execute(message: Message) {
const urlfishService = this.client.getService<URLFishService>("urlfish");
const links = urlfishService.scanMessage(message);

if (links.length > 0) {
await message.delete();
}
urlfishService.verifyMessage(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import EventListener from "@sudobot/core/EventListener";
import { Events } from "@sudobot/types/ClientEvents";
import type URLFishService from "../../services/URLFishService";
import { Message } from "discord.js";
import type URLFishService from "../../services/URLFishService";

export default class NormalMessageUpdateEventListener extends EventListener<Events.NormalMessageUpdate> {
public readonly name = Events.NormalMessageUpdate;

async execute(oldMessage: Message, newMessage: Message) {
const urlfishService = this.client.getService<URLFishService>("urlfish");
const links = urlfishService.scanMessage(newMessage);

if (links.length > 0) {
await newMessage.delete();
}
urlfishService.verifyMessage(newMessage);
}
}
9 changes: 8 additions & 1 deletion extensions/urlfish/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import "module-alias/register";

import { Extension } from "@sudobot/core/Extension";
import { Schema } from "./types/config";

export default class URLFish extends Extension {}
export default class URLFish extends Extension {
public override guildConfig() {
return {
urlfish: Schema
};
}
}
147 changes: 143 additions & 4 deletions extensions/urlfish/src/services/URLFishService.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import Service from "@sudobot/core/Service";
import FileSystem from "@sudobot/polyfills/FileSystem";
import { downloadFile } from "@sudobot/utils/download";
import { channelInfo, userInfo } from "@sudobot/utils/embed";
import { safeChannelFetch } from "@sudobot/utils/fetch";
import { logDebug } from "@sudobot/utils/logger";
import { sudoPrefix } from "@sudobot/utils/utils";
import { downloadFile } from "@sudobot/utils/download";
import { Message } from "discord.js";
import { Colors, Message } from "discord.js";
import { ActionToTake, Config, GuildConfigWithExtension } from "src/types/config";

export const name = "urlfish";

export default class URLFishService extends Service {
private readonly domainListURL = "https://raw.githubusercontent.com/mitchellkrogza/Phishing.Database/master/phishing-domains-ACTIVE.txt";
private readonly domainListURL =
"https://raw.githubusercontent.com/mitchellkrogza/Phishing.Database/master/phishing-domains-ACTIVE.txt";
private _list: string[] = [];

async boot() {
Expand Down Expand Up @@ -43,7 +47,7 @@ export default class URLFishService extends Service {
const phishingURLs: string[] = [];

for (const url of urls) {
const domain = url.startsWith("http") ? url.replace(/https?:\/?\/?/i, '') : url;
const domain = url.startsWith("http") ? url.replace(/https?:\/?\/?/i, "") : url;

if (this.list.includes(domain)) {
phishingURLs.push(url);
Expand All @@ -52,4 +56,139 @@ export default class URLFishService extends Service {

return phishingURLs;
}

async verifyMessage(message: Message) {
const config = this.client.configManager.get<GuildConfigWithExtension>(message.guildId!)?.urlfish;

if (
!config?.enabled ||
(config.channels && "enabled_in" in config.channels && !config.channels.enabled_in.includes(message.channelId)) ||
(config.channels && "disabled_in" in config.channels && config.channels.disabled_in.includes(message.channelId)) ||
(await this.client.permissionManager.isImmuneToAutoMod(message.member!))
) {
return;
}

const links = this.scanMessage(message);

if (links.length > 0) {
await this.takeAction(message, config);
await this.logMessage(message, config, links, config.action);
}
}

async takeAction(message: Message<boolean>, config: NonNullable<Config>) {
switch (config.action) {
case "delete":
if (message.deletable) {
await message.delete();
}

break;
case "warn":
await this.client.infractionManager.createMemberWarn(message.member!, {
guild: message.guild!,
reason:
config.infraction_reason ??
"We have detected phishing URLs in your message. Please refrain from posting these links.",
moderator: this.client.user!,
notifyUser: true,
sendLog: true
});

break;
case "mute":
await this.client.infractionManager.createMemberMute(message.member!, {
guild: message.guild!,
reason:
config.infraction_reason ??
"We have detected phishing URLs in your message. Please refrain from posting these links.",
moderator: this.client.user!,
notifyUser: true,
sendLog: true,
duration: config.mute_duration ?? 3_600_000, // 1 hour,
autoRemoveQueue: true
});

break;
case "kick":
await this.client.infractionManager.createMemberKick(message.member!, {
guild: message.guild!,
reason: config.infraction_reason ?? "We have detected phishing URLs in your message.",
moderator: this.client.user!,
notifyUser: true,
sendLog: true
});

break;
case "ban":
await this.client.infractionManager.createUserBan(message.author, {
guild: message.guild!,
reason: config.infraction_reason ?? "We have detected phishing URLs in your message.",
moderator: this.client.user!,
notifyUser: true,
sendLog: true,
deleteMessageSeconds: 604_800, // 7 days,
autoRemoveQueue: true
});

break;
}
}

async logMessage(message: Message<boolean>, config: Config, links: string[], action: ActionToTake) {
if (!config?.log_channel || !message.guild) {
return;
}

const logChannel = await safeChannelFetch(message.guild, config?.log_channel);

if (!logChannel?.isTextBased()) {
return;
}

const joinedLinks = links.join("\n");

await logChannel.send({
embeds: [
{
title: `Phishing URLs detected in ${message.url}`,
fields: [
{
name: "URLs",
value: joinedLinks.substring(0, 1020) + (joinedLinks.length > 1020 ? "..." : "")
},
{
name: "Channel",
value: channelInfo(message.channel),
inline: true
},
{
name: "User",
value: userInfo(message.author),
inline: true
},
{
name: "Action",
value:
action === "ban"
? "Banned"
: action[0].toUpperCase() + action.substring(1) + (action.endsWith("e") ? "d" : "ed"),
inline: true
}
],
description: message.content || "*No content*",
color: Colors.Red,
timestamp: message.createdAt.toISOString(),
author: {
name: message.author.username,
icon_url: message.author.displayAvatarURL()
},
footer: {
text: `Detected by URLFish`
}
}
]
});
}
}
40 changes: 40 additions & 0 deletions extensions/urlfish/src/types/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GuildConfig } from "@sudobot/types/GuildConfigSchema";
import { zSnowflake } from "@sudobot/types/SnowflakeSchema";
import { z } from "zod";

export const Schema = z
.object({
enabled: z.boolean().default(false).describe("Whether or not URLFish is enabled"),
channels: z
.object({
enabled_in: z.array(zSnowflake).describe("Channels to enable URLFish in")
})
.or(
z.object({
disabled_in: z.array(zSnowflake).describe("Channels to disable URLFish in")
})
)
.optional()
.describe("Channels to enable or disable URLFish in"),
log_channel: zSnowflake.optional().describe("Channel to log URLFish events to"),
action: z
.enum(["delete", "warn", "mute", "kick", "ban"])
.default("delete")
.describe("Action to take when a phishing URL is detected"),
mute_duration: z
.number()
.positive()
.min(1000)
.optional()
.describe("Duration to mute a user for when a phishing URL is detected"),
infraction_reason: z.string().optional().describe("Reason to use when creating an infraction")
})
.describe("URLFish Configuration")
.optional();

export type Config = z.infer<typeof Schema>;
export type GuildConfigForExtension = {
urlfish: Config;
};
export type GuildConfigWithExtension = GuildConfig & GuildConfigForExtension;
export type ActionToTake = NonNullable<Config>["action"];

0 comments on commit 2f9d850

Please sign in to comment.