Merge pull request #45 from yagop/develop
New version 0.8.2! Changes about config and plugins.
This commit is contained in:
commit
0cc9ea0041
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
res/
|
res/
|
||||||
|
data/
|
||||||
|
bot/config.lua
|
173
bot/bot.lua
173
bot/bot.lua
@ -1,10 +1,11 @@
|
|||||||
http = require("socket.http")
|
http = require("socket.http")
|
||||||
https = require("ssl.https")
|
https = require("ssl.https")
|
||||||
URL = require("socket.url")
|
URL = require("socket.url")
|
||||||
json = (loadfile "./bot/JSON.lua")()
|
json = (loadfile "./libs/JSON.lua")()
|
||||||
|
serpent = (loadfile "./libs/serpent.lua")()
|
||||||
require("./bot/utils")
|
require("./bot/utils")
|
||||||
|
|
||||||
VERSION = 'v0.7.7'
|
VERSION = '0.8.2'
|
||||||
|
|
||||||
function on_msg_receive (msg)
|
function on_msg_receive (msg)
|
||||||
vardump(msg)
|
vardump(msg)
|
||||||
@ -13,7 +14,6 @@ function on_msg_receive (msg)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
update_user_stats(msg)
|
|
||||||
do_action(msg)
|
do_action(msg)
|
||||||
|
|
||||||
mark_read(get_receiver(msg), ok_cb, false)
|
mark_read(get_receiver(msg), ok_cb, false)
|
||||||
@ -22,15 +22,21 @@ end
|
|||||||
function ok_cb(extra, success, result)
|
function ok_cb(extra, success, result)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Callback to remove tmp files
|
function on_binlog_replay_end ()
|
||||||
function rmtmp_cb(file_path, success, result)
|
started = 1
|
||||||
os.remove(file_path)
|
-- 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
|
end
|
||||||
|
|
||||||
function msg_valid(msg)
|
function msg_valid(msg)
|
||||||
-- if msg.from.id == our_id then
|
-- Dont process outgoing messages
|
||||||
-- return true
|
|
||||||
-- end
|
|
||||||
if msg.out then
|
if msg.out then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -42,6 +48,20 @@ function msg_valid(msg)
|
|||||||
end
|
end
|
||||||
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
|
-- Where magic happens
|
||||||
function do_action(msg)
|
function do_action(msg)
|
||||||
local receiver = get_receiver(msg)
|
local receiver = get_receiver(msg)
|
||||||
@ -52,19 +72,28 @@ function do_action(msg)
|
|||||||
text = '['..msg.media.type..']'
|
text = '['..msg.media.type..']'
|
||||||
end
|
end
|
||||||
-- print("Received msg", text)
|
-- print("Received msg", text)
|
||||||
|
|
||||||
|
msg.text = do_lex(msg, text)
|
||||||
|
|
||||||
for name, desc in pairs(plugins) do
|
for name, desc in pairs(plugins) do
|
||||||
-- print("Trying module", name)
|
-- print("Trying module", name)
|
||||||
for k, pattern in pairs(desc.patterns) do
|
for k, pattern in pairs(desc.patterns) do
|
||||||
-- print("Trying", text, "against", pattern)
|
-- print("Trying", text, "against", pattern)
|
||||||
matches = { string.match(text, pattern) }
|
matches = { string.match(text, pattern) }
|
||||||
if matches[1] then
|
if matches[1] then
|
||||||
print(" matches",pattern)
|
print(" matches", pattern)
|
||||||
if desc.run ~= nil then
|
if desc.run ~= nil then
|
||||||
result = desc.run(msg, matches)
|
-- If plugin is for privileged user
|
||||||
print(" sending", result)
|
if desc.privileged and not is_sudo(msg) then
|
||||||
if (result) then
|
local text = 'This plugin requires privileged user'
|
||||||
_send_msg(receiver, result)
|
send_msg(receiver, text, ok_cb, false)
|
||||||
return
|
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
|
end
|
||||||
end
|
end
|
||||||
@ -88,60 +117,51 @@ function _send_msg( destination, text)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Save the content of _config to config.lua
|
||||||
function load_config()
|
function save_config( )
|
||||||
local f = assert(io.open('./bot/config.json', "r"))
|
serialize_to_file(_config, './data/config.lua')
|
||||||
local c = f:read "*a"
|
print ('saved config into ./data/config.lua')
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function update_user_stats(msg)
|
|
||||||
-- Save user to _users table
|
function load_config( )
|
||||||
local from_id = tostring(msg.from.id)
|
local f = io.open('./data/config.lua', "r")
|
||||||
local to_id = tostring(msg.to.id)
|
-- If config.lua doesnt exists
|
||||||
local user_name = get_name(msg)
|
if not f then
|
||||||
-- If last name is nil dont save last_name.
|
print ("Created new config file: data/config.lua")
|
||||||
local user_last_name = msg.from.last_name
|
create_config()
|
||||||
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
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
local actual_num = _users[to_id][from_id].msg_num
|
f:close()
|
||||||
_users[to_id][from_id].msg_num = actual_num + 1
|
|
||||||
-- And update last_name
|
|
||||||
_users[to_id][from_id].last_name = user_last_name
|
|
||||||
end
|
end
|
||||||
|
local config = loadfile ("./data/config.lua")()
|
||||||
|
for v,user in pairs(config.sudo_users) do
|
||||||
|
print("Allowed user: " .. user)
|
||||||
|
end
|
||||||
|
return config
|
||||||
end
|
end
|
||||||
|
|
||||||
function load_user_stats()
|
-- Create a basic config.json file and saves it.
|
||||||
local f = io.open('res/users.json', "r+")
|
function create_config( )
|
||||||
-- If file doesn't exists
|
-- A simple config with basic plugins and ourserves as priviled user
|
||||||
if f == nil then
|
config = {
|
||||||
f = io.open('res/users.json', "w+")
|
enabled_plugins = {
|
||||||
f:write("{}") -- Write empty table
|
"9gag",
|
||||||
f:close()
|
"echo",
|
||||||
return {}
|
"get",
|
||||||
else
|
"set",
|
||||||
local c = f:read "*a"
|
"images",
|
||||||
f:close()
|
"img_google",
|
||||||
return json:decode(c)
|
"location",
|
||||||
end
|
"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
|
end
|
||||||
|
|
||||||
function on_our_id (id)
|
function on_our_id (id)
|
||||||
@ -163,22 +183,12 @@ end
|
|||||||
function on_get_difference_end ()
|
function on_get_difference_end ()
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_binlog_replay_end ()
|
-- Enable plugins in config.json
|
||||||
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
|
|
||||||
function load_plugins()
|
function load_plugins()
|
||||||
for k, v in pairs(scandir("plugins")) do
|
for k, v in pairs(_config.enabled_plugins) do
|
||||||
-- Load only lua files
|
print("Loading plugin", v)
|
||||||
if (v:match(".lua$")) then
|
t = loadfile("plugins/"..v..'.lua')()
|
||||||
print("Loading plugin", v)
|
table.insert(plugins, t)
|
||||||
t = loadfile("plugins/" .. v)()
|
|
||||||
table.insert(plugins, t)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -199,10 +209,3 @@ end
|
|||||||
-- Start and load values
|
-- Start and load values
|
||||||
our_id = 0
|
our_id = 0
|
||||||
now = os.time()
|
now = os.time()
|
||||||
|
|
||||||
config = load_config()
|
|
||||||
_users = load_user_stats()
|
|
||||||
|
|
||||||
-- load plugins
|
|
||||||
plugins = {}
|
|
||||||
load_plugins()
|
|
@ -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": ""
|
|
||||||
}
|
|
||||||
}
|
|
@ -61,12 +61,18 @@ function download_to_file( url , noremove )
|
|||||||
file:close()
|
file:close()
|
||||||
|
|
||||||
if noremove == nil then
|
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
|
end
|
||||||
|
|
||||||
return file_path
|
return file_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Callback to remove a file
|
||||||
|
function rmtmp_cb(file_path, success, result)
|
||||||
|
os.remove(file_path)
|
||||||
|
end
|
||||||
|
|
||||||
function vardump(value, depth, key)
|
function vardump(value, depth, key)
|
||||||
local linePrefix = ""
|
local linePrefix = ""
|
||||||
local spaces = ""
|
local spaces = ""
|
||||||
@ -125,7 +131,7 @@ end
|
|||||||
function is_sudo(msg)
|
function is_sudo(msg)
|
||||||
local var = false
|
local var = false
|
||||||
-- Check users id in config
|
-- 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
|
if user == msg.from.id then
|
||||||
var = true
|
var = true
|
||||||
end
|
end
|
||||||
@ -133,6 +139,7 @@ function is_sudo(msg)
|
|||||||
return var
|
return var
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Returns the name of the sender
|
||||||
function get_name(msg)
|
function get_name(msg)
|
||||||
local name = msg.from.first_name
|
local name = msg.from.first_name
|
||||||
if name == nil then
|
if name == nil then
|
||||||
@ -140,3 +147,42 @@ function get_name(msg)
|
|||||||
end
|
end
|
||||||
return name
|
return name
|
||||||
end
|
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
|
@ -14,4 +14,4 @@ if [ ! -f ./tg/bin/telegram-cli ]; then
|
|||||||
exit
|
exit
|
||||||
fi
|
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
|
||||||
|
@ -14,13 +14,13 @@
|
|||||||
-- the web-page links above, and the 'AUTHOR_NOTE' string below are
|
-- the web-page links above, and the 'AUTHOR_NOTE' string below are
|
||||||
-- maintained. Enjoy.
|
-- maintained. Enjoy.
|
||||||
--
|
--
|
||||||
local VERSION = 20140920.13 -- version history at end of file
|
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 20140920.13 ]-"
|
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
|
-- The 'AUTHOR_NOTE' variable exists so that information about the source
|
||||||
-- of the package is maintained even in compiled versions. It's included in
|
-- of the package is maintained even in compiled versions. It's also
|
||||||
-- OBJDEF mostly to quiet warnings about unused variables.
|
-- included in OBJDEF below mostly to quiet warnings about unused variables.
|
||||||
--
|
--
|
||||||
local OBJDEF = {
|
local OBJDEF = {
|
||||||
VERSION = VERSION,
|
VERSION = VERSION,
|
||||||
@ -33,7 +33,7 @@ local OBJDEF = {
|
|||||||
-- http://www.json.org/
|
-- 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)
|
-- 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
|
-- 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)
|
-- local lua_value = JSON:decode(raw_json_text)
|
||||||
--
|
--
|
||||||
@ -58,22 +60,27 @@ local OBJDEF = {
|
|||||||
-- { "Larry", "Curly", "Moe" }
|
-- { "Larry", "Curly", "Moe" }
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
-- The encode and decode routines accept an optional second argument, "etc", which is not used
|
-- The encode and decode routines accept an optional second argument,
|
||||||
-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any
|
-- "etc", which is not used during encoding or decoding, but upon error
|
||||||
-- type (including nil).
|
-- is passed along to error handlers. It can be of any type (including nil).
|
||||||
|
--
|
||||||
|
--
|
||||||
|
--
|
||||||
|
-- ERROR HANDLING
|
||||||
--
|
--
|
||||||
-- With most errors during decoding, this code calls
|
-- With most errors during decoding, this code calls
|
||||||
--
|
--
|
||||||
-- JSON:onDecodeError(message, text, location, etc)
|
-- JSON:onDecodeError(message, text, location, etc)
|
||||||
--
|
--
|
||||||
-- with a message about the error, and if known, the JSON text being parsed and the byte count
|
-- with a message about the error, and if known, the JSON text being
|
||||||
-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your
|
-- parsed and the byte count where the problem was discovered. You can
|
||||||
-- own function.
|
-- replace the default JSON:onDecodeError() with your own function.
|
||||||
--
|
--
|
||||||
-- The default onDecodeError() merely augments the message with data about the text and the
|
-- The default onDecodeError() merely augments the message with data
|
||||||
-- location if known (and if a second 'etc' argument had been provided to decode(), its value is
|
-- about the text and the location if known (and if a second 'etc'
|
||||||
-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's
|
-- argument had been provided to decode(), its value is tacked onto the
|
||||||
-- built-in assert(), and can also be overridden.
|
-- 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
|
-- For example, in an Adobe Lightroom plugin, you might use something like
|
||||||
--
|
--
|
||||||
@ -95,9 +102,10 @@ local OBJDEF = {
|
|||||||
--
|
--
|
||||||
-- JSON:onDecodeOfHTMLError(message, text, nil, etc)
|
-- JSON:onDecodeOfHTMLError(message, text, nil, etc)
|
||||||
--
|
--
|
||||||
-- The use of the fourth 'etc' argument allows stronger coordination between decoding and error
|
-- The use of the fourth 'etc' argument allows stronger coordination
|
||||||
-- reporting, especially when you provide your own error-handling routines. Continuing with the
|
-- between decoding and error reporting, especially when you provide your
|
||||||
-- the Adobe Lightroom plugin example:
|
-- own error-handling routines. Continuing with the the Adobe Lightroom
|
||||||
|
-- plugin example:
|
||||||
--
|
--
|
||||||
-- function JSON:onDecodeError(message, text, location, etc)
|
-- function JSON:onDecodeError(message, text, location, etc)
|
||||||
-- local note = "Internal Error: invalid JSON data"
|
-- local note = "Internal Error: invalid JSON data"
|
||||||
@ -121,42 +129,136 @@ local OBJDEF = {
|
|||||||
--
|
--
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
|
--
|
||||||
-- DECODING AND STRICT TYPES
|
-- DECODING AND STRICT TYPES
|
||||||
--
|
--
|
||||||
-- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally
|
-- Because both JSON objects and JSON arrays are converted to Lua tables,
|
||||||
-- possible to tell which a JSON type a particular Lua table was derived from, or guarantee
|
-- it's not normally possible to tell which original JSON type a
|
||||||
-- decode-encode round-trip equivalency.
|
-- particular Lua table was derived from, or guarantee decode-encode
|
||||||
|
-- round-trip equivalency.
|
||||||
--
|
--
|
||||||
-- However, if you enable strictTypes, e.g.
|
-- 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
|
-- JSON.strictTypes = true
|
||||||
--
|
--
|
||||||
-- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua
|
-- then the Lua table resulting from the decoding of a JSON object or
|
||||||
-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type.
|
-- 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
|
-- (This is not the default because other routines may not work well with
|
||||||
-- metatable set, for example, Lightroom API calls.)
|
-- 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 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 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:
|
-- On error during encoding, this code calls:
|
||||||
--
|
--
|
||||||
-- JSON:onEncodeError(message, etc)
|
-- JSON:onEncodeError(message, etc)
|
||||||
--
|
--
|
||||||
-- which you can override in your local JSON object.
|
-- which you can override in your local JSON object.
|
||||||
--
|
--
|
||||||
-- If the Lua table contains both string and numeric keys, it fits neither JSON's
|
-- The 'etc' in the error call is the second argument to encode()
|
||||||
-- idea of an object, nor its idea of an array. To get around this, when any string
|
-- and encode_pretty(), or nil if it wasn't provided.
|
||||||
-- key exists (or when non-positive numeric keys exist), numeric keys are converted
|
--
|
||||||
-- to strings.
|
--
|
||||||
|
-- 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,
|
-- For example,
|
||||||
-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
|
-- 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
|
-- To prohibit this conversion and instead make it an error condition, set
|
||||||
-- JSON.noKeyConversion = true
|
-- 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 isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray
|
||||||
local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
|
local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
|
||||||
|
|
||||||
@ -692,8 +800,13 @@ end
|
|||||||
--
|
--
|
||||||
-- Encode
|
-- 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
|
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
|
if value == nil then
|
||||||
return 'null'
|
return 'null'
|
||||||
@ -739,6 +852,13 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
--
|
--
|
||||||
local T = value
|
local T = value
|
||||||
|
|
||||||
|
if type(options) ~= 'table' then
|
||||||
|
options = {}
|
||||||
|
end
|
||||||
|
if type(indent) ~= 'string' then
|
||||||
|
indent = ""
|
||||||
|
end
|
||||||
|
|
||||||
if parents[T] then
|
if parents[T] then
|
||||||
self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
|
self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
|
||||||
else
|
else
|
||||||
@ -754,13 +874,13 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
--
|
--
|
||||||
local ITEMS = { }
|
local ITEMS = { }
|
||||||
for i = 1, maximum_number_key do
|
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
|
end
|
||||||
|
|
||||||
if indent then
|
if options.pretty then
|
||||||
result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
|
result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
|
||||||
else
|
else
|
||||||
result_value = "[" .. table.concat(ITEMS, ",") .. "]"
|
result_value = "[" .. table.concat(ITEMS, ",") .. "]"
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif object_keys then
|
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
|
local TT = map or T
|
||||||
|
|
||||||
if indent then
|
if options.pretty then
|
||||||
|
|
||||||
local KEYS = { }
|
local KEYS = { }
|
||||||
local max_key_length = 0
|
local max_key_length = 0
|
||||||
for _, key in ipairs(object_keys) do
|
for _, key in ipairs(object_keys) do
|
||||||
local encoded = encode_value(self, tostring(key), parents, etc, "")
|
local encoded = encode_value(self, tostring(key), parents, etc, options, indent)
|
||||||
max_key_length = math.max(max_key_length, #encoded)
|
if options.align_keys then
|
||||||
|
max_key_length = math.max(max_key_length, #encoded)
|
||||||
|
end
|
||||||
table.insert(KEYS, encoded)
|
table.insert(KEYS, encoded)
|
||||||
end
|
end
|
||||||
local key_indent = indent .. " "
|
local key_indent = indent .. tostring(options.indent or "")
|
||||||
local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4)
|
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 FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s"
|
||||||
|
|
||||||
local COMBINED_PARTS = { }
|
local COMBINED_PARTS = { }
|
||||||
for i, key in ipairs(object_keys) do
|
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))
|
table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
|
||||||
end
|
end
|
||||||
result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"
|
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 = { }
|
local PARTS = { }
|
||||||
for _, key in ipairs(object_keys) do
|
for _, key in ipairs(object_keys) do
|
||||||
local encoded_val = encode_value(self, TT[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, indent)
|
local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent)
|
||||||
table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
|
table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
|
||||||
end
|
end
|
||||||
result_value = "{" .. table.concat(PARTS, ",") .. "}"
|
result_value = "{" .. table.concat(PARTS, ",") .. "}"
|
||||||
@ -813,18 +935,18 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function OBJDEF:encode(value, etc)
|
function OBJDEF:encode(value, etc, options)
|
||||||
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
||||||
OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
|
OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
|
||||||
end
|
end
|
||||||
return encode_value(self, value, {}, etc, nil)
|
return encode_value(self, value, {}, etc, options or nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
function OBJDEF:encode_pretty(value, etc)
|
function OBJDEF:encode_pretty(value, etc, options)
|
||||||
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
||||||
OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
|
OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
|
||||||
end
|
end
|
||||||
return encode_value(self, value, {}, etc, "")
|
return encode_value(self, value, {}, etc, options or default_pretty_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
function OBJDEF.__tostring()
|
function OBJDEF.__tostring()
|
||||||
@ -850,6 +972,16 @@ return OBJDEF:new()
|
|||||||
--
|
--
|
||||||
-- Version history:
|
-- 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,
|
-- 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.
|
-- so that the source of the package, and its version number, are visible in compiled copies.
|
||||||
--
|
--
|
128
libs/serpent.lua
Normal file
128
libs/serpent.lua
Normal file
@ -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 }
|
@ -1,13 +1,30 @@
|
|||||||
local f = io.open('./res/values.json', "r+")
|
local _file_values = './data/values.lua'
|
||||||
if f == nil then
|
|
||||||
f = io.open('./res/values.json', "w+")
|
function read_file_values( )
|
||||||
f:write("{}") -- Write empty table
|
local f = io.open(_file_values, "r+")
|
||||||
f:close()
|
-- If file doesn't exists
|
||||||
_values = {}
|
if f == nil then
|
||||||
else
|
-- Create a new empty table
|
||||||
local c = f:read "*a"
|
print ('Created value file '.._file_values)
|
||||||
f:close()
|
serialize_to_file({}, _file_values)
|
||||||
_values = json:decode(c)
|
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
|
end
|
||||||
|
|
||||||
function get_value(chat, value_name)
|
function get_value(chat, value_name)
|
||||||
@ -40,11 +57,27 @@ function run(msg, matches)
|
|||||||
return get_value(chat_id, matches[1])
|
return get_value(chat_id, matches[1])
|
||||||
end
|
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 {
|
return {
|
||||||
description = "retrieves variables saved with !set",
|
description = "retrieves variables saved with !set",
|
||||||
usage = "!get (value_name)",
|
usage = "!get (value_name)",
|
||||||
patterns = {
|
patterns = {
|
||||||
"^!get (%a+)$",
|
"^!get (%a+)$",
|
||||||
"^!get$"},
|
"^!get$"},
|
||||||
run = run
|
run = run,
|
||||||
|
lex = lex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
42
plugins/invite.lua
Normal file
42
plugins/invite.lua
Normal file
@ -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
|
@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
-- Globals
|
-- Globals
|
||||||
-- If you have a google api key for the geocoding/timezone api
|
-- 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"
|
base_api = "https://maps.googleapis.com/maps/api"
|
||||||
|
|
||||||
function delay_s(delay)
|
function delay_s(delay)
|
||||||
|
113
plugins/plugins.lua
Normal file
113
plugins/plugins.lua
Normal file
@ -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
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
|||||||
|
local _file_values = './data/values.lua'
|
||||||
|
|
||||||
function save_value(chat, text )
|
function save_value(chat, text )
|
||||||
var_name, var_value = string.match(text, "!set (%a+) (.+)")
|
var_name, var_value = string.match(text, "!set (%a+) (.+)")
|
||||||
if (var_name == nil or var_value == nil) then
|
if (var_name == nil or var_value == nil) then
|
||||||
@ -8,10 +10,8 @@ function save_value(chat, text )
|
|||||||
end
|
end
|
||||||
_values[chat][var_name] = var_value
|
_values[chat][var_name] = var_value
|
||||||
|
|
||||||
local json_text = json:encode_pretty(_values)
|
-- Save values to file
|
||||||
file = io.open ("./res/values.json", "w+")
|
serialize_to_file(_values, _file_values)
|
||||||
file:write(json_text)
|
|
||||||
file:close()
|
|
||||||
|
|
||||||
return "Saved "..var_name.." = "..var_value
|
return "Saved "..var_name.." = "..var_value
|
||||||
end
|
end
|
||||||
|
@ -1,28 +1,98 @@
|
|||||||
function run(msg, matches)
|
-- Saves the number of messages from a user
|
||||||
vardump(_users)
|
-- Can check the number of messages with !stats
|
||||||
-- 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()
|
|
||||||
|
|
||||||
|
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 text = ""
|
||||||
local to_id = tostring(msg.to.id)
|
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
|
if user.last_name == nil then
|
||||||
text = text..user.name.." ["..id.."]: "..user.msg_num.."\n"
|
text = text..user.name.." ["..id.."]: "..user.msg_num.."\n"
|
||||||
else
|
else
|
||||||
text = text..user.name.." "..user.last_name.." ["..id.."]: "..user.msg_num.."\n"
|
text = text..user.name.." "..user.last_name.." ["..id.."]: "..user.msg_num.."\n"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
print("usuarios: "..text)
|
||||||
return text
|
return text
|
||||||
end
|
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 {
|
return {
|
||||||
description = "Numer of messages by user",
|
description = "Numer of messages by user",
|
||||||
usage = "!stats",
|
usage = "!stats",
|
||||||
patterns = {"^!stats"},
|
patterns = {
|
||||||
|
".*",
|
||||||
|
"^!(stats)"
|
||||||
|
},
|
||||||
run = run
|
run = run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end
|
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
-- Globals
|
-- Globals
|
||||||
-- If you have a google api key for the geocoding/timezone api
|
-- 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"
|
base_api = "https://maps.googleapis.com/maps/api"
|
||||||
dateFormat = "%A %d %B - %H:%M:%S"
|
dateFormat = "%A %d %B - %H:%M:%S"
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
local OAuth = require "OAuth"
|
local OAuth = require "OAuth"
|
||||||
|
|
||||||
local consumer_key = config.twitter.consumer_key
|
local consumer_key = ""
|
||||||
local consumer_secret = config.twitter.consumer_secret
|
local consumer_secret = ""
|
||||||
local access_token = config.twitter.access_token
|
local access_token = ""
|
||||||
local access_token_secret = config.twitter.access_token_secret
|
local access_token_secret = ""
|
||||||
|
|
||||||
local client = OAuth.new(consumer_key, consumer_secret, {
|
local client = OAuth.new(consumer_key, consumer_secret, {
|
||||||
RequestToken = "https://api.twitter.com/oauth/request_token",
|
RequestToken = "https://api.twitter.com/oauth/request_token",
|
||||||
@ -16,16 +16,23 @@ local client = OAuth.new(consumer_key, consumer_secret, {
|
|||||||
|
|
||||||
function run(msg, matches)
|
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"
|
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)
|
local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", twitter_url)
|
||||||
print(response_body)
|
|
||||||
local response = json:decode(response_body)
|
local response = json:decode(response_body)
|
||||||
|
|
||||||
print("response = ", response)
|
|
||||||
|
|
||||||
local header = "Tweet from " .. response.user.name .. " (@" .. response.user.screen_name .. ")\n"
|
local header = "Tweet from " .. response.user.name .. " (@" .. response.user.screen_name .. ")\n"
|
||||||
local text = response.text
|
local text = response.text
|
||||||
|
|
||||||
@ -66,5 +73,3 @@ return {
|
|||||||
patterns = {"https://twitter.com/[^/]+/status/([0-9]+)"},
|
patterns = {"https://twitter.com/[^/]+/status/([0-9]+)"},
|
||||||
run = run
|
run = run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,31 +1,45 @@
|
|||||||
local OAuth = require "OAuth"
|
local OAuth = require "OAuth"
|
||||||
|
|
||||||
local consumer_key = config.twitter.consumer_key
|
local consumer_key = ""
|
||||||
local consumer_secret = config.twitter.consumer_secret
|
local consumer_secret = ""
|
||||||
local access_token = config.twitter.access_token
|
local access_token = ""
|
||||||
local access_token_secret = config.twitter.access_token_secret
|
local access_token_secret = ""
|
||||||
|
|
||||||
local client = OAuth.new(consumer_key, consumer_secret, {
|
local client = OAuth.new(consumer_key, consumer_secret, {
|
||||||
RequestToken = "https://api.twitter.com/oauth/request_token",
|
RequestToken = "https://api.twitter.com/oauth/request_token",
|
||||||
AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"},
|
AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"},
|
||||||
AccessToken = "https://api.twitter.com/oauth/access_token"
|
AccessToken = "https://api.twitter.com/oauth/access_token"
|
||||||
}, {
|
}, {
|
||||||
OAuthToken = access_token,
|
OAuthToken = access_token,
|
||||||
OAuthTokenSecret = access_token_secret
|
OAuthTokenSecret = access_token_secret
|
||||||
})
|
})
|
||||||
|
|
||||||
function run(msg, matches)
|
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
|
if not is_sudo(msg) then
|
||||||
return "You aren't allowed to send tweets"
|
return "You aren't allowed to send tweets"
|
||||||
end
|
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", {
|
client:PerformRequest("POST", "https://api.twitter.com/1.1/statuses/update.json", {
|
||||||
status = matches[1]
|
status = matches[1]
|
||||||
})
|
})
|
||||||
if response_code ~= 200 then
|
if response_code ~= 200 then
|
||||||
return "Error: "..response_code
|
return "Error: "..response_code
|
||||||
end
|
end
|
||||||
return "Tweet sended"
|
return "Tweet sended"
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Reference in New Issue
Block a user