diff --git a/.gitignore b/.gitignore index 15814d6..6061194 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/conf.json +/secrets.json /rules.json /local diff --git a/deno.lock b/deno.lock index 6647dcf..520d132 100644 --- a/deno.lock +++ b/deno.lock @@ -157,13 +157,9 @@ } }, "redirects": { - "https://deno.land/x/denomailer/mod.ts": "https://deno.land/x/denomailer@1.6.0/mod.ts", - "https://deno.land/x/dotenv/load.ts": "https://deno.land/x/dotenv@v3.2.2/load.ts", - "https://deno.land/x/smtp/mod.ts": "https://deno.land/x/smtp@v0.7.0/mod.ts", "https://deno.land/x/zod/mod.ts": "https://deno.land/x/zod@v3.22.4/mod.ts" }, "remote": { - "https://deno.land/std@0.173.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1", "https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", "https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", "https://deno.land/std@0.213.0/bytes/concat.ts": "9cac3b4376afbef98ff03588eb3cf948e0d1eb6c27cfe81a7651ab6dd3adc54a", @@ -261,27 +257,6 @@ "https://deno.land/std@0.213.0/uuid/v3.ts": "aff081baee55498ed5804d006735a77b252ac1645e3b418058807218371de577", "https://deno.land/std@0.213.0/uuid/v4.ts": "8a9c60c887446651be5d50b468a3d702b87bb821fc35f0edcb5515c3bc07b256", "https://deno.land/std@0.213.0/uuid/v5.ts": "f6771dc89e89f26e74a9b51d25d6b711c27d2ddf3a3650312dd46e7edfe2491e", - "https://deno.land/std@0.81.0/_util/assert.ts": "e1f76e77c5ccb5a8e0dbbbe6cce3a56d2556c8cb5a9a8802fc9565af72462149", - "https://deno.land/std@0.81.0/bytes/mod.ts": "e4f91c6473fe13e3cf1a23649137f87f49135c10bc08fc0f83382a0fb0b03744", - "https://deno.land/std@0.81.0/encoding/utf8.ts": "1b7e77db9a12363c67872f8a208886ca1329f160c1ca9133b13d2ed399688b99", - "https://deno.land/std@0.81.0/io/bufio.ts": "3cbbe1f761c1c636d1e7128ed4e7fdca6bf21d9199aa3cae71e69972a6ae8f93", - "https://deno.land/std@0.81.0/textproto/mod.ts": "4c378eda3cb6216608bb4c3a34201761c65f6980c4669455ca224c330cd5b790", - "https://deno.land/x/denomailer@1.6.0/client/basic/QUE.ts": "5af1dfcc5814bf4542f098908ac1fdd8a1a1c2b1597138a121c95eaa791315d0", - "https://deno.land/x/denomailer@1.6.0/client/basic/client.ts": "462e4db45ae218647812ceae720d55ea33e0e928f9138fee9da5913cbb1e20f9", - "https://deno.land/x/denomailer@1.6.0/client/basic/connection.ts": "68de68d7551d8303629905c2b7581cb09b45646e530ce93a5786ca1aba61055c", - "https://deno.land/x/denomailer@1.6.0/client/basic/transforms.ts": "e630a23d24e9b397e231ae8796c0a0080770ac6f5ab9bffc105d3717706e62c9", - "https://deno.land/x/denomailer@1.6.0/client/mod.ts": "8ec4c25d9586f83f8629768311a077eaf03b1490dcb872030ba2e27fadb674d8", - "https://deno.land/x/denomailer@1.6.0/client/pool.ts": "0466e69ca8959aa85501cc6b30d7f5fd8e43b0a6ac88ecc60dab71081e801bae", - "https://deno.land/x/denomailer@1.6.0/client/worker/worker.ts": "a4c3a3e2e1fde0967817ece7c345a565eb44a7312acf8d46ce620d4ff4443b31", - "https://deno.land/x/denomailer@1.6.0/config/client.ts": "302f5c18fbb5531b5615613084b86d44120acc210e072f4135e431fa27fc4526", - "https://deno.land/x/denomailer@1.6.0/config/mail/attachments.ts": "1f357bddc9d5e813c3f647498db81a165a1a8a7163116c58dc10cf01427fd81e", - "https://deno.land/x/denomailer@1.6.0/config/mail/content.ts": "3925d4c3baaabed4e08933159d34b1450e6426b35a5bc323a0666780bef20192", - "https://deno.land/x/denomailer@1.6.0/config/mail/email.ts": "bb0ca104bf9cb54af6613a04b3f8cb05290f4fb012e7113f1d050cab10226a7c", - "https://deno.land/x/denomailer@1.6.0/config/mail/encoding.ts": "0bc5983ada3b902333925cdca225f8ea5e28fffbc2f1bd2b0ccb9a423f6f7fcc", - "https://deno.land/x/denomailer@1.6.0/config/mail/headers.ts": "ce94874beb5a1a7248b5b91bf1ae3b3aed2d4c0541f3f448f2bbfad6c8f570ee", - "https://deno.land/x/denomailer@1.6.0/config/mail/mod.ts": "a7fafa3386a45a585d7983d816b09ddc28b2f2b84097a614f1e38685b1f62868", - "https://deno.land/x/denomailer@1.6.0/deps.ts": "12bef188bb2a490fedc82ac1889f3d438e8a15887c423b045fee532b31a43102", - "https://deno.land/x/denomailer@1.6.0/mod.ts": "71a197dff098194ab53691abd3c9d22a276ef04e1382eb85f5632dbcb5a83bf3", "https://deno.land/x/discordeno@18.0.1/bot.ts": "b6c4f1c966f1a968186921619b6e5ebfec7c5eb0dc2e49a66d2c86b37cb2acc7", "https://deno.land/x/discordeno@18.0.1/gateway/manager/calculateTotalShards.ts": "2d2ebe860861d58524416446426d78e5b881c17b3a565ea4822c67f5534214bc", "https://deno.land/x/discordeno@18.0.1/gateway/manager/calculateWorkerId.ts": "44c46f2977104a5f92cc21cf31d6b2bc5dcfcefba23495cd619dbdf074a00af1", @@ -671,10 +646,6 @@ "https://deno.land/x/discordeno@18.0.1/util/utils.ts": "b16797ea1918af635f0c04c345a7c9b57c078310ac18d0c86936ec8abfaeddeb", "https://deno.land/x/discordeno@18.0.1/util/validateLength.ts": "7c610911d72082f9cfe2c455737cd37d8ce8f323483f0ef65fdfea6a993984b5", "https://deno.land/x/discordeno@18.0.1/util/verifySignature.ts": "8ba1c3d2698f347b4f32a76bd33edeb67ee9d23c34f419a797c393926786bb97", - "https://deno.land/x/smtp@v0.7.0/code.ts": "f388fae4995b4d35d99fb6b8bfded522f5a3e7e7d63babdf318a059d6db43baf", - "https://deno.land/x/smtp@v0.7.0/deps.ts": "5e2a437e3ae35f0e83719fd2e707858dcb750c1111ff5bebc729522a1380b53d", - "https://deno.land/x/smtp@v0.7.0/mod.ts": "9b0d8fbdacc184d1af10f727980e51486e0ddf9d2ec7227c8dfce90db5bfbcf5", - "https://deno.land/x/smtp@v0.7.0/smtp.ts": "47c72a99925ad07f3174037f9325dbb8b703dc1177277b9161dc6209c7fa4f90", "https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea", "https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", "https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", diff --git a/src/bot.ts b/src/bot.ts index fd47139..a641adb 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -12,16 +12,13 @@ import { import { channel, log_from, SimpleResult, split_promise } from "./utils.ts"; const log = (...args: unknown[]) => log_from(import.meta.url, ...args); -/** - * Wraps a discord bot and implements required actions. - */ export class EpitlsBot { - private bot; - private token; - private rest; - private assoc_channel; + bot; + token; + rest; + assoc_channel; - public constructor(bot_token: string) { + constructor(bot_token: string) { this.token = bot_token; const intents = [GatewayIntentBits.Guilds]; this.bot = new Client({ intents }); @@ -32,11 +29,7 @@ export class EpitlsBot { >(); } - /** - * Connects to discord API server and registers slash commands.\ - * Needs to be run after construction as it is an asynchronous operation. - */ - public async start() { + async start() { const { promise, resolver } = split_promise(); this.bot.on("ready", () => resolver()); this.bot.login(this.token); @@ -44,41 +37,6 @@ export class EpitlsBot { await this.register_commands(); } - /** - * Assigns a discord role to a discord user. - */ - public async assign_role(user_id: string, guild_id: string, role_id: string) { - const guild = await this.bot.guilds.fetch(guild_id); - const member = await guild.members.fetch({ user: user_id }); - const role = await guild.roles.fetch(role_id); - if (role === null) return console.error("Role", role_id, "not found in guild", guild_id); - member.roles.add(role_id); - log(`Assigned role '${role.name}' to user '${member.displayName}'.`); - } - - /** - * Pull-style API to wait for and receive account association requests. - */ - public async *receive_associations() { - while (true) yield await this.assoc_channel.receive(); - } - - /** - * Connected bot name accessor. - */ - public bot_name() { - return this.bot.user?.displayName; - } - - public async get_username(user_id: string) { - try { - const user = await this.bot.users.fetch(user_id); - return user.displayName; - } catch (_) { - return undefined; - } - } - private async register_commands() { const cmd = new SlashCommandBuilder() .setName("associate") @@ -112,11 +70,23 @@ export class EpitlsBot { this.assoc_channel.send({ cri_login, discord_user_id, callback: resolver }); await interaction.reply(message_command_response_sending_email(email)); - log(`Started verification for discord id '${discord_user_id}' with cri login '${cri_login}'.`); const result = await promise; if (result === true) interaction.editReply(message_command_response_success()); else interaction.editReply(message_command_response_error(result)); } + + async assign_role(user_id: string, guild_id: string, role_id: string) { + const guild = await this.bot.guilds.fetch(guild_id); + const member = await guild.members.fetch({ user: user_id }); + const role = await guild.roles.fetch(role_id); + if (role === null) return console.error("Role", role_id, "not found in guild", guild_id); + member.roles.add(role_id); + log(`Assigned role '${role.name}' to user '${member.displayName}'.`); + } + + async *receive_associations() { + while (true) yield await this.assoc_channel.receive(); + } } function message_command_response_sending_email(email: string) { diff --git a/src/cri.ts b/src/cri.ts index a980044..ec98ab6 100644 --- a/src/cri.ts +++ b/src/cri.ts @@ -1,19 +1,12 @@ import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"; -/** - * Wraps the CRI API. - */ export class CriApi { - private token; - - public constructor(token: string) { + token; + constructor(token: string) { this.token = token; } - /** - * Fetches an array of the groups an user has been part of. - */ - public async groups_of(login: string) { + async groups_of(login: string) { const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, { headers: { accept: "application/json", @@ -21,14 +14,14 @@ export class CriApi { }, }); const body = await response.json(); - const group = z.object({ slug: z.string() }); + const group_parser = z.object({ slug: z.string() }); const parser = z.object({ - primary_group: group, + primary_group: group_parser, groups_history: z.array(z.object({ - group, + group: group_parser, graduation_year: z.number().or(z.null()), })), - current_groups: z.array(group), + current_groups: z.array(group_parser), }); const parsed = parser.parse(body); const result = new Set(); @@ -38,16 +31,13 @@ export class CriApi { return Array.from(result.values()); } - /** - * Tests wether a given login exists within the CRI registry. - */ - public async user_exists(login: string) { + async user_exists(login: string) { const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, { headers: { accept: "application/json", authorization: "Basic " + this.token, }, }); - return response.status === 200; + return response.status !== 404; } } diff --git a/src/email.ts b/src/email.ts deleted file mode 100644 index c712ed9..0000000 --- a/src/email.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ClientOptions, SMTPClient } from "https://deno.land/x/denomailer@1.6.0/mod.ts"; -import { log_from } from "./utils.ts"; -const log = (...args: unknown[]) => log_from(import.meta.url, ...args); - -export type EmailerConfig = { - hostname: string; - port: number; - username: string; - password: string; -}; - -/** - * Wraps emailing process. - */ -export class Emailer { - private sender_address; - private client_options; - - public constructor(config: EmailerConfig) { - const client_options: ClientOptions = { - connection: { - hostname: config.hostname, - auth: { - username: config.username, - password: config.password, - }, - port: config.port, - tls: true, - }, - }; - this.client_options = client_options; - this.sender_address = config.username; - } - - public async send_confirmation_mail(discord_username: string, cri_email: string, link: string) { - await this.send(cri_email, CONFIRMATION_EMAIL_SUBJECT, confirmation_email_body(discord_username, link)); - } - - private async send(to_email: string, subject: string, content: string) { - const client = new SMTPClient(this.client_options); - const from = this.sender_address; - const to = to_email; - await client.send({ from, to, subject, html: content }); - await client.close(); - log(`Sent an email to '${to_email}'.`); - } -} - -const CONFIRMATION_EMAIL_SUBJECT = "Confirmation d'association à un compte discord"; -function confirmation_email_body(discord_username: string, link: string) { - return ` - - - -
-Bonjour,
-Ceci est un message automatique de confirmation pour l'association
-du compte discord '${discord_username}' à cet email.
-
-Pour terminer l'association, veuillez suivre sur le lien suivant :
-${link}
-
-Si vous n'êtes pas à l'origine de cette demande, veuillez ignorer ce message.
-
-
----
-Je suis un robot et cette action à été effectuée automatiquement.
-Vous pouvez contacter le développeur de se service à l'email matthieu at imagevo dot fr.
-
- - -`; -} diff --git a/src/main.ts b/src/main.ts index 7c4912a..b07657d 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,78 +1,48 @@ #!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --unstable-kv -import { log_from, read_conf, root_path, SimpleResult } from "./utils.ts"; +import { log_from, read_secrets, root_path, SimpleResult, wait } from "./utils.ts"; import { State } from "./state.ts"; import { RuleSet } from "./rules.ts"; import { EpitlsBot } from "./bot.ts"; import { CriApi } from "./cri.ts"; -import { Emailer } from "./email.ts"; -import { WebVerifier } from "./verifier.ts"; const log = (...args: unknown[]) => log_from(import.meta.url, ...args); async function main() { - const conf = await read_conf(root_path() + "/conf.json"); - + const secrets = await read_secrets(root_path() + "/secrets.json"); const rules = await RuleSet.from_file(root_path() + "/rules.json"); - log("Loaded rules for", rules.size(), "roles."); - + log("Loaded rules for", rules.rules.size, "groups."); const state = await State.from_dir(root_path() + "/local"); log("Loaded state with", await state.users_count(), "users."); - - const bot = new EpitlsBot(conf.discord.bot_token); + const bot = new EpitlsBot(secrets.discord_bot_token); await bot.start(); - log(`Started bot '${bot.bot_name()}' .`); + log(`Started bot '${bot.bot.user?.displayName}' .`); + const cri_api = new CriApi(secrets.cri_token); - const verifier = new WebVerifier(conf.verif_http); - await verifier.start(); - log(`Started web verifier at '${verifier.url()}' .`); - - const cri_api = new CriApi(conf.cri.cri_token); - const mailer = new Emailer(conf.email_smtp); - - const service = new Service(state, bot, cri_api, rules, mailer, verifier); + const service = new Service(state, bot, cri_api, rules); await service.serve(); } -/** - * Context of the service. - */ class Service { state; bot; cri_api; rules; - emailer; - verifier; - constructor(state: State, bot: EpitlsBot, cri_api: CriApi, rules: RuleSet, emailer: Emailer, verifier: WebVerifier) { + constructor(state: State, bot: EpitlsBot, cri_api: CriApi, rules: RuleSet) { this.state = state; this.bot = bot; this.cri_api = cri_api; this.rules = rules; - this.emailer = emailer; - this.verifier = verifier; } - /** - * Launches main loops. - */ async serve() { await this.update_all_users_roles(); - // for all received associations, trigger the association procedure. (async () => { for await (const { discord_user_id, cri_login, callback } of this.bot.receive_associations()) { this.association_procedure(discord_user_id, cri_login).then(callback); } })(); - - // for all links that must be sent, trigger mail sending. - (async () => { - for await (const { cri_login, discord_id, link } of this.verifier.links_to_send()) { - const username = await this.bot.get_username(discord_id); - this.emailer.send_confirmation_mail(username ?? "", cri_login + "@epita.fr", link); - } - })(); } async update_all_users_roles() { @@ -85,18 +55,15 @@ class Service { async update_user_roles(cri_login: string, discord_user_id: string) { const groups = await this.cri_api.groups_of(cri_login); - // log("found groups", groups); const roles = groups.map((group) => this.rules.roles_for_group(group)).flat(); - // log("found setting roles", roles); for (const { guild_id, role_id } of roles) await this.bot.assign_role(discord_user_id, guild_id, role_id); } async association_procedure(discord_user_id: string, cri_login: string): Promise { try { + await wait(1000); if (!await this.cri_api.user_exists(cri_login)) return "No such login."; - const res = await this.verifier.verification(discord_user_id, cri_login); - if (res !== true) return res; - await this.state.set_user(discord_user_id, cri_login); + this.state.set_user(discord_user_id, cri_login); await this.update_user_roles(cri_login, discord_user_id); } catch (error) { console.error(error); diff --git a/src/rules.ts b/src/rules.ts index f609a0d..1f5cac0 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -1,44 +1,19 @@ -import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"; - +type SerializedRule = { group_id: string; target_role: TargetRole }; export type TargetRole = { guild_id: string; role_id: string }; - -/** - * A set of rules for associating CRI groups with Discord roles. - */ export class RuleSet { - private rules; - - public constructor() { + rules; + constructor() { this.rules = new Map(); } - /** - * Reads a RuleSet from a JSON serialized file. - */ - public static async from_file(path: string) { + static async from_file(path: string) { const result = new RuleSet(); const file_content = await Deno.readTextFile(path); - const parsed = parse_rules(file_content); + const parsed = JSON.parse(file_content) as SerializedRule[]; for (const { group_id, target_role } of parsed) result.append_rule(group_id, target_role); return result; } - /** - * Gets which Discord roles must be assigned to a user which is part of a given CRI group. - */ - public roles_for_group(group_id: string) { - return this.rules.get(group_id) ?? []; - } - - /** - * Number of managed Discord roles. - */ - public size() { - let result = 0; - for (const roles of this.rules.values()) result += roles.length; - return result; - } - private append_rule(group_id: string, target_role: TargetRole) { let roles = this.rules.get(group_id); if (roles === undefined) { @@ -47,11 +22,8 @@ export class RuleSet { } roles.push(target_role); } -} -function parse_rules(content: string) { - return z.array(z.object({ - group_id: z.string(), - target_role: z.object({ guild_id: z.string(), role_id: z.string() }), - })).parse(JSON.parse(content)); + roles_for_group(group_id: string) { + return this.rules.get(group_id) ?? []; + } } diff --git a/src/state.ts b/src/state.ts index 3459188..498b7a8 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,49 +1,29 @@ import { v1 as uuid } from "https://deno.land/std@0.213.0/uuid/mod.ts"; export type StoredUser = { discord_user_id: string; cri_login: string }; -/** - * Wraps the persistent state, containing user associaitons. - */ export class State { - /** - * note : We are using a Deno.Kv as storage.\ - * Its API is comparable to a Key-Value database. - */ kv; - constructor(kv: Deno.Kv) { this.kv = kv; } - /** - * Creates a persistent State that is stored in the specified directory. - */ public static async from_dir(local_path: string) { await Deno.mkdir(local_path, { recursive: true }); const kv = await Deno.openKv(local_path + "/kv"); return new State(kv); } - /** - * Generates all stored users associations. - */ public async *users() { const query = this.kv.list({ prefix: ["users/"] }); for await (const { value } of query) yield value as StoredUser; } - /** - * Count stored users associations. - */ public async users_count() { let result = 0; for await (const _ of this.users()) result += 1; return result; } - /** - * Get the association of a user given its discord user id. - */ public async get_user(discord_user_id: string) { const { value: user_uuid } = await this.kv.get(["users_by_discord_id/", discord_user_id]); if (user_uuid === null) return undefined; @@ -51,9 +31,6 @@ export class State { return stored as StoredUser; } - /** - * Appends a new user association. - */ public async set_user(discord_user_id: string, cri_login: string) { await this.remove_user(discord_user_id); await this.add_user(discord_user_id, cri_login); @@ -66,10 +43,9 @@ export class State { await this.kv.set(["users_by_discord_id/", discord_user_id], user_uuid); } - private async remove_user(discord_user_id: string) { + async remove_user(discord_user_id: string) { const { value: user_uuid } = await this.kv.get(["users_by_discord_id/", discord_user_id]); if (user_uuid === null) return undefined; await this.kv.delete(["users/", user_uuid as string]); - await this.kv.delete(["users_by_discord_id/", discord_user_id]); } } diff --git a/src/utils.ts b/src/utils.ts index 1acda34..72367dc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,18 +1,11 @@ import * as path from "https://deno.land/std@0.213.0/path/mod.ts"; import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"; -/** - * Gets the root path of the clonned project. - */ export function root_path() { const this_path = new URL(import.meta.url).pathname; return path.resolve(this_path, "..", ".."); } -/** - * Creates handles to a channel.\ - * c.f; https://en.wikipedia.org/wiki/Channel_(programming) - */ export function channel() { const inner = { items: [] as T[], @@ -36,9 +29,6 @@ export function channel() { const resolves_to = (item: T) => new Promise((r) => r(item)); -/** - * Returns both a promise and its resolver. - */ export function split_promise() { let resolver: null | ((item: T) => void) = null; const promise = new Promise((r) => resolver = r); @@ -46,65 +36,20 @@ export function split_promise() { return { promise, resolver }; } -export type Resolver = (item: T) => void; - -/** - * Logging function factory. - */ export function log_from(url: string, ...args: unknown[]) { const date = new Date().toLocaleString("fr-FR"); const file = path.basename(new URL(url).pathname); console.log(`[${date}][epitls][${file}]`, ...args); } -export type Conf = Awaited>; -/** - * Reads and parse a configuration file containing the current instance' secrets. - */ -export async function read_conf(path: string) { +export async function read_secrets(path: string) { const content = await Deno.readTextFile(path); return z.object({ - discord: z.object({ - bot_token: z.string(), - }), - cri: z.object({ - cri_token: z.string(), - }), - email_smtp: z.object({ - hostname: z.string(), - port: z.number(), - username: z.string(), - password: z.string(), - }), - verif_http: z.object({ - port: z.number(), - url_prefix: z.string(), - }), + discord_bot_token: z.string(), + cri_token: z.string(), }).parse(JSON.parse(content)); } -export function placeholder_conf() { - const result: Conf = { - "discord": { - "bot_token": "available at 'https://discord.com/developers/applications'", - }, - "cri": { - "cri_token": "'username:password' base64-encoded", - }, - "email_smtp": { - "hostname": "smtp.gmail.com", - "port": 465, - "username": "bot@gmail.com", - "password": "PWD", - }, - "verif_http": { - "port": 80, - "url_prefix": "localhost", - }, - }; - return JSON.stringify(result, null, 4); -} - export type SimpleResult = true | string; export async function wait(ms: number) { diff --git a/src/verifier.ts b/src/verifier.ts deleted file mode 100644 index e37d154..0000000 --- a/src/verifier.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { v1 as uuid } from "https://deno.land/std@0.213.0/uuid/mod.ts"; -import { channel, log_from, Resolver, SimpleResult, split_promise, wait } from "./utils.ts"; -const log = (...args: unknown[]) => log_from(import.meta.url, ...args); - -export type WebVerifierConf = { - port: number; - url_prefix: string; -}; - -export type LinkToSend = { - discord_id: string; - cri_login: string; - link: string; -}; -export class WebVerifier { - conf; - awaiting; - link_channel; - - public constructor(conf: WebVerifierConf) { - this.conf = conf; - this.awaiting = new Map>(); - this.link_channel = channel(); - } - - public async start() { - const { promise, resolver } = split_promise(); - Deno.serve({ - hostname: "0.0.0.0", - port: this.conf.port, - onListen: () => resolver(), - }, (request) => this.serve(request)); - return await promise; - } - - public async verification(discord_id: string, cri_login: string) { - const { link, uid } = this.create_verif_link(); - const { promise, resolver } = split_promise(); - this.awaiting.set(uid, resolver); - log(`Created verification link '${link}' for discord id '${discord_id}' to cri login '${cri_login}'.`); - this.link_channel.send({ cri_login, discord_id, link }); - this.start_peremption(uid); - return await promise; - } - - public async *links_to_send() { - while (true) yield await this.link_channel.receive(); - } - - public url() { - return `http://${this.conf.url_prefix}`; - } - - private serve(request: Request): Response { - const url = new URL(request.url); - if (!url.pathname.startsWith("/verify/")) return response_failure("Invalid path."); - - const [uid] = url.pathname.slice("/verify/".length).split("/"); - const resolver = this.awaiting.get(uid); - if (resolver === undefined) return response_failure("Invalid verification link."); - - resolver(true); - this.awaiting.delete(uid); - log(`Verified link ${uid} successfully.`); - return response_success(); - } - - private create_verif_link() { - const uid = uuid.generate() as string; - const link = `http://${this.conf.url_prefix}/verify/${uid}`; - return { uid, link }; - } - - private async start_peremption(uid: string) { - await wait(5 * 60 * 1000); // 5 mins - const resolver = this.awaiting.get(uid); - if (resolver === undefined) return; - resolver("Verification link timeout."); - this.awaiting.delete(uid); - log(`Link '${uid}' timed out.`); - } -} - -function response_failure(message: string) { - const body = ` - - - Epitls • Verification - - -
-❌ Failure :
-${message}    
-
- - -`; - return new Response(body, { headers: { "Content-Type": "text/html" } }); -} - -function response_success() { - const body = ` - - - Epitls • Verification - - -
-✅ Verified successfully.    
-
- - -`; - return new Response(body, { headers: { "Content-Type": "text/html" } }); -} diff --git a/watch.sh b/watch.sh deleted file mode 100755 index 76ae163..0000000 --- a/watch.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -e -cd "$(dirname "$(realpath "$0")")" - -nodemon -w src -e ts -x ./src/main.ts