Skip to content

Commit

Permalink
feat: add opt-in bot logging (#2579)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranet authored Feb 11, 2024
1 parent 7f9dd23 commit 2de2179
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 29 deletions.
3 changes: 2 additions & 1 deletion scripts/SetMigrations.sql
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ VALUES
(1662291099795, 'V73RemoveSuggestions1662291099795'),
(1706262119737, 'V74RemoveUserTable1706262119737'),
(1707505580449, 'V75SplitModerationType1707505580449'),
(1707558132765, 'V76AddOptOutUnknownMessageLogging1707558132765');
(1707558132765, 'V76AddOptOutUnknownMessageLogging1707558132765'),
(1707605222927, 'V77AddEventsIncludeBots1707605222927');

COMMIT;
1 change: 1 addition & 0 deletions src/languages/en-US/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"eventsBanRemove": "This event posts anonymous moderation logs when a user gets unbanned. You must set up `channels.moderation-logs`.",
"eventsUnknownMessages": "Whether or not I should post updates on unknown command messages.",
"eventsTwemojiReactions": "Whether or not twemoji reactions are posted in the reaction logs channel.",
"eventsIncludeBots": "Whether or not I should ignore bots in message logs.",
"language": "The language I will use for your server. It may not be available in the language you want.",
"messagesIgnoreChannels": "The channels configured to not increase the point counter for users.",
"messagesModerationAutoDelete": "Whether or not moderation commands should be auto-deleted or not.",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/database/entities/GuildEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export class GuildEntity extends BaseEntity implements IBaseEntity {
@Column('boolean', { name: 'events.twemoji-reactions', default: false })
public eventsTwemojiReactions = false;

@ConfigurableKey({ description: LanguageKeys.Settings.EventsIncludeBots })
@Column('boolean', { name: 'events.include-bots', default: false })
public eventsIncludeBots = false;

@ConfigurableKey({ description: LanguageKeys.Settings.MessagesIgnoreChannels, type: 'textchannel' })
@Column('varchar', { name: 'messages.ignore-channels', length: 19, array: true, default: () => 'ARRAY[]::VARCHAR[]' })
public messagesIgnoreChannels: string[] = [];
Expand Down
3 changes: 2 additions & 1 deletion src/lib/database/keys/settings/Events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const BanAdd = 'eventsBanAdd';
export const BanRemove = 'eventsBanRemove';
export const UnknownMessages = 'eventsUnknownMessages';
export const Twemoji = 'eventsTwemojiReactions';
export const IncludeTwemoji = 'eventsTwemojiReactions';
export const IncludeBots = 'eventsIncludeBots';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm';

export class V77AddEventsIncludeBots1707605222927 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn('guilds', new TableColumn({ name: 'events.include-bots', type: 'boolean', default: false }));
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('guilds', 'events.include-bots');
}
}
1 change: 1 addition & 0 deletions src/lib/i18n/languageKeys/keys/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const EventsBanRemove = T('settings:eventsBanRemove');
export const EventsUnknownMessages = T('settings:eventsUnknownMessages');
export const EventsTwemojiReactions = T('settings:eventsTwemojiReactions');
export const MessagesIgnoreChannels = T('settings:messagesIgnoreChannels');
export const EventsIncludeBots = T('settings:eventsIncludeBots');
export const MessagesModerationAutoDelete = T('settings:messagesModerationAutoDelete');
export const MessagesModerationDM = T('settings:messagesModerationDm');
export const MessagesModerationMessageDisplay = T('settings:messagesModerationMessageDisplay');
Expand Down
52 changes: 40 additions & 12 deletions src/listeners/messages/rawMessageDeleteNotify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GuildSettings, readSettings } from '#lib/database';
import { LanguageKeys } from '#lib/i18n/languageKeys';
import type { GuildMessage } from '#lib/types';
import { Colors } from '#utils/constants';
import { makeRow } from '#utils/deprecate';
import { getLogger } from '#utils/functions';
Expand All @@ -10,7 +11,14 @@ import { isNsfwChannel } from '@sapphire/discord.js-utilities';
import { Listener } from '@sapphire/framework';
import type { TFunction } from '@sapphire/plugin-i18next';
import { cutText, isNullish, isNullishOrEmpty } from '@sapphire/utilities';
import { ButtonStyle, GatewayDispatchEvents, messageLink, type GatewayMessageDeleteDispatchData, type GuildTextBasedChannel } from 'discord.js';
import {
ButtonStyle,
GatewayDispatchEvents,
messageLink,
type GatewayMessageDeleteDispatchData,
type GuildTextBasedChannel,
type Snowflake
} from 'discord.js';

