Skip to content

Commit

Permalink
Add threads archive cron
Browse files Browse the repository at this point in the history
Discord archives threads after specific number of days (max 7 days). But
our threads may need more time to be alive. This is a cron job that will
monitor threads and ask user to archive it or extend it to be long
living thread that won't get archived.
  • Loading branch information
nkuba committed Apr 6, 2023
1 parent 1dc0981 commit 73d0c43
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ dist
.yarn/install-state.gz
.pnp.*

brain.json
data/

# direnv
.envrc
78 changes: 78 additions & 0 deletions cron/thread-lifecycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const GUILD = process.env.GUILD

const { MessageActionRow, MessageButton } = require("discord.js")
const moment = require("moment")

const { read } = require("../utils/storage")

function weekdaysBefore(theMoment, days) {
let newMoment = theMoment.clone()
while (days > 0) {
if (newMoment.isoWeekday() < 6) {
days -= 1
}
newMoment = newMoment.subtract(1, "days")
}
return newMoment
}

function archiveThreadPrompt(client, thread) {
if (thread.ownerId === client.user.id) {
thread.setArchived(true)
return
}

const row = new MessageActionRow()
.addComponents(
new MessageButton()
.setCustomId("archive-thread")
.setLabel("Archive The Thread")
.setStyle("DANGER")
)
.addComponents(
new MessageButton()
.setCustomId("long-running-thread")
.setLabel("Long-Running Thread")
.setStyle("SECONDARY")
)

thread.send({
content: `<@${thread.ownerId}>, it's been a bit since this thread has seen activity. Ready to archive it?`,
components: [row],
})
}

module.exports = {
schedule: "*/15 * * * *",
timezone: "America/New_York",
execute(client) {
return async () => {
const guild = await client.guilds.fetch(GUILD)
const channels = await guild.channels.fetch()
const longRunningThreadIds = (await read("long-running-thread-ids")) || {}
const archiveThreshold = weekdaysBefore(moment(), 4)
channels
.filter((channel) => channel.isText() && channel.viewable)
.forEach(async (channel) => {
const threads = await channel.threads.fetch()
threads.threads.forEach(async (thread) => {
const messages = await thread.messages.fetch({ limit: 1 })
const lastActivity = Math.max(
(messages.first() && messages.first().createdTimestamp) || 0,
thread.archiveTimestamp
)
if (moment(lastActivity).isAfter(archiveThreshold)) {
return
}

if (longRunningThreadIds[thread.id]) {
await thread.setArchived(true)
thread.setArchived(false)
} else {
archiveThreadPrompt(client, thread)
}
})
})
}
},
}
30 changes: 30 additions & 0 deletions scripts/button-interactions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { read, write } = require("../utils/storage")

async function archiveThread(client, interaction) {
const guild = await client.guilds.fetch(interaction.guildId)
const channel = await guild.channels.fetch(interaction.channelId)
await interaction.reply("Done!")
channel.setArchived(true)
}

async function markThreadLongRunning(interaction) {
let longRunningThreadIds = (await read("long-running-thread-ids")) || {}
longRunningThreadIds[interaction.channelId] = true
await write("long-running-thread-ids", longRunningThreadIds)
await interaction.reply("Alright. I'll keep the thread alive.")
}

module.exports = {
trigger: "interactionCreate",
execute(client) {
return async (interaction) => {
if (!interaction.isButton()) return
if (interaction.customId === "archive-thread") {
archiveThread(client, interaction)
}
if (interaction.customId === "long-running-thread") {
markThreadLongRunning(interaction)
}
}
},
}
37 changes: 37 additions & 0 deletions utils/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const EventEmitter = require("events")

let brainLock = false
const emitter = new EventEmitter()
const fs = require("fs")

const storageFilePath = "data/storage.json"

module.exports = {
async read(key) {
if (brainLock) {
await new Promise((resolve) => emitter.once("unlocked", resolve))
}
brainLock = true

const data = await fs.promises.readFile(storageFilePath)
const value = JSON.parse(data.toString())[key]

brainLock = false
emitter.emit("unlocked")
return value
},
async write(key, val) {
if (brainLock) {
await new Promise((resolve) => emitter.once("unlocked", resolve))
}
brainLock = true

const data = await fs.promises.readFile(storageFilePath)
let brain = JSON.parse(data.toString())
brain[key] = val
await fs.promises.writeFile(storageFilePath, JSON.stringify(brain, null, 2))

brainLock = false
emitter.emit("unlocked")
},
}

0 comments on commit 73d0c43

Please sign in to comment.