Skip to content
Merged
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
8 changes: 8 additions & 0 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export default defineConfig({
'prefer-template': 'warn',
'typescript/no-explicit-any': 'error',
'prefer-const': 'error',
'id-length': [
'error',
{
min: 2,
checkGeneric: false,
exceptions: ['_', 'i', 'j', 'x', 'y', 'z'],
},
],
},
ignorePatterns: ['node_modules', 'dist', 'build', 'coverage', '.git'],
});
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum OptionKey {
TAG_PREFIX
MAX_TAGS_PER_MESSAGE
DAYS_TO_KEEP_TAGS
DAYS_TO_KEEP_USER_BOT_MESSAGES
}

// Options for the bot, stored in the database.
Expand Down
2 changes: 2 additions & 0 deletions src/common/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { readyEvent } from '@/features/ready/index.js';
import type { DiscordEvent } from './types.js';
import archiveChannels from '@/features/archive-channels/index.js';
import { tagReceivedEvent } from '@/features/tags/tag-received.js';
import { reactionAddEvent } from '@/features/reactions/index.js';

export const events: DiscordEvent[] = [
readyEvent,
Expand All @@ -13,4 +14,5 @@ export const events: DiscordEvent[] = [
interactionCreateEvent,
archiveChannels,
tagReceivedEvent,
reactionAddEvent,
].flat();
1 change: 1 addition & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// oxlint-disable id-length
import '@/loadEnvFile.js';

function optionalEnv(key: string): string | undefined {
Expand Down
38 changes: 38 additions & 0 deletions src/features/reactions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createEvent } from '@/common/events/create-event.js';
import {
Events,
MessageReaction,
PartialMessageReaction,
PartialUser,
User,
} from 'discord.js';
import { removeUserBotMessage } from './remove-user-bot-message.js';

export type ReactionAddEvent = {
reaction: MessageReaction | PartialMessageReaction;
user: User | PartialUser;
};

const handlers = [removeUserBotMessage];

export const reactionAddEvent = createEvent(
{
name: Events.MessageReactionAdd,
},
async (reaction, user, details) => {
if (user.bot) {
return;
}
try {
if (reaction.partial) {
await reaction.fetch();
}
} catch {
return;
}

for (const handler of handlers) {
await handler(reaction, user, details);
}
}
);
45 changes: 45 additions & 0 deletions src/features/reactions/remove-user-bot-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ClientEvents, Events, GuildMember } from 'discord.js';
import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js';

const DELETE_EMOJIS = ['🗑️', '❌'];

export const removeUserBotMessage: (
...args: ClientEvents[Events.MessageReactionAdd]
) => Promise<void> | void = async (reaction, user) => {
if (
reaction.emoji.name === null ||
!DELETE_EMOJIS.includes(reaction.emoji.name)
) {
return;
}

if (reaction.message.author?.id !== user.client.user.id) {
return;
}

const guild = reaction.message.guild;
if (guild === null) {
return;
}

let member: GuildMember;
try {
member =
guild.members.cache.get(user.id) ?? (await guild.members.fetch(user.id));
} catch {
return;
}

try {
const deleted = await UserBotMessagesService.deleteUserBotMessage({
messageId: reaction.message.id,
user: member,
});

if (deleted) {
await reaction.message.delete();
}
} catch {
return;
}
};
Comment thread
hmd-ali marked this conversation as resolved.
3 changes: 3 additions & 0 deletions src/features/ready/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fetchAndCachePublicChannelsMessages } from '@/util/cache.js';
import { syncGuidesToChannel } from '@/util/post-guides.js';
import { leaveIfNotAllowedServer } from '@/util/server-guard.js';
import { syncArchiveCategoryChannels } from '../archive-channels/util.js';
import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js';

