Simp-O-Matic

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

commit 40304172679f6534e1862dd08334a6aee7f05e69
parent 33bbb244393b5384f10fa5ab6a42fb5d21880f55
Author: Bruno <b-coimbra@hotmail.com>
Date:   Wed, 27 May 2020 21:35:31 -0300

Merge remote-tracking branch 'origin/features/cron'

Diffstat:
Mlib/commands/cron.ts | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 210 insertions(+), 90 deletions(-)

diff --git a/lib/commands/cron.ts b/lib/commands/cron.ts @@ -4,17 +4,46 @@ import DEFAULT_GUILD_CONFIG from '../default'; type Greenwich = 'pm' | 'am'; type Ordinal = 'st' | 'nd' | 'rd' | 'th'; +type AnyValue = '*'; + +interface ListValue { + values: string[]; +} + +interface RangeValue { + range: string[]; + step?: string; +} + +interface PlaceValue { + [place: number]: object; +} + +type Expr = string + | RangeValue + | ListValue + | AnyValue; + +enum Place { + MINUTE, + HOUR, + MONTHDAY, + MONTH, + WEEKDAY +} interface Schedule { - hours?: string; - minutes?: string; - dayOfMonth?: string; - month?: string; - dayOfWeek?: string; - greenwich?: Greenwich; - ordinal?: Ordinal; + hours: Expr; + minutes: Expr; + dayOfMonth: Expr; + month: Expr; + dayOfWeek: Expr; + greenwich: Greenwich; + ordinal: Ordinal; } +type Defaults = keyof Omit<Schedule, 'greenwich' | 'ordinal'>; + interface Command { name: string; args?: string[]; @@ -22,22 +51,24 @@ interface Command { interface Cron { id: number; - schedule?: Schedule; + schedule?: Partial<Schedule>; command?: Command; executed_at?: number; } const MATCHERS = { - hour_mins: - /^(((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, + hour_mins: '^(((0|1)[0-9])|2[0-3]):[0-5][0-9]\\s?(pm|am)?$', + day_of_month: '^(?:\\b)(([1-9]|0[1-9]|[1-2][0-9]|3[0-1])(st|nd|rd|th)?)(?:\\b|\\/)$', + any_value: '^\\*$', + numerics: '(^(\\d\\.?)+$)', + range: '(^\\d+\\-\\d+|\\*\\/\\d+)(\\/\\d+)?$', + list: '^(\\d+\\,\\d+)$', 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)/ + prefix: (x: string) => "^\\" + x, + ordinals: '(st|nd|rd|th)', + greenwich: '(pm|am)' }; const RESPONSES = { @@ -49,10 +80,8 @@ const RESPONSES = { }, empty: ":warning: There are no cron jobs being executed.", clear: "Cleared all executed cron jobs.", - 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.`, + 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) => { const { schedule } = cron; let result: string = ""; @@ -64,7 +93,7 @@ const RESPONSES = { if (schedule?.hours && schedule?.minutes) { result += `: ${schedule.hours}:${schedule.minutes}`; if (schedule?.greenwich) - result += `${schedule.greenwich.toUpperCase()}`; + result += schedule.greenwich.toUpperCase(); result += ' :clock3: '; } @@ -101,38 +130,38 @@ export class Timer { this.homescope = homescope; } - get defaultDate() { + get now(): number { const now = new Date(); - const default_values = { - month: now.getMonth() - 1 - }; - return default_values; - } - - compare(job: Cron): void { - const 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()); + now.toLocaleString('en-US', { timeZone: 'America/New_York' }); + now.setDate(now.getDate()); + now.setUTCHours(now.getHours() % 12); + now.setSeconds(0); + now.setMilliseconds(0); + return now.getTime(); } timestamp(job: Cron): number { const date = new Date(); const { hours, minutes, month, dayOfMonth } = job.schedule; + date.toLocaleString('en-US', { timeZone: 'America/New_York' }); date.setUTCHours(Number(hours), Number(minutes), 0); - date.setMonth(Number(month) - 1); + date.setMonth(Number(month)); date.setMilliseconds(0); - date.setDate(Number(dayOfMonth)); + date.setSeconds(0); + date.setDate(Number(dayOfMonth) - 1); return date.getTime(); } - dispatch(job: Cron, timespan: number) { + compare(job: Cron): void { + if (this.now === this.timestamp(job)) + this.dispatch(job, this.now); + else + console.log('SKIPPED', this.now, this.timestamp(job)); + } + + dispatch(job: Cron, timespan: number): void { if (job.executed_at === timespan) return; @@ -143,6 +172,8 @@ export class Timer { job.executed_at = timespan; + this.homescope.message.answer("Ran cron #" + job.id); + this.homescope.main.process_command( this.homescope.message, true @@ -150,7 +181,7 @@ export class Timer { } verify(jobs: Cron[]): void { - jobs.forEach((job: Cron) => this.compare(job)); + jobs.forEach(job => this.compare(job)); } } @@ -180,8 +211,8 @@ export default (home_scope: HomeScope) => { const submit = () => CONFIG.cron_jobs = crons; - const matches = (value: string, regex: RegExp): string | undefined => - (value.match(regex) || {})?.input; + const matches = (value: string, regex: string): string | undefined => + (value.match(new RegExp(regex)) || {})?.input; const rm = (job: number) => { delete crons[crons.map(x => x.id).indexOf(job)]; @@ -208,60 +239,149 @@ export default (home_scope: HomeScope) => { ); }; - const parse = (argm: string[]): Cron => { - const cron: Cron = { + const tokenize = (args: string[]): Cron => { + const { prefix } = CONFIG.commands; + + let cron: Cron = { id: crons.slice(-1)[0]?.id + 1 || 0 }; - argm.some((argument, i) => { - argument = argument.trim(); - - switch (argument) { - 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, mins] = argument.split(':'); - const [min, greenwich] = mins.split(MATCHERS.greenwich); - - cron.schedule = { - hours: hour, - 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, ordinal: ordinal as Ordinal }; - - cron.schedule = { - ...cron.schedule, - ...date - }; - break; - } + const add = (schedule: Partial<Schedule>): void => { + cron.schedule = { + ...cron.schedule, + ...schedule + }; + }; + + const populate = (value: Expr, position: number): void => { + add(({ + [Place.HOUR]: { hours: value }, + [Place.MINUTE]: { minutes: value }, + [Place.MONTHDAY]: { dayOfMonth: value }, + [Place.WEEKDAY]: { dayOfWeek: value }, + [Place.MONTH]: { month: value } + } as PlaceValue)[position]); + }; + + const set_command = (expr: string, index: number): void => { + cron.command = { + name: expr, + args: args.slice(index + 1) + }; + }; + + const set_hour_mins = (expr: string): void => { + const [hour, mins] = expr.split(':'); + const [min, greenwich] = mins.split(new RegExp(MATCHERS.greenwich)); + + add({ + hours: hour, + minutes: min, + greenwich: greenwich as Greenwich + }); + }; + + const set_day_of_month = (expr: string): void => { + const [dayOfMonth, ordinal] = + expr.split(new RegExp(MATCHERS.ordinals)); + + const date = + matches(expr, MATCHERS.ordinals) === undefined + ? { month: expr } + : { dayOfMonth, ordinal: ordinal as Ordinal }; + + add(date); + }; + + const set_day_of_week = (expr: string): void => { + add({ + dayOfWeek: ( + MATCHERS.weekdays.indexOf(expr) + 1 + ).toString() + }); + }; + + const set_numerics = (expr: Expr, position: number): void => { + populate(expr, position); + }; + + const set_any_value = (position: number): void => { + if (cron.schedule?.hours && + cron.schedule?.minutes && + cron.schedule?.hours !== '*' && + cron.schedule?.minutes !== '*' && + cron.schedule?.greenwich) position += 1; + + args[position] = ''; + populate('*', position); + }; + + const set_range = (expr: string, position: number): void => { + let [from, to, step] = expr.split(/[\-\/]/); + const values: RangeValue = { range: [from, to] }; + + const has_step = (range: string[]) => + range[0] == '*' && Number(range[1]) != NaN; + + if (has_step(values.range)) + step = values.range[1]; + + if (step) values.step = step; + + populate(values, position); + }; + + const set_list = (expr: string, position: number): void => { + const values = expr.split(','); + const list: ListValue = { values }; + + populate(list, position); + }; + + const get_defaults = (key: Defaults): string => { + const now = new Date(); + + return { + hours: now.getHours(), + minutes: now.getMinutes(), + month: now.getMonth(), + dayOfMonth: now.getDate(), + dayOfWeek: now.getDay() + }[key].toString(); + }; + + args.some((argument, i) => { + let position = args.indexOf(argument); + + const MAPPINGS: Record<string, Function> = { + [MATCHERS.prefix(prefix)]: () => set_command(argument, i), + [MATCHERS.hour_mins]: () => set_hour_mins(argument), + [MATCHERS.range]: () => set_range(argument, position), + [MATCHERS.list]: () => set_list(argument, position), + [MATCHERS.numerics]: () => set_numerics(argument, position), + [MATCHERS.day_of_month]: () => set_day_of_month(argument), + [MATCHERS.any_value]: () => set_any_value(position) + }; + + for (let matcher in MAPPINGS) + if (matches(argument, matcher)) + MAPPINGS[matcher](); argument = argument.toLowerCase(); - if (MATCHERS.weekdays.includes(argument)) { - cron.schedule = { - ...cron.schedule, - dayOfWeek: ( - MATCHERS.weekdays.indexOf(argument) + 1 - ).toString() - }; - } + if (MATCHERS.weekdays.includes(argument)) + set_day_of_week(argument); }); + Object + .keys(cron.schedule as object) + .forEach((value: string) => { + let key = <Defaults>value; + + if (cron.schedule && cron.schedule[key] == '*') + cron!.schedule[key] = get_defaults(key); + }); + return cron; }; @@ -270,7 +390,7 @@ export default (home_scope: HomeScope) => { else if (args[0] === 'rm') { const job: number = Number(args[1]); - (isNaN(job)) + isNaN(job) ? message.answer(RESPONSES.help.rm) : rm(job); } @@ -278,7 +398,7 @@ export default (home_scope: HomeScope) => { clear(); } else { - const cron: Cron = parse(args); + const cron: Cron = tokenize(args); if (!cron?.command) message.answer(RESPONSES.help.command);