add API comments
This commit is contained in:
parent
fe34c8a91c
commit
d62bf01b4a
7 changed files with 85 additions and 0 deletions
16
src/bot.ts
16
src/bot.ts
|
@ -12,6 +12,9 @@ import {
|
||||||
import { channel, log_from, SimpleResult, split_promise } from "./utils.ts";
|
import { channel, log_from, SimpleResult, split_promise } from "./utils.ts";
|
||||||
const log = (...args: unknown[]) => log_from(import.meta.url, ...args);
|
const log = (...args: unknown[]) => log_from(import.meta.url, ...args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a discord bot and implements required actions.
|
||||||
|
*/
|
||||||
export class EpitlsBot {
|
export class EpitlsBot {
|
||||||
private bot;
|
private bot;
|
||||||
private token;
|
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() {
|
public async start() {
|
||||||
const { promise, resolver } = split_promise<void>();
|
const { promise, resolver } = split_promise<void>();
|
||||||
this.bot.on("ready", () => resolver());
|
this.bot.on("ready", () => resolver());
|
||||||
|
@ -37,6 +44,9 @@ export class EpitlsBot {
|
||||||
await this.register_commands();
|
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) {
|
public async assign_role(user_id: string, guild_id: string, role_id: string) {
|
||||||
const guild = await this.bot.guilds.fetch(guild_id);
|
const guild = await this.bot.guilds.fetch(guild_id);
|
||||||
const member = await guild.members.fetch({ user: user_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}'.`);
|
log(`Assigned role '${role.name}' to user '${member.displayName}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull-style API to wait for and receive account association requests.
|
||||||
|
*/
|
||||||
public async *receive_associations() {
|
public async *receive_associations() {
|
||||||
while (true) yield await this.assoc_channel.receive();
|
while (true) yield await this.assoc_channel.receive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connected bot name accessor.
|
||||||
|
*/
|
||||||
public bot_name() {
|
public bot_name() {
|
||||||
return this.bot.user?.displayName;
|
return this.bot.user?.displayName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the CRI API.
|
||||||
|
*/
|
||||||
export class CriApi {
|
export class CriApi {
|
||||||
private token;
|
private token;
|
||||||
|
|
||||||
|
@ -7,6 +10,9 @@ export class CriApi {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches an array of the groups an user has been part of.
|
||||||
|
*/
|
||||||
public async groups_of(login: string) {
|
public async groups_of(login: string) {
|
||||||
const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, {
|
const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -32,6 +38,9 @@ export class CriApi {
|
||||||
return Array.from(result.values());
|
return Array.from(result.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests wether a given login exists within the CRI registry.
|
||||||
|
*/
|
||||||
public async user_exists(login: string) {
|
public async user_exists(login: string) {
|
||||||
const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, {
|
const response = await fetch(`https://cri.epita.fr/api/v2/users/${login}/`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -7,6 +7,9 @@ export type EmailerConfig = {
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps emailing process.
|
||||||
|
*/
|
||||||
export class Emailer {
|
export class Emailer {
|
||||||
private client;
|
private client;
|
||||||
private config;
|
private config;
|
||||||
|
|
|
@ -22,6 +22,9 @@ async function main() {
|
||||||
await service.serve();
|
await service.serve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context of the service.
|
||||||
|
*/
|
||||||
class Service {
|
class Service {
|
||||||
state;
|
state;
|
||||||
bot;
|
bot;
|
||||||
|
@ -35,6 +38,9 @@ class Service {
|
||||||
this.rules = rules;
|
this.rules = rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main loops.
|
||||||
|
*/
|
||||||
async serve() {
|
async serve() {
|
||||||
await this.update_all_users_roles();
|
await this.update_all_users_roles();
|
||||||
|
|
||||||
|
|
12
src/rules.ts
12
src/rules.ts
|
@ -1,6 +1,9 @@
|
||||||
export type TargetRole = { guild_id: string; role_id: string };
|
export type TargetRole = { guild_id: string; role_id: string };
|
||||||
type SerializedRule = { group_id: string; target_role: TargetRole };
|
type SerializedRule = { group_id: string; target_role: TargetRole };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of rules for associating CRI groups with Discord roles.
|
||||||
|
*/
|
||||||
export class RuleSet {
|
export class RuleSet {
|
||||||
private rules;
|
private rules;
|
||||||
|
|
||||||
|
@ -8,6 +11,9 @@ export class RuleSet {
|
||||||
this.rules = new Map<string, TargetRole[]>();
|
this.rules = new Map<string, TargetRole[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a RuleSet from a JSON serialized file.
|
||||||
|
*/
|
||||||
public static async from_file(path: string) {
|
public static async from_file(path: string) {
|
||||||
const result = new RuleSet();
|
const result = new RuleSet();
|
||||||
const file_content = await Deno.readTextFile(path);
|
const file_content = await Deno.readTextFile(path);
|
||||||
|
@ -16,10 +22,16 @@ export class RuleSet {
|
||||||
return result;
|
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) {
|
public roles_for_group(group_id: string) {
|
||||||
return this.rules.get(group_id) ?? [];
|
return this.rules.get(group_id) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of managed Discord roles.
|
||||||
|
*/
|
||||||
public size() {
|
public size() {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
for (const roles of this.rules.values()) result += roles.length;
|
for (const roles of this.rules.values()) result += roles.length;
|
||||||
|
|
23
src/state.ts
23
src/state.ts
|
@ -1,29 +1,49 @@
|
||||||
import { v1 as uuid } from "https://deno.land/std@0.213.0/uuid/mod.ts";
|
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 };
|
export type StoredUser = { discord_user_id: string; cri_login: string };
|
||||||
|
/**
|
||||||
|
* Wraps the persistent state, containing user associaitons.
|
||||||
|
*/
|
||||||
export class State {
|
export class State {
|
||||||
|
/**
|
||||||
|
* note : We are using a Deno.Kv as storage.\
|
||||||
|
* Its API is comparable to a Key-Value database.
|
||||||
|
*/
|
||||||
kv;
|
kv;
|
||||||
|
|
||||||
constructor(kv: Deno.Kv) {
|
constructor(kv: Deno.Kv) {
|
||||||
this.kv = kv;
|
this.kv = kv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a persistent State that is stored in the specified directory.
|
||||||
|
*/
|
||||||
public static async from_dir(local_path: string) {
|
public static async from_dir(local_path: string) {
|
||||||
await Deno.mkdir(local_path, { recursive: true });
|
await Deno.mkdir(local_path, { recursive: true });
|
||||||
const kv = await Deno.openKv(local_path + "/kv");
|
const kv = await Deno.openKv(local_path + "/kv");
|
||||||
return new State(kv);
|
return new State(kv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates all stored users associations.
|
||||||
|
*/
|
||||||
public async *users() {
|
public async *users() {
|
||||||
const query = this.kv.list({ prefix: ["users/"] });
|
const query = this.kv.list({ prefix: ["users/"] });
|
||||||
for await (const { value } of query) yield value as StoredUser;
|
for await (const { value } of query) yield value as StoredUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count stored users associations.
|
||||||
|
*/
|
||||||
public async users_count() {
|
public async users_count() {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
for await (const _ of this.users()) result += 1;
|
for await (const _ of this.users()) result += 1;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the association of a user given its discord user id.
|
||||||
|
*/
|
||||||
public async get_user(discord_user_id: string) {
|
public async get_user(discord_user_id: string) {
|
||||||
const { value: user_uuid } = await this.kv.get(["users_by_discord_id/", discord_user_id]);
|
const { value: user_uuid } = await this.kv.get(["users_by_discord_id/", discord_user_id]);
|
||||||
if (user_uuid === null) return undefined;
|
if (user_uuid === null) return undefined;
|
||||||
|
@ -31,6 +51,9 @@ export class State {
|
||||||
return stored as StoredUser;
|
return stored as StoredUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a new user association.
|
||||||
|
*/
|
||||||
public async set_user(discord_user_id: string, cri_login: string) {
|
public async set_user(discord_user_id: string, cri_login: string) {
|
||||||
await this.remove_user(discord_user_id);
|
await this.remove_user(discord_user_id);
|
||||||
await this.add_user(discord_user_id, cri_login);
|
await this.add_user(discord_user_id, cri_login);
|
||||||
|
|
16
src/utils.ts
16
src/utils.ts
|
@ -1,11 +1,18 @@
|
||||||
import * as path from "https://deno.land/std@0.213.0/path/mod.ts";
|
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";
|
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() {
|
export function root_path() {
|
||||||
const this_path = new URL(import.meta.url).pathname;
|
const this_path = new URL(import.meta.url).pathname;
|
||||||
return path.resolve(this_path, "..", "..");
|
return path.resolve(this_path, "..", "..");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates handles to a channel.\
|
||||||
|
* c.f; https://en.wikipedia.org/wiki/Channel_(programming)
|
||||||
|
*/
|
||||||
export function channel<T>() {
|
export function channel<T>() {
|
||||||
const inner = {
|
const inner = {
|
||||||
items: [] as T[],
|
items: [] as T[],
|
||||||
|
@ -29,6 +36,9 @@ export function channel<T>() {
|
||||||
|
|
||||||
const resolves_to = <T>(item: T) => new Promise<T>((r) => r(item));
|
const resolves_to = <T>(item: T) => new Promise<T>((r) => r(item));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns both a promise and its resolver.
|
||||||
|
*/
|
||||||
export function split_promise<T>() {
|
export function split_promise<T>() {
|
||||||
let resolver: null | ((item: T) => void) = null;
|
let resolver: null | ((item: T) => void) = null;
|
||||||
const promise = new Promise<T>((r) => resolver = r);
|
const promise = new Promise<T>((r) => resolver = r);
|
||||||
|
@ -36,12 +46,18 @@ export function split_promise<T>() {
|
||||||
return { promise, resolver };
|
return { promise, resolver };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging function factory.
|
||||||
|
*/
|
||||||
export function log_from(url: string, ...args: unknown[]) {
|
export function log_from(url: string, ...args: unknown[]) {
|
||||||
const date = new Date().toLocaleString("fr-FR");
|
const date = new Date().toLocaleString("fr-FR");
|
||||||
const file = path.basename(new URL(url).pathname);
|
const file = path.basename(new URL(url).pathname);
|
||||||
console.log(`[${date}][epitls][${file}]`, ...args);
|
console.log(`[${date}][epitls][${file}]`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and parse a configuration file containing the current instance' secrets.
|
||||||
|
*/
|
||||||
export async function read_secrets(path: string) {
|
export async function read_secrets(path: string) {
|
||||||
const content = await Deno.readTextFile(path);
|
const content = await Deno.readTextFile(path);
|
||||||
return z.object({
|
return z.object({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue