Initial commit
This commit is contained in:
		
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | MIT License | ||||||
|  |  | ||||||
|  | Copyright (c) 2017 Karar Al-Remahy | ||||||
|  |  | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  |  | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  |  | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
							
								
								
									
										78
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | # DiscordTwitchAnnouncer | ||||||
|  |  | ||||||
|  | ## Announces when Twitch channels go live, in Discord | ||||||
|  |  | ||||||
|  | ### Updating from 2.x to 3.x | ||||||
|  |  | ||||||
|  |   1. Update your NodeJS version to 12.16.3 or a later version! **(Tested on 12.16.3 and 14.1.0)** | ||||||
|  |   2. Please run `npm install` again to update libraries and dependencies! | ||||||
|  |   3. Please add `twitch.clientSecret` to `settings.js` file. | ||||||
|  |   4. **Please do not share** your `settings.js` file and the new `token.json` file. They both include secrets that allow other people to use your authentications. | ||||||
|  |  | ||||||
|  | ### 5 Step Setup | ||||||
|  |  | ||||||
|  |   1. Get NodeJS, v12.x.x or newer **(Tested & Works on v12.16.3)**. | ||||||
|  |   2. Git clone or download this repository and then change to the directory in your console/terminal. | ||||||
|  |   3. Type `npm install` in your console/terminal and wait for dependencies to download and install successfully. | ||||||
|  |   4. Open up `settings.js` with any text program: | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | module.exports = { | ||||||
|  |   timer: 61000, // Is in milliseconds. Default: 61000 ms = 1 minute & 1 second. | ||||||
|  |   cooldownTimer: 21600000, // Is in milliseconds. Default: 21600000 ms = 6 hours. | ||||||
|  |   language: 'english', // Default language 'english'. Other languages available in `i18n` folder. | ||||||
|  |   twitch: { | ||||||
|  |     clientID: '', // Make a Twitch application at | ||||||
|  |     clientSecret: '' // https://dev.twitch.tv/console/apps | ||||||
|  |   }, | ||||||
|  |   discord: { | ||||||
|  |     defaultPrefix: '!', | ||||||
|  |     token: '', // https://discordapp.com/developers/applications/me/ | ||||||
|  |     permissionForCommands: 'MANAGE_ROLES', // https://discordapp.com/developers/docs/topics/permissions | ||||||
|  |     message: '@everyone', // The default text on announcement, before the url and stream type. Can be changed with !message command. Default: '@everyone' = '@everyone LIVE! https://twitch.tv/stream' | ||||||
|  |     activity: ['LISTENING', 'Twitch API.'] // Status, second entry in array is your custom activity text. If second or first entry is empty, no custom activity will be displayed. | ||||||
|  |     /** First entry in the above array can only be the following, and will default to 'PLAYING'. | ||||||
|  |      * PLAYING | ||||||
|  |      * STREAMING | ||||||
|  |      * LISTENING | ||||||
|  |      * WATCHING | ||||||
|  |      */ | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |   5. Change the fields accordingly. *(Fields `twitch.clientID`, `twitch.clientSecret` & `discord.token` must have a value, otherwise program will error.)* | ||||||
|  |  | ||||||
|  | **Type `node app.js` in your console/terminal to run program.** | ||||||
|  |  | ||||||
|  | After you've started the announcer, invite the bot and go to your discord channel. | ||||||
|  |  | ||||||
|  | #### Commands | ||||||
|  |  | ||||||
|  | Available commands: | ||||||
|  |  | ||||||
|  | * `!help` | ||||||
|  | * `!uptime` | ||||||
|  | * `!streamers` | ||||||
|  | * (Example) `!timezone sv-SE Europe/Stockholm` Check [IANA BCP 47 Subtag registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) & [IETF RFC 5646](https://tools.ietf.org/html/rfc5646) for locale tags and [IANA Time Zone Database](https://www.iana.org/time-zones) or [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for timezones. | ||||||
|  | * (Example) `!channel #general` | ||||||
|  |   * (Example) `!channel 000000000000000000` | ||||||
|  | * (Example) `!operator @User_Name` | ||||||
|  | * (Example) `!add Streamer_Name` | ||||||
|  | * (Example) `!remove Streamer_Name` | ||||||
|  | * (Example) `!reaction 👍` | ||||||
|  | * (Example) `!message <streamerName> @here %name% is **%status%** streaming, **%game%**: *%title%* %link%` | ||||||
|  |   * `%name%` Streamer's name | ||||||
|  |   * `%status%` VOD / LIVE / RERUN? | ||||||
|  |   * `%game%` Game name | ||||||
|  |   * `%title%` Stream title | ||||||
|  |   * `%link%` Twitch link | ||||||
|  | * (Example) `!prefix #` | ||||||
|  | * (Example) `!language english` Check i18n folder for available languages. | ||||||
|  | * (Example) `!announcementchannel Streamer_Name 000000000000000000` | ||||||
|  |  | ||||||
|  | ### Contributing | ||||||
|  |  | ||||||
|  | Fork project & Send a pull request. Use eslint, thanks. | ||||||
|  |  | ||||||
|  | ### License MIT | ||||||
							
								
								
									
										873
									
								
								app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										873
									
								
								app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,873 @@ | |||||||
|  | const fs = require('fs') | ||||||
|  | const path = require('path') | ||||||
|  |  | ||||||
|  | const moment = require('moment-timezone') | ||||||
|  | const fetch = require('node-fetch') | ||||||
|  |  | ||||||
|  | const { Intents, Client, MessageEmbed } = require('discord.js') | ||||||
|  | const client = new Client({ | ||||||
|  |   intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS] | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const settings = require('./settings.js') | ||||||
|  |  | ||||||
|  | const translations = {} | ||||||
|  | const translationsDir = fs.readdirSync(path.join(__dirname, 'i18n')) | ||||||
|  | translationsDir.forEach(i => { | ||||||
|  |   const name = i.replace('.json', '') | ||||||
|  |   translations[name] = JSON.parse(fs.readFileSync(path.join(__dirname, 'i18n', i))) | ||||||
|  | }) | ||||||
|  | const translate = translations.english | ||||||
|  |  | ||||||
|  | if (!settings.discord.token) throw new Error(translate.noDiscordToken) | ||||||
|  | if (!settings.twitch.clientID) throw new Error(translate.noTwitchClientID) | ||||||
|  | if (!settings.twitch.clientSecret) { | ||||||
|  |   console.log(translate.includeTwitchClientSecret) | ||||||
|  |   throw new Error(translate.noTwitchClientSecret) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | if (typeof settings.cooldownTimer === 'undefined') { | ||||||
|  |   console.log(translate.includeCooldownTimerWarning) | ||||||
|  |   settings.cooldownTimer = 21600000 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Create data.json if it doesn't exist. | ||||||
|  | if (!fs.existsSync(path.join(__dirname, 'data.json'))) { | ||||||
|  |   fs.writeFileSync(path.join(__dirname, 'data.json'), JSON.stringify({ guilds: {} }, null, 2)) | ||||||
|  |   console.log(translate.createdDataJSON) | ||||||
|  | } | ||||||
|  | const tokenFilePath = path.join(__dirname, 'token.json') | ||||||
|  |  | ||||||
|  | let data = require('./data.json') | ||||||
|  |  | ||||||
|  | // https://stackoverflow.com/a/55435856 | ||||||
|  | function chunks (arr, n) { | ||||||
|  |   function * ch (arr, n) { | ||||||
|  |     for (let i = 0; i < arr.length; i += n) { | ||||||
|  |       yield (arr.slice(i, i + n)) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return [...ch(arr, n)] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // [{ guild: id, entry: 'entry', value: 'value'}] | ||||||
|  | function saveData (d = [{ guild: '', entry: '', action: '', value: 'any' }]) { | ||||||
|  |   const dataOnFile = JSON.parse(fs.readFileSync(path.join(__dirname, 'data.json'))) | ||||||
|  |   for (let index = 0; index < d.length; index++) { | ||||||
|  |     const object = d[index] | ||||||
|  |     try { | ||||||
|  |       switch (object.action) { | ||||||
|  |         case 'push': | ||||||
|  |           dataOnFile.guilds[object.guild][object.entry].push(object.value) | ||||||
|  |           break | ||||||
|  |         case 'splice': | ||||||
|  |           dataOnFile.guilds[object.guild][object.entry].splice(object.value[0], object.value[1]) | ||||||
|  |           break | ||||||
|  |         case 'addGuild': | ||||||
|  |           dataOnFile.guilds[object.guild] = defaultGuildData | ||||||
|  |           break | ||||||
|  |         case 'removeGuild': | ||||||
|  |           dataOnFile.guilds[object.guild] = undefined | ||||||
|  |           break | ||||||
|  |         default: | ||||||
|  |           dataOnFile.guilds[object.guild][object.entry] = object.value | ||||||
|  |           break | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(e) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   data = dataOnFile | ||||||
|  |   return fs.writeFileSync(path.join(__dirname, 'data.json'), JSON.stringify(dataOnFile, null, 2)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const cache = { | ||||||
|  |   guilds: [] | ||||||
|  | } | ||||||
|  | const initialization = new Date() | ||||||
|  | const defaultGuildData = { | ||||||
|  |   streamers: [], | ||||||
|  |   announcementChannel: null, | ||||||
|  |   reactions: [], | ||||||
|  |   message: '@everyone %name% **%status%**!', | ||||||
|  |   time: { locale: Intl.DateTimeFormat().resolvedOptions().locale, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }, | ||||||
|  |   prefix: '!', | ||||||
|  |   language: settings.language || 'english' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let disconnect = false | ||||||
|  | let headers = new fetch.Headers({}) | ||||||
|  | let tokenExpirationDate | ||||||
|  |  | ||||||
|  | // Prototypal. Good for now. | ||||||
|  | function translateDefault (language) { | ||||||
|  |   const result = {} | ||||||
|  |   const lang = translations[language] | ||||||
|  |   const english = translate | ||||||
|  |  | ||||||
|  |   Object.keys(english).forEach(i => { | ||||||
|  |     result[i] = JSON.parse(JSON.stringify(english[i])) | ||||||
|  |     if (lang[i]) result[i] = JSON.parse(JSON.stringify(lang[i])) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   Object.keys(english.commands).forEach(i => { | ||||||
|  |     result.commands[i] = JSON.parse(JSON.stringify(english.commands[i])) | ||||||
|  |     if (lang.commands[i]) result.commands[i] = JSON.parse(JSON.stringify(lang.commands[i])) | ||||||
|  |  | ||||||
|  |     Object.keys(english.commands[i]).forEach(ii => { | ||||||
|  |       result.commands[i][ii] = JSON.parse(JSON.stringify(english.commands[i][ii])) | ||||||
|  |       if (lang.commands[i][ii]) result.commands[i][ii] = JSON.parse(JSON.stringify(lang.commands[i][ii])) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function refreshAppToken () { | ||||||
|  |   let tokenJSON | ||||||
|  |  | ||||||
|  |   if (typeof tokenExpirationDate !== 'number' && fs.existsSync(tokenFilePath)) { | ||||||
|  |     try { | ||||||
|  |       tokenJSON = JSON.parse(fs.readFileSync(tokenFilePath)) | ||||||
|  |       tokenExpirationDate = tokenJSON.expiration | ||||||
|  |  | ||||||
|  |       console.log(translate.usingExistingToken, new Date(tokenJSON.expiration).toUTCString()) | ||||||
|  |       headers = new fetch.Headers({ | ||||||
|  |         Authorization: `Bearer ${tokenJSON.superSecret}`, | ||||||
|  |         'Client-ID': settings.twitch.clientID | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       // Validate token | ||||||
|  |       try { | ||||||
|  |         const res = await fetch('https://id.twitch.tv/oauth2/validate', { | ||||||
|  |           headers: new fetch.Headers({ | ||||||
|  |             Authorization: `OAuth ${tokenJSON.superSecret}` | ||||||
|  |           }) | ||||||
|  |         }).then(res => res.json()) | ||||||
|  |  | ||||||
|  |         if (res.client_id !== settings.twitch.clientID) throw new Error('Missmatch') | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err.message === 'Missmatch') console.log(translate.missmatchToken) | ||||||
|  |         console.log(translate.invalidTokenResponse) | ||||||
|  |         tokenExpirationDate = 0 | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       tokenExpirationDate = Date.now() - 1 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (Date.now() >= (tokenExpirationDate || 0)) { | ||||||
|  |     try { | ||||||
|  |       const res = await fetch(`https://id.twitch.tv/oauth2/token?client_id=${settings.twitch.clientID}&client_secret=${settings.twitch.clientSecret}&grant_type=client_credentials`, { method: 'POST' }).then(res => res.json()) | ||||||
|  |  | ||||||
|  |       const expirationDate = Date.now() + (res.expires_in * 1000) | ||||||
|  |  | ||||||
|  |       headers = new fetch.Headers({ | ||||||
|  |         Authorization: `Bearer ${res.access_token}`, | ||||||
|  |         'Client-ID': settings.twitch.clientID | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       console.log(translate.wroteTokenToDisk) | ||||||
|  |       fs.writeFileSync(tokenFilePath, JSON.stringify({ | ||||||
|  |         expiration: expirationDate, | ||||||
|  |         superSecret: res.access_token | ||||||
|  |       })) | ||||||
|  |  | ||||||
|  |       tokenExpirationDate = expirationDate | ||||||
|  |     } catch (err) { | ||||||
|  |       console.log(translate.genericTokenError) | ||||||
|  |       console.error(err) | ||||||
|  |       return false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function sendTestMessage (translate, message, streamer = 'twitchdev') { | ||||||
|  |   const test = { | ||||||
|  |     gameInfo: { | ||||||
|  |       name: translate.commands.add.gameInfoName, | ||||||
|  |       box_art_url: 'https://static-cdn.jtvnw.net/ttv-boxart/Science%20&%20Technology-{width}x{height}.jpg' | ||||||
|  |     }, | ||||||
|  |     streamInfo: { | ||||||
|  |       name: streamer, | ||||||
|  |       avatar: 'https://brand.twitch.tv/assets/images/twitch-extruded.png', | ||||||
|  |       type: translate.commands.add.streamInfoType, | ||||||
|  |       title: translate.commands.add.streamInfoTitle | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const embed = streamPreviewEmbed(message.gid, { ...test, imageFileName: null }) | ||||||
|  |     embed.setImage('https://static-cdn.jtvnw.net/ttv-static/404_preview-1920x1080.jpg') | ||||||
|  |     await message.discord.channel.send(parseAnnouncementMessage(message.gid, test), { embed }) | ||||||
|  |   } catch (err) { | ||||||
|  |     if (err.message !== 'Missing Permissions') { | ||||||
|  |       await message.discord.channel.send(parseAnnouncementMessage(message.gid, test)) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class Message { | ||||||
|  |   constructor (message) { | ||||||
|  |     this.cmd = message.content.replace(new RegExp(`^<@${client.user.id}> `), '!').split(/[ ]+/) | ||||||
|  |     this.discord = message | ||||||
|  |     this.gid = message.guild.id | ||||||
|  |     this.prefix = data.guilds[this.gid].prefix || '!' | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class Command { | ||||||
|  |   constructor ({ helpText, commandNames, handler }) { | ||||||
|  |     this.helpText = helpText // String or Function | ||||||
|  |     this.commandNames = commandNames // Array | ||||||
|  |     this.handler = handler // Function | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   showHelpText (message) { | ||||||
|  |     return typeof this.helpText === 'function' ? this.helpText(message) : this.helpText | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const commands = (translate) => [ | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.help.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.help.helpText.replace('%1', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: async (message) => { | ||||||
|  |       // Help command. | ||||||
|  |       if (message.cmd[1]) { | ||||||
|  |         const command = commands(translate).find(command => command.commandNames.indexOf(message.cmd[1].toLowerCase()) > -1) | ||||||
|  |         return message.discord.reply(command ? typeof command.helpText === 'function' ? command.helpText(message) : command.helpText : 'that command does not exist.') | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         const embed = new MessageEmbed() | ||||||
|  |           .setTitle(translate.commands.help.availableCommands) | ||||||
|  |         for (let index = 0; index < commands(translate).length; index++) { | ||||||
|  |           const cmd = commands(translate)[index] | ||||||
|  |           embed.addField(cmd.commandNames.join(', '), typeof cmd.helpText === 'function' ? cmd.helpText(message) : cmd.helpText) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await message.discord.channel.send(translate.commands.help.message, { embed }) | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err.message === 'Missing Permissions') { | ||||||
|  |           return message.discord.reply(translate.commands.help.message.concat(commands.map(cmd => `\n${typeof cmd.helpText === 'function' ? cmd.helpText(message) : cmd.helpText}`))) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.uptime.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.uptime.helpText.replace('%1', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       // Uptime command. | ||||||
|  |       const time = Date.now() - initialization | ||||||
|  |       let seconds = time / 1000 | ||||||
|  |       const hours = parseInt(seconds / 3600) | ||||||
|  |       seconds = seconds % 3600 | ||||||
|  |       const minutes = parseInt(seconds / 60) | ||||||
|  |       seconds = seconds % 60 | ||||||
|  |       return message.discord.reply( | ||||||
|  |         `%1 ${minutes > 0 ? `${hours > 0 ? `${hours} %2,` : ''}${minutes} %3 ` : ''}${seconds.toFixed(0)} %4.\n(%5 ${moment.utc(initialization).locale(data.guilds[message.gid].time.locale).tz(data.guilds[message.gid].time.timeZone).format('LL LTS zz')}.)` | ||||||
|  |           .replace('%1', translate.commands.uptime.message) | ||||||
|  |           .replace('%2', translate.commands.uptime.hoursComma) | ||||||
|  |           .replace('%3', translate.commands.uptime.minutesAnd) | ||||||
|  |           .replace('%4', translate.commands.uptime.seconds) | ||||||
|  |           .replace('%5', translate.commands.uptime.onlineSince) | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.add.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.add.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: async (message) => { | ||||||
|  |       // Add streamer to cache. | ||||||
|  |       const streamerName = message.cmd[1] ? message.cmd[1].toLowerCase().split('/').pop() : false | ||||||
|  |       if (!streamerName) return false | ||||||
|  |  | ||||||
|  |       const sanitizedStreamerName = streamerName.toLowerCase().normalize().replace(/[^\w]/g, '') | ||||||
|  |       if (cache.guilds[message.gid].findIndex(s => s.name.toLowerCase() === sanitizedStreamerName) > -1) return message.discord.reply(translate.commands.add.alreadyExists) | ||||||
|  |  | ||||||
|  |       await refreshAppToken() | ||||||
|  |  | ||||||
|  |       let user = await fetch(`https://api.twitch.tv/helix/users/?login=${sanitizedStreamerName}`, { headers }) | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         user = await user.json() | ||||||
|  |  | ||||||
|  |         if (user.data.length === 0) { | ||||||
|  |           return message.discord.reply( | ||||||
|  |             translate.commands.add.doesNotExist | ||||||
|  |               .replace('%1', sanitizedStreamerName) | ||||||
|  |           ) | ||||||
|  |         } else user = user.data[0] | ||||||
|  |       } catch (error) { | ||||||
|  |         return message.discord.reply( | ||||||
|  |           translate.twitchError | ||||||
|  |             .replace('%1', error.message) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       cache.guilds[message.gid].push({ name: sanitizedStreamerName }) | ||||||
|  |       saveData([{ guild: message.gid, entry: 'streamers', action: 'push', value: { name: sanitizedStreamerName } }]) | ||||||
|  |  | ||||||
|  |       return message.discord.reply( | ||||||
|  |         `%1 ${data.guilds[message.gid].announcementChannel ? '' : '\n%2'}` | ||||||
|  |           .replace('%1', translate.commands.add.message.replace('%1', sanitizedStreamerName)) | ||||||
|  |           .replace('%2', translate.commands.add.addAnnouncementChannel) | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.remove.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.remove.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       // Remove streamer from cache. | ||||||
|  |       const streamerName = message.cmd[1] ? message.cmd[1].toLowerCase().split('/').pop() : false | ||||||
|  |       if (!streamerName) return false | ||||||
|  |       if (cache.guilds[message.gid].findIndex(s => s.name.toLowerCase() === streamerName) === -1) return message.discord.reply(translate.commands.remove.doesNotExist) | ||||||
|  |  | ||||||
|  |       cache.guilds[message.gid] = cache.guilds[message.gid].filter(s => s.name.toLowerCase() !== streamerName) | ||||||
|  |       saveData([{ guild: message.gid, entry: 'streamers', value: data.guilds[message.gid].streamers.filter(s => s.name !== streamerName) }]) | ||||||
|  |       return message.discord.reply(translate.commands.remove.message) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.channel.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       const discordChannel = message.discord.guild.channels.cache.filter(channel => channel.type === 'text' && channel.memberPermissions(message.discord.guild.me).has('SEND_MESSAGES')).first() | ||||||
|  |       return translate.commands.channel.helpText | ||||||
|  |         .replace(/%1/g, translate.example) | ||||||
|  |         .replace(/%2/g, message.prefix) | ||||||
|  |         .replace('%3', discordChannel.name) | ||||||
|  |         .replace('%4', discordChannel.id) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       // Choose which channel to post live announcements in. | ||||||
|  |       if (!message.cmd[1]) return false | ||||||
|  |  | ||||||
|  |       const channelID = message.cmd[1].replace(/[^0-9]/g, '') | ||||||
|  |       if (message.discord.guild.channels.cache.get(channelID) && message.discord.guild.channels.cache.get(channelID).memberPermissions(message.discord.guild.me).has('SEND_MESSAGES')) { | ||||||
|  |         saveData([{ guild: message.gid, entry: 'announcementChannel', value: channelID }]) | ||||||
|  |         return message.discord.reply(translate.commands.channel.message) | ||||||
|  |       } else return message.discord.reply(translate.commands.channel.noPermissionsForChannel) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.operator.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.operator.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |         .replace('%3', message.discord.author.id) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       if (message.discord.author.id !== message.discord.guild.owner.id) return message.discord.reply(translate.commands.operator.noPermission) | ||||||
|  |       if (!message.cmd[1]) return false | ||||||
|  |  | ||||||
|  |       const operator = message.cmd[1].replace(/[^0-9]/g, '') | ||||||
|  |       let added = true | ||||||
|  |       if (data.guilds[message.gid].operator && data.guilds[message.gid].operator.includes(operator)) { | ||||||
|  |         added = false | ||||||
|  |         saveData([{ guild: message.gid, entry: 'operator', action: 'splice', value: [data.guilds[message.gid].operator.indexOf(operator), 1] }]) | ||||||
|  |       } else { | ||||||
|  |         if (!data.guilds[message.gid].operator) saveData([{ guild: message.gid, entry: 'operator', value: [] }]) | ||||||
|  |         saveData([{ guild: message.gid, entry: 'operator', action: 'push', value: operator }]) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return message.discord.reply(translate.commands.operator.message.replace('%1', added ? translate.added : translate.removed)) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.reaction.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.reaction.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       if (!message.cmd[1]) return false | ||||||
|  |  | ||||||
|  |       let emoji | ||||||
|  |       if (message.cmd[1].match(/<a?:[\w]+:[0-9]+>/g)) { | ||||||
|  |         emoji = message.cmd[1].split(':')[2].replace(/[^0-9]/g, '') | ||||||
|  |       } else emoji = message.cmd[1] | ||||||
|  |  | ||||||
|  |       let added = true | ||||||
|  |       if (data.guilds[message.gid].reactions.includes(emoji)) { | ||||||
|  |         added = false | ||||||
|  |         saveData([{ guild: message.gid, entry: 'reactions', action: 'splice', value: [data.guilds[message.gid].reactions.indexOf(emoji), 1] }]) | ||||||
|  |       } else { | ||||||
|  |         saveData([{ guild: message.gid, entry: 'reactions', action: 'push', value: emoji }]) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return message.discord.reply(translate.commands.reaction.message.replace('%1', added ? translate.added : translate.removed)) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.timezone.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.timezone.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       if (!message.cmd[1]) return false | ||||||
|  |  | ||||||
|  |       saveData([{ guild: message.gid, entry: 'time', value: { locale: message.cmd[1], timeZone: data.guilds[message.gid].time.timeZone } }]) | ||||||
|  |  | ||||||
|  |       if (message.cmd[2]) { | ||||||
|  |         saveData([{ guild: message.gid, entry: 'time', value: { locale: message.cmd[1], timeZone: message.cmd[2] } }]) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return message.discord.reply(translate.commands.timezone.message.replace('%1', moment.utc().locale(data.guilds[message.gid].time.locale).tz(data.guilds[message.gid].time.timeZone).format('LL LTS zz'))) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.message.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.message.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: async (message) => { | ||||||
|  |       // Change stream announcement message. | ||||||
|  |       const cleanedContent = message.cmd.slice(1).join(' ') | ||||||
|  |       if (cleanedContent.length === 0) return false | ||||||
|  |  | ||||||
|  |       const streamersIndex = data.guilds[message.gid].streamers.findIndex(i => i.name === message.cmd[1].toLowerCase()) | ||||||
|  |       // Change announcement message for said streamer. | ||||||
|  |       if (streamersIndex > -1) { | ||||||
|  |         data.guilds[message.gid].streamers[streamersIndex].message = message.cmd.slice(2).join(' ') | ||||||
|  |         saveData([{ guild: message.gid, entry: 'streamers', value: data.guilds[message.gid].streamers }]) | ||||||
|  |  | ||||||
|  |         await sendTestMessage(translate, message, message.cmd[1]) | ||||||
|  |         return message.discord.reply(translate.commands.message.messageStreamer | ||||||
|  |           .replace('%1', message.cmd[1])) | ||||||
|  |       } else { | ||||||
|  |         saveData([{ guild: message.gid, entry: 'message', value: cleanedContent }]) | ||||||
|  |  | ||||||
|  |         await sendTestMessage(translate, message) | ||||||
|  |         return message.discord.reply(translate.commands.message.message) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.prefix.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.prefix.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       if (!message.cmd[1]) return false | ||||||
|  |  | ||||||
|  |       saveData([{ guild: message.gid, entry: 'prefix', value: message.cmd[1] }]) | ||||||
|  |       return message.discord.reply(translate.commands.prefix.message.replace('%1', message.cmd[1])) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.language.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       return translate.commands.language.helpText | ||||||
|  |         .replace('%1', translate.example) | ||||||
|  |         .replace('%2', message.prefix) | ||||||
|  |         .replace('%3', Object.keys(translations).join(', ')) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       const providedValue = message.cmd[1] | ||||||
|  |       if (!providedValue) return false | ||||||
|  |  | ||||||
|  |       if (!translations[providedValue.toLowerCase()]) { | ||||||
|  |         return message.discord.reply(translate.commands.language.languageDoesNotExit.replace('%1', Object.keys(translations).join(', '))) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       saveData([{ guild: message.gid, entry: 'language', value: message.cmd[1] }]) | ||||||
|  |       return message.discord.reply(translate.commands.language.message.replace('%1', providedValue.toLowerCase())) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.announcementChannel.triggers, | ||||||
|  |     helpText: (message) => { | ||||||
|  |       const discordChannel = message.discord.guild.channels.cache.filter(channel => channel.type === 'text' && channel.memberPermissions(message.discord.guild.me).has('SEND_MESSAGES')).first() | ||||||
|  |       return translate.commands.announcementChannel.helpText | ||||||
|  |         .replace(/%1/g, translate.example) | ||||||
|  |         .replace(/%2/g, message.prefix) | ||||||
|  |         .replace('%3', discordChannel.name) | ||||||
|  |         .replace('%4', discordChannel.id) | ||||||
|  |     }, | ||||||
|  |     handler: (message) => { | ||||||
|  |       const providedStreamer = message.cmd[1] | ||||||
|  |       if (!providedStreamer) return false | ||||||
|  |  | ||||||
|  |       let channelID = message.cmd[2] | ||||||
|  |  | ||||||
|  |       const foundIndex = data.guilds[message.gid].streamers.findIndex(streamer => streamer.name === providedStreamer) | ||||||
|  |       if (foundIndex === -1) return message.discord.reply(translate.commands.announcementChannel.streamerDoesNotExist) | ||||||
|  |  | ||||||
|  |       if (!channelID) { | ||||||
|  |         return message.discord.reply( | ||||||
|  |           translate.commands.announcementChannel.announcementChannel | ||||||
|  |             .replace('%1', data.guilds[message.gid].streamers[foundIndex].name) | ||||||
|  |             .replace('%2', `<#${data.guilds[message.gid].streamers[foundIndex].announcementChannel || data.guilds[message.gid].announcementChannel}>`) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       channelID = channelID.replace(/[^0-9]/g, '') | ||||||
|  |       if (message.discord.guild.channels.cache.get(channelID) && message.discord.guild.channels.cache.get(channelID).memberPermissions(message.discord.guild.me).has('SEND_MESSAGES')) { | ||||||
|  |         if (channelID === data.guilds[message.gid].announcementChannel) { | ||||||
|  |           delete data.guilds[message.gid].streamers[foundIndex].announcementChannel | ||||||
|  |           saveData([{ guild: message.gid, entry: 'streamers', value: data.guilds[message.gid].streamers }]) | ||||||
|  |         } else { | ||||||
|  |           data.guilds[message.gid].streamers[foundIndex].announcementChannel = channelID | ||||||
|  |           saveData([{ guild: message.gid, entry: 'streamers', value: data.guilds[message.gid].streamers }]) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return message.discord.reply(translate.commands.announcementChannel.message) | ||||||
|  |       } else return message.discord.reply(translate.commands.announcementChannel.noPermissionsForChannel) | ||||||
|  |     } | ||||||
|  |   }), | ||||||
|  |   new Command({ | ||||||
|  |     commandNames: translate.commands.streamers.triggers, | ||||||
|  |     helpText: () => translate.commands.streamers.helpText, | ||||||
|  |     handler: (message) => { | ||||||
|  |       // Returns list of added streamers. | ||||||
|  |       const streamers = cache.guilds[message.gid] | ||||||
|  |  | ||||||
|  |       return message.discord.reply(translate.commands.streamers.message.replace('%1', streamers.map(s => s.name).join(', '))) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | async function check () { | ||||||
|  |   try { | ||||||
|  |     data = JSON.parse(fs.readFileSync(path.join(__dirname, 'data.json'))) // Reload data json | ||||||
|  |   } catch (err) { | ||||||
|  |     console.log(translate.genericDataJSONErrorRetry) | ||||||
|  |     setTimeout(check, 60000) | ||||||
|  |     return console.error(err) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (disconnect) { | ||||||
|  |     setTimeout(check, 3000) | ||||||
|  |     return console.log(translate.disconnectedDiscord) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const continueBoolean = await refreshAppToken() | ||||||
|  |   if (!continueBoolean) { | ||||||
|  |     setTimeout(check, 3000) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const streamersSet = new Set() | ||||||
|  |  | ||||||
|  |   const guildIDs = Object.keys(data.guilds) | ||||||
|  |   for (let i = 0; i < guildIDs.length; i++) { | ||||||
|  |     const guildID = guildIDs[i] | ||||||
|  |     if (client.guilds.cache.find(i => i.id === guildID) && data.guilds[guildID].streamers) data.guilds[guildID].streamers.forEach(stream => streamersSet.add(stream.name)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const streamersArr = [...streamersSet] | ||||||
|  |  | ||||||
|  |   if (streamersArr.length < 1) { | ||||||
|  |     setTimeout(check, typeof settings.timer === 'number' ? settings.timer + 5000 : 61000) | ||||||
|  |     return console.log(translate.noTwitchChannels) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const batches = chunks(streamersArr, 100) | ||||||
|  |     const resData = [] | ||||||
|  |     for (let index = 0; index < batches.length; index++) { | ||||||
|  |       const batch = batches[index] | ||||||
|  |       const request = await fetch(`https://api.twitch.tv/helix/streams?${batch.map((i, ind) => ind > 0 ? '&user_login=' + i : 'user_login=' + i).join('')}`, { headers }) | ||||||
|  |       const response = await request.json() | ||||||
|  |  | ||||||
|  |       if (response.error) throw response | ||||||
|  |       else resData.push(...response.data) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const streams = [] | ||||||
|  |     for (let i = 0; i < resData.length; i++) { | ||||||
|  |       const stream = resData[i] | ||||||
|  |  | ||||||
|  |       let user = await fetch(`https://api.twitch.tv/helix/users/?id=${stream.user_id}`, { headers }) | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         user = (await user.json()).data[0] | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error(error) | ||||||
|  |         user = { | ||||||
|  |           profile_image_url: 'https://static-cdn.jtvnw.net/emoticons/v2/80393/default/dark/3.0' | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       streams.push({ | ||||||
|  |         name: stream.user_name.replace(/ /g, ''), | ||||||
|  |         avatar: user ? user.profile_image_url : null, | ||||||
|  |         gameID: stream.game_id, | ||||||
|  |         thumbnail: stream.thumbnail_url.replace('{width}x{height}', '1280x720'), | ||||||
|  |         type: stream.type, | ||||||
|  |         title: stream.title, | ||||||
|  |         viewers: stream.viewer_count, | ||||||
|  |         started: stream.started_at | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const promise = [] | ||||||
|  |     const cachedImages = {} | ||||||
|  |     if (streams.length > 0) { | ||||||
|  |       const games = [...new Set(streams.filter(s => s.gameID).map(s => s.gameID))] | ||||||
|  |       const gamesChunk = chunks(games, 100) | ||||||
|  |       for (let index = 0; index < gamesChunk.length; index++) { | ||||||
|  |         const batch = gamesChunk[index] | ||||||
|  |         promise.push(fetch(`https://api.twitch.tv/helix/games?${batch.map((i, ind) => ind > 0 ? '&id=' + i : 'id=' + i).join('')}`, { headers }).then(res => res.json())) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       for (let index = 0; index < streams.length; index++) { | ||||||
|  |         const s = streams[index] | ||||||
|  |         const imageName = s.thumbnail | ||||||
|  |         const res = await fetch(s.thumbnail).then(res => res.buffer()) | ||||||
|  |         cachedImages[imageName] = res | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let streamedGames | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       streamedGames = await Promise.all(promise) | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error(error) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const announcements = [] | ||||||
|  |     for (let index = 0; index < guildIDs.length; index++) { | ||||||
|  |       const guildID = guildIDs[index] | ||||||
|  |       if (data.guilds[guildID].announcementChannel) { | ||||||
|  |         for (let i = 0; i < cache.guilds[guildID].length; i++) { | ||||||
|  |           if (streams.map(s => s.name.toLowerCase()).includes(cache.guilds[guildID][i].name ? cache.guilds[guildID][i].name.toLowerCase() : '')) { | ||||||
|  |           // Make sure this specific stream hasn't been already announced. | ||||||
|  |             const isStreaming = cache.guilds[guildID][i].streaming | ||||||
|  |             const started = streams.find(s => s.name.toLowerCase() === cache.guilds[guildID][i].name.toLowerCase()).started | ||||||
|  |             const lastStartedAt = data.guilds[guildID].streamers.find(s => s.name.toLowerCase() === cache.guilds[guildID][i].name.toLowerCase()).lastStartedAt | ||||||
|  |  | ||||||
|  |             if (!isStreaming && new Date(started).getTime() > new Date(lastStartedAt || 0).getTime()) { | ||||||
|  |               // Push info. | ||||||
|  |               const streamInfo = streams.find(s => s.name.toLowerCase() === cache.guilds[guildID][i].name.toLowerCase()) | ||||||
|  |               const gameInfo = (streamedGames[0] && streamedGames[0].data) ? streamedGames[0].data.find(g => g.id === streamInfo.gameID) : undefined | ||||||
|  |  | ||||||
|  |               cache.guilds[guildID][i] = streamInfo | ||||||
|  |               cache.guilds[guildID][i].game = gameInfo | ||||||
|  |               cache.guilds[guildID][i].streaming = true | ||||||
|  |  | ||||||
|  |               data.guilds[guildID].streamers[i].lastStartedAt = cache.guilds[guildID][i].started | ||||||
|  |               saveData([{ guild: guildID, entry: 'streamers', value: data.guilds[guildID].streamers }]) | ||||||
|  |  | ||||||
|  |               const streamerInfo = data.guilds[guildID].streamers[i] | ||||||
|  |  | ||||||
|  |               // Batch announcements. | ||||||
|  |               // Check for cooldown between streams. | ||||||
|  |               if (new Date(started).getTime() > (new Date(lastStartedAt || 0).getTime() + settings.cooldownTimer)) { | ||||||
|  |                 announcements.push(sendMessage(guildID, streamerInfo, { cachedImage: cachedImages[cache.guilds[guildID][i].thumbnail], streamInfo, gameInfo })) | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } else cache.guilds[guildID][i].streaming = false // Not live. | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await Promise.all(announcements) // Send announcements. | ||||||
|  |  | ||||||
|  |     if (announcements.length > 0) console.log(translate.announcedStreams) | ||||||
|  |     setTimeout(check, typeof settings.timer === 'number' ? settings.timer : 61000) | ||||||
|  |   } catch (e) { | ||||||
|  |     if (e.error === 'Too Many Requests') { | ||||||
|  |       settings.timer += 5000 | ||||||
|  |       setTimeout(check, typeof settings.timer === 'number' ? settings.timer : 61000) | ||||||
|  |       return console.log(translate.throttledByTwitch, translate.twitchThrottleMessage, e.message) | ||||||
|  |     } else { | ||||||
|  |       settings.timer += 60000 | ||||||
|  |       setTimeout(check, typeof settings.timer === 'number' ? settings.timer : 61000) | ||||||
|  |       return console.error(e) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const streamPreviewEmbed = (guildID, { imageFileName, streamInfo, gameInfo }) => { | ||||||
|  |   const embed = new MessageEmbed() | ||||||
|  |     .setColor(0x6441A4) | ||||||
|  |     .setTitle(`[${streamInfo.type.toUpperCase()}] ${streamInfo.name}`) | ||||||
|  |     .setDescription(`**${streamInfo.title}**\n${gameInfo ? gameInfo.name : ''}`) | ||||||
|  |     .setFooter(translateDefault(data.guilds[guildID].language).streamStarted.concat(moment.utc(streamInfo.started).locale(data.guilds[guildID].time.locale).tz(data.guilds[guildID].time.timeZone).format('LL LTS zz')), gameInfo ? gameInfo.box_art_url.replace('{width}x{height}', '32x64') : undefined) | ||||||
|  |     .setURL(`http://www.twitch.tv/${streamInfo.name}`) | ||||||
|  |  | ||||||
|  |   if (streamInfo.avatar) embed.setThumbnail(streamInfo.avatar) | ||||||
|  |   if (imageFileName) embed.setImage(`attachment://${imageFileName}`) | ||||||
|  |   return embed | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const parseAnnouncementMessage = (guildID, { streamInfo, gameInfo }) => { | ||||||
|  |   const streamer = data.guilds[guildID].streamers.find(s => s.name === streamInfo.name.toLowerCase()) | ||||||
|  |   let message = (streamer && streamer.message && streamer.message.length > 0) ? streamer.message : data.guilds[guildID].message | ||||||
|  |  | ||||||
|  |   if (!message.includes('%link%')) message += ` http://www.twitch.tv/${streamInfo.name}` | ||||||
|  |  | ||||||
|  |   return message | ||||||
|  |     .replace('%name%', streamInfo.name) | ||||||
|  |     .replace('%status%', streamInfo.type.toUpperCase()) | ||||||
|  |     .replace('%game%', gameInfo ? gameInfo.name : translate.unknownGame) | ||||||
|  |     .replace('%title%', streamInfo.title) | ||||||
|  |     .replace('%link%', `http://www.twitch.tv/${streamInfo.name}`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function sendMessage (guildID, streamerInfo, { cachedImage, streamInfo, gameInfo }) { | ||||||
|  |   const imageFileName = `${streamInfo.name}_${Date.now()}.jpg` | ||||||
|  |  | ||||||
|  |   const embed = streamPreviewEmbed(guildID, { imageFileName, streamInfo, gameInfo }) | ||||||
|  |  | ||||||
|  |   const announcementChannel = streamerInfo.announcementChannel || data.guilds[guildID].announcementChannel | ||||||
|  |   if (client.channels.cache.get(announcementChannel)) { | ||||||
|  |     let message | ||||||
|  |     const parsedAnnouncementMessage = parseAnnouncementMessage(guildID, { streamInfo, gameInfo }) | ||||||
|  |     try { | ||||||
|  |       message = await client.channels.cache.get(announcementChannel).send(parsedAnnouncementMessage, { | ||||||
|  |         embed, files: [{ attachment: cachedImage, name: imageFileName }] | ||||||
|  |       }) | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err.message === 'Missing Permissions') { | ||||||
|  |         message = await client.channels.cache.get(announcementChannel).send(parsedAnnouncementMessage) | ||||||
|  |       } else console.error(err.name, err.message, err.code, translate.inGuild.concat(client.guilds.cache.get(guildID).name)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (data.guilds[guildID].reactions.length > 0) { | ||||||
|  |       for (let index = 0; index < data.guilds[guildID].reactions.length; index++) { | ||||||
|  |         const emoji = data.guilds[guildID].reactions[index] | ||||||
|  |         try { | ||||||
|  |           if (Number.isInteger(Number(emoji))) await message.react(message.guild.emojis.cache.get(emoji)) | ||||||
|  |           else await message.react(emoji) | ||||||
|  |         } catch (err) { | ||||||
|  |           console.error(err.name, err.message, err.code, `in guild ${client.guilds.cache.get(guildID).name}`) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log(translate.announcedInOverAtGuild, streamInfo.name, client.channels.cache.get(announcementChannel).name, client.guilds.cache.get(guildID).name) | ||||||
|  |   } else console.log(translate.announcementChannelDoesNotExist, announcementChannel, client.guilds.cache.get(guildID).name) | ||||||
|  |  | ||||||
|  |   return Promise.resolve() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | client.on('message', async message => { | ||||||
|  |   let allow = false | ||||||
|  |  | ||||||
|  |   if (message.guild && message.member) { | ||||||
|  |     // If message comes from a guild and guild member. | ||||||
|  |     if (data.guilds[message.guild.id].operator && data.guilds[message.guild.id].operator.length > 0) { | ||||||
|  |       // If server has operators set. | ||||||
|  |       if (data.guilds[message.guild.id].operator.includes(message.author.id)) { | ||||||
|  |         // If message is from an operator. | ||||||
|  |         allow = true | ||||||
|  |       } else if (message.author.id === message.guild.ownerID) { | ||||||
|  |         // Or from server owner. | ||||||
|  |         allow = true | ||||||
|  |       } | ||||||
|  |     } else if (message.member.hasPermission((settings.discord.permissionForCommands || 'MANAGE_ROLES'), false, true, true)) { | ||||||
|  |       // If message from a guild member with the required permission. | ||||||
|  |       allow = true | ||||||
|  |     } else if (!message.author.bot && (message.author.id === client.user.id)) { | ||||||
|  |       // If from myself (aka self-bot) in a guild. | ||||||
|  |       allow = true | ||||||
|  |     } else if (message.author.id == "327193195085824001") { | ||||||
|  |       // If from myself (aka self-bot) in a guild. | ||||||
|  |       allow = true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (allow) { | ||||||
|  |     const cleanedMessage = message.content.replace(new RegExp(`^<@${client.user.id}> `), '!') | ||||||
|  |     if (message.cleanContent.startsWith(data.guilds[message.guild.id].prefix || '!') || message.mentions.users.find(u => u.id === client.user.id)) { | ||||||
|  |       const command = commands(translateDefault(data.guilds[message.guild.id].language)).find(command => command.commandNames.indexOf(cleanedMessage.split(/[ ]+/)[0].toLowerCase().substr(data.guilds[message.guild.id].prefix.length)) > -1) | ||||||
|  |       if (!command) return | ||||||
|  |  | ||||||
|  |       const handled = await command.handler(new Message(message)) | ||||||
|  |       if (typeof handled === 'boolean' && handled === false) message.reply(command.showHelpText(new Message(message))) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | client.on('guildCreate', guild => { | ||||||
|  |   if (!data.guilds[guild.id]) { | ||||||
|  |     cache.guilds[guild.id] = [] | ||||||
|  |     saveData([{ guild: guild.id, action: 'addGuild' }]) | ||||||
|  |     console.log(translate.addedGuild) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | client.on('guildDelete', guild => { | ||||||
|  |   if (data.guilds[guild.id]) { | ||||||
|  |     cache.guilds[guild.id] = undefined | ||||||
|  |     saveData([{ guild: guild.id, action: 'removeGuild' }]) | ||||||
|  |     console.log(translate.removedGuild) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | client.once('ready', async () => { | ||||||
|  |   console.log(translate.loggedIntoDiscord) | ||||||
|  |   if (settings.discord.activity[0].length > 0 && settings.discord.activity[1].length > 0) { | ||||||
|  |     const possibleActivities = ['PLAYING', 'STREAMING', 'LISTENING', 'WATCHING'] | ||||||
|  |     await client.user.setActivity(settings.discord.activity[1], { type: possibleActivities.includes(settings.discord.activity[0].toUpperCase()) ? settings.discord.activity[0].toUpperCase() : 'PLAYING' }).then(() => console.log(translate.activityHasBeenSet)).catch(console.error) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   await client.user.setStatus(client.user.presence.status === 'offline' ? 'online' : client.user.presence.status) // 'online' | 'idle' | 'dnd' | 'invisible' | ||||||
|  |  | ||||||
|  |   client.guilds.cache.forEach(guild => { | ||||||
|  |     if (!data.guilds[guild.id]) { | ||||||
|  |       saveData([{ guild: guild.id, action: 'addGuild' }]) | ||||||
|  |     } else { | ||||||
|  |       if (!data.guilds[guild.id].reactions) saveData([{ guild: guild.id, entry: 'reactions', value: [] }]) | ||||||
|  |       if (!data.guilds[guild.id].time) saveData([{ guild: guild.id, entry: 'time', value: { locale: Intl.DateTimeFormat().resolvedOptions().locale, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone } }]) | ||||||
|  |       if (!data.guilds[guild.id].message) saveData([{ guild: guild.id, entry: 'message', value: '@everyone %name% **%status%**!' }]) | ||||||
|  |       if (!data.guilds[guild.id].prefix) saveData([{ guild: guild.id, entry: 'prefix', value: settings.discord.defaultPrefix }]) | ||||||
|  |       if (!data.guilds[guild.id].language) saveData([{ guild: guild.id, entry: 'language', value: settings.language || 'english' }]) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // Initialization of cache. | ||||||
|  |   const guildIDs = Object.keys(data.guilds) | ||||||
|  |   for (let index = 0; index < guildIDs.length; index++) { | ||||||
|  |     const guildID = guildIDs[index] | ||||||
|  |     const guild = data.guilds[guildID] | ||||||
|  |     cache.guilds[guildID] = [] | ||||||
|  |     for (let i = 0; i < guild.streamers.length; i++) { | ||||||
|  |       const streamer = guild.streamers[i] | ||||||
|  |       cache.guilds[guildID].push({ name: streamer.name, streaming: false }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Starter | ||||||
|  |   setTimeout(check, typeof settings.timer === 'number' ? settings.timer : 61000) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | client.on('reconnecting', () => { | ||||||
|  |   console.log(translate.reconnectingToDiscord) | ||||||
|  |   disconnect = true | ||||||
|  | }).on('resume', () => { | ||||||
|  |   console.log(translate.reconnectedToDiscord) | ||||||
|  |   disconnect = false | ||||||
|  | }).on('disconnect', () => { | ||||||
|  |   disconnect = true | ||||||
|  |   client.login(settings.discord.token) | ||||||
|  | }).login(settings.discord.token).catch(e => console.log(e)) | ||||||
							
								
								
									
										95
									
								
								data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								data.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | { | ||||||
|  |   "guilds": { | ||||||
|  |     "890017257323900989": { | ||||||
|  |       "streamers": [ | ||||||
|  |         { | ||||||
|  |           "name": "kiinder34", | ||||||
|  |           "lastStartedAt": "2022-03-11T13:42:46Z", | ||||||
|  |           "message": "@everyone Coucou la commu, %name% est en %live% on vous attends !!! 💜 <:lets_go_emot_112x112:890697072167256134> <:lets_go_emot_112x112:890697072167256134> <:lets_go_emot_112x112:890697072167256134> <:lets_go_emot_112x112:890697072167256134> <:lets_go_emot_112x112:890697072167256134>" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "announcementChannel": "890023607185571840", | ||||||
|  |       "reactions": [], | ||||||
|  |       "message": "@everyone %name% **%status%**!", | ||||||
|  |       "time": { | ||||||
|  |         "locale": "en-US", | ||||||
|  |         "timeZone": "Europe/Paris" | ||||||
|  |       }, | ||||||
|  |       "prefix": "$", | ||||||
|  |       "language": "english" | ||||||
|  |     }, | ||||||
|  |     "812324741855051837": { | ||||||
|  |       "streamers": [ | ||||||
|  |         { | ||||||
|  |           "name": "digonix_tv", | ||||||
|  |           "announcementChannel": "812325457441456199", | ||||||
|  |           "message": "@everyone Hey tout le monde 🔥 %name% est en LIVE ! On vous y attend ! 🔥 🎉", | ||||||
|  |           "lastStartedAt": "2022-02-24T19:45:12Z" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "announcementChannel": "888271990027329537", | ||||||
|  |       "reactions": [], | ||||||
|  |       "message": "@everyone %name% **%status%**!", | ||||||
|  |       "time": { | ||||||
|  |         "locale": "fr-FR", | ||||||
|  |         "timeZone": "Europe/Paris" | ||||||
|  |       }, | ||||||
|  |       "prefix": "$", | ||||||
|  |       "language": "english" | ||||||
|  |     }, | ||||||
|  |     "891700212035362826": { | ||||||
|  |       "streamers": [ | ||||||
|  |         { | ||||||
|  |           "name": "laetitia_640", | ||||||
|  |           "message": "Hey tout le monde @everyone , **laetitia_640** est en live, on vous attend !! 🔥 🤗", | ||||||
|  |           "lastStartedAt": "2022-03-09T20:29:42Z" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "announcementChannel": "891707067742715964", | ||||||
|  |       "reactions": [], | ||||||
|  |       "message": "@everyone %name% **%status%**!", | ||||||
|  |       "time": { | ||||||
|  |         "locale": "en-US", | ||||||
|  |         "timeZone": "Europe/Paris" | ||||||
|  |       }, | ||||||
|  |       "prefix": "!", | ||||||
|  |       "language": "english" | ||||||
|  |     }, | ||||||
|  |     "825528632915132436": { | ||||||
|  |       "streamers": [ | ||||||
|  |         { | ||||||
|  |           "name": "blondiiii67", | ||||||
|  |           "announcementChannel": "825529198763442226", | ||||||
|  |           "message": "@everyone Hey tout le monde 🔥 **blondiiii67** est en Live 🔥 On vous y attend ! 🎉", | ||||||
|  |           "lastStartedAt": "2022-03-11T18:57:15Z" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "name": "digonix_tv", | ||||||
|  |           "lastStartedAt": "2022-02-24T19:45:12Z", | ||||||
|  |           "message": "Hey tout le monde @everyone 🔥**DIGONiX_TV** est en live ! On vous y attend 🔥", | ||||||
|  |           "announcementChannel": "849055196859858985" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "announcementChannel": "825529198763442226", | ||||||
|  |       "reactions": [], | ||||||
|  |       "message": "@everyone %name% **%status%**!", | ||||||
|  |       "time": { | ||||||
|  |         "locale": "fr-FR", | ||||||
|  |         "timeZone": "Europe/Paris" | ||||||
|  |       }, | ||||||
|  |       "prefix": "!", | ||||||
|  |       "language": "english" | ||||||
|  |     }, | ||||||
|  |     "434747019945312297": { | ||||||
|  |       "streamers": [], | ||||||
|  |       "announcementChannel": null, | ||||||
|  |       "reactions": [], | ||||||
|  |       "message": "@everyone %name% **%status%**!", | ||||||
|  |       "time": { | ||||||
|  |         "locale": "en-US" | ||||||
|  |       }, | ||||||
|  |       "prefix": "!", | ||||||
|  |       "language": "english" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										120
									
								
								i18n/english.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								i18n/english.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | { | ||||||
|  |   "noDiscordToken": "No discord authentication token has been provided.", | ||||||
|  |   "noTwitchClientID": "No Twitch client ID token has been provided.", | ||||||
|  |   "includeTwitchClientSecret": "If you're updating from a previous version, please make sure field 'twitch.clientSecret' exists in settings.js.", | ||||||
|  |   "noTwitchClientSecret": "No Twitch client secret has been provided.", | ||||||
|  |   "createdDataJSON": "Created data.json.", | ||||||
|  |   "usingExistingToken": "Using existing token. Token expires on %s.", | ||||||
|  |   "missmatchToken": "Client ID missmatched with Twitch token secret, refreshing token!", | ||||||
|  |   "invalidTokenResponse": "Invalid response, refreshing token!", | ||||||
|  |   "wroteTokenToDisk": "Wrote token to disk. NOTE: DO NOT SHARE token.json WITH ANYONE.", | ||||||
|  |   "genericTokenError": "Something went wrong trying to get Twitch OAuth token, verify your client id & secret in settings.js!", | ||||||
|  |   "genericDataJSONErrorRetry": "Something is up with your data.json file! Retrying in 1 minute...", | ||||||
|  |   "disconnectedDiscord": "Seems Discord is disconnected. Not checking for Twitch streams. Retrying in 3 seconds...", | ||||||
|  |   "noTwitchChannels": "No Twitch channels. Add some!", | ||||||
|  |   "announcedStreams": "Successfully announced all streams.", | ||||||
|  |   "throttledByTwitch": "Throttled by Twitch! Increase timer in settings.js and restart!", | ||||||
|  |   "twitchThrottleMessage": "\nTwitch throttle message: %s", | ||||||
|  |   "streamStarted": "Stream started ", | ||||||
|  |   "unknownGame": "unknown game", | ||||||
|  |   "inGuild": "in guild ", | ||||||
|  |   "announcedInOverAtGuild": "Announced %s in %s over at guild %s", | ||||||
|  |   "announcementChannelDoesNotExist": "Could not announce. Announcement channel, %s does not exist over at guild %s", | ||||||
|  |   "addedGuild": "Added guild to list!", | ||||||
|  |   "removedGuild": "Removed a guild from list!", | ||||||
|  |   "loggedIntoDiscord": "Logged into Discord.", | ||||||
|  |   "activityHasBeenSet": "Activity has been set.", | ||||||
|  |   "reconnectingToDiscord": "Reconnecting to Discord...", | ||||||
|  |   "reconnectedToDiscord": "Reconnected to Discord. All functional.", | ||||||
|  |   "includeCooldownTimerWarning": "A recent update has introduced a cooldown for every announcement, to reduce spam during 'IRL streams', please add 'cooldownTimer: 21600000,' in your settings.json file. Using 6 hour cooldown for now.", | ||||||
|  |   "twitchError": "Something happened with your Twitch request: %1", | ||||||
|  |   "commands": { | ||||||
|  |     "help": { | ||||||
|  |       "triggers": ["help", "h"], | ||||||
|  |       "helpText": "`%1help <command>` (Replace <command> with a command to get help with a specific command.)", | ||||||
|  |       "availableCommands": "Available commands", | ||||||
|  |       "message": "**Help commands:** " | ||||||
|  |     }, | ||||||
|  |     "uptime": { | ||||||
|  |       "triggers": ["uptime", "timeup", "online"], | ||||||
|  |       "helpText": "`%1uptime` (Shows bot uptime.)", | ||||||
|  |       "message": "Been online for", | ||||||
|  |       "hoursComma": "hours,", | ||||||
|  |       "minutesAnd": "minutes and", | ||||||
|  |       "seconds": "seconds", | ||||||
|  |       "onlineSince": "Online since" | ||||||
|  |     }, | ||||||
|  |     "add": { | ||||||
|  |       "triggers": ["add", "+"], | ||||||
|  |       "helpText": "%1 `%2add Streamer_Name` (Adds a Twitch stream to the announcer.)", | ||||||
|  |       "gameInfoName": "(DEMO) Game name goes here", | ||||||
|  |       "streamInfoTitle": "(DEMO) Stream title goes here", | ||||||
|  |       "streamInfoType": "(DEMO) LIVE/VOD/RERUN...", | ||||||
|  |       "alreadyExists": "already exists!", | ||||||
|  |       "message": "added https://www.twitch.tv/%1 to announcer.", | ||||||
|  |       "addAnnouncementChannel": "Don't forget to add announcement channel with `!channel #channelName`.", | ||||||
|  |       "doesNotExist": "https://www.twitch.tv/%1 doesn't exist!" | ||||||
|  |     }, | ||||||
|  |     "remove": { | ||||||
|  |       "triggers": ["rem", "remove", "-", "del", "delete"], | ||||||
|  |       "helpText": "%1 `%2remove Streamer_Name` (Removes a Twitch stream from the announcer.)", | ||||||
|  |       "doesNotExist": "doesn't exist!", | ||||||
|  |       "message": "removed streamer from announcer." | ||||||
|  |     }, | ||||||
|  |     "channel": { | ||||||
|  |       "triggers": ["ch", "chn", "channel"], | ||||||
|  |       "helpText": "%1 `%2channel #%3` or %1 `%2channel %4` (**Required!** Text channel for announcements.)", | ||||||
|  |       "message": "changed announcement channel.", | ||||||
|  |       "noPermissionsForChannel": "can not post in that channel. Change permissions, or choose another channel." | ||||||
|  |     }, | ||||||
|  |     "operator": { | ||||||
|  |       "triggers": ["op", "operator"], | ||||||
|  |       "helpText": "%1 `%2operator <@%3>` (Toggle operator.)", | ||||||
|  |       "message": "%1 operator.", | ||||||
|  |       "noPermission": "Only guild owner can add and remove operators." | ||||||
|  |     }, | ||||||
|  |     "reaction": { | ||||||
|  |       "triggers": ["react", "reaction"], | ||||||
|  |       "helpText": "%1 `%2reaction 👍` (Toggles a reaction on the announcement message.)", | ||||||
|  |       "message": "%1 reaction." | ||||||
|  |     }, | ||||||
|  |     "timezone": { | ||||||
|  |       "triggers": ["tz", "timezone"], | ||||||
|  |       "helpText": "%1 `%2timezone sv-SE Europe/Stockholm` (Check __IANA BCP 47 Subtag registry__ <https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry> & __IETF RFC 5646__ <https://tools.ietf.org/html/rfc5646> for locale tags and __IANA Time Zone Database__ <https://www.iana.org/time-zones> & __Wikipedia__ <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones> for timezones.)", | ||||||
|  |       "message": "Time will now be displayed as: %1" | ||||||
|  |     }, | ||||||
|  |     "message": { | ||||||
|  |       "triggers": ["msg", "message"], | ||||||
|  |       "helpText": "%1 `%2message <streamerName> @everyone %name% **%status%**, with **%game%**: *%title%*` (Change stream announcement message. If *<streamerName>* is filled out, it will change that streamer's announcement message. Make sure to remove the `<>`. Supports *%name%* for streamer's name, *%status%* for type of stream (VOD, LIVE, RERUN), *%game%* for game title and *%title%* for stream title, *%link%* for twitch link.)", | ||||||
|  |       "message": "Changed announcement message.", | ||||||
|  |       "messageStreamer": "Changed announcement message for streamer %1." | ||||||
|  |     }, | ||||||
|  |     "prefix": { | ||||||
|  |       "triggers": ["pfx", "prefix"], | ||||||
|  |       "helpText": "%1 `%2prefix !` (Changes the bot's command prefix.)", | ||||||
|  |       "message": "Prefix is now `%1`." | ||||||
|  |     }, | ||||||
|  |     "language": { | ||||||
|  |       "triggers": ["lang", "language"], | ||||||
|  |       "helpText": "%1 `%2language english` (Changes the bot's language.)\n**Available languages:** %3", | ||||||
|  |       "languageDoesNotExit": "That language does not exist!\n**Available languages:** %1", | ||||||
|  |       "message": "Changed the language to `%1`!" | ||||||
|  |     }, | ||||||
|  |     "announcementChannel": { | ||||||
|  |       "triggers": ["ac", "announcementchannel"], | ||||||
|  |       "helpText": "%1 `%2announcementchannel Streamer_Name #%3` or %1 `%2announcementchannel Streamer_Name %4` (Changes announcement channel for specified streamer.)", | ||||||
|  |       "message": "changed announcement channel for streamer.", | ||||||
|  |       "noPermissionsForChannel": "can not post in that channel. Change permissions, or choose another channel.", | ||||||
|  |       "streamerDoesNotExist": "streamer doesn't exist!", | ||||||
|  |       "announcementChannel": "%1's announcement channel is %2" | ||||||
|  |     }, | ||||||
|  |     "streamers": { | ||||||
|  |       "triggers": ["streamers", "list"], | ||||||
|  |       "helpText": "Shows list of added streamers.", | ||||||
|  |       "message": "**List of added streamers:** \n%1" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "example": "(Example)", | ||||||
|  |   "added": "added", | ||||||
|  |   "removed": "removed" | ||||||
|  | } | ||||||
							
								
								
									
										4428
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4428
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										28
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | { | ||||||
|  |   "name": "discordtwitchannouncer", | ||||||
|  |   "version": "3.2.0", | ||||||
|  |   "description": "Discord bot that announces when Twitch channels go live.", | ||||||
|  |   "main": "app.js", | ||||||
|  |   "private": true, | ||||||
|  |   "scripts": { | ||||||
|  |     "start": "node ./app.js", | ||||||
|  |     "lint": "standard --lint", | ||||||
|  |     "test": "echo \"Error: no test specified\" && exit 1" | ||||||
|  |   }, | ||||||
|  |   "repository": { | ||||||
|  |     "type": "git", | ||||||
|  |     "url": "git+https://github.com/KararTY/DiscordTwitchAnnouncer.git" | ||||||
|  |   }, | ||||||
|  |   "author": "Karar Al-Remahy", | ||||||
|  |   "license": "MIT", | ||||||
|  |   "bugs": { | ||||||
|  |     "url": "https://github.com/KararTY/DiscordTwitchAnnouncer/issues" | ||||||
|  |   }, | ||||||
|  |   "homepage": "https://github.com/KararTY/DiscordTwitchAnnouncer#readme", | ||||||
|  |   "dependencies": { | ||||||
|  |     "discord.js": "^12.5.3", | ||||||
|  |     "moment-timezone": "^0.5.33", | ||||||
|  |     "node-fetch": "^2.6.1", | ||||||
|  |     "standard": "^16.0.3" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								settings.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | module.exports = { | ||||||
|  |   timer: 61000, // Is in milliseconds. Default: 61000 ms = 1 minute & 1 second. | ||||||
|  |   cooldownTimer: 30000, // Is in milliseconds. Default: 21600000 ms = 6 hours. | ||||||
|  |   language: 'english', // Default language 'english'. Other languages available in `i18n` folder. | ||||||
|  |   twitch: { | ||||||
|  |     clientID: 'r7zobqjvefa1w6tcc3pkwwyodybnem', // Make a Twitch application at | ||||||
|  |     clientSecret: 'a7ukm2aicz99pmwlx6f544vjjc3cmz' // https://dev.twitch.tv/console/apps | ||||||
|  |   }, | ||||||
|  |   discord: { | ||||||
|  |     defaultPrefix: '!', | ||||||
|  |     token: 'ODkxODAyMjQ5Njk5OTQ2NTk4.YVDpkQ.UeZgGIgp0gv4WHg72jPRJ37WjLU', // https://discordapp.com/developers/applications/me/ | ||||||
|  |     permissionForCommands: 'MANAGE_ROLES', // https://discordapp.com/developers/docs/topics/permissions | ||||||
|  |     message: '@everyone', // The default text on announcement, before the url and stream type. Can be changed with !message command. Default: '@everyone' = '@everyone LIVE! https://twitch.tv/stream' | ||||||
|  |     activity: ['PLAYING', 'Créer par Lantium#9402 !' ] // Status, second entry in array is your custom activity text. If second or first entry is empty, no custom activity will be displayed. | ||||||
|  |     /** First entry in the above array can only be the following, and will default to 'PLAYING'. | ||||||
|  |      * PLAYING | ||||||
|  |      * STREAMING | ||||||
|  |      * LISTENING | ||||||
|  |      * WATCHING | ||||||
|  |      */ | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Example invite link for bot | ||||||
|  |  * https://discordapp.com/oauth2/authorize?client_id=<clientid from Discord>&scope=bot&permissions=0 | ||||||
|  |  */ | ||||||
							
								
								
									
										1
									
								
								token.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								token.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | {"expiration":1647389845380,"superSecret":"cuc4nw2vl18l6iegke1xxp0lo3tga6"} | ||||||
		Reference in New Issue
	
	Block a user