diff --git a/.gitignore b/.gitignore index d6d2cd0..e150a82 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -res/ \ No newline at end of file +res/ +data/ +bot/config.lua \ No newline at end of file diff --git a/bot/bot.lua b/bot/bot.lua index 70f5d8b..eceabc2 100644 --- a/bot/bot.lua +++ b/bot/bot.lua @@ -1,10 +1,11 @@ http = require("socket.http") https = require("ssl.https") URL = require("socket.url") -json = (loadfile "./bot/JSON.lua")() +json = (loadfile "./libs/JSON.lua")() +serpent = (loadfile "./libs/serpent.lua")() require("./bot/utils") -VERSION = 'v0.7.7' +VERSION = '0.8.2' function on_msg_receive (msg) vardump(msg) @@ -13,7 +14,6 @@ function on_msg_receive (msg) return end - update_user_stats(msg) do_action(msg) mark_read(get_receiver(msg), ok_cb, false) @@ -22,15 +22,21 @@ end function ok_cb(extra, success, result) end --- Callback to remove tmp files -function rmtmp_cb(file_path, success, result) - os.remove(file_path) +function on_binlog_replay_end () + started = 1 + -- Uncomment the line to enable cron plugins. + -- postpone (cron_plugins, false, 5.0) + -- See plugins/ping.lua as an example for cron + + _config = load_config() + + -- load plugins + plugins = {} + load_plugins() end function msg_valid(msg) - -- if msg.from.id == our_id then - -- return true - -- end + -- Dont process outgoing messages if msg.out then return false end @@ -42,6 +48,20 @@ function msg_valid(msg) end end +function do_lex(msg, text) + for name, desc in pairs(plugins) do + if (desc.lex ~= nil) then + result = desc.lex(msg, text) + if (result ~= nil) then + print ("Mutating to " .. result) + text = result + end + end + end + -- print("Text mutated to " .. text) + return text +end + -- Where magic happens function do_action(msg) local receiver = get_receiver(msg) @@ -52,19 +72,28 @@ function do_action(msg) text = '['..msg.media.type..']' end -- print("Received msg", text) + + msg.text = do_lex(msg, text) + for name, desc in pairs(plugins) do -- print("Trying module", name) for k, pattern in pairs(desc.patterns) do -- print("Trying", text, "against", pattern) matches = { string.match(text, pattern) } if matches[1] then - print(" matches",pattern) + print(" matches", pattern) if desc.run ~= nil then - result = desc.run(msg, matches) - print(" sending", result) - if (result) then - _send_msg(receiver, result) - return + -- If plugin is for privileged user + if desc.privileged and not is_sudo(msg) then + local text = 'This plugin requires privileged user' + send_msg(receiver, text, ok_cb, false) + else + result = desc.run(msg, matches) + -- print(" sending", result) + if (result) then + result = do_lex(msg, result) + _send_msg(receiver, result) + end end end end @@ -88,60 +117,51 @@ function _send_msg( destination, text) end end - -function load_config() - local f = assert(io.open('./bot/config.json', "r")) - local c = f:read "*a" - local config = json:decode(c) - if config.sh_enabled then - print ("!sh command is enabled") - for v,user in pairs(config.sudo_users) do - print("Allowed user: " .. user) - end - end - f:close() - return config +-- Save the content of _config to config.lua +function save_config( ) + serialize_to_file(_config, './data/config.lua') + print ('saved config into ./data/config.lua') end -function update_user_stats(msg) - -- Save user to _users table - local from_id = tostring(msg.from.id) - local to_id = tostring(msg.to.id) - local user_name = get_name(msg) - -- If last name is nil dont save last_name. - local user_last_name = msg.from.last_name - local user_print_name = msg.from.print_name - if _users[to_id] == nil then - _users[to_id] = {} - end - if _users[to_id][from_id] == nil then - _users[to_id][from_id] = { - name = user_name, - last_name = user_last_name, - print_name = user_print_name, - msg_num = 1 - } + +function load_config( ) + local f = io.open('./data/config.lua', "r") + -- If config.lua doesnt exists + if not f then + print ("Created new config file: data/config.lua") + create_config() else - local actual_num = _users[to_id][from_id].msg_num - _users[to_id][from_id].msg_num = actual_num + 1 - -- And update last_name - _users[to_id][from_id].last_name = user_last_name + f:close() end + local config = loadfile ("./data/config.lua")() + for v,user in pairs(config.sudo_users) do + print("Allowed user: " .. user) + end + return config end -function load_user_stats() - local f = io.open('res/users.json', "r+") - -- If file doesn't exists - if f == nil then - f = io.open('res/users.json', "w+") - f:write("{}") -- Write empty table - f:close() - return {} - else - local c = f:read "*a" - f:close() - return json:decode(c) - end +-- Create a basic config.json file and saves it. +function create_config( ) + -- A simple config with basic plugins and ourserves as priviled user + config = { + enabled_plugins = { + "9gag", + "echo", + "get", + "set", + "images", + "img_google", + "location", + "media", + "plugins", + "stats", + "time", + "version", + "youtube" }, + sudo_users = {our_id} + } + serialize_to_file(config, './data/config.lua') + print ('saved config into ./data/config.lua') end function on_our_id (id) @@ -163,22 +183,12 @@ end function on_get_difference_end () end -function on_binlog_replay_end () - started = 1 - -- Uncomment the line to enable cron plugins. - -- postpone (cron_plugins, false, 5.0) - -- See plugins/ping.lua as an example for cron -end - --- load all plugins in the plugins/ directory +-- Enable plugins in config.json function load_plugins() - for k, v in pairs(scandir("plugins")) do - -- Load only lua files - if (v:match(".lua$")) then - print("Loading plugin", v) - t = loadfile("plugins/" .. v)() - table.insert(plugins, t) - end + for k, v in pairs(_config.enabled_plugins) do + print("Loading plugin", v) + t = loadfile("plugins/"..v..'.lua')() + table.insert(plugins, t) end end @@ -199,10 +209,3 @@ end -- Start and load values our_id = 0 now = os.time() - -config = load_config() -_users = load_user_stats() - --- load plugins -plugins = {} -load_plugins() \ No newline at end of file diff --git a/bot/config.json b/bot/config.json deleted file mode 100644 index feff6fc..0000000 --- a/bot/config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "rmtmp_delay": 20, - "google_api_key": "", - "log_file": "/var/www/html/log.txt", - "sh_enabled": false, - "sudo_users": [ 0, 1 ], - "twitter": { - "access_token": "", - "access_token_secret": "", - "consumer_key": "", - "consumer_secret": "" - } -} diff --git a/bot/utils.lua b/bot/utils.lua index 3f774b8..506727b 100644 --- a/bot/utils.lua +++ b/bot/utils.lua @@ -61,12 +61,18 @@ function download_to_file( url , noremove ) file:close() if noremove == nil then - postpone(rmtmp_cb, file_path, config.rmtmp_delay) + print(file_path.."will be removed in 20 seconds") + postpone(rmtmp_cb, file_path, 20) end return file_path end +-- Callback to remove a file +function rmtmp_cb(file_path, success, result) + os.remove(file_path) +end + function vardump(value, depth, key) local linePrefix = "" local spaces = "" @@ -125,7 +131,7 @@ end function is_sudo(msg) local var = false -- Check users id in config - for v,user in pairs(config.sudo_users) do + for v,user in pairs(_config.sudo_users) do if user == msg.from.id then var = true end @@ -133,10 +139,50 @@ function is_sudo(msg) return var end +-- Returns the name of the sender function get_name(msg) local name = msg.from.first_name if name == nil then name = msg.from.id end return name +end + +-- Returns at table of lua files inside plugins +function plugins_names( ) + local files = {} + for k, v in pairs(scandir("plugins")) do + -- Ends with .lua + if (v:match(".lua$")) then + table.insert(files, v) + end + end + return files +end + +-- Function name explains what it does. +function file_exists(name) + local f = io.open(name,"r") + if f ~= nil then + io.close(f) + return true + else + return false + end +end + +-- Save into file the data serialized for lua. +function serialize_to_file(data, file) + file = io.open(file, 'w+') + local serialized = serpent.block(data, { + comment = false, + name = "_" + }) + file:write(serialized) + file:close() +end + +-- Retruns true if the string is empty +function string:isempty() + return self == nil or self == '' end \ No newline at end of file diff --git a/res/.gitkeep b/data/.gitkeep similarity index 100% rename from res/.gitkeep rename to data/.gitkeep diff --git a/launch.sh b/launch.sh index 1057d6b..09399ab 100755 --- a/launch.sh +++ b/launch.sh @@ -14,4 +14,4 @@ if [ ! -f ./tg/bin/telegram-cli ]; then exit fi -./tg/bin/telegram-cli -k tg/tg-server.pub -s ./bot/bot.lua +./tg/bin/telegram-cli -k tg/tg-server.pub -s ./bot/bot.lua -W -l 1 diff --git a/bot/JSON.lua b/libs/JSON.lua similarity index 79% rename from bot/JSON.lua rename to libs/JSON.lua index 8723771..5f11425 100644 --- a/bot/JSON.lua +++ b/libs/JSON.lua @@ -14,13 +14,13 @@ -- the web-page links above, and the 'AUTHOR_NOTE' string below are -- maintained. Enjoy. -- -local VERSION = 20140920.13 -- version history at end of file -local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20140920.13 ]-" +local VERSION = 20141223.14 -- version history at end of file +local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20141223.14 ]-" -- -- The 'AUTHOR_NOTE' variable exists so that information about the source --- of the package is maintained even in compiled versions. It's included in --- OBJDEF mostly to quiet warnings about unused variables. +-- of the package is maintained even in compiled versions. It's also +-- included in OBJDEF below mostly to quiet warnings about unused variables. -- local OBJDEF = { VERSION = VERSION, @@ -33,7 +33,7 @@ local OBJDEF = { -- http://www.json.org/ -- -- --- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines -- -- local lua_value = JSON:decode(raw_json_text) -- @@ -41,9 +41,11 @@ local OBJDEF = { -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability -- -- --- DECODING -- --- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- DECODING (from a JSON string to a Lua table) +-- +-- +-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines -- -- local lua_value = JSON:decode(raw_json_text) -- @@ -58,22 +60,27 @@ local OBJDEF = { -- { "Larry", "Curly", "Moe" } -- -- --- The encode and decode routines accept an optional second argument, "etc", which is not used --- during encoding or decoding, but upon error is passed along to error handlers. It can be of any --- type (including nil). +-- The encode and decode routines accept an optional second argument, +-- "etc", which is not used during encoding or decoding, but upon error +-- is passed along to error handlers. It can be of any type (including nil). +-- +-- +-- +-- ERROR HANDLING -- -- With most errors during decoding, this code calls -- -- JSON:onDecodeError(message, text, location, etc) -- --- with a message about the error, and if known, the JSON text being parsed and the byte count --- where the problem was discovered. You can replace the default JSON:onDecodeError() with your --- own function. +-- with a message about the error, and if known, the JSON text being +-- parsed and the byte count where the problem was discovered. You can +-- replace the default JSON:onDecodeError() with your own function. -- --- The default onDecodeError() merely augments the message with data about the text and the --- location if known (and if a second 'etc' argument had been provided to decode(), its value is --- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's --- built-in assert(), and can also be overridden. +-- The default onDecodeError() merely augments the message with data +-- about the text and the location if known (and if a second 'etc' +-- argument had been provided to decode(), its value is tacked onto the +-- message as well), and then calls JSON.assert(), which itself defaults +-- to Lua's built-in assert(), and can also be overridden. -- -- For example, in an Adobe Lightroom plugin, you might use something like -- @@ -95,9 +102,10 @@ local OBJDEF = { -- -- JSON:onDecodeOfHTMLError(message, text, nil, etc) -- --- The use of the fourth 'etc' argument allows stronger coordination between decoding and error --- reporting, especially when you provide your own error-handling routines. Continuing with the --- the Adobe Lightroom plugin example: +-- The use of the fourth 'etc' argument allows stronger coordination +-- between decoding and error reporting, especially when you provide your +-- own error-handling routines. Continuing with the the Adobe Lightroom +-- plugin example: -- -- function JSON:onDecodeError(message, text, location, etc) -- local note = "Internal Error: invalid JSON data" @@ -121,42 +129,136 @@ local OBJDEF = { -- -- -- - +-- -- DECODING AND STRICT TYPES -- --- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally --- possible to tell which a JSON type a particular Lua table was derived from, or guarantee --- decode-encode round-trip equivalency. +-- Because both JSON objects and JSON arrays are converted to Lua tables, +-- it's not normally possible to tell which original JSON type a +-- particular Lua table was derived from, or guarantee decode-encode +-- round-trip equivalency. -- -- However, if you enable strictTypes, e.g. -- --- JSON = (loadfile "JSON.lua")() --load the routines +-- JSON = assert(loadfile "JSON.lua")() --load the routines -- JSON.strictTypes = true -- --- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua --- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type. +-- then the Lua table resulting from the decoding of a JSON object or +-- JSON array is marked via Lua metatable, so that when re-encoded with +-- JSON:encode() it ends up as the appropriate JSON type. -- --- (This is not the default because other routines may not work well with tables that have a --- metatable set, for example, Lightroom API calls.) +-- (This is not the default because other routines may not work well with +-- tables that have a metatable set, for example, Lightroom API calls.) -- -- --- ENCODING +-- ENCODING (from a lua table to a JSON string) -- --- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines -- -- local raw_json_text = JSON:encode(lua_table_or_value) -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability - +-- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false }) +-- -- On error during encoding, this code calls: -- --- JSON:onEncodeError(message, etc) +-- JSON:onEncodeError(message, etc) -- -- which you can override in your local JSON object. -- --- If the Lua table contains both string and numeric keys, it fits neither JSON's --- idea of an object, nor its idea of an array. To get around this, when any string --- key exists (or when non-positive numeric keys exist), numeric keys are converted --- to strings. +-- The 'etc' in the error call is the second argument to encode() +-- and encode_pretty(), or nil if it wasn't provided. +-- +-- +-- PRETTY-PRINTING +-- +-- An optional third argument, a table of options, allows a bit of +-- configuration about how the encoding takes place: +-- +-- pretty = JSON:encode(val, etc, { +-- pretty = true, -- if false, no other options matter +-- indent = " ", -- this provides for a three-space indent per nesting level +-- align_keys = false, -- see below +-- }) +-- +-- encode() and encode_pretty() are identical except that encode_pretty() +-- provides a default options table if none given in the call: +-- +-- { pretty = true, align_keys = false, indent = " " } +-- +-- For example, if +-- +-- JSON:encode(data) +-- +-- produces: +-- +-- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} +-- +-- then +-- +-- JSON:encode_pretty(data) +-- +-- produces: +-- +-- { +-- "city": "Kyoto", +-- "climate": { +-- "avg_temp": 16, +-- "humidity": "high", +-- "snowfall": "minimal" +-- }, +-- "country": "Japan", +-- "wards": 11 +-- } +-- +-- The following three lines return identical results: +-- JSON:encode_pretty(data) +-- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " }) +-- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " }) +-- +-- An example of setting your own indent string: +-- +-- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " }) +-- +-- produces: +-- +-- { +-- | "city": "Kyoto", +-- | "climate": { +-- | | "avg_temp": 16, +-- | | "humidity": "high", +-- | | "snowfall": "minimal" +-- | }, +-- | "country": "Japan", +-- | "wards": 11 +-- } +-- +-- An example of setting align_keys to true: +-- +-- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true }) +-- +-- produces: +-- +-- { +-- "city": "Kyoto", +-- "climate": { +-- "avg_temp": 16, +-- "humidity": "high", +-- "snowfall": "minimal" +-- }, +-- "country": "Japan", +-- "wards": 11 +-- } +-- +-- which I must admit is kinda ugly, sorry. This was the default for +-- encode_pretty() prior to version 20141223.14. +-- +-- +-- AMBIGUOUS SITUATIONS DURING THE ENCODING +-- +-- During the encode, if a Lua table being encoded contains both string +-- and numeric keys, it fits neither JSON's idea of an object, nor its +-- idea of an array. To get around this, when any string key exists (or +-- when non-positive numeric keys exist), numeric keys are converted to +-- strings. -- -- For example, -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) @@ -165,6 +267,9 @@ local OBJDEF = { -- -- To prohibit this conversion and instead make it an error condition, set -- JSON.noKeyConversion = true +-- + + -- @@ -181,6 +286,9 @@ local OBJDEF = { -- --------------------------------------------------------------------------- +local default_pretty_indent = " " +local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } + local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject @@ -692,8 +800,13 @@ end -- -- Encode -- +-- 'options' is nil, or a table with possible keys: +-- pretty -- if true, return a pretty-printed version +-- indent -- a string (usually of spaces) used to indent each nested level +-- align_keys -- if true, align all the keys when formatting a table +-- local encode_value -- must predeclare because it calls itself -function encode_value(self, value, parents, etc, indent) -- non-nil indent means pretty-printing +function encode_value(self, value, parents, etc, options, indent) if value == nil then return 'null' @@ -739,6 +852,13 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means -- local T = value + if type(options) ~= 'table' then + options = {} + end + if type(indent) ~= 'string' then + indent = "" + end + if parents[T] then self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) else @@ -754,13 +874,13 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means -- local ITEMS = { } for i = 1, maximum_number_key do - table.insert(ITEMS, encode_value(self, T[i], parents, etc, indent)) + table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) end - if indent then + if options.pretty then result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" else - result_value = "[" .. table.concat(ITEMS, ",") .. "]" + result_value = "[" .. table.concat(ITEMS, ",") .. "]" end elseif object_keys then @@ -769,22 +889,24 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means -- local TT = map or T - if indent then + if options.pretty then local KEYS = { } local max_key_length = 0 for _, key in ipairs(object_keys) do - local encoded = encode_value(self, tostring(key), parents, etc, "") - max_key_length = math.max(max_key_length, #encoded) + local encoded = encode_value(self, tostring(key), parents, etc, options, indent) + if options.align_keys then + max_key_length = math.max(max_key_length, #encoded) + end table.insert(KEYS, encoded) end - local key_indent = indent .. " " - local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4) + local key_indent = indent .. tostring(options.indent or "") + local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "") local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" local COMBINED_PARTS = { } for i, key in ipairs(object_keys) do - local encoded_val = encode_value(self, TT[key], parents, etc, subtable_indent) + local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) end result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" @@ -793,8 +915,8 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means local PARTS = { } for _, key in ipairs(object_keys) do - local encoded_val = encode_value(self, TT[key], parents, etc, indent) - local encoded_key = encode_value(self, tostring(key), parents, etc, indent) + local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) + local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent) table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) end result_value = "{" .. table.concat(PARTS, ",") .. "}" @@ -813,18 +935,18 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means end -function OBJDEF:encode(value, etc) +function OBJDEF:encode(value, etc, options) if type(self) ~= 'table' or self.__index ~= OBJDEF then OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) end - return encode_value(self, value, {}, etc, nil) + return encode_value(self, value, {}, etc, options or nil) end -function OBJDEF:encode_pretty(value, etc) +function OBJDEF:encode_pretty(value, etc, options) if type(self) ~= 'table' or self.__index ~= OBJDEF then OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) end - return encode_value(self, value, {}, etc, "") + return encode_value(self, value, {}, etc, options or default_pretty_options) end function OBJDEF.__tostring() @@ -850,6 +972,16 @@ return OBJDEF:new() -- -- Version history: -- +-- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really +-- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines +-- more flexible, and changed the default encode_pretty() to be more generally useful. +-- +-- Added a third 'options' argument to the encode() and encode_pretty() routines, to control +-- how the encoding takes place. +-- +-- Updated docs to add assert() call to the loadfile() line, just as good practice so that +-- if there is a problem loading JSON.lua, the appropriate error message will percolate up. +-- -- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string, -- so that the source of the package, and its version number, are visible in compiled copies. -- diff --git a/libs/serpent.lua b/libs/serpent.lua new file mode 100644 index 0000000..dd08b7a --- /dev/null +++ b/libs/serpent.lua @@ -0,0 +1,128 @@ +local n, v = "serpent", 0.272 -- (C) 2012-13 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local keyword, globals, G = {}, {}, (_G or _ENV) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end)) end + local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..maxn.."d"):format(d) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) end + if type(mt) == 'table' and (mt.__serialize or mt.__tostring) then -- knows how to serialize itself + seen[t] = insref or spath + if mt.__serialize then t = mt.__serialize(t) else t = tostring(t) end + ttype = type(t) end -- new value falls through to be serialized + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('max', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1) + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + local ok, res = pcall(string.dump, t) + local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or + "((loadstring or load)("..safestr(res)..",'@serialized'))")..comment(t, level)) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local f, res = (loadstring or load)('return '..data) + if not f then f, res = (loadstring or load)(data) end + if not f then return f, res end + if opts and opts.safe == false then return pcall(f) end + + local count, thread = 0, coroutine.running() + local h, m, c = debug.gethook(thread) + debug.sethook(function (e, l) count = count + 1 + if count >= 3 then error("cannot call functions") end + end, "c") + local res = {pcall(f)} + count = 0 -- set again, otherwise it's tripped on the next sethook + debug.sethook(thread, h, m, c) + return (table.unpack or unpack)(res) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } diff --git a/plugins/get.lua b/plugins/get.lua index d9bad27..c254dc5 100644 --- a/plugins/get.lua +++ b/plugins/get.lua @@ -1,13 +1,30 @@ -local f = io.open('./res/values.json', "r+") -if f == nil then - f = io.open('./res/values.json', "w+") - f:write("{}") -- Write empty table - f:close() - _values = {} -else - local c = f:read "*a" - f:close() - _values = json:decode(c) +local _file_values = './data/values.lua' + +function read_file_values( ) + local f = io.open(_file_values, "r+") + -- If file doesn't exists + if f == nil then + -- Create a new empty table + print ('Created value file '.._file_values) + serialize_to_file({}, _file_values) + else + print ('Stats loaded: '.._file_values) + f:close() + end + return loadfile (_file_values)() +end + +_values = read_file_values() + +function fetch_value(chat, value_name) + if (_values[chat] == nil) then + return nil + end + if (value_name == nil ) then + return nil + end + local value = _values[chat][value_name] + return value end function get_value(chat, value_name) @@ -40,11 +57,27 @@ function run(msg, matches) return get_value(chat_id, matches[1]) end +function lex(msg, text) + local chat_id = tostring(msg.to.id) + local s, e = text:find("%$%a+") + if (s == nil) then + return nil + end + local var = text:sub(s + 1, e) + local value = fetch_value(chat_id, var) + if (value == nil) then + value = "(unknown value " .. var .. ")" + end + return text:sub(0, s - 1) .. value .. text:sub(e + 1) +end + return { description = "retrieves variables saved with !set", usage = "!get (value_name)", patterns = { "^!get (%a+)$", "^!get$"}, - run = run + run = run, + lex = lex } + diff --git a/plugins/invite.lua b/plugins/invite.lua new file mode 100644 index 0000000..b47e848 --- /dev/null +++ b/plugins/invite.lua @@ -0,0 +1,42 @@ +-- Invite other user to the chat group. +-- Use !invite name User_name or !invite id id_number +-- The User_name is the print_name (there are no spaces but _) + +do + +local function run(msg, matches) + -- User submitted a user name + if matches[1] == "name" then + user = matches[2] + user = string.gsub(user," ","_") + end + -- User submitted an id + if matches[1] == "id" then + user = matches[2] + user = 'user#id'..user + end + -- The message must come from a chat group + if msg.to.type == 'chat' then + chat = 'chat#id'..msg.to.id + else + return 'This isnt a chat group!' + end + print ("Trying to add: "..user.." to "..chat) + status = chat_add_user (chat, user, ok_cb, false) + if not status then + return "An error happened" + end + return "Added user: "..user.." to "..chat +end + +return { + description = "Invite other user to the chat group", + usage = "!invite name [user_name], !invite id [user_id]", + patterns = { + "^!invite (name) (.*)$", + "^!invite (id) (%d+)$" + }, + run = run +} + +end \ No newline at end of file diff --git a/plugins/location.lua b/plugins/location.lua index 7990a15..f6d876b 100644 --- a/plugins/location.lua +++ b/plugins/location.lua @@ -7,7 +7,9 @@ -- Globals -- If you have a google api key for the geocoding/timezone api -api_key = config.google_api_key or nil + +api_key = nil + base_api = "https://maps.googleapis.com/maps/api" function delay_s(delay) diff --git a/plugins/plugins.lua b/plugins/plugins.lua new file mode 100644 index 0000000..af729a7 --- /dev/null +++ b/plugins/plugins.lua @@ -0,0 +1,113 @@ +function enable_plugin( filename ) + -- Check if plugin is enabled + if plugin_enabled(filename) then + return 'Plugin '..filename..' is enabled' + end + -- Checks if plugin exists + if plugin_exists(filename) then + -- Add to the config table + table.insert(_config.enabled_plugins, filename) + save_config() + -- Reload the plugins + return reload_plugins( ) + else + return 'Plugin '..filename..' does not exists' + end +end + +function disable_plugin( name ) + -- Check if plugins exists + if not plugin_exists(name) then + return 'Plugin '..name..' does not exists' + end + local k = plugin_enabled(name) + -- Check if plugin is enabled + if not k then + return 'Plugin '..name..' not enabled' + end + -- Disable and reload + table.remove(_config.enabled_plugins, k) + save_config( ) + return reload_plugins(true) +end + +function reload_plugins( ) + plugins = {} + load_plugins() + return list_plugins(true) +end + +-- Retruns the key (index) in the config.enabled_plugins table +function plugin_enabled( name ) + for k,v in pairs(_config.enabled_plugins) do + if name == v then + return k + end + end + -- If not found + return false +end + +-- Returns true if file exists in plugins folder +function plugin_exists( name ) + for k,v in pairs(plugins_names()) do + if name..'.lua' == v then + return true + end + end + return false +end + +function list_plugins(only_enabled) + local text = '' + for k, v in pairs( plugins_names( )) do + -- ✔ enabled, ❌ disabled + local status = '❌' + -- Check if is enabled + for k2, v2 in pairs(_config.enabled_plugins) do + if v == v2..'.lua' then + status = '✔' + end + end + if not only_enabled or status == '✔' then + -- get the name + v = string.match (v, "(.*)%.lua") + text = text..v..' '..status..'\n' + end + end + return text +end + +function run(msg, matches) + -- Show the available plugins + if matches[1] == '!plugins' then + return list_plugins() + end + -- Enable a plugin + if matches[1] == 'enable' then + print("enable: "..matches[2]) + return enable_plugin(matches[2]) + end + -- Disable a plugin + if matches[1] == 'disable' then + print("disable: "..matches[2]) + return disable_plugin(matches[2]) + end + -- Reload all the plugins! + if matches[1] == 'reload' then + return reload_plugins(true) + end +end + +return { + description = "Enables, disables and reloads plugins", + usage = "!plugins, !plugins enable [plugin], !plugins disable [plugin], !plugins reload", + patterns = { + "^!plugins$", + "^!plugins? (enable) (.*)$", + "^!plugins? (disable) (.*)$", + "^!plugins? (reload)$" + }, + run = run, + privileged = true +} \ No newline at end of file diff --git a/plugins/reload.lua b/plugins/reload.lua deleted file mode 100644 index f6bfaef..0000000 --- a/plugins/reload.lua +++ /dev/null @@ -1,14 +0,0 @@ - -function run(msg, matches) - plugins = {} - load_plugins() - return 'Plugins reloaded' -end - -return { - description = "Reloads bot plugins", - usage = "!reload", - patterns = {"^!reload$"}, - run = run -} - diff --git a/plugins/set.lua b/plugins/set.lua index c706f98..d971b9c 100644 --- a/plugins/set.lua +++ b/plugins/set.lua @@ -1,3 +1,5 @@ +local _file_values = './data/values.lua' + function save_value(chat, text ) var_name, var_value = string.match(text, "!set (%a+) (.+)") if (var_name == nil or var_value == nil) then @@ -8,10 +10,8 @@ function save_value(chat, text ) end _values[chat][var_name] = var_value - local json_text = json:encode_pretty(_values) - file = io.open ("./res/values.json", "w+") - file:write(json_text) - file:close() + -- Save values to file + serialize_to_file(_values, _file_values) return "Saved "..var_name.." = "..var_value end diff --git a/plugins/stats.lua b/plugins/stats.lua index 2986ddb..e9fcf7e 100644 --- a/plugins/stats.lua +++ b/plugins/stats.lua @@ -1,28 +1,98 @@ -function run(msg, matches) - vardump(_users) - -- Save stats to file - local json_users = json:encode_pretty(_users) - vardump(json_users) - file_users = io.open ("./res/users.json", "w") - file_users:write(json_users) - file_users:close() +-- Saves the number of messages from a user +-- Can check the number of messages with !stats +do + +local socket = require('socket') +local _file_stats = './data/stats.lua' +local _stats + +function update_user_stats(msg) + -- Save user to stats table + local from_id = tostring(msg.from.id) + local to_id = tostring(msg.to.id) + local user_name = get_name(msg) + print ('New message from '..user_name..'['..from_id..']'..' to '..to_id) + -- If last name is nil dont save last_name. + local user_last_name = msg.from.last_name + local user_print_name = msg.from.print_name + if _stats[to_id] == nil then + print ('New stats key to_id: '..to_id) + _stats[to_id] = {} + end + if _stats[to_id][from_id] == nil then + print ('New stats key from_id: '..to_id) + _stats[to_id][from_id] = { + name = user_name, + last_name = user_last_name, + print_name = user_print_name, + msg_num = 1 + } + else + print ('Updated '..to_id..' '..from_id) + local actual_num = _stats[to_id][from_id].msg_num + _stats[to_id][from_id].msg_num = actual_num + 1 + -- And update last_name + _stats[to_id][from_id].last_name = user_last_name + end +end + +function read_file_stats( ) + local f = io.open(_file_stats, "r+") + -- If file doesn't exists + if f == nil then + -- Create a new empty table + print ('Created user stats file '.._file_stats) + serialize_to_file({}, _file_stats) + else + print ('Stats loaded: '.._file_stats) + f:close() + end + return loadfile (_file_stats)() +end + + +local function save_stats() + -- Save stats to file + serialize_to_file(_stats, _file_stats) +end + +local function get_stats_status( msg ) + -- vardump(stats) local text = "" local to_id = tostring(msg.to.id) - for id, user in pairs(_users[to_id]) do + for id, user in pairs(_stats[to_id]) do if user.last_name == nil then text = text..user.name.." ["..id.."]: "..user.msg_num.."\n" else text = text..user.name.." "..user.last_name.." ["..id.."]: "..user.msg_num.."\n" end end + print("usuarios: "..text) return text end +local function run(msg, matches) + if matches[1] == "stats" then -- Hack + return get_stats_status(msg) + else + print ("update stats") + update_user_stats(msg) + save_stats() + end +end + +_stats = read_file_stats() + return { description = "Numer of messages by user", usage = "!stats", - patterns = {"^!stats"}, + patterns = { + ".*", + "^!(stats)" + }, run = run -} \ No newline at end of file +} + +end \ No newline at end of file diff --git a/plugins/time.lua b/plugins/time.lua index 462a2e2..0bf0ed9 100644 --- a/plugins/time.lua +++ b/plugins/time.lua @@ -5,7 +5,8 @@ -- Globals -- If you have a google api key for the geocoding/timezone api -api_key = config.google_api_key or nil +api_key = nil + base_api = "https://maps.googleapis.com/maps/api" dateFormat = "%A %d %B - %H:%M:%S" diff --git a/plugins/twitter.lua b/plugins/twitter.lua index e1cc3b0..725873a 100644 --- a/plugins/twitter.lua +++ b/plugins/twitter.lua @@ -1,9 +1,9 @@ local OAuth = require "OAuth" -local consumer_key = config.twitter.consumer_key -local consumer_secret = config.twitter.consumer_secret -local access_token = config.twitter.access_token -local access_token_secret = config.twitter.access_token_secret +local consumer_key = "" +local consumer_secret = "" +local access_token = "" +local access_token_secret = "" local client = OAuth.new(consumer_key, consumer_secret, { RequestToken = "https://api.twitter.com/oauth/request_token", @@ -16,16 +16,23 @@ local client = OAuth.new(consumer_key, consumer_secret, { function run(msg, matches) + if consumer_key:isempty() then + return "Twitter Consumer Key is empty, write it in plugins/twitter.lua" + end + if consumer_secret:isempty() then + return "Twitter Consumer Secret is empty, write it in plugins/twitter.lua" + end + if access_token:isempty() then + return "Twitter Access Token is empty, write it in plugins/twitter.lua" + end + if access_token_secret:isempty() then + return "Twitter Access Token Secret is empty, write it in plugins/twitter.lua" + end + local twitter_url = "https://api.twitter.com/1.1/statuses/show/" .. matches[1] .. ".json" - - print(twitter_url) - local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", twitter_url) - print(response_body) local response = json:decode(response_body) - print("response = ", response) - local header = "Tweet from " .. response.user.name .. " (@" .. response.user.screen_name .. ")\n" local text = response.text @@ -65,6 +72,4 @@ return { usage = "", patterns = {"https://twitter.com/[^/]+/status/([0-9]+)"}, run = run -} - - +} \ No newline at end of file diff --git a/plugins/twitter_send.lua b/plugins/twitter_send.lua index d982247..cf243f7 100644 --- a/plugins/twitter_send.lua +++ b/plugins/twitter_send.lua @@ -1,31 +1,45 @@ local OAuth = require "OAuth" -local consumer_key = config.twitter.consumer_key -local consumer_secret = config.twitter.consumer_secret -local access_token = config.twitter.access_token -local access_token_secret = config.twitter.access_token_secret +local consumer_key = "" +local consumer_secret = "" +local access_token = "" +local access_token_secret = "" local client = OAuth.new(consumer_key, consumer_secret, { RequestToken = "https://api.twitter.com/oauth/request_token", AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, AccessToken = "https://api.twitter.com/oauth/access_token" -}, { + }, { OAuthToken = access_token, OAuthTokenSecret = access_token_secret }) function run(msg, matches) + if consumer_key:isempty() then + return "Twitter Consumer Key is empty, write it in plugins/twitter.lua" + end + if consumer_secret:isempty() then + return "Twitter Consumer Secret is empty, write it in plugins/twitter.lua" + end + if access_token:isempty() then + return "Twitter Access Token is empty, write it in plugins/twitter.lua" + end + if access_token_secret:isempty() then + return "Twitter Access Token Secret is empty, write it in plugins/twitter.lua" + end + if not is_sudo(msg) then return "You aren't allowed to send tweets" end - local response_code, response_headers, response_status_line, response_body = + + local response_code, response_headers, response_status_line, response_body = client:PerformRequest("POST", "https://api.twitter.com/1.1/statuses/update.json", { - status = matches[1] - }) + status = matches[1] + }) if response_code ~= 200 then - return "Error: "..response_code + return "Error: "..response_code end - return "Tweet sended" + return "Tweet sended" end return {