Initial commit

This commit is contained in:
Lantium 2022-04-17 02:52:27 +02:00
commit f3823243e6
10 changed files with 5672 additions and 0 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v12.22.9

21
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View 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
View 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
View File

@ -0,0 +1 @@
{"expiration":1647389845380,"superSecret":"cuc4nw2vl18l6iegke1xxp0lo3tga6"}