874 lines
35 KiB
JavaScript
874 lines
35 KiB
JavaScript
|
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))
|