From 5542ac5ce54abbce77fa37df4487d574fa21a43a Mon Sep 17 00:00:00 2001 From: Yessiest Date: Sun, 8 May 2022 19:26:52 +0400 Subject: [PATCH] Added CRON --- .gitignore | 3 + bot.lua | 36 ++-- libraries/cron.lua | 405 ++++++++++++++++++++++++++++++++++++++++++ libraries/tasklib.lua | 38 ---- plugins/cron/init.lua | 229 ++++++++++++++++++++++++ 5 files changed, 658 insertions(+), 53 deletions(-) create mode 100644 libraries/cron.lua delete mode 100644 libraries/tasklib.lua create mode 100644 plugins/cron/init.lua diff --git a/.gitignore b/.gitignore index 1f1c1bc..f77a67c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,8 @@ /servers /discordia.log /gateway.json +/luvit +/lit +/luvi *.so *.o diff --git a/bot.lua b/bot.lua index 12a10fd..8f27b75 100644 --- a/bot.lua +++ b/bot.lua @@ -8,27 +8,33 @@ client = discordia.Client() --activate the import system local import = require("import")(require) +local server_ids = { + "640251445949759499" +} local servers = {} --create server local server = import("classes.server-handler") client:on("ready",function() print("starting test") - if not servers["640251445949759499"] then - servers["640251445949759499"] = server(client,client:getGuild("640251445949759499"),{ - path = os.getenv("HOME").."/bot-savedata/640251445949759499/", - autosave_frequency = 20, - default_plugins = { - "meta", - "help", - "plugins", - "esolang", - "tools", - "reactions", - "roledefaults", - "security" - } - }) + for _,id in pairs(server_ids) do + if not servers[id] then + servers[id] = server(client,client:getGuild(id),{ + path = os.getenv("HOME").."/bot-savedata/"..id.."/", + autosave_frequency = 20, + default_plugins = { + "meta", + "help", + "plugins", + "esolang", + "tools", + "reactions", + "roledefaults", + "security", + "cron" + } + }) + end end end) diff --git a/libraries/cron.lua b/libraries/cron.lua new file mode 100644 index 0000000..3c42114 --- /dev/null +++ b/libraries/cron.lua @@ -0,0 +1,405 @@ +-- Lua cron parser +--[[ +Copyright © 2022 Yessiest + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +-- Adjustments for lua5.1 +if _VERSION=="Lua 5.1" then + table.unpack = unpack +end +local cron = { + directive_handler = nil +} + +local units = { + m = 60, + h = 60*60, + d = 60*60*24, + y = 60*60*24*356, + w = 60*60*24*7 +} +cron.convert_delay = function(str) + local time = os.time() + str:gsub("(%d+)([hmdyw])",function(n,unit) + time = time+(units[unit]*tonumber(n)) + end) + return time +end + +-- Utility functions +local mdays = {31,28,31,30,31,30,31,31,30,31,30,31} +cron._date = function(d,m,y) + local current_date = os.date("*t") + local y = ("2000"):sub(1,4-tostring(y):len())..tostring(y) + d = tonumber(d or current_date.day) + m = tonumber(m or current_date.month) + y = tonumber(y or current_date.year) + if ((y%4 == 0) and (y%100 ~= 0)) or ((y%100 == 0) and (y%400 == 0)) then + mdays[2] = 29 + else + mdays[2] = 28 + end + return { + assert((d > 0) and (d <= (mdays[m] or 31)) and d, "Invalid day: "..tostring(d)), + assert(mdays[m] and m, "Invalid month: "..tostring(m)), + y + } +end + +cron._time = function(h,m) + local current_date = os.date("*t") + h = tonumber(h or current_date,hour) + m = tonumber(m or current_date.min) + return { + assert((h >= 0) and (h < 24) and h, "Invalid hour: "..tostring(h)), + assert((m >= 0) and (m < 60) and m, "Invalid min: "..tostring(m)) + } +end + +cron._compare_tables = function(d1,d2) + for k,v in pairs(d1) do + if d2[k] ~= v then + return false + end + end + return true +end +-- Token types, in (regex, type, preprocessor) format +local token_types = { + {"@(%w+)", "directive", function(text) + return text + end}, + {"(%d%d)%.(%d%d)%.(%d%d%d?%d?)","date",function(d,m,y) + return cron._date(d,m,y) + end}, + {"(%d%d):(%d%d)","time",function(h,m) + return cron._time(h,m) + end}, + {"(%d*,.*)", "any_list",function(text) + return function(num) + local status = false + text:gsub("%d*",function(number) + if num == tonumber(number) then + status = true + end + end) + return status + end + end}, + {"%*/(%d+)", "any_modulo", function(text) + return function(num) + return (num % tonumber(text) == 0) + end + end}, + {"%*", "any", function() + return function() + return true + end + end}, + {"%d+", "number", function(text) + return function(num) + return num == tonumber(text) + end + end}, + {"^%s*$","spacer", function(text) return text end}, + {"%S+","command", function(text) return text end} +} +-- Valid argument matching predicates for directives +local predtypes = { + {"([<>])(=?)(%d*)","comparison",function(lm,eq,number) + local number = tonumber(number) + return function(input) + local input = tonumber(input) + if not input then return false end + return ((eq == "=") and number == input) or + ((lm == ">") and number < input) or + ((lm == "<") and number > input) + end + end}, + {"/([^/]*)/","regex",function(regex) + return function(input) + return (tostring(input):match(regex) ~= nil) + end + end}, + {"\"([^\"]*)\"","string",function(str) + return function(input) + return str==tostring(input) + end + end}, + {"'([^']*)'","string",function(str) + return function(input) + return str==tostring(input) + end + end}, + {"%d+","number",function(number) + return function(input) + return number == tostring(input) + end + end}, + {"%*","any",function() + return function() + return true + end + end}, + {":","delimiter",function() + return function() + error("Delimiter is not a predicate!") + end + end}, + {"%s+","spacer",function() + return function() + error("Spacer is not a predicate!") + end + end}, + {"%S+","command", function(text) + return function() + return text + end + end} +} + +-- Valid syntactic constructions +local syntax = { + {{"number","number","number","number","number"},"cronjob", + function(min,hour,day,mo,dw,comm) + return function(date) + local status = min(date.min) + status = status and hour(date.hour) + status = status and day(date.day) + status = status and mo(date.month) + status = status and dw(wday) + return status,comm + end + end}, + {{"date","time"},"onetime",function(date,time,comm) + local time = os.time({day = date[1], month = date[2], year = date[3], + hour = time[1], min = time[2] + }) + return function(cdate) + return os.time(cdate) >= time,comm + end + end}, + {{"time","date"},"onetime",function(time,date,comm) + local time = os.time({day = date[1], month = date[2], year = date[3], + hour = time[1], min = time[2] + }) + return function(cdate) + return os.time(cdate) >= time,comm + end + end} +} + +local find_strings = function(text) + -- Find 2 string delimiters. + -- Partition text into before and after if the string is empty + -- Partition text into before, string and after if the string isn't empty + local strings = {text} + while strings[#strings]:match("[\"'/]") do + local string = strings[#strings] + -- Opening character for a string + local open_pos = string:find("[\"'/]") + local open_char = string:sub(open_pos,open_pos) + if strings[#strings]:sub(open_pos+1,open_pos+1) == open_char then + -- Empty string + local text_before = string:sub(1,open_pos-1) + local text_after = string:sub(open_pos+2,-1) + strings[#strings] = text_before + table.insert(strings,open_char..open_char) + table.insert(strings,text_after) + else + -- Non-empty string + local text_before = string:sub(1,open_pos-1) + local _,closing_position = string:sub(open_pos,-1):find("[^\\]"..open_char) + if not closing_position then + break + else + closing_position = closing_position+open_pos-1 + end + local text_string = string:sub(open_pos,closing_position) + local text_after = string:sub(closing_position+1,-1) + strings[#strings] = text_before + table.insert(strings,text_string) + table.insert(strings,text_after) + end + end + for k,v in pairs(strings) do + if v:len() == 0 then + table.remove(strings,k) + end + end + return strings + -- P.S: This one is the best one i've written. Sure it looks clunky, but it + -- does exactly what I expect it to do - handle cases when there are string + -- delimiters inside other strings. Lovely. Also kinda horrifying. +end + +local startfrom = function(pos,t) + local newtable = {} + for i = pos,#t do + newtable[i+1-pos] = t[i] + end + return newtable +end + +cron._split = function(text) + -- Parse strings + local tokens = {} + text:gsub("(%S*)(%s*)",function(text,padding) + table.insert(tokens,text) + if padding:len() > 0 then + table.insert(tokens,padding) + end + end) + return tokens +end + + +cron._split_with_strings = function(text) + -- Parse strings + local nt = find_strings(text) + local tokens = {} + for k,v in pairs(nt) do + if not ((v:sub(1,1) == v:sub(-1,-1)) and (v:match("^[\"'/]"))) then + -- Parse space-separated tokens + v:gsub("(%S*)(%s*)",function(text,padding) + table.insert(tokens,text) + if padding:len() > 0 then + table.insert(tokens,padding) + end + end) + else + -- Insert pre-parsed strings into tokens + table.insert(tokens,v) + end + end + return tokens +end + +cron.parse_token = function(text) + local token = {text} + for _,pair in pairs(token_types) do + if text:match(pair[1]) then + token.type = pair[2] + token[1] = pair[3](token[1]:match(pair[1])) + return token + end + end +end + +cron.parse_directive = function(tokens) + table.remove(tokens,1) + -- Prepare predicate chain + local argmatches = {} + local stop = nil + for k,v in pairs(tokens) do + for _,pair in pairs(predtypes) do + if v:match(pair[1]) then + -- Stop at delimiter + if pair[2] == "delimiter" then + stop = k + break + end + -- Ignore spacers - they're not predicates + if pair[2] ~= "spacer" then + table.insert(argmatches,pair[3](v:match(pair[1]))) + end + break + end + end + end + -- We use a delimiter so that command start wouldn't be ambiguous + -- Rather than defining an amount of arguments to directives, we + -- simply allow the directive to match any amount of arguments all times + if not stop then + return false, "Directive arguments should end with a : delimiter" + end + local command = table.concat(startfrom(stop+2,tokens)) + -- Return the function that matches against a predicate chain + return function(arguments) + for k,v in pairs(argmatches) do + if not v(arguments[k]) then + return false + end + end + return true, command + end,"directive" +end + +cron.parse_generic = function(tokens) + -- Parse tokens + local parsed_tokens = {} + for k,v in pairs(tokens) do + local status,token = pcall(cron.parse_token,v) + if not status then + return false,token + end + table.insert(parsed_tokens,token) + end + -- Match against a syntactic construction + for k,v in pairs(syntax) do + local matches = true + local args = {} + for pos,type in pairs(v[1]) do + -- Remove trailing spacer tokens + while parsed_tokens[pos] and parsed_tokens[pos].type == "spacer" do + table.remove(parsed_tokens,pos) + end + if not parsed_tokens[pos] then + break + end + -- Numbers are a special case because they can be matched + -- by multiple predicates + if type == "number" then + if (parsed_tokens[pos].type ~= "number") and + (not parsed_tokens[pos].type:match("^any")) then + matches = false + break + end + else + if (parsed_tokens[pos].type ~= type) then + matches = false + break + end + end + table.insert(args,parsed_tokens[pos][1]) + end + if matches then + -- Calculate cut position + local cut_pos = #v[1]*2+1 + local command = table.concat(startfrom(cut_pos,tokens)) + args[#args+1] = command + return v[3](table.unpack(args)),v[2] + end + end + return false, "Syntax doesn't match any valid construction" +end + +cron.parse_line = function(line) + local tokens = cron._split(line) + local status,first_token = pcall(cron.parse_token,tokens[1]) + if not status then + return false,first_token + end + if first_token.type == "directive" then + + return cron.parse_directive(cron._split_with_strings(line)) + -- ... + else + return cron.parse_generic(tokens) + -- ... + end +end + +cron.parse = function(text) + text:gsub("\n.-\n?$",function(line) + cron.parse_line(line) + end) +end + +return cron diff --git a/libraries/tasklib.lua b/libraries/tasklib.lua deleted file mode 100644 index 49e3a47..0000000 --- a/libraries/tasklib.lua +++ /dev/null @@ -1,38 +0,0 @@ -local class = import("classes.baseclass") -local taskhandler = class("TaskHandler") -local sqlite = import("sqlite3") --- DB format: --- --- ID (INT) | event(STR) | args (STR) | date (STR) | command (STR) --- ---------|------------|------------|------------|-------------- --- 1 | time | NULL |12 30 1 10 | ?echo today is yes day --- 2 | msg | hi |NULL | ?echo yes hello --- -local exists = function(tab,sv) - for k,v in pairs(tab) do - if v == sv then - return true - end - end - return false -end -function taskhandler:__init(dbpath) - self.db = sqlite.open(dbpath) - local query = self.db:exec("SELECT * FROM sqlite_master;") - if not exists(query.tbl_name,"tasks") then - self.db:exec([[ -CREATE TABLE tasks( - ID INTEGER PRIMARY KEY, - event STR, - args STR, - date STR, - command STR -); - ]]) - end - self.cache = {} -end - -function taskhandler:daily_cache() - self.db: - diff --git a/plugins/cron/init.lua b/plugins/cron/init.lua new file mode 100644 index 0000000..a9a8720 --- /dev/null +++ b/plugins/cron/init.lua @@ -0,0 +1,229 @@ +local pluginc = import("classes.plugin") +local command = import("classes.command") +local plugin = pluginc("cron") +local cron = import("cron") +local fake_message = import("fake_message") +local md5 = import("md5") +local events = { + timer = {}, + event = {} +} + +local exec = function(v,command) + local channel = client:getChannel(v.channel) + if not channel then + log("ERROR","Unable to retrieve timer channel: "..tostring(v.channel)) + return + end + local msg = channel:getMessage(v.id) + if not msg then + log("ERROR","Unable to retrieve timer message: "..tostring(v.id)) + return + end + command_handler:handle(fake_message(msg,{ + delete = function() end, + content = command + })) +end + +if not config.events then + config.events = { + timer = {}, + event = {message = {}} + } +end + +local event = command("event",{ + help = {embed={ + title = "Add a cron event", + description = "Description coming soon", + fields = { + {name = "Usage:",value = "event ..."}, + {name = "Perms:",value = "administrator"}, + } + }}, + perms = { + "administrator" + }, + exec = function(msg,args,opts) + local arg = table.concat(args," ") + local func,functype = cron.parse_line(arg) + if not func then + msg:reply(functype) + return false + end + local hash = md5.sumhexa(arg):sub(1,16) + if functype == "directive" then + local event_name = arg:match("^@(%w+)") + if not events.event[event_name] then events.event[event_name] = {} end + events.event[event_name][hash] = { + func, + channel = tostring(msg.channel.id), + id = tostring(msg.id), + user = tostring(msg.author.id), + type = functype + } + if not config.events.event[event_name] then config.events.event[event_name] = {} end + config.events.event[event_name][hash] = { + arg, + channel = tostring(msg.channel.id), + id = tostring(msg.id), + user = tostring(msg.author.id), + type = functype + } + else + events.timer[hash] = { + func, + channel = tostring(msg.channel.id), + id = tostring(msg.id), + user = tostring(msg.author.id), + type = functype + } + config.events.timer[hash] = { + arg, + channel = tostring(msg.channel.id), + id = tostring(msg.id), + user = tostring(msg.author.id), + type = functype + } + end + return true + end +}) +plugin:add_command(event) + +local delay = command("delay",{ + help = {embed={ + title = "Delay a command", + description = "Delay fromat is , where unit is one of the follwing:\n\"h\" - hour,\n\"m\" - minute,\n\"d\" - day,\n\"w\" - week,\n\"y\" - year", + fields = { + {name = "Usage:",value = "delay "}, + {name = "Perms:",value = "administrator"}, + } + }}, + perms = { + "administrator" + }, + exec = function(msg,args,opts) + local format = args[1] + table.remove(args,1) + local arg = os.date("%d.%m.%y %H:%M ",cron.convert_delay(format))..table.concat(args," ") + local func,functype = cron.parse_line(arg) + if not func then + msg:reply(functype) + return false + end + local hash = md5.sumhexa(arg):sub(1,16) + events.timer[hash] = { + func, + channel = tostring(msg.channel.id), + id = tostring(msg.id), + user = tostring(msg.author.id), + type = functype + } + config.events.timer[hash] = { + arg, + channel = tostring(msg.channel.id), + id = tostring(msg.id), + user = tostring(msg.author.id), + type = functype + } + return true + end +}) +plugin:add_command(delay) + +local delay = command("events",{ + help = {embed={ + title = "View your running events", + description = "nuff said.", + fields = { + {name = "Usage:",value = "events "}, + {name = "Perms:",value = "administrator"}, + } + }}, + perms = { + "administrator" + }, + args = { + "number" + }, + exec = function(msg,args,opts) + local uevents = {} + local uhashes = {} + local upto = 5*args[1] + for k,v in pairs(config.events.timer) do + if v.user == tostring(msg.author.id) then + table.insert(uevents,v) + table.insert(uhashes,k) + end + if #events == upto then + break + end + end + local stop = false + for k,v in pairs(config.events.event) do + for _,events in pairs(v) do + if v.user == tostring(msg.author.id) then + table.insert(uevents,v) + table.insert(uhashes,k) + end + if #events == upto then + stop = true + break + end + end + if stop then + break + end + end + local message = {embed = { + title = "Your events: ", + description = "", + footer = { + text = "Events "..tostring(upto-4).." - "..tostring(upto) + } + }} + for I = upto-4,upto do + if not uhashes[I] then + break + end + message.embed.description = message.embed.description.."["..uhashes[I].."] `"..uevents[I][1].."`\n" + end + msg:reply(message) + end +}) +plugin:add_command(delay) + +local timer = discordia.Clock() +timer:on("min",function() + for k,v in pairs(events.timer) do + local status,command = v[1](os.date("*t")) + if status then + exec(v,command) + if v.type == "onetime" then + events.timer[k] = nil + config.events.timer[k] = nil + end + end + end +end) + +client:on("messageCreate",function(msg) + local content = msg.content + local user = msg.author.name + for k,v in pairs(events.event.message or {}) do + local status,command = v[1]({content,user}) + if status then + exec(v,command) + end + end + for k,v in pairs(events.event.messageOnce or {}) do + local status,command = v[1]({content,user}) + events.event.messageOnce[k] = nil + config.events.event.messageOnce[k] = nil + end +end) + +timer:start(true) +return plugin