export const readyEvent = createEvent(
{
Expand Down Expand Up @@ -71,5 +72,7 @@ export const readyEvent = createEvent(
error
);
}

void UserBotMessagesService.startExpiredMessageCleanup();
}
);
4 changes: 2 additions & 2 deletions src/features/showcase/edit-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ const modalHandler: ModalSubmitInteraction = {
if (prevTagIds.length !== newProjectTags.length) {
return false;
}
const s = new Set(prevTagIds);
return newProjectTags.every((t) => s.has(t));
const previousTagIdsSet = new Set(prevTagIds);
return newProjectTags.every((tag) => previousTagIdsSet.has(tag));
};
if (!tagsEqual()) {
changes.push({
Expand Down
4 changes: 3 additions & 1 deletion src/features/showcase/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ export const resolveTagNames = (
tagIds: readonly string[],
availableTags: GuildForumTag[]
): string[] => {
return tagIds.map((id) => availableTags.find((t) => t.id === id)?.name ?? id);
return tagIds.map(
(id) => availableTags.find((tag) => tag.id === id)?.name ?? id
);
};

export const getShowcaseLogChannel = (guild: Guild | null) => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/tags/create-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const submissionHandler: ModalSubmitInteraction = {
await interaction.reply({
components: [
ErrorMessages.Tags.TagAlreadyExists(
existingTags.map((t) => t.name).join(', ')
existingTags.map((tag) => tag.name).join(', ')
),
],
flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
Expand Down
4 changes: 2 additions & 2 deletions src/features/tags/edit-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const editTagCommandHandler = async (
.setCustomId('aliases')
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setValue(tag.aliases.map((a) => a.name).join(', '))
.setValue(tag.aliases.map((alias) => alias.name).join(', '))
),
new LabelBuilder()
.setLabel('Short Description')
Expand Down Expand Up @@ -142,7 +142,7 @@ const modalHandler: ModalSubmitInteraction = {
await interaction.reply({
components: [
ErrorMessages.Tags.TagAlreadyExists(
existingTags.map((t) => t.name).join(', ')
existingTags.map((tag) => tag.name).join(', ')
),
],
flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
Expand Down
5 changes: 5 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const OptionsDefaults = {
type: 'number',
displayName: 'Days to Keep Tags',
},
[OptionKey.DAYS_TO_KEEP_USER_BOT_MESSAGES]: {
value: '7',
type: 'number',
displayName: 'Days to Keep User Bot Messages',
},
} as const satisfies Record<OptionKey, OptionValue>;

export type OptionTypeOf<K extends OptionKey> =
Expand Down
9 changes: 6 additions & 3 deletions src/services/user-bot-messages/user-bot-messages-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { GuildMember } from 'discord.js';
import { prisma } from '@/db/prisma.js';
import { isStaff } from '@/util/permissions.js';
import { DAY } from '@/constants/time.js';

const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // every hour
import { getBotOption } from '@/options.js';
import { OptionKey } from '@/generated/prisma/enums.js';

export const UserBotMessagesService = {
async deleteUserBotMessage({
Expand Down Expand Up @@ -65,6 +65,9 @@ export const UserBotMessagesService = {
};

void cleanup();
setInterval(cleanup, CLEANUP_INTERVAL_MS);
const daysToKeep = getBotOption(
OptionKey.DAYS_TO_KEEP_USER_BOT_MESSAGES
).value;
setInterval(cleanup, daysToKeep * DAY);
},
};
21 changes: 9 additions & 12 deletions src/util/advent-scheduler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { promises as fs } from 'node:fs';
import test from 'node:test';
import test, { it } from 'node:test';
import { config } from '@/env.js';

const { loadTracker, saveTracker } = await import('./advent-scheduler.js');
Expand All @@ -13,17 +13,14 @@ async function cleanupTestTracker() {
}
}

void test('advent scheduler: tracker file operations', async (t) => {
await t.test(
'should create empty tracker if file does not exist',
async () => {
await cleanupTestTracker();
const tracker = await loadTracker();
assert.deepEqual(tracker, {});
}
);
void test('advent scheduler: tracker file operations', async () => {
await it('should create empty tracker if file does not exist', async () => {
await cleanupTestTracker();
const tracker = await loadTracker();
assert.deepEqual(tracker, {});
});

await t.test('should save and load tracker data correctly', async () => {
await it('should save and load tracker data correctly', async () => {
const testData = {
'2025': [1, 2, 3],
'2026': [1],
Expand All @@ -33,7 +30,7 @@ void test('advent scheduler: tracker file operations', async (t) => {
assert.deepEqual(loaded, testData);
});

await t.test('should track multiple days per year', async () => {
await it('should track multiple days per year', async () => {
const tracker = {
'2025': [1, 5, 10, 15, 20, 25],
};
Expand Down
2 changes: 1 addition & 1 deletion src/util/advent-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async function markDayAsPosted(year: number, day: number): Promise<void> {

if (!tracker[yearKey].includes(day)) {
tracker[yearKey].push(day);
tracker[yearKey].sort((a, b) => a - b);
tracker[yearKey].sort((firstDay, secondDay) => firstDay - secondDay);
await saveTracker(tracker);
}
}
Expand Down
28 changes: 14 additions & 14 deletions src/util/fuzzy-search.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
export const levenshtein = (a: string, b: string) => {
const dp = Array.from({ length: a.length + 1 }, () =>
Array(b.length + 1).fill(0)
export const levenshtein = (originalString: string, targetString: string) => {
const dp = Array.from({ length: originalString.length + 1 }, () =>
Array(targetString.length + 1).fill(0)
);

for (let i = 0; i <= a.length; i++) {
for (let i = 0; i <= originalString.length; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= b.length; j++) {
for (let j = 0; j <= targetString.length; j++) {
dp[0][j] = j;
}

for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
for (let i = 1; i <= originalString.length; i++) {
for (let j = 1; j <= targetString.length; j++) {
const cost = originalString[i - 1] === targetString[j - 1] ? 0 : 1;
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // deletion
dp[i][j - 1] + 1, // insertion
Expand All @@ -21,7 +21,7 @@ export const levenshtein = (a: string, b: string) => {
}
}

return dp[a.length][b.length];
return dp[originalString.length][targetString.length];
};

const bestSubstringDistance = (query: string, text: string): number => {
Expand Down Expand Up @@ -71,7 +71,7 @@ export function fuzzySearch<T>({
query = query.trim().toLowerCase();
const queryLen = query.length;

const scored = items.map((item) => {
const scoringArray = items.map((item) => {
let maxFuzzyScore = 0;
let titleMatchScore = 0;

Expand Down Expand Up @@ -110,11 +110,11 @@ export function fuzzySearch<T>({
});

return (
scored
scoringArray
// Filter by the original fuzzy score threshold (optional, you could use a lower threshold)
.filter((s) => s.score >= threshold)
.sort((a, b) => b.score - a.score)
.filter((result) => result.score >= threshold)
.sort((first, second) => second.score - first.score)
.slice(0, limit)
.map((s) => s.item)
.map((result) => result.item)
);
}
Loading