@ApplyOptions<Listener.Options>({ event: GatewayDispatchEvents.MessageDelete, emitter: 'ws' })
export class UserListener extends Listener {
Expand All @@ -23,26 +31,23 @@ export class UserListener extends Listener {
const channel = guild.channels.cache.get(data.channel_id) as GuildTextBasedChannel;
if (!channel) return;

const message = channel.messages.cache.get(data.id);
const message = channel.messages.cache.get(data.id) as GuildMessage | undefined;

const key = GuildSettings.Channels.Logs[isNsfwChannel(channel) ? 'MessageDeleteNsfw' : 'MessageDelete'];
const [allowUnknownMessages, ignoredChannels, logChannelId, ignoredDeletes, ignoredAll, t] = await readSettings(guild, (settings) => [
const [t, targetChannelId, ...settings] = await readSettings(guild, (settings) => [
settings.getLanguage(),
settings[key],
settings[GuildSettings.Events.UnknownMessages],
settings[GuildSettings.Events.IncludeBots],
settings[GuildSettings.Messages.IgnoreChannels],
settings[key],
settings[GuildSettings.Channels.Ignore.MessageDelete],
settings[GuildSettings.Channels.Ignore.All],
settings.getLanguage()
settings[GuildSettings.Channels.Ignore.All]
]);

await getLogger(guild).send({
key,
channelId: logChannelId,
condition: () =>
!(!allowUnknownMessages && isNullish(message)) ||
!ignoredChannels.includes(channel.id) ||
!ignoredDeletes.some((id) => id === channel.id || channel.parentId === id) ||
!ignoredAll.some((id) => id === channel.id || channel.parentId === id),
channelId: targetChannelId,
condition: () => this.onCondition(message, channel, ...settings),
makeMessage: () => {
const embed = new EmbedBuilder().setColor(Colors.Red).setTimestamp();

Expand All @@ -65,6 +70,29 @@ export class UserListener extends Listener {
});
}

private onCondition(
message: GuildMessage | undefined,
channel: GuildTextBasedChannel,
allowUnknownMessages: boolean,
includeBots: boolean,
ignoredChannels: readonly Snowflake[],
ignoredDeletes: readonly Snowflake[],
ignoredAll: readonly Snowflake[]
) {
// If includeBots is false, and the message author is a bot, return false
if (!includeBots && message?.author.bot) return false;
// If allowUnknownMessages is false, and the message is nullish, return false
if (!allowUnknownMessages && isNullish(message)) return false;
// If the channel is in the ignoredChannels array, return false
if (ignoredChannels.includes(channel.id)) return false;
// If the channel or its parent is in the ignoredDeletes array, return false
if (ignoredDeletes.some((id) => id === channel.id || channel.parentId === id)) return false;
// If the channel or its parent is in the ignoredAll array, return false
if (ignoredAll.some((id) => id === channel.id || channel.parentId === id)) return false;
// All checks passed, return true
return true;
}

private getJumpButton(t: TFunction, channelId: string, messageId: string) {
return makeRow(
new ButtonBuilder()
Expand Down
51 changes: 37 additions & 14 deletions src/listeners/messages/rawMessageUpdateNotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
bold,
messageLink,
strikethrough,
type APIUser,
type GatewayMessageUpdateDispatchData,
type GuildTextBasedChannel
type GuildTextBasedChannel,
type Snowflake
} from 'discord.js';

@ApplyOptions<Listener.Options>({ event: GatewayDispatchEvents.MessageUpdate, emitter: 'ws' })
Expand All @@ -31,36 +33,33 @@ export class UserListener extends Listener {
const channel = guild.channels.cache.get(data.channel_id) as GuildTextBasedChannel;
if (!channel) return;

const old = channel.messages.cache.get(data.id) as GuildMessage | undefined;
const oldContent = old?.content;
const cachedMessage = channel.messages.cache.get(data.id) as GuildMessage | undefined;
const oldContent = cachedMessage?.content;
const currentContent = data.content ?? '';
if ((old && old.content === currentContent) || data.webhook_id || !data.author) return;
if ((cachedMessage && cachedMessage.content === currentContent) || data.webhook_id || !data.author) return;

const key = GuildSettings.Channels.Logs[isNsfwChannel(channel) ? 'MessageUpdateNsfw' : 'MessageUpdate'];
const [allowUnknownMessages, ignoredChannels, logChannelId, ignoredEdits, ignoredAll, t] = await readSettings(guild, (settings) => [
const [t, logChannelId, ...settings] = await readSettings(guild, (settings) => [
settings.getLanguage(),
settings[key],
settings[GuildSettings.Events.UnknownMessages],
settings[GuildSettings.Events.IncludeBots],
settings[GuildSettings.Messages.IgnoreChannels],
settings[key],
settings[GuildSettings.Channels.Ignore.MessageEdit],
settings[GuildSettings.Channels.Ignore.All],
settings.getLanguage()
settings[GuildSettings.Channels.Ignore.All]
]);

await getLogger(guild).send({
key,
channelId: logChannelId,
condition: () =>
!(!allowUnknownMessages && isNullish(old)) ||
!ignoredChannels.includes(channel.id) ||
!ignoredEdits.some((id) => id === channel.id || channel.parentId === id) ||
!ignoredAll.some((id) => id === channel.id || channel.parentId === id),
condition: () => this.onCondition(cachedMessage, channel, data.author!, ...settings),
makeMessage: () => {
const embed = new SkyraEmbed()
.setColor(Colors.Amber)
.setAuthor(getFullEmbedAuthor(data.author!, messageLink(data.channel_id, data.id)))
.setTimestamp();

if (isNullish(old)) {
if (isNullish(cachedMessage)) {
embed //
.splitFields(currentContent)
.setFooter({ text: t(LanguageKeys.Events.Messages.MessageUpdateUnknown, { channel: `#${channel.name}` }) });
Expand All @@ -74,6 +73,30 @@ export class UserListener extends Listener {
});
}

private onCondition(
cachedMessage: GuildMessage | undefined,
channel: GuildTextBasedChannel,
author: APIUser,
allowUnknownMessages: boolean,
includeBots: boolean,
ignoredChannels: readonly Snowflake[],
ignoredEdits: readonly Snowflake[],
ignoredAll: readonly Snowflake[]
) {
// If includeBots is false, and the message author is a bot, return false
if (!includeBots && author.bot) return false;
// If allowUnknownMessages is false, and the message is nullish, return false
if (!allowUnknownMessages && isNullish(cachedMessage)) return false;
// If the channel is in the ignoredChannels array, return false
if (ignoredChannels.includes(channel.id)) return false;
// If the channel or its parent is in the ignoredEdits array, return false
if (ignoredEdits.some((id) => id === channel.id || channel.parentId === id)) return false;
// If the channel or its parent is in the ignoredAll array, return false
if (ignoredAll.some((id) => id === channel.id || channel.parentId === id)) return false;
// All checks passed, return true
return true;
}

private getMessageDifference(old: string, current: string) {
const oldEmpty = isNullishOrEmpty(old);
const currentEmpty = isNullishOrEmpty(current);
Expand Down
2 changes: 1 addition & 1 deletion src/listeners/reactions/rawReactionAddNotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class UserListener extends Listener {
(settings) => [
settings[GuildSettings.Selfmod.Reactions.Allowed],
settings[key],
settings[GuildSettings.Events.Twemoji],
settings[GuildSettings.Events.IncludeTwemoji],
settings[GuildSettings.Messages.IgnoreChannels],
settings[GuildSettings.Channels.Ignore.ReactionAdd],
settings[GuildSettings.Channels.Ignore.All],
Expand Down

0 comments on commit 2de2179

Please sign in to comment.