Initial commit
This commit is contained in:
commit
f3823243e6
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"}
|
Loading…
Reference in New Issue
Block a user