Simp-O-Matic

Dumb Discord bot in TS.
git clone git://git.knutsen.co/Simp-O-Matic
Log | Files | Refs | README | LICENSE

commit 66644cd66d097ad6246a870a70e5d4f2eb3a1e93
parent e502583b90f1a0c89ee41688a236112d54b55dce
Author: Bruno <b-coimbra@hotmail.com>
Date:   Thu, 26 Mar 2020 16:04:39 -0300

added cron command

Diffstat:
Mlib/commands/cron.ts | 216+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mlib/default.ts | 1+
Mlib/extensions.ts | 8++++++--
Mlib/main.ts | 5++++-
4 files changed, 190 insertions(+), 40 deletions(-)

diff --git a/lib/commands/cron.ts b/lib/commands/cron.ts @@ -1,9 +1,18 @@ +import { FORMATS } from '../extensions'; +import { help_info } from '../utils'; +import DEFAULT_GUILD_CONFIG from '../default'; + +type Greenwich = 'pm' | 'am'; +type Ordinal = 'st' | 'nd' | 'rd' | 'th'; + interface Schedule { hours?: string; minutes?: string; dayOfMonth?: string; month?: string; dayOfWeek?: string; + greenwich?: Greenwich; + ordinal?: Ordinal } interface Command { @@ -12,19 +21,10 @@ interface Command { }; interface Cron { - job: number, + id: number, schedule?: Schedule, command?: Command -}; - -const RESPONSES = { - help: { - rm: '.cron rm #[job-index]', - command: ':warning: There is no command to execute the cron job on.', - schedule: ':warning: There is no schedule to execute the command on.' - }, - added: 'New cron job has been added', // TODO - empty: ":warning: There are no cron jobs being executed." + executed_at?: number; }; const MATCHERS = { @@ -32,58 +32,201 @@ const MATCHERS = { /^(((0|1)[0-9])|2[0-3]):[0-5][0-9]\s?(pm|am)?$/i, day_of_month: /(?:\b)(([1-9]|0[1-9]|[1-2][0-9]|3[0-1])(st|nd|rd|th)?)(?:\b|\/)/i, - weekdays: 'mon tue wed thu fri sat sun'.split(' '), - prefix: (prefix: string) => new RegExp(`^${prefix}`), + weekdays: 'sun mon tue wed thu fri sat'.split(' '), + months: 'jan feb mar apr may jun jul aug sep oct nov dec' + .split(' ').map(month => month.capitalize()), + prefix: (x: string) => new RegExp("^\\" + x), ordinals: /st|nd|rd|th/, greenwich: /pm|am/ }; +const RESPONSES = { + help: { + rm: 'The syntax for removing a cron job is: ' + + '.cron rm #[job-index]'.format(FORMATS.block), + command: ':warning: There is no command to execute the cron job on.', + schedule: ':warning: There is no schedule to execute the command on.' + }, + empty: ":warning: There are no cron jobs being executed.", + removed: (id: number) => + `Removed cron job #${id.toString().format(FORMATS.bold)}.`, + added: (cron: Cron) => + `New cron (#${cron.id.toString().format(FORMATS.bold)}) has been added.`, + list: (cron: Cron) => { + let { schedule } = cron; + let result: string = ""; + + result += `#${cron.id} `.format(FORMATS.bold); + result += `${cron.command.name.shorten(20)}`.format(FORMATS.block); + + if (schedule?.hours && schedule?.minutes) { + result += `: ${schedule.hours}:${schedule.minutes}`; + if (schedule?.greenwich) + result += `${schedule.greenwich.toUpperCase()}`; + result += ' :clock3: '; + } + + if (schedule?.dayOfWeek) { + let weekday = MATCHERS.weekdays[ + Number(schedule.dayOfWeek) - 1 + ]?.toUpperCase(); + + result += `${weekday}, `; + } + + if (schedule?.dayOfMonth) { + let month = MATCHERS.months[ + Number(schedule.month) - 1 + ]?.capitalize(); + + result += `${month} ${schedule.dayOfMonth}`; + + if (schedule?.ordinal) + result += `${schedule.ordinal}`; + } + + return result; + } +}; + +export class Timer { + private homescope: HomeScope; + + constructor(homescope: HomeScope) { + this.homescope = homescope; + } + + compare(job: Cron): void { + let current = new Date(); + current.setDate(current.getDate()); + current.setUTCHours(current.getHours() % 12); + current.setSeconds(0); + current.setMilliseconds(0); + + if (current.getTime() === this.timestamp(job)) + this.dispatch(job, current.getTime()); + } + + timestamp(job: Cron): number { + let date = new Date(); + let { hours, minutes, month, dayOfMonth } = job.schedule; + + date.setUTCHours(Number(hours), Number(minutes), 0); + date.setMonth(Number(month) - 1); + date.setMilliseconds(0); + date.setDate(Number(dayOfMonth)); + + return date.getTime(); + } + + dispatch(job: Cron, timespan: number) { + if (job.executed_at === timespan) + return; + + console.log('Executed cron job #' + job.id); + + this.homescope.message.content = + `${this.homescope.CONFIG.commands.prefix} ${job.command.name} ${job.command.args.join(' ')}`; + + job.executed_at = timespan; + + this.homescope.main.process_command( + this.homescope.message + ); + } + + verify(jobs: Cron[]): void { + jobs.forEach((job: Cron) => this.compare(job)) + } +} + export default (home_scope: HomeScope) => { const { message, args, CONFIG } = home_scope; - // let command = `20:32pm 26th 7 fri ${prefix}echo poopoo`; - let crons: Cron[] = CONFIG.cron_jobs; + if (args.length === 0 || args[0] === 'help') { + return message.channel.send( + help_info('cron', CONFIG.commands.prefix) + ); + } + + const cleanup = (jobs: Cron[]): Cron[] => jobs + .filter(x => x != null) + .map((x, i) => { + x.id = i; + return x; + }); + + let crons: Cron[] = cleanup(CONFIG.cron_jobs); + const timer = new Timer(home_scope); + + setInterval(() => { + timer.verify(crons); + }, DEFAULT_GUILD_CONFIG.cron_interval); + + const submit = () => + CONFIG.cron_jobs = crons; const matches = (value: string, regex: RegExp): string | undefined => (value.match(regex) || {})?.input; - const rm = (job: number) => - delete crons[crons.map(cron => cron.job).indexOf(job)]; + const rm = (job: number) => { + delete crons[crons.map(x => x.id).indexOf(job)]; + crons = cleanup(crons); + submit(); + message.answer(RESPONSES.removed(job)); + }; - // TODO const list = () => { if (crons.length == 0) return message.answer(RESPONSES.empty); - crons.each(cron => message.answer("Can't list jobs yet...")); + console.log('list command:', crons + .filter(x => x != null) + .map(x => RESPONSES.list(x)) + .join("\n")); + + message.channel.send( + crons + .filter(x => x != null) + .map(x => RESPONSES.list(x)) + .join("\n") + ); } - const parse = (args: string[]): Cron => { + const parse = (argm: string[]): Cron => { let cron: Cron = { - job: crons.slice(-1)[0]?.job + 1 || 0 + id: crons.slice(-1)[0]?.id + 1 || 0 }; - args.some((argument, i) => { + argm.some((argument, i) => { + argument = argument.trim(); + switch (argument) { - case (matches(argument, MATCHERS.prefix(CONFIG.commands.prefix))): - cron.command = { - name: argument, - args: args.slice(i + 1) + case ( + matches(argument, MATCHERS.prefix(CONFIG.commands.prefix)) + ): cron.command = { + name: argument.split(CONFIG.commands.prefix)[1], + args: argm.slice(i + 1) }; break; case (matches(argument, MATCHERS.hour_mins)): - const [hour, min] = argument.split(':'); + const [hour, mins] = argument.split(':'); + const [min, greenwich] = mins.split(MATCHERS.greenwich); cron.schedule = { hours: hour, - minutes: min.split(MATCHERS.greenwich)[0] + minutes: min, + greenwich: greenwich as Greenwich }; break; case (matches(argument, MATCHERS.day_of_month)): + const [dayOfMonth, ordinal] = + argument.split(MATCHERS.ordinals); + const date = matches(argument, MATCHERS.ordinals) == undefined ? { month: argument } - : { dayOfMonth: argument.split(MATCHERS.ordinals)[0] }; + : { dayOfMonth, ordinal: ordinal as Ordinal }; cron.schedule = { ...cron.schedule, @@ -107,27 +250,26 @@ export default (home_scope: HomeScope) => { return cron; }; - let options = args.join(' ').split(' '); - - if (options[0] === 'ls') + if (args[0] === 'ls') list(); - else if (options[0] === 'rm') { - let job: number = Number(options[1]); + else if (args[0] === 'rm') { + let job: number = Number(args[1]); (isNaN(job)) ? message.answer(RESPONSES.help.rm) : rm(job); } else { - const cron: Cron = parse(options); + const cron: Cron = parse(args); if (!cron?.command) message.answer(RESPONSES.help.command); else if (!cron?.schedule) message.answer(RESPONSES.help.schedule) else { - crons.push(parse(options)); - message.answer(RESPONSES.added); + crons.push(cron); + submit(); + message.answer(RESPONSES.added(cron)); } } }; diff --git a/lib/default.ts b/lib/default.ts @@ -12,6 +12,7 @@ const DEFAULT_GUILD_CONFIG : Types.Config = { '541761315887120399': 'Moscow' }, cron_jobs: [], + cron_interval: 2000, commands: { prefix: '!', max_history: 40, diff --git a/lib/extensions.ts b/lib/extensions.ts @@ -1,3 +1,5 @@ +import { SimpOMatic } from './main'; + // Global Extensions: declare global { type HomeScope = { @@ -6,7 +8,8 @@ declare global { GIT_URL: string, HELP_MESSAGES: string[], HELP_SECTIONS: string[] , ALL_HELP: string[], CONFIG: Types.Config, SECRETS: any, KNOWN_COMMANDS: string[], - expand_alias: (operator: string, args: string[], message: Message) => string + expand_alias: (operator: string, args: string[], message: Message) => string, + main: SimpOMatic; }; namespace Types { @@ -26,6 +29,7 @@ declare global { system_channel: string, pp_sizes: { [key: string]: number } cron_jobs: any[], + cron_interval: number; weather_locations: { [key: string]: string }, commands: { prefix: string, @@ -197,7 +201,7 @@ export const FORMATS: TextFormat = { }; String.prototype.format = function (fmt: string) { - return `${fmt}${this}${fmt}`; + return `${fmt}${this.toString()}${fmt}`; }; String.prototype.shorten = function (width=40) { diff --git a/lib/main.ts b/lib/main.ts @@ -27,6 +27,7 @@ import { pastebin_latest, pastebin_update, pastebin_url } from './api/pastebin'; import { Guild } from 'discord.js'; +import { Timer } from './commands/cron'; // Anything that hasn't been defined in `bot.json` // will be taken care of by the defaults. @@ -254,10 +255,12 @@ export class SimpOMatic { HELP_SOURCE, HELP_KEY, GIT_URL, HELP_MESSAGES, HELP_SECTIONS, ALL_HELP, CONFIG, SECRETS, KNOWN_COMMANDS, - expand_alias: this.expand_alias }; + expand_alias: this.expand_alias, + main: this }; const commands = read_dir(`${__dirname}/commands`) .map(n => n.slice(0, -3)); + if (commands.includes(operator)) return import(`./commands/${operator}`).then(mod => mod.default(homescope));