diff --git a/.eslintrc.js b/.eslintrc.js index 6dabd63..88bb4b1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,6 +58,7 @@ module.exports = { __SERVER__: true, __DEV__: true, __LOCAL__: false, + __REGISTER_DISCORD_COMMANDS__: false, __TEST__: false, }, } diff --git a/jest/config.js b/jest/config.js index f907412..23e308f 100644 --- a/jest/config.js +++ b/jest/config.js @@ -22,6 +22,7 @@ module.exports = { __CLIENT__: true, __SERVER__: false, __LOCAL__: false, + __REGISTER_DISCORD_COMMANDS__: false, __TEST__: true, }, maxConcurrency: 50, diff --git a/package.json b/package.json index 5b6c268..82a34bb 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "ser": "yarn dev:build && node ./public/server", "dev:build": "cross-env NODE_ENV=development webpack --config ./webpack/server.config.ts", "local-start": "cross-env LOCAL=true yarn build && node ./public/server", + "discord-register": "cross-env REGISTER_DISCORD_COMMANDS=true yarn build && node ./public/server", "start": "node ./public/server", "build": "run-s build:*", "build:server": "cross-env NODE_ENV=production webpack --config ./webpack/server.config.ts", @@ -94,6 +95,7 @@ "core-js": "^3.15.2", "cross-env": "^7.0.3", "detect-node": "^2.1.0", + "discord.js": "^14.7.1", "express": "^4.17.1", "fs": "^0.0.1-security", "google-auth-library": "^7.10.1", diff --git a/public/favicon.ico b/public/favicon.ico old mode 100644 new mode 100755 index c2c86b8..3f747cb Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/components/Asks/index.tsx b/src/components/Asks/index.tsx index e343c08..f1f7f39 100644 --- a/src/components/Asks/index.tsx +++ b/src/components/Asks/index.tsx @@ -10,7 +10,7 @@ import { AskDayWishes, fetchFor as fetchForDayWishes } from "./AskDayWishes" // import { AskHosting, fetchFor as fetchForHosting } from "./AskHosting" // import { AskMeals, fetchFor as fetchForMeals } from "./AskMeals" // import { AskPersonalInfo, fetchFor as fetchForPersonalInfo } from "./AskPersonalInfo" -// import { AskTeamWishes, fetchFor as fetchForTeamWishes } from "./AskTeamWishes" +import { AskTeamWishes, fetchFor as fetchForTeamWishes } from "./AskTeamWishes" // import { // AskParticipationDetails, // fetchFor as fetchForParticipationDetails, @@ -27,7 +27,7 @@ const Asks = (): JSX.Element | null => { AskDiscord(asks, 5) AskDayWishes(asks, 10) - // AskTeamWishes(asks, 11) + AskTeamWishes(asks, 11) // AskParticipationDetails(asks, 12) // AskPersonalInfo(asks, 15) // AskHosting(asks, 20) @@ -72,7 +72,7 @@ export const fetchFor = [ ...fetchForDayWishes, // ...fetchForHosting, // ...fetchForMeals, - // ...fetchForTeamWishes, + ...fetchForTeamWishes, // ...fetchForParticipationDetails, // ...fetchForPersonalInfo, ] diff --git a/src/components/LoginForm/index.tsx b/src/components/LoginForm/index.tsx index 8b53bbf..36f8597 100644 --- a/src/components/LoginForm/index.tsx +++ b/src/components/LoginForm/index.tsx @@ -29,7 +29,7 @@ const LoginForm = (): JSX.Element => { return (
- Si tu es bénévole, connecte-toi pour accéder à ton espace. + Si tu es bénévole ou que tu l'as déjà été, connecte-toi pour accéder à ton espace.
diff --git a/src/components/Teams/TeamList.tsx b/src/components/Teams/TeamList.tsx index cf4ab3a..790d037 100644 --- a/src/components/Teams/TeamList.tsx +++ b/src/components/Teams/TeamList.tsx @@ -2,10 +2,10 @@ import React, { memo } from "react" import { useSelector } from "react-redux" import styles from "./styles.module.scss" import TeamItem from "./TeamItem" -import { selectSortedActiveTeams } from "../../store/teamList" +import { selectSortedTeams } from "../../store/teamList" const TeamList: React.FC = (): JSX.Element | null => { - const teams = useSelector(selectSortedActiveTeams) + const teams = useSelector(selectSortedTeams) if (!teams || teams.length === 0) return null return ( diff --git a/src/components/VolunteerBoard/Board.tsx b/src/components/VolunteerBoard/Board.tsx index c9103f4..da4bd87 100644 --- a/src/components/VolunteerBoard/Board.tsx +++ b/src/components/VolunteerBoard/Board.tsx @@ -7,8 +7,8 @@ import DayWishesFormModal from "./DayWishesForm/DayWishesFormModal" // import MealsFormModal from "./MealsForm/MealsFormModal" // import ParticipationDetails from "./ParticipationDetails/ParticipationDetails" // import ParticipationDetailsFormModal from "./ParticipationDetailsForm/ParticipationDetailsFormModal" -// import TeamWishes from "./TeamWishes/TeamWishes" -// import TeamWishesFormModal from "./TeamWishesForm/TeamWishesFormModal" +import TeamWishes from "./TeamWishes/TeamWishes" +import TeamWishesFormModal from "./TeamWishesForm/TeamWishesFormModal" // import VolunteerTeam from "./VolunteerTeam/VolunteerTeam" import withUserConnected from "../../utils/withUserConnected" import ContentTitle from "../ui/Content/ContentTitle" @@ -16,7 +16,7 @@ import { fetchFor as fetchForDayWishesForm } from "./DayWishesForm/DayWishesForm // import { fetchFor as fetchForHostingForm } from "./HostingForm/HostingForm" // import { fetchFor as fetchForMealsForm } from "./MealsForm/MealsForm" // import { fetchFor as fetchForParticipationDetailsForm } from "./ParticipationDetailsForm/ParticipationDetailsForm" -// import { fetchFor as fetchForTeamWishesForm } from "./TeamWishesForm/TeamWishesForm" +import { fetchFor as fetchForTeamWishesForm } from "./TeamWishesForm/TeamWishesForm" import { fetchFor as fetchForPersonalInfoForm } from "./PersonalInfoForm/PersonalInfoForm" import PersonalInfo from "./PersonalInfo/PersonalInfo" import PersonalInfoFormModal from "./PersonalInfoForm/PersonalInfoFormModal" @@ -43,10 +43,10 @@ const Board: FC = (): JSX.Element => ( {/* - + */} - + {/* @@ -66,5 +66,5 @@ export const fetchFor = [ // ...fetchForHostingForm, // ...fetchForMealsForm, // ...fetchForParticipationDetailsForm, - // ...fetchForTeamWishesForm, + ...fetchForTeamWishesForm, ] diff --git a/src/components/VolunteerBoard/DayWishes/DayWishes.tsx b/src/components/VolunteerBoard/DayWishes/DayWishes.tsx index 041beec..eeeb082 100644 --- a/src/components/VolunteerBoard/DayWishes/DayWishes.tsx +++ b/src/components/VolunteerBoard/DayWishes/DayWishes.tsx @@ -38,6 +38,12 @@ const DayWishes: FC = (): JSX.Element | null => { Je ne sais pas encore si je participerai à PeL 2023
)} + {participation === "à distance" && ( +
+ Je participerai à PeL 2023 ! Sans y + être pendant le weekend. +
+ )} {participation === "inconnu" && (
Participation à PeL 2023{" "} diff --git a/src/config/default.ts b/src/config/default.ts index 5dc2c59..3b4d494 100755 --- a/src/config/default.ts +++ b/src/config/default.ts @@ -16,4 +16,5 @@ export default { ], }, DEV_JWT_SECRET: "fakeqA6uF#msq2312bebf2FLFn4XzWQ6dttXSJwBX#?gL2JWf!", + DEV_DISCORD_TOKEN: "fakeqA6uF#msq2312bebf2FLFn4XzWQ6dttXSJwBX#?gL2JWf!", } diff --git a/src/routes/index.ts b/src/routes/index.ts index 13395e2..309a7b2 100755 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -11,7 +11,7 @@ import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../page import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register" import AsyncKnowledge, { loadData as loadKnowledgeData } from "../pages/Knowledge" // import AsyncLoans, { loadData as loadLoansData } from "../pages/Loans" -// import AsyncLoaning, { loadData as loadLoaningData } from "../pages/Loaning" +import AsyncLoaning, { loadData as loadLoaningData } from "../pages/Loaning" import AsyncKnowledgeCards, { loadData as loadCardKnowledgeData } from "../pages/KnowledgeCards" import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams" import AsyncBoard, { loadData as loadBoardData } from "../pages/Board" @@ -51,11 +51,11 @@ export default [ // component: AsyncLoans, // loadData: loadLoansData, // }, - // { - // path: "/emprunter", - // component: AsyncLoaning, - // loadData: loadLoaningData, - // }, + { + path: "/emprunter", + component: AsyncLoaning, + loadData: loadLoaningData, + }, { path: "/fiches", component: AsyncKnowledgeCards, diff --git a/src/server/discordBot.ts b/src/server/discordBot.ts new file mode 100644 index 0000000..d57f7a3 --- /dev/null +++ b/src/server/discordBot.ts @@ -0,0 +1,405 @@ +import _ from "lodash" +import { + Client, + GatewayIntentBits, + Collection, + Events, + SlashCommandBuilder, + CommandInteraction, + /* REST, Routes, */ Partials, + MessageReaction, + PartialMessageReaction, + Guild, + User, + PartialUser, +} from "discord.js" +import { promises as fs, constants } from "fs" +import path from "path" + +import { translationVolunteer, Volunteer, VolunteerWithoutId } from "../services/volunteers" +import { + translationDiscordRoles, + DiscordRole, + DiscordRoleWithoutId, +} from "../services/discordRoles" +import { getSheet } from "./gsheets/accessors" +import config from "../config" + +let cachedToken: string +// let cachedClientId: string +let cachedGuildId: string +const CREDS_PATH = path.resolve(process.cwd(), "access/discordToken.json") +getCreds() // Necessary until we can make async express middleware + +type Command = { + data: SlashCommandBuilder + execute: (interaction: CommandInteraction) => Promise +} +const commands: Collection = new Collection() + +// const userCommand: Command = { +// data: new SlashCommandBuilder() +// .setName('user') +// .setDescription('Provides information about the user.'), +// async execute(interaction: CommandInteraction) { + +// const { commandName } = interaction + +// if (commandName === 'user') { +// const message = await interaction.reply({ content: 'You can react with Unicode emojis!', fetchReply: true }) +// message.react('😄') +// } +// }, +// } + +let hasDiscordAccessReturn: boolean | undefined +export async function hasDiscordAccess(): Promise { + if (hasDiscordAccessReturn !== undefined) { + return hasDiscordAccessReturn + } + try { + // eslint-disable-next-line no-bitwise + await fs.access(CREDS_PATH, constants.R_OK | constants.W_OK) + hasDiscordAccessReturn = true + } catch { + hasDiscordAccessReturn = false + } + return hasDiscordAccessReturn +} + +// export async function discordRegisterCommands(): Promise { +// if (!__REGISTER_DISCORD_COMMANDS__) { +// return +// } + +// if (!(await hasDiscordAccess())) { +// console.error(`Discord bot: no creds found, not running bot`) +// return +// } + +// await getCreds() + +// const commandsToRegister = [] +// commandsToRegister.push(userCommand.data.toJSON()) + +// const rest = new REST({ version: '10' }).setToken(cachedToken) + +// try { +// await rest.put( +// Routes.applicationGuildCommands(cachedClientId, cachedGuildId), +// { body: commandsToRegister }, +// ) +// } catch (error) { +// console.error(error) +// } + +// process.exit() +// } + +export async function discordBot(): Promise { + try { + if (__REGISTER_DISCORD_COMMANDS__) { + return + } + + if (!(await hasDiscordAccess())) { + console.error(`Discord bot: no creds found, not running bot`) + return + } + await getCreds() + + const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.MessageContent, + ], + partials: [Partials.Message, Partials.Channel, Partials.Reaction], + }) + + // commands.set(userCommand.data.name, userCommand) + + client.once(Events.ClientReady, (localClient) => { + setInterval(() => setBotReactions(localClient), 5 * 60 * 1000) + setTimeout(() => setBotReactions(localClient), 20 * 1000) + + setInterval(() => setAllRoles(localClient), 5 * 60 * 1000) + setTimeout(() => setAllRoles(localClient), 5 * 1000) + }) + + client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return + + const command = commands.get(interaction.commandName) + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`) + return + } + + try { + await command.execute(interaction) + } catch (error) { + console.error(error) + } + }) + + client.on(Events.MessageReactionAdd, async (reaction, user) => { + await fetchPartial(reaction) + + await setRolesFromEmoji(client, user, reaction, "add") + }) + + client.on(Events.MessageReactionRemove, async (reaction, user) => { + await fetchPartial(reaction) + + await setRolesFromEmoji(client, user, reaction, "remove") + }) + + client.login(cachedToken) + } catch (error) { + console.error("Discord error", error) + } +} + +async function setBotReactions(client: Client) { + try { + const discordRolesSheet = await getSheet( + "DiscordRoles", + new DiscordRole(), + translationDiscordRoles + ) + const discordRolesList = await discordRolesSheet.getList() + if (!discordRolesList) { + return + } + + client.channels.cache.each(async (channel) => { + if (!channel.isTextBased()) { + return + } + + discordRolesList.forEach(async (discordRole: DiscordRole) => { + let message + try { + message = await channel.messages.fetch(discordRole.messageId) + } catch (error) { + return + } + + const reaction = message.reactions.cache.find( + (r) => r.emoji.name === discordRole.emoji + ) + if (reaction) { + return + } + await message.react(discordRole.emoji) + }) + }) + } catch (error) { + console.error("Error in setBotReactions", error) + } +} + +async function setAllRoles(client: Client) { + try { + const volunteerSheet = await getSheet( + "Volunteers", + new Volunteer(), + translationVolunteer + ) + const volunteerList = await volunteerSheet.getList() + if (!volunteerList) { + return + } + + const volunteerByDiscordId = _.mapKeys(volunteerList, (v) => v.discordId.toString()) + + const guild = await client.guilds.fetch(cachedGuildId) + + if (!guild || !guild.members.cache) { + return + } + + // Set (maybe) volunteer roles + + const volunteerRoleIds: { [key: string]: string } = _.mapValues( + { + oui: "Bénévole", + "peut-etre": "Bénévole incertain", + "à distance": "Bénévole à distance", + non: "", + }, + (v) => (_.isEmpty(v) ? "" : guild.roles.cache.find((role) => role.name === v)?.id || "") + ) + + await setVolunteersRoles( + guild, + volunteerByDiscordId, + volunteerRoleIds, + (volunteer: Volunteer) => volunteer.active + ) + + // Set Team- and Référent- roles + + const teamIds = { + "1": "accueil", + "5": "paillante", + "21": "photo", + "4": "tournois", + // "19": "exposants-asso", ignored because it's mixed with volunteers of last edition + "2": "jav", + "18": "jeux-xl", + "27": "ring", + "8": "coindespetitsjoueurs", + "23": "jeuxdelires", + "3": "jdd", + "9": "figurines", + "10": "jdr", + "28": "jeuxhistoire", + "6": "évènements", + "29": "presse", + "30": "initiation-18xx", + "0": "", + } + + const teamRoleIds: { [key: string]: string } = _.mapValues(teamIds, (v) => + _.isEmpty(v) + ? "" + : guild.roles.cache.find((role) => role.name === `Team-${v}`)?.id || "" + ) + + await setVolunteersRoles(guild, volunteerByDiscordId, teamRoleIds, (volunteer: Volunteer) => + _.includes(["oui", "peut-etre", "à distance"], volunteer.active) + ? `${volunteer.team}` + : "" + ) + + const referentRoleIds: { [key: string]: string } = _.mapValues(teamIds, (v) => + _.isEmpty(v) + ? "" + : guild.roles.cache.find((role) => role.name === `Référent-${v}`)?.id || "" + ) + + await setVolunteersRoles( + guild, + volunteerByDiscordId, + referentRoleIds, + (volunteer: Volunteer) => + _.includes(["oui", "peut-etre", "à distance"], volunteer.active) && + volunteer.roles.includes("référent") + ? `${volunteer.team}` + : "0" + ) + } catch (error) { + console.error("Error in setAllRoles", error) + } +} + +async function setVolunteersRoles( + guild: Guild, + volunteerByDiscordId: { [key: string]: Volunteer }, + volunteerRoleIds: { [key: string]: string }, + funcKey: (volunteer: Volunteer) => string +) { + const members = await guild.members.fetch() + members.each(async (member) => { + const volunteer = volunteerByDiscordId[member.id] + if (!volunteer) { + return + } + + _.forOwn(volunteerRoleIds, (roleId, active) => { + if (!roleId) { + return + } + + const shouldHaveRole = active === funcKey(volunteer) + const hasRole = member.roles.cache.has(roleId) + + if (hasRole && !shouldHaveRole) { + member.roles.remove(roleId) + } else if (!hasRole && shouldHaveRole) { + member.roles.add(roleId) + } + }) + }) +} + +async function setRolesFromEmoji( + client: Client, + user: User | PartialUser, + reaction: MessageReaction | PartialMessageReaction, + action: "add" | "remove" +) { + const discordRolesSheet = await getSheet( + "DiscordRoles", + new DiscordRole(), + translationDiscordRoles + ) + const discordRolesList = await discordRolesSheet.getList() + if (!discordRolesList) { + return + } + + await client.guilds.fetch() + const guild = client.guilds.resolve(cachedGuildId) + + if (!guild || !guild.members.cache) { + return + } + + discordRolesList.forEach(async (discordRole: DiscordRole) => { + if ( + reaction.message.id === discordRole.messageId && + reaction.emoji.name === discordRole.emoji + ) { + const roleId = guild.roles.cache.find((role) => role.name === discordRole.role) + if (!roleId) { + return + } + + const member = guild.members.cache.find((m) => m.id === user.id) + if (!member) { + return + } + await member.fetch() + if (action === "add") { + member.roles.add(roleId) + } else if (action === "remove") { + member.roles.remove(roleId) + } + } + }) +} + +async function fetchPartial(reaction: MessageReaction | PartialMessageReaction): Promise { + if (reaction.partial) { + try { + await reaction.fetch() + } catch (error) { + console.error("Something went wrong when fetching the message", error) + return false + } + } + return true +} + +async function getCreds(): Promise { + if (!cachedToken) { + try { + const credsContent = await fs.readFile(CREDS_PATH) + const parsedCreds = credsContent && JSON.parse(credsContent.toString()) + if (!parsedCreds) { + return + } + cachedToken = parsedCreds.token + // cachedClientId = parsedCreds.clientId + cachedGuildId = parsedCreds.guildId + } catch (e: any) { + cachedToken = config.DEV_DISCORD_TOKEN + } + } +} diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index dcda453..5449cd1 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -11,8 +11,8 @@ export { SheetNames } from "./localDb" const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") -const REMOTE_UPDATE_DELAY = 40000 -const DELAY_BETWEEN_ATTEMPTS = 10000 +const REMOTE_UPDATE_DELAY = 120000 +const DELAY_BETWEEN_ATTEMPTS = 30000 const DELAY_BETWEEN_FIRST_LOAD = 1500 let creds: string | undefined | null @@ -59,7 +59,7 @@ export async function getSheet< sheet = new Sheet(sheetName, specimen, translation) await sheet.waitForFirstLoad() sheetList[sheetName] = sheet - setInterval(() => sheet.dbUpdate(), REMOTE_UPDATE_DELAY) + setInterval(() => sheet.dbUpdate(), REMOTE_UPDATE_DELAY * (1 + Math.random() / 10)) } else { sheet = sheetList[sheetName] as Sheet } @@ -265,6 +265,7 @@ export class Sheet< } await tryNTimesVoidReturn(async () => { + console.log(`dbSaveAsync on ${this.name} at ${new Date()}`) // Load sheet into an array of objects const rows = await sheet.getRows() if (!rows[0]) { @@ -317,6 +318,7 @@ export class Sheet< await rows[rowToDelete].delete() } } + console.log(`dbSaveAsync successful on ${this.name} at ${new Date()}`) }) } @@ -330,6 +332,7 @@ export class Sheet< await tryNTimesVoidReturn(async () => { // Load sheet into an array of objects + console.log(`dbLoadAsync on ${this.name} at ${new Date()}`) const rows = (await sheet.getRows()) as StringifiedElement[] const elements: Element[] = [] if (!rows[0]) { @@ -357,6 +360,7 @@ export class Sheet< }) this._state = elements + console.log(`dbLoadAsync successful on ${this.name} at ${new Date()}`) }) } diff --git a/src/server/gsheets/discordRoles.ts b/src/server/gsheets/discordRoles.ts new file mode 100644 index 0000000..658937e --- /dev/null +++ b/src/server/gsheets/discordRoles.ts @@ -0,0 +1,14 @@ +import ExpressAccessors from "./expressAccessors" +import { + DiscordRole, + DiscordRoleWithoutId, + translationDiscordRoles, +} from "../../services/discordRoles" + +const expressAccessor = new ExpressAccessors( + "DiscordRoles", + new DiscordRole(), + translationDiscordRoles +) + +export const discordRolesListGet = expressAccessor.listGet() diff --git a/src/server/gsheets/localDb.ts b/src/server/gsheets/localDb.ts index ebf3dff..9a2a1f8 100644 --- a/src/server/gsheets/localDb.ts +++ b/src/server/gsheets/localDb.ts @@ -16,6 +16,8 @@ export class SheetNames { Boxes = "Boîtes" + DiscordRoles = "Rôles Discord" + Games = "Jeux" Miscs = "Divers" diff --git a/src/server/index.ts b/src/server/index.ts index d8e93b5..3210c9f 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -50,6 +50,7 @@ import { import { wishListGet, wishAdd } from "./gsheets/wishes" import config from "../config" import { notificationsSubscribe, notificationMain } from "./notifications" +import { /* discordRegisterCommands, */ discordBot, hasDiscordAccess } from "./discordBot" import checkAccess from "./checkAccess" import { hasGSheetsAccess } from "./gsheets/accessors" import { addStatus, showStatusAt } from "./status" @@ -64,6 +65,9 @@ checkAccess() notificationMain() +// discordRegisterCommands() +discordBot() + const app = express() // Allow receiving big images @@ -243,6 +247,14 @@ if (hasPushNotifAccess) { addStatus("Push notif:", chalk.blue(`🚧 offline, simulated`)) } +hasDiscordAccess().then((hasApiAccess: boolean) => { + if (hasApiAccess) { + addStatus("Discord bot:", chalk.green(`✅ online through discord.js`)) + } else { + addStatus("Discord bot:", chalk.blue(`🚧 no creds, disabled`)) + } +}) + hasSecret().then((has: boolean) => { if (has) { addStatus("JWT secret:", chalk.green(`✅ prod private one from file`)) diff --git a/src/server/notifications.ts b/src/server/notifications.ts index 393bba3..854811a 100644 --- a/src/server/notifications.ts +++ b/src/server/notifications.ts @@ -108,7 +108,9 @@ async function notifyAboutAnnouncement(): Promise { } const audience = volunteerList.filter( - (v) => v.acceptsNotifs === "oui" && (v.active === "oui" || v.active === "peut-etre") + (v) => + v.acceptsNotifs === "oui" && + (v.active === "oui" || v.active === "peut-etre" || v.active === "à distance") ) console.error( diff --git a/src/services/discordRoles.ts b/src/services/discordRoles.ts new file mode 100644 index 0000000..07859ed --- /dev/null +++ b/src/services/discordRoles.ts @@ -0,0 +1,28 @@ +/* eslint-disable max-classes-per-file */ +export class DiscordRole { + id = 0 + + messageId = "" + + emoji = "" + + role = "" +} + +export const translationDiscordRoles: { [k in keyof DiscordRole]: string } = { + id: "id", + messageId: "messageId", + emoji: "emoji", + role: "rôle", +} + +export const elementName = "DiscordRoles" + +export type DiscordRoleWithoutId = Omit + +export interface DiscordRole { + id: DiscordRole["id"] + messageId: DiscordRole["messageId"] + emoji: DiscordRole["emoji"] + role: DiscordRole["role"] +} diff --git a/src/store/teamList.ts b/src/store/teamList.ts index 17b096a..e975c99 100644 --- a/src/store/teamList.ts +++ b/src/store/teamList.ts @@ -59,6 +59,10 @@ export const selectTeamList = createSelector( } ) +export const selectSortedTeams = createSelector(selectTeamList, (teams) => + [...teams].sort((a, b) => get(a, "order", 0) - get(b, "order", 0)) +) + export const selectSortedActiveTeams = createSelector(selectTeamList, (teams) => [...teams.filter((team) => get(team, "status") === "active")].sort( (a, b) => get(a, "order", 0) - get(b, "order", 0) diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 48d8021..74fc31b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -2,6 +2,7 @@ declare const __CLIENT__: boolean declare const __SERVER__: boolean declare const __DEV__: boolean declare const __LOCAL__: boolean +declare const __REGISTER_DISCORD_COMMANDS__: boolean declare const __TEST__: boolean declare module "*.svg" @@ -19,6 +20,7 @@ declare namespace NodeJS { __SERVER__: boolean __DEV__: boolean __LOCAL__: boolean + __REGISTER_DISCORD_COMMANDS__: boolean __TEST__: boolean $RefreshReg$: () => void $RefreshSig$$: () => void diff --git a/webpack/base.config.ts b/webpack/base.config.ts index a86a6e8..1750fad 100644 --- a/webpack/base.config.ts +++ b/webpack/base.config.ts @@ -9,6 +9,7 @@ import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer" export const isDev = process.env.NODE_ENV === "development" const isLocal = process.env.LOCAL === "true" +const isRegisterDiscordCommands = process.env.REGISTER_DISCORD_COMMANDS === "true" const getStyleLoaders = (isWeb: boolean, isSass?: boolean) => { let loaders: RuleSetUseItem[] = [ { @@ -49,6 +50,7 @@ const getPlugins = (isWeb: boolean) => { __SERVER__: !isWeb, __DEV__: isDev, __LOCAL__: isLocal, + __REGISTER_DISCORD_COMMANDS__: isRegisterDiscordCommands, }), ] diff --git a/yarn.lock b/yarn.lock index 20f1f08..9b2a16f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1037,6 +1037,42 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@discordjs/builders@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.4.0.tgz#b951b5e6ce4e459cd06174ce50dbd51c254c1d47" + integrity sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA== + dependencies: + "@discordjs/util" "^0.1.0" + "@sapphire/shapeshift" "^3.7.1" + discord-api-types "^0.37.20" + fast-deep-equal "^3.1.3" + ts-mixer "^6.0.2" + tslib "^2.4.1" + +"@discordjs/collection@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.3.0.tgz#65bf9674db72f38c25212be562bb28fa0dba6aa3" + integrity sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg== + +"@discordjs/rest@^1.4.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-1.5.0.tgz#dc15474ab98cf6f31291bf61bbc72bcf4f30cea2" + integrity sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA== + dependencies: + "@discordjs/collection" "^1.3.0" + "@discordjs/util" "^0.1.0" + "@sapphire/async-queue" "^1.5.0" + "@sapphire/snowflake" "^3.2.2" + discord-api-types "^0.37.23" + file-type "^18.0.0" + tslib "^2.4.1" + undici "^5.13.0" + +"@discordjs/util@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@discordjs/util/-/util-0.1.0.tgz#e42ca1bf407bc6d9adf252877d1b206e32ba369a" + integrity sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ== + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -1442,6 +1478,24 @@ redux-thunk "^2.4.1" reselect "^4.1.5" +"@sapphire/async-queue@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.0.tgz#2f255a3f186635c4fb5a2381e375d3dfbc5312d8" + integrity sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA== + +"@sapphire/shapeshift@^3.7.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-3.8.1.tgz#b98dc6a7180f9b38219267917b2e6fa33f9ec656" + integrity sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw== + dependencies: + fast-deep-equal "^3.1.3" + lodash "^4.17.21" + +"@sapphire/snowflake@^3.2.2": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.4.0.tgz#25c012158a9feea2256c718985dbd6c1859a5022" + integrity sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw== + "@sendgrid/client@^7.7.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.7.0.tgz#f8f67abd604205a0d0b1af091b61517ef465fdbf" @@ -1556,6 +1610,11 @@ "@testing-library/dom" "^8.0.0" "@types/react-dom" "<18.0.0" +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2164,6 +2223,13 @@ anymatch "^3.0.0" source-map "^0.6.0" +"@types/ws@^8.5.3": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" + integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== + dependencies: + "@types/node" "*" + "@types/xml2js@^0.4.11": version "0.4.11" resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.11.tgz#bf46a84ecc12c41159a7bd9cf51ae84129af0e79" @@ -3102,6 +3168,13 @@ buffer@^5.2.1: base64-js "^1.3.1" ieee754 "^1.1.13" +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -4006,6 +4079,29 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +discord-api-types@^0.37.20, discord-api-types@^0.37.23: + version "0.37.31" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.31.tgz#128d33d641fd9a92fba97a47d7052e1f5694ec27" + integrity sha512-k9DQQ7Wv+ehiF7901qk/FnP47k6O2MHm3meQFee4gUzi5dfGAVLf7SfLNtb4w7G2dmukJyWQtVJEDF9oMb9yuQ== + +discord.js@^14.7.1: + version "14.7.1" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.7.1.tgz#26079d0ff4d27daf02480a403c456121f0682bd9" + integrity sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA== + dependencies: + "@discordjs/builders" "^1.4.0" + "@discordjs/collection" "^1.3.0" + "@discordjs/rest" "^1.4.0" + "@discordjs/util" "^0.1.0" + "@sapphire/snowflake" "^3.2.2" + "@types/ws" "^8.5.3" + discord-api-types "^0.37.20" + fast-deep-equal "^3.1.3" + lodash.snakecase "^4.1.1" + tslib "^2.4.1" + undici "^5.13.0" + ws "^8.11.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4834,6 +4930,15 @@ file-type@^12.0.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" integrity sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg== +file-type@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-18.2.0.tgz#c2abec00d1af0f09151e1549e3588aab0bac5001" + integrity sha512-M3RQMWY3F2ykyWZ+IHwNCjpnUmukYhtdkGGC1ZVEUb0ve5REGF7NNJ4Q9ehCUabtQKtSVFOMbFTXgJlFb0DQIg== + dependencies: + readable-web-to-node-stream "^3.0.2" + strtok3 "^7.0.0" + token-types "^5.0.1" + file-type@^3.8.0: version "3.9.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" @@ -5648,7 +5753,7 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7014,6 +7119,11 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== +lodash.snakecase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -7990,6 +8100,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +peek-readable@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" + integrity sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A== + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -8787,6 +8902,13 @@ readable-stream@^3.1.1, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-web-to-node-stream@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" + integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== + dependencies: + readable-stream "^3.6.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -9495,6 +9617,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -9665,6 +9792,14 @@ strnum@^1.0.4: resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== +strtok3@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-7.0.0.tgz#868c428b4ade64a8fd8fee7364256001c1a4cbe5" + integrity sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^5.0.0" + style-search@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" @@ -10000,6 +10135,14 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +token-types@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-5.0.1.tgz#aa9d9e6b23c420a675e55413b180635b86a093b4" + integrity sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg== + dependencies: + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" + totalist@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" @@ -10070,6 +10213,11 @@ ts-jest@^27.0.3: semver "7.x" yargs-parser "20.x" +ts-mixer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.2.tgz#3e4e4bb8daffb24435f6980b15204cb5b287e016" + integrity sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A== + ts-node@^10.0.0: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -10109,6 +10257,11 @@ tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -10215,6 +10368,13 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +undici@^5.13.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.16.0.tgz#6b64f9b890de85489ac6332bd45ca67e4f7d9943" + integrity sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ== + dependencies: + busboy "^1.6.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -10705,6 +10865,11 @@ ws@^7.3.1, ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.11.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" + integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"