adds renaming and rule for historical participation
This commit is contained in:
parent
fb141abd05
commit
09e05c5c53
5 changed files with 114 additions and 36 deletions
76
src/bot.ts
76
src/bot.ts
|
@ -4,16 +4,18 @@ import {
|
||||||
Client,
|
Client,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
|
Guild,
|
||||||
Interaction,
|
Interaction,
|
||||||
REST,
|
REST,
|
||||||
Routes,
|
Routes,
|
||||||
|
SlashCommandBooleanOption,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
|
SlashCommandRoleOption,
|
||||||
SlashCommandStringOption,
|
SlashCommandStringOption,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
} from "npm:discord.js@14.14.1";
|
} from "npm:discord.js@14.14.1";
|
||||||
import { channel, log_from, SimpleResult, split_promise } from "./utils.ts";
|
|
||||||
import { RuleSet } from "./rules.ts";
|
import { RuleSet } from "./rules.ts";
|
||||||
import { SlashCommandSubcommandBuilder } from "npm:discord.js@14.14.1";
|
import { channel, log_from, SimpleResult, split_promise, try_ } from "./utils.ts";
|
||||||
import { SlashCommandRoleOption } from "npm:discord.js@14.14.1";
|
|
||||||
const log = log_from(import.meta.url);
|
const log = log_from(import.meta.url);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,15 +56,34 @@ export class EpitlsBot {
|
||||||
* Assigns a discord role to a discord user.
|
* 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) {
|
||||||
try {
|
|
||||||
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 this.try_fetch_member(guild, user_id);
|
||||||
|
if (member === undefined) return;
|
||||||
const role = await guild.roles.fetch(role_id);
|
const role = await guild.roles.fetch(role_id);
|
||||||
if (role === null) return console.error("Role", role_id, "not found in guild", guild_id);
|
if (role === null) return console.error("Role", role_id, "not found in guild", guild_id);
|
||||||
member.roles.add(role_id);
|
member.roles.add(role_id);
|
||||||
log(`Assigned role '${role.name}' to user '${member.displayName}'.`);
|
log(`Assigned to user '${member.displayName}' role '${role.name}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update_user_name(user_id: string, must_contain: string, default_suffix: string) {
|
||||||
|
for (const guild of this.bot.guilds.cache.values()) {
|
||||||
|
const user = await this.try_fetch_member(guild, user_id);
|
||||||
|
if (user === undefined) continue;
|
||||||
|
const name = user.displayName;
|
||||||
|
if (to_comparable(name).includes(to_comparable(must_contain))) continue;
|
||||||
|
const DISCORD_MAX_NAME_LENGTH = 35;
|
||||||
|
const generated_name_prefix = name.slice(0, DISCORD_MAX_NAME_LENGTH - default_suffix.length);
|
||||||
|
const generated_name = generated_name_prefix + default_suffix;
|
||||||
|
log(`Setting name of '${name}' to '${generated_name}'.`);
|
||||||
|
await try_(async () => await user.setNickname(generated_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async try_fetch_member(guild: Guild, user_id: string) {
|
||||||
|
try {
|
||||||
|
return await guild.members.fetch({ user: user_id });
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
//
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +145,12 @@ export class EpitlsBot {
|
||||||
.setName("role")
|
.setName("role")
|
||||||
.setDescription("Role à assigner.")
|
.setDescription("Role à assigner.")
|
||||||
.setRequired(true),
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addBooleanOption(
|
||||||
|
new SlashCommandBooleanOption()
|
||||||
|
.setName("incl_historical")
|
||||||
|
.setDescription("Assigner le rôle aux membres qui ont été membre du groupe, mais qui ne le sont plus.")
|
||||||
|
.setRequired(true),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.addSubcommand(
|
.addSubcommand(
|
||||||
|
@ -185,10 +212,10 @@ export class EpitlsBot {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rules = [] as GuildRule[];
|
const rules = [] as GuildRule[];
|
||||||
for (const { group_id, role_id } of this.rules.roles_for_guild(guild_id)) {
|
for (const { group_id, role_id, include_historical } of this.rules.roles_for_guild(guild_id)) {
|
||||||
const role = await interaction.guild!.roles.fetch(role_id);
|
const role = await interaction.guild!.roles.fetch(role_id);
|
||||||
const role_name = role?.name ?? "<DELETED>";
|
const role_name = role?.name ?? "<DELETED>";
|
||||||
rules.push({ group_id, role_name });
|
rules.push({ group_id, role_name, include_historical });
|
||||||
}
|
}
|
||||||
await interaction.reply(message_command_response_rule_list_success(rules));
|
await interaction.reply(message_command_response_rule_list_success(rules));
|
||||||
}
|
}
|
||||||
|
@ -203,11 +230,12 @@ export class EpitlsBot {
|
||||||
}
|
}
|
||||||
const group_id = interaction.options.getString("group", true);
|
const group_id = interaction.options.getString("group", true);
|
||||||
const role = interaction.options.getRole("role", true);
|
const role = interaction.options.getRole("role", true);
|
||||||
|
const include_historical = interaction.options.getBoolean("incl_historical", true);
|
||||||
// TODO : test if role exists
|
// TODO : test if role exists
|
||||||
const role_id = role.id;
|
const role_id = role.id;
|
||||||
const role_name = role.name;
|
const role_name = role.name;
|
||||||
await this.rules.append_rule(group_id, { guild_id, role_id });
|
await this.rules.append_rule(group_id, { guild_id, role_id }, include_historical);
|
||||||
await interaction.reply(message_command_response_rule_add_success({ group_id, role_name }));
|
await interaction.reply(message_command_response_rule_add_success({ group_id, role_name, include_historical }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handle_command_rule_remove(interaction: ChatInputCommandInteraction<CacheType>) {
|
private async handle_command_rule_remove(interaction: ChatInputCommandInteraction<CacheType>) {
|
||||||
|
@ -221,10 +249,10 @@ export class EpitlsBot {
|
||||||
const group_id = interaction.options.getString("group", true);
|
const group_id = interaction.options.getString("group", true);
|
||||||
const removed = await this.rules.remove_rules(group_id, guild_id);
|
const removed = await this.rules.remove_rules(group_id, guild_id);
|
||||||
const rules = [] as GuildRule[];
|
const rules = [] as GuildRule[];
|
||||||
for (const { group_id, target_role: { role_id } } of removed) {
|
for (const { group_id, target_role: { role_id }, include_historical } of removed) {
|
||||||
const role = await interaction.guild!.roles.fetch(role_id);
|
const role = await interaction.guild!.roles.fetch(role_id);
|
||||||
const role_name = role?.name ?? "<DELETED>";
|
const role_name = role?.name ?? "<DELETED>";
|
||||||
rules.push({ group_id, role_name });
|
rules.push({ group_id, role_name, include_historical });
|
||||||
}
|
}
|
||||||
await interaction.reply(message_command_response_rule_remove_success(rules));
|
await interaction.reply(message_command_response_rule_remove_success(rules));
|
||||||
}
|
}
|
||||||
|
@ -262,13 +290,12 @@ function message_command_response_list_error(msg: string) {
|
||||||
return { embeds: [embed] };
|
return { embeds: [embed] };
|
||||||
}
|
}
|
||||||
|
|
||||||
type GuildRule = { group_id: string; role_name: string };
|
type GuildRule = { group_id: string; role_name: string; include_historical: boolean };
|
||||||
function message_command_response_rule_list_success(rules: GuildRule[]) {
|
function message_command_response_rule_list_success(rules: GuildRule[]) {
|
||||||
const rule_list = rules.map((r) => `- \`${r.group_id}\` → \`@${r.role_name}\``).join("\n");
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle("`📚️`| Liste des règles")
|
.setTitle("`📚️`| Liste des règles")
|
||||||
.setDescription(`Règles d'associations groupe / rôle.
|
.setDescription(`Règles d'associations groupe / rôle.
|
||||||
${rule_list}
|
${rules.map(display_rule).join("\n")}
|
||||||
`);
|
`);
|
||||||
return { embeds: [embed] };
|
return { embeds: [embed] };
|
||||||
}
|
}
|
||||||
|
@ -277,17 +304,28 @@ function message_command_response_rule_add_success(rule: GuildRule) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle("`✅`| Règle ajouté")
|
.setTitle("`✅`| Règle ajouté")
|
||||||
.setDescription(`Règle d'association ajoutée :
|
.setDescription(`Règle d'association ajoutée :
|
||||||
- \`${rule.group_id}\` → \`@${rule.role_name}\`
|
${display_rule(rule)}
|
||||||
`);
|
`);
|
||||||
return { embeds: [embed] };
|
return { embeds: [embed] };
|
||||||
}
|
}
|
||||||
|
|
||||||
function message_command_response_rule_remove_success(rules: GuildRule[]) {
|
function message_command_response_rule_remove_success(rules: GuildRule[]) {
|
||||||
const rule_list = rules.map((r) => `- \`${r.group_id}\` → \`@${r.role_name}\``).join("\n");
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle("`✅`| Règle ajouté")
|
.setTitle("`✅`| Règle ajouté")
|
||||||
.setDescription(`Règle d'association retirées :
|
.setDescription(`Règle d'association retirées :
|
||||||
${rule_list}
|
${rules.map(display_rule).join("\n")}
|
||||||
`);
|
`);
|
||||||
return { embeds: [embed] };
|
return { embeds: [embed] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function to_comparable(input: string) {
|
||||||
|
return input
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize("NFD") // normalize accents
|
||||||
|
.replace(/\p{Diacritic}/gu, ""); // remotes accents
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_rule(rule: GuildRule) {
|
||||||
|
if (rule.include_historical) return `- \`${rule.group_id}\` [H] → \`@${rule.role_name}\``;
|
||||||
|
else return `- \`${rule.group_id}\` → \`@${rule.role_name}\``;
|
||||||
|
}
|
||||||
|
|
25
src/cri.ts
25
src/cri.ts
|
@ -31,11 +31,15 @@ export class CriApi {
|
||||||
current_groups: z.array(group),
|
current_groups: z.array(group),
|
||||||
});
|
});
|
||||||
const parsed = parser.parse(response);
|
const parsed = parser.parse(response);
|
||||||
const result = new Set<string>();
|
const current = new Set<string>();
|
||||||
result.add(parsed.primary_group.slug);
|
const history = new Set<string>();
|
||||||
for (const group of parsed.current_groups) result.add(group.slug);
|
current.add(parsed.primary_group.slug);
|
||||||
for (const { group } of parsed.groups_history) result.add(group.slug);
|
for (const group of parsed.current_groups) current.add(group.slug);
|
||||||
return Array.from(result.values());
|
for (const { group } of parsed.groups_history) history.add(group.slug);
|
||||||
|
return {
|
||||||
|
current: Array.from(current.values()),
|
||||||
|
history: Array.from(history.values()),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,6 +57,17 @@ export class CriApi {
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async get_user_name(login: string) {
|
||||||
|
log(`Fetching username of '${login}'.`);
|
||||||
|
const response = await fetch_json_auth(`https://cri.epita.fr/api/v2/users/${login}/`, this.token);
|
||||||
|
const parser = z.object({
|
||||||
|
first_name: z.string(),
|
||||||
|
last_name: z.string(),
|
||||||
|
});
|
||||||
|
const parsed = parser.parse(response);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
public async get_all_groups() {
|
public async get_all_groups() {
|
||||||
const result = [] as string[];
|
const result = [] as string[];
|
||||||
const response_parser = z.object({
|
const response_parser = z.object({
|
||||||
|
|
14
src/main.ts
14
src/main.ts
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { log_from, read_conf, root_path, SimpleResult } from "./utils.ts";
|
import { log_from, read_conf, root_path, SimpleResult } from "./utils.ts";
|
||||||
import { State } from "./state.ts";
|
import { State } from "./state.ts";
|
||||||
import { RuleSet } from "./rules.ts";
|
import { RuleSet, TargetRole } from "./rules.ts";
|
||||||
import { EpitlsBot } from "./bot.ts";
|
import { EpitlsBot } from "./bot.ts";
|
||||||
import { CriApi } from "./cri.ts";
|
import { CriApi } from "./cri.ts";
|
||||||
import { Emailer } from "./email.ts";
|
import { Emailer } from "./email.ts";
|
||||||
|
@ -86,6 +86,7 @@ class Service {
|
||||||
const promises = [] as Promise<void>[];
|
const promises = [] as Promise<void>[];
|
||||||
for await (const { cri_login, discord_user_id } of this.state.users()) {
|
for await (const { cri_login, discord_user_id } of this.state.users()) {
|
||||||
promises.push(this.update_user_roles(cri_login, discord_user_id));
|
promises.push(this.update_user_roles(cri_login, discord_user_id));
|
||||||
|
promises.push(this.update_user_name(cri_login, discord_user_id));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
@ -93,11 +94,20 @@ class Service {
|
||||||
async update_user_roles(cri_login: string, discord_user_id: string) {
|
async update_user_roles(cri_login: string, discord_user_id: string) {
|
||||||
const groups = await this.cri_api.groups_of(cri_login);
|
const groups = await this.cri_api.groups_of(cri_login);
|
||||||
// log("found groups", groups);
|
// log("found groups", groups);
|
||||||
const roles = groups.map((group) => [...this.rules.roles_for_group(group)]).flat();
|
const roles = [] as TargetRole[];
|
||||||
|
|
||||||
|
for (const group of groups.current) roles.push(...this.rules.roles_for_group(group, false));
|
||||||
|
for (const group of groups.history) roles.push(...this.rules.roles_for_group(group, true));
|
||||||
// log("found setting roles", roles);
|
// 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);
|
for (const { guild_id, role_id } of roles) await this.bot.assign_role(discord_user_id, guild_id, role_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update_user_name(cri_login: string, discord_user_id: string) {
|
||||||
|
const { first_name, last_name } = await this.cri_api.get_user_name(cri_login);
|
||||||
|
const suffix = ` [${first_name} .${last_name[0] ?? ""}]`;
|
||||||
|
await this.bot.update_user_name(discord_user_id, first_name, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
async association_procedure(discord_user_id: string, cri_login: string): Promise<SimpleResult> {
|
async association_procedure(discord_user_id: string, cri_login: string): Promise<SimpleResult> {
|
||||||
try {
|
try {
|
||||||
if (!await this.cri_api.user_exists(cri_login)) return "No such login.";
|
if (!await this.cri_api.user_exists(cri_login)) return "No such login.";
|
||||||
|
|
21
src/rules.ts
21
src/rules.ts
|
@ -25,7 +25,9 @@ export class RuleSet {
|
||||||
const result = new RuleSet(path);
|
const result = new RuleSet(path);
|
||||||
const file_content = await Deno.readTextFile(path);
|
const file_content = await Deno.readTextFile(path);
|
||||||
const parsed = parse_rules(file_content);
|
const parsed = parse_rules(file_content);
|
||||||
for (const { group_id, target_role } of parsed) await result.append_rule(group_id, target_role);
|
for (const { group_id, target_role, include_historical } of parsed) {
|
||||||
|
await result.append_rule(group_id, target_role, include_historical);
|
||||||
|
}
|
||||||
result.update_channel = channel();
|
result.update_channel = channel();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -33,13 +35,17 @@ export class RuleSet {
|
||||||
/**
|
/**
|
||||||
* Gets which Discord roles must be assigned to a user which is part of a given CRI group.
|
* 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, historical: boolean) {
|
||||||
for (const rule of this.rules) if (rule.group_id === group_id) yield rule.target_role;
|
for (const rule of this.rules) {
|
||||||
|
if (rule.group_id !== group_id) continue;
|
||||||
|
if (!rule.include_historical && historical) continue;
|
||||||
|
yield rule.target_role;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public *roles_for_guild(target_guild_id: string) {
|
public *roles_for_guild(target_guild_id: string) {
|
||||||
for (const { group_id, target_role: { guild_id, role_id } } of this.rules) {
|
for (const { group_id, target_role: { guild_id, role_id }, include_historical } of this.rules) {
|
||||||
if (target_guild_id === guild_id) yield { group_id, role_id };
|
if (target_guild_id === guild_id) yield { group_id, role_id, include_historical };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,14 +56,14 @@ export class RuleSet {
|
||||||
return this.rules.length;
|
return this.rules.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async append_rule(group_id: string, target_role: TargetRole) {
|
public async append_rule(group_id: string, target_role: TargetRole, include_historical: boolean) {
|
||||||
for (const r of this.rules) {
|
for (const r of this.rules) {
|
||||||
const same_group = r.group_id === group_id;
|
const same_group = r.group_id === group_id;
|
||||||
const same_target_role = r.target_role.guild_id === target_role.guild_id &&
|
const same_target_role = r.target_role.guild_id === target_role.guild_id &&
|
||||||
r.target_role.role_id === target_role.role_id;
|
r.target_role.role_id === target_role.role_id;
|
||||||
if (same_group && same_target_role) return;
|
if (same_group && same_target_role) return;
|
||||||
}
|
}
|
||||||
this.rules.push({ group_id, target_role });
|
this.rules.push({ group_id, target_role, include_historical });
|
||||||
await this.save();
|
await this.save();
|
||||||
this.update_channel.send({ updated_groups: [group_id] });
|
this.update_channel.send({ updated_groups: [group_id] });
|
||||||
}
|
}
|
||||||
|
@ -96,5 +102,6 @@ function parse_rules(content: string) {
|
||||||
return z.array(z.object({
|
return z.array(z.object({
|
||||||
group_id: z.string(),
|
group_id: z.string(),
|
||||||
target_role: z.object({ guild_id: z.string(), role_id: z.string() }),
|
target_role: z.object({ guild_id: z.string(), role_id: z.string() }),
|
||||||
|
include_historical: z.boolean().default(false),
|
||||||
})).parse(JSON.parse(content));
|
})).parse(JSON.parse(content));
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,3 +122,11 @@ export function* reverse<T>(items: Iterable<T>) {
|
||||||
const collected = [...items];
|
const collected = [...items];
|
||||||
for (const item of collected.reverse()) yield item;
|
for (const item of collected.reverse()) yield item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function try_<T>(f: () => Promise<T> | T) {
|
||||||
|
try {
|
||||||
|
return await f();
|
||||||
|
} catch (_) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue