Skip to content

Commit

Permalink
Merge pull request #88 from en3sis/feat/standup-plugin
Browse files Browse the repository at this point in the history
New: standup plugin
  • Loading branch information
en3sis authored Apr 11, 2024
2 parents ed458f3 + 654c8f7 commit 15a4f7d
Show file tree
Hide file tree
Showing 12 changed files with 789 additions and 768 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,21 @@
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"prettier": "^3.0.3",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
},
"dependencies": {
"@huggingface/inference": "^2.5.0",
"@supabase/supabase-js": "^2.33.1",
"@types/node-schedule": "^2.1.6",
"axios": "^1.6.0",
"cron": "^3.1.7",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"discord.js": "^14.13.0",
"dotenv": "^16.0.2",
"lodash": "^4.17.21",
"node-cache": "^5.1.2",
"node-cron": "^3.0.2",
"openai": "^3.2.1",
"redis": "^4.6.8",
"sentiment": "^5.0.2",
Expand Down
38 changes: 38 additions & 0 deletions src/commands/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { guildActivitySetChannel } from '../controllers/plugins/guild-activity.controller'
import { verifyGuildPluginSettings } from '../controllers/plugins/verify.controller'
import { logger } from '../utils/debugging'
import { standupPluginController } from '../controllers/plugins/standup.controller'

const list = pluginsListNames()

Expand Down Expand Up @@ -111,6 +112,31 @@ module.exports = {
.setDescription('Channel which will recive the notifications')
.setRequired(true),
),
)
.addSubcommand((command) =>
command
.setName('standup')
.setDescription(
'Schedules a message from Monday to Friday to be sent to a specific channel',
)
.addChannelOption((option) =>
option
.setName('channel')
.setDescription('Channel where the message will be sent')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('hour')
.setDescription('At what time the message will be sent (24h format). Example: 9, 21...')
.setRequired(true),
)
.addStringOption((option) =>
option.setName('message').setDescription('Message to be sent').setRequired(true),
)
.addStringOption((option) =>
option.setName('role').setDescription('Role to mention in the message, example: @here'),
),
),
async execute(interaction: CommandInteraction) {
try {
Expand Down Expand Up @@ -147,6 +173,18 @@ module.exports = {
})
} else if (interaction.options.getSubcommand() === 'verify') {
await verifyGuildPluginSettings(interaction)
} else if (interaction.options.getSubcommand() === 'standup') {
const channel = interaction.options.get('channel')!.value as string
const expression = interaction.options.get('hour')!.value as string
const message = interaction.options.get('message')!.value as string
const role = interaction.options.get('role')!.value as string

await standupPluginController(interaction, {
channelId: channel,
expression,
message,
role,
})
}
} catch (error) {
logger('❌ Command: plugins: ', error)
Expand Down
3 changes: 1 addition & 2 deletions src/controllers/bot/plugins.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import supabase from '../../libs/supabase'
import { initialGuildPluginState, pluginsList } from '../../models/plugins.model'
import {
GuildPluginData,
PluginsThreadsMetadata,
PluginsThreadsSettings,
PluginsThreadsMetadata,
} from '../../types/plugins'
import { encrypt } from '../../utils/crypto'
import { GuildPlugin } from './guilds.controller'
Expand Down Expand Up @@ -144,7 +144,6 @@ export const toggleGuildPlugin = async (
.update({ enabled: toggle })
.eq('name', name)
.eq('owner', interaction.guildId)
// .or(`name.eq.guildMemberAdd, name.eq.guildMemberRemove`)

await interaction.editReply({
content: `The plugin ${name} was successfully ${toggle ? 'enabled' : 'disabled'}`,
Expand Down
97 changes: 97 additions & 0 deletions src/controllers/plugins/standup.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { CommandInteraction, TextChannel } from 'discord.js'
import * as cron from 'cron'
import { StandupScheduleMetadata } from '../../types/plugins'
import { updateMetadataGuildPlugin } from '../bot/plugins.controller'
import supabase from '../../libs/supabase'
import { scheduledTasks } from '../tasks/cron-jobs'
import { Hans } from '../..'

// TODO: @en3sis: Allow multiples standup schedules
export const standupPluginController = async (
interaction: CommandInteraction,
metadata: StandupScheduleMetadata,
) => {
try {
const { channelId, expression, role } = metadata

const _expression = `0 ${expression} * * 1-5`
const isValidExpression = RegExp(/^[0-9,]+$/).test(expression)

if (isValidExpression) {
await updateMetadataGuildPlugin(
{ ...metadata, expression: _expression },
'standup',
interaction.guildId,
)

await interaction.editReply({
content: `Enabled Standup Notifications in <#${channelId}> from **Monday to Friday** at **${expression}h** and mentioning the role ${role} \n\n You can disable it by running **/plugins toggle standup false**`,
})
} else {
await interaction.editReply({
content: `Invalid cron expression: ${expression}, please provide a 24h format (eg: 9, 12, 15)`,
})
}
} catch (error) {
console.error('❌ ERROR: standupPluginController(): ', error)
}
}

export const initStadupsSchedules = async () => {
try {
const { error, data } = await supabase
.from('guilds_plugins')
.select('owner, metadata, enabled')
.eq('name', 'standup')

if (error) throw error

data.forEach(async (standupGuildPlugin) => {
if (!standupGuildPlugin.metadata || !standupGuildPlugin.enabled) return

await registerStandupSchedule(
standupGuildPlugin.owner,
standupGuildPlugin.metadata as StandupScheduleMetadata,
)
})
} catch (error) {
console.error('❌ ERROR: registerStandupSchedule(): ', error)
}
}

export const registerStandupSchedule = async (owner: string, metadata: StandupScheduleMetadata) => {
try {
const { channelId, expression, role, message } = metadata

const job = new cron.CronJob(expression, () => {
const channel = Hans.channels.cache.get(channelId) as TextChannel

if (channel) {
// INFO: Current date in format Tuesday, 1st of December 2020
const currentDate = new Date().toLocaleDateString('en-GB', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})

// INFO: Opens a tread on the messaged sent to the channel.
channel.send(`📆 Standup: **${currentDate}** | ${role ?? ''}`).then((msg) => {
msg.startThread({
name: `${message ?? '✍️ Please write down your standup'}`,
autoArchiveDuration: 1440,
})
})
}
})

job.start()
// TODO: @en3sis: Expand to more than one job
scheduledTasks[`${owner}#standup`] = job
if (!!process.env.ISDEV) {
console.debug(`✅ Standup schedule registered count:`, Object.keys(scheduledTasks).length)
}
} catch (error) {
console.error('❌ ERROR: registerStandupSchedule(): ', error)
}
}
28 changes: 20 additions & 8 deletions src/controllers/tasks/cron-jobs.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { Client } from 'discord.js'
import { initStadupsSchedules } from '../plugins/standup.controller'

export const CronJobsTasks = async (Hans: Client) => {
// INFO: Stores the scheduled task so we can stop it later.
export const scheduledTasks = {}

/** Schedule cron jobs. */
export const scheduleCronJobs = async () => {
try {
// await redditPluginInit(Hans)
// cron.schedule(
// process.env.ISDEV === 'true' ? CRON_JOB_TIME_DEV : CRON_JOB_TIME,
// async () => await redditPluginInit(Hans),
// )
// INFO: Standup Plugin
await initStadupsSchedules()
} catch (error) {
console.error('❌ ERROR: CronJobsTasks(): ', error)
console.error('❌ ERROR: scheduleCronJobs(): ', error)
}
}

/**
* Stops a specific cron job.
* @param {string} id - The id of the cron job.
* */
export const stopSpecificCronJob = (id: string) => {
if (scheduledTasks[id]) {
scheduledTasks[id].stop()
delete scheduledTasks[id]
}
}
4 changes: 4 additions & 0 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { insertAllGuilds } from '../controllers/bot/guilds.controller'
import { notifyPulse } from '../controllers/events/ready.controller'
import { configsRealtime } from '../realtime/presence.realtime'
import { reportErrorToMonitoring } from '../utils/monitoring'
import { scheduleCronJobs } from '../controllers/tasks/cron-jobs'

module.exports = {
name: 'ready',
Expand Down Expand Up @@ -41,6 +42,9 @@ module.exports = {

// INFO: Start the realtime presence, this will listen to the database changes and update the bot presence.
configsRealtime()

// INFO: Schedule cron jobs.
await scheduleCronJobs()
} catch (error) {
console.log('❌ ERROR: ready(): ', error)

Expand Down
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ for (const file of eventFiles) {
Hans.login(process.env.DISCORD_TOKEN!)

Hans.on('error', (err) => !!process.env.ISDEV && console.log('❌ ERROR: initHans()', err))

Hans.on('debug', (msg) => !!process.env.ISDEV && console.log('🐛 DEBUG: initHans()', msg))

Hans.on('unhandledRejection', async (error) => {
const _embed = {
title: `unhandledRejection`,
Expand Down
8 changes: 8 additions & 0 deletions src/models/plugins.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export const pluginsList: Record<string, GenericPluginParts> = {
description: 'Verifies that the user is human.',
category: 'moderation',
},
standup: {
...genericStructure,
description: 'Notifies the members to post their standup.',
category: 'productivity',
},
// messageReactionAdd: {
// ...genericStructure,
// description: 'Notifies when a reaction is added to a message.',
Expand Down Expand Up @@ -116,5 +121,8 @@ export const initialGuildPluginState = () => {
verify: {
default_enabled: false,
},
standup: {
default_enabled: false,
},
}
}
12 changes: 12 additions & 0 deletions src/realtime/presence.realtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { setPresence } from '../controllers/bot/config.controller'
import { registerStandupSchedule } from '../controllers/plugins/standup.controller'
import { stopSpecificCronJob } from '../controllers/tasks/cron-jobs'
import { deleteFromCache } from '../libs/node-cache'
import supabase from '../libs/supabase'

Expand Down Expand Up @@ -30,7 +32,17 @@ export const configsRealtime = () => {
table: 'guilds_plugins',
},
async (payload) => {
console.log('Guild plugin updated:', payload.new)
// INFO: Refresh the cache for the guild plugins
deleteFromCache(`guildPlugins:${payload.new.owner}:${payload.new.name}`)

if (payload.new.name === 'standup') {
stopSpecificCronJob(`${payload.new.owner}#standup`)

if (payload.new.enabled && payload.new.metadata) {
await registerStandupSchedule(payload.new.owner, payload.new.metadata)
}
}
},
)
.subscribe()
Expand Down
7 changes: 7 additions & 0 deletions src/types/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export type PluginsThreadsMetadata = {
enabled?: boolean
}

export type StandupScheduleMetadata = {
channelId: string
expression: string
role: string
message: string
}

export type PluginsThreadsSettings = {
interaction: CommandInteraction
metadata: PluginsThreadsMetadata
Expand Down
3 changes: 3 additions & 0 deletions supabase/seed.template.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
INSERT INTO users_settings (id, created_at, user_id, metadata, type) VALUES
(1, '2023-09-13 08:05:31+00', USER_ID_ONE, '{"timezone":"Europe/Berlin"}', 'timezone'),
(2, '2023-09-13 08:06:04.711699+00', USER_ID_TWO, '{"timezone":"Africa/Ceuta"}', 'timezone');

alter
publication supabase_realtime add table public.guilds_plugins;
Loading

0 comments on commit 15a4f7d

Please sign in to comment.