Skip to content

Commit

Permalink
Merge branch 'feature-licence' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
BodomBeach committed May 29, 2024
2 parents bf7d84b + c6a8b36 commit b3ba576
Show file tree
Hide file tree
Showing 14 changed files with 1,099 additions and 15 deletions.
827 changes: 820 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"dependencies": {
"@discordjs/builders": "^1.8.1",
"@discordjs/rest": "^2.3.0",
"axios": "^1.7.2",
"discord-api-types": "^0.37.84",
"discord.js": "^14.15.2",
"dotenv": "^16.3.1",
"path": "^0.12.7",
"puppeteer": "^15.5.0"
"puppeteer": "^15.5.0",
"sqlite3": "^5.1.7"
},
"imports": {
"#events/*": "./events/*",
Expand Down
7 changes: 4 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Projet bénévole porté par les membres du club.

## Usage
### Commandes disponibles
- `/help` - affiche un guide d'utilisation pour les nouveaux membres
- `/licence` - permet au membre d'activer sa licence FFVL sur discord, lui donnant accès à tous les salons pour l'année en cours (gain du rôle "Licencié")
- `/archive` - déplace le salon dans le bon dossier 📁ARCHIVES. Créé un nouveau dossier 📁ARCHIVES si besoin. Uniquement [Sorties/Événements/Compétitions]
- `/balise` - affiche les dernières valeurs et directions de vent autour de Grenoble. Source : [murblanc.org/sthil](https://murblanc.org/sthil)

Expand All @@ -15,13 +17,12 @@ Projet bénévole porté par les membres du club.

## Todo

- Fonction qui manipule les rôles des membres pour afficher certains salons uniquement aux membres ayant cotisé au club pour l'année en cours
- Commande `live` pour mettre de suivre les canards crosseurs sur une journée
- Améliorer la commande `/archive` pour qu'elle transforme le salon en thread dans un unique salon `archives`, nous permettant alors de garder l'historique des sorties ad vitam aeternam (@Romain.L ?)
- Commande `/notam` pour afficher toutes les NOTAM en cours entre 2 points GPS
- ~~Fonction de covoiturage~~ -> abandonné, trop compliqué à mettre en oeuvre
- Améliorer la commande `/archive` pour qu'elle transforme le salon en thread dans un unique salon `archives`, nous permettant alors de garder l'historique des sorties ad vitam aeternam (@Romain.L ?)
- Ajouter un webhook qui notifie la création d'un nouvel article sur le site du Duck
- Ajouter un salon `creation-sorties` où seul le bot peut écrire, pour les membres qui souhaitent recevoir une notification lorsqu'une nouvelle sortie est proposée
- ~~Fonction de covoiturage~~ -> abandonné, trop compliqué à mettre en oeuvre

## Installation

Expand Down
3 changes: 1 addition & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ const fs = require('fs');
const { Client, GatewayIntentBits } = require('discord.js');

// Create a new Client with the Guilds intent
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages] });

// Loading all events
const events = fs
.readdirSync('src/events')
.filter((file) => file.endsWith('.js'));

for (let event of events) {

const eventFile = require(`./events/${event}`);

if (eventFile.once)
Expand Down
8 changes: 8 additions & 0 deletions src/events/commands/archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ const { archive } = require('../../utils/archive.js');

// Creates an Object in JSON with the data required by Discord's API to create a SlashCommand
const create = () => {
<<<<<<< HEAD
const command = new SlashCommandBuilder()
.setName('archive')
.setDescription('Archive ce salon')
return command.toJSON();
};
=======
const command = new SlashCommandBuilder()
.setName('archive')
.setDescription('Archive ce salon (fonctionne uniquement sur les salons sorties/compétitions/événements)')
return command.toJSON();
};
>>>>>>> feature-licence

// Called by the interactionCreate event listener when the corresponding command is invoked
const invoke = async (interaction) => {
Expand Down
17 changes: 17 additions & 0 deletions src/events/commands/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { HelpMessage } = require('../../utils/helpMessage.js');

// Creates an Object in JSON with the data required by Discord's API to create a SlashCommand
const create = () => {
const command = new SlashCommandBuilder()
.setName('help')
.setDescription('Affiche le guide d\`utilisation du discord')
return command.toJSON();
};

const invoke = async (interaction) => {
const message = await new HelpMessage(interaction.guild).execute()
await interaction.reply({ content: message , ephemeral: true })
};

module.exports = { create, invoke };
97 changes: 97 additions & 0 deletions src/events/commands/licence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { HelpMessage } = require('../../utils/helpMessage.js');
const axios = require('axios');
const sqlite3 = require('sqlite3').verbose();

// Creates an Object in JSON with the data required by Discord's API to create a SlashCommand
const create = () => {
const command = new SlashCommandBuilder()
.setName('licence')
.setDescription('Active ta licence FFVL pour obtenir l\'accès complet au Discord')
.addStringOption(option =>
option.setName('numero_licence')
.setDescription('Ton numéro de licence FFVL (https://intranet.ffvl.fr)')
.setRequired(true));
return command.toJSON();
};

const invoke = async (interaction) => {
await interaction.deferReply({ ephemeral: true });

const db = new sqlite3.Database('db.sqlite')

// params
const username = interaction.user.username
const licenseNumber = interaction.options.getString('numero_licence')
const currentYear = new Date().getFullYear()
const structureId = process.env.STRUCTURE_ID

console.log(`${interaction.user.username} used /licence ${licenseNumber}`);

// Check if user already has a licence activated for current year
const alreadyActivated = await asyncGet(db, 'SELECT * FROM licenses WHERE username = ? AND year = ?', [username, currentYear])
if (alreadyActivated) {
await interaction.editReply(`Ton compte Discord est déjà associé à la licence **${alreadyActivated.license_number}**. En cas de problème, tu peux contacter un admin.`);
return
}

// Check if licence number is already taken by someone else
const licenseTaken = await asyncGet(db, 'SELECT * FROM licenses WHERE license_number = ?', [licenseNumber])
if (licenseTaken) {
await interaction.editReply(`La licence **${licenseNumber}** est déjà associée à un autre utilisateur. En cas de problème, tu peux contacter un admin.`);
return
}
console.log(`https://data.ffvl.fr/php/verif_lic_adh.php?num=${licenseNumber}&stru=${structureId}`);
const response = await axios.get(`https://data.ffvl.fr/php/verif_lic_adh.php?num=${licenseNumber}&stru=${structureId}`)
console.log('FFVL response', response);
if (response.data == 1) {

interaction.member.roles.add(interaction.guild.roles.cache.find(role => role.name == 'Licencié ' + currentYear))
// Insert row into db
db.run(`INSERT INTO licenses(username, license_number, year) VALUES(?, ?, ?);`, [username, licenseNumber, currentYear], function (err) {
if (err) { console.log(err.message); }
console.log(`License succesfully activated for user ${username}`);
});

await interaction.editReply({ content: successMessage(currentYear), ephemeral: true })
const helpMessage = await new HelpMessage(interaction.guild).execute()
await interaction.followUp({ content: helpMessage, ephemeral: true });

} else {
console.log(`License not found ${username}`);
await interaction.editReply({ content: failureMessage(currentYear), ephemeral: true })
}

};

const successMessage = (year) => {
return `
Bien joué, ton numéro de licence a bien été activé :partying_face:
Tu as désormais le rôle **Licencié ${year}** et tu a accès à tous les salons :duck:
Voici quelques astuces pour t'aider à t'y retrouver dans le discord.
`
}

const failureMessage = (year) => {
return `
Une erreur est survenue avec ce numéro de licence :thinking:
Soit ce numéro de licence n'existe pas à la FFVL
Soit le numéro existe mais la cotisation au Duck n'a pas été enregistrée pour l'année ${year}
En cas de problème, tu peux contacter un admin.
`
}

const asyncGet = (db, sql, params) => {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
}

module.exports = { create, invoke };
18 changes: 18 additions & 0 deletions src/events/messageCreate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const once = false;
const name = 'messageCreate';

async function invoke(interaction) {

const welcomeChannel = interaction.guild.channels.cache.find(channel => channel.name === 'bienvenue-et-regles');

// Ignore messages from bots
if (interaction.author.bot) return;

// Delete any message that is not a /command in the welcome channel. This is the only way to allow only commands in this channel
if (interaction.channel === welcomeChannel) {
await interaction.delete();
}
return;
}

module.exports = { once, name, invoke };
4 changes: 4 additions & 0 deletions src/events/ready.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ const fs = require('fs');
const { serverStats } = require('../jobs/serverStats.js');
const { channelCleanup } = require('../jobs/channelCleanup.js');
const registerCommands = require('../utils/registerCommands.js');
const {initMessages} = require('../utils/initMessages.js')
require('../utils/initDb.js')

const once = true;
const name = 'ready';

async function invoke(client) {

initMessages(client);

// start regular jobs
setInterval(() => { channelCleanup(client) }, process.env.CHANNEL_CLEANUP_INTERVAL || 3600000); // every hour
setInterval(() => { serverStats(client) }, process.env.STATS_INTERVAL || 600000); // every 10 min
Expand Down
3 changes: 1 addition & 2 deletions src/jobs/channelCleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const { archive } = require('../utils/archive.js');

function channelCleanup(client) {


console.log('===== starting scheduled channel cleanup =====');
const allowedCategories = ['🪂 SORTIES', '🏃Sorties pas rapente'];
const guild = client.guilds.cache.get(process.env.GUILD_ID);
Expand All @@ -12,7 +11,7 @@ function channelCleanup(client) {

// do nothing for these format for now, too risky
const forbidden1 = /(\d{1,2})-(\d{1,2})-(\d{1,2})/ // 04-05-06
const forbidden2 = /-(janvier|fevrier|mars|avril|mai|juin|juillet|septembre|octobre|novembre|decembre)-/ // 04-05-juin-word
const forbidden2 = /-(janvier|fevrier|mars|avril|mai|juin|juillet|septembre|octobre|novembre|decembre)-/ // 04-05-juin

categories.forEach(category => {
const today = new Date()
Expand Down
43 changes: 43 additions & 0 deletions src/utils/helpMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class HelpMessage {

constructor(guild) {
this.guild = guild
}

execute = async () => {

const regleChannel = await this.parseChannel('📌règles-sorties')
const spontChannel = await this.parseChannel('🍀sorties-spontanées')
const blablaChannel = await this.parseChannel('👥bla-bla-parapente')
const orgaChannel = await this.parseChannel('🔧organisation-du-serveur')
const devbotChannel = await this.parseChannel('🤖dév-bots')

return `
:duck: **__Guide d'utilisation du discord__ ** :duck:
- Tu veux planifier une sortie future, c'est par ici ${regleChannel}
- Tu décides d'aller voler au dernier moment (le jour même), pas besoin de créer un salon dédié, il suffit de poster un message dans ${spontChannel}
- Tu veux discuter ou poser une question sur un sujet spécifique ? Il existe surement un salon qui correspond dans la catégorie **🐤 GENERAL**
- Dans **⛰ SITES DE VOL**, nous mettons à jour les informations importantes de chaque site, n'hésite pas à consulter ces salons lorsque tu prépares tes vols.
- Tu ne trouves pas le bon salon ? Tu peux toujours parler dans ${blablaChannel}, où le spam est autorisé !
- Tu reçois trop de notifications ? Discord possède plein d'options pour régler les notifications comme il te plait, ce guide pourra t'aider : <placeholder>
- Tu peux inviter des amis au discord sans restriction.
- Envie d'aider à l'amélioration du serveur, rejoins ${orgaChannel}
- Envie de contribuer à l'amélioration du bot, rejoins ${devbotChannel}
- \`Ctrl + /\` affiche la liste des raccourcis utiles (ordinateur uniquement)
À tout moment, tu peux utiliser la commande **\`/help\`** pour retrouver ce guide.
`
}

parseChannel = async (string) => {
const channel = this.guild.channels.cache.find(channel => channel.name === string)

// fallback to string if channel is not found
return channel ? `<#${channel.id}>` : '#' + string;
}

}
module.exports = { HelpMessage }

25 changes: 25 additions & 0 deletions src/utils/initDb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const sqlite3 = require('sqlite3').verbose();

const db = new sqlite3.Database('db.sqlite', (err) => {
if (err) {
console.error("Error opening database:", err.message);
return;
}

db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS licenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
license_number TEXT UNIQUE NOT NULL,
year INTEGER NOT NULL
)
`, (err) => {
if (err) {
console.error("Error creating table:", err.message);
}
});
});
});

db.close();
38 changes: 38 additions & 0 deletions src/utils/initMessages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
async function initMessages(client) {

const guild = client.guilds.cache.get(process.env.GUILD_ID);
welcomeMessage = `
**Bienvenue sur le serveur discord du Duck :duck:**
Ce serveur discord est là pour nous permettre de planifier et d'organiser des sorties, événements club et compétitions.
C'est aussi un endroit où l'on peut discuter de tous les sujets qui concernent le club, le parapente et plus encore !
Avant toute chose, nous t'invitons à compléter les 2 étapes ci-dessous pour profiter pleinement du discord.
**__1 - Configure ton Prénom NOM__**
Pour des raisons de sécurité lors des sorties club et pour assurer une bonne communication entre les membres, il faut renseigner ton nom et prénom usuels. Si tu as plusieurs serveurs discord, ne t'inquiète pas, ce nouveau **Prénom NOM** sera visible uniquement sur le discord du Duck et tu garderas ton pseudonyme sur les autres serveurs.
Voilà comment faire :
- Option 1 :arrow_forward: Tape la commande **\`/nick\` \`<Prénom>\` \`<NOM>\`** dans le champ de texte en bas de cette page.
- Option 2 :arrow_forward: https://br.atsit.in/fr/?p=12704
**__2 - Active ta licence__**
Pour avoir accès à tous les salons, tu dois avoir cotisé au Duck pour l'année en cours et activer ta licence FFVL sur discord.
Pour cela, il suffit de taper la commande **\`/licence\`** suivi de ton [numéro de licence FFVL](https://intranet.ffvl.fr) :
Exemple : **\`/licence\` \`0315897E\`**
Notre bot vérifiera que ta cotisation est à jour et te donnera accès au reste des salons !
Si tu rencontres un problème, n'hésite pas à contacter un admin du serveur.
`
const welcomeChannel = client.channels.cache.find(channel => channel.name === 'bienvenue-et-regles');
let messages = await welcomeChannel.messages.fetch({ limit: 1 });

// Create or edit existing embed
if (messages.size === 0) {
welcomeChannel.send(welcomeMessage);
} else {
messages.first().edit(welcomeMessage);
};
console.log('initialized welcome message');
}

module.exports = { initMessages }
20 changes: 20 additions & 0 deletions src/utils/updatePermissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This service update permissions on all channels programatically (doing it manually is time consuming and error prone)
// Basically, it just hide most channels to role "@everyone" (except a few channels) whereas all other roles can see everything

function updatePermissions(client) {
const allowedChannels = ['👥bla-bla-parapente', 'bienvenue'];

const guild = client.guilds.cache.get(process.env.GUILD_ID);
const channels = guild.channels.cache.filter(channel => !allowedChannels.includes(channel.name))
// Fetch all channels in the guild

channels.forEach(channel => {
// Update the permissions for the @everyone role in each channel
channel.permissionOverwrites.create(guild.roles.everyone, { ViewChannel: false });
console.log(1);
});

console.log('Updated permissions');
}

module.exports = {updatePermissions}

0 comments on commit b3ba576

Please sign in to comment.