diff --git a/src/bot.ts b/src/bot.ts index ae052df..8da026e 100755 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,8 +1,5 @@ #!/bin/env -S deno run -A --unstable-kv -import { assertExists } from "https://deno.land/std@0.223.0/assert/assert_exists.ts"; -import { assert } from "https://deno.land/std@0.223.0/assert/assert.ts"; - import { AutocompleteInteraction, ChatInputCommandInteraction, @@ -12,9 +9,10 @@ import { TextChannel, } from "npm:discord.js"; -import { Channel, channel, collect, days_to_ms, log_from, wait } from "./lib/utils.ts"; import { Storage } from "./lib/storage.ts"; -import { Devoir } from "./lib/storage.ts"; +import { _1d, _1min, Channel, channel, collect, days_to_ms, log_from, trimmed } from "./lib/utils.ts"; +import { update_loop } from "./lib/board.ts"; +import { notification_loop } from "./lib/notification.ts"; const log = log_from(import.meta); @@ -64,13 +62,6 @@ async function main() { log("Oki."); } -async function fetch_feed_channel(bot: Client, feed: { channel_id: string }) { - const feed_channel = await bot.channels.fetch(feed.channel_id); - assertExists(feed_channel); - assert(feed_channel instanceof TextChannel); - return feed_channel; -} - function build_api_commands(subjects: Set) { const subjects_as_choices = [...subjects.values()].map((name) => ({ name, value: name })); const devoir_command = new SlashCommandBuilder() @@ -244,115 +235,4 @@ async function handle_autocomplete(interaction: AutocompleteInteraction, storage log("Unknown command", interaction.commandName); } -async function update_loop(bot: Client, storage: Storage, update_display: Channel) { - log("Waiting for updates."); - while (true) { - const _trigger = await update_display.receive(); - log("Updating board."); - for await (const [_, feed] of storage.feeds.list()) { - const feed_channel = await fetch_feed_channel(bot, feed); - const embed = new EmbedBuilder().setTitle("`📚` Devoirs") - .setFooter({ text: "Mise à jour " + Date.now() }); - const sorted_devoirs = (await (collect(storage.devoirs.list()))) - .map(([_, d]) => d).toSorted((a, b) => a.date > b.date ? 1 : -1); - for (const devoir of sorted_devoirs) { - embed.addFields({ name: format_devoir_title(devoir), value: devoir.description }); - } - const board_message = await feed_channel.messages.fetch(feed.board_message_id); - board_message.edit({ embeds: [embed], content: "" }); - } - } -} - -const _1min = 60 * 1000; -const _24h = 24 * 60 * _1min; -const _7d = 7 * _24h; - -function format_devoir_title(devoir: Devoir) { - const date = new Date(devoir.date); - return `${date.getDate()}/${date.getMonth() + 1} - ${devoir.subject}`; -} - -async function notification_loop(bot: Client, storage: Storage) { - while (true) { - // get all devoirs to notify - const devoirs_to_notify = new Set(); - const notification_threshold = _7d; - for await (const [devoir_id, devoir] of storage.devoirs.list()) { - const time_left = devoir.date - Date.now(); - if (time_left < 0) continue; // TODO : delete devoir. - if (time_left > notification_threshold) continue; - devoirs_to_notify.add(devoir_id.id); - } - - // delete all obsolete notifications - for await (const [notification_id, notification] of storage.notifications.list()) { - if (devoirs_to_notify.has(notification.devoir_id)) continue; - const feed = await storage.feeds.get({ id: notification.feed_id }); - if (feed === null) continue; - const feed_channel = await fetch_feed_channel(bot, feed); - try { - const message = await feed_channel.messages.fetch(notification.message_id); - await message.delete(); - } catch (_) { /* . */ } - await storage.notifications.delete(notification_id); - await storage.feeds.update({ id: notification.feed_id }, (f) => f.notification_ids.delete(notification_id.id)); - } - - // create missing messages. - for await (const [feed_id, feed] of storage.feeds.list()) { - const feed_channel = await fetch_feed_channel(bot, feed); - // find devoirs needing to create a notification for - const devoirs_to_notify_in_feed = new Set(devoirs_to_notify.values()); - for (const existing_notification_id of feed.notification_ids.values()) { - const notification = await storage.notifications.get({ id: existing_notification_id }); - assertExists(notification); - devoirs_to_notify_in_feed.delete(notification.devoir_id); - } - // create notifications - for (const devoir_id of devoirs_to_notify_in_feed.values()) { - const devoir = await storage.devoirs.get({ id: devoir_id }); - if (devoir === null) continue; - const embed = new EmbedBuilder() - .setTitle(format_devoir_title(devoir)) - .setDescription(devoir.description); - const message = await feed_channel.send({ embeds: [embed] }); - const notification_id = await storage.notifications.add({ - devoir_id: devoir_id, - message_id: message.id, - feed_id: feed_id.id, - }); - storage.feeds.update(feed_id, (f) => f.notification_ids.add(notification_id.id)); - } - } - - // for each notification, update. - for await (const [_, notification] of storage.notifications.list()) { - const devoir = await storage.devoirs.get({ id: notification.devoir_id }); - if (devoir === null) continue; - const feed = await storage.feeds.get({ id: notification.feed_id }); - if (feed === null) continue; - const feed_channel = await fetch_feed_channel(bot, feed); - const message = await feed_channel.messages.fetch(notification.message_id); - const ms_left = devoir.date - Date.now(); - const days_left = Math.floor(ms_left / _24h); - const embed = new EmbedBuilder() - .setTitle(`${format_devoir_title(devoir)} (${days_left} jours)`) - .setDescription(devoir.description) - .setColor(0x8888ff); - if (days_left <= 5) embed.setColor(0xffd726); - if (days_left <= 3) embed.setColor(0xff8b26); - if (days_left <= 1) embed.setColor(0xff2626); - await message.edit({ embeds: [embed], content: "" }); - } - - await wait(15 * _1min); - } -} - -function trimmed(text: string, width: number) { - if (text.length < width) return text; - else return text.slice(0, width - 4) + "..."; -} - if (import.meta.main) await main(); diff --git a/src/lib/board.ts b/src/lib/board.ts new file mode 100644 index 0000000..0ca2180 --- /dev/null +++ b/src/lib/board.ts @@ -0,0 +1,27 @@ +import { Client, EmbedBuilder } from "npm:discord.js"; + +import { fetch_feed_channel, format_devoir_title } from "./lib.ts"; +import { Storage } from "./storage.ts"; +import { Channel, collect, log_from } from "./utils.ts"; + +const log = log_from(import.meta); + +export async function update_loop(bot: Client, storage: Storage, update_display: Channel) { + log("Waiting for updates."); + while (true) { + const _trigger = await update_display.receive(); + log("Updating board."); + for await (const [_, feed] of storage.feeds.list()) { + const feed_channel = await fetch_feed_channel(bot, feed); + const embed = new EmbedBuilder().setTitle("`📚` Devoirs") + .setFooter({ text: "Mise à jour " + Date.now() }); + const sorted_devoirs = (await (collect(storage.devoirs.list()))) + .map(([_, d]) => d).toSorted((a, b) => a.date > b.date ? 1 : -1); + for (const devoir of sorted_devoirs) { + embed.addFields({ name: format_devoir_title(devoir), value: devoir.description }); + } + const board_message = await feed_channel.messages.fetch(feed.board_message_id); + board_message.edit({ embeds: [embed], content: "" }); + } + } +} diff --git a/src/lib/lib.ts b/src/lib/lib.ts new file mode 100644 index 0000000..26e355e --- /dev/null +++ b/src/lib/lib.ts @@ -0,0 +1,18 @@ +import { assert } from "https://deno.land/std@0.223.0/assert/assert.ts"; +import { assertExists } from "https://deno.land/std@0.223.0/assert/assert_exists.ts"; + +import { Client, TextChannel } from "npm:discord.js"; + +import { Devoir } from "./storage.ts"; + +export async function fetch_feed_channel(bot: Client, feed: { channel_id: string }) { + const feed_channel = await bot.channels.fetch(feed.channel_id); + assertExists(feed_channel); + assert(feed_channel instanceof TextChannel); + return feed_channel; +} + +export function format_devoir_title(devoir: Devoir) { + const date = new Date(devoir.date); + return `${date.getDate()}/${date.getMonth() + 1} - ${devoir.subject}`; +} diff --git a/src/lib/notification.ts b/src/lib/notification.ts new file mode 100644 index 0000000..e8d1b9d --- /dev/null +++ b/src/lib/notification.ts @@ -0,0 +1,84 @@ +import { assertExists } from "https://deno.land/std@0.223.0/assert/assert_exists.ts"; + +import { Client, EmbedBuilder } from "npm:discord.js"; + +import { _1d, _1min, wait } from "./utils.ts"; +import { fetch_feed_channel, format_devoir_title } from "./lib.ts"; +import { Storage } from "./storage.ts"; + +export async function notification_loop(bot: Client, storage: Storage) { + while (true) { + // get all devoirs to notify + const devoirs_to_notify = new Set(); + const notification_threshold = 7 * _1d; + for await (const [devoir_id, devoir] of storage.devoirs.list()) { + const time_left = devoir.date - Date.now(); + if (time_left < 0) continue; // TODO : delete devoir. + if (time_left > notification_threshold) continue; + devoirs_to_notify.add(devoir_id.id); + } + + // delete all obsolete notifications + for await (const [notification_id, notification] of storage.notifications.list()) { + if (devoirs_to_notify.has(notification.devoir_id)) continue; + const feed = await storage.feeds.get({ id: notification.feed_id }); + if (feed === null) continue; + const feed_channel = await fetch_feed_channel(bot, feed); + try { + const message = await feed_channel.messages.fetch(notification.message_id); + await message.delete(); + } catch (_) { /* . */ } + await storage.notifications.delete(notification_id); + await storage.feeds.update({ id: notification.feed_id }, (f) => f.notification_ids.delete(notification_id.id)); + } + + // create missing messages. + for await (const [feed_id, feed] of storage.feeds.list()) { + const feed_channel = await fetch_feed_channel(bot, feed); + // find devoirs needing to create a notification for + const devoirs_to_notify_in_feed = new Set(devoirs_to_notify.values()); + for (const existing_notification_id of feed.notification_ids.values()) { + const notification = await storage.notifications.get({ id: existing_notification_id }); + assertExists(notification); + devoirs_to_notify_in_feed.delete(notification.devoir_id); + } + // create notifications + for (const devoir_id of devoirs_to_notify_in_feed.values()) { + const devoir = await storage.devoirs.get({ id: devoir_id }); + if (devoir === null) continue; + const embed = new EmbedBuilder() + .setTitle(format_devoir_title(devoir)) + .setDescription(devoir.description); + const message = await feed_channel.send({ embeds: [embed] }); + const notification_id = await storage.notifications.add({ + devoir_id: devoir_id, + message_id: message.id, + feed_id: feed_id.id, + }); + storage.feeds.update(feed_id, (f) => f.notification_ids.add(notification_id.id)); + } + } + + // for each notification, update. + for await (const [_, notification] of storage.notifications.list()) { + const devoir = await storage.devoirs.get({ id: notification.devoir_id }); + if (devoir === null) continue; + const feed = await storage.feeds.get({ id: notification.feed_id }); + if (feed === null) continue; + const feed_channel = await fetch_feed_channel(bot, feed); + const message = await feed_channel.messages.fetch(notification.message_id); + const ms_left = devoir.date - Date.now(); + const days_left = Math.floor(ms_left / _1d); + const embed = new EmbedBuilder() + .setTitle(format_devoir_title(devoir) + ` (${days_left} jours)`) + .setDescription(devoir.description) + .setColor(0x8888ff); + if (days_left <= 5) embed.setColor(0xffd726); + if (days_left <= 3) embed.setColor(0xff8b26); + if (days_left <= 1) embed.setColor(0xff2626); + await message.edit({ embeds: [embed], content: "" }); + } + + await wait(15 * _1min); + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index fa38a49..4790637 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -47,3 +47,12 @@ export function channel() { export async function wait(ms: number) { await new Promise((resolver) => setTimeout(resolver, ms)); } + +export function trimmed(text: string, width: number) { + if (text.length < width) return text; + else return text.slice(0, width - 4) + "..."; +} + +export const _1min = 60 * 1000; +export const _1h = 60 * _1min; +export const _1d = 24 * _1h;