├── .gitignore ├── public ├── assets │ ├── img │ │ ├── products │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ └── icons │ │ │ ├── discord.png │ │ │ ├── icon16.png │ │ │ ├── icon180.png │ │ │ ├── icon192.png │ │ │ ├── icon32.png │ │ │ └── icon512.png │ ├── fonts │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2 │ │ └── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2 │ ├── css │ │ ├── custom.css │ │ ├── Fully-responsive-table.css │ │ └── Inter.css │ └── js │ │ ├── matomo.js │ │ ├── dashboard.js │ │ ├── dark-mode-switch.js │ │ ├── navbar.js │ │ ├── language.js │ │ └── translations │ │ ├── en.json │ │ ├── nl.json │ │ ├── de.json │ │ ├── fr.json │ │ └── cs.json ├── manifest.json ├── dashboard.html ├── privacy-policy.html └── terms-of-service.html ├── events ├── error.js ├── guildCreate.js ├── guildDelete.js ├── ready.js └── interactionCreate.js ├── slash ├── stats.js ├── info.js ├── checkperm.js ├── setup.js ├── delete.js ├── deploy.js └── language.js ├── config.js ├── modules ├── functions.js ├── logger.js └── language.js ├── models ├── buttonService.js ├── database.js └── embedService.js ├── controllers ├── languageController.js ├── deleteController.js ├── generalController.js └── setupController.js ├── package.json ├── LICENSE ├── web.js ├── README.md ├── app ├── functions.js ├── getAPI.js └── postAPI.js ├── index.js ├── twitchalerts.sql ├── languages ├── en-US.json ├── es-ES.json ├── nl.json ├── de.json ├── pt-BR.json ├── fr.json ├── cs.json └── ko.json └── services └── fetchLive.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | data 4 | .idea -------------------------------------------------------------------------------- /public/assets/img/products/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/products/1.png -------------------------------------------------------------------------------- /public/assets/img/products/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/products/2.png -------------------------------------------------------------------------------- /public/assets/img/products/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/products/3.png -------------------------------------------------------------------------------- /public/assets/img/icons/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/icons/discord.png -------------------------------------------------------------------------------- /public/assets/img/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/icons/icon16.png -------------------------------------------------------------------------------- /public/assets/img/icons/icon180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/icons/icon180.png -------------------------------------------------------------------------------- /public/assets/img/icons/icon192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/icons/icon192.png -------------------------------------------------------------------------------- /public/assets/img/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/icons/icon32.png -------------------------------------------------------------------------------- /public/assets/img/icons/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/img/icons/icon512.png -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harfeur/TwitchAlerts/HEAD/public/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2 -------------------------------------------------------------------------------- /events/error.js: -------------------------------------------------------------------------------- 1 | const logger = require("../modules/logger.js"); 2 | module.exports = async (client, error) => { 3 | logger.error(`An error event was sent by Discord.js: \n${JSON.stringify(error)}`); 4 | }; 5 | -------------------------------------------------------------------------------- /events/guildCreate.js: -------------------------------------------------------------------------------- 1 | const logger = require("../modules/logger.js"); 2 | 3 | module.exports = async (client, guild) => { 4 | logger.log(`[GUILD JOIN] ${guild.id} added the bot. Owner: ${guild.ownerId}`); 5 | await client.container.pg.addNewGuild(guild.id); 6 | }; 7 | -------------------------------------------------------------------------------- /slash/stats.js: -------------------------------------------------------------------------------- 1 | const GeneralController = require("../controllers/generalController"); 2 | 3 | exports.run = GeneralController.stats; 4 | 5 | exports.commandData = { 6 | name: "stats", 7 | description: "Show's the bots stats." 8 | }; 9 | 10 | exports.conf = { 11 | guildOnly: false, 12 | translate: false 13 | }; -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const {GatewayIntentBits, Partials} = require("discord.js"); 2 | 3 | const config = { 4 | // Bot Support, level 8 by default. Array of user ID strings 5 | "support": [], 6 | 7 | intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], 8 | partials: [Partials.Channel], 9 | }; 10 | 11 | module.exports = config; 12 | -------------------------------------------------------------------------------- /events/guildDelete.js: -------------------------------------------------------------------------------- 1 | const logger = require("../modules/logger.js"); 2 | 3 | module.exports = (client, guild) => { 4 | if (!guild.available) return; // If there is an outage, return. 5 | 6 | logger.log(`[GUILD LEAVE] ${guild.id} removed the bot.`); 7 | if (!client.container.debug) { 8 | client.container.pg.deleteGuild(guild.id); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"Twitch Alerts","icons":[{"src":"https://twitchbot.harfeur.fr/assets/img/icons/icon192.png?h=f0ab3d72d3828d68c58b734de28a9739","type":"image/png","sizes":"192x192"},{"src":"https://twitchbot.harfeur.fr/assets/img/icons/icon512.png?h=6369af451e4c9296c1fd74d8c306c795","type":"image/png","sizes":"512x512"}],"start_url":"/","theme_color":"#9146ff","display":"fullscreen"} -------------------------------------------------------------------------------- /public/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | .channelMention { 2 | color: rgb(88,101,242); 3 | background-color: rgba(88,101,242,0.15); 4 | transition: background-color 50ms ease-out,color 50ms ease-out; 5 | cursor: pointer; 6 | } 7 | 8 | .channelMention:hover { 9 | color: white; 10 | background-color: rgb(88,101,242); 11 | } 12 | 13 | .channelIcon { 14 | width: 1.05rem; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /slash/info.js: -------------------------------------------------------------------------------- 1 | const GeneralController = require("../controllers/generalController"); 2 | const {getString} = require("../modules/language"); 3 | 4 | exports.run = GeneralController.info; 5 | 6 | exports.commandData = { 7 | name: getString("en-US", "INFO_CMD_NAME"), 8 | description: getString("en-US", "INFO_CMD_DESC"), 9 | }; 10 | 11 | exports.conf = { 12 | guildOnly: false, 13 | translate: true 14 | }; -------------------------------------------------------------------------------- /modules/functions.js: -------------------------------------------------------------------------------- 1 | const logger = require("./logger.js"); 2 | 3 | process.on("uncaughtException", (err) => { 4 | const errorMsg = err.stack.replace(new RegExp(`${__dirname}/`, "g"), "./"); 5 | logger.error(`Uncaught Exception: ${errorMsg}`); 6 | console.error(err); 7 | process.exit(1); 8 | }); 9 | 10 | process.on("unhandledRejection", err => { 11 | logger.error(`Unhandled rejection: ${err}`); 12 | console.error(err); 13 | }); -------------------------------------------------------------------------------- /slash/checkperm.js: -------------------------------------------------------------------------------- 1 | const GeneralController = require("../controllers/generalController"); 2 | 3 | exports.run = GeneralController.checkperm; 4 | 5 | exports.commandData = { 6 | name: "checkperm", 7 | description: "Check if the bot has all the required permissions in this channel.", 8 | dm_permission: false, 9 | default_member_permissions: "32" 10 | }; 11 | 12 | exports.conf = { 13 | guildOnly: false, 14 | translate: true 15 | }; -------------------------------------------------------------------------------- /slash/setup.js: -------------------------------------------------------------------------------- 1 | const SetupController = require("../controllers/setupController"); 2 | const {getString} = require("../modules/language"); 3 | 4 | exports.run = SetupController.setup; 5 | 6 | exports.modalSubmit = SetupController.modalSubmit; 7 | 8 | exports.commandData = { 9 | name: getString("en-US", "SETUP_CMD_NAME"), 10 | description: getString("en-US", "SETUP_CMD_DESC"), 11 | dm_permission: false, 12 | default_member_permissions: "32" 13 | }; 14 | 15 | exports.conf = { 16 | guildOnly: false, 17 | translate: true 18 | }; -------------------------------------------------------------------------------- /slash/delete.js: -------------------------------------------------------------------------------- 1 | const DeleteController = require("../controllers/deleteController"); 2 | const {getString} = require("../modules/language"); 3 | 4 | exports.run = DeleteController.delete; 5 | 6 | exports.selectMenu = DeleteController.menuSelect; 7 | 8 | exports.commandData = { 9 | name: getString("en-US", "DELETE_CMD_NAME"), 10 | description: getString("en-US", "DELETE_CMD_DESC"), 11 | dm_permission: false, 12 | default_member_permissions: "32" 13 | }; 14 | 15 | exports.conf = { 16 | guildOnly: false, 17 | translate: true 18 | }; -------------------------------------------------------------------------------- /public/assets/js/matomo.js: -------------------------------------------------------------------------------- 1 | var _paq = window._paq = window._paq || []; 2 | /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ 3 | _paq.push(['trackPageView']); 4 | _paq.push(['enableLinkTracking']); 5 | (function() { 6 | var u="https://analytics.harfeur.fr/"; 7 | _paq.push(['setTrackerUrl', u+'matomo.php']); 8 | _paq.push(['setSiteId', '4']); 9 | var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; 10 | g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); 11 | })(); -------------------------------------------------------------------------------- /models/buttonService.js: -------------------------------------------------------------------------------- 1 | const {ButtonBuilder, ButtonStyle} = require("discord.js"); 2 | const {DurationFormatter} = require("@sapphire/time-utilities"); 3 | new DurationFormatter(); 4 | module.exports = { 5 | getLinkButton: (name, link, emoji = "") => { 6 | const button = new ButtonBuilder() 7 | .setLabel(name) 8 | .setURL(link) 9 | .setStyle(ButtonStyle.Link); 10 | if (emoji !== "") button.setEmoji(emoji); 11 | return button; 12 | }, 13 | 14 | getButton: (name, style, id, emoji = "", disabled = false) => { 15 | const button = new ButtonBuilder() 16 | .setLabel(name) 17 | .setStyle(style) 18 | .setCustomId(id) 19 | .setDisabled(disabled); 20 | if (emoji !== "") button.setEmoji(emoji); 21 | return button; 22 | } 23 | } -------------------------------------------------------------------------------- /slash/deploy.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, interaction) => { 2 | 3 | const [globalCmds, guildCmds] = client.container.slashcmds.partition(c => !c.conf.guildOnly); 4 | 5 | await interaction.deferReply(); 6 | 7 | await interaction.editReply("Deploying commands"); 8 | 9 | await client.guilds.cache.get(interaction.guild.id)?.commands.set(guildCmds.map(c => c.commandData)); 10 | 11 | await client.application?.commands.set(globalCmds.map(c => c.commandData)).catch(e => console.log(e)); 12 | 13 | await interaction.editReply("All commands deployed!"); 14 | }; 15 | 16 | exports.commandData = { 17 | name: "deploy", 18 | description: "Deploy the created commands.", 19 | options: [], 20 | dm_permission: false, 21 | default_member_permissions: "8" 22 | }; 23 | 24 | exports.conf = { 25 | guildOnly: true, 26 | translate: false 27 | }; -------------------------------------------------------------------------------- /controllers/languageController.js: -------------------------------------------------------------------------------- 1 | const {getString} = require("../modules/language"); 2 | const logger = require("../modules/logger"); 3 | module.exports = class LanguageController { 4 | static async language(client, interaction) { 5 | if (!client.container.debug) await interaction.deferReply({ephemeral: true}); 6 | 7 | const newLang = interaction.options.getString("value"); 8 | if (!client.container.debug) { 9 | await client.container.pg.setGuildLanguage(interaction.guild.id, newLang); 10 | await interaction.editReply(interaction.getLocalizedString("LANGUAGE_UPDATE", { 11 | language: `**${newLang === "default" ? interaction.getLocalizedString("LANGUAGE_DEFAULT") : getString(newLang, "LANGUAGE")}**` 12 | })); 13 | } 14 | logger.debug(`Changed language to ${newLang} in guild ${interaction.guild.id}`); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch-alerts", 3 | "version": "2.0.0", 4 | "description": "Get real time alerts on discord when going live on twitch", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "web": "node web.js" 9 | }, 10 | "engines": { 11 | "node": ">=16" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Harfeur/TwitchAlerts.git" 16 | }, 17 | "author": "Harfeur (https://www.harfeur.fr)", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Harfeur/TwitchAlerts/issues" 21 | }, 22 | "homepage": "https://twitchbot.harfeur.fr", 23 | "dependencies": { 24 | "@sapphire/time-utilities": "^1.7.12", 25 | "@twurple/api": "^7.2.0", 26 | "@twurple/auth": "^7.2.0", 27 | "@twurple/eventsub-http": "^7.2.0", 28 | "body-parser": "^1.20.3", 29 | "colorette": "^2.0.20", 30 | "cookie-parser": "^1.4.7", 31 | "discord-oauth2": "^2.12.1", 32 | "discord.js": "^14.16.3", 33 | "express": "^4.21.1", 34 | "node-twitch": "^0.5.0", 35 | "pg": "^8.13.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 YorkAARGH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /slash/language.js: -------------------------------------------------------------------------------- 1 | const LanguageController = require("../controllers/languageController"); 2 | const {languagesList, getString} = require("../modules/language"); 3 | 4 | exports.run = LanguageController.language; 5 | 6 | let language = [{ 7 | name: getString("en-US", "LANGUAGE_DEFAULT"), 8 | name_localizations: {}, 9 | value: "default" 10 | }] 11 | 12 | for (const [lang, _] of languagesList) { 13 | language[0].name_localizations[lang] = getString(lang, "LANGUAGE_DEFAULT"); 14 | language.push({ 15 | name: getString(lang, "LANGUAGE"), 16 | value: lang 17 | }) 18 | } 19 | 20 | exports.commandData = { 21 | name: getString("en-US", "LANGUAGE_CMD_NAME"), 22 | description: getString("en-US", "LANGUAGE_CMD_DESC"), 23 | dm_permission: false, 24 | options: [ 25 | { 26 | type: 3, 27 | name: getString("en-US", "LANGUAGE_OPTION_NAME"), 28 | description: getString("en-US", "LANGUAGE_OPTION_DESC"), 29 | required: true, 30 | choices: language 31 | } 32 | ], 33 | default_member_permissions: "32" 34 | }; 35 | 36 | exports.conf = { 37 | guildOnly: false, 38 | translate: true 39 | }; -------------------------------------------------------------------------------- /web.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | const DiscordOauth2 = require("discord-oauth2"); 4 | const logger = require("./modules/logger"); 5 | 6 | // Connexion à Discord 7 | const oauth = new DiscordOauth2({ 8 | clientId: process.env.DISCORD_CLIENT_ID, 9 | clientSecret: process.env.DISCORD_CLIENT_SECRET, 10 | redirectUri: "https://" + process.env.DOMAIN + "/connect" 11 | }); 12 | 13 | async function init(pgsql, discord, twitch, fetchLive){ 14 | let cookies = new Map(); 15 | 16 | const app = express(); 17 | 18 | // CONFIGURATION ================================== 19 | app.use(cookieParser()); 20 | 21 | app.use('/', express.static('public')); 22 | 23 | // ROUTES ========================================= 24 | 25 | let functions = require('./app/functions')(oauth, cookies); 26 | 27 | require('./app/getAPI.js')(app, pgsql, oauth, discord, twitch, functions, __dirname, cookies); 28 | require('./app/postAPI.js')(app, pgsql, oauth, discord, twitch, functions, __dirname, cookies); 29 | 30 | // LAUNCH ======================================== 31 | app.listen(process.env.PORT, async function () { 32 | logger.log(`Server started on port ${process.env.PORT}`); 33 | fetchLive.markAsReady(); 34 | }); 35 | } 36 | 37 | module.exports = init; -------------------------------------------------------------------------------- /modules/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | Logger class for easy and aesthetically pleasing console logging 3 | */ 4 | const {cyan, red, magenta, gray, yellow, white, blue, green} = require("colorette"); 5 | const {Timestamp} = require("@sapphire/time-utilities"); 6 | 7 | exports.log = (content, type = "log") => { 8 | const timestamp = `[${cyan(new Timestamp("YYYY-MM-DD HH:mm:ss"))}]:`; 9 | 10 | switch (type) { 11 | case "log": 12 | return console.log(`${timestamp} ${gray(type.toUpperCase())} ${content} `); 13 | case "warn": 14 | return console.log(`${timestamp} ${yellow(type.toUpperCase())} ${content} `); 15 | case "error": 16 | console.error(content); 17 | return console.log(`${timestamp} ${red(type.toUpperCase())} ${content} `); 18 | case "debug": 19 | return console.log(`${timestamp} ${magenta(type.toUpperCase())} ${content} `); 20 | case "cmd": 21 | return console.log(`${timestamp} ${white(type.toUpperCase())} ${content}`); 22 | case "database": 23 | return console.log(`${timestamp} ${blue(type.toUpperCase())} ${content}`); 24 | case "ready": 25 | return console.log(`${timestamp} ${green(type.toUpperCase())} ${content}`); 26 | default: 27 | throw new TypeError("Logger type must be either warn, debug, log, ready, cmd or error."); 28 | } 29 | }; 30 | 31 | exports.error = (...args) => this.log(...args, "error"); 32 | 33 | exports.warn = (...args) => this.log(...args, "warn"); 34 | 35 | exports.debug = (...args) => this.log(...args, "debug"); 36 | 37 | exports.cmd = (...args) => this.log(...args, "cmd"); 38 | -------------------------------------------------------------------------------- /public/assets/css/Fully-responsive-table.css: -------------------------------------------------------------------------------- 1 | table { 2 | /*border: 1px solid #ccc;*/ 3 | border-collapse: collapse; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | table-layout: fixed; 8 | } 9 | 10 | table tr { 11 | /*background-color: #f8f8f8;*/ 12 | /*border: 1px solid #ddd;*/ 13 | padding: .35em; 14 | } 15 | 16 | table th, table td { 17 | padding: .625em; 18 | text-align: center; 19 | vertical-align: middle; 20 | } 21 | 22 | table th { 23 | font-size: .85em; 24 | letter-spacing: .1em; 25 | text-transform: uppercase; 26 | } 27 | 28 | @media screen and (max-width: 600px) { 29 | table { 30 | border: 0; 31 | } 32 | } 33 | 34 | @media screen and (max-width: 600px) { 35 | table thead { 36 | border: none; 37 | clip: rect(0 0 0 0); 38 | height: 1px; 39 | margin: -1px; 40 | overflow: hidden; 41 | padding: 0; 42 | position: absolute; 43 | width: 1px; 44 | } 45 | } 46 | 47 | @media screen and (max-width: 600px) { 48 | table tr { 49 | /*border-bottom: 3px solid #ddd;*/ 50 | display: block; 51 | margin-bottom: .625em; 52 | } 53 | } 54 | 55 | @media screen and (max-width: 600px) { 56 | table td { 57 | /*border-bottom: 1px solid #ddd;*/ 58 | display: block; 59 | font-size: .8em; 60 | text-align: right; 61 | } 62 | } 63 | 64 | @media screen and (max-width: 600px) { 65 | table td::before { 66 | content: attr(data-label); 67 | float: left; 68 | font-weight: bold; 69 | text-transform: uppercase; 70 | } 71 | } 72 | 73 | @media screen and (max-width: 600px) { 74 | table td:last-child { 75 | border-bottom: 0; 76 | } 77 | } 78 | 79 | *, *:after, *:before { 80 | -webkit-box-sizing: border-box; 81 | box-sizing: border-box; 82 | } 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch Alerts 2 | 3 | I am NOT affiliated with Twitch. 4 | 5 | ## How to use ? 6 | 7 | For the regular user, you can [add the bot to your server](https://discord.com/oauth2/authorize?client_id=748846588163784794&scope=bot%20applications.commands&permissions=478208). 8 | 9 | ### I want to host the bot 10 | 11 | You are allowed to host the bot and make changes, but you need to respect the license : You can't use the bot as a commercial purpose (you can't make money using the bot). 12 | 13 | To host the bot, you can download all the files, run `npm install`, and add the following variables to the environment: 14 | ``` 15 | DATABASE_URL = Link to the database (I use PostgreSQL database) 16 | DISCORD_CLIENT_ID = ID of your Discord application 17 | DISCORD_CLIENT_SECRET = Client secret of your Discord application 18 | DISCORD_REDIRECT_URI = Redirect URL : it is normally https://yoururl.com/connect 19 | PORT = Port of the web server 20 | TWITCH_BOT_CLIENT_ID = ID of your Twitch application 21 | TWITCH_BOT_CLIENT_SECRET = Client secret of your Twitch application 22 | TWITCHBOT = Bot secret of your Discord application 23 | URL = URL of your server (with port if different from default) 24 | ``` 25 | 26 | You can then run the bot with `node index.js` and (optionally) the webserver with `node web.js`. 27 | 28 | ## License 29 | 30 | Inspired from GuideBot (see LICENSE) 31 | 32 | Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 33 | -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | const logger = require("../modules/logger.js"); 2 | const FetchLive = require('../services/fetchLive'); 3 | const {ReverseProxyAdapter, EventSubHttpListener} = require("@twurple/eventsub-http"); 4 | const {AppTokenAuthProvider} = require("@twurple/auth"); 5 | const {ApiClient} = require("@twurple/api"); 6 | 7 | module.exports = async client => { 8 | logger.log(`${client.user.tag}, ready to serve ${client.guilds.cache.map(g => g.memberCount).reduce((a, b) => a + b)} users in ${client.guilds.cache.size} servers.`, "ready"); 9 | await client.application.fetch(); 10 | await client.application.commands.fetch(); 11 | 12 | let webhooks = []; 13 | /* 14 | for (let i = 0; i < parseInt(process.env.WEBHOOK_CLIENTS); i++) { 15 | const authProvider = new AppTokenAuthProvider(process.env[`WEBHOOK_CLIENT_${i}`], process.env[`WEBHOOK_SECRET_${i}`]) 16 | const apiClient = new ApiClient({authProvider}); 17 | const webhookMiddleware = new EventSubHttpListener({ 18 | apiClient, 19 | adapter: new ReverseProxyAdapter({ 20 | hostName: `webhook${i}.${process.env.DOMAIN}`, 21 | port: parseInt(process.env.PORT) + i + 1 22 | }), 23 | secret: process.env[`WEBHOOK_SECRET_${i}`] 24 | }); 25 | webhookMiddleware.start(); 26 | webhooks.push(webhookMiddleware); 27 | webhookMiddleware.onSubscriptionCreateFailure((sub, err) => { 28 | logger.error(err); 29 | }); 30 | webhookMiddleware.onRevoke(sub => { 31 | logger.error("Revocation"); 32 | }); 33 | } 34 | */ 35 | const fl = new FetchLive(client, webhooks); 36 | 37 | await require('../web.js')(client.container.pg, client, client.container.twitch, fl); 38 | }; 39 | -------------------------------------------------------------------------------- /public/assets/js/dashboard.js: -------------------------------------------------------------------------------- 1 | $(() => { 2 | $.get('/servers').done(data => { 3 | data.active.forEach(server => { 4 | let serverImage = $(`Server icon`); 5 | let serverLink = $("").attr("href", `/dashboard/${server.id}`).append(serverImage); 6 | 7 | let serverName = $("
").append($("").text(server.name)); 8 | 9 | let serverAlerts = $("

") 10 | .append($("").text(server.alerts + "\xa0")) 11 | .append($(``)); 12 | 13 | let element = $("

").append( 14 | $("
").append([serverLink, serverName, serverAlerts]) 15 | ); 16 | $("#active").append(element); 17 | }); 18 | 19 | data.inactive.forEach(server => { 20 | let serverImage = $(`Server icon`); 21 | let serverLink = $("").attr("href", server.invite).append(serverImage); 22 | 23 | let serverName = $("
").append($("").text(server.name)); 24 | 25 | let element = $("
").append( 26 | $("
").append([serverLink, serverName]) 27 | ); 28 | $("#inactive").append(element); 29 | }); 30 | 31 | $(".loading").hide(); 32 | 33 | translatePage(); 34 | }).fail(error => { 35 | $(".loading") 36 | .append($("")) 37 | .append($("").text(error.responseText)); 38 | }) 39 | }); -------------------------------------------------------------------------------- /public/assets/js/dark-mode-switch.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Dark Mode Switch v1.0.1 (https://github.com/coliff/dark-mode-switch) 3 | * Copyright 2021 C.Oliff 4 | * Licensed under MIT (https://github.com/coliff/dark-mode-switch/blob/main/LICENSE) 5 | */ 6 | 7 | var darkSwitch = document.getElementById("darkSwitch"); 8 | window.addEventListener("load", function () { 9 | if (darkSwitch) { 10 | initTheme(); 11 | darkSwitch.addEventListener("change", function () { 12 | resetTheme(); 13 | }); 14 | } 15 | }); 16 | 17 | /** 18 | * Summary: function that adds or removes the attribute 'data-theme' depending if 19 | * the switch is 'on' or 'off'. 20 | * 21 | * Description: initTheme is a function that uses localStorage from JavaScript DOM, 22 | * to store the value of the HTML switch. If the switch was already switched to 23 | * 'on' it will set an HTML attribute to the body named: 'data-theme' to a 'dark' 24 | * value. If it is the first time opening the page, or if the switch was off the 25 | * 'data-theme' attribute will not be set. 26 | * @return {void} 27 | */ 28 | function initTheme() { 29 | var darkThemeSelected = 30 | localStorage.getItem("darkSwitch") !== null && 31 | localStorage.getItem("darkSwitch") === "dark"; 32 | darkSwitch.checked = darkThemeSelected; 33 | darkThemeSelected 34 | ? document.body.setAttribute("data-theme", "dark") 35 | : document.body.removeAttribute("data-theme"); 36 | } 37 | 38 | /** 39 | * Summary: resetTheme checks if the switch is 'on' or 'off' and if it is toggled 40 | * on it will set the HTML attribute 'data-theme' to dark so the dark-theme CSS is 41 | * applied. 42 | * @return {void} 43 | */ 44 | function resetTheme() { 45 | if (darkSwitch.checked) { 46 | document.body.setAttribute("data-theme", "dark"); 47 | localStorage.setItem("darkSwitch", "dark"); 48 | } else { 49 | document.body.removeAttribute("data-theme"); 50 | localStorage.removeItem("darkSwitch"); 51 | } 52 | } -------------------------------------------------------------------------------- /app/functions.js: -------------------------------------------------------------------------------- 1 | const DiscordOauth2 = require("discord-oauth2"); 2 | 3 | const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | 5 | 6 | /** 7 | * @param {DiscordOauth2} oauth Discord Bot 8 | * @param {Map} cookies Liste des utilisateurs connectés et leurs cookies 9 | * @return {checkToken, makeid} 10 | */ 11 | module.exports = function (oauth, cookies) { 12 | 13 | async function checkToken(cookie) { 14 | if (!cookies.has(cookie)) return false; 15 | let data = cookies.get(cookie) 16 | if (data.expires_in * 1000 + data.time <= Date.now() + 60000) { 17 | let res; 18 | try { 19 | res = await oauth.tokenRequest({ 20 | refreshToken: data.refreshToken, 21 | grantType: "refresh_token" 22 | }); 23 | } catch (err) { 24 | console.error(err); 25 | return false; 26 | } 27 | cookies.set(cookie, { 28 | ...data, 29 | ...res, 30 | time: Date.now() 31 | }); 32 | data = cookies.get(cookie); 33 | } 34 | if (data.timeGuilds + 60000 <= Date.now()) { // Refresh guilds 35 | try { 36 | let guilds = await oauth.getUserGuilds(data.access_token); 37 | cookies.set(cookie, { 38 | ...data, 39 | guilds: guilds, 40 | timeGuilds: Date.now() 41 | }); 42 | } catch (err) { 43 | console.error(err); 44 | return false; 45 | } 46 | } 47 | return true; 48 | } 49 | 50 | //random id 51 | function makeid(length) { 52 | let result = ''; 53 | const charactersLength = CHARS.length; 54 | for (let i = 0; i < length; i++) { 55 | result += CHARS.charAt(Math.floor(Math.random() * charactersLength)); 56 | } 57 | return result; 58 | } 59 | 60 | return {checkToken, makeid}; 61 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This will check if the node version you are running is the required 2 | // Node version, if it isn't it will throw the following error to inform 3 | // you. 4 | if (Number(process.version.slice(1).split(".")[0]) < 16) throw new Error("Node 16.x or higher is required. Update Node on your system."); 5 | 6 | const {Client, Collection} = require("discord.js"); 7 | const Database = require('./models/database'); 8 | 9 | const {readdirSync} = require("fs"); 10 | const {intents, partials} = require("./config"); 11 | const logger = require("./modules/logger"); 12 | const {translateCommand} = require("./modules/language"); 13 | const {AppTokenAuthProvider} = require("@twurple/auth"); 14 | const {ApiClient} = require("@twurple/api"); 15 | 16 | const client = new Client({intents, partials, shards:'auto'}); 17 | 18 | const slashcmds = new Collection(); 19 | 20 | client.container = { 21 | slashcmds, 22 | debug: process.env.NODE_ENV !== "production", 23 | pg: new Database(process.env.DATABASE_URL) 24 | }; 25 | 26 | const init = async () => { 27 | 28 | const slashFiles = readdirSync("./slash").filter(file => file.endsWith(".js")); 29 | for (const file of slashFiles) { 30 | const command = require(`./slash/${file}`); 31 | const commandName = file.split(".")[0]; 32 | logger.log(`Loading Slash command: ${commandName}. 👌`, "log"); 33 | 34 | translateCommand(command, commandName); 35 | 36 | client.container.slashcmds.set(command.commandData.name, command); 37 | } 38 | 39 | const eventFiles = readdirSync("./events/").filter(file => file.endsWith(".js")); 40 | for (const file of eventFiles) { 41 | const eventName = file.split(".")[0]; 42 | logger.log(`Loading Event: ${eventName}. 👌`, "log"); 43 | const event = require(`./events/${file}`); 44 | 45 | client.on(eventName, event.bind(null, client)); 46 | } 47 | 48 | // Twitch 49 | const authProvider = new AppTokenAuthProvider(process.env.TWITCH_BOT_CLIENT_ID, process.env.TWITCH_BOT_CLIENT_SECRET); 50 | client.container.twitch = new ApiClient({authProvider}); 51 | 52 | await client.login(process.env.TWITCHBOT); 53 | }; 54 | 55 | init(); 56 | -------------------------------------------------------------------------------- /controllers/deleteController.js: -------------------------------------------------------------------------------- 1 | const {ActionRowBuilder, StringSelectMenuBuilder} = require("discord.js"); 2 | const logger = require("../modules/logger"); 3 | 4 | module.exports = class DeleteController { 5 | static async delete(client, interaction) { 6 | if (!client.container.debug) await interaction.deferReply({ephemeral: true}); 7 | 8 | const guild = interaction.guild; 9 | const alerts = await client.container.pg.listAlertsByGuild(guild.id); 10 | 11 | if (alerts.length === 0) { 12 | logger.debug(`No alerts for guild ${interaction.guild.id}`); 13 | if (!client.container.debug) await interaction.editReply(interaction.getLocalizedString("DELETE_EMPTY", { 14 | command: ` c.name==="setup").id}>` 15 | })); 16 | return; 17 | } 18 | 19 | const menu = new StringSelectMenuBuilder() 20 | .setCustomId("delete_select") 21 | .setPlaceholder(interaction.getLocalizedString("DELETE_CHOOSE_PLACEHOLDER")); 22 | 23 | for (let i = 0; i < alerts.length && i < 25; i++) { 24 | let q = alerts[i]; 25 | let user = await client.container.twitch.users.getUserById(q.streamer_id); 26 | menu.addOptions([{ 27 | label: user?.displayName ?? q.streamer_id, 28 | value: user?.id ?? q.streamer_id, 29 | description: guild.channels.resolve(q.alert_channel)?.name ?? interaction.getLocalizedString("DELETE_CHANNEL_NOT_FOUND", {canalid: q.alert_channel}) 30 | }]); 31 | } 32 | 33 | const row = new ActionRowBuilder().addComponents(menu); 34 | 35 | if (!client.container.debug) await interaction.editReply({components: [row]}); 36 | logger.debug(`Sent alerts list on guild ${interaction.guild.id}`); 37 | } 38 | 39 | static async menuSelect(client, interaction) { 40 | if (!client.container.debug) { 41 | await client.container.pg.deleteAlert(interaction.guild.id, interaction.values[0]); 42 | await interaction.update({ 43 | content: interaction.getLocalizedString("DELETE_SUCCESS"), 44 | components: [] 45 | }); 46 | } 47 | logger.debug(`Deleted alert for streamer ${interaction.values[0]} in guild ${interaction.guild.id}`); 48 | } 49 | } -------------------------------------------------------------------------------- /events/interactionCreate.js: -------------------------------------------------------------------------------- 1 | const logger = require("../modules/logger.js"); 2 | const {getLocalizedString} = require("../modules/language"); 3 | const {InteractionType} = require('discord.js'); 4 | 5 | module.exports = async (client, interaction) => { 6 | const language = interaction.language = interaction.locale ?? "en-US" 7 | interaction.getLocalizedString = getLocalizedString(language); 8 | 9 | const cmd = client.container.slashcmds.get(interaction.commandName ?? interaction.customId.split("_")[0]); 10 | 11 | if (!cmd) return; 12 | 13 | try { 14 | if (interaction.type === InteractionType.ApplicationCommand) { 15 | logger.log(`${interaction.user.id} on server ${interaction.guild.id} ran slash command ${interaction.commandName}`, "cmd"); 16 | await cmd.run(client, interaction); 17 | } else if (interaction.isStringSelectMenu()) { 18 | logger.log(`${interaction.user.id} on server ${interaction.guild.id} ran string menu ${interaction.customId}`, "cmd"); 19 | await cmd.selectMenu(client, interaction); 20 | } else if (interaction.isModalSubmit()) { 21 | logger.log(`${interaction.user.id} on server ${interaction.guild.id} ran modal submit ${interaction.customId}`, "cmd"); 22 | await cmd.modalSubmit(client, interaction); 23 | } 24 | } catch (e) { 25 | logger.error(e.message); 26 | console.error(e); 27 | if (client.container.debug) return; 28 | if (interaction.replied) 29 | interaction.followUp({ 30 | content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\``, 31 | ephemeral: true 32 | }) 33 | .catch(e => console.error("An error occurred following up on an error", e)); 34 | else if (interaction.deferred) 35 | interaction.editReply({ 36 | content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\``, 37 | ephemeral: true 38 | }) 39 | .catch(e => console.error("An error occurred following up on an error", e)); 40 | else 41 | interaction.reply({ 42 | content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\``, 43 | ephemeral: true 44 | }) 45 | .catch(e => console.error("An error occurred replying on an error", e)); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /controllers/generalController.js: -------------------------------------------------------------------------------- 1 | const {ActionRowBuilder, PermissionsBitField} = require("discord.js"); 2 | const embedService = require("../models/embedService"); 3 | const buttonService = require("../models/buttonService"); 4 | 5 | 6 | module.exports = class GeneralController { 7 | static async stats(client, interaction) { 8 | const statsEmbed = embedService.generateStatEmbed(client, interaction); 9 | if (!client.container.debug) await interaction.reply({embeds: [statsEmbed]}); 10 | } 11 | 12 | static async info(client, interaction) { 13 | const statsEmbed = embedService.generateInfoEmbed(client, interaction); 14 | const link = new ActionRowBuilder() 15 | .addComponents( 16 | buttonService.getLinkButton("GitHub", "https://github.com/Harfeur/TwitchAlerts"), 17 | buttonService.getLinkButton(interaction.getLocalizedString("INFO_INVITE"), "https://discord.com/oauth2/authorize?client_id=748846588163784794&scope=bot+applications.commands&permissions=216064"), 18 | buttonService.getLinkButton(interaction.getLocalizedString("INFO_SUPPORT"), "https://discord.gg/uY98wtmvXf") 19 | ) 20 | if (!client.container.debug) await interaction.reply({embeds: [statsEmbed], components: [link]}); 21 | } 22 | 23 | static async checkperm(client, interaction) { 24 | await interaction.deferReply({ephemeral: true}); 25 | 26 | const channel = interaction.channel; 27 | 28 | let message = `Permissions in <#${channel.id}>:\n`; 29 | 30 | const sendMsg = channel.permissionsFor(client.user).has(PermissionsBitField.Flags.SendMessages); 31 | const embedLinks = channel.permissionsFor(client.user).has(PermissionsBitField.Flags.EmbedLinks); 32 | const viewChan = channel.permissionsFor(client.user).has(PermissionsBitField.Flags.ViewChannel); 33 | const everyone = channel.permissionsFor(client.user).has(PermissionsBitField.Flags.MentionEveryone); 34 | const readMsg = channel.permissionsFor(client.user).has(PermissionsBitField.Flags.ReadMessageHistory); 35 | 36 | message += `- Send Message: ${sendMsg ? "✅" : "❌"}\n`; 37 | message += `- Embed Links: ${embedLinks ? "✅" : "❌"}\n`; 38 | message += `- View Channel: ${viewChan ? "✅" : "❌"}\n`; 39 | message += `- Mention everyone: ${everyone ? "✅" : "❌"}\n`; 40 | message += `- Read Message History: ${readMsg ? "✅" : "❌"}`; 41 | 42 | await interaction.editReply({content: message}); 43 | } 44 | } -------------------------------------------------------------------------------- /public/assets/js/navbar.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; // Start of use strict 3 | 4 | var mainNav = document.querySelector('#mainNav'); 5 | 6 | if (mainNav) { 7 | 8 | // Collapse Navbar 9 | var collapseNavbar = function() { 10 | 11 | var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop; 12 | 13 | if (scrollTop > 100) { 14 | mainNav.classList.add("navbar-shrink"); 15 | } else { 16 | mainNav.classList.remove("navbar-shrink"); 17 | } 18 | }; 19 | // Collapse now if page is not at top 20 | collapseNavbar(); 21 | // Collapse the navbar when page is scrolled 22 | document.addEventListener("scroll", collapseNavbar); 23 | } 24 | 25 | // bageutteBox init 26 | if (document.getElementsByClassName('popup-gallery').length > 0) { 27 | baguetteBox.run('.popup-gallery', { animation: 'slideIn' }); 28 | } 29 | 30 | function initParallax() { 31 | 32 | if (!('requestAnimationFrame' in window)) return; 33 | if (/Mobile|Android/.test(navigator.userAgent)) return; 34 | 35 | var parallaxItems = document.querySelectorAll('[data-bss-parallax]'); 36 | 37 | if (!parallaxItems.length) return; 38 | 39 | var defaultSpeed = 0.5; 40 | var visible = []; 41 | var scheduled; 42 | 43 | window.addEventListener('scroll', scroll); 44 | window.addEventListener('resize', scroll); 45 | 46 | scroll(); 47 | 48 | function scroll() { 49 | 50 | visible.length = 0; 51 | 52 | for (var i = 0; i < parallaxItems.length; i++) { 53 | var rect = parallaxItems[i].getBoundingClientRect(); 54 | var speed = parseFloat(parallaxItems[i].getAttribute('data-bss-parallax-speed'), 10) || defaultSpeed; 55 | 56 | if (rect.bottom > 0 && rect.top < window.innerHeight) { 57 | visible.push({ 58 | speed: speed, 59 | node: parallaxItems[i] 60 | }); 61 | } 62 | 63 | } 64 | 65 | cancelAnimationFrame(scheduled); 66 | 67 | if (visible.length) { 68 | scheduled = requestAnimationFrame(update); 69 | } 70 | 71 | } 72 | 73 | function update() { 74 | 75 | for (var i = 0; i < visible.length; i++) { 76 | var node = visible[i].node; 77 | var speed = visible[i].speed; 78 | 79 | node.style.transform = 'translate3d(0, ' + (-window.scrollY * speed) + 'px, 0)'; 80 | } 81 | 82 | } 83 | } 84 | 85 | initParallax(); 86 | })(); // End of use strict 87 | 88 | -------------------------------------------------------------------------------- /twitchalerts.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 15.8 (Debian 15.8-1.pgdg120+1) 6 | -- Dumped by pg_dump version 15.8 (Debian 15.8-1.pgdg120+1) 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | SET default_tablespace = ''; 20 | 21 | SET default_table_access_method = heap; 22 | 23 | -- 24 | -- Name: alerts; Type: TABLE; Schema: public; Owner: twitchalerts 25 | -- 26 | 27 | CREATE TABLE public.alerts ( 28 | guild_id bigint NOT NULL, 29 | streamer_id bigint NOT NULL, 30 | alert_channel bigint NOT NULL, 31 | alert_message bigint, 32 | alert_start text NOT NULL, 33 | alert_end text NOT NULL, 34 | alert_pref_display_game boolean DEFAULT true NOT NULL, 35 | alert_pref_display_viewers boolean DEFAULT true NOT NULL 36 | ); 37 | 38 | 39 | ALTER TABLE public.alerts OWNER TO twitchalerts; 40 | 41 | -- 42 | -- Name: guilds; Type: TABLE; Schema: public; Owner: twitchalerts 43 | -- 44 | 45 | CREATE TABLE public.guilds ( 46 | guild_id bigint NOT NULL, 47 | guild_language character varying(10) DEFAULT 'default'::character varying NOT NULL 48 | ); 49 | 50 | 51 | ALTER TABLE public.guilds OWNER TO twitchalerts; 52 | 53 | -- 54 | -- Name: streamers; Type: TABLE; Schema: public; Owner: twitchalerts 55 | -- 56 | 57 | CREATE TABLE public.streamers ( 58 | streamer_id bigint NOT NULL, 59 | streamer_live boolean DEFAULT false NOT NULL 60 | ); 61 | 62 | 63 | ALTER TABLE public.streamers OWNER TO twitchalerts; 64 | 65 | -- 66 | -- Name: alerts alerts_pkey; Type: CONSTRAINT; Schema: public; Owner: twitchalerts 67 | -- 68 | 69 | ALTER TABLE ONLY public.alerts 70 | ADD CONSTRAINT alerts_pkey PRIMARY KEY (guild_id, streamer_id); 71 | 72 | 73 | -- 74 | -- Name: guilds guilds_pkey; Type: CONSTRAINT; Schema: public; Owner: twitchalerts 75 | -- 76 | 77 | ALTER TABLE ONLY public.guilds 78 | ADD CONSTRAINT guilds_pkey PRIMARY KEY (guild_id); 79 | 80 | 81 | -- 82 | -- Name: streamers streamers_pkey; Type: CONSTRAINT; Schema: public; Owner: twitchalerts 83 | -- 84 | 85 | ALTER TABLE ONLY public.streamers 86 | ADD CONSTRAINT streamers_pkey PRIMARY KEY (streamer_id); 87 | 88 | 89 | -- 90 | -- Name: alerts alerts_guild_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: twitchalerts 91 | -- 92 | 93 | ALTER TABLE ONLY public.alerts 94 | ADD CONSTRAINT alerts_guild_id_fkey FOREIGN KEY (guild_id) REFERENCES public.guilds(guild_id); 95 | 96 | 97 | -- 98 | -- Name: alerts alerts_streamer_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: twitchalerts 99 | -- 100 | 101 | ALTER TABLE ONLY public.alerts 102 | ADD CONSTRAINT alerts_streamer_id_fkey FOREIGN KEY (streamer_id) REFERENCES public.streamers(streamer_id); 103 | 104 | 105 | -- 106 | -- PostgreSQL database dump complete 107 | -- 108 | 109 | -------------------------------------------------------------------------------- /public/assets/js/language.js: -------------------------------------------------------------------------------- 1 | const defaultLocale = "en"; 2 | const supportedLocales = ["fr", "en", "cs", "de"]; 3 | 4 | let locale; 5 | let translations = {}; 6 | 7 | const initialLocale = supportedOrDefault(browserLocales(true)); 8 | 9 | $(() => { 10 | setLocale(initialLocale); 11 | 12 | bindLocaleSwitcher(initialLocale); 13 | }); 14 | 15 | /** 16 | * Load translations for the given locale and translate 17 | * the page to this locale 18 | * 19 | * @param {string} newLocale 20 | */ 21 | async function setLocale(newLocale) { 22 | if (newLocale === locale) return; 23 | 24 | const newTranslations = await fetchTranslationsFor(newLocale); 25 | 26 | locale = newLocale; 27 | translations = newTranslations; 28 | 29 | $("html").attr("lang", locale); 30 | localStorage.setItem("lang", locale); 31 | 32 | translatePage(); 33 | } 34 | 35 | // Retrieve translations JSON object for the given 36 | // locale over the network 37 | function fetchTranslationsFor(newLocale) { 38 | return $.get(`/assets/js/translations/${newLocale}.json`); 39 | } 40 | 41 | // Replace the inner text of each element that has a 42 | // data-i18n-key attribute with the translation corresponding 43 | // to its data-i18n-key 44 | function translatePage() { 45 | $("[data-i18n]").each(function () { 46 | $(this).text(translations[$(this).attr("data-i18n")]); 47 | }); 48 | $("[data-i18n-label]").each(function () { 49 | $(this).attr("data-label", translations[$(this).attr("data-i18n-label")]); 50 | }); 51 | $("[data-i18n-placeholder]").each(function () { 52 | $(this).attr("placeholder", translations[$(this).attr("data-i18n-placeholder")]); 53 | }); 54 | } 55 | 56 | //// LANGUAGE SWITCHING 57 | 58 | /** 59 | * Whenever the user selects a new locale, we 60 | * load the locale's translations and update 61 | * the page 62 | * 63 | * @param {string} initialValue 64 | */ 65 | function bindLocaleSwitcher(initialValue) { 66 | $("#translation-switcher") 67 | .val(initialValue) 68 | .on('change', function () { 69 | setLocale($(this).val()); 70 | }); 71 | } 72 | 73 | //// DETECT THE USER'S PREFERRED LANGUAGE 74 | 75 | /** 76 | * Return True if the given locale is in the supported locales array 77 | * 78 | * @param {string} locale 79 | * @return {boolean} 80 | */ 81 | function isSupported(locale) { 82 | return supportedLocales.indexOf(locale) > -1; 83 | } 84 | 85 | /** 86 | * Retrieve the first locale we support from the given 87 | * array, or return our default locale 88 | * 89 | * @param {Array} locales 90 | * @return {string} 91 | */ 92 | function supportedOrDefault(locales) { 93 | return localStorage.getItem("lang") || locales.find(isSupported) || defaultLocale; 94 | } 95 | 96 | /** 97 | * Retrieve user-preferred locales from the browser 98 | * 99 | * @param {boolean} languageCodeOnly - when true, returns 100 | * ["en", "fr"] instead of ["en-US", "fr-FR"] 101 | * @returns {Array | undefined} 102 | */ 103 | function browserLocales(languageCodeOnly = false) { 104 | return navigator.languages.map((locale) => 105 | languageCodeOnly ? locale.split("-")[0] : locale, 106 | ); 107 | } -------------------------------------------------------------------------------- /languages/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} is STREAMING", 3 | "START": "Started", 4 | "STATUS": "Status", 5 | "GAME": "Game", 6 | "LENGTH": "Duration", 7 | "LENGTH_TIME": "{hours} h {minutes} min", 8 | "VIEWERS": "Viewers", 9 | "LIVE_END": "Stream ended", 10 | "STATS_TITLE": "Statistics", 11 | "STATS_MEMORY": "Memory Usage", 12 | "STATS_UPTIME": "Uptime", 13 | "STATS_USERS": "Users", 14 | "STATS_SERVERS": "Servers", 15 | "STATS_CHANNELS": "Channels", 16 | "INFO_TITLE": "Information and help", 17 | "INFO_DESCRIPTION": "This bot allows you to display an alert during a Twitch live, and to update the message frequently. Messages and language are customizable. Start with {setup} to create a new alert.", 18 | "INFO_PERMISSIONS": "Change permissions", 19 | "INFO_PERMISSIONS_DESC": "By default, only people with the `Manage Server` permission can set up the bot. To allow other people to run commands, go to server settings, then in the `Integrations` tab change the permissions associated with Twitch Alerts commands.", 20 | "INFO_ERROR": "Alerts are not displayed", 21 | "INFO_ERROR_DESC": "If no alerts appear after a minute, while the streamer is live, check that the bot has the following permissions in the alerts channel: `View Channels`, `Send Messages ` and `Embed Links`", 22 | "INFO_LANGUAGE": "Change language", 23 | "INFO_LANGUAGE_DESC": "By default, alerts adapt to the server language. To change the server language, access the server settings, community tab. If you want to change the language of the alerts without changing the server language, use the command {language}. Commands will always be displayed in the language of your Discord.", 24 | "INFO_TRANSLATE": "My language does not appear", 25 | "INFO_TRANSLATE_DESC": "If your language does not appear, you can contribute by translating the bot on [OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)", 26 | "INFO_INVITE": "Invite the bot", 27 | "INFO_SUPPORT": "Support server", 28 | "SETUP_TITLE": "Setup a new alert", 29 | "SETUP_STREAMER": "Twitch channel", 30 | "SETUP_START": "Alert message on stream starting", 31 | "SETUP_START_PLACEHOLDER": "Stream is starting @everyone !", 32 | "SETUP_END": "End stream message", 33 | "SETUP_END_PLACEHOLDER": "Stream is over. Watch the replay:", 34 | "SETUP_NO_RESULT": "No result. Are you sure you typed the username correctly ?", 35 | "SETUP_ALREADY": "There is already an alert with this user. You can delete it with {command}", 36 | "SETUP_NO_PERMISSIONS": "I don't have permission to send a text message in this channel... Please verify the permissions, and retry.", 37 | "SETUP_SUCCESS": "Success! Alerts will come when the next stream will start, or soon if it is already launched.", 38 | "DELETE_EMPTY": "No alert have been setup. Start with {command}", 39 | "DELETE_CHOOSE_TITLE": "Please choose an alert to delete", 40 | "DELETE_CHOOSE_PLACEHOLDER": "Choose an alert to delete", 41 | "DELETE_SUCCESS": "Removal performed successfully", 42 | "DELETE_CHANNEL_NOT_FOUND": "Channel no. {canalid} not found", 43 | "LANGUAGE_UPDATE": "The language of the alerts has been defined to {language}.", 44 | "DATABASE_ERROR": "Error with the database!", 45 | "SETUP_CMD_NAME": "setup", 46 | "SETUP_CMD_DESC": "Add a new alert in the current chanel", 47 | "DELETE_CMD_NAME": "delete", 48 | "DELETE_CMD_DESC": "Delete an alert from the server", 49 | "LANGUAGE_CMD_NAME": "language", 50 | "LANGUAGE_CMD_DESC": "Change the language of the alerts", 51 | "LANGUAGE_OPTION_NAME": "value", 52 | "LANGUAGE_OPTION_DESC": "The language to select for the alerts", 53 | "LANGUAGE_DEFAULT": "Server language", 54 | "INFO_CMD_NAME": "info", 55 | "INFO_CMD_DESC": "Show Twitch Alerts bot information and help", 56 | "LANGUAGE": "English" 57 | } 58 | -------------------------------------------------------------------------------- /modules/language.js: -------------------------------------------------------------------------------- 1 | const {readdirSync} = require("fs"); 2 | const logger = require("./logger"); 3 | 4 | // Initialisation of languages 5 | const languages = new Map(); 6 | const langs = readdirSync("./languages/").filter(file => file.endsWith(".json")); 7 | for (const lang of langs) { 8 | const json = require(`../languages/${lang}`); 9 | const language = new Map(); 10 | for (const key of Object.keys(json)) 11 | language.set(key, json[key]); 12 | languages.set(lang.split(".json")[0], language); 13 | } 14 | 15 | function createString(string, vars = {}) { 16 | for (const key of Object.keys(vars)) { 17 | string = string.replaceAll(`{${key}}`, vars[key]); 18 | } 19 | return string; 20 | } 21 | 22 | function getLocalizedString(language) { 23 | const lang = languages.get(language) ?? languages.get("en-US"); 24 | return function (id, vars = {}) { 25 | const string = lang.get(id) ?? ""; 26 | return createString(string, vars); 27 | } 28 | } 29 | 30 | function getString(language, id, vars = {}) { 31 | const lang = languages.get(language) ?? languages.get("en-US"); 32 | const string = lang.get(id) ?? ""; 33 | return createString(string, vars); 34 | } 35 | 36 | const regex = new RegExp('^[-_\\p{L}\\p{N}\\p{sc=Deva}\\p{sc=Thai}]{1,32}$', 'um') 37 | 38 | module.exports = { 39 | languagesList: languages, 40 | 41 | getLocalizedString, 42 | 43 | getString, 44 | 45 | translateCommand: (command, commandName) => { 46 | if (command.conf.translate) { 47 | let objects = [command.commandData] 48 | while (objects.length !== 0) { 49 | let obj = objects.shift(); 50 | let names = {} 51 | let descs = {} 52 | for (const [lang, _] of languages) { 53 | if ("type" in obj) { 54 | names[lang] = getString(lang, `${commandName.toUpperCase()}_OPTION_NAME`).toLocaleLowerCase().replaceAll(" ", "_").substring(0, 32); 55 | descs[lang] = getString(lang, `${commandName.toUpperCase()}_OPTION_DESC`) 56 | if (descs[lang].length > 100) { 57 | descs[lang] = descs[lang].substring(0, 96) + " ..." 58 | } 59 | 60 | if (! regex.test(names[lang])) { 61 | logger.error(`Please check string ${commandName.toUpperCase()}_OPTION_NAME (${names[lang]}) in file ${lang}.json : Regex not matched by Discord`); 62 | names[lang] = obj.name; 63 | } 64 | } 65 | else if ("description" in obj) { 66 | names[lang] = getString(lang, `${commandName.toUpperCase()}_CMD_NAME`).toLocaleLowerCase().replaceAll(" ", "_").substring(0, 32); 67 | descs[lang] = getString(lang, `${commandName.toUpperCase()}_CMD_DESC`) 68 | if (descs[lang].length > 100) { 69 | descs[lang] = descs[lang].substring(0, 96) + " ..." 70 | } 71 | 72 | if (! regex.test(names[lang])) { 73 | logger.error(`Please check string ${commandName.toUpperCase()}_CMD_NAME} (${names[lang]}) in file ${lang}.json : Regex not matched by Discord`); 74 | names[lang] = obj.name; 75 | } 76 | } 77 | if (names[lang] === "") { 78 | names[lang] = obj.name; 79 | } 80 | 81 | if (descs[lang] === "") { 82 | descs[lang] = obj.description; 83 | } 84 | 85 | 86 | } 87 | obj.name_localizations = names; 88 | obj.description_localizations = descs; 89 | 90 | if (obj.options) obj.options.forEach(opt => objects.push(opt)); 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /languages/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} está en DIRECTO", 3 | "START": "Empezó", 4 | "STATUS": "Estado", 5 | "GAME": "Juego", 6 | "LENGTH": "Duración", 7 | "LENGTH_TIME": "{hours} h {minutes} min", 8 | "VIEWERS": "Espectadores", 9 | "LIVE_END": "Stream finalizado", 10 | "STATS_TITLE": "Estadísticas", 11 | "STATS_MEMORY": "Uso de memoria", 12 | "STATS_UPTIME": "Tiempo activo", 13 | "STATS_USERS": "Usuarios", 14 | "STATS_SERVERS": "Servidores", 15 | "STATS_CHANNELS": "Canales", 16 | "INFO_TITLE": "Información y ayuda", 17 | "INFO_DESCRIPTION": "Este bot le permite mostrar una alerta durante un Twitch en vivo y actualizar el mensaje con frecuencia. Los mensajes y el idioma son personalizables. Comience con {setup} para crear una nueva alerta.", 18 | "INFO_PERMISSIONS": "Cambiar permisos", 19 | "INFO_PERMISSIONS_DESC": "De forma predeterminada, solo las personas con el permiso `Administrar servidor` pueden configurar el bot. Para permitir que otras personas ejecuten comandos, vaya a la configuración del servidor, luego en la pestaña `Integraciones` cambie los permisos asociados con los comandos de Twitch Alerts.", 20 | "INFO_ERROR": "No se muestran las alertas", 21 | "INFO_ERROR_DESC": "Si no aparecen las alertas después de un minuto, mientras la transmisión aún está activa, verifique que el bot tiene los permisos `Ver canales`, `Enviar mensajes` e `Insertar enlaces` en las alertas del canal.", 22 | "INFO_LANGUAGE": "Cambiar idioma", 23 | "INFO_LANGUAGE_DESC": "Por defecto, las alertas se adaptan al idioma del servidor. Para cambiar el idioma del servidor, acceda a la configuración del servidor, pestaña de comunidad. Si desea cambiar el idioma de las alertas sin cambiar el idioma del servidor, use el comando {language}. Los comandos siempre se mostrarán en el idioma de tu Discord.", 24 | "INFO_TRANSLATE": "Mi idioma no está en la lista", 25 | "INFO_TRANSLATE_DESC": "Si tu idioma no aparece en la lista, puedes contribuir traduciendo el bot en [OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)", 26 | "INFO_INVITE": "Invitar al bot", 27 | "INFO_SUPPORT": "Servidor de soporte", 28 | "SETUP_TITLE": "Configurar alerta nueva", 29 | "SETUP_STREAMER": "Canal de Twitch", 30 | "SETUP_START": "Mensaje de alerta al inicio del directo", 31 | "SETUP_START_PLACEHOLDER": "¡Comienza el directo @everyone!", 32 | "SETUP_END": "Mensaje al finalizar el directo", 33 | "SETUP_END_PLACEHOLDER": "Se acabó el Stream. Video:", 34 | "SETUP_NO_RESULT": "Sin coincidencias. Asegurese de que el nombre de usuario es el correcto.", 35 | "SETUP_ALREADY": "Ya hay una alerta con ese nombre de usuario. La puedes eliminar con {command}", 36 | "SETUP_NO_PERMISSIONS": "No tengo permiso para enviar mensajes de texto en este canal... Por favor, verifica los permisos e intentalo de nuevo.", 37 | "SETUP_SUCCESS": "¡Éxito! Las alertas llegarán cuando comience la próxima transmisión, o en breve si ya comenzó.", 38 | "DELETE_EMPTY": "No se ha configurado ninguna alerta. Comience con {command}", 39 | "DELETE_CHOOSE_TITLE": "Por favor, seleccione la alerta que desee eliminar", 40 | "DELETE_CHOOSE_PLACEHOLDER": "Elija la alerta a borrar", 41 | "DELETE_SUCCESS": "Alerta eliminada", 42 | "DELETE_CHANNEL_NOT_FOUND": "Canal número {canalid} no encontrado", 43 | "LANGUAGE_UPDATE": "Se ha establecido el idioma de las alertas al {language}.", 44 | "DATABASE_ERROR": "¡Error de base de datos!", 45 | "SETUP_CMD_NAME": "añadir", 46 | "SETUP_CMD_DESC": "Crea una alerta nueva en el canal actual", 47 | "DELETE_CMD_NAME": "borrar", 48 | "DELETE_CMD_DESC": "Borra una alerta del servidor", 49 | "LANGUAGE_CMD_NAME": "idioma", 50 | "LANGUAGE_CMD_DESC": "Cambia el idioma de las alertas", 51 | "LANGUAGE_OPTION_NAME": "valor", 52 | "LANGUAGE_OPTION_DESC": "El idioma a elegir para las alertas", 53 | "LANGUAGE_DEFAULT": "Idioma del servidor", 54 | "INFO_CMD_NAME": "info", 55 | "INFO_CMD_DESC": "Muestra la información y ayuda del bot Twitch Alerts", 56 | "LANGUAGE": "Español (España)" 57 | } 58 | -------------------------------------------------------------------------------- /controllers/setupController.js: -------------------------------------------------------------------------------- 1 | const {TextInputBuilder, ActionRowBuilder, ModalBuilder, PermissionsBitField} = require("discord.js"); 2 | const logger = require("../modules/logger"); 3 | 4 | module.exports = class SetupController { 5 | static async setup(client, interaction) { 6 | if (!interaction.channel.permissionsFor(client.user).has([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.EmbedLinks, PermissionsBitField.Flags.ViewChannel])) { 7 | logger.debug(`Missing bot permissions in channel ${interaction.channel.id} in guild ${interaction.guild.id}`); 8 | if (!client.container.debug) await interaction.reply({ 9 | content: interaction.getLocalizedString("SETUP_NO_PERMISSIONS"), 10 | ephemeral: true 11 | }); 12 | return; 13 | } 14 | 15 | const inputStreamer = new TextInputBuilder() 16 | .setCustomId("streamer") 17 | .setLabel(interaction.getLocalizedString("SETUP_STREAMER")) 18 | .setStyle(1) 19 | .setPlaceholder("harfeur") 20 | .setRequired(true); 21 | const inputStart = new TextInputBuilder() 22 | .setCustomId("start") 23 | .setLabel(interaction.getLocalizedString("SETUP_START")) 24 | .setStyle(2) 25 | .setPlaceholder(interaction.getLocalizedString("SETUP_START_PLACEHOLDER")) 26 | .setRequired(true); 27 | const inputEnd = new TextInputBuilder() 28 | .setCustomId("end") 29 | .setLabel(interaction.getLocalizedString("SETUP_END")) 30 | .setStyle(2) 31 | .setPlaceholder(interaction.getLocalizedString("SETUP_END_PLACEHOLDER")) 32 | .setRequired(true); 33 | 34 | const row1 = new ActionRowBuilder() 35 | .addComponents(inputStreamer); 36 | const row2 = new ActionRowBuilder() 37 | .addComponents(inputStart); 38 | const row3 = new ActionRowBuilder() 39 | .addComponents(inputEnd); 40 | 41 | const modal = new ModalBuilder() 42 | .setTitle(interaction.getLocalizedString("SETUP_TITLE")) 43 | .setCustomId("setup_modal") 44 | .addComponents(row1, row2, row3); 45 | 46 | if (!client.container.debug) await interaction.showModal(modal); 47 | logger.debug(`Setup modal sent to ${interaction.user.id} in ${interaction.guild.id}`); 48 | } 49 | 50 | static async modalSubmit(client, interaction) { 51 | if (!client.container.debug) await interaction.deferReply({ephemeral: true}); 52 | let userName = interaction.fields.getTextInputValue("streamer"); 53 | if (userName.includes("twitch.tv/")) { 54 | userName = userName.split("twitch.tv/")[1]; 55 | } 56 | const user = await client.container.twitch.users.getUserByName(userName); 57 | if (!user) { 58 | logger.debug(`Failed to retrieve streamer ${interaction.fields.getTextInputValue("streamer")}`); 59 | if (!client.container.debug) await interaction.editReply(interaction.getLocalizedString("SETUP_NO_RESULT")); 60 | return; 61 | } 62 | 63 | const alert = await client.container.pg.getAlert(interaction.guild.id, user.id); 64 | if (alert.length !== 0) { 65 | logger.debug(`Streamer ${user.displayName} already existing in server ${interaction.guild.id}`); 66 | if (!client.container.debug) await interaction.editReply(interaction.getLocalizedString("SETUP_ALREADY", { 67 | command: ` c.name==="delete").id}>` 68 | })); 69 | return; 70 | } 71 | 72 | const messageLIVE = interaction.fields.getTextInputValue("start"); 73 | const messageFIN = interaction.fields.getTextInputValue("end"); 74 | 75 | if (!client.container.debug) { 76 | await client.container.pg.addAlert(interaction.guild.id, user.id, interaction.channel.id, messageLIVE, messageFIN, true, true); 77 | await interaction.editReply(interaction.getLocalizedString("SETUP_SUCCESS")); 78 | } 79 | 80 | logger.debug(`New alert added for ${user.displayName} in guild ${interaction.guild.id}`); 81 | } 82 | } -------------------------------------------------------------------------------- /languages/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} is gaan Streamen", 3 | "START": "Gestart", 4 | "STATUS": "Status", 5 | "GAME": "Spel", 6 | "LENGTH": "Lengte", 7 | "LENGTH_TIME": "{hours} u. {minutes} sec.", 8 | "VIEWERS": "Kijkers", 9 | "LIVE_END": "Stream is afgelopen.", 10 | "STATS_TITLE": "Statistieken", 11 | "STATS_MEMORY": "Geheugen gebruik", 12 | "STATS_UPTIME": "Werktijd", 13 | "STATS_USERS": "Gebruikers", 14 | "STATS_SERVERS": "Servers", 15 | "STATS_CHANNELS": "Kanalen", 16 | "INFO_TITLE": "Informatie en hulp:", 17 | "INFO_DESCRIPTION": "Met Twitch Alerts *BOT* krijg je tijdens de Twitch-Stream de notificatie en het bericht word regelmatig bijgewerkt. De berichten en taal zijn aanpasbaar. Begin met {setup} om een nieuwe notificatie te maken.", 18 | "INFO_PERMISSIONS": "Pas de Discord permissie's aan.", 19 | "INFO_PERMISSIONS_DESC": "Als Standaard mogen personen met de machtiging `Server beheren` Twitch Alerts *BOT* instellen. Om andere personen toegang te geven om commando's uit te voeren, ga je naar de serverinstellingen en wijzig je op het tabblad 'Integraties' de machtigingen die zijn gekoppeld aan Twitch Alerts commando's.", 20 | "INFO_ERROR": "Twitch notificaties worden niet weergegeven.", 21 | "INFO_ERROR_DESC": "Worden er geen Twitch notificaties weergegeven na een tijdje, terwijl de Streamer wel streamt, controleer dan de volgende permissies in het Notificatie kanaal: 'Kanaal bekijken' , 'Berichten verzenden' en 'Ingesloten links'", 22 | "INFO_LANGUAGE": "Selecteer U taal:", 23 | "INFO_LANGUAGE_DESC": "Als standaard worden de notificaties aangepast naar de server taal, Om de servertaal te wijzigen, gaat u naar de serverinstellingen, tab Community. Als u de taal van de waarschuwingen wilt wijzigen zonder de servertaal te wijzigen, gebruikt u de opdracht {language}. Commando's worden altijd weergegeven in de taal van je Discord.", 24 | "INFO_TRANSLATE": "Mijn taal wordt niet staat er niet tussen?", 25 | "INFO_TRANSLATE_DESC": "Als je taal er niet tussen staat, kan je ons mee helpen Twitch Alerts *BOT* te vertalen via [OneSky} (https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)", 26 | "INFO_INVITE": "Nodig de Twitch Alerts *BOT* uit!", 27 | "INFO_SUPPORT": "Ondersteuning Server", 28 | "SETUP_TITLE": "Maak een nieuwe Twitch notificatie aan!", 29 | "SETUP_STREAMER": "Twitch notificatie kanaal", 30 | "SETUP_START": "Twitch notificatie wanneer de Streamer begint,", 31 | "SETUP_START_PLACEHOLDER": "Streamer is begonnen @everyone", 32 | "SETUP_END": "De stream is afgelopen.", 33 | "SETUP_END_PLACEHOLDER": "De stream is afgelopen! Hier kan je de samenvatting vinden:", 34 | "SETUP_NO_RESULT": "Geen resultaat gevonden. Is de Streamernaam juist heb ingetypt?", 35 | "SETUP_ALREADY": "Er is al een Twitch notificatie voor deze Streamer. U kunt deze verwijderen door {command}", 36 | "SETUP_NO_PERMISSIONS": "Ik heb niet de juiste rechten om een bericht in de dit kanaal te plaatsen... Controleer de permissies en probeer opnieuw.", 37 | "SETUP_SUCCESS": "Gelukt! Twitch notificaties wordt gestart wanneer de Stream begint, of wanneer de streamer al gestart is.", 38 | "DELETE_EMPTY": "Nog geen Twitch notificaties ge\u00efnstalleerd. Begin hier met {command}", 39 | "DELETE_CHOOSE_TITLE": "Selecteer een notificatie om te verwijderen.", 40 | "DELETE_CHOOSE_PLACEHOLDER": "Kies een notificatie om te verwijderen.", 41 | "DELETE_SUCCESS": "Succesvol verwijderd", 42 | "DELETE_CHANNEL_NOT_FOUND": "kanaal no. {canalid} niet gevonden", 43 | "LANGUAGE_UPDATE": "De taal van de notificaties zijn gedefinieerd naar {language}.", 44 | "DATABASE_ERROR": "een fout in de data-bank!", 45 | "SETUP_CMD_NAME": "installeer", 46 | "SETUP_CMD_DESC": "Voeg een nieuwe notificatie toe aan het huidige kanaal", 47 | "DELETE_CMD_NAME": "verwijder", 48 | "DELETE_CMD_DESC": "Verwijder een notificatie van deze server", 49 | "LANGUAGE_CMD_NAME": "taal", 50 | "LANGUAGE_CMD_DESC": "Verander de taal van de notificaties", 51 | "LANGUAGE_OPTION_NAME": "waarde", 52 | "LANGUAGE_OPTION_DESC": "De taal te selecteren voor de notificaties", 53 | "LANGUAGE_DEFAULT": "Server taal", 54 | "INFO_CMD_NAME": "informatie", 55 | "INFO_CMD_DESC": "Toon Twitch Alerts informatie en help", 56 | "LANGUAGE": "Nederlands" 57 | } -------------------------------------------------------------------------------- /languages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} STREAMT", 3 | "START": "Begonnen", 4 | "STATUS": "Status", 5 | "GAME": "Spiel", 6 | "LENGTH": "Dauer", 7 | "LENGTH_TIME": "{hours} h {minutes} min", 8 | "VIEWERS": "Zuschauer", 9 | "LIVE_END": "Stream beendet", 10 | "STATS_TITLE": "Statistiken", 11 | "STATS_MEMORY": "Speichernutzung", 12 | "STATS_UPTIME": "Vergangene Zeit", 13 | "STATS_USERS": "Benutzer", 14 | "STATS_SERVERS": "Server", 15 | "STATS_CHANNELS": "Kan\u00e4le", 16 | "INFO_TITLE": "Informationen und Hilfe", 17 | "INFO_DESCRIPTION": "Dieser Bot erm\u00f6glicht dir, einen Alert w\u00e4hrend eines Twitch-Livestreams anzuzeigen und die Nachricht regelm\u00e4\u00dfig zu aktualisieren. Nachrichten und Sprachen sind anpassbar. Beginne mit {setup}, um einen neuen Alert zu erstellen.", 18 | "INFO_PERMISSIONS": "Berechtigungen ver\u00e4ndern", 19 | "INFO_PERMISSIONS_DESC": "Standardm\u00e4\u00dfig k\u00f6nnen nur Leute mit der Berechtigung `Server verwalten` den Bot verwalten. Um anderen Leuten das Ausf\u00fchren von Befehlen zu erlauben, gehe in die Servereinstellungen, dann zu `Integrationen` und \u00e4ndere die Berechtigungen bez\u00fcglich Twitch Alerts Befehle.", 20 | "INFO_ERROR": "Alerts werden nicht angezeigt", 21 | "INFO_ERROR_DESC": "Wenn nach einer Minute kein Alert erscheint w\u00e4hrend der Streamer live ist, stelle sicher, dass der Bot die folgenden Berechtigungen im Alert-Kanal hat: `Kan\u00e4le ansehen`, `Nachrichten versenden` und `Links einbetten`", 22 | "INFO_LANGUAGE": "Sprache \u00e4ndern", 23 | "INFO_LANGUAGE_DESC": "Standardm\u00e4\u00dfig werden Alerts der Serversprache angepasst. Um die Serversprache zu \u00e4ndern, gelange in die Servereinstellungen in den Community-Tab. Wenn du die Sprache der Alerts \u00e4ndern m\u00f6chtest, ohne dabei die Serversprache zu ver\u00e4ndern, nutze den Befehl {language}. Befehle werden immer in der Sprache deines Discords angezeigt.", 24 | "INFO_TRANSLATE": "Meine Sprache ist nicht aufgelistet", 25 | "INFO_TRANSLATE_DESC": "Wenn deine Sprache nicht aufgelistet ist, kannst du mit beitragen, den Bot auf [OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943) zu \u00fcbersetzen", 26 | "INFO_INVITE": "Bot einladen", 27 | "INFO_SUPPORT": "Support-Server", 28 | "SETUP_TITLE": "Neuen Alert einrichten", 29 | "SETUP_STREAMER": "Twitch-Kanal", 30 | "SETUP_START": "Alert-Nachricht bei Beginn des Streams", 31 | "SETUP_START_PLACEHOLDER": "Stream beginnt @everyone !", 32 | "SETUP_END": "Nachricht bei Streamende", 33 | "SETUP_END_PLACEHOLDER": "Der Stream ist zu Ende. Gucke die Wiederholung:", 34 | "SETUP_NO_RESULT": "Kein Ergebnis. Bist du sicher, dass du den Nutzernamen korrekt getippt hast?", 35 | "SETUP_ALREADY": "Es gibt bereits einen Alert mit diesem Benutzer. Du kannst ihn mit {command} l\u00f6schen.", 36 | "SETUP_NO_PERMISSIONS": "Ich habe keine Berechtigung, um Nachrichten in diesem Kanal zu senden. Bitte \u00fcberpr\u00fcfe die Berechtigungen und versuche es erneut.", 37 | "SETUP_SUCCESS": "Erfolg! Alerts werden kommen, sobald der n\u00e4chste Stream startet oder bald, falls er bereits startete.", 38 | "DELETE_EMPTY": "Noch keine Alerts erstellt. Beginne mit {command}", 39 | "DELETE_CHOOSE_TITLE": "Bitte w\u00e4hle den Alert, der gel\u00f6scht werden soll", 40 | "DELETE_CHOOSE_PLACEHOLDER": "W\u00e4hle den Alert, der gel\u00f6scht werden soll", 41 | "DELETE_SUCCESS": "Erfolgreich gel\u00f6scht", 42 | "DELETE_CHANNEL_NOT_FOUND": "Kanal {canalid} nicht gefunden", 43 | "LANGUAGE_UPDATE": "Die Sprache der Alerts wurde zu {language} ge\u00e4ndert.", 44 | "DATABASE_ERROR": "Fehler mit der Datenbank!", 45 | "SETUP_CMD_NAME": "Einrichtung", 46 | "SETUP_CMD_DESC": "F\u00fcge einen neuen Alert in den aktuellen Kanal hinzu", 47 | "DELETE_CMD_NAME": "L\u00f6schen", 48 | "DELETE_CMD_DESC": "L\u00f6sche einen Alert vom Server", 49 | "LANGUAGE_CMD_NAME": "Sprache", 50 | "LANGUAGE_CMD_DESC": "\u00c4ndere die Sprache der Alerts", 51 | "LANGUAGE_OPTION_NAME": "Wert", 52 | "LANGUAGE_OPTION_DESC": "Die Sprache, die f\u00fcr die Alerts ausgew\u00e4hlt werden soll", 53 | "LANGUAGE_DEFAULT": "Serversprache", 54 | "INFO_CMD_NAME": "Info", 55 | "INFO_CMD_DESC": "Rufe Twitch Alerts Bot-Informationen und Hilfe auf", 56 | "LANGUAGE": "Deutsch" 57 | } -------------------------------------------------------------------------------- /languages/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} \u00e9 STREAMING", 3 | "START": "Come\u00e7ou", 4 | "STATUS": "Status", 5 | "GAME": "Jogo", 6 | "LENGTH": "Dura\u00e7\u00e3o", 7 | "LENGTH_TIME": "{hours} h {minutes} min", 8 | "VIEWERS": "Espectadores", 9 | "LIVE_END": "Transmiss\u00e3o terminada", 10 | "STATS_TITLE": "Estat\u00edsticas", 11 | "STATS_MEMORY": "Uso da mem\u00f3ria", 12 | "STATS_UPTIME": "Hor\u00e1rio de funcionamento", 13 | "STATS_USERS": "Usu\u00e1rios", 14 | "STATS_SERVERS": "Servidores", 15 | "STATS_CHANNELS": "Canais", 16 | "INFO_TITLE": "Informa\u00e7\u00e3o e ajuda", 17 | "INFO_DESCRIPTION": "Este bot permite exibir um alerta durante um Twitch ao vivo, e atualizar a mensagem com freq\u00fc\u00eancia. As mensagens e o idioma s\u00e3o personaliz\u00e1veis. Comece com {setup} para criar um novo alerta.", 18 | "INFO_PERMISSIONS": "Mudan\u00e7a de permiss\u00f5es", 19 | "INFO_PERMISSIONS_DESC": "Por padr\u00e3o, somente pessoas com permiss\u00e3o do `Gerenciar servidor` podem configurar o bot. Para permitir que outras pessoas executem comandos, v\u00e1 para as configura\u00e7\u00f5es do servidor e, em seguida, na aba `Integra\u00e7\u00f5es` altere as permiss\u00f5es associadas aos comandos de Alertas de Twitch.", 20 | "INFO_ERROR": "Os alertas n\u00e3o s\u00e3o exibidos", 21 | "INFO_ERROR_DESC": "Se nenhum alerta aparecer ap\u00f3s um minuto, enquanto a serpentina estiver ao vivo, verifique se o bot tem as seguintes permiss\u00f5es no canal de alertas: `Ver canais`, `Enviar mensagens` e `Inserir links`.", 22 | "INFO_LANGUAGE": "Mudan\u00e7a de idioma", 23 | "INFO_LANGUAGE_DESC": "Por padr\u00e3o, os alertas se adaptam ao idioma do servidor. Para alterar o idioma do servidor, acesse as configura\u00e7\u00f5es do servidor, aba comunidade. Se voc\u00ea quiser mudar o idioma dos alertas sem mudar o idioma do servidor, use o comando {language}. Os comandos ser\u00e3o sempre exibidos no idioma de seu Discord.", 24 | "INFO_TRANSLATE": "Meu idioma n\u00e3o aparece", 25 | "INFO_TRANSLATE_DESC": "Se seu idioma n\u00e3o aparecer, voc\u00ea pode contribuir traduzindo o bot em [OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)", 26 | "INFO_INVITE": "Convide o bot", 27 | "INFO_SUPPORT": "Servidor de suporte", 28 | "SETUP_TITLE": "Configurar um novo alerta", 29 | "SETUP_STREAMER": "Canal Twitch", 30 | "SETUP_START": "In\u00edcio da mensagem de alerta no fluxo", 31 | "SETUP_START_PLACEHOLDER": "Stream est\u00e1 come\u00e7ando @everyone !", 32 | "SETUP_END": "Mensagem de fluxo final", 33 | "SETUP_END_PLACEHOLDER": "O fluxo de \u00e1gua acabou. Veja a repeti\u00e7\u00e3o:", 34 | "SETUP_NO_RESULT": "Nenhum resultado. Voc\u00ea tem certeza de ter digitado o nome de usu\u00e1rio corretamente?", 35 | "SETUP_ALREADY": "J\u00e1 existe um alerta com este usu\u00e1rio. Voc\u00ea pode apag\u00e1-lo com {command}", 36 | "SETUP_NO_PERMISSIONS": "N\u00e3o tenho permiss\u00e3o para enviar uma mensagem de texto neste canal... Por favor, verifique as permiss\u00f5es, e tente novamente.", 37 | "SETUP_SUCCESS": "Sucessos ! Os alertas vir\u00e3o quando o pr\u00f3ximo fluxo come\u00e7ar, ou logo se j\u00e1 tiver sido lan\u00e7ado.", 38 | "DELETE_EMPTY": "Nenhum alerta foi configurado. Comece com {command}", 39 | "DELETE_CHOOSE_TITLE": "Por favor, escolha um alerta para excluir", 40 | "DELETE_CHOOSE_PLACEHOLDER": "Escolha um alerta para excluir", 41 | "DELETE_SUCCESS": "Remo\u00e7\u00e3o realizada com sucesso", 42 | "DELETE_CHANNEL_NOT_FOUND": "Canal n\u00ba. {canalid} n\u00e3o encontrado", 43 | "LANGUAGE_UPDATE": "A linguagem dos alertas foi definida para {language}.", 44 | "DATABASE_ERROR": "Erro com o banco de dados!", 45 | "SETUP_CMD_NAME": "configura\u00e7\u00e3o", 46 | "SETUP_CMD_DESC": "Adicionar um novo alerta no canal atual", 47 | "DELETE_CMD_NAME": "excluir", 48 | "DELETE_CMD_DESC": "Apagar um alerta do servidor", 49 | "LANGUAGE_CMD_NAME": "idioma", 50 | "LANGUAGE_CMD_DESC": "Alterar o idioma dos alertas", 51 | "LANGUAGE_OPTION_NAME": "valor", 52 | "LANGUAGE_OPTION_DESC": "O idioma a ser selecionado para os alertas", 53 | "LANGUAGE_DEFAULT": "Idioma do servidor", 54 | "INFO_CMD_NAME": "info", 55 | "INFO_CMD_DESC": "Mostrar informa\u00e7\u00f5es e ajuda do Twitch Alerts bot", 56 | "LANGUAGE": "Portugu\u00eas" 57 | } -------------------------------------------------------------------------------- /languages/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} est en LIVE", 3 | "START": "D\u00e9but", 4 | "STATUS": "Statut", 5 | "GAME": "Jeu", 6 | "LENGTH": "Dur\u00e9e", 7 | "LENGTH_TIME": "{hours} h {minutes} min", 8 | "VIEWERS": "Spectateurs", 9 | "LIVE_END": "LIVE termin\u00e9", 10 | "STATS_TITLE": "Statistiques", 11 | "STATS_MEMORY": "Utilisation de la m\u00e9moire", 12 | "STATS_UPTIME": "Temps de fonctionnement", 13 | "STATS_USERS": "Utilisateurs", 14 | "STATS_SERVERS": "Serveurs", 15 | "STATS_CHANNELS": "Canaux", 16 | "INFO_TITLE": "Informations et aide", 17 | "INFO_DESCRIPTION": "Ce bot permet d'afficher une alerte lors d'un live Twitch, et de mettre \u00e0 jour le message fr\u00e9quemment. Les messages et la langue sont personalisables. Commencez avec {setup} pour cr\u00e9er une nouvelle alerte.", 18 | "INFO_PERMISSIONS": "Modifier les permissions", 19 | "INFO_PERMISSIONS_DESC": "Par d\u00e9faut, seules les personnes avec la permission `G\u00e9rer le serveur` peuvent param\u00e9trer le bot. Pour autoriser d'autres personnes \u00e0 ex\u00e9cuter les commandes, acc\u00e9dez aux param\u00e8tres du serveur, puis dans l'onglet `Int\u00e9grations` modifiez les permissions associ\u00e9es aux commandes de Twitch Alerts.", 20 | "INFO_ERROR": "Les alertes ne s'affichent pas", 21 | "INFO_ERROR_DESC": "Si aucune alerte n'apparait au bout d'une minute, alors que le streamer est en live, v\u00e9rifiez que le bot poss\u00e8de les permissions suivantes dans le canal des alertes: `Voir les salons`, `Envoyer des messages`, `Int\u00e9grer des liens` et `Voir les anciens messages`", 22 | "INFO_LANGUAGE": "Changer la langue", 23 | "INFO_LANGUAGE_DESC": "Par d\u00e9faut, les alertes s'adaptent \u00e0 la langue du serveur. Pour modifier la langue du serveur, acc\u00e9dez aux param\u00e8tres du serveur, onglet communaut\u00e9. Si vous souhaitez modifier la langue des alertes sans modifier la langue du serveur, utilisez la commande {language}. Les commandes seront toujours affich\u00e9es dans la langue de votre Discord.", 24 | "INFO_TRANSLATE": "Ma langue n'apparait pas", 25 | "INFO_TRANSLATE_DESC": "Si votre langue n'apparait pas, vous pouvez contribuer en traduisant le bot sur [OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)", 26 | "INFO_INVITE": "Inviter le bot", 27 | "INFO_SUPPORT": "Serveur d'aide", 28 | "SETUP_TITLE": "Param\u00e9trer une nouvelle alerte", 29 | "SETUP_STREAMER": "Chaine Twitch", 30 | "SETUP_START": "Message d'alerte en d\u00e9but de LIVE", 31 | "SETUP_START_PLACEHOLDER": "Le live commence @everyone !", 32 | "SETUP_END": "Message de fin de LIVE", 33 | "SETUP_END_PLACEHOLDER": "Le live est termin\u00e9. Voici la rediffusion :", 34 | "SETUP_NO_RESULT": "Aucun r\u00e9sultat, \u00eates vous s\u00fbr d'avoir bien saisi le pseudo ?", 35 | "SETUP_ALREADY": "Il existe d\u00e9j\u00e0 une alerte avec cet utilisateur. Vous pouvez le supprimer avec {command}", 36 | "SETUP_NO_PERMISSIONS": "Je n'ai pas la permission d'envoyer un message dans ce canal... Merci de v\u00e9rifier les permissions et de refaire la commande une fois termin\u00e9.", 37 | "SETUP_SUCCESS": "C'est fini ! Les notifications apparaitront lors du prochain LIVE, ou bientot si un LIVE est d\u00e9j\u00e0 en cours.", 38 | "DELETE_EMPTY": "Aucune alerte n'a \u00e9t\u00e9 configur\u00e9e sur serveur. Commencez avec {command}", 39 | "DELETE_CHOOSE_TITLE": "Merci de choisir une alerte \u00e0 supprimer", 40 | "DELETE_CHOOSE_PLACEHOLDER": "Choisissez une alerte \u00e0 supprimer", 41 | "DELETE_SUCCESS": "Suppression effectu\u00e9e avec succ\u00e8s", 42 | "DELETE_CHANNEL_NOT_FOUND": "Canal n\u00b0{canalid} introuvable", 43 | "LANGUAGE_UPDATE": "La langue des alertes a bien \u00e9t\u00e9 d\u00e9finie sur {language}.", 44 | "DATABASE_ERROR": "Erreur avec la base de donn\u00e9es !", 45 | "SETUP_CMD_NAME": "ajout", 46 | "SETUP_CMD_DESC": "Ajouter une nouvelle alerte dans le canal courant", 47 | "DELETE_CMD_NAME": "supprimer", 48 | "DELETE_CMD_DESC": "Supprimer une alerte sur ce serveur", 49 | "LANGUAGE_CMD_NAME": "langue", 50 | "LANGUAGE_CMD_DESC": "Changer la langue des alertes", 51 | "LANGUAGE_OPTION_NAME": "valeur", 52 | "LANGUAGE_OPTION_DESC": "La langue \u00e0 s\u00e9lectionner pour les alertes", 53 | "LANGUAGE_DEFAULT": "Langue du serveur", 54 | "INFO_CMD_NAME": "info", 55 | "INFO_CMD_DESC": "Afficher des informations et de l'aide sur Twitch Alerts", 56 | "LANGUAGE": "Fran\u00e7ais" 57 | } -------------------------------------------------------------------------------- /languages/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name} pr\u00e1v\u011b streamuje!", 3 | "START": "Za\u010dal\/a", 4 | "STATUS": "Stav", 5 | "GAME": "Hra", 6 | "LENGTH": "Trv\u00e1n\u00ed", 7 | "LENGTH_TIME": "{hours} h {minutes} min", 8 | "VIEWERS": "Div\u00e1ci", 9 | "LIVE_END": "Vys\u00edl\u00e1n\u00ed skon\u010dilo.", 10 | "STATS_TITLE": "Statistika", 11 | "STATS_MEMORY": "Vyu\u017eit\u00ed pam\u011bti", 12 | "STATS_UPTIME": "Doba vys\u00edl\u00e1n\u00ed", 13 | "STATS_USERS": "U\u017eivatel\u00e9", 14 | "STATS_SERVERS": "Servery", 15 | "STATS_CHANNELS": "Kan\u00e1ly", 16 | "INFO_TITLE": "Informace a podpora", 17 | "INFO_DESCRIPTION": "Tento bot V\u00e1m umo\u017e\u0148uje zobrazit upozorn\u011bn\u00ed b\u011bhem \u017eiv\u00e9ho vys\u00edl\u00e1n\u00ed na Twitchi a pr\u016fb\u011b\u017en\u011b tato upozorn\u011bn\u00ed aktualizuje. Zpr\u00e1vy a jazyk jsou p\u0159izp\u016fsobiteln\u00e9. Za\u010dn\u011bte p\u0159\u00edkazem {setup}, abyste vytvo\u0159ili nov\u00e9 upozorn\u011bn\u00ed.", 18 | "INFO_PERMISSIONS": "Zm\u011bnit pr\u00e1va", 19 | "INFO_PERMISSIONS_DESC": "Ve v\u00fdchoz\u00edm nastaven\u00ed mohou bota nastavovat pouze lid\u00e9, kte\u0159\u00ed maj\u00ed pr\u00e1va pro spr\u00e1vu serveru. Abyste povolili ostatn\u00edm u\u017eivatel\u016fm pou\u017e\u00edvat p\u0159\u00edkazy bota, jd\u011bte do nastaven\u00ed serveru, pot\u00e9 do z\u00e1lo\u017eky 'Propojen\u00ed' a zvolte 'Spravovat' u Twitch Alerts bota. Zde m\u016f\u017eete nastavit pr\u00e1va t\u00fdkaj\u00edc\u00ed se p\u0159\u00edkaz\u016f pro bota.", 20 | "INFO_ERROR": "Upozorn\u011bn\u00ed nejsou zobrazena", 21 | "INFO_ERROR_DESC": "Pokud se nezobraz\u00ed \u017e\u00e1dn\u00e1 upozorn\u011bn\u00ed do minuty, kdy streamer za\u010dal vys\u00edlat, zkontrolujte zda m\u00e1 bot v dan\u00e9m kan\u00e1lu nastavena pr\u00e1va pro zobrazen\u00ed kan\u00e1l\u016f, odes\u00edl\u00e1n\u00ed zpr\u00e1v a sd\u00edlen\u00ed odkaz\u016f.", 22 | "INFO_LANGUAGE": "Zm\u011bnit jazyk", 23 | "INFO_LANGUAGE_DESC": "Ve v\u00fdchoz\u00edm nastaven\u00ed se upozorn\u011bn\u00ed nastav\u00ed do jazyka serveru. Pokud chcete zm\u011bnit jazyk serveru, jd\u011bte do nastaven\u00ed serveru a z\u00e1lo\u017eky 'Komunita'. Pokud chcete zm\u011bnit jazyk upozorn\u011bn\u00ed, ani\u017e byste zm\u011bnili jazyk serveru, pou\u017eijte p\u0159\u00edkaz {language}. P\u0159\u00edkazy budou v\u017edy zobrazeny v jazyce Va\u0161eho Discordu.", 24 | "INFO_TRANSLATE": "M\u016fj jazyk se nezobrazuje", 25 | "INFO_TRANSLATE_DESC": "Pokud se V\u00e1\u0161 jazyk nezobrazuje, m\u016f\u017eete se pod\u00edlet na p\u0159ekladu bota na [OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)", 26 | "INFO_INVITE": "Pozvat bota", 27 | "INFO_SUPPORT": "Server podpory", 28 | "SETUP_TITLE": "Nastavit nov\u00e9 upozorn\u011bn\u00ed", 29 | "SETUP_STREAMER": "Twitch kan\u00e1l", 30 | "SETUP_START": "Zpr\u00e1va pro upozorn\u011bn\u00ed, kdy\u017e za\u010dne vys\u00edl\u00e1n\u00ed", 31 | "SETUP_START_PLACEHOLDER": "Vys\u00edl\u00e1n\u00ed za\u010dalo @everyone !", 32 | "SETUP_END": "Zpr\u00e1va po ukon\u010den\u00ed vys\u00edl\u00e1n\u00ed", 33 | "SETUP_END_PLACEHOLDER": "Vys\u00edl\u00e1n\u00ed je u konce. Zhl\u00e9dn\u011bte z\u00e1znam zde:", 34 | "SETUP_NO_RESULT": "\u017d\u00e1dn\u00fd v\u00fdsledek. Jste si jisti, \u017ee jste zadali spr\u00e1vn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no?", 35 | "SETUP_ALREADY": "Upozorn\u011bn\u00ed pro tohoto u\u017eivatele ji\u017e existuje. M\u016f\u017eete jej smazat pomoc\u00ed {command}", 36 | "SETUP_NO_PERMISSIONS": "Nem\u00e1m pr\u00e1va pro odesl\u00e1n\u00ed zpr\u00e1vy v tomto kan\u00e1lu. Pros\u00edm, zkontrolujte pr\u00e1va a zkuste to znovu.", 37 | "SETUP_SUCCESS": "Hotovo! Upozorn\u011bn\u00ed vysko\u010d\u00ed jakmile za\u010dne nov\u00e9 vys\u00edl\u00e1n\u00ed, nebo za chv\u00edli, pokud ji\u017e vys\u00edl\u00e1n\u00ed b\u011b\u017e\u00ed.", 38 | "DELETE_EMPTY": "\u017d\u00e1dn\u00e9 upozorn\u011bn\u00ed nebylo nastaveno. Za\u010dn\u011bte pou\u017eit\u00edm {command}", 39 | "DELETE_CHOOSE_TITLE": "Pros\u00edm, vyberte upozorn\u011bn\u00ed, kter\u00e9 chcete vymazat.", 40 | "DELETE_CHOOSE_PLACEHOLDER": "Vyberte upozorn\u011bn\u00ed, kter\u00e9 chcete vymazat.", 41 | "DELETE_SUCCESS": "Vymaz\u00e1n\u00ed bylo \u00fasp\u011b\u0161n\u011b dokon\u010deno.", 42 | "DELETE_CHANNEL_NOT_FOUND": "Kan\u00e1l \u010d\u00edslo {canalid} nebyl nalezen.", 43 | "LANGUAGE_UPDATE": "Jazyk upozorn\u011bn\u00ed byl nastaven na {language}.", 44 | "DATABASE_ERROR": "Chyba v datab\u00e1zi!", 45 | "SETUP_CMD_NAME": "nastaven\u00ed", 46 | "SETUP_CMD_DESC": "P\u0159idat nov\u00e9 upozorn\u011bn\u00ed v tomto kan\u00e1lu", 47 | "DELETE_CMD_NAME": "vymazat", 48 | "DELETE_CMD_DESC": "Smazat upozorn\u011bn\u00ed ze serveru.", 49 | "LANGUAGE_CMD_NAME": "jazyk", 50 | "LANGUAGE_CMD_DESC": "Zm\u011bnit jazyk upozorn\u011bn\u00ed", 51 | "LANGUAGE_OPTION_NAME": "hodnota", 52 | "LANGUAGE_OPTION_DESC": "V\u00fdb\u011br jazyka pro upozorn\u011bn\u00ed", 53 | "LANGUAGE_DEFAULT": "Jazyk serveru", 54 | "INFO_CMD_NAME": "info", 55 | "INFO_CMD_DESC": "Zobrazit Twitch Alerts bot informace a pomoc", 56 | "LANGUAGE": "\u010ce\u0161tina" 57 | } -------------------------------------------------------------------------------- /models/database.js: -------------------------------------------------------------------------------- 1 | const {Client} = require("pg"); 2 | 3 | class Database extends Client { 4 | 5 | constructor(url) { 6 | super({ 7 | connectionString: url, 8 | }); 9 | this.connect(); 10 | 11 | this.debug = process.env.NODE_ENV !== "production"; 12 | } 13 | 14 | async addNewGuild(guild_id) { 15 | if (this.debug) return; 16 | return await this.query("INSERT INTO guilds(guild_id) VALUES ($1) ON CONFLICT DO NOTHING", [guild_id]); 17 | } 18 | 19 | async setGuildLanguage(guild_id, language) { 20 | if (this.debug) return; 21 | await this.addNewGuild(guild_id); 22 | return await this.query("UPDATE guilds SET guild_language=$1 WHERE guild_id=$2", [language, guild_id]); 23 | } 24 | 25 | async deleteGuild(guild_id) { 26 | if (this.debug) return; 27 | const alerts = await this.listAlertsByGuild(guild_id); 28 | for (const alert of alerts) { 29 | await this.deleteAlert(guild_id, alert.streamer_id); 30 | } 31 | return await this.query("DELETE FROM guilds WHERE guild_id=$1", [guild_id]); 32 | } 33 | 34 | async listAllAlerts() { 35 | return (await this.query("SELECT * FROM streamers JOIN alerts USING(streamer_id) JOIN guilds USING(guild_id)")).rows; 36 | } 37 | 38 | async listAllStreamers() { 39 | return (await this.query("SELECT * FROM streamers")).rows; 40 | } 41 | 42 | async listAlertsByGuild(guild_id) { 43 | return (await this.query("SELECT * FROM streamers JOIN alerts USING(streamer_id) JOIN guilds USING(guild_id) WHERE guild_id=$1", [guild_id])).rows; 44 | } 45 | async countAlertsByGuild(guild_id) { 46 | return (await this.query("SELECT COUNT(streamer_id) AS count FROM alerts WHERE guild_id=$1", [guild_id])).rows[0].count; 47 | } 48 | 49 | async listAlertsByStreamer(streamer) { 50 | return (await this.query("SELECT * FROM streamers JOIN alerts USING(streamer_id) JOIN guilds USING(guild_id) WHERE streamer_id=$1", [streamer])).rows; 51 | } 52 | 53 | async addStreamer(streamer) { 54 | return await this.query("INSERT INTO streamers(streamer_id) VALUES ($1) ON CONFLICT DO NOTHING", [streamer]); 55 | } 56 | 57 | async removeStreamerIfEmpty(streamer) { 58 | const q = await this.listAlertsByStreamer(streamer); 59 | if (q.length === 0) await this.query("DELETE FROM streamers WHERE streamer_id=$1", [streamer]); 60 | } 61 | 62 | async addAlert(guild_id, streamer, channel, start, end, display_game, display_viewers) { 63 | if (this.debug) return; 64 | await this.addNewGuild(guild_id); 65 | await this.addStreamer(streamer); 66 | await this.query("INSERT INTO alerts(guild_id, streamer_id, alert_channel, alert_start, alert_end, alert_pref_display_game, alert_pref_display_viewers) VALUES ($1, $2, $3, $4, $5, $6, $7)", 67 | [guild_id, streamer, channel, start, end, display_game, display_viewers]); 68 | } 69 | async editAlert(guild_id, oldStreamer, newStreamer, start, end, display_game, display_viewers) { 70 | if (this.debug) return; 71 | if (oldStreamer !== newStreamer) await this.addStreamer(newStreamer); 72 | await this.query("UPDATE alerts SET alert_start=$1, alert_end=$2, streamer_id=$3, alert_pref_display_game=$4, alert_pref_display_viewers=$5 WHERE streamer_id=$6 AND guild_id=$7", 73 | [start, end, newStreamer, display_game, display_viewers, oldStreamer, guild_id]); 74 | if (oldStreamer !== newStreamer) { 75 | await this.removeStreamerIfEmpty(oldStreamer); 76 | } 77 | } 78 | async moveAlert(guild_id, streamer, channel) { 79 | if (this.debug) return; 80 | return await this.query("UPDATE alerts SET alert_channel=$1 WHERE streamer_id=$2 AND guild_id=$3", 81 | [channel, streamer, guild_id]); 82 | } 83 | async deleteAlert(guild_id, streamer) { 84 | if (this.debug) return; 85 | await this.query("DELETE FROM alerts WHERE guild_id=$1 AND streamer_id=$2", [guild_id, streamer]); 86 | await this.removeStreamerIfEmpty(streamer); 87 | } 88 | async setAlertMessage(guild_id, streamer, message) { 89 | if (this.debug) return; 90 | return await this.query("UPDATE alerts SET alert_message=$1 WHERE streamer_id=$2 AND guild_id=$3", 91 | [message, streamer, guild_id]); 92 | } 93 | async removeAlertMessage(guild_id, streamer) { 94 | if (this.debug) return; 95 | return await this.query("UPDATE alerts SET alert_message=NULL WHERE streamer_id=$1 AND guild_id=$2", 96 | [streamer, guild_id]); 97 | } 98 | 99 | async getAlert(guild_id, streamer) { 100 | return (await this.query("SELECT * FROM streamers JOIN alerts USING(streamer_id) JOIN guilds USING(guild_id) WHERE guild_id=$1 AND streamer_id=$2", [guild_id, streamer])).rows; 101 | } 102 | 103 | async deleteFromID(id) { 104 | if (this.debug) return; 105 | return await this.query("DELETE FROM alerts WHERE guild_id=$1 OR alert_channel=$1", [id]); 106 | } 107 | } 108 | 109 | module.exports = Database; -------------------------------------------------------------------------------- /models/embedService.js: -------------------------------------------------------------------------------- 1 | const {version, EmbedBuilder} = require("discord.js"); 2 | const {DurationFormatter} = require("@sapphire/time-utilities"); 3 | const {getString} = require("../modules/language"); 4 | const durationFormatter = new DurationFormatter(); 5 | 6 | module.exports = { 7 | generateStatEmbed: (client, interaction) => { 8 | const duration = durationFormatter.format(client.uptime); 9 | return new EmbedBuilder() 10 | .setColor('#0099ff') 11 | .setAuthor({ 12 | name: interaction.getLocalizedString("STATS_TITLE"), 13 | iconURL: client.user.avatarURL(), 14 | url: 'https://truckyapp.com' 15 | }) 16 | .setThumbnail(client.user.avatarURL()) 17 | .addFields([ 18 | { 19 | name: interaction.getLocalizedString("STATS_MEMORY"), 20 | value: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2) + "MB", 21 | inline: true 22 | }, 23 | {name: interaction.getLocalizedString("STATS_UPTIME"), value: duration, inline: true}, 24 | { 25 | name: interaction.getLocalizedString("STATS_USERS"), 26 | value: client.guilds.cache.map(g => g.memberCount).reduce((a, b) => a + b).toLocaleString(), 27 | inline: true 28 | }, 29 | { 30 | name: interaction.getLocalizedString("STATS_SERVERS"), 31 | value: client.guilds.cache.size.toLocaleString(), 32 | inline: true 33 | }, 34 | { 35 | name: interaction.getLocalizedString("STATS_CHANNELS"), 36 | value: client.channels.cache.size.toLocaleString(), 37 | inline: true 38 | }, 39 | {name: 'Discord.js', value: "v" + version, inline: true}, 40 | {name: 'NodeJS', value: process.version, inline: true}, 41 | ]) 42 | .setTimestamp(); 43 | }, 44 | 45 | generateInfoEmbed: (client, interaction) => { 46 | return new EmbedBuilder() 47 | .setTitle(interaction.getLocalizedString("INFO_TITLE")) 48 | .setURL("https://twitchbot.harfeur.fr") 49 | .setDescription(interaction.getLocalizedString("INFO_DESCRIPTION", {setup: ` c.name==="info").id}>`})) 50 | .setColor('#e603f8') 51 | .setThumbnail(client.user.avatarURL()) 52 | .addFields([ 53 | { 54 | name: interaction.getLocalizedString("INFO_PERMISSIONS"), 55 | value: interaction.getLocalizedString("INFO_PERMISSIONS_DESC") 56 | }, 57 | { 58 | name: interaction.getLocalizedString("INFO_ERROR"), 59 | value: interaction.getLocalizedString("INFO_ERROR_DESC") 60 | }, 61 | { 62 | name: interaction.getLocalizedString("INFO_LANGUAGE"), 63 | value: interaction.getLocalizedString("INFO_LANGUAGE_DESC", {language: ` c.name==="language").id}>`}) 64 | }, 65 | { 66 | name: interaction.getLocalizedString("INFO_TRANSLATE"), 67 | value: interaction.getLocalizedString("INFO_TRANSLATE_DESC") 68 | }, 69 | ]) 70 | .setTimestamp(); 71 | }, 72 | 73 | generateLiveEmbed: (user, stream, game, alert, lang) => { 74 | const now = Date.now(); 75 | const debut = stream.startDate; 76 | 77 | const heures = Math.trunc(((now - debut) / 60000) / 60); 78 | const minutes = Math.trunc((now - debut) / 60000 - heures * 60); 79 | 80 | const embed = new EmbedBuilder() 81 | .setColor(9442302) 82 | .setTimestamp(debut) 83 | .setTitle("🔴 " + getString(lang, "TITLE", {name: user.displayName})) 84 | .setURL(`https://www.twitch.tv/${user.name}`) 85 | .setThumbnail(user.profilePictureUrl) 86 | .setFooter({ 87 | text: getString(lang, "START") 88 | }) 89 | .setAuthor({ 90 | name: "Twitch", 91 | url: `https://www.twitch.tv/${user.name}`, 92 | icon_url: "https://cdn3.iconfinder.com/data/icons/social-messaging-ui-color-shapes-2-free/128/social-twitch-circle-512.png" 93 | }) 94 | .addFields({ 95 | name: getString(lang, "STATUS"), 96 | value: `❯ ${stream.title}` 97 | }); 98 | if (game && alert.alert_pref_display_game) 99 | embed.setImage(game.getBoxArtUrl(272, 380)) 100 | .addFields({ 101 | name: getString(lang, "GAME"), 102 | value: `❯ ${game.name}`, 103 | inline: true 104 | }); 105 | embed.addFields({ 106 | name: getString(lang, "LENGTH"), 107 | value: "❯ " + getString(lang, "LENGTH_TIME", {hours: heures, minutes: minutes}), 108 | inline: true 109 | }); 110 | if (alert.alert_pref_display_viewers) 111 | embed.addFields({ 112 | name: getString(lang, "VIEWERS"), 113 | value: `❯ ${stream.viewers}`, 114 | inline: true 115 | }); 116 | 117 | return embed; 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /public/assets/js/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "navHome": "Home", 3 | "navHelp": "Support", 4 | "navDarkMode": "Dark mode", 5 | "navDashboard": "Dashboard", 6 | "footSources": "Sources", 7 | "footDiscord": "Discord", 8 | "footAbout": "About", 9 | "short": "Twitch real-time alerts", 10 | "slogan": "The best choice for Twitch alerts on Discord", 11 | "usedBy1": "Used by", 12 | "usedBy2": "Discord servers", 13 | "services": "Our services", 14 | "servicesTitle": "What we offer", 15 | "service1H": "Custom notifications", 16 | "service1T": "Choose the message to be displayed at the beginning and at the end of the LIVE. You can mention @everyone if you wish", 17 | "service2H": "Real time update", 18 | "service2T": "You change game during LIVE? Don't worry! Our bot will modify the alert, to stay always up to date!", 19 | "service3H": "Simple configuration", 20 | "service3T": "Thanks to Discord's new features, you can easily set up your alerts and see at a glance all the alerts you've set up", 21 | "service4H": "Encourage replay", 22 | "service4T": "When your LIVE is over, a summary and link to the replay will be available directly in place of your alert.", 23 | "info1H": "Help with the translation", 24 | "info1T": "Translations are voluntary, you can help", 25 | "info1B": "Help", 26 | "info2H": "Need help", 27 | "info2T": "Join the Discord server if you have a problem", 28 | "info2B": "Join", 29 | "info3H": "Operation", 30 | "info3T": "The bot being open-source, you can consult and help the development", 31 | "github": "GitHub", 32 | "terms": "Terms of use", 33 | "terms1": "1. Use of our services", 34 | "terms11": "You may use our Bot only if you comply with these terms and all applicable laws. If you do not agree with any of these terms, you may not use or access this site. The materials contained in this site are protected by applicable copyright and trademark laws.", 35 | "terms12": "Any use or access by persons under the age of 13 is prohibited.", 36 | "terms2": "2. Use", 37 | "terms21": "If we become aware of abuse related to a bug in the bot or otherwise, by the majority of server members or the server owner, we have the right to make the bot leave your server and never join it again.", 38 | "terms22": "If we find that a user is abusing the bot or using its function to intimidate or cause problems for others, we reserve the right to ban that user from using our bot.", 39 | "terms3": "3. Copyright", 40 | "terms31": "You are not allowed to host this bot and use it for production purposes. You may download, modify and duplicate it only for development purposes, and you may submit a modification request.", 41 | "terms4": "4. Privacy and contact", 42 | "terms41": "If you have any problems, you can contact us at contact@harfeur.fr. Please also see our", 43 | "privacy": "Privacy policy", 44 | "privacy1": "What information is stored?", 45 | "privacy11": "When you create a new alert, your server ID, channel ID and Twitch ID are recorded. When the Twitch user is live, we temporarily store the alert's message ID.", 46 | "privacy2": "How and where is the data stored?", 47 | "privacy21": "The data is stored in a PostgreSQL database.", 48 | "privacy22": "The bot, the database and the website are located on a single self-hosted server in France.", 49 | "privacy3": "How can I delete my data?", 50 | "privacy31": "You can delete the alert, which will delete the data stored. You can also contact us at contact@harfeur.fr for more information, and to access all stored data in accordance with the GDPR.", 51 | "privacy4": "Is the website tracking me?", 52 | "privacy41": "When accessing this website, we store a cookie named user which allows you to stay logged in and view the dashboard. This cookie is not used to track your actions.", 53 | "privacy42": "We use a self-hosted Matomo service that collects statistics about who visits the site. The data stored is your browser, the pages visited, and the first two bytes of your IP address. If you do not want this data to be saved, you can activate the Do Not Track setting in your browser.", 54 | "serversH": "My servers", 55 | "serversT": "List of servers to which you have permissions to manage the bot (Manage server permission)", 56 | "loading": "Loading in progress...", 57 | "othersH": "Other servers", 58 | "othersT": "List of servers to which you can add the bot (Manage server permission)", 59 | "alert1": "alert", 60 | "alert2": "alerts", 61 | "createButton": "Create an alert", 62 | "editButton": "Modify", 63 | "moveButton": "Move", 64 | "duplicateButton": "Duplicate", 65 | "deleteButton": "Delete", 66 | "colStreamer": "Streamer", 67 | "colChannel": "Alert Channel", 68 | "colStart": "Alert message", 69 | "colEnd": "End message", 70 | "colActions": "Actions", 71 | "modalCreate": "Create an alert", 72 | "modalEdit": "Modify an alert", 73 | "modalMove": "Move an alert", 74 | "modalDuplicate": "Duplicate an alert", 75 | "modalDelete": "Delete an alert", 76 | "modalDeleteText": "Are you sure you want to delete this alert?", 77 | "inputStreamer": "Streamer", 78 | "inputStart": "Start stream message", 79 | "inputStartPlaceholder": "Stream starts @everyone", 80 | "inputEnd": "Stream end message", 81 | "inputEndPlaceholder": "The stream is finished. Watch the replay:", 82 | "inputChannel": "Text channel", 83 | "inputChannelPlaceholder": "Name or ID", 84 | "inputGame": "Display game", 85 | "inputViewers": "Display viewers count", 86 | "create": "Create", 87 | "edit": "Modify", 88 | "move": "Move", 89 | "duplicate": "Duplicate", 90 | "delete": "Delete", 91 | "cancel": "Cancel" 92 | } -------------------------------------------------------------------------------- /public/assets/js/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "navHome": "Thuis", 3 | "navHelp": "Ondersteuning", 4 | "navDarkMode": "Donkere modes", 5 | "navDashboard": "Dashboard", 6 | "footSources": "Bronnen", 7 | "footDiscord": "Discord", 8 | "footAbout": "Over", 9 | "short": "Twitch live notificaties", 10 | "slogan": "De beste keuze voor Twitch notificaties in Discord", 11 | "usedBy1": "Gebruikt door", 12 | "usedBy2": "Discord servers", 13 | "services": "Onze diensten", 14 | "servicesTitle": "Wat wij bieden", 15 | "service1H": "Aangepast notificaties", 16 | "service1T": "Maak zelf een bericht bij het begin en aan het einde van de LIVE stream. U kunt ook @everyone vermelden.", 17 | "service2H": "word LIVE bijgewerkt", 18 | "service2T": "Ben je een ander spel gaan spelen tijdens de LIVE? Geen zorgen! Deze BOT past automatisch de alert aan, Zodat die altijd is bijgewerkt!", 19 | "service3H": "Eenvoudige configuratie", 20 | "service3T": "Dankzij de nieuwe functie van Discord kun je eenvoudig je notificatie instellen en in \u00e9\u00e9n oogopslag alle notificaties zien die je hebt ingesteld.", 21 | "service4H": "Moedig terug kijken aan", 22 | "service4T": "Wanneer je klaar bent met LIVE, word de LIVE direct vervangen voor de samenvatting met de juiste snelkoppeling.", 23 | "info1H": "Help ons met de vertaling", 24 | "info1T": "Vertalingen zijn vrijwilligers, Wil jij helpen?", 25 | "info1B": "Help", 26 | "info2H": "Hulp nodig", 27 | "info2T": "Kom in de Discord server als er problemen zijn.", 28 | "info2B": "Aansluiten", 29 | "info3H": "Bewerken", 30 | "info3T": "De BOT is een open-bron. Daardoor kunt u de ontwikkeling raadplegen en helpen.", 31 | "github": "Github", 32 | "terms": "Gebruiksvoorwaarden", 33 | "terms1": "1. gebruik maken van onze Diensten.", 34 | "terms11": "U mag onze Twitch Alerts *BOT* alleen gebruiken als u voldoet aan deze voorwaarden en alle toepasselijke wetten. \nAls u niet akkoord gaat met een van deze voorwaarden,\nMag u deze site niet gebruiken of openen.\nHet materiaal op deze site wordt beschermd door toepasselijke auteursrechten en handelsmerkwetten.", 35 | "terms12": "Elk gebruik of toegang door personen onder de 13 jaar is verboden.", 36 | "terms2": "2. Gebruik", 37 | "terms21": "Als we ons bewust worden van misbruik gerelateerd aan een bug in de BOT of anderszins, door de meerderheid van de serverleden of de servereigenaar, behouden we het recht om de BOT van uw server te verwijderen en komt de BOT nooit meer bij in de Discord Server.", 38 | "terms22": "Als we ontdekken dat een gebruiker de bot misbruikt of de functies gebruikt om anderen te intimideren of problemen te veroorzaken, behouden we ons het recht voor om die gebruiker te verbieden onze BOT te gebruiken.", 39 | "terms3": "3. Auteursrechten", 40 | "terms31": "U mag deze bot niet hosten en gebruiken voor productiedoeleinden. U mag deze downloaden, wijzigen en dupliceren voor ontwikkelingsdoeleinden en u kunt een wijzigingsverzoek indienen.", 41 | "terms4": "4. Privacy en contact", 42 | "terms41": "Heb je problemen van welke aard ook, kan je ons bereiken op contact@harfeur.fr. Zie ook onze", 43 | "privacy": "Privacy wetgeving", 44 | "privacy1": "Welke informatie word opgeslagen?", 45 | "privacy11": "Wanneer er een nieuwe melding is aangemaakt, worden je server-ID, kanaal-ID en Twitch-ID geregistreerd. Wanneer de Twitch-gebruiker live is, slaan we tijdelijk de bericht-ID van de melding op.", 46 | "privacy2": "Hoe en waar word de data opgeslagen?", 47 | "privacy21": "De data word opgeslagen op een PostgreSQL databank.", 48 | "privacy22": "De BOT, De databank en de website worden gehost vanaf een enkel zelf-hosted server in Frankrijk.", 49 | "privacy3": "Hoe kan ik mijn data verwijderen?", 50 | "privacy31": "U kunt de waarschuwing verwijderen, waardoor de opgeslagen gegevens worden verwijderd. U kunt ook contact met ons opnemen via contact@harfeur.fr voor meer informatie en om toegang te krijgen tot alle opgeslagen gegevens in overeenstemming met de AVG.", 51 | "privacy4": "Volgt de website mij?", 52 | "privacy41": "Wanneer u deze website bezoekt, slaan we een cookie met de naam gebruiker op, waarmee u ingelogd kunt blijven en het dashboard kunt bekijken. Deze cookie wordt niet gebruikt om uw acties bij te houden.", 53 | "privacy42": "We gebruiken een door onszelf gehoste Matomo-service die statistieken verzamelt over wie de site bezoekt. De opgeslagen gegevens zijn uw browser, de bezochte pagina's en de eerste twee bytes van uw IP-adres. Als u niet wilt dat deze gegevens worden opgeslagen, kunt u de Do Not Track-instellingen in uw browser activeren.", 54 | "serversH": "Mijn Servers", 55 | "serversT": "Lijst van servers waar U toegang tot heeft om de BOT te mogen beheren (Beheer server instellingen)", 56 | "loading": "Bezig met laden...", 57 | "othersH": "Andere severs", 58 | "othersT": "Lijst van server waar U de BOT kan toevoegen (Beheer server instellingen)", 59 | "alert1": "Notificatie", 60 | "alert2": "Notificaties", 61 | "createButton": "Maak een Alarm aan", 62 | "editButton": "Aanpassen", 63 | "moveButton": "Verplaatsen", 64 | "duplicateButton": "Dupliceren", 65 | "deleteButton": "Verwijderen", 66 | "colStreamer": "Streamer", 67 | "colChannel": "Notificatie kanaal", 68 | "colStart": "Notificatie bericht", 69 | "colEnd": "Einde streamer notificatie", 70 | "colActions": "Gebeurtenis", 71 | "modalCreate": "Maak een notificatie", 72 | "modalEdit": "Pas een notificatie aan", 73 | "modalMove": "Verplaats een notificatie", 74 | "modalDuplicate": "Dupliceer een notificatie", 75 | "modalDelete": "Verwijder een notificatie", 76 | "modalDeleteText": "Weet U zeker dat deze notificatie moet worden verwijderd?", 77 | "inputStreamer": "Streamer", 78 | "inputStart": "Start stream bericht", 79 | "inputStartPlaceholder": "Stream is begonnen @everyone", 80 | "inputEnd": "Bericht stream is afgelopen", 81 | "inputEndPlaceholder": "De stream is voorbij, kijk hier naar de herhaling:", 82 | "inputChannel": "Tekst kanaal", 83 | "inputChannelPlaceholder": "Naam of ID", 84 | "inputGame": "Display game", 85 | "inputViewers": "Display viewers count", 86 | "create": "Cre\u00eber", 87 | "edit": "Aanpassen", 88 | "move": "Verplaats", 89 | "duplicate": "Dupliceer", 90 | "delete": "Verwijder", 91 | "cancel": "Annuleer" 92 | } -------------------------------------------------------------------------------- /public/assets/js/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "navHome": "Startseite", 3 | "navHelp": "Support", 4 | "navDarkMode": "Dunkelmodus", 5 | "navDashboard": "Dashboard", 6 | "footSources": "Quellen", 7 | "footDiscord": "Discord", 8 | "footAbout": "\u00dcber", 9 | "short": "Twitch Alerts in Echtzeit", 10 | "slogan": "Die beste Wahl f\u00fcr Twitch Alerts auf Discord", 11 | "usedBy1": "Verwendet auf", 12 | "usedBy2": "Discord-Servern", 13 | "services": "Unsere Dienste", 14 | "servicesTitle": "Was wir bieten", 15 | "service1H": "Benutzerdefinierte Alerts", 16 | "service1T": "Erstelle eine Nachricht, die am Anfang und am Ende des Streams angezeigt wird - ganz nach deiner Wahl. Du kannst @everyone erw\u00e4hnen, wenn du m\u00f6chtest", 17 | "service2H": "Echtzeit-Aktualisierungen", 18 | "service2T": "Du \u00e4nderst das Spiel w\u00e4hrend des Streams? Keine Sorge! Unser Bot wird die Nachricht bearbeiten, um immer auf dem neuesten Stand zu bleiben!", 19 | "service3H": "Einfache Konfiguration ", 20 | "service3T": "Dank Discords neuen Funktionen, kannst du einfach deine Alerts erstellen und auf einem Blick sehen, welche du bereits erstellt hast", 21 | "service4H": "Verlinke Wiederholungen", 22 | "service4T": "Wenn dein Stream zu Ende ist, wird eine Zusammenfassung und ein Link zur Wiederholung anstelle des Alerts verf\u00fcgbar sein.", 23 | "info1H": "Hilf mit \u00dcbersetzungen", 24 | "info1T": "\u00dcbersetzungen sind freiwillig, du kannst helfen", 25 | "info1B": "Helfen", 26 | "info2H": "Hilfe ben\u00f6tigt", 27 | "info2T": "Betrete den Discord-Server, wenn du ein Problem hast", 28 | "info2B": "Beitreten", 29 | "info3H": "Projekt", 30 | "info3T": "Der Bot ist Open Source, du kannst die Entwicklung beobachten und helfen", 31 | "github": "GitHub", 32 | "terms": "Nutzungsbedingungen ", 33 | "terms1": "1. Nutzung unserer Dienste", 34 | "terms11": "Sie d\u00fcrfen unseren Bot nur verwenden, wenn Sie diese Bedingungen und alle anwendbaren Gesetze einhalten. Wenn Sie mit einer dieser Bedingungen nicht einverstanden sind, d\u00fcrfen Sie diese Website nicht nutzen oder darauf zugreifen. Die auf dieser Website enthaltenen Materialien sind durch geltende Urheber- und Markenrechte gesch\u00fctzt.", 35 | "terms12": "Jegliche Nutzung oder der Zugriff durch Personen unter 13 Jahren ist untersagt.", 36 | "terms2": "2. Nutzung", 37 | "terms21": "Wenn uns ein Missbrauch im Zusammenhang mit einem Fehler im Bot oder anderweitig durch die Mehrheit der Servermitglieder oder den Serverbesitzer bekannt wird, haben wir das Recht, den Bot dazu zu bringen, Ihren Server zu verlassen und ihm nie wieder beizutreten.", 38 | "terms22": "Wenn wir feststellen, dass ein Benutzer den Bot missbraucht oder seine Funktion nutzt, um andere einzusch\u00fcchtern oder ihnen Probleme zu bereiten, behalten wir uns das Recht vor, diesem Benutzer die Verwendung unseres Bots zu verbieten.", 39 | "terms3": "3. Urheberrechte", 40 | "terms31": "Sie d\u00fcrfen diesen Bot nicht hosten und f\u00fcr Produktionszwecke verwenden. Sie d\u00fcrfen es nur zu Entwicklungszwecken herunterladen, \u00e4ndern und vervielf\u00e4ltigen, und Sie k\u00f6nnen eine \u00c4nderungsanfrage stellen.", 41 | "terms4": "4. Datenschutz und Kontakt", 42 | "terms41": "Bei Problemen k\u00f6nnen Sie uns unter contact@harfeur.fr kontaktieren. Bitte beachten Sie auch unsere", 43 | "privacy": "Datenschutzbedingungen", 44 | "privacy1": "Welche Informationen werden gespeichert?", 45 | "privacy11": "Wenn Sie ein neuen Alert erstellen, werden Ihre Server-ID, Kanal-ID und Twitch-ID aufgezeichnet. Wenn der Twitch-Benutzer live ist, speichern wir vor\u00fcbergehend die Nachrichten-ID des Alerts.", 46 | "privacy2": "Wie und wo werden die Daten gespeichert?", 47 | "privacy21": "Die Daten werden in einer PostgreSQL-Datenbank gespeichert.", 48 | "privacy22": "Der Bot, die Datenbank und die Website befinden sich auf einem einzigen selbst gehosteten Server in Frankreich.", 49 | "privacy3": "Wie kann ich meine Daten l\u00f6schen?", 50 | "privacy31": "Sie k\u00f6nnen den Alert l\u00f6schen, wodurch die gespeicherten Daten gel\u00f6scht werden. Sie k\u00f6nnen uns auch unter contact@harfeur.fr kontaktieren, um weitere Informationen zu erhalten und auf alle gespeicherten Daten gem\u00e4\u00df der DSGVO zuzugreifen.", 51 | "privacy4": "Verfolgt mich die Website?", 52 | "privacy41": "Beim Zugriff auf diese Website speichern wir ein Cookie namens Benutzer, mit dem Sie angemeldet bleiben und das Dashboard anzeigen k\u00f6nnen. Dieses Cookie wird nicht verwendet, um Ihre Aktionen zu verfolgen.", 53 | "privacy42": "Wir verwenden einen selbst gehosteten Matomo-Dienst, der Statistiken dar\u00fcber sammelt, wer die Website besucht. Die gespeicherten Daten sind Ihr Browser, die besuchten Seiten und die ersten zwei Bytes Ihrer IP-Adresse. Wenn Sie eine Speicherung dieser Daten nicht w\u00fcnschen, k\u00f6nnen Sie in Ihrem Browser die Do-Not-Track-Einstellung aktivieren.", 54 | "serversH": "Meine Server", 55 | "serversT": "Liste der Server, auf denen du berechtigst bist, den Bot zu verwalten (Server verwalten Berechtigung)", 56 | "loading": "L\u00e4dt...", 57 | "othersH": "Weitere Server", 58 | "othersT": "Liste der Server, auf die du den Bot hinzuf\u00fcgen kannst (Server verwalten Berechtigung)", 59 | "alert1": "Alert", 60 | "alert2": "Alerts", 61 | "createButton": "Alert erstellen", 62 | "editButton": "Bearbeiten", 63 | "moveButton": "Verschieben", 64 | "duplicateButton": "Duplizieren", 65 | "deleteButton": "L\u00f6schen", 66 | "colStreamer": "Streamer", 67 | "colChannel": "Alert-Kanal", 68 | "colStart": "Alert-Nachricht", 69 | "colEnd": "End-Nachricht", 70 | "colActions": "Aktionen", 71 | "modalCreate": "Alert erstellen", 72 | "modalEdit": "Alert bearbeiten", 73 | "modalMove": "Alert verschieben", 74 | "modalDuplicate": "Alert duplizieren", 75 | "modalDelete": "Alert l\u00f6schen", 76 | "modalDeleteText": "Bist du sicher, dass du diesen Alert l\u00f6schen m\u00f6chtest?", 77 | "inputStreamer": "Streamer", 78 | "inputStart": "Nachricht bei Streambeginn", 79 | "inputStartPlaceholder": "Stream beginnt @everyone", 80 | "inputEnd": "Nachricht bei Streamende", 81 | "inputEndPlaceholder": "Der Stream ist zu Ende. Gucke die Wiederholung:", 82 | "inputChannel": "Textkanal", 83 | "inputChannelPlaceholder": "Name oder ID", 84 | "inputGame": "Display game", 85 | "inputViewers": "Display viewers count", 86 | "create": "Erstellen", 87 | "edit": "Bearbeiten", 88 | "move": "Verschieben", 89 | "duplicate": "Duplizieren", 90 | "delete": "L\u00f6schen", 91 | "cancel": "Abbrechen" 92 | } -------------------------------------------------------------------------------- /languages/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "{name}\uc774(\uac00) \ubc29\uc1a1 \uc911 \uc785\ub2c8\ub2e4.", 3 | "START": "\ubc29\uc1a1 \uc2dc\uc791\uc2dc\uac04", 4 | "STATUS": "\ubc29\uc1a1 \uc81c\ubaa9", 5 | "GAME": "\uce74\ud14c\uace0\ub9ac", 6 | "LENGTH": "\uc9c4\ud589\uc2dc\uac04", 7 | "LENGTH_TIME": "{hours}\uc2dc\uac04 {minutes}\ubd84", 8 | "VIEWERS": "\uc2dc\uccad\uc790 \uc218", 9 | "LIVE_END": "\ubc29\uc1a1\uc774 \uc885\ub8cc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", 10 | "STATS_TITLE": "\ud1b5\uacc4", 11 | "STATS_MEMORY": "\uba54\ubaa8\ub9ac \uc0ac\uc6a9\ub7c9", 12 | "STATS_UPTIME": "\uc2dc\uac04", 13 | "STATS_USERS": "\uc0ac\uc6a9\uc790", 14 | "STATS_SERVERS": "\uc11c\ubc84", 15 | "STATS_CHANNELS": "\ucc44\ub110", 16 | "INFO_TITLE": "\uc815\ubcf4 \ubc0f \ub3c4\uc6c0\ub9d0", 17 | "INFO_DESCRIPTION": "\uc774 \ubd07\uc744 \uc0ac\uc6a9\ud558\uba74 \ud2b8\uc704\uce58 \ub77c\uc774\ube0c \uc911\uc5d0 \uc54c\ub78c\uc744 \ud45c\uc2dc\ud558\uace0 \uba54\uc2dc\uc9c0\ub97c \uc790\uc8fc \uc5c5\ub370\uc774\ud2b8\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uba54\uc2dc\uc9c0\uc640 \uc5b8\uc5b4\ub294 \uc0ac\uc6a9\uc790 \uc815\uc758\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc0c8 \uc54c\ub78c\ub97c \uc0dd\uc131\ud558\ub824\uba74 {setup}\ub85c \uc2dc\uc791\ud558\uc2ed\uc2dc\uc624.", 18 | "INFO_PERMISSIONS": "\uad8c\ud55c \ubcc0\uacbd", 19 | "INFO_PERMISSIONS_DESC": "\uae30\ubcf8\uc801\uc73c\ub85c \uc11c\ubc84 \uad00\ub9ac \uad8c\ud55c\uc744 \uac00\uc9c4 \uc0ac\uc6a9\uc790\ub9cc \ubd07\uc744 \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \n\ub2e4\ub978 \uc0ac\uc6a9\uc790\uac00 \uba85\ub839\uc744 \uc2e4\ud589\ud560 \uc218 \uc788\ub3c4\ub85d \ud558\ub824\uba74, \uc11c\ubc84 \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud55c \ub2e4\uc74c \n'\uc5f0\ub3d9' \ud0ed\uc5d0\uc11c Twitch Alerts\uc5d0 \uad00\ub9ac\uc744 \ub20c\ub7ec\uc11c \uba85\ub839\uc5b4 \uad8c\ud55c\uc5d0 \uc5ed\ud560 \ub610\ub294 \uba64\ubc84\ub97c \ucd94\uac00\ud574\uc8fc\uc138\uc694.", 20 | "INFO_ERROR": "\uc54c\ub78c\uc774 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc74c", 21 | "INFO_ERROR_DESC": "\uc2a4\ud2b8\ub9ac\uba38\uac00 \ucd94\uac00\ub41c \uc0c1\ud0dc\uc5d0\uc11c 1\ubd84\uc774 \uc9c0\ub098\ub3c4 \uc54c\ub78c\uc774 \ub098\ud0c0\ub098\uc9c0 \uc54a\uc73c\uba74 \n\ucc44\ub110\ud3b8\uc9d1\uc5d0\uc11c '\ucc44\ub110 \ubcf4\uae30', '\uba54\uc138\uc9c0 \ubcf4\ub0b4\uae30','\ub9c1\ud06c \ucca8\ubd80'\ub4f1\uc758 \uad8c\ud55c \ud65c\uc131\ud654 \ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624", 22 | "INFO_LANGUAGE": "\uc5b8\uc5b4 \ubcc0\uacbd", 23 | "INFO_LANGUAGE_DESC": "\uae30\ubcf8\uc801\uc73c\ub85c Twitch Alerts\ub294(\uc740) \uc11c\ubc84 \uc5b8\uc5b4\ub85c \uc801\uc6a9\ub429\ub2c8\ub2e4. \uc11c\ubc84\uc5d0\uc11c \uc5b8\uc5b4\ub97c \ubcc0\uacbd\ub824\uba74\n'\uc11c\ubc84 \uc124\uc815'\uc5d0\uc11c \ucee4\ubba4\ub2c8\ud2f0 \ud0ed\uc5d0 \uc77c\ubc18\uc5d0\uc11c '\uc11c\ubc84 \uc8fc\uc694 \uc5b8\uc5b4'\ub97c \ubcc0\uacbd\ud574\uc8fc\uc2ed\uc2dc\uc624.\n\uc11c\ubc84 \uc124\uc815\uc5d0\uc11c \ubcc0\uacbd\ud558\uc9c0 \uc54a\uace0 Twitch Alerts \uc5b8\uc5b4\ub97c \ubcc0\uacbd\ud558\ub824\uba74 {language} \uba85\ub839\uc5b4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \uba85\ub839\uc740 \ud56d\uc0c1 Discord \uc5b8\uc5b4\ub85c \ud45c\uc2dc\ub429\ub2c8\ub2e4.", 24 | "INFO_TRANSLATE": "\ub0b4 \uc5b8\uc5b4\uac00 \ub098\ud0c0\ub098\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", 25 | "INFO_TRANSLATE_DESC": "\ub9cc\uc57d \ub2f9\uc2e0\uc758 \uc5b8\uc5b4\uac00 \ub098\ud0c0\ub098\uc9c0 \uc54a\ub290\ub2e4\uba74,\n[OneSky](https:\/\/harfeur.oneskyapp.com\/collaboration\/project?id=386943)\uc5d0\uc11c \n\ubd07\uc744 \ubcc0\uc5ed\ud558\uc5ec \uae30\uc5ec\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", 26 | "INFO_INVITE": "\ubd07 \ucd08\ub300", 27 | "INFO_SUPPORT": "\uc9c0\uc6d0 \uc11c\ubc84", 28 | "SETUP_TITLE": "\uc0c8\ub85c\uc6b4 \uc54c\ub78c \uc124\uc815", 29 | "SETUP_STREAMER": "\ud2b8\uc704\uce58 \ucc44\ub110(\uc544\uc774\ub514)", 30 | "SETUP_START": "\uc2a4\ud2b8\ub9ac\ubc0d \uc2dc\uc791 \uc2dc \uc54c\ub78c \uba54\uc138\uc9c0", 31 | "SETUP_START_PLACEHOLDER": "\uc2a4\ud2b8\ub9ac\ubc0d\uc774 \uc2dc\uc791\ub429\ub2c8\ub2e4! @everyone", 32 | "SETUP_END": "\uc2a4\ud2b8\ub9ac\ubc0d \uc885\ub8cc \uba54\uc138\uc9c0", 33 | "SETUP_END_PLACEHOLDER": "\uc2a4\ud2b8\ub9ac\ubc0d\uc774 \uc885\ub8cc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc\ubcf4\uae30 \ubcf4\ub7ec\uac00\uae30:", 34 | "SETUP_NO_RESULT": "\ucc3e\uc744 \uc218 \uc5c6\ub294 \uc544\uc774\ub514\uc785\ub2c8\ub2e4. \uc0ac\uc6a9\uc790\uc758 \uc544\uc774\ub514\ub97c \uc62c\ubc14\ub974\uac8c \uc785\ub825\ud588\uc2b5\ub2c8\uae4c?", 35 | "SETUP_ALREADY": "\uc774 \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc774\ubbf8 \uacbd\uace0\uac00 \uc788\uc2b5\ub2c8\ub2e4. {command}\uc744(\ub97c) \uc0ac\uc6a9\ud558\uc5ec \uc0ad\uc81c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4", 36 | "SETUP_NO_PERMISSIONS": "\uc774 \ucc44\ub110\uc5d0\uc11c \ubb38\uc790 \uba54\uc2dc\uc9c0\ub97c \ubcf4\ub0bc \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4... \uc0ac\uc6a9 \uad8c\ud55c\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uc2ed\uc2dc\uc624.", 37 | "SETUP_SUCCESS": "\uc124\uc815\uc644\ub8cc! \ub2e4\uc74c \uc2a4\ud2b8\ub9ac\ubc0d\uc774 \uc2dc\uc791\ub420 \ub54c \ub610\ub294 \uc774\ubbf8 \uc2dc\uc791 \ub41c \uacbd\uc6b0 \uace7 \uc54c\ub78c\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", 38 | "DELETE_EMPTY": "\uc124\uc815\ub41c \uc54c\ub9bc\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. {command}\ub85c \uc2dc\uc791", 39 | "DELETE_CHOOSE_TITLE": "\uc0ad\uc81c\ud560 \uc2a4\ud2b8\ub9ac\uba38 \uc54c\ub78c\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", 40 | "DELETE_CHOOSE_PLACEHOLDER": "\uc0ad\uc81c\ud560 \uc54c\ub9bc \uc120\ud0dd", 41 | "DELETE_SUCCESS": "\uc131\uacf5\uc801\uc73c\ub85c \uc81c\uac70\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", 42 | "DELETE_CHANNEL_NOT_FOUND": "\ucc44\ub110 \uc5c6\uc74c, {canalid}\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", 43 | "LANGUAGE_UPDATE": "Twitch Alerts\uc758 \uc5b8\uc5b4\uac00{language}(\uc73c)\ub85c \ubcc0\uacbd\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", 44 | "DATABASE_ERROR": "\ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4!", 45 | "SETUP_CMD_NAME": "\ucd94\uac00", 46 | "SETUP_CMD_DESC": "\ud604\uc7ac \ucc44\ub110\uc5d0 \uc0c8 \uc54c\ub9bc \ucd94\uac00", 47 | "DELETE_CMD_NAME": "\uc0ad\uc81c", 48 | "DELETE_CMD_DESC": "\uc11c\ubc84\uc5d0\uc11c \uc2a4\ud2b8\ub9ac\uba38 \uc54c\ub78c \uc0ad\uc81c", 49 | "LANGUAGE_CMD_NAME": "\uc5b8\uc5b4", 50 | "LANGUAGE_CMD_DESC": "Twitch Alerts\uc758 \uc5b8\uc5b4 \ubcc0\uacbd", 51 | "LANGUAGE_OPTION_NAME": "\uac12", 52 | "LANGUAGE_OPTION_DESC": "Twitch Alerts\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc5b8\uc5b4", 53 | "LANGUAGE_DEFAULT": "\uc11c\ubc84 \uc5b8\uc5b4", 54 | "INFO_CMD_NAME": "\uc815\ubcf4", 55 | "INFO_CMD_DESC": "Twitch Alerts \ubd07 \uc815\ubcf4 \ubc0f \ub3c4\uc6c0\ub9d0 \ud45c\uc2dc", 56 | "LANGUAGE": "\ud55c\uad6d\uc5b4" 57 | } -------------------------------------------------------------------------------- /public/assets/js/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "navHome": "Accueil", 3 | "navHelp": "Support", 4 | "navDarkMode": "Mode sombre", 5 | "navDashboard": "Tableau de bord", 6 | "footSources": "Sources", 7 | "footDiscord": "Discord", 8 | "footAbout": "\u00c0 propos", 9 | "short": "Alertes Twitch en temps r\u00e9el", 10 | "slogan": "Le meilleur choix pour des alertes Twitch sur Discord", 11 | "usedBy1": "Utilis\u00e9 par", 12 | "usedBy2": "serveur Discord", 13 | "services": "Nos services", 14 | "servicesTitle": "Ce que nous proposons", 15 | "service1H": "Notifications personnalis\u00e9es", 16 | "service1T": "Choisissez le message \u00e0 afficher au d\u00e9but et \u00e0 la fin du LIVE. Vous pouvez mentionner @everyone si vous le souhaitez.", 17 | "service2H": "Mise \u00e0 jour en temps r\u00e9el", 18 | "service2T": "Vous changez de jeu en cours de LIVE ? Pas de panique ! Notre bot se chargera de modifier l'alerte, pour rester toujours \u00e0 jour !", 19 | "service3H": "Configuration simple", 20 | "service3T": "Gr\u00e2ce aux nouvelles fonctionnalit\u00e9s de Discord, configurez simplement vos alertes, et voyez en un coup d'oeil toutes celles configur\u00e9es.", 21 | "service4H": "Incitez \u00e0 la rediffusion", 22 | "service4T": "Lorsque votre LIVE est termin\u00e9, un r\u00e9sum\u00e9 et un lien vers la rediffusion sera disponible directement \u00e0 la place de votre alerte.", 23 | "info1H": "Aider \u00e0 la traduction", 24 | "info1T": "Les traductions \u00e9tant b\u00e9n\u00e9voles, vous pouvez aider", 25 | "info1B": "Aider", 26 | "info2H": "Besoin d'aide", 27 | "info2T": "Rejoignez le serveur Discord si vous avez un probl\u00e8me", 28 | "info2B": "Rejoindre", 29 | "info3H": "Fonctionnement", 30 | "info3T": "Le bot \u00e9tant open-source, vous pouvez consulter et aider au d\u00e9veloppement", 31 | "github": "GitHub", 32 | "terms": "Conditions d'utilisation", 33 | "terms1": "1. Utilisation de nos services", 34 | "terms11": "Vous pouvez utiliser notre Bot uniquement si vous \u00eates en conformit\u00e9 avec les pr\u00e9sentes conditions et toutes les lois applicables. Si vous n'\u00eates pas d'accord avec l'une de ces conditions, il vous est interdit d'utiliser ou d'acc\u00e9der \u00e0 ce site. Les documents contenus dans ce site sont prot\u00e9g\u00e9s par la l\u00e9gislation applicable en mati\u00e8re de droits d'auteur et de marques.", 35 | "terms12": "Toute utilisation ou tout acc\u00e8s par des personnes \u00e2g\u00e9es de moins de 13 ans est interdit.", 36 | "terms2": "2. Utilisation", 37 | "terms21": "Si nous avons connaissance d'un abus li\u00e9 \u00e0 un bug dans le bot ou autre, par la majorit\u00e9 des membres du serveur ou le propri\u00e9taire du serveur, nous avons le droit de faire en sorte que le bot quitte votre serveur et ne le rejoigne plus jamais.", 38 | "terms22": "Si nous constatons qu'un utilisateur abuse du bot ou utilise sa fonction pour intimider ou causer des probl\u00e8mes \u00e0 d'autres personnes, nous nous r\u00e9servons le droit d'interdire \u00e0 cet utilisateur d'utiliser notre bot.", 39 | "terms3": "3. Droits d'auteurs", 40 | "terms31": "Vous n'\u00eates pas autoris\u00e9 \u00e0 h\u00e9berger ce bot et \u00e0 l'utiliser \u00e0 des fins de production. Vous pouvez t\u00e9l\u00e9charger, modifier et dupliquer uniquement \u00e0 des fins de d\u00e9veloppement, et vous pouvez soumettre une demande de modification.", 41 | "terms4": "4. Confidentialit\u00e9 et contact", 42 | "terms41": "En cas de probl\u00e8me, vous pouvez nous contacter \u00e0 l'adresse contact@harfeur.fr. Veuillez \u00e9galement consulter notre", 43 | "privacy": "Politique de confidentialit\u00e9", 44 | "privacy1": "Quelles informations sont enregistr\u00e9es ?", 45 | "privacy11": "Lorsque vous cr\u00e9ez une nouvelle alerte, votre ID de serveur, votre ID de canal et votre ID Twitch sont enregistr\u00e9s. Lorsque l'utilisateur Twitch est en direct, nous stockons temporairement l'ID du message de l'alerte.", 46 | "privacy2": "Comment et o\u00f9 les donn\u00e9es sont-elles stock\u00e9es ?", 47 | "privacy21": "Les donn\u00e9es sont stock\u00e9es dans une base de donn\u00e9es PostgreSQL.", 48 | "privacy22": "Le bot, la base de donn\u00e9es et le site sont situ\u00e9s sur un seul et m\u00eame serveur auto-h\u00e9berg\u00e9 en France en r\u00e9gion Toulousaine.", 49 | "privacy3": "Comment puis-je supprimer mes donn\u00e9es ?", 50 | "privacy31": "Vous pouvez supprimer l'alerte, ce qui supprimera les donn\u00e9es enregistr\u00e9es. Vous pouvez \u00e9galement nous contacter \u00e0 l'adresse contact@harfeur.fr pour plus d'informations, et pour acc\u00e9der \u00e0 toutes les donn\u00e9es stock\u00e9es conform\u00e9ment au RGPD.", 51 | "privacy4": "Est-ce-que le site web me piste ?", 52 | "privacy41": "En acc\u00e9dant \u00e0 ce site internet, nous stockons un cookie nomm\u00e9 user qui permet de rester connect\u00e9 et afficher le tableau de bord. Ce cookie n'est pas utilis\u00e9 pour suivre vos actions.", 53 | "privacy42": "Nous utilisons un service auto-h\u00e9berg\u00e9 Matomo qui permet de recueillir des statistiques sur les personnes qui visitent le site. Les donn\u00e9es sauvegard\u00e9es sont votre navigateur, les pages consult\u00e9es, et les deux premiers octets de votre adresse IP. Si vous ne souhaitez pas que ces donn\u00e9es soient enregistr\u00e9es, vous pouvez activer le param\u00e8tre Do Not Track de votre navigateur.", 54 | "serversH": "Mes serveurs", 55 | "serversT": "Liste des serveurs auxquels vous avez les permissions de g\u00e9rer le bot (permission G\u00e9rer le serveur)", 56 | "loading": "Chargement en cours...", 57 | "othersH": "Autres serveurs", 58 | "othersT": "Liste des serveurs auxquels vous pouvez ajouter le bot (permission G\u00e9rer le serveur)", 59 | "alert1": "alerte", 60 | "alert2": "alertes", 61 | "createButton": "Cr\u00e9er une alerte", 62 | "editButton": "Modifier", 63 | "moveButton": "D\u00e9placer", 64 | "duplicateButton": "Dupliquer", 65 | "deleteButton": "Supprimer", 66 | "colStreamer": "Streameur \/ Streameuse", 67 | "colChannel": "Canal de l'alerte", 68 | "colStart": "Message de l'alerte", 69 | "colEnd": "Message de fin", 70 | "colActions": "Actions", 71 | "modalCreate": "Cr\u00e9er une alerte", 72 | "modalEdit": "Modifier une alerte", 73 | "modalMove": "D\u00e9placer une alerte", 74 | "modalDuplicate": "Dupliquer une alerte", 75 | "modalDelete": "Supprimer une alerte", 76 | "modalDeleteText": "\u00cates-vous s\u00fbr de vouloir supprimer cette alerte ?", 77 | "inputStreamer": "Streameur \/ Streameuse", 78 | "inputStart": "Message de d\u00e9but de stream", 79 | "inputStartPlaceholder": "Le stream commence @everyone", 80 | "inputEnd": "Message de fin de stream", 81 | "inputEndPlaceholder": "Le stream est termin\u00e9. Regardez le replay :", 82 | "inputChannel": "Canal texte", 83 | "inputChannelPlaceholder": "Nom ou identifiant", 84 | "inputGame": "Afficher le jeu", 85 | "inputViewers": "Afficher le nombre de spectateurs", 86 | "create": "Cr\u00e9er", 87 | "edit": "Modifier", 88 | "move": "D\u00e9placer", 89 | "duplicate": "Dupliquer", 90 | "delete": "Supprimer", 91 | "cancel": "Annuler" 92 | } -------------------------------------------------------------------------------- /public/assets/js/translations/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navHome": "Dom\u016f", 3 | "navHelp": "Podpora", 4 | "navDarkMode": "Tmav\u00fd re\u017eim", 5 | "navDashboard": "\u0158\u00edd\u00edc\u00ed panel", 6 | "footSources": "Zdroje", 7 | "footDiscord": "Discord", 8 | "footAbout": "O n\u00e1s", 9 | "short": "Twitch upozorn\u011bn\u00ed v re\u00e1ln\u00e9m \u010dase", 10 | "slogan": "Nejlep\u0161\u00ed volba pro Twitch upozorn\u011bn\u00ed na Discordu", 11 | "usedBy1": "Vyu\u017e\u00edvan\u00fd", 12 | "usedBy2": "Discord servery", 13 | "services": "Na\u0161e slu\u017eby", 14 | "servicesTitle": "Co nab\u00edz\u00edme", 15 | "service1H": "P\u0159izp\u016fsobiteln\u00e1 ozn\u00e1men\u00ed", 16 | "service1T": "Nastavte si vlastn\u00ed zpr\u00e1vu, kter\u00e1 se zobraz\u00ed p\u0159i zah\u00e1jen\u00ed i p\u0159i ukon\u010den\u00ed streamu. M\u016f\u017eete pou\u017e\u00edt i ozna\u010den\u00ed @everyone, kdy\u017e budete cht\u00edt.", 17 | "service2H": "Aktualizace v re\u00e1ln\u00e9m \u010dase", 18 | "service2T": "M\u011bn\u00edte hry b\u011bhem streamu? Bu\u010fte bez obav! N\u00e1\u0161 bot uprav\u00ed upozorn\u011bn\u00ed tak, aby bylo v\u017edy aktu\u00e1ln\u00ed!", 19 | "service3H": "Jednoduch\u00e9 nastaven\u00ed", 20 | "service3T": "D\u00edky nov\u00fdm funkc\u00edm Discordu, m\u016f\u017eete jednodu\u0161e nastavit v\u0161echna Va\u0161e upozorn\u011bn\u00ed a kr\u00e1sn\u011b uvid\u00edte v\u0161echna upozorn\u011bn\u00ed, co jste si nastavili.", 21 | "service4H": "Povzbu\u010fte sv\u00e9 publikum ke zhl\u00e9dnut\u00ed Va\u0161ich z\u00e1znam\u016f", 22 | "service4T": "Kdy\u017e bude V\u00e1\u0161 stream u konce, bot uprav\u00ed Va\u0161e upozorn\u011bn\u00ed na shrnut\u00ed streamu v\u010detn\u011b linku na z\u00e1znam streamu.", 23 | "info1H": "Pomozte s p\u0159ekladem", 24 | "info1T": "P\u0159eklady jsou dobrovoln\u00e9. M\u016f\u017eete pomoci i Vy.", 25 | "info1B": "Pomoci z p\u0159ekladem", 26 | "info2H": "Pot\u0159ebujete pomoc", 27 | "info2T": "P\u0159ipojte se na n\u00e1\u0161 Discord server, pokud m\u00e1te jak\u00fdkoliv probl\u00e9m.", 28 | "info2B": "P\u0159ipojit se", 29 | "info3H": "Provoz", 30 | "info3T": "Tento bot je open-source, tak\u017ee m\u016f\u017eete konzultovat a pom\u00e1hat s v\u00fdvojem.", 31 | "github": "GitHub", 32 | "terms": "Podm\u00ednky u\u017e\u00edv\u00e1n\u00ed", 33 | "terms1": "1. Vyu\u017eit\u00ed na\u0161ich slu\u017eeb", 34 | "terms11": "Na\u0161eho bota m\u016f\u017eete pou\u017e\u00edvat pouze v p\u0159\u00edpad\u011b, \u017ee budete dodr\u017eovat tyto podm\u00ednky a v\u0161echny p\u0159\u00edslu\u0161n\u00e9 z\u00e1kony. Pokud s n\u011bkterou z t\u011bchto podm\u00ednek nesouhlas\u00edte, nem\u011bli byste tuto str\u00e1nku pou\u017e\u00edvat ani k n\u00ed p\u0159istupovat. Materi\u00e1ly obsa\u017een\u00e9 na t\u00e9to str\u00e1nce jsou chr\u00e1n\u011bny p\u0159\u00edslu\u0161n\u00fdmi z\u00e1kony o autorsk\u00fdch pr\u00e1vech a ochrann\u00fdch zn\u00e1mk\u00e1ch.", 35 | "terms12": "Jak\u00e9koli pou\u017eit\u00ed nebo p\u0159\u00edstup osob\u00e1m mlad\u0161\u00edm 13 let je zak\u00e1z\u00e1no.", 36 | "terms2": "2. U\u017e\u00edv\u00e1n\u00ed", 37 | "terms21": "Pokud se dozv\u00edme o zneu\u017eit\u00ed souvisej\u00edc\u00edm s chybou bota nebo jin\u00e9m zneu\u017eit\u00ed v\u011bt\u0161inou \u010dlen\u016f serveru nebo vlastn\u00edkem serveru, m\u00e1me pr\u00e1vo p\u0159im\u011bt robota, aby opustil V\u00e1\u0161 server a nikdy se k n\u011bmu znovu nep\u0159ipojil.", 38 | "terms22": "Pokud zjist\u00edme, \u017ee u\u017eivatel bota zneu\u017e\u00edv\u00e1 nebo pou\u017e\u00edv\u00e1 jeho funkci k zastra\u0161ov\u00e1n\u00ed nebo zp\u016fsobuje probl\u00e9my ostatn\u00edm, vyhrazujeme si pr\u00e1vo zak\u00e1zat tomuto u\u017eivateli pou\u017e\u00edv\u00e1n\u00ed na\u0161eho bota.", 39 | "terms3": "3. Autorsk\u00e1 pr\u00e1va", 40 | "terms31": "Nem\u00e1te povoleno hostovat tohoto robota a pou\u017e\u00edvat jej pro produk\u010dn\u00ed \u00fa\u010dely. M\u016f\u017eete jej st\u00e1hnout, upravit a duplikovat pouze pro \u00fa\u010dely v\u00fdvoje a m\u016f\u017eete podat \u017e\u00e1dost o \u00fapravu.", 41 | "terms4": "4. Soukrom\u00ed a kontakt", 42 | "terms41": "Pokud m\u00e1te n\u011bjak\u00e9 probl\u00e9my, m\u016f\u017eete n\u00e1s kontaktovat na contact@harfeur.fr. Pod\u00edvejte se pros\u00edm tak\u00e9 na na\u0161e", 43 | "privacy": "Z\u00e1sady ochrany osobn\u00edch \u00fadaj\u016f", 44 | "privacy1": "Jak\u00e9 informace jsou uchov\u00e1v\u00e1ny?", 45 | "privacy11": "Kdy\u017e vytvo\u0159\u00edte nov\u00e9 upozorn\u011bn\u00ed, zaznamen\u00e1 se va\u0161e ID serveru, ID kan\u00e1lu a Twitch ID. Kdy\u017e u\u017eivatel Twitche vys\u00edl\u00e1, do\u010dasn\u011b ukl\u00e1d\u00e1me ID zpr\u00e1vy upozorn\u011bn\u00ed.", 46 | "privacy2": "Jak a kde jsou data uchov\u00e1v\u00e1na?", 47 | "privacy21": "Data jsou ulo\u017eena v datab\u00e1zi PostgreSQL.", 48 | "privacy22": "Bot, datab\u00e1ze a webov\u00e1 str\u00e1nka jsou um\u00edst\u011bny na jedin\u00e9m osobn\u011b hostovan\u00e9m serveru ve Francii.", 49 | "privacy3": "Jak vyma\u017eu sv\u00e1 data?", 50 | "privacy31": "Upozorn\u011bn\u00ed m\u016f\u017eete smazat, \u010d\u00edm\u017e dojde k vymaz\u00e1n\u00ed ulo\u017een\u00fdch dat. M\u016f\u017eete n\u00e1s tak\u00e9 kontaktovat na contact@harfeur.fr pro v\u00edce informac\u00ed a pro p\u0159\u00edstup ke v\u0161em ulo\u017een\u00fdm \u00fadaj\u016fm v souladu s GDPR.", 51 | "privacy4": "Sleduje mne tato webov\u00e1 str\u00e1nka?", 52 | "privacy41": "P\u0159i p\u0159\u00edstupu na tuto webovou str\u00e1nku ukl\u00e1d\u00e1me soubor cookie s n\u00e1zvem u\u017eivatel, kter\u00fd v\u00e1m umo\u017e\u0148uje z\u016fstat p\u0159ihl\u00e1\u0161eni a zobrazovat \u0159\u00eddic\u00ed panel. Tento soubor cookie se nepou\u017e\u00edv\u00e1 ke sledov\u00e1n\u00ed va\u0161ich akc\u00ed.", 53 | "privacy42": "Pou\u017e\u00edv\u00e1me samoobslu\u017enou slu\u017ebu Matomo, kter\u00e1 shroma\u017e\u010fuje statistiky o tom, kdo str\u00e1nky nav\u0161t\u011bvuje. Ulo\u017een\u00e1 data jsou v\u00e1\u0161 prohl\u00ed\u017ee\u010d, nav\u0161t\u00edven\u00e9 str\u00e1nky a prvn\u00ed dva bajty va\u0161\u00ed IP adresy. Pokud nechcete, aby se tato data ukl\u00e1dala, m\u016f\u017eete si ve sv\u00e9m prohl\u00ed\u017ee\u010di aktivovat nastaven\u00ed Do Not Track.", 54 | "serversH": "Moje servery", 55 | "serversT": "Seznam server\u016f, u kter\u00fdch m\u00e1te pr\u00e1vo p\u0159istupovat k nastaven\u00ed bota (Spr\u00e1va opr\u00e1vn\u011bn\u00ed serveru)", 56 | "loading": "Prob\u00edh\u00e1 na\u010d\u00edt\u00e1n\u00ed...", 57 | "othersH": "Ostatn\u00ed servery", 58 | "othersT": "Seznam server\u016f, na kter\u00e9 m\u016f\u017eete bota p\u0159idat. (Spr\u00e1va opr\u00e1vn\u011bn\u00ed serveru)", 59 | "alert1": "upozorn\u011bn\u00ed", 60 | "alert2": "upozorn\u011bn\u00ed", 61 | "createButton": "Vytvo\u0159it upozorn\u011bn\u00ed", 62 | "editButton": "Upravit", 63 | "moveButton": "P\u0159esunout", 64 | "duplicateButton": "Duplikovat", 65 | "deleteButton": "Smazat", 66 | "colStreamer": "Streamer (vys\u00edlaj\u00edc\u00ed)", 67 | "colChannel": "Kan\u00e1l upozorn\u011bn\u00ed", 68 | "colStart": "Zpr\u00e1va upozorn\u011bn\u00ed", 69 | "colEnd": "Zpr\u00e1va po ukon\u010den\u00ed vys\u00edl\u00e1n\u00ed", 70 | "colActions": "Akce", 71 | "modalCreate": "Vytvo\u0159it upozorn\u011bn\u00ed", 72 | "modalEdit": "Upravit upzorn\u011bn\u00ed", 73 | "modalMove": "P\u0159esunout upozorn\u011bn\u00ed", 74 | "modalDuplicate": "Duplikovat upozorn\u011bn\u00ed", 75 | "modalDelete": "Smazat upozorn\u011bn\u00ed", 76 | "modalDeleteText": "Opravdu chcete toto upozorn\u011bn\u00ed smazat?", 77 | "inputStreamer": "Streamer (vys\u00edlaj\u00edc\u00ed)", 78 | "inputStart": "Zpr\u00e1va upozorn\u011bn\u00ed na za\u010d\u00e1tek vys\u00edl\u00e1n\u00ed", 79 | "inputStartPlaceholder": "Vys\u00edl\u00e1n\u00ed za\u010dalo @everyone", 80 | "inputEnd": "Zpr\u00e1va upozorn\u011bn\u00ed konce vys\u00edl\u00e1n\u00ed", 81 | "inputEndPlaceholder": "Vys\u00edl\u00e1n\u00ed je u konce. Z\u00e1znam zhl\u00e9dn\u011bte zde:", 82 | "inputChannel": "Textov\u00fd kan\u00e1l", 83 | "inputChannelPlaceholder": "N\u00e1zev nebo ID", 84 | "inputGame": "Display game", 85 | "inputViewers": "Display viewers count", 86 | "create": "Vytvo\u0159it", 87 | "edit": "Upravit", 88 | "move": "P\u0159esunout", 89 | "duplicate": "Duplikovat", 90 | "delete": "Smazat", 91 | "cancel": "Zru\u0161it" 92 | } -------------------------------------------------------------------------------- /app/getAPI.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Database = require('../models/database'); 3 | const DiscordOauth2 = require("discord-oauth2"); 4 | const Discord = require('discord.js'); 5 | const {PermissionsBitField} = require("discord.js"); 6 | const {ApiClient} = require("@twurple/api"); 7 | const f = require("./functions"); 8 | const logger = require("../modules/logger"); 9 | 10 | const SCOPE = "guilds identify guilds.members.read"; 11 | 12 | /** 13 | * @param {express.Application} app Application express 14 | * @param {Database} pgsql Base de données 15 | * @param {DiscordOauth2} oauth Discord Bot 16 | * @param {Discord.Client} discord 17 | * @param {ApiClient} twitch 18 | * @param {f} functions 19 | * @param {String} dirname Nom du répertoire du serveur 20 | * @param {Map} cookies Liste des utilisateurs connectés et leurs cookies 21 | */ 22 | module.exports = function (app, pgsql, oauth, discord, twitch, functions, dirname, cookies) { 23 | 24 | const DISCORD_URL = oauth.generateAuthUrl({ 25 | scope: SCOPE, 26 | redirectUri: "https://" + process.env.DOMAIN + "/connect", 27 | clientId: process.env.DISCORD_CLIENT_ID, 28 | prompt: "none", 29 | responseType: "code" 30 | }) 31 | 32 | app.get('/dashboard', async (req, res) => { 33 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 34 | res.sendFile('/public/dashboard.html', { 35 | root: dirname 36 | }); 37 | } else { 38 | res.redirect(DISCORD_URL); 39 | } 40 | }); 41 | 42 | app.get('/dashboard/:guild_id', async (req, res) => { 43 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 44 | try { 45 | let guild = await discord.guilds.fetch(req.params.guild_id); 46 | let member = await guild.members.fetch(cookies.get(req.cookies.user).id); 47 | if (!member.permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) throw new Error("Permissions insuffisantes pour l'utilisateur"); 48 | } catch (err) { 49 | logger.error(err); 50 | res.redirect("/dashboard"); 51 | return; 52 | } 53 | res.sendFile('/public/server.html', { 54 | root: dirname 55 | }); 56 | } else { 57 | res.redirect(DISCORD_URL); 58 | } 59 | }); 60 | 61 | app.get('/connect', async (req, res) => { 62 | if (req.query.guild_id) { 63 | res.redirect("/dashboard/" + req.query.guild_id); 64 | } else if (req.query.code) { 65 | try { 66 | let data = await oauth.tokenRequest({ 67 | code: req.query.code, scope: SCOPE, grantType: "authorization_code" 68 | }); 69 | 70 | // Create a cookie 71 | let cookie; 72 | do { 73 | cookie = functions.makeid(32); 74 | } while (cookies.has(cookie)); 75 | 76 | // Cache user values 77 | let user = await oauth.getUser(data.access_token); 78 | let guilds = await oauth.getUserGuilds(data.access_token); 79 | 80 | cookies.set(cookie, { 81 | time: Date.now(), timeGuilds: Date.now(), id: user.id, guilds: guilds, ...data 82 | }); 83 | 84 | res.cookie("user", cookie, { 85 | maxAge: 3600000 * 24 * 30, 86 | secure: true, 87 | httpOnly: true 88 | }); 89 | res.redirect("/dashboard"); 90 | } catch (err) { 91 | res.sendStatus(500); 92 | logger.error(err); 93 | } 94 | } else { 95 | res.redirect(DISCORD_URL); 96 | } 97 | }); 98 | 99 | app.get('/servers', async (req, res) => { 100 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 101 | let guilds = cookies.get(req.cookies.user).guilds; 102 | 103 | let data = { 104 | active: [], inactive: [] 105 | } 106 | 107 | try { 108 | for (const i in guilds) { 109 | const guild_partial = guilds[i]; 110 | let permissions = new PermissionsBitField(guild_partial.permissions); 111 | if (permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) { 112 | try { 113 | let guild = await discord.guilds.fetch(guild_partial.id); 114 | let count = await pgsql.countAlertsByGuild(guild.id); 115 | data.active.push({ 116 | name: guild.name, 117 | id: guild.id, 118 | alerts: count, 119 | icon: guild.icon ? "https://cdn.discordapp.com/icons/" + guild.id + "/" + guild.icon + ".png" : "/assets/img/icons/discord.png" 120 | }); 121 | } catch (err) { 122 | data.inactive.push({ 123 | name: guild_partial.name, 124 | id: guild_partial.id, 125 | icon: guild_partial.icon ? "https://cdn.discordapp.com/icons/" + guild_partial.id + "/" + guild_partial.icon + ".png" : "/assets/img/icons/discord.png", 126 | invite: oauth.generateAuthUrl({ 127 | clientId: process.env.DISCORD_CLIENT_ID, 128 | scope: "bot applications.commands", 129 | permissions: 478208, 130 | guildId: guild_partial.id, 131 | disableGuildSelect: true 132 | }) 133 | }); 134 | } 135 | } 136 | } 137 | } catch (err) { 138 | res.sendStatus(500); 139 | logger.error(err); 140 | return; 141 | } 142 | res.send(data); 143 | } else { 144 | res.sendStatus(401); 145 | } 146 | }) 147 | 148 | app.get('/alerts', async (req, res) => { 149 | if (req.cookies.user && await functions.checkToken(req.cookies.user) && req.query.server) { 150 | let guild; 151 | try { 152 | guild = await discord.guilds.fetch(req.query.server); 153 | // let member = await guild.members.fetch(cookies.get(req.cookies.user).id); 154 | // if (!member.permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) throw new Error("Permissions insuffisantes pour l'utilisateur"); 155 | } catch (err) { 156 | logger.error(err); 157 | res.sendStatus(401); 158 | return; 159 | } 160 | 161 | let data = { 162 | alerts: [], 163 | guild_id: guild.id, 164 | guild_name: guild.name, 165 | icon: guild.icon ? "https://cdn.discordapp.com/icons/" + guild.id + "/" + guild.icon + ".png" : "/assets/img/icons/discord.png" 166 | } 167 | 168 | const alerts = await pgsql.listAlertsByGuild(guild.id); 169 | for (const alert of alerts) { 170 | const user = await twitch.users.getUserById(alert.streamer_id); 171 | let channel; 172 | try { 173 | channel = await guild.channels.fetch(alert.alert_channel); 174 | } catch (e) { 175 | channel = { 176 | id: alert.alert_channel, 177 | name: "unknown" 178 | } 179 | } 180 | 181 | data.alerts.push({ 182 | icon: user?.profilePictureUrl ?? "", 183 | name: user?.displayName ?? alert.streamer_id, 184 | id: user?.id ?? alert.streamer_id, 185 | channel_id: channel.id, 186 | channel_name: channel.name, 187 | start: alert.alert_start, 188 | end: alert.alert_end, 189 | display_game: alert.alert_pref_display_game, 190 | display_viewers: alert.alert_pref_display_viewers 191 | }); 192 | } 193 | data.alerts.sort((a, b) => { 194 | let fa = a.name.toLowerCase(); 195 | let fb = b.name.toLowerCase(); 196 | 197 | if (fa < fb) { 198 | return 1; 199 | } 200 | if (fa > fb) { 201 | return -1; 202 | } 203 | return 0; 204 | }) 205 | res.send(data); 206 | } else { 207 | res.sendStatus(402); 208 | } 209 | }); 210 | 211 | } -------------------------------------------------------------------------------- /services/fetchLive.js: -------------------------------------------------------------------------------- 1 | const {EmbedBuilder, PermissionsBitField} = require("discord.js"); 2 | const {getString} = require("../modules/language"); 3 | const logger = require("../modules/logger"); 4 | const {generateLiveEmbed} = require("../models/embedService"); 5 | 6 | class FetchLive { 7 | 8 | /** 9 | * 10 | * @param client 11 | * @param webhooks {EventSubHttpListener[]} 12 | */ 13 | constructor(client, webhooks) { 14 | this.client = client; 15 | 16 | this.webhooks = webhooks; 17 | this.ready = false; 18 | 19 | this.subscriptions = new Map(); 20 | 21 | // Stores data of current streams 22 | this.streamers = new Map(); 23 | 24 | // Stores guild or channel not found to delete old alerts 25 | this.notfound = new Map(); 26 | } 27 | 28 | async markAsReady() { 29 | if (this.ready) return; 30 | this.ready = true; 31 | 32 | while (true) { 33 | try { 34 | logger.debug("Checking streams..."); 35 | await this.checkCurrentStreams(); 36 | await this.updateAlerts(); 37 | logger.debug("Alerts updated"); 38 | } catch (error) { 39 | logger.error(error); 40 | } 41 | } 42 | } 43 | 44 | deleteNotFound(id) { 45 | if (!this.notfound.get(id)) this.notfound.set(id, 0); 46 | this.notfound.set(id, this.notfound.get(id) + 1); 47 | 48 | if (this.notfound.get(id) === 10) { 49 | this.notfound.delete(id); 50 | this.client.container.pg.deleteFromID(id); 51 | } 52 | } 53 | 54 | async checkCurrentStreams() { 55 | const s = Date.now() 56 | 57 | const streamers = await this.client.container.pg.listAllStreamers(); 58 | while (streamers.length) { 59 | // We take the first 100 streamers, because we're limited by Twitch 60 | const streamers100 = streamers.splice(0, 100).map(s => s.streamer_id); 61 | const streamsData = await this.client.container.twitch.streams.getStreamsByUserIds(streamers100); 62 | 63 | // We save the data of the current stream in this.streamers. 64 | for (const streamerID of streamers100) { 65 | if (process.env.BLOCKED?.includes(streamerID)) continue; 66 | const stream = streamsData.filter(stream => stream.userId === streamerID)[0]; 67 | if (stream) { 68 | this.streamers.set(streamerID, stream); 69 | } else { 70 | this.streamers.delete(streamerID); 71 | } 72 | } 73 | } 74 | 75 | logger.debug(`${this.streamers.size} streamers are streaming`); 76 | } 77 | 78 | async updateAlerts() { 79 | const alerts = await this.client.container.pg.listAllAlerts(); 80 | for (const alert of alerts) { 81 | const stream = this.streamers.get(alert.streamer_id); 82 | if (stream || alert.alert_message) { 83 | await this.updateAlert(alert, stream); 84 | } 85 | } 86 | } 87 | 88 | async showStreamOnlineMessage(alert, stream, channel, user, lang) { 89 | if (!user) user = await stream.getUser(); 90 | const game = await stream.getGame(); 91 | 92 | const embed = await generateLiveEmbed(user, stream, game, alert, lang) 93 | 94 | if (!alert.alert_message) { 95 | channel.send({ 96 | content: `${alert.alert_start}\n`, 97 | embeds: [embed] 98 | }).then(msg => { 99 | this.client.container.pg.setAlertMessage(alert.guild_id, alert.streamer_id, msg.id); 100 | }).catch(err => { 101 | logger.debug(`Can't send message in channel ${channel.id}`) 102 | }); 103 | } else { 104 | channel.messages.fetch(alert.alert_message) 105 | .then(message => { 106 | message.edit({ 107 | content: `${alert.alert_start}\n`, 108 | embeds: [embed] 109 | }).catch(err => { 110 | logger.debug(`Can't edit message ${alert.alert_message} in channel ${channel.id}`); 111 | }); 112 | }) 113 | .catch(err => { 114 | logger.debug(`Can't find message ${alert.alert_message} in channel ${channel.id}`) 115 | }) 116 | } 117 | } 118 | 119 | async showStreamOfflineMessage(alert, channel, user, lang) { 120 | if (!user) user = await this.client.container.twitch.users.getUserById(alert.streamer_id); 121 | 122 | await this.client.container.pg.removeAlertMessage(alert.guild_id, alert.streamer_id); 123 | if (alert.alert_message) 124 | channel.messages.fetch(alert.alert_message) 125 | .then(async message => { 126 | 127 | const videos = await this.client.container.twitch.videos.getVideosByUser(alert.streamer_id, { 128 | limit: 1, 129 | orderBy: "time", 130 | type: "archive" 131 | }); 132 | const video = videos.data.length !== 0 ? videos.data[0] : null 133 | 134 | let embed; 135 | 136 | if (!video) { 137 | // Pas de redif 138 | if (message.embeds.length > 0) { 139 | embed = new EmbedBuilder(message.embeds[0].data); 140 | embed.setTitle(`${user.displayName} - ${getString(lang, "LIVE_END")}`); 141 | embed.setFields(embed.data.fields.filter(field => field.name !== getString(lang, "VIEWERS"))); 142 | message.edit({ 143 | content: alert.alert_end, 144 | embeds: [embed] 145 | }).catch(err => { 146 | logger.debug(`Can't edit message ${alert.alert_message} in channel ${channel.id}`); 147 | }); 148 | } else { 149 | message.edit(alert.alert_end).catch(err => { 150 | logger.debug(`Can't edit message ${alert.alert_message} in channel ${channel.id}`); 151 | }); 152 | } 153 | } else { 154 | if (message.embeds.length > 0) { 155 | embed = new EmbedBuilder(message.embeds[0].data); 156 | embed.setTitle(`${user.displayName} - ${getString(lang, "LIVE_END")}`); 157 | embed.setFields(embed.data.fields.filter(field => field.name !== getString(lang, "VIEWERS"))); 158 | embed.setURL(video.url); 159 | message.edit({ 160 | content: `${alert.alert_end} <${video.url}>`, 161 | embeds: [embed] 162 | }).catch(err => { 163 | logger.debug(`Can't edit message ${alert.alert_message} in channel ${channel.id}`); 164 | }); 165 | } else { 166 | message.edit(`${alert.alert_end} <${video.url}>`).catch(err => { 167 | logger.debug(`Can't edit message ${alert.alert_message} in channel ${channel.id}`); 168 | }); 169 | } 170 | } 171 | }).catch(err => { 172 | logger.debug(`Can't find message ${alert.alert_message} in channel ${channel.id}`) 173 | }); 174 | } 175 | 176 | async updateAlert(alert, stream, user=null) { 177 | // Fetch server 178 | let guild; 179 | try { 180 | guild = await this.client.guilds.fetch(alert.guild_id); 181 | } catch (e) { 182 | this.deleteNotFound(alert.guild_id); 183 | if (this.client.container.debug) logger.debug(`Guild ${alert.guild_id} not found`); 184 | return; 185 | } 186 | if (!guild.available) return; 187 | 188 | // Fetch channel 189 | let channel; 190 | try { 191 | channel = await guild.channels.fetch(alert.alert_channel); 192 | } catch (e) { 193 | this.deleteNotFound(alert.alert_channel); 194 | if (this.client.container.debug) logger.debug(`Channel ${alert.alert_channel} not found`); 195 | return; 196 | } 197 | if (!channel.permissionsFor(this.client.user).has([ 198 | PermissionsBitField.Flags.SendMessages, 199 | PermissionsBitField.Flags.EmbedLinks, 200 | PermissionsBitField.Flags.ViewChannel, 201 | PermissionsBitField.Flags.ReadMessageHistory 202 | ])) { 203 | if (alert.alert_message) await this.client.container.pg.removeAlertMessage(alert.guild_id, alert.streamer_id); 204 | return; 205 | } 206 | 207 | const lang = alert.guild_language !== "default" ? alert.guild_language : guild.preferredLocale; 208 | 209 | if (stream) { 210 | await this.showStreamOnlineMessage(alert, stream, channel, user, lang); 211 | } else { 212 | await this.showStreamOfflineMessage(alert, channel, user, lang); 213 | } 214 | } 215 | } 216 | 217 | module.exports = FetchLive; -------------------------------------------------------------------------------- /app/postAPI.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Database = require('../models/database'); 3 | const DiscordOauth2 = require("discord-oauth2"); 4 | const Discord = require('discord.js'); 5 | const {ApiClient} = require("@twurple/api"); 6 | const f = require("./functions"); 7 | const bodyParser = require("body-parser"); 8 | const logger = require("../modules/logger"); 9 | 10 | 11 | /** 12 | * @param {express.Application} app Application express 13 | * @param {Database} pgsql Base de données 14 | * @param {DiscordOauth2} oauth Discord Bot 15 | * @param {Discord.Client} discord 16 | * @param {ApiClient} twitch 17 | * @param {f} functions 18 | * @param {String} dirname Nom du répertoire du serveur 19 | * @param {Map} cookies Liste des utilisateurs connectés et leurs cookies 20 | */ 21 | module.exports = function (app, pgsql, oauth, discord, twitch, functions, dirname, cookies) { 22 | 23 | app.post("/edit", bodyParser.json(), bodyParser.urlencoded({extended: true}), async (req, res) => { 24 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 25 | if (req.body.guild_id && req.body.streamer_id && req.body.streamer_name && req.body.start && req.body.end && /^\d+$/.test(req.body.streamer_id) && req.body.display_game && req.body.display_viewers) { 26 | req.body.display_game = (req.body.display_game === "true"); 27 | req.body.display_viewers = (req.body.display_viewers === "true"); 28 | try { 29 | let guild = await discord.guilds.fetch(req.body.guild_id); 30 | let member = await guild.members.fetch(cookies.get(req.cookies.user).id); 31 | if (!member.permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) throw new Error("Permissions insuffisantes pour l'utilisateur"); 32 | } catch (err) { 33 | logger.error(err); 34 | res.sendStatus(401); 35 | return; 36 | } 37 | try { 38 | const user = await twitch.users.getUserByName(req.body.streamer_name); 39 | if (!user) { 40 | res.sendStatus(406); 41 | return; 42 | } 43 | await pgsql.editAlert(req.body.guild_id, req.body.streamer_id, user.id, req.body.start, req.body.end, req.body.display_game, req.body.display_viewers) 44 | res.send({ 45 | id: user.id, 46 | displayName: user.displayName, 47 | profilePictureUrl: user.profilePictureUrl 48 | }); 49 | } catch (err) { 50 | if (err.code === "23505") res.sendStatus(409); else { 51 | res.sendStatus(500); 52 | logger.error(err); 53 | } 54 | } 55 | } else { 56 | res.sendStatus(400); 57 | } 58 | } else { 59 | res.sendStatus(401); 60 | } 61 | }); 62 | 63 | app.post("/move", bodyParser.json(), bodyParser.urlencoded({extended: true}), async (req, res) => { 64 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 65 | if (req.body.guild_id && req.body.streamer_id && req.body.channel && /^\d+$/.test(req.body.streamer_id)) { 66 | let guild; 67 | try { 68 | guild = await discord.guilds.fetch(req.body.guild_id); 69 | let member = await guild.members.fetch(cookies.get(req.cookies.user).id); 70 | if (!member.permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) throw new Error("Permissions insuffisantes pour l'utilisateur"); 71 | } catch (err) { 72 | logger.error(err); 73 | res.sendStatus(401); 74 | return; 75 | } 76 | 77 | let channel; 78 | try { 79 | if (/^\d+$/.test(req.body.channel)) { 80 | channel = await guild.channels.fetch(req.body.channel); 81 | } else { 82 | channel = guild.channels.cache.find(c => c.name === req.body.channel); 83 | if (!channel) throw new Error("Non trouvé"); 84 | } 85 | if (channel.type !== Discord.ChannelType.GuildText && channel.type !== Discord.ChannelType.GuildAnnouncement) throw new Error("Canal non texte"); 86 | } catch (e) { 87 | res.sendStatus(404); 88 | return; 89 | } 90 | 91 | try { 92 | await pgsql.moveAlert(req.body.guild_id, req.body.streamer_id, channel.id); 93 | res.send({ 94 | channel_id: channel.id, channel_name: channel.name 95 | }); 96 | } catch (err) { 97 | res.sendStatus(500); 98 | logger.error(err); 99 | } 100 | } else { 101 | res.sendStatus(400); 102 | } 103 | } else { 104 | res.sendStatus(401); 105 | } 106 | }) 107 | 108 | app.post("/create", bodyParser.json(), bodyParser.urlencoded({extended: true}), async (req, res) => { 109 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 110 | if (req.body.guild_id && req.body.streamer_name && req.body.start && req.body.end && req.body.channel && req.body.display_game && req.body.display_viewers) { 111 | req.body.display_game = (req.body.display_game === "true"); 112 | req.body.display_viewers = (req.body.display_viewers === "true"); 113 | // Check rights 114 | let guild; 115 | try { 116 | guild = await discord.guilds.fetch(req.body.guild_id); 117 | let member = await guild.members.fetch(cookies.get(req.cookies.user).id); 118 | if (!member.permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) throw new Error("Permissions insuffisantes pour l'utilisateur"); 119 | } catch (err) { 120 | logger.error(err); 121 | res.sendStatus(401); 122 | return; 123 | } 124 | //Check channel 125 | let channel; 126 | try { 127 | if (/^\d+$/.test(req.body.channel)) { 128 | channel = await guild.channels.fetch(req.body.channel); 129 | } else { 130 | channel = guild.channels.cache.find(c => c.name === req.body.channel); 131 | if (!channel) throw new Error("Non trouvé"); 132 | } 133 | if (channel.type !== Discord.ChannelType.GuildText && channel.type !== Discord.ChannelType.GuildAnnouncement) throw new Error("Canal non texte"); 134 | } catch (e) { 135 | res.sendStatus(404); 136 | return; 137 | } 138 | // Check twitch and upload 139 | try { 140 | const user = await twitch.users.getUserByName(req.body.streamer_name); 141 | if (!user) { 142 | res.sendStatus(406); 143 | return; 144 | } 145 | await pgsql.addAlert(req.body.guild_id, user.id, channel.id, req.body.start, req.body.end, req.body.display_game, req.body.display_viewers); 146 | res.send({ 147 | alert: { 148 | icon: user.profilePictureUrl, 149 | name: user.displayName, 150 | id: user.id, 151 | channel_id: channel.id, 152 | channel_name: channel.name, 153 | start: req.body.start, 154 | end: req.body.end, 155 | display_game: req.body.display_game, 156 | display_viewers: req.body.display_viewers 157 | }, 158 | guild_id: guild.id, 159 | guild_name: guild.name, 160 | icon: guild.icon ? "https://cdn.discordapp.com/icons/" + guild.id + "/" + guild.icon + ".png" : "/assets/img/icons/discord.png" 161 | }); 162 | } catch (err) { 163 | if (err.code === "23505") res.sendStatus(409); else { 164 | res.sendStatus(500); 165 | logger.error(err); 166 | } 167 | } 168 | } else { 169 | res.sendStatus(400); 170 | } 171 | } else { 172 | res.sendStatus(401); 173 | } 174 | }); 175 | 176 | app.post("/delete", bodyParser.json(), bodyParser.urlencoded({extended: true}), async (req, res) => { 177 | if (req.cookies.user && await functions.checkToken(req.cookies.user)) { 178 | if (req.body.guild_id && req.body.streamer_id && /^\d+$/.test(req.body.streamer_id)) { 179 | try { 180 | let guild = await discord.guilds.fetch(req.body.guild_id); 181 | let member = await guild.members.fetch(cookies.get(req.cookies.user).id); 182 | if (!member.permissions.has(Discord.PermissionsBitField.Flags.ManageGuild)) throw new Error("Permissions insuffisantes pour l'utilisateur"); 183 | } catch (err) { 184 | logger.error(err); 185 | res.sendStatus(401); 186 | return; 187 | } 188 | try { 189 | await pgsql.deleteAlert(req.body.guild_id, req.body.streamer_id); 190 | res.send("Done"); 191 | } catch (err) { 192 | if (err.code === "23505") res.sendStatus(409); else { 193 | res.sendStatus(500); 194 | logger.error(err); 195 | } 196 | } 197 | } else { 198 | res.sendStatus(400); 199 | } 200 | } else { 201 | res.sendStatus(401); 202 | } 203 | }); 204 | } -------------------------------------------------------------------------------- /public/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tableau de bord - Twitch Alerts 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 59 |
60 | 61 |
62 |
63 |
64 |

Mes serveurs

65 |

Liste des serveurs auxquels vous avez les permissions de gérer le bot (permission Gérer le serveur)

66 |
67 |
68 |
69 |
70 |

Chargement en cours...

71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |

Autres serveurs

82 |

Liste des serveurs auxquels vous pouvez ajouter le bot (permission Gérer le serveur)

83 |
84 |
85 |
86 |
87 |

Chargement en cours...

88 |
89 |
90 |
91 |
92 |
93 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /public/privacy-policy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Politique de confidentialité - Twitch Alerts 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 59 |
60 |
61 |
62 |
63 |

Politique de confidentialité

64 |
65 |
66 |
67 |
68 |

Quelles informations sont enregistrées ?

69 |

Lorsque vous créez une nouvelle alerte, votre ID de serveur, votre ID de canal et votre ID Twitch sont enregistrés. Lorsque l'utilisateur Twitch est en direct, nous stockons temporairement l'ID du message de l'alerte.

70 |
71 |
72 |
73 |
74 |

Comment et où les données sont-elles stockées ?

75 |

Les données sont stockées dans une base de données PostgreSQL.

76 |

Le bot, la base de données et le site sont situés sur un seul et même serveur auto-hébergé en France en région Toulousaine.

77 |
78 |
79 |
80 |
81 |

Comment puis-je supprimer mes données ?

82 |

Vous pouvez supprimer l'alerte, ce qui supprimera les données enregistrées. Vous pouvez également nous contacter à l'adresse contact@harfeur.fr pour plus d'informations, et pour accéder à toutes les données stockées conformément au RGPD.

83 |
84 |
85 |
86 |
87 |

Est-ce-que le site web me piste ?

88 |

En accédant à ce site internet, nous stockons un cookie nommé user qui permet de rester connecté et afficher le tableau de bord. Ce cookie n'est pas utilisé pour suivre vos actions.

89 |

Nous utilisons un service auto-hébergé Matomo qui permet de recueillir des statistiques sur les personnes qui visitent le site. Les données sauvegardées sont votre navigateur, les pages consultées, et les deux premiers octets de votre adresse IP. Si vous ne souhaitez pas que ces données soient enregistrées, vous pouvez activer le paramètre Do Not Track de votre navigateur.

90 |
91 |
92 |
93 |
94 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /public/terms-of-service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Conditions d'utilisation - Twitch Alerts 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 59 |
60 |
61 |
62 |
63 |

Conditions d'utilisation

64 |
65 |
66 |
67 |
68 |

1. Utilisation de nos services

69 |

Vous pouvez utiliser notre Bot uniquement si vous êtes en conformité avec les présentes conditions et toutes les lois applicables. Si vous n'êtes pas d'accord avec l'une de ces conditions, il vous est interdit d'utiliser ou d'accéder à ce site. Les documents contenus dans ce site sont protégés par la législation applicable en matière de droits d'auteur et de marques.

70 |

Toute utilisation ou tout accès par des personnes âgées de moins de 13 ans est interdit.

71 |
72 |
73 |
74 |
75 |

2. Utilisation

76 |

Si nous avons connaissance d'un abus lié à un bug dans le bot ou autre, par la majorité des membres du serveur ou le propriétaire du serveur, nous avons le droit de faire en sorte que le bot quitte votre serveur et ne le rejoigne plus jamais.

77 |

Si nous constatons qu'un utilisateur abuse du bot ou utilise sa fonction pour intimider ou causer des problèmes à d'autres personnes, nous nous réservons le droit d'interdire à cet utilisateur d'utiliser notre bot.

78 |
79 |
80 |
81 |
82 |

3. Droits d'auteurs

83 |

Vous n'êtes pas autorisé à héberger ce bot et à l'utiliser à des fins de production. Vous pouvez télécharger, modifier et dupliquer uniquement à des fins de développement, et vous pouvez soumettre une demande de modification.

84 |
85 |
86 |
87 |
88 |

4. Confidentialité et contact

89 |

En cas de problème, vous pouvez nous contacter à l'adresse contact@harfeur.fr. Veuillez également consulter notre politique de confidentialité.

90 |
91 |
92 |
93 |
94 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /public/assets/css/Inter.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 4 | font-weight: 300; 5 | font-style: normal; 6 | font-display: swap; 7 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 8 | } 9 | 10 | @font-face { 11 | font-family: 'Inter'; 12 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 13 | font-weight: 300; 14 | font-style: normal; 15 | font-display: swap; 16 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 17 | } 18 | 19 | @font-face { 20 | font-family: 'Inter'; 21 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 22 | font-weight: 300; 23 | font-style: normal; 24 | font-display: swap; 25 | unicode-range: U+1F00-1FFF; 26 | } 27 | 28 | @font-face { 29 | font-family: 'Inter'; 30 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 31 | font-weight: 300; 32 | font-style: normal; 33 | font-display: swap; 34 | unicode-range: U+0370-03FF; 35 | } 36 | 37 | @font-face { 38 | font-family: 'Inter'; 39 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 40 | font-weight: 300; 41 | font-style: normal; 42 | font-display: swap; 43 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 44 | } 45 | 46 | @font-face { 47 | font-family: 'Inter'; 48 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 49 | font-weight: 300; 50 | font-style: normal; 51 | font-display: swap; 52 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 53 | } 54 | 55 | @font-face { 56 | font-family: 'Inter'; 57 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 58 | font-weight: 300; 59 | font-style: normal; 60 | font-display: swap; 61 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 62 | } 63 | 64 | @font-face { 65 | font-family: 'Inter'; 66 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 67 | font-weight: 400; 68 | font-style: normal; 69 | font-display: swap; 70 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 71 | } 72 | 73 | @font-face { 74 | font-family: 'Inter'; 75 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 76 | font-weight: 400; 77 | font-style: normal; 78 | font-display: swap; 79 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 80 | } 81 | 82 | @font-face { 83 | font-family: 'Inter'; 84 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 85 | font-weight: 400; 86 | font-style: normal; 87 | font-display: swap; 88 | unicode-range: U+1F00-1FFF; 89 | } 90 | 91 | @font-face { 92 | font-family: 'Inter'; 93 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 94 | font-weight: 400; 95 | font-style: normal; 96 | font-display: swap; 97 | unicode-range: U+0370-03FF; 98 | } 99 | 100 | @font-face { 101 | font-family: 'Inter'; 102 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 103 | font-weight: 400; 104 | font-style: normal; 105 | font-display: swap; 106 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 107 | } 108 | 109 | @font-face { 110 | font-family: 'Inter'; 111 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 112 | font-weight: 400; 113 | font-style: normal; 114 | font-display: swap; 115 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 116 | } 117 | 118 | @font-face { 119 | font-family: 'Inter'; 120 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 121 | font-weight: 400; 122 | font-style: normal; 123 | font-display: swap; 124 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 125 | } 126 | 127 | @font-face { 128 | font-family: 'Inter'; 129 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 130 | font-weight: 600; 131 | font-style: normal; 132 | font-display: swap; 133 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 134 | } 135 | 136 | @font-face { 137 | font-family: 'Inter'; 138 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 139 | font-weight: 600; 140 | font-style: normal; 141 | font-display: swap; 142 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 143 | } 144 | 145 | @font-face { 146 | font-family: 'Inter'; 147 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 148 | font-weight: 600; 149 | font-style: normal; 150 | font-display: swap; 151 | unicode-range: U+1F00-1FFF; 152 | } 153 | 154 | @font-face { 155 | font-family: 'Inter'; 156 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 157 | font-weight: 600; 158 | font-style: normal; 159 | font-display: swap; 160 | unicode-range: U+0370-03FF; 161 | } 162 | 163 | @font-face { 164 | font-family: 'Inter'; 165 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 166 | font-weight: 600; 167 | font-style: normal; 168 | font-display: swap; 169 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 170 | } 171 | 172 | @font-face { 173 | font-family: 'Inter'; 174 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 175 | font-weight: 600; 176 | font-style: normal; 177 | font-display: swap; 178 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 179 | } 180 | 181 | @font-face { 182 | font-family: 'Inter'; 183 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 184 | font-weight: 600; 185 | font-style: normal; 186 | font-display: swap; 187 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 188 | } 189 | 190 | @font-face { 191 | font-family: 'Inter'; 192 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 193 | font-weight: 700; 194 | font-style: normal; 195 | font-display: swap; 196 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 197 | } 198 | 199 | @font-face { 200 | font-family: 'Inter'; 201 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 202 | font-weight: 700; 203 | font-style: normal; 204 | font-display: swap; 205 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 206 | } 207 | 208 | @font-face { 209 | font-family: 'Inter'; 210 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 211 | font-weight: 700; 212 | font-style: normal; 213 | font-display: swap; 214 | unicode-range: U+1F00-1FFF; 215 | } 216 | 217 | @font-face { 218 | font-family: 'Inter'; 219 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 220 | font-weight: 700; 221 | font-style: normal; 222 | font-display: swap; 223 | unicode-range: U+0370-03FF; 224 | } 225 | 226 | @font-face { 227 | font-family: 'Inter'; 228 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 229 | font-weight: 700; 230 | font-style: normal; 231 | font-display: swap; 232 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 233 | } 234 | 235 | @font-face { 236 | font-family: 'Inter'; 237 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 238 | font-weight: 700; 239 | font-style: normal; 240 | font-display: swap; 241 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 242 | } 243 | 244 | @font-face { 245 | font-family: 'Inter'; 246 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 247 | font-weight: 700; 248 | font-style: normal; 249 | font-display: swap; 250 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 251 | } 252 | 253 | @font-face { 254 | font-family: 'Inter'; 255 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 256 | font-weight: 800; 257 | font-style: normal; 258 | font-display: swap; 259 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 260 | } 261 | 262 | @font-face { 263 | font-family: 'Inter'; 264 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 265 | font-weight: 800; 266 | font-style: normal; 267 | font-display: swap; 268 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 269 | } 270 | 271 | @font-face { 272 | font-family: 'Inter'; 273 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 274 | font-weight: 800; 275 | font-style: normal; 276 | font-display: swap; 277 | unicode-range: U+1F00-1FFF; 278 | } 279 | 280 | @font-face { 281 | font-family: 'Inter'; 282 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 283 | font-weight: 800; 284 | font-style: normal; 285 | font-display: swap; 286 | unicode-range: U+0370-03FF; 287 | } 288 | 289 | @font-face { 290 | font-family: 'Inter'; 291 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 292 | font-weight: 800; 293 | font-style: normal; 294 | font-display: swap; 295 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 296 | } 297 | 298 | @font-face { 299 | font-family: 'Inter'; 300 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 301 | font-weight: 800; 302 | font-style: normal; 303 | font-display: swap; 304 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 305 | } 306 | 307 | @font-face { 308 | font-family: 'Inter'; 309 | src: url(/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2?h=99360fb6e3d2c11563ad52f91e8d53b8) format('woff2'); 310 | font-weight: 800; 311 | font-style: normal; 312 | font-display: swap; 313 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 314 | } --------------------------------------------------------------------------------