Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
"workerActivity": "PoracleJS Helper", // Worker Activity
"disableAutoGreetings": false, // whether Poracle will greet users who are assigned a role and get permission to use the bot
"uploadEmbedImages": false, // whether Poracle will retrieve and upload images direct to discord CDN
"dynamicRsvpEditing": true, // whether Poracle will edit existing raid messages when RSVP counts change instead of sending new messages
// checkRole - whether to check all discord users for role membership (every _checkRoleInterval_
// hours, deleting them if not (note general.roleCheckMode must be set to
// actually carry out the deletions rather than just logging them)
Expand Down
6 changes: 5 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,14 @@ function handleShutdown() {
const workerSaves = []
for (const worker of discordWorkers) {
workerSaves.push(worker.saveTimeouts())
workerSaves.push(worker.saveRaidCache())
}
if (telegram) workerSaves.push(telegram.saveTimeouts())
if (telegramChannel) workerSaves.push(telegramChannel.saveTimeouts())
if (discordWebhookWorker) workerSaves.push(discordWebhookWorker.saveTimeouts())
if (discordWebhookWorker) {
workerSaves.push(discordWebhookWorker.saveTimeouts())
workerSaves.push(discordWebhookWorker.saveRaidCache())
}

gymCache.save(true)
Promise.all(workerSaves)
Expand Down
12 changes: 12 additions & 0 deletions src/controllers/raid.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ class Raid extends Controller {
const templateType = 'raid'
const message = await this.createMessage(logReference, templateType, platform, cares.template, language, cares.ping, view)

const raidMessageKey = `${data.gym_id}:${data.end}:${data.pokemon_id}:${cares.type}:${cares.id}`

const work = {
lat: data.latitude.toString()
.substring(0, 8),
Expand All @@ -486,6 +488,10 @@ class Raid extends Controller {
emoji: data.emoji,
logReference,
language,
raidMessageKey,
gymId: data.gym_id,
raidEndTime: data.end,
pokemonId: data.pokemon_id,
}
jobs.push(work)
}
Expand Down Expand Up @@ -599,6 +605,8 @@ class Raid extends Controller {
const templateType = 'egg'
const message = await this.createMessage(logReference, templateType, platform, cares.template, language, cares.ping, view)

const raidMessageKey = `${data.gym_id}:${data.end}:${data.pokemon_id}:${cares.type}:${cares.id}`

const work = {
lat: data.latitude.toString().substring(0, 8),
lon: data.longitude.toString().substring(0, 8),
Expand All @@ -611,6 +619,10 @@ class Raid extends Controller {
emoji: data.emoji,
logReference,
language,
raidMessageKey,
gymId: data.gym_id,
raidEndTime: data.end,
pokemonId: data.pokemon_id,
}
jobs.push(work)
}
Expand Down
48 changes: 48 additions & 0 deletions src/lib/discord/commando/commands/rsvp-clean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
exports.run = async (client, msg) => {
try {
// Check target
if (!client.config.discord.admins.includes(msg.author.id) && msg.channel.type !== 'DM') {
return await msg.author.send(client.translator.translate('Please run commands in Direct Messages'))
}

const startMessage = await msg.reply('Clearing raid message cache - this will force all future RSVP updates to send new messages instead of editing existing ones...')

// Clear raid message caches for all discord workers
let clearedCount = 0
for (const worker of client.discordWorkers) {
const keys = worker.raidMessageCache.keys()
clearedCount += keys.length
worker.raidMessageCache.flushAll()
await worker.saveRaidCache()
}

// Clear raid message cache for webhook worker
if (client.discordWebhookWorker) {
const keys = client.discordWebhookWorker.raidMessageCache.keys()
clearedCount += keys.length
client.discordWebhookWorker.raidMessageCache.flushAll()
await client.discordWebhookWorker.saveRaidCache()
}

await startMessage.delete()
const finishMessage = await msg.reply(`Raid message cache cleared! Removed ${clearedCount} cached raid messages. All future RSVP updates will send new messages.`)
setTimeout(() => { finishMessage.delete() }, 15000)
} catch (err) {
await msg.reply('Failed to run rsvp-clean, check logs')
client.logs.log.error(`rsvp-clean command "${msg.content}" unhappy:`, err)
}
}

exports.conf = {
enabled: true,
guildOnly: false,
aliases: [],
permLevel: 1,
}

exports.help = {
name: 'rsvp-clean',
description: 'Clear raid message cache (admin only)',
usage: 'rsvp-clean',
example: '!rsvp-clean',
}
119 changes: 118 additions & 1 deletion src/lib/discord/discordWebhookWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class DiscordWebhookWorker {
this.webhookQueue = []
this.rehydrateTimeouts = rehydrateTimeouts
this.webhookTimeouts = new NodeCache()
this.raidMessageCache = new NodeCache()
this.query = query

this.queueProcessor = new FairPromiseQueue(this.webhookQueue, this.config.tuning.concurrentDiscordWebhookConnections, ((t) => t.target))
Expand Down Expand Up @@ -107,6 +108,70 @@ class DiscordWebhookWorker {

const logReference = data.logReference ? data.logReference : 'Unknown'

// Check if this is an RSVP update for an existing raid message
if (data.raidMessageKey && this.config.discord.dynamicRsvpEditing) {
const existingMsg = this.raidMessageCache.get(data.raidMessageKey)
if (existingMsg && existingMsg.messageId) {
this.logs.discord.info(`${logReference}: http(s)> ${data.name} WEBHOOK Editing raid message (RSVP update)`)
try {
const editUrl = `${data.target}/messages/${existingMsg.messageId}`
const timeoutMs = this.config.tuning.discordTimeout || 10000

const res = await this.retrySender(`${logReference} (edit)`, async () => {
let uploadData = data.message
let headers = null

if (this.config.discord.uploadEmbedImages && data.message.embeds && data.message.embeds.length && data.message.embeds[0].image && data.message.embeds[0].image.url) {
const copyMessage = JSON.parse(JSON.stringify(data.message))
const imageUrl = copyMessage.embeds[0].image.url
copyMessage.embeds[0].image.url = 'attachment://map.png'

const response = await axios.get(imageUrl, { responseType: 'arraybuffer' })
const buffer = Buffer.from(response.data, 'utf-8')

const formData = new FormData()
formData.append('payload_json', JSON.stringify(copyMessage))
formData.append('file', buffer, 'map.png')

headers = formData.getHeaders()
uploadData = formData
}

const source = axios.CancelToken.source()
const timeout = setTimeout(() => {
source.cancel(`Timeout waiting for response - ${timeoutMs}ms`)
}, timeoutMs)

const result = await axios({
method: 'patch',
url: editUrl,
data: uploadData,
headers,
validateStatus: ((status) => status < 500),
cancelToken: source.token,
})

clearTimeout(timeout)
return result
})

if (res.status >= 200 && res.status <= 299) {
this.logs.discord.info(`${logReference}: ${data.name} WEBHOOK Edit successful (status ${res.status})`)
return true
} else if (res.status === 404) {
this.logs.discord.warn(`${logReference}: ${data.name} WEBHOOK Message not found (404), will send new message`)
this.raidMessageCache.del(data.raidMessageKey)
} else {
this.logs.discord.warn(`${logReference}: ${data.name} WEBHOOK Edit failed (status ${res.status}), will send new message`)
this.raidMessageCache.del(data.raidMessageKey)
}
} catch (editErr) {
this.logs.discord.warn(`${logReference}: ${data.name} WEBHOOK Failed to edit message, will send new message`, editErr)
this.raidMessageCache.del(data.raidMessageKey)
}
}
}

this.logs.discord.info(`${logReference}: http(s)> ${data.name} WEBHOOK Sending discord message`)
this.logs.discord.debug(`${logReference}: http(s)> ${data.name} WEBHOOK Sending discord message to ${data.target}`, data.message)

Expand Down Expand Up @@ -169,6 +234,20 @@ class DiscordWebhookWorker {
}
this.logs.discord.silly(`${logReference}: ${data.name} WEBHOOK results ${data.target} ${res.statusText} ${res.status}`, res.headers)

// Store message ID for future RSVP edits
if (data.raidMessageKey && this.config.discord.dynamicRsvpEditing && res.status === 200) {
const msgId = res.data.id
const ttl = Math.max((data.raidEndTime * 1000 - Date.now()) / 1000 + 600, 60)
this.raidMessageCache.set(data.raidMessageKey, {
messageId: msgId,
targetType: 'webhook',
webhookUrl: data.target,
gymId: data.gymId,
endTime: data.raidEndTime,
pokemonId: data.pokemonId,
}, ttl)
}

if (data.clean && res.status === 200) {
const msgId = res.data.id
this.webhookTimeouts.set(msgId, data.target, Math.floor(msgDeletionMs / 1000) + 1)
Expand Down Expand Up @@ -197,7 +276,14 @@ class DiscordWebhookWorker {
async saveTimeouts() {
// eslint-disable-next-line no-underscore-dangle
this.webhookTimeouts._checkData(false)
return fsp.writeFile('.cache/cleancache-webhookWorker.json', JSON.stringify(this.webhookTimeouts.data), 'utf8')
const cleanPromise = fsp.writeFile('.cache/cleancache-webhookWorker.json', JSON.stringify(this.webhookTimeouts.data), 'utf8')

// Also save raid cache
// eslint-disable-next-line no-underscore-dangle
this.raidMessageCache._checkData(false)
const raidPromise = fsp.writeFile('.cache/raidmessage-webhookWorker.json', JSON.stringify(this.raidMessageCache.data), 'utf8')

return Promise.all([cleanPromise, raidPromise])
}

async deleteMessage(logReference, hookName, hookUrl, msgId) {
Expand Down Expand Up @@ -274,6 +360,37 @@ class DiscordWebhookWorker {
this.logs.log.info(`Error processing historic deletes ${err}`)
}
}

// Also load raid cache
await this.loadRaidCache()
}

async loadRaidCache() {
let loaddatatxt

try {
loaddatatxt = await fsp.readFile('.cache/raidmessage-webhookWorker.json', 'utf8')
} catch {
return
}

const now = Date.now()

let data
try {
data = JSON.parse(loaddatatxt)
} catch {
this.logs.log.warn('Raid message cache for webhookWorker contains invalid data - ignoring')
return
}

for (const key of Object.keys(data)) {
const msgData = data[key]
if (msgData.t > now) {
const newTtl = Math.floor((msgData.t - now) / 1000)
this.raidMessageCache.set(key, msgData.v, newTtl)
}
}
}
}

Expand Down
Loading