diff --git a/.gitignore b/.gitignore index 6061194..15814d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/secrets.json +/conf.json /rules.json /local diff --git a/deno.lock b/deno.lock index 520d132..6647dcf 100644 --- a/deno.lock +++ b/deno.lock @@ -157,9 +157,13 @@ } }, "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", @@ -257,6 +261,27 @@ "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", @@ -646,6 +671,10 @@ "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 54020b5..fd47139 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -70,6 +70,15 @@ export class EpitlsBot { 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") @@ -103,6 +112,7 @@ 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)); diff --git a/src/email.ts b/src/email.ts index fae4e20..c712ed9 100644 --- a/src/email.ts +++ b/src/email.ts @@ -1,4 +1,6 @@ -import { SmtpClient } from "https://deno.land/x/smtp@v0.7.0/mod.ts"; +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; @@ -11,21 +13,61 @@ export type EmailerConfig = { * Wraps emailing process. */ export class Emailer { - private client; - private config; - private sender_email; + private sender_address; + private client_options; - public constructor(config: EmailerConfig, sender_email: string) { - this.config = config; - this.sender_email = sender_email; - this.client = new SmtpClient(); + 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(to_email: string, subject: string, content: string) { - const from = this.sender_email; + 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 this.client.connectTLS(this.config); - await this.client.send({ from, to, subject, content }); - await this.client.close(); + 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 ae6796c..00dd472 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,24 +1,35 @@ #!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --unstable-kv -import { log_from, read_secrets, root_path, SimpleResult, wait } from "./utils.ts"; +import { log_from, read_conf, 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 secrets = await read_secrets(root_path() + "/secrets.json"); + const conf = await read_conf(root_path() + "/conf.json"); + const rules = await RuleSet.from_file(root_path() + "/rules.json"); log("Loaded rules for", rules.size(), "roles."); + const state = await State.from_dir(root_path() + "/local"); log("Loaded state with", await state.users_count(), "users."); - const bot = new EpitlsBot(secrets.discord_bot_token); + + const bot = new EpitlsBot(conf.discord.bot_token); await bot.start(); log(`Started bot '${bot.bot_name()}' .`); - const cri_api = new CriApi(secrets.cri_token); - const service = new Service(state, bot, cri_api, rules); + 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); await service.serve(); } @@ -30,25 +41,38 @@ class Service { bot; cri_api; rules; + emailer; + verifier; - constructor(state: State, bot: EpitlsBot, cri_api: CriApi, rules: RuleSet) { + constructor(state: State, bot: EpitlsBot, cri_api: CriApi, rules: RuleSet, emailer: Emailer, verifier: WebVerifier) { this.state = state; this.bot = bot; this.cri_api = cri_api; this.rules = rules; + this.emailer = emailer; + this.verifier = verifier; } /** - * Main loops. + * 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 ?? "
+❌ Failure : +${message} ++ +