diff --git a/src/bot.ts b/src/bot.ts index af3a38a..54020b5 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -12,6 +12,9 @@ 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; @@ -29,6 +32,10 @@ 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() { const { promise, resolver } = split_promise(); this.bot.on("ready", () => resolver()); @@ -37,6 +44,9 @@ 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 }); @@ -46,10 +56,16 @@ export class EpitlsBot { 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; } diff --git a/src/cri.ts b/src/cri.ts index de83883..024c59b 100644 --- a/src/cri.ts +++ b/src/cri.ts @@ -1,5 +1,8 @@ import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"; +/** + * Wraps the CRI API. + */ export class CriApi { private token; @@ -7,6 +10,9 @@ export class CriApi { this.token = token; } + /** + * Fetches an array of the groups an user has been part of. + */ public async groups_of(login: string) { const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, { headers: { @@ -32,6 +38,9 @@ export class CriApi { return Array.from(result.values()); } + /** + * Tests wether a given login exists within the CRI registry. + */ public async user_exists(login: string) { const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, { headers: { diff --git a/src/email.ts b/src/email.ts index 71501ed..fae4e20 100644 --- a/src/email.ts +++ b/src/email.ts @@ -7,6 +7,9 @@ export type EmailerConfig = { password: string; }; +/** + * Wraps emailing process. + */ export class Emailer { private client; private config; diff --git a/src/main.ts b/src/main.ts index 2039526..ae6796c 100755 --- a/src/main.ts +++ b/src/main.ts @@ -22,6 +22,9 @@ async function main() { await service.serve(); } +/** + * Context of the service. + */ class Service { state; bot; @@ -35,6 +38,9 @@ class Service { this.rules = rules; } + /** + * Main loops. + */ async serve() { await this.update_all_users_roles(); diff --git a/src/rules.ts b/src/rules.ts index 2a1b0b5..295927d 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -1,6 +1,9 @@ export type TargetRole = { guild_id: string; role_id: string }; type SerializedRule = { group_id: string; target_role: TargetRole }; +/** + * A set of rules for associating CRI groups with Discord roles. + */ export class RuleSet { private rules; @@ -8,6 +11,9 @@ export class RuleSet { this.rules = new Map(); } + /** + * Reads a RuleSet from a JSON serialized file. + */ public static async from_file(path: string) { const result = new RuleSet(); const file_content = await Deno.readTextFile(path); @@ -16,10 +22,16 @@ export class RuleSet { 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; diff --git a/src/state.ts b/src/state.ts index c8e699b..3459188 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,29 +1,49 @@ 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; @@ -31,6 +51,9 @@ 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); diff --git a/src/utils.ts b/src/utils.ts index 72367dc..71075da 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,18 @@ 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[], @@ -29,6 +36,9 @@ 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); @@ -36,12 +46,18 @@ export function split_promise() { return { promise, resolver }; } +/** + * 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); } +/** + * Reads and parse a configuration file containing the current instance' secrets. + */ export async function read_secrets(path: string) { const content = await Deno.readTextFile(path); return z.object({