Automod mayhaps?
This commit is contained in:
parent
358aaa345d
commit
e4714b426b
Binary file not shown.
Binary file not shown.
@ -173,16 +173,20 @@ public class UserController(ILogger<UserController> logger, Configuration config
|
|||||||
total_messages = messages.Count(), total_channels = channels.Count,
|
total_messages = messages.Count(), total_channels = channels.Count,
|
||||||
messages_per_channel = channels.ToDictionary(c => c.ChannelId, c => messages.Count(m => m.ChannelId == c.ChannelId))
|
messages_per_channel = channels.ToDictionary(c => c.ChannelId, c => messages.Count(m => m.ChannelId == c.ChannelId))
|
||||||
});
|
});
|
||||||
var results = channels
|
if (messages.Any()) {
|
||||||
.Select(ctx => DeleteMessagesForChannel(ctx.GuildId, ctx.ChannelId!, id, mqChannel, messageDeleteChunkSize))
|
var results = channels
|
||||||
.ToList();
|
.Select(ctx => DeleteMessagesForChannel(ctx.GuildId, ctx.ChannelId!, id, mqChannel, messageDeleteChunkSize))
|
||||||
var a = AggregateAsyncEnumerablesWithoutOrder(results);
|
.ToList();
|
||||||
await foreach (var result in a) {
|
var a = AggregateAsyncEnumerablesWithoutOrder(results);
|
||||||
yield return result;
|
await foreach (var result in a) {
|
||||||
}
|
yield return result;
|
||||||
|
}
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
if (messages.Count() >= 100) {
|
||||||
await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages");
|
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
||||||
|
await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async IAsyncEnumerable<AsyncActionResult> DeleteMessagesForChannel(
|
private async IAsyncEnumerable<AsyncActionResult> DeleteMessagesForChannel(
|
||||||
@ -203,10 +207,10 @@ public class UserController(ILogger<UserController> logger, Configuration config
|
|||||||
var messageIds = _db.Database.SqlQuery<string>($"""
|
var messageIds = _db.Database.SqlQuery<string>($"""
|
||||||
DELETE FROM messages
|
DELETE FROM messages
|
||||||
WHERE id IN (
|
WHERE id IN (
|
||||||
SELECT id FROM messages
|
SELECT id FROM messages
|
||||||
WHERE author_id = {authorId}
|
WHERE author_id = {authorId}
|
||||||
AND channel_id = {channelId}
|
AND channel_id = {channelId}
|
||||||
AND guild_id = {guildId}
|
AND guild_id = {guildId}
|
||||||
LIMIT {messageDeleteChunkSize}
|
LIMIT {messageDeleteChunkSize}
|
||||||
) RETURNING id;
|
) RETURNING id;
|
||||||
""").ToList();
|
""").ToList();
|
||||||
@ -410,7 +414,7 @@ public class UserController(ILogger<UserController> logger, Configuration config
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// "op": 0,
|
// "op": 0,
|
||||||
// "t": "GUILD_ROLE_UPDATE",
|
// "t": "GUILD_ROLE_UPDATE",
|
||||||
@ -432,7 +436,7 @@ public class UserController(ILogger<UserController> logger, Configuration config
|
|||||||
// },
|
// },
|
||||||
// "s": 38
|
// "s": 38
|
||||||
// }
|
// }
|
||||||
|
|
||||||
[HttpGet("test")]
|
[HttpGet("test")]
|
||||||
public async IAsyncEnumerable<string> Test() {
|
public async IAsyncEnumerable<string> Test() {
|
||||||
var factory = new ConnectionFactory {
|
var factory = new ConnectionFactory {
|
||||||
@ -446,7 +450,7 @@ public class UserController(ILogger<UserController> logger, Configuration config
|
|||||||
var roleId = "1391303296148639051"; //Spacebar Maintainer
|
var roleId = "1391303296148639051"; //Spacebar Maintainer
|
||||||
// int color = 16711680; //Administrator
|
// int color = 16711680; //Administrator
|
||||||
int color = 99839; //Spacebar Maintainer
|
int color = 99839; //Spacebar Maintainer
|
||||||
|
|
||||||
await mqChannel.ExchangeDeclareAsync(exchange: guildId, type: ExchangeType.Fanout, durable: false);
|
await mqChannel.ExchangeDeclareAsync(exchange: guildId, type: ExchangeType.Fanout, durable: false);
|
||||||
|
|
||||||
var props = new BasicProperties() { Type = "GUILD_ROLE_UPDATE" };
|
var props = new BasicProperties() { Type = "GUILD_ROLE_UPDATE" };
|
||||||
@ -492,4 +496,4 @@ public class UserController(ILogger<UserController> logger, Configuration config
|
|||||||
sw.Restart();
|
sw.Restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,30 +19,41 @@
|
|||||||
import { handleMessage, postHandleMessage, route } from "@spacebar/api";
|
import { handleMessage, postHandleMessage, route } from "@spacebar/api";
|
||||||
import {
|
import {
|
||||||
Attachment,
|
Attachment,
|
||||||
|
AutomodRule,
|
||||||
|
AutomodTriggerTypes,
|
||||||
Channel,
|
Channel,
|
||||||
Config,
|
Config,
|
||||||
DmChannelDTO,
|
DmChannelDTO,
|
||||||
|
emitEvent,
|
||||||
FieldErrors,
|
FieldErrors,
|
||||||
|
getPermission,
|
||||||
|
getUrlSignature,
|
||||||
Member,
|
Member,
|
||||||
Message,
|
Message,
|
||||||
MessageCreateEvent,
|
MessageCreateEvent,
|
||||||
|
NewUrlSignatureData,
|
||||||
|
NewUrlUserSignatureData,
|
||||||
ReadState,
|
ReadState,
|
||||||
Rights,
|
Rights,
|
||||||
Snowflake,
|
Snowflake,
|
||||||
User,
|
|
||||||
emitEvent,
|
|
||||||
getPermission,
|
|
||||||
getUrlSignature,
|
|
||||||
uploadFile,
|
uploadFile,
|
||||||
NewUrlSignatureData,
|
User,
|
||||||
NewUrlUserSignatureData,
|
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import { HTTPError } from "lambert-server";
|
import { HTTPError } from "lambert-server";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import { FindManyOptions, FindOperator, LessThan, MoreThan, MoreThanOrEqual } from "typeorm";
|
import { FindManyOptions, FindOperator, LessThan, MoreThan, MoreThanOrEqual } from "typeorm";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import { isTextChannel, MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, Reaction } from "@spacebar/schemas";
|
import {
|
||||||
|
AutomodCustomWordsRule,
|
||||||
|
AutomodRuleActionType,
|
||||||
|
AutomodRuleEventType,
|
||||||
|
isTextChannel,
|
||||||
|
MessageCreateAttachment,
|
||||||
|
MessageCreateCloudAttachment,
|
||||||
|
MessageCreateSchema,
|
||||||
|
Reaction,
|
||||||
|
} from "@spacebar/schemas";
|
||||||
|
|
||||||
const router: Router = Router({ mergeParams: true });
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
@ -404,6 +415,72 @@ router.post(
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
message.member.roles = message.member.roles.filter((x) => x.id != x.guild_id).map((x) => x.id);
|
message.member.roles = message.member.roles.filter((x) => x.id != x.guild_id).map((x) => x.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (message.content)
|
||||||
|
for (const rule of await AutomodRule.find({ where: { guild_id: message.guild_id, enabled: true, event_type: AutomodRuleEventType.MESSAGE_SEND } })) {
|
||||||
|
if (rule.exempt_channels.includes(channel_id)) continue;
|
||||||
|
if (message.member.roles.some((x) => rule.exempt_roles.includes(x.id))) continue;
|
||||||
|
if (rule.trigger_type == AutomodTriggerTypes.CUSTOM_WORDS) {
|
||||||
|
const triggerMeta = rule.trigger_metadata as AutomodCustomWordsRule;
|
||||||
|
const regexes = triggerMeta.regex_patterns
|
||||||
|
.map((x) => new RegExp(x, "i"))
|
||||||
|
.concat(
|
||||||
|
triggerMeta.keyword_filter
|
||||||
|
.map((k) =>
|
||||||
|
k
|
||||||
|
// Convert simple wildcard patterns to regex
|
||||||
|
.replace(".", "\\.")
|
||||||
|
.replace("?", ".")
|
||||||
|
.replace("*", ".*"),
|
||||||
|
)
|
||||||
|
.map((k) => new RegExp(k, "i")),
|
||||||
|
);
|
||||||
|
const allowedRegexes = triggerMeta.allow_list
|
||||||
|
.map((k) =>
|
||||||
|
k
|
||||||
|
// Convert simple wildcard patterns to regex
|
||||||
|
.replace(".", "\\.")
|
||||||
|
.replace("?", ".")
|
||||||
|
.replace("*", ".*"),
|
||||||
|
)
|
||||||
|
.map((k) => new RegExp(k, "i"));
|
||||||
|
|
||||||
|
const matches = regexes
|
||||||
|
.map((r) => message.content!.match(r))
|
||||||
|
.filter((x) => x !== null && x.length > 0)
|
||||||
|
.filter((x) => !allowedRegexes.some((ar) => ar.test(x![0])));
|
||||||
|
if (matches.length > 0) {
|
||||||
|
console.log("Automod triggered by message:", message.id, "matches:", matches);
|
||||||
|
if (rule.actions.some((x) => x.type == AutomodRuleActionType.SEND_ALERT_MESSAGE && x.metadata.channel_id)) {
|
||||||
|
const alertActions = rule.actions.filter((x) => x.type == AutomodRuleActionType.SEND_ALERT_MESSAGE);
|
||||||
|
for (const action of alertActions) {
|
||||||
|
const alertChannel = await Channel.findOne({ where: { id: action.metadata.channel_id } });
|
||||||
|
if (!alertChannel) continue;
|
||||||
|
const msg = Message.create({
|
||||||
|
channel_id: alertChannel.id,
|
||||||
|
content: `Automod Alert: Message ${message.id} by <@${message.author_id}> in <#${channel.id}> triggered automod rule "${rule.name}".\nMatched terms: ${matches
|
||||||
|
.map((x) => `\`${x![0]}\``)
|
||||||
|
.join(", ")}`,
|
||||||
|
author: message.author,
|
||||||
|
guild_id: message.channel.guild_id,
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
Message.insert(message),
|
||||||
|
emitEvent({
|
||||||
|
event: "MESSAGE_CREATE",
|
||||||
|
channel_id: msg.channel_id,
|
||||||
|
data: msg.toJSON(),
|
||||||
|
} as MessageCreateEvent),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Automod] failed to process message:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_state = await ReadState.findOne({
|
let read_state = await ReadState.findOne({
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export class CDNServer extends Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cleanupSignaturesInDb() {
|
async cleanupSignaturesInDb() {
|
||||||
this.log("verbose", "[Server] Cleaning up signatures in database");
|
this.log("verbose", "[CDN] Cleaning up signatures in database");
|
||||||
const attachmentsToFix = await Attachment.find({
|
const attachmentsToFix = await Attachment.find({
|
||||||
where: { url: Like("%?ex=%") },
|
where: { url: Like("%?ex=%") },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Snowflake } from "../../Identifiers";
|
||||||
|
|
||||||
export type AutomodRuleTriggerMetadata = AutomodMentionSpamRule | AutomodSuspectedSpamRule | AutomodCommonlyFlaggedWordsRule | AutomodCustomWordsRule;
|
export type AutomodRuleTriggerMetadata = AutomodMentionSpamRule | AutomodSuspectedSpamRule | AutomodCommonlyFlaggedWordsRule | AutomodCustomWordsRule;
|
||||||
|
|
||||||
export class AutomodMentionSpamRule {
|
export class AutomodMentionSpamRule {
|
||||||
@ -26,12 +28,64 @@ export class AutomodMentionSpamRule {
|
|||||||
export class AutomodSuspectedSpamRule {}
|
export class AutomodSuspectedSpamRule {}
|
||||||
|
|
||||||
export class AutomodCommonlyFlaggedWordsRule {
|
export class AutomodCommonlyFlaggedWordsRule {
|
||||||
allow_list: [string];
|
allow_list: string[];
|
||||||
presets: [number];
|
presets: AutomodKeywordPresetType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AutomodCustomWordsRule {
|
export class AutomodCustomWordsRule {
|
||||||
allow_list: [string];
|
allow_list: string[];
|
||||||
keyword_filter: [string];
|
keyword_filter: string[];
|
||||||
regex_patterns: [string];
|
regex_patterns: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AutomodRuleEventType {
|
||||||
|
MESSAGE_SEND = 1,
|
||||||
|
GUILD_MEMBER_EVENT = 2,
|
||||||
|
}
|
||||||
|
export enum AutomodRuleTriggerType {
|
||||||
|
KEYWORD = 1,
|
||||||
|
HARMFUL_LINK = 2,
|
||||||
|
SPAM = 3,
|
||||||
|
KEYWORD_PRESET = 4,
|
||||||
|
MENTION_SPAM = 5,
|
||||||
|
USER_PROFILE = 6,
|
||||||
|
GUILD_POLICY = 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AutomodKeywordPresetType {
|
||||||
|
PROFANITY = 1,
|
||||||
|
SEXUAL_CONTENT = 2,
|
||||||
|
SLURS = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AutomodRuleActionType {
|
||||||
|
BLOCK_MESSAGE = 1,
|
||||||
|
SEND_ALERT_MESSAGE = 2,
|
||||||
|
TIMEOUT_USER = 3,
|
||||||
|
QUARANTINE_USER = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AutomodAction = {
|
||||||
|
type: AutomodRuleActionType.BLOCK_MESSAGE;
|
||||||
|
metadata: {
|
||||||
|
custom_message?: string;
|
||||||
|
}
|
||||||
|
} | {
|
||||||
|
type: AutomodRuleActionType.SEND_ALERT_MESSAGE;
|
||||||
|
metadata: {
|
||||||
|
channel_id: Snowflake;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: AutomodRuleActionType.TIMEOUT_USER;
|
||||||
|
metadata: {
|
||||||
|
duration_seconds: number;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: AutomodRuleActionType.QUARANTINE_USER;
|
||||||
|
metadata: {
|
||||||
|
duration_seconds: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export interface AutomodRuleActionMetadata {
|
||||||
|
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
import { BaseClass } from "./BaseClass";
|
import { BaseClass } from "./BaseClass";
|
||||||
import { Entity, JoinColumn, ManyToOne, Column } from "typeorm";
|
import { Entity, JoinColumn, ManyToOne, Column } from "typeorm";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
import { AutomodRuleTriggerMetadata } from "@spacebar/schemas";
|
import { AutomodAction, AutomodRuleActionType, AutomodRuleEventType, AutomodRuleTriggerMetadata, AutomodRuleTriggerType } from "@spacebar/schemas";
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
name: "automod_rules",
|
name: "automod_rules",
|
||||||
@ -33,13 +33,13 @@ export class AutomodRule extends BaseClass {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
event_type: number; // No idea...
|
event_type: AutomodRuleEventType;
|
||||||
|
|
||||||
@Column({ type: "simple-array" })
|
@Column({ type: "simple-array" })
|
||||||
exempt_channels: [string];
|
exempt_channels: string[];
|
||||||
|
|
||||||
@Column({ type: "simple-array" })
|
@Column({ type: "simple-array" })
|
||||||
exempt_roles: [string];
|
exempt_roles: string[];
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
@ -51,7 +51,7 @@ export class AutomodRule extends BaseClass {
|
|||||||
position: number;
|
position: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
trigger_type: number;
|
trigger_type: AutomodRuleTriggerType;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: "simple-json",
|
type: "simple-json",
|
||||||
@ -63,5 +63,5 @@ export class AutomodRule extends BaseClass {
|
|||||||
@Column({
|
@Column({
|
||||||
type: "simple-json",
|
type: "simple-json",
|
||||||
})
|
})
|
||||||
actions: { type: number; metadata: unknown }[];
|
actions: AutomodAction[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,8 @@ let pairs: ConfigEntity[];
|
|||||||
// Config keys are separated with _
|
// Config keys are separated with _
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
public static async init() {
|
public static async init(force: boolean = false) {
|
||||||
if (config) return config;
|
if (config && !force) return config;
|
||||||
console.log("[Config] Loading configuration...");
|
console.log("[Config] Loading configuration...");
|
||||||
if (!process.env.CONFIG_PATH) {
|
if (!process.env.CONFIG_PATH) {
|
||||||
pairs = await validateConfig();
|
pairs = await validateConfig();
|
||||||
@ -59,7 +59,7 @@ export class Config {
|
|||||||
public static get() {
|
public static get() {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
// If we haven't initialised the config yet, return default config.
|
// If we haven't initialised the config yet, return default config.
|
||||||
// Typeorm instantiates each entity once when initising database,
|
// Typeorm instantiates each entity once when initialising database,
|
||||||
// which means when we use config values as default values in entity classes,
|
// which means when we use config values as default values in entity classes,
|
||||||
// the config isn't initialised yet and would throw an error about the config being undefined.
|
// the config isn't initialised yet and would throw an error about the config being undefined.
|
||||||
|
|
||||||
@ -100,7 +100,8 @@ async function applyConfig(val: ConfigValue) {
|
|||||||
else console.log("[WARNING] JSON config file in use, and writing is disabled! Programmatic config changes will not be persisted, and your config will not get updated!");
|
else console.log("[WARNING] JSON config file in use, and writing is disabled! Programmatic config changes will not be persisted, and your config will not get updated!");
|
||||||
else {
|
else {
|
||||||
const pairs = generatePairs(val);
|
const pairs = generatePairs(val);
|
||||||
await Promise.all(pairs.map((pair) => pair.save()));
|
// keys are sorted to try to influence database order...
|
||||||
|
await Promise.all(pairs.sort((x, y) => x.key > y.key ? 1 : -1).map((pair) => pair.save()));
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
@ -134,9 +135,13 @@ function pairsToConfig(pairs: ConfigEntity[]) {
|
|||||||
|
|
||||||
const validateConfig = async () => {
|
const validateConfig = async () => {
|
||||||
let hasErrored = false;
|
let hasErrored = false;
|
||||||
|
const totalStartTime = new Date();
|
||||||
const config = await ConfigEntity.find({ select: { key: true } });
|
const config = await ConfigEntity.find({ select: { key: true } });
|
||||||
|
|
||||||
for (const row in config) {
|
for (const row in config) {
|
||||||
|
// extension methods...
|
||||||
|
if(typeof config[row] === "function") continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const found = await ConfigEntity.findOne({
|
const found = await ConfigEntity.findOne({
|
||||||
where: { key: config[row].key },
|
where: { key: config[row].key },
|
||||||
@ -153,9 +158,11 @@ const validateConfig = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[Config] Total config load time:", new Date().getTime() - totalStartTime.getTime(), "ms");
|
||||||
|
|
||||||
if (hasErrored) {
|
if (hasErrored) {
|
||||||
console.error(
|
console.error(
|
||||||
"Your config has invalid values. Fix them first https://docs.spacebar.chat/setup/server/configuration",
|
"[Config] Your config has invalid values. Fix them first https://docs.spacebar.chat/setup/server/configuration",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user