├── data └── .gitkeep ├── .gitmodules ├── etc └── superflux.conf ├── libs ├── redis.lua ├── mimetype.lua ├── dkjson.lua └── JSON.lua ├── README.md ├── plugins ├── help.lua ├── invite.lua ├── plugins.lua ├── help_old.lua ├── id.lua ├── moderation.lua └── membercontrol.lua ├── launch.sh ├── bot ├── bot.lua └── utils.lua └── LICENSE /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tg"] 2 | path = tg 3 | url = https://github.com/mustafaflux/tg 4 | branch = test 5 | -------------------------------------------------------------------------------- /etc/superflux.conf: -------------------------------------------------------------------------------- 1 | description "superflux upstart script" 2 | 3 | respawn 4 | respawn limit 15 5 5 | 6 | start on runlevel [2345] 7 | stop on shutdown 8 | 9 | setuid yourusername 10 | exec /bin/sh telegrambotpath/launch.sh -------------------------------------------------------------------------------- /libs/redis.lua: -------------------------------------------------------------------------------- 1 | local Redis = require 'redis' 2 | local FakeRedis = require 'fakeredis' 3 | 4 | local params = { 5 | host = os.getenv('REDIS_HOST') or '127.0.0.1', 6 | port = tonumber(os.getenv('REDIS_PORT') or 6379) 7 | } 8 | 9 | local database = os.getenv('REDIS_DB') 10 | local password = os.getenv('REDIS_PASSWORD') 11 | 12 | -- Overwrite HGETALL 13 | Redis.commands.hgetall = Redis.command('hgetall', { 14 | response = function(reply, command, ...) 15 | local new_reply = { } 16 | for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end 17 | return new_reply 18 | end 19 | }) 20 | 21 | local redis = nil 22 | 23 | -- Won't launch an error if fails 24 | local ok = pcall(function() 25 | redis = Redis.connect(params) 26 | end) 27 | 28 | if not ok then 29 | 30 | local fake_func = function() 31 | print('\27[31mCan\'t connect with Redis, install/configure it!\27[39m') 32 | end 33 | fake_func() 34 | fake = FakeRedis.new() 35 | 36 | print('\27[31mRedis addr: '..params.host..'\27[39m') 37 | print('\27[31mRedis port: '..params.port..'\27[39m') 38 | 39 | redis = setmetatable({fakeredis=true}, { 40 | __index = function(a, b) 41 | if b ~= 'data' and fake[b] then 42 | fake_func(b) 43 | end 44 | return fake[b] or fake_func 45 | end }) 46 | 47 | else 48 | if password then 49 | redis:auth(password) 50 | end 51 | if database then 52 | redis:select(database) 53 | end 54 | end 55 | 56 | 57 | return redis 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ~~This is Simple README 2 | 3 | ~~ Source Superflux For Supergroups 4 | 5 | +~~Developer By : @Mustafaflux On Telegram , 6 | 7 | To install on C9 your next steps 8 | 9 | 10 | 11 | #Step1 : writ this > in Terminal 12 | 13 | sudo apt-get update 14 | 15 | 16 | 17 | #step2 : writ this > in Terminal 18 | 19 | sudo apt-get install libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev make unzip git redis-server g++ 20 | libjansson-dev libpython-dev expat libexpat1-dev 21 | 22 | 23 | 24 | #step3 : writ > 25 | 26 | git clone https://github.com/mustafaflux/superflux-bot.git 27 | 28 | 29 | #step4 : Writ > 30 | 31 | cd superflux-bot 32 | 33 | 34 | 35 | step5 : Writ > 36 | 37 | ./launch.sh install 38 | 39 | 40 | 41 | (When the installation finishes) 42 | 43 | 44 | 45 | step6 : Writ > 46 | 47 | ./launch.sh 48 | 49 | 50 | 51 | Then ask You Number To put him Bot 52 | 53 | [[ If You Want install on VPS DO JUST From #step3 to #step6]] 54 | 55 | ---------------------------- 56 | 57 | For the inauguration #SourceSuperflux 58 | 59 | 60 | 61 | ️#one_Commnd_in_New_terimnal 💠 62 | 63 | 64 | 65 | sudo apt-get update && sudo apt-get upgrade -y && sudo apt-get autoremove && sudo apt-get autoclean && sudo apt-get install 66 | libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev make unzip git redis-server g++ libjansson-dev 67 | libpython-dev expat libexpat1-dev -y && cd $HOME && rm -rf superflux-bot && rm -rf .telegram-cli && git clone https://github.com/mustafaflux/superflux-bot.git && cd superflux-bot && ./launch.sh install && ./launch.sh 68 | ~~~~~ 69 | -------------------------------------------------------------------------------- /plugins/help.lua: -------------------------------------------------------------------------------- 1 | local function run(msg, matches) 2 | if is_chat_msg(msg) then 3 | local text = [[‌✅Commands to lock|unlock 4 | 💭/close|open link 5 | 💭/close|open member 6 | 💭/close|open name 7 | 💭/close|open bot 8 | 💭/close|open photo 9 | 💭/close|open sticker 10 | 💭/close|open file 11 | 💭/close|open audio 12 | 13 | ➕ 14 | ✅Commands for control member 15 | 💭/kick : by 16 | 💭/ban : by 17 | 💭/unban : by 18 | 💭/kickme 19 | 20 | ➕ 21 | ✅Group control 22 | 💭/rules 23 | 💭/setrules 24 | 💭/about 25 | 💭/setabout 26 | 💭/setphoto : then send photo 27 | 💭/setname 28 | 💭/id 29 | 💭/id chat 30 | 💭/group settings 31 | 💭/getlink : send link in your pv 32 | 💭/relink : change link group and send new link your pv 33 | 💭/modlist 34 | 💭/help 35 | 36 | ➕ 37 | ✅ Group Promote commands 38 | 💭/spromote : up leader by 39 | 💭/sdemote : in promote by 40 | 💭/promote : by 41 | 💭/demote : by 42 | 43 | ➖🔸➖🔹➖🔸➖🔹➖]] 44 | return text 45 | end 46 | if is_channel_msg(msg) then 47 | local text = [[‌‌✅Commands to lock|unlock 48 | 💭/close|open link 49 | 💭/close|open member 50 | 💭/close|open name 51 | 💭/close|open bot 52 | 💭/close|open photo 53 | 💭/close|open sticker 54 | 💭/close|open file 55 | 💭/close|open audio 56 | 💭/close|open talk 57 | 58 | ➕ 59 | ✅Commands for control member 60 | 💭/kick : by 61 | 💭/ban : by 62 | 💭/unban : by 63 | 💭/kickme 64 | 65 | ➕ 66 | ✅Group control 67 | 💭/rules 68 | 💭/setrules 69 | 💭/about 70 | 💭/setabout 71 | 💭/setphoto : then send photo 72 | 💭/setname 73 | 💭/id 74 | 💭/id chat 75 | 💭/group settings 76 | 💭/getlink : send link in your pv 77 | 💭/relink : change link group and send new link your pv 78 | 💭/modlist 79 | 💭/help 80 | 81 | ➕ 82 | ✅ Group Promote commands 83 | 💭/spromote : up leader by 84 | 💭/sdemote : in promote by 85 | 💭/promote : by 86 | 💭/demote : by 87 | 88 | ➖🔸➖🔹➖🔸➖🔹➖]] 89 | return text 90 | else 91 | local text = [[aaa]] 92 | --return text 93 | end 94 | end 95 | 96 | return { 97 | description = "Help plugin. Get info from other plugins. ", 98 | usage = { 99 | "!help: Show list of plugins.", 100 | }, 101 | patterns = { 102 | "^/(help)$", 103 | }, 104 | run = run, 105 | } 106 | -------------------------------------------------------------------------------- /plugins/invite.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function callback(extra, success, result) 4 | local receiver = extra.receiver 5 | local username = extra.username 6 | if success == 1 then 7 | print("Success!") 8 | else 9 | send_large_msg(resuser, "Sorry, i can't invite @"..username) 10 | end 11 | end 12 | 13 | local function resuser(extra, success, result) 14 | local receiver = extra.receiver 15 | local username = extra.username 16 | if success == 1 then 17 | local user = "user#id"..result.id 18 | if string.find(receiver, 'channel#id') then 19 | channel_invite_user(receiver, user, callback, {receiver=receiver, username=username}) 20 | else 21 | chat_add_user(receiver, user, callback, {receiver=receiver, username=username}) 22 | end 23 | else 24 | send_large_msg(receiver, "User not found!") 25 | end 26 | end 27 | 28 | local function get_msg_callback(extra, success, result) 29 | if success ~= 1 then return end 30 | local get_cmd = extra.get_cmd 31 | local receiver = extra.receiver 32 | local user_id = result.from.peer_id 33 | local chat_id = result.to.id 34 | if result.from.username then 35 | username = '@'..result.from.username 36 | else 37 | username = string.gsub(result.from.print_name, '_', ' ') 38 | end 39 | if get_cmd == 'invite' then 40 | if user_id == our_id then 41 | return nil 42 | end 43 | local user = "user#id"..user_id 44 | if string.find(receiver, 'channel#id') then 45 | channel_invite_user(receiver, user, callback, {receiver=receiver, username=username}) 46 | else 47 | chat_add_user(receiver, user, callback, {receiver=receiver, username=username}) 48 | end 49 | end 50 | end 51 | 52 | local function run(msg, matches) 53 | local receiver = get_receiver(msg) 54 | local get_cmd = matches[1] 55 | if msg.to.type == "chat" then 56 | local chat = "chat#id"..msg.to.id 57 | resolve_username(username, resuser, {receiver=get_receiver(msg), username=username}) 58 | end 59 | 60 | if msg.to.type == "channel" then 61 | if matches[1] == "invite" then 62 | if not is_momod(msg) then 63 | return 64 | end 65 | if not matches[2] and msg.reply_id then 66 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 67 | end 68 | if not matches[2] then 69 | return 70 | end 71 | local username = string.gsub(matches[2], "@", "") 72 | res_user(username, resuser, {receiver=get_receiver(msg), username=username}) 73 | end 74 | end 75 | 76 | end 77 | 78 | return { 79 | description = "Invite other user to the chat group", 80 | usage = { 81 | moderator = { 82 | "!invite : Invite other user to this chat", 83 | }, 84 | }, 85 | patterns = { 86 | "^/(invite) (.*)$", 87 | "^/(invite)$", 88 | }, 89 | run = run, 90 | moderated = true 91 | } 92 | end -------------------------------------------------------------------------------- /launch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | THIS_DIR=$(cd $(dirname $0); pwd) 4 | cd $THIS_DIR 5 | 6 | update() { 7 | git pull 8 | git submodule update --init --recursive 9 | install_rocks 10 | } 11 | 12 | # Will install luarocks on THIS_DIR/.luarocks 13 | install_luarocks() { 14 | git clone https://github.com/keplerproject/luarocks.git 15 | cd luarocks 16 | git checkout tags/v2.2.1 # Current stable 17 | 18 | PREFIX="$THIS_DIR/.luarocks" 19 | 20 | ./configure --prefix=$PREFIX --sysconfdir=$PREFIX/luarocks --force-config 21 | 22 | RET=$?; if [ $RET -ne 0 ]; 23 | then echo "Error. Exiting."; exit $RET; 24 | fi 25 | 26 | make build && make install 27 | RET=$?; if [ $RET -ne 0 ]; 28 | then echo "Error. Exiting.";exit $RET; 29 | fi 30 | 31 | cd .. 32 | rm -rf luarocks 33 | } 34 | 35 | install_rocks() { 36 | ./.luarocks/bin/luarocks install luasocket 37 | RET=$?; if [ $RET -ne 0 ]; 38 | then echo "Error. Exiting."; exit $RET; 39 | fi 40 | 41 | ./.luarocks/bin/luarocks install oauth 42 | RET=$?; if [ $RET -ne 0 ]; 43 | then echo "Error. Exiting."; exit $RET; 44 | fi 45 | 46 | ./.luarocks/bin/luarocks install redis-lua 47 | RET=$?; if [ $RET -ne 0 ]; 48 | then echo "Error. Exiting."; exit $RET; 49 | fi 50 | 51 | ./.luarocks/bin/luarocks install lua-cjson 52 | RET=$?; if [ $RET -ne 0 ]; 53 | then echo "Error. Exiting."; exit $RET; 54 | fi 55 | 56 | ./.luarocks/bin/luarocks install fakeredis 57 | RET=$?; if [ $RET -ne 0 ]; 58 | then echo "Error. Exiting."; exit $RET; 59 | fi 60 | 61 | ./.luarocks/bin/luarocks install xml 62 | RET=$?; if [ $RET -ne 0 ]; 63 | then echo "Error. Exiting."; exit $RET; 64 | fi 65 | 66 | ./.luarocks/bin/luarocks install feedparser 67 | RET=$?; if [ $RET -ne 0 ]; 68 | then echo "Error. Exiting."; exit $RET; 69 | fi 70 | 71 | ./.luarocks/bin/luarocks install serpent 72 | RET=$?; if [ $RET -ne 0 ]; 73 | then echo "Error. Exiting."; exit $RET; 74 | fi 75 | } 76 | 77 | install() { 78 | git pull 79 | git submodule update --init --recursive 80 | patch -i "patches/disable-python-and-libjansson.patch" -p 0 --batch --forward 81 | RET=$?; 82 | 83 | cd tg 84 | if [ $RET -ne 0 ]; then 85 | autoconf -i 86 | fi 87 | ./configure && make 88 | 89 | RET=$?; if [ $RET -ne 0 ]; then 90 | echo "Error. Exiting."; exit $RET; 91 | fi 92 | cd .. 93 | install_luarocks 94 | install_rocks 95 | } 96 | 97 | if [ "$1" = "install" ]; then 98 | install 99 | elif [ "$1" = "update" ]; then 100 | update 101 | else 102 | if [ ! -f ./tg/telegram.h ]; then 103 | echo "tg not found" 104 | echo "Run $0 install" 105 | exit 1 106 | fi 107 | 108 | if [ ! -f ./tg/bin/telegram-cli ]; then 109 | echo "tg binary not found" 110 | echo "Run $0 install" 111 | exit 1 112 | fi 113 | 114 | if [ -f $HOME/.telegram-cli/state ]; then 115 | echo "state found" 116 | echo "remove it!" 117 | rm -rf $HOME/.telegram-cli/state 118 | fi 119 | 120 | ./tg/bin/telegram-cli -k ./tg/tg-server.pub -s ./bot/bot.lua -l 1 -E $@ --disable-link-preview 121 | fi 122 | -------------------------------------------------------------------------------- /libs/mimetype.lua: -------------------------------------------------------------------------------- 1 | -- Thanks to https://github.com/catwell/lua-toolbox/blob/master/mime.types 2 | do 3 | 4 | local mimetype = {} 5 | 6 | -- TODO: Add more? 7 | local types = { 8 | ["text/html"] = "html", 9 | ["text/css"] = "css", 10 | ["text/xml"] = "xml", 11 | ["image/gif"] = "gif", 12 | ["image/jpeg"] = "jpg", 13 | ["application/x-javascript"] = "js", 14 | ["application/atom+xml"] = "atom", 15 | ["application/rss+xml"] = "rss", 16 | ["text/mathml"] = "mml", 17 | ["text/plain"] = "txt", 18 | ["text/vnd.sun.j2me.app-descriptor"] = "jad", 19 | ["text/vnd.wap.wml"] = "wml", 20 | ["text/x-component"] = "htc", 21 | ["image/png"] = "png", 22 | ["image/tiff"] = "tiff", 23 | ["image/vnd.wap.wbmp"] = "wbmp", 24 | ["image/x-icon"] = "ico", 25 | ["image/x-jng"] = "jng", 26 | ["image/x-ms-bmp"] = "bmp", 27 | ["image/svg+xml"] = "svg", 28 | ["image/webp"] = "webp", 29 | ["application/java-archive"] = "jar", 30 | ["application/mac-binhex40"] = "hqx", 31 | ["application/msword"] = "doc", 32 | ["application/pdf"] = "pdf", 33 | ["application/postscript"] = "ps", 34 | ["application/rtf"] = "rtf", 35 | ["application/vnd.ms-excel"] = "xls", 36 | ["application/vnd.ms-powerpoint"] = "ppt", 37 | ["application/vnd.wap.wmlc"] = "wmlc", 38 | ["application/vnd.google-earth.kml+xml"] = "kml", 39 | ["application/vnd.google-earth.kmz"] = "kmz", 40 | ["application/x-7z-compressed"] = "7z", 41 | ["application/x-cocoa"] = "cco", 42 | ["application/x-java-archive-diff"] = "jardiff", 43 | ["application/x-java-jnlp-file"] = "jnlp", 44 | ["application/x-makeself"] = "run", 45 | ["application/x-perl"] = "pl", 46 | ["application/x-pilot"] = "prc", 47 | ["application/x-rar-compressed"] = "rar", 48 | ["application/x-redhat-package-manager"] = "rpm", 49 | ["application/x-sea"] = "sea", 50 | ["application/x-shockwave-flash"] = "swf", 51 | ["application/x-stuffit"] = "sit", 52 | ["application/x-tcl"] = "tcl", 53 | ["application/x-x509-ca-cert"] = "crt", 54 | ["application/x-xpinstall"] = "xpi", 55 | ["application/xhtml+xml"] = "xhtml", 56 | ["application/zip"] = "zip", 57 | ["application/octet-stream"] = "bin", 58 | ["audio/midi"] = "mid", 59 | ["audio/mpeg"] = "mp3", 60 | ["audio/ogg"] = "ogg", 61 | ["audio/x-m4a"] = "m4a", 62 | ["audio/x-realaudio"] = "ra", 63 | ["video/3gpp"] = "3gpp", 64 | ["video/mp4"] = "mp4", 65 | ["video/mpeg"] = "mpeg", 66 | ["video/quicktime"] = "mov", 67 | ["video/webm"] = "webm", 68 | ["video/x-flv"] = "flv", 69 | ["video/x-m4v"] = "m4v", 70 | ["video/x-mng"] = "mng", 71 | ["video/x-ms-asf"] = "asf", 72 | ["video/x-ms-wmv"] = "wmv", 73 | ["video/x-msvideo"] = "avi" 74 | } 75 | 76 | -- Returns the common file extension from a content-type 77 | function mimetype.get_mime_extension(content_type) 78 | return types[content_type] 79 | end 80 | 81 | -- Returns the mimetype and subtype 82 | function mimetype.get_content_type(extension) 83 | for k,v in pairs(types) do 84 | if v == extension then 85 | return k 86 | end 87 | end 88 | end 89 | 90 | -- Returns the mimetype without the subtype 91 | function mimetype.get_content_type_no_sub(extension) 92 | for k,v in pairs(types) do 93 | if v == extension then 94 | -- Before / 95 | return k:match('([%w-]+)/') 96 | end 97 | end 98 | end 99 | 100 | return mimetype 101 | end -------------------------------------------------------------------------------- /plugins/plugins.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | -- Returns the key (index) in the config.enabled_plugins table 4 | local function plugin_enabled( name ) 5 | for k,v in pairs(_config.enabled_plugins) do 6 | if name == v then 7 | return k 8 | end 9 | end 10 | -- If not found 11 | return false 12 | end 13 | 14 | -- Returns true if file exists in plugins folder 15 | local function plugin_exists( name ) 16 | for k,v in pairs(plugins_names()) do 17 | if name..'.lua' == v then 18 | return true 19 | end 20 | end 21 | return false 22 | end 23 | 24 | local function list_plugins(only_enabled) 25 | local text = '' 26 | for k, v in pairs( plugins_names( )) do 27 | -- ✔ enabled, ❌ disabled 28 | local status = '❌' 29 | -- Check if is enabled 30 | for k2, v2 in pairs(_config.enabled_plugins) do 31 | if v == v2..'.lua' then 32 | status = '✔' 33 | end 34 | end 35 | if not only_enabled or status == '✔' then 36 | -- get the name 37 | v = string.match (v, "(.*)%.lua") 38 | text = text..v..' '..status..'\n' 39 | end 40 | end 41 | return text 42 | end 43 | 44 | local function reload_plugins( ) 45 | plugins = {} 46 | load_plugins() 47 | return list_plugins(true) 48 | end 49 | 50 | 51 | local function enable_plugin( plugin_name ) 52 | print('checking if '..plugin_name..' exists') 53 | -- Check if plugin is enabled 54 | if plugin_enabled(plugin_name) then 55 | return 'Plugin '..plugin_name..' is enabled' 56 | end 57 | -- Checks if plugin exists 58 | if plugin_exists(plugin_name) then 59 | -- Add to the config table 60 | table.insert(_config.enabled_plugins, plugin_name) 61 | print(plugin_name..' added to _config table') 62 | save_config() 63 | -- Reload the plugins 64 | return reload_plugins( ) 65 | else 66 | return 'Plugin '..plugin_name..' does not exists' 67 | end 68 | end 69 | 70 | local function disable_plugin( name, chat ) 71 | -- Check if plugins exists 72 | if not plugin_exists(name) then 73 | return 'Plugin '..name..' does not exists' 74 | end 75 | local k = plugin_enabled(name) 76 | -- Check if plugin is enabled 77 | if not k then 78 | return 'Plugin '..name..' not enabled' 79 | end 80 | -- Disable and reload 81 | table.remove(_config.enabled_plugins, k) 82 | save_config( ) 83 | return reload_plugins(true) 84 | end 85 | 86 | local function disable_plugin_on_chat(receiver, plugin) 87 | if not plugin_exists(plugin) then 88 | return "Plugin doesn't exists" 89 | end 90 | 91 | if not _config.disabled_plugin_on_chat then 92 | _config.disabled_plugin_on_chat = {} 93 | end 94 | 95 | if not _config.disabled_plugin_on_chat[receiver] then 96 | _config.disabled_plugin_on_chat[receiver] = {} 97 | end 98 | 99 | _config.disabled_plugin_on_chat[receiver][plugin] = true 100 | 101 | save_config() 102 | return 'Plugin '..plugin..' disabled on this chat' 103 | end 104 | 105 | local function reenable_plugin_on_chat(receiver, plugin) 106 | if not _config.disabled_plugin_on_chat then 107 | return 'There aren\'t any disabled plugins' 108 | end 109 | 110 | if not _config.disabled_plugin_on_chat[receiver] then 111 | return 'There aren\'t any disabled plugins for this chat' 112 | end 113 | 114 | if not _config.disabled_plugin_on_chat[receiver][plugin] then 115 | return 'This plugin is not disabled' 116 | end 117 | 118 | _config.disabled_plugin_on_chat[receiver][plugin] = false 119 | save_config() 120 | return 'Plugin '..plugin..' is enabled again' 121 | end 122 | 123 | local function run(msg, matches) 124 | -- Show the available plugins 125 | if matches[1] == '!plugins' then 126 | return list_plugins() 127 | end 128 | 129 | -- Re-enable a plugin for this chat 130 | if matches[1] == 'enable' and matches[3] == 'chat' then 131 | local receiver = get_receiver(msg) 132 | local plugin = matches[2] 133 | print("enable "..plugin..' on this chat') 134 | return reenable_plugin_on_chat(receiver, plugin) 135 | end 136 | 137 | -- Enable a plugin 138 | if matches[1] == 'enable' then 139 | local plugin_name = matches[2] 140 | print("enable: "..matches[2]) 141 | return enable_plugin(plugin_name) 142 | end 143 | 144 | -- Disable a plugin on a chat 145 | if matches[1] == 'disable' and matches[3] == 'chat' then 146 | local plugin = matches[2] 147 | local receiver = get_receiver(msg) 148 | print("disable "..plugin..' on this chat') 149 | return disable_plugin_on_chat(receiver, plugin) 150 | end 151 | 152 | -- Disable a plugin 153 | if matches[1] == 'disable' then 154 | print("disable: "..matches[2]) 155 | return disable_plugin(matches[2]) 156 | end 157 | 158 | -- Reload all the plugins! 159 | if matches[1] == 'reload' then 160 | return reload_plugins(true) 161 | end 162 | end 163 | 164 | return { 165 | description = "Plugin to manage other plugins. Enable, disable or reload.", 166 | usage = { 167 | "/plugins: list all plugins.", 168 | "/plugins enable [plugin]: enable plugin.", 169 | "/plugins disable [plugin]: disable plugin.", 170 | "/plugins disable [plugin] chat: disable plugin only this chat.", 171 | "/plugins reload: reloads all plugins." }, 172 | patterns = { 173 | "^/plugins$", 174 | "^/plugins? (enable) ([%w_%.%-]+)$", 175 | "^/plugins? (disable) ([%w_%.%-]+)$", 176 | "^/plugins? (enable) ([%w_%.%-]+) (chat)", 177 | "^/plugins? (disable) ([%w_%.%-]+) (chat)", 178 | "^/plugins? (reload)$" }, 179 | run = run, 180 | privileged = true 181 | } 182 | 183 | end -------------------------------------------------------------------------------- /plugins/help_old.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | function pairsByKeys(t, f) 4 | local a = {} 5 | for n in pairs(t) do table.insert(a, n) end 6 | table.sort(a, f) 7 | local i = 0 -- iterator variable 8 | local iter = function () -- iterator function 9 | i = i + 1 10 | if a[i] == nil then return nil 11 | else return a[i], t[a[i]] 12 | end 13 | end 14 | return iter 15 | end 16 | 17 | -- Returns true if is not empty 18 | local function has_usage_data(dict) 19 | if (dict.usage == nil or dict.usage == '') then 20 | return false 21 | end 22 | return true 23 | end 24 | 25 | -- Get commands for that plugin 26 | local function plugin_help(name,number,requester) 27 | local plugin = "" 28 | if number then 29 | local i = 0 30 | for name in pairsByKeys(plugins) do 31 | if plugins[name].hidden then 32 | name = nil 33 | else 34 | i = i + 1 35 | if i == tonumber(number) then 36 | plugin = plugins[name] 37 | end 38 | end 39 | end 40 | else 41 | plugin = plugins[name] 42 | if not plugin then return nil end 43 | end 44 | 45 | local text = "" 46 | if (type(plugin.usage) == "table") then 47 | for ku,usage in pairs(plugin.usage) do 48 | if ku == 'user' then -- usage for user 49 | if (type(plugin.usage.user) == "table") then 50 | for k,v in pairs(plugin.usage.user) do 51 | text = text..v..'\n' 52 | end 53 | elseif has_usage_data(plugin) then -- Is not empty 54 | text = text..plugin.usage.user..'\n' 55 | end 56 | elseif ku == 'moderator' then -- usage for moderator 57 | if requester == 'moderator' or requester == 'admin' or requester == 'sudo' then 58 | if (type(plugin.usage.moderator) == "table") then 59 | for k,v in pairs(plugin.usage.moderator) do 60 | text = text..v..'\n' 61 | end 62 | elseif has_usage_data(plugin) then -- Is not empty 63 | text = text..plugin.usage.moderator..'\n' 64 | end 65 | end 66 | elseif ku == 'admin' then -- usage for admin 67 | if requester == 'admin' or requester == 'sudo' then 68 | if (type(plugin.usage.admin) == "table") then 69 | for k,v in pairs(plugin.usage.admin) do 70 | text = text..v..'\n' 71 | end 72 | elseif has_usage_data(plugin) then -- Is not empty 73 | text = text..plugin.usage.admin..'\n' 74 | end 75 | end 76 | elseif ku == 'sudo' then -- usage for sudo 77 | if requester == 'sudo' then 78 | if (type(plugin.usage.sudo) == "table") then 79 | for k,v in pairs(plugin.usage.sudo) do 80 | text = text..v..'\n' 81 | end 82 | elseif has_usage_data(plugin) then -- Is not empty 83 | text = text..plugin.usage.sudo..'\n' 84 | end 85 | end 86 | else 87 | text = text..usage..'\n' 88 | end 89 | end 90 | text = text..'======================\n' 91 | elseif has_usage_data(plugin) then -- Is not empty 92 | text = text..plugin.usage..'\n======================\n' 93 | end 94 | return text 95 | end 96 | 97 | 98 | -- !help command 99 | local function telegram_help() 100 | local i = 0 101 | local text = "Plugins list:\n\n" 102 | -- Plugins names 103 | for name in pairsByKeys(plugins) do 104 | if plugins[name].hidden then 105 | name = nil 106 | else 107 | i = i + 1 108 | text = text..i..'. '..name..'\n' 109 | end 110 | end 111 | text = text..'\n'..'There are '..i..' plugins help available.' 112 | text = text..'\n'..'Write "!help [plugin name]" or "!help [plugin number]" for more info.' 113 | text = text..'\n'..'Or "!help all" to show all info.' 114 | return text 115 | end 116 | 117 | 118 | -- !help all command 119 | local function help_all(requester) 120 | local ret = "" 121 | for name in pairsByKeys(plugins) do 122 | if plugins[name].hidden then 123 | name = nil 124 | else 125 | ret = ret .. plugin_help(name, nil, requester) 126 | end 127 | end 128 | return ret 129 | end 130 | 131 | local function run(msg, matches) 132 | if is_sudo(msg) then 133 | requester = "sudo" 134 | elseif is_admin(msg) then 135 | requester = "admin" 136 | elseif is_momod(msg) then 137 | requester = "moderator" 138 | else 139 | requester = "user" 140 | end 141 | if matches[1] == "!hhelp" then 142 | return telegram_help() 143 | elseif matches[1] == "!hhelp all" then 144 | return help_all(requester) 145 | else 146 | local text = "" 147 | if tonumber(matches[1]) then 148 | text = plugin_help(nil, matches[1], requester) 149 | else 150 | text = plugin_help(matches[1], nil, requester) 151 | end 152 | if not text then 153 | text = telegram_help() 154 | end 155 | return text 156 | end 157 | end 158 | 159 | return { 160 | description = "Help plugin. Get info from other plugins. ", 161 | usage = { 162 | "!hhelp: Show list of plugins.", 163 | "!hhelp all: Show all commands for every plugin.", 164 | "!hhelp [plugin name]: Commands for that plugin.", 165 | "!hhelp [number]: Commands for that plugin. Type !help to get the plugin number." 166 | }, 167 | patterns = { 168 | "^!hhelp$", 169 | "^!hhelp all", 170 | "^!hhelp (.+)" 171 | }, 172 | run = run 173 | } 174 | 175 | end -------------------------------------------------------------------------------- /plugins/id.lua: -------------------------------------------------------------------------------- 1 | local function usernameinfo (user) 2 | if user.username then 3 | return '@'..user.username 4 | end 5 | if user.print_name then 6 | return user.print_name 7 | end 8 | local text = '' 9 | if user.first_name then 10 | text = user.last_name..' ' 11 | end 12 | if user.lastname then 13 | text = text..user.last_name 14 | end 15 | return text 16 | end 17 | 18 | local function channelUserIDs (extra, success, result) 19 | local receiver = extra.receiver 20 | print('Result') 21 | vardump(result) 22 | 23 | local text = '' 24 | for k,user in ipairs(result) do 25 | local id = user.peer_id 26 | local username = usernameinfo (user) 27 | text = text..("%s - %s\n"):format(username, id) 28 | end 29 | send_large_msg(receiver, text) 30 | end 31 | 32 | local function returnids (extra, success, result) 33 | local receiver = extra.receiver 34 | local chatname = result.print_name 35 | local id = result.peer_id 36 | 37 | local text = ('ID for chat %s (%s):\n'):format(chatname, id) 38 | for k,user in ipairs(result.members) do 39 | local username = usernameinfo(user) 40 | local id = user.peer_id 41 | local userinfo = ("%s - %s\n"):format(username, id) 42 | text = text .. userinfo 43 | end 44 | send_large_msg(receiver, text) 45 | end 46 | 47 | local function run(msg, matches) 48 | local receiver = get_receiver(msg) 49 | 50 | -- Id of the user and info about group / channel 51 | if matches[1] == "/id" then 52 | if msg.to.type == 'channel' then 53 | return ('Chat ID: %s\nUser ID: %s'):format(msg.to.id, msg.from.id) 54 | end 55 | if msg.to.type == 'chat' then 56 | return ('Chat ID: %s\nUser ID: %s'):format(msg.to.id, msg.from.id) 57 | end 58 | return ('User ID: %s'):format(msg.from.id) 59 | elseif matches[1] == 'chat' or matches[1] == 'channel' then 60 | local type = matches[1] 61 | local chanId = matches[2] 62 | -- !ids? (chat) (%d+) 63 | if chanId then 64 | local chan = ("%s#id%s"):format(type, chanId) 65 | if type == 'chat' then 66 | chat_info(chan, returnids, {receiver=receiver}) 67 | else 68 | channel_get_users(chan, channelUserIDs, {receiver=receiver}) 69 | end 70 | else 71 | -- !id chat/channel 72 | local chan = ("%s#id%s"):format(msg.to.type, msg.to.id) 73 | if msg.to.type == 'channel' then 74 | channel_get_users(chan, channelUserIDs, {receiver=receiver}) 75 | end 76 | if msg.to.type == 'chat' then 77 | chat_info(chan, returnids, {receiver=receiver}) 78 | else 79 | return "You are not in a group." 80 | end 81 | end 82 | elseif matches[1] == "member" and matches[2] == "@" then 83 | 84 | local nick = matches[3] 85 | local chan = get_receiver(msg) 86 | 87 | if msg.to.type == 'chat' then 88 | chat_info(chan, function (extra, success, result) 89 | local receiver = extra.receiver 90 | local nick = extra.nick 91 | local found 92 | for k,user in pairs(result.members) do 93 | if user.username == nick then 94 | found = user 95 | end 96 | end 97 | if not found then 98 | send_msg(receiver, "User not found on this chat.", ok_cb, false) 99 | else 100 | local text = found.peer_id 101 | send_msg(receiver, text, ok_cb, false) 102 | end 103 | end, {receiver=chan, nick=nick}) 104 | elseif msg.to.type == 'channel' then 105 | -- TODO 106 | return 'Channels currently not supported' 107 | else 108 | return 'You are not in a group' 109 | end 110 | elseif matches[1] == "members" and matches[2] == "name" then 111 | 112 | local text = matches[3] 113 | local chan = get_receiver(msg) 114 | 115 | if msg.to.type == 'chat' then 116 | chat_info(chan, function (extra, success, result) 117 | local members = result.members 118 | local receiver = extra.receiver 119 | local text = extra.text 120 | 121 | local founds = {} 122 | for k,member in pairs(members) do 123 | local fields = {'first_name', 'print_name', 'username'} 124 | for k,field in pairs(fields) do 125 | if member[field] and type(member[field]) == "string" then 126 | if member[field]:match(text) then 127 | local id = tostring(member.peer_id) 128 | founds[id] = member 129 | end 130 | end 131 | end 132 | end 133 | if next(founds) == nil then -- Empty table 134 | send_msg(receiver, "User not found on this chat.", ok_cb, false) 135 | else 136 | local text = "" 137 | for k,user in pairs(founds) do 138 | local first_name = user.first_name or "" 139 | local print_name = user.print_name or "" 140 | local user_name = user.user_name or "" 141 | local id = user.peer_id or "" -- This would be funny 142 | text = text.."First name: "..first_name.."\n" 143 | .."Print name: "..print_name.."\n" 144 | .."User name: "..user_name.."\n" 145 | .."ID: "..id 146 | end 147 | send_msg(receiver, text, ok_cb, false) 148 | end 149 | end, {receiver=chan, text=text}) 150 | elseif msg.to.type == 'channel' then 151 | -- TODO 152 | return 'Channels currently not supported' 153 | else 154 | return 'You are not in a group' 155 | end 156 | end 157 | end 158 | 159 | return { 160 | description = "Know your id or the id of a chat members.", 161 | usage = { 162 | "/id: Return your ID and the chat id if you are in one.", 163 | "/ids chat: Return the IDs of the current chat members.", 164 | "/ids chat : Return the IDs of the members.", 165 | "/ids channel: Return the IDs of the current channel members.", 166 | "/ids channel : Return the IDs of the members.", 167 | "/id member @: Return the member @ ID from the current chat", 168 | "/id members name : Search for users with on first_name, print_name or username on current chat" 169 | }, 170 | patterns = { 171 | "^/id$", 172 | "^/ids? (chat) (%d+)$", 173 | "^/ids? (chat)$", 174 | "^/ids (channel)$", 175 | "^/ids (channel) (%d+)$", 176 | "^/id (member) (@)(.+)", 177 | "^/id (members) (name) (.+)" 178 | }, 179 | run = run 180 | } 181 | -------------------------------------------------------------------------------- /bot/bot.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ';.luarocks/share/lua/5.2/?.lua' 2 | ..';.luarocks/share/lua/5.2/?/init.lua' 3 | package.cpath = package.cpath .. ';.luarocks/lib/lua/5.2/?.so' 4 | 5 | require("./bot/utils") 6 | 7 | local f = assert(io.popen('/usr/bin/git describe --tags', 'r')) 8 | VERSION = assert(f:read('*a')) 9 | f:close() 10 | 11 | -- This function is called when tg receive a msg 12 | function on_msg_receive (msg) 13 | if not started then 14 | return 15 | end 16 | 17 | msg = backward_msg_format(msg) 18 | 19 | local receiver = get_receiver(msg) 20 | 21 | -- vardump(msg) 22 | msg = pre_process_service_msg(msg) 23 | if msg_valid(msg) then 24 | msg = pre_process_msg(msg) 25 | if msg then 26 | match_plugins(msg) 27 | mark_read(receiver, ok_cb, false) 28 | end 29 | end 30 | end 31 | 32 | function ok_cb(extra, success, result) 33 | end 34 | 35 | function on_binlog_replay_end() 36 | started = true 37 | postpone (cron_plugins, false, 60*5.0) 38 | -- See plugins/isup.lua as an example for cron 39 | 40 | _config = load_config() 41 | 42 | -- load plugins 43 | plugins = {} 44 | load_plugins() 45 | end 46 | 47 | function msg_valid(msg) 48 | -- Don't process outgoing messages 49 | if msg.out then 50 | print('\27[36mNot valid: msg from us\27[39m') 51 | return false 52 | end 53 | 54 | -- Before bot was started 55 | if msg.date < now then 56 | print('\27[36mNot valid: old msg\27[39m') 57 | return false 58 | end 59 | 60 | if msg.unread == 0 then 61 | print('\27[36mNot valid: readed\27[39m') 62 | return false 63 | end 64 | 65 | if not msg.to.id then 66 | print('\27[36mNot valid: To id not provided\27[39m') 67 | return false 68 | end 69 | 70 | if not msg.from.id then 71 | print('\27[36mNot valid: From id not provided\27[39m') 72 | return false 73 | end 74 | 75 | if msg.from.id == our_id then 76 | print('\27[36mNot valid: Msg from our id\27[39m') 77 | return false 78 | end 79 | 80 | if msg.to.type == 'encr_chat' then 81 | print('\27[36mNot valid: Encrypted chat\27[39m') 82 | return false 83 | end 84 | 85 | if msg.from.id == 777000 then 86 | print('\27[36mNot valid: Telegram message\27[39m') 87 | return false 88 | end 89 | 90 | return true 91 | end 92 | 93 | -- 94 | function pre_process_service_msg(msg) 95 | if msg.service then 96 | local action = msg.action or {type=""} 97 | -- Double ! to discriminate of normal actions 98 | msg.text = "!!tgservice " .. action.type 99 | 100 | -- wipe the data to allow the bot to read service messages 101 | if msg.out then 102 | msg.out = false 103 | end 104 | if msg.from.id == our_id then 105 | msg.from.id = 0 106 | end 107 | end 108 | return msg 109 | end 110 | 111 | -- Apply plugin.pre_process function 112 | function pre_process_msg(msg) 113 | for name,plugin in pairs(plugins) do 114 | if plugin.pre_process and msg then 115 | print('Preprocess', name) 116 | msg = plugin.pre_process(msg) 117 | end 118 | end 119 | 120 | return msg 121 | end 122 | 123 | -- Go over enabled plugins patterns. 124 | function match_plugins(msg) 125 | for name, plugin in pairs(plugins) do 126 | match_plugin(plugin, name, msg) 127 | end 128 | end 129 | 130 | -- Check if plugin is on _config.disabled_plugin_on_chat table 131 | local function is_plugin_disabled_on_chat(plugin_name, receiver) 132 | local disabled_chats = _config.disabled_plugin_on_chat 133 | -- Table exists and chat has disabled plugins 134 | if disabled_chats and disabled_chats[receiver] then 135 | -- Checks if plugin is disabled on this chat 136 | for disabled_plugin,disabled in pairs(disabled_chats[receiver]) do 137 | if disabled_plugin == plugin_name and disabled then 138 | if plugins[disabled_plugin].hidden then 139 | print('Plugin '..disabled_plugin..' is disabled on this chat') 140 | else 141 | local warning = 'Plugin '..disabled_plugin..' is disabled on this chat' 142 | print(warning) 143 | send_msg(receiver, warning, ok_cb, false) 144 | end 145 | return true 146 | end 147 | end 148 | end 149 | return false 150 | end 151 | 152 | function match_plugin(plugin, plugin_name, msg) 153 | local receiver = get_receiver(msg) 154 | 155 | -- Go over patterns. If one matches it's enough. 156 | for k, pattern in pairs(plugin.patterns) do 157 | local matches = match_pattern(pattern, msg.text) 158 | if matches then 159 | print("msg matches: ", pattern) 160 | 161 | if is_plugin_disabled_on_chat(plugin_name, receiver) then 162 | return nil 163 | end 164 | -- Function exists 165 | if plugin.run then 166 | -- If plugin is for privileged users only 167 | if not warns_user_not_allowed(plugin, msg) then 168 | local result = plugin.run(msg, matches) 169 | if result then 170 | send_large_msg(receiver, result) 171 | end 172 | end 173 | end 174 | -- One patterns matches 175 | return 176 | end 177 | end 178 | end 179 | 180 | -- DEPRECATED, use send_large_msg(destination, text) 181 | function _send_msg(destination, text) 182 | send_large_msg(destination, text) 183 | end 184 | 185 | -- Save the content of _config to config.lua 186 | function save_config( ) 187 | serialize_to_file(_config, './data/config.lua') 188 | print ('saved config into ./data/config.lua') 189 | end 190 | 191 | -- Returns the config from config.lua file. 192 | -- If file doesn't exist, create it. 193 | function load_config( ) 194 | local f = io.open('./data/config.lua', "r") 195 | -- If config.lua doesn't exist 196 | if not f then 197 | print ("Created new config file: data/config.lua") 198 | create_config() 199 | else 200 | f:close() 201 | end 202 | local config = loadfile ("./data/config.lua")() 203 | for v,user in pairs(config.sudo_users) do 204 | print("Allowed user: " .. user) 205 | end 206 | return config 207 | end 208 | 209 | -- Create a basic config.json file and saves it. 210 | function create_config( ) 211 | -- A simple config with basic plugins and ourselves as privileged user 212 | config = { 213 | enabled_plugins = { 214 | "help", 215 | "id", 216 | "plugins", 217 | }, 218 | sudo_users = {our_id}, 219 | disabled_channels = {}, 220 | moderation = {data = 'data/moderation.json'} 221 | } 222 | serialize_to_file(config, './data/config.lua') 223 | print ('saved config into ./data/config.lua') 224 | end 225 | 226 | function on_our_id (id) 227 | our_id = id 228 | end 229 | 230 | function on_user_update (user, what) 231 | --vardump (user) 232 | end 233 | 234 | function on_chat_update (chat, what) 235 | --vardump (chat) 236 | end 237 | 238 | function on_secret_chat_update (schat, what) 239 | --vardump (schat) 240 | end 241 | 242 | function on_get_difference_end () 243 | end 244 | 245 | -- Enable plugins in config.json 246 | function load_plugins() 247 | for k, v in pairs(_config.enabled_plugins) do 248 | print("Loading plugin", v) 249 | 250 | local ok, err = pcall(function() 251 | local t = loadfile("plugins/"..v..'.lua')() 252 | plugins[v] = t 253 | end) 254 | 255 | if not ok then 256 | print('\27[31mError loading plugin '..v..'\27[39m') 257 | print('\27[31m'..err..'\27[39m') 258 | end 259 | 260 | end 261 | end 262 | 263 | function load_data(filename) 264 | 265 | local f = io.open(filename) 266 | if not f then 267 | return {} 268 | end 269 | local s = f:read('*all') 270 | f:close() 271 | local data = JSON.decode(s) 272 | 273 | return data 274 | 275 | end 276 | 277 | function save_data(filename, data) 278 | 279 | local s = JSON.encode(data) 280 | local f = io.open(filename, 'w') 281 | f:write(s) 282 | f:close() 283 | 284 | end 285 | 286 | -- Call and postpone execution for cron plugins 287 | function cron_plugins() 288 | 289 | for name, plugin in pairs(plugins) do 290 | -- Only plugins with cron function 291 | if plugin.cron ~= nil then 292 | plugin.cron() 293 | end 294 | end 295 | 296 | -- Called again in 5 mins 297 | postpone (cron_plugins, false, 5*60.0) 298 | end 299 | 300 | -- Start and load values 301 | our_id = 0 302 | now = os.time() 303 | math.randomseed(now) 304 | started = false 305 | -------------------------------------------------------------------------------- /bot/utils.lua: -------------------------------------------------------------------------------- 1 | URL = require "socket.url" 2 | http = require "socket.http" 3 | https = require "ssl.https" 4 | ltn12 = require "ltn12" 5 | serpent = require "serpent" 6 | feedparser = require "feedparser" 7 | 8 | json = (loadfile "./libs/JSON.lua")() 9 | mimetype = (loadfile "./libs/mimetype.lua")() 10 | redis = (loadfile "./libs/redis.lua")() 11 | JSON = (loadfile "./libs/dkjson.lua")() 12 | 13 | http.TIMEOUT = 10 14 | 15 | function get_receiver(msg) 16 | if msg.to.type == 'user' then 17 | return 'user#id'..msg.from.id 18 | end 19 | if msg.to.type == 'chat' then 20 | return 'chat#id'..msg.to.id 21 | end 22 | if msg.to.type == 'encr_chat' then 23 | return msg.to.print_name 24 | end 25 | if msg.to.type == 'channel' then 26 | return 'channel#id'..msg.to.id 27 | end 28 | end 29 | 30 | function is_user_msg( msg ) 31 | if msg.to.type == 'user' then 32 | return true 33 | end 34 | return false 35 | end 36 | 37 | function is_chat_msg( msg ) 38 | if msg.to.type == 'chat' then 39 | return true 40 | end 41 | return false 42 | end 43 | 44 | function is_channel_msg( msg ) 45 | if msg.to.type == 'channel' then 46 | return true 47 | end 48 | return false 49 | end 50 | function string.random(length) 51 | local str = ""; 52 | for i = 1, length do 53 | math.random(97, 122) 54 | str = str..string.char(math.random(97, 122)); 55 | end 56 | return str; 57 | end 58 | 59 | function string:split(sep) 60 | local sep, fields = sep or ":", {} 61 | local pattern = string.format("([^%s]+)", sep) 62 | self:gsub(pattern, function(c) fields[#fields+1] = c end) 63 | return fields 64 | end 65 | 66 | -- DEPRECATED 67 | function string.trim(s) 68 | print("string.trim(s) is DEPRECATED use string:trim() instead") 69 | return s:gsub("^%s*(.-)%s*$", "%1") 70 | end 71 | 72 | -- Removes spaces 73 | function string:trim() 74 | return self:gsub("^%s*(.-)%s*$", "%1") 75 | end 76 | 77 | function get_http_file_name(url, headers) 78 | -- Eg: foo.var 79 | local file_name = url:match("[^%w]+([%.%w]+)$") 80 | -- Any delimited alphanumeric on the url 81 | file_name = file_name or url:match("[^%w]+(%w+)[^%w]+$") 82 | -- Random name, hope content-type works 83 | file_name = file_name or str:random(5) 84 | 85 | local content_type = headers["content-type"] 86 | 87 | local extension = nil 88 | if content_type then 89 | extension = mimetype.get_mime_extension(content_type) 90 | end 91 | if extension then 92 | file_name = file_name.."."..extension 93 | end 94 | 95 | local disposition = headers["content-disposition"] 96 | if disposition then 97 | -- attachment; filename=CodeCogsEqn.png 98 | file_name = disposition:match('filename=([^;]+)') or file_name 99 | end 100 | 101 | return file_name 102 | end 103 | 104 | -- Saves file to /tmp/. If file_name isn't provided, 105 | -- will get the text after the last "/" for filename 106 | -- and content-type for extension 107 | function download_to_file(url, file_name) 108 | print("url to download: "..url) 109 | 110 | local respbody = {} 111 | local options = { 112 | url = url, 113 | sink = ltn12.sink.table(respbody), 114 | redirect = true 115 | } 116 | 117 | -- nil, code, headers, status 118 | local response = nil 119 | 120 | if url:starts('https') then 121 | options.redirect = false 122 | response = {https.request(options)} 123 | else 124 | response = {http.request(options)} 125 | end 126 | 127 | local code = response[2] 128 | local headers = response[3] 129 | local status = response[4] 130 | 131 | if code ~= 200 then return nil end 132 | 133 | file_name = file_name or get_http_file_name(url, headers) 134 | 135 | local file_path = "/tmp/"..file_name 136 | print("Saved to: "..file_path) 137 | 138 | file = io.open(file_path, "w+") 139 | file:write(table.concat(respbody)) 140 | file:close() 141 | 142 | return file_path 143 | end 144 | 145 | function vardump(value) 146 | print(serpent.block(value, {comment=false})) 147 | end 148 | 149 | -- taken from http://stackoverflow.com/a/11130774/3163199 150 | function scandir(directory) 151 | local i, t, popen = 0, {}, io.popen 152 | for filename in popen('ls -a "'..directory..'"'):lines() do 153 | i = i + 1 154 | t[i] = filename 155 | end 156 | return t 157 | end 158 | 159 | -- http://www.lua.org/manual/5.2/manual.html#pdf-io.popen 160 | function run_command(str) 161 | local cmd = io.popen(str) 162 | local result = cmd:read('*all') 163 | cmd:close() 164 | return result 165 | end 166 | 167 | -- User has superuser privileges 168 | function is_sudo(msg) 169 | local var = false 170 | -- Check users id in config 171 | for v,user in pairs(_config.sudo_users) do 172 | if user == msg.from.id then 173 | var = true 174 | end 175 | end 176 | return var 177 | end 178 | 179 | -- user has admins privileges 180 | function is_admin(msg) 181 | local var = false 182 | local data = load_data(_config.moderation.data) 183 | local user = msg.from.id 184 | local admins = 'admins' 185 | if data[tostring(admins)] then 186 | if data[tostring(admins)][tostring(user)] then 187 | var = true 188 | end 189 | end 190 | for v,user in pairs(_config.sudo_users) do 191 | if user == msg.from.id then 192 | var = true 193 | end 194 | end 195 | return var 196 | end 197 | 198 | -- user has moderator privileges 199 | function is_momod(msg) 200 | local var = false 201 | local data = load_data(_config.moderation.data) 202 | local user = msg.from.id 203 | if data[tostring(msg.to.id)] then 204 | if data[tostring(msg.to.id)]['moderators'] then 205 | if data[tostring(msg.to.id)]['moderators'][tostring(user)] then 206 | var = true 207 | end 208 | end 209 | end 210 | if data['admins'] then 211 | if data['admins'][tostring(user)] then 212 | var = true 213 | end 214 | end 215 | for v,user in pairs(_config.sudo_users) do 216 | if user == msg.from.id then 217 | var = true 218 | end 219 | end 220 | return var 221 | end 222 | 223 | -- Returns the name of the sender 224 | function get_name(msg) 225 | local name = msg.from.first_name 226 | if name == nil then 227 | name = msg.from.id 228 | end 229 | return name 230 | end 231 | 232 | -- Returns at table of lua files inside plugins 233 | function plugins_names( ) 234 | local files = {} 235 | for k, v in pairs(scandir("plugins")) do 236 | -- Ends with .lua 237 | if (v:match(".lua$")) then 238 | table.insert(files, v) 239 | end 240 | end 241 | return files 242 | end 243 | 244 | -- Function name explains what it does. 245 | function file_exists(name) 246 | local f = io.open(name,"r") 247 | if f ~= nil then 248 | io.close(f) 249 | return true 250 | else 251 | return false 252 | end 253 | end 254 | 255 | -- Save into file the data serialized for lua. 256 | -- Set uglify true to minify the file. 257 | function serialize_to_file(data, file, uglify) 258 | file = io.open(file, 'w+') 259 | local serialized 260 | if not uglify then 261 | serialized = serpent.block(data, { 262 | comment = false, 263 | name = '_' 264 | }) 265 | else 266 | serialized = serpent.dump(data) 267 | end 268 | file:write(serialized) 269 | file:close() 270 | end 271 | 272 | -- Returns true if the string is empty 273 | function string:isempty() 274 | return self == nil or self == '' 275 | end 276 | 277 | -- Returns true if the string is blank 278 | function string:isblank() 279 | self = self:trim() 280 | return self:isempty() 281 | end 282 | 283 | -- DEPRECATED!!!!! 284 | function string.starts(String, Start) 285 | print("string.starts(String, Start) is DEPRECATED use string:starts(text) instead") 286 | return Start == string.sub(String,1,string.len(Start)) 287 | end 288 | 289 | -- Returns true if String starts with Start 290 | function string:starts(text) 291 | return text == string.sub(self,1,string.len(text)) 292 | end 293 | 294 | -- Send image to user and delete it when finished. 295 | -- cb_function and cb_extra are optionals callback 296 | function _send_photo(receiver, file_path, cb_function, cb_extra) 297 | local cb_extra = { 298 | file_path = file_path, 299 | cb_function = cb_function, 300 | cb_extra = cb_extra 301 | } 302 | -- Call to remove with optional callback 303 | send_photo(receiver, file_path, cb_function, cb_extra) 304 | end 305 | 306 | -- Download the image and send to receiver, it will be deleted. 307 | -- cb_function and cb_extra are optionals callback 308 | function send_photo_from_url(receiver, url, cb_function, cb_extra) 309 | -- If callback not provided 310 | cb_function = cb_function or ok_cb 311 | cb_extra = cb_extra or false 312 | 313 | local file_path = download_to_file(url, false) 314 | if not file_path then -- Error 315 | local text = 'Error downloading the image' 316 | send_msg(receiver, text, cb_function, cb_extra) 317 | else 318 | print("File path: "..file_path) 319 | _send_photo(receiver, file_path, cb_function, cb_extra) 320 | end 321 | end 322 | 323 | -- Same as send_photo_from_url but as callback function 324 | function send_photo_from_url_callback(cb_extra, success, result) 325 | local receiver = cb_extra.receiver 326 | local url = cb_extra.url 327 | 328 | local file_path = download_to_file(url, false) 329 | if not file_path then -- Error 330 | local text = 'Error downloading the image' 331 | send_msg(receiver, text, ok_cb, false) 332 | else 333 | print("File path: "..file_path) 334 | _send_photo(receiver, file_path, ok_cb, false) 335 | end 336 | end 337 | 338 | -- Send multiple images asynchronous. 339 | -- param urls must be a table. 340 | function send_photos_from_url(receiver, urls) 341 | local cb_extra = { 342 | receiver = receiver, 343 | urls = urls, 344 | remove_path = nil 345 | } 346 | send_photos_from_url_callback(cb_extra) 347 | end 348 | 349 | -- Use send_photos_from_url. 350 | -- This function might be difficult to understand. 351 | function send_photos_from_url_callback(cb_extra, success, result) 352 | -- cb_extra is a table containing receiver, urls and remove_path 353 | local receiver = cb_extra.receiver 354 | local urls = cb_extra.urls 355 | local remove_path = cb_extra.remove_path 356 | 357 | -- The previously image to remove 358 | if remove_path ~= nil then 359 | os.remove(remove_path) 360 | print("Deleted: "..remove_path) 361 | end 362 | 363 | -- Nil or empty, exit case (no more urls) 364 | if urls == nil or #urls == 0 then 365 | return false 366 | end 367 | 368 | -- Take the head and remove from urls table 369 | local head = table.remove(urls, 1) 370 | 371 | local file_path = download_to_file(head, false) 372 | local cb_extra = { 373 | receiver = receiver, 374 | urls = urls, 375 | remove_path = file_path 376 | } 377 | 378 | -- Send first and postpone the others as callback 379 | send_photo(receiver, file_path, send_photos_from_url_callback, cb_extra) 380 | end 381 | 382 | -- Callback to remove a file 383 | function rmtmp_cb(cb_extra, success, result) 384 | local file_path = cb_extra.file_path 385 | local cb_function = cb_extra.cb_function or ok_cb 386 | local cb_extra = cb_extra.cb_extra 387 | 388 | if file_path ~= nil then 389 | os.remove(file_path) 390 | print("Deleted: "..file_path) 391 | end 392 | -- Finally call the callback 393 | cb_function(cb_extra, success, result) 394 | end 395 | 396 | -- Send document to user and delete it when finished. 397 | -- cb_function and cb_extra are optionals callback 398 | function _send_document(receiver, file_path, cb_function, cb_extra) 399 | local cb_extra = { 400 | file_path = file_path, 401 | cb_function = cb_function or ok_cb, 402 | cb_extra = cb_extra or false 403 | } 404 | -- Call to remove with optional callback 405 | send_document(receiver, file_path, rmtmp_cb, cb_extra) 406 | end 407 | 408 | -- Download the image and send to receiver, it will be deleted. 409 | -- cb_function and cb_extra are optionals callback 410 | function send_document_from_url(receiver, url, cb_function, cb_extra) 411 | local file_path = download_to_file(url, false) 412 | print("File path: "..file_path) 413 | _send_document(receiver, file_path, cb_function, cb_extra) 414 | end 415 | 416 | -- Parameters in ?a=1&b=2 style 417 | function format_http_params(params, is_get) 418 | local str = '' 419 | -- If is get add ? to the beginning 420 | if is_get then str = '?' end 421 | local first = true -- Frist param 422 | for k,v in pairs (params) do 423 | if v then -- nil value 424 | if first then 425 | first = false 426 | str = str..k.. "="..v 427 | else 428 | str = str.."&"..k.. "="..v 429 | end 430 | end 431 | end 432 | return str 433 | end 434 | 435 | -- Check if user can use the plugin and warns user 436 | -- Returns true if user was warned and false if not warned (is allowed) 437 | function warns_user_not_allowed(plugin, msg) 438 | if not user_allowed(plugin, msg) then 439 | local text = 'This plugin requires privileged user' 440 | local receiver = get_receiver(msg) 441 | send_msg(receiver, text, ok_cb, false) 442 | return true 443 | else 444 | return false 445 | end 446 | end 447 | 448 | -- Check if user can use the plugin 449 | function user_allowed(plugin, msg) 450 | if plugin.privileged and not is_sudo(msg) then 451 | return false 452 | end 453 | return true 454 | end 455 | 456 | 457 | function send_order_msg(destination, msgs) 458 | local cb_extra = { 459 | destination = destination, 460 | msgs = msgs 461 | } 462 | send_order_msg_callback(cb_extra, true) 463 | end 464 | 465 | function send_order_msg_callback(cb_extra, success, result) 466 | local destination = cb_extra.destination 467 | local msgs = cb_extra.msgs 468 | local file_path = cb_extra.file_path 469 | if file_path ~= nil then 470 | os.remove(file_path) 471 | print("Deleted: " .. file_path) 472 | end 473 | if type(msgs) == 'string' then 474 | send_large_msg(destination, msgs) 475 | elseif type(msgs) ~= 'table' then 476 | return 477 | end 478 | if #msgs < 1 then 479 | return 480 | end 481 | local msg = table.remove(msgs, 1) 482 | local new_cb_extra = { 483 | destination = destination, 484 | msgs = msgs 485 | } 486 | if type(msg) == 'string' then 487 | send_msg(destination, msg, send_order_msg_callback, new_cb_extra) 488 | elseif type(msg) == 'table' then 489 | local typ = msg[1] 490 | local nmsg = msg[2] 491 | new_cb_extra.file_path = nmsg 492 | if typ == 'document' then 493 | send_document(destination, nmsg, send_order_msg_callback, new_cb_extra) 494 | elseif typ == 'image' or typ == 'photo' then 495 | send_photo(destination, nmsg, send_order_msg_callback, new_cb_extra) 496 | elseif typ == 'audio' then 497 | send_audio(destination, nmsg, send_order_msg_callback, new_cb_extra) 498 | elseif typ == 'video' then 499 | send_video(destination, nmsg, send_order_msg_callback, new_cb_extra) 500 | else 501 | send_file(destination, nmsg, send_order_msg_callback, new_cb_extra) 502 | end 503 | end 504 | end 505 | 506 | -- Same as send_large_msg_callback but friendly params 507 | function send_large_msg(destination, text) 508 | local cb_extra = { 509 | destination = destination, 510 | text = text 511 | } 512 | send_large_msg_callback(cb_extra, true) 513 | end 514 | 515 | -- If text is longer than 4096 chars, send multiple msg. 516 | -- https://core.telegram.org/method/messages.sendMessage 517 | function send_large_msg_callback(cb_extra, success, result) 518 | local text_max = 4096 519 | 520 | local destination = cb_extra.destination 521 | local text = cb_extra.text 522 | local text_len = string.len(text) 523 | local num_msg = math.ceil(text_len / text_max) 524 | 525 | if num_msg <= 1 then 526 | send_msg(destination, text, ok_cb, false) 527 | else 528 | 529 | local my_text = string.sub(text, 1, 4096) 530 | local rest = string.sub(text, 4096, text_len) 531 | 532 | local cb_extra = { 533 | destination = destination, 534 | text = rest 535 | } 536 | 537 | send_msg(destination, my_text, send_large_msg_callback, cb_extra) 538 | end 539 | end 540 | 541 | -- Returns a table with matches or nil 542 | function match_pattern(pattern, text, lower_case) 543 | if text then 544 | local matches = {} 545 | if lower_case then 546 | matches = { string.match(text:lower(), pattern) } 547 | else 548 | matches = { string.match(text, pattern) } 549 | end 550 | if next(matches) then 551 | return matches 552 | end 553 | end 554 | -- nil 555 | end 556 | 557 | -- Function to read data from files 558 | function load_from_file(file, default_data) 559 | local f = io.open(file, "r+") 560 | -- If file doesn't exists 561 | if f == nil then 562 | -- Create a new empty table 563 | default_data = default_data or {} 564 | serialize_to_file(default_data, file) 565 | print ('Created file', file) 566 | else 567 | print ('Data loaded from file', file) 568 | f:close() 569 | end 570 | return loadfile (file)() 571 | end 572 | 573 | -- See http://stackoverflow.com/a/14899740 574 | function unescape_html(str) 575 | local map = { 576 | ["lt"] = "<", 577 | ["gt"] = ">", 578 | ["amp"] = "&", 579 | ["quot"] = '"', 580 | ["apos"] = "'" 581 | } 582 | new = string.gsub(str, '(&(#?x?)([%d%a]+);)', function(orig, n, s) 583 | var = map[s] or n == "#" and string.char(s) 584 | var = var or n == "#x" and string.char(tonumber(s,16)) 585 | var = var or orig 586 | return var 587 | end) 588 | return new 589 | end 590 | 591 | -- Workarrond to format the message as previously was received 592 | function backward_msg_format (msg) 593 | for k,name in ipairs({'from', 'to'}) do 594 | local longid = msg[name].id 595 | msg[name].id = msg[name].peer_id 596 | msg[name].peer_id = longid 597 | msg[name].type = msg[name].peer_type 598 | end 599 | if msg.action and (msg.action.user or msg.action.link_issuer) then 600 | local user = msg.action.user or msg.action.link_issuer 601 | local longid = user.id 602 | user.id = user.peer_id 603 | user.peer_id = longid 604 | user.type = user.peer_type 605 | end 606 | return msg 607 | end 608 | -------------------------------------------------------------------------------- /plugins/moderation.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function callback(extra, success, result) 4 | vardump(success) 5 | end 6 | 7 | local function is_spromoted(chat_id, user_id) 8 | local hash = 'sprom:'..chat_id..':'..user_id 9 | local spromoted = redis:get(hash) 10 | return spromoted or false 11 | end 12 | 13 | local function spromote(receiver, user_id, username) 14 | local chat_id = string.gsub(receiver, '.+#id', '') 15 | local data = load_data(_config.moderation.data) 16 | if not data[tostring(chat_id)] then 17 | return send_large_msg(receiver, 'Group is not added.') 18 | end 19 | if data[tostring(chat_id)]['moderators'][tostring(user_id)] then 20 | if is_spromoted(chat_id, user_id) then 21 | return send_large_msg(receiver, 'Already as moderator leader') 22 | end 23 | local hash = 'sprom:'..chat_id..':'..user_id 24 | redis:set(hash, true) 25 | send_large_msg(receiver, 'User '..username..' ['..user_id..'] promoted as moderator leader') 26 | return 27 | else 28 | data[tostring(chat_id)]['moderators'][tostring(user_id)] = string.gsub(username, '@', '') 29 | save_data(_config.moderation.data, data) 30 | local hash = 'sprom:'..chat_id..':'..user_id 31 | redis:set(hash, true) 32 | send_large_msg(receiver, 'User '..username..' ['..user_id..'] promoted as moderator leader') 33 | return 34 | end 35 | end 36 | 37 | local function sdemote(receiver, user_id, username) 38 | local chat_id = string.gsub(receiver, '.+#id', '') 39 | if not is_spromoted(chat_id, user_id) then 40 | return send_large_msg(receiver, 'Not a moderator leader') 41 | end 42 | local data = load_data(_config.moderation.data) 43 | data[chat_id]['moderators'][tostring(user_id)] = nil 44 | save_data(_config.moderation.data, data) 45 | local hash = 'sprom:'..chat_id..':'..user_id 46 | redis:del(hash) 47 | send_large_msg(receiver, 'User '..username..' ['..user_id..'] demoted!') 48 | end 49 | 50 | local function check_member(cb_extra, success, result) 51 | local receiver = cb_extra.receiver 52 | local data = cb_extra.data 53 | local msg = cb_extra.msg 54 | for k,v in pairs(result.members) do 55 | local user_id = v.id 56 | if user_id ~= our_id then 57 | local username = v.username 58 | data[tostring(msg.to.id)] = { 59 | moderators ={}, 60 | settings = { 61 | set_name = string.gsub(msg.to.print_name, '_', ' '), 62 | lock_name = 'no', 63 | lock_photo = 'no', 64 | lock_member = 'no', 65 | lock_bot = 'yes', 66 | lock_link = 'no', 67 | lock_inviteme = 'no', 68 | lock_sticker = 'no', 69 | lock_image = 'no', 70 | --lock_video = 'no', 71 | --lock_audio = 'no', 72 | lock_file = 'no', 73 | lock_talk = 'no' 74 | }, 75 | group_type = msg.to.type, 76 | blocked_words = {}, 77 | } 78 | save_data(_config.moderation.data, data) 79 | local hash = 'sprom:'..msg.to.id..':'..user_id 80 | redis:set(hash, true) 81 | return send_large_msg(receiver, 'You have been promoted as moderator for this group.') 82 | end 83 | end 84 | end 85 | 86 | local function modadd(msg) 87 | if not is_admin(msg) then 88 | return 89 | end 90 | local data = load_data(_config.moderation.data) 91 | if data[tostring(msg.to.id)] then 92 | return 'Group is already added.' 93 | end 94 | data[tostring(msg.to.id)] = { 95 | moderators ={}, 96 | settings = { 97 | set_name = string.gsub(msg.to.print_name, '_', ' '), 98 | lock_name = 'no', 99 | lock_photo = 'no', 100 | lock_member = 'no', 101 | lock_bot = 'yes', 102 | lock_link = 'no', 103 | lock_inviteme = 'no', 104 | lock_sticker = 'no', 105 | lock_image = 'no', 106 | --lock_video = 'no', 107 | --lock_audio = 'no', 108 | lock_file = 'no', 109 | lock_talk = 'no', 110 | }, 111 | group_type = msg.to.type, 112 | blocked_words = {}, 113 | } 114 | save_data(_config.moderation.data, data) 115 | return 'Group has been added.' 116 | end 117 | 118 | local function modrem(msg) 119 | if not is_admin(msg) then 120 | return "You're not admin" 121 | end 122 | local data = load_data(_config.moderation.data) 123 | local receiver = get_receiver(msg) 124 | if not data[tostring(msg.to.id)] then 125 | return 'Group is not added.' 126 | end 127 | 128 | data[tostring(msg.to.id)] = nil 129 | save_data(_config.moderation.data, data) 130 | 131 | return 'Group has been removed' 132 | end 133 | 134 | local function promote(receiver, username, user_id) 135 | local data = load_data(_config.moderation.data) 136 | local group = string.gsub(receiver, '.+#id', '') 137 | if not data[group] then 138 | return send_large_msg(receiver, 'Group is not added.') 139 | end 140 | if data[group]['moderators'][tostring(user_id)] then 141 | return send_large_msg(receiver, username..' is already a moderator.') 142 | end 143 | data[group]['moderators'][tostring(user_id)] = string.gsub(username, '@', '') 144 | save_data(_config.moderation.data, data) 145 | return send_large_msg(receiver, username..' has been promoted.') 146 | end 147 | 148 | local function demote(receiver, username, user_id) 149 | local data = load_data(_config.moderation.data) 150 | local group = string.gsub(receiver, '.+#id', '') 151 | if not data[group] then 152 | return send_large_msg(receiver, 'Group is not added.') 153 | end 154 | if not data[group]['moderators'][tostring(user_id)] then 155 | return send_large_msg(receiver, string.gsub(username, '@', '')..' is not a moderator.') 156 | end 157 | data[group]['moderators'][tostring(user_id)] = nil 158 | save_data(_config.moderation.data, data) 159 | return send_large_msg(receiver, username..' has been demoted.') 160 | end 161 | 162 | local function upmanager(receiver, username, user_id) 163 | channel_set_admin(receiver, 'user#id'..user_id, callback, false) 164 | return send_large_msg(receiver, 'Done!') 165 | end 166 | 167 | local function inmanager(receiver, username, user_id) 168 | channel_set_unadmin(receiver, 'user#id'..user_id, callback, false) 169 | return send_large_msg(receiver, 'Done!') 170 | end 171 | 172 | local function admin_promote(receiver, username, user_id) 173 | local data = load_data(_config.moderation.data) 174 | if not data['admins'] then 175 | data['admins'] = {} 176 | save_data(_config.moderation.data, data) 177 | end 178 | 179 | if data['admins'][tostring(user_id)] then 180 | return send_large_msg(receiver, username..' is already as admin.') 181 | end 182 | 183 | data['admins'][tostring(user_id)] = string.gsub(username, '@', '') 184 | save_data(_config.moderation.data, data) 185 | return send_large_msg(receiver, username..' has been promoted as admin.') 186 | end 187 | 188 | local function admin_demote(receiver, username, user_id) 189 | local data = load_data(_config.moderation.data) 190 | if not data['admins'] then 191 | data['admins'] = {} 192 | save_data(_config.moderation.data, data) 193 | end 194 | 195 | if not data['admins'][tostring(user_id)] then 196 | return send_large_msg(receiver, username..' is not an admin.') 197 | end 198 | 199 | data['admins'][tostring(user_id)] = nil 200 | save_data(_config.moderation.data, data) 201 | 202 | return send_large_msg(receiver, 'Admin '..username..' has been demoted.') 203 | end 204 | 205 | local function username_id(cb_extra, success, result) 206 | local get_cmd = cb_extra.get_cmd 207 | local receiver = cb_extra.receiver 208 | local member = cb_extra.member 209 | local text = 'No user @'..member..' in this group.' 210 | for k,v in pairs(result.members) do 211 | vusername = v.username 212 | if vusername == member then 213 | username = member 214 | user_id = v.peer_id 215 | if get_cmd == 'promote' then 216 | return promote(receiver, username, user_id) 217 | elseif get_cmd == 'demote' then 218 | if is_spromoted(string.gsub(receiver,'.+#id', ''), user_id) then 219 | return send_large_msg(receiver, 'Can\'t demote leader') 220 | end 221 | return demote(receiver, username, user_id) 222 | elseif get_cmd == 'adminprom' then 223 | if user_id == our_id then 224 | return 225 | end 226 | return admin_promote(receiver, username, user_id) 227 | elseif get_cmd == 'admindem' then 228 | if user_id == our_id then 229 | return 230 | end 231 | return admin_demote(receiver, username, user_id) 232 | elseif get_cmd == 'spromote' then 233 | return spromote(receiver, user_id, username) 234 | elseif get_cmd == 'sdemote' then 235 | return sdemote(receiver, user_id, username) 236 | end 237 | end 238 | end 239 | send_large_msg(receiver, text) 240 | end 241 | 242 | local function channel_username_id(cb_extra, success, result) 243 | local get_cmd = cb_extra.get_cmd 244 | local receiver = cb_extra.receiver 245 | local member = cb_extra.member 246 | local text = 'No user @'..member..' in this group.' 247 | for k,v in pairs(result) do 248 | vusername = v.username 249 | if vusername == member then 250 | username = member 251 | user_id = v.peer_id 252 | if get_cmd == 'promote' then 253 | return promote(receiver, username, user_id) 254 | elseif get_cmd == 'demote' then 255 | if is_spromoted(string.gsub(receiver,'.+#id', ''), user_id) then 256 | return send_large_msg(receiver, 'Can\'t demote leader') 257 | end 258 | return demote(receiver, username, user_id) 259 | elseif get_cmd == 'adminprom' then 260 | if user_id == our_id then 261 | return 262 | end 263 | return admin_promote(receiver, username, user_id) 264 | elseif get_cmd == 'admindem' then 265 | if user_id == our_id then 266 | return 267 | end 268 | return admin_demote(receiver, username, user_id) 269 | elseif get_cmd == 'spromote' then 270 | return spromote(receiver, user_id, username) 271 | elseif get_cmd == 'sdemote' then 272 | return sdemote(receiver, user_id, username) 273 | elseif get_cmd == 'upmanager' then 274 | return upmanager(receiver, username, user_id) 275 | elseif get_cmd == 'inmanager' then 276 | return inmanager(receiver, username, user_id) 277 | end 278 | end 279 | end 280 | send_large_msg(receiver, text) 281 | end 282 | 283 | local function get_msg_callback(extra, success, result) 284 | if success ~= 1 then return end 285 | local get_cmd = extra.get_cmd 286 | local receiver = extra.receiver 287 | local user_id = result.from.peer_id 288 | local chat_id = result.to.id 289 | if result.from.username then 290 | username = '@'..result.from.username 291 | else 292 | username = string.gsub(result.from.print_name, '_', ' ') 293 | end 294 | if get_cmd == 'spromote' then 295 | if user_id == our_id then 296 | return nil 297 | end 298 | return spromote(receiver, user_id, username) 299 | end 300 | if get_cmd == 'sdemote' then 301 | if user_id == our_id then 302 | return nil 303 | end 304 | return sdemote(receiver, user_id, username) 305 | end 306 | if get_cmd == 'promote' then 307 | if user_id == our_id then 308 | return nil 309 | end 310 | return promote(receiver, username, user_id) 311 | end 312 | if get_cmd == 'demote' then 313 | if user_id == our_id then 314 | return nil 315 | end 316 | if is_spromoted(chat_id, user_id) then 317 | return send_large_msg(receiver, 'Can\'t demote leader') 318 | end 319 | return demote(receiver, username, user_id) 320 | end 321 | if get_cmd == 'upmanager' then 322 | return upmanager(receiver, username, user_id) 323 | end 324 | if get_cmd == 'inmanager' then 325 | return inmanager(receiver, username, user_id) 326 | end 327 | end 328 | 329 | local function modlist(msg) 330 | local data = load_data(_config.moderation.data) 331 | if not data[tostring(msg.to.id)] then 332 | return 'Group is not added.' 333 | end 334 | -- determine if table is empty 335 | if next(data[tostring(msg.to.id)]['moderators']) == nil then --fix way 336 | return 'No moderator in this group.' 337 | end 338 | local message = 'List of moderators for ' .. string.gsub(msg.to.print_name, '_', ' ') .. ':\n' 339 | for k,v in pairs(data[tostring(msg.to.id)]['moderators']) do 340 | if is_spromoted(msg.to.id, k) then 341 | message = message .. '- '..v..' [' ..k.. '] * \n' 342 | else 343 | message = message .. '- '..v..' [' ..k.. '] \n' 344 | end 345 | end 346 | 347 | return message 348 | end 349 | 350 | local function admin_list(msg) 351 | local data = load_data(_config.moderation.data) 352 | if not data['admins'] then 353 | data['admins'] = {} 354 | save_data(_config.moderation.data, data) 355 | end 356 | if next(data['admins']) == nil then --fix way 357 | return 'No admin available.' 358 | end 359 | local message = 'List for Bot admins:\n' 360 | for k,v in pairs(data['admins']) do 361 | message = message .. '- ' .. v ..' ['..k..'] \n' 362 | end 363 | return message 364 | end 365 | 366 | function run(msg, matches) 367 | if is_chat_msg(msg) then 368 | local get_cmd = matches[1] 369 | local receiver = get_receiver(msg) 370 | if matches[1] == 'modadd' then 371 | return modadd(msg) 372 | end 373 | 374 | if matches[1] == 'modrem' then 375 | return modrem(msg) 376 | end 377 | 378 | if matches[1] == 'promote' then 379 | if not is_momod(msg) then 380 | return 381 | end 382 | if not matches[2] and msg.reply_id then 383 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 384 | return 385 | end 386 | if not matches[2] then 387 | return 388 | end 389 | local member = string.gsub(matches[2], "@", "") 390 | chat_info(receiver, username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 391 | end 392 | 393 | if matches[1] == 'demote' then 394 | if not is_momod(msg) then 395 | return "Only moderator can demote" 396 | end 397 | if not matches[2] and msg.reply_id then 398 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 399 | return 400 | end 401 | if not matches[2] then 402 | return 403 | end 404 | if string.gsub(matches[2], "@", "") == msg.from.username then 405 | return "You can't demote yourself" 406 | end 407 | local member = string.gsub(matches[2], "@", "") 408 | chat_info(receiver, username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 409 | end 410 | 411 | if matches[1] == 'spromote' then 412 | if not is_admin(msg) then 413 | return "Only admin can promote moderator leader" 414 | end 415 | if not matches[2] and msg.reply_id then 416 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 417 | return 418 | end 419 | if not matches[2] then 420 | return 421 | end 422 | local member = string.gsub(matches[2], "@", "") 423 | chat_info(receiver, username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 424 | end 425 | 426 | if matches[1] == 'sdemote' then 427 | if not is_admin(msg) then 428 | return "Only moderator can demote moderator leader" 429 | end 430 | if not matches[2] and msg.reply_id then 431 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 432 | return 433 | end 434 | if not matches[2] then 435 | return 436 | end 437 | if string.match(matches[2], '^%d+$') then 438 | return sdemote(receiver, matches[2], matches[2]) 439 | end 440 | local member = string.gsub(matches[2], "@", "") 441 | chat_info(receiver, username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 442 | end 443 | 444 | if matches[1] == 'modlist' then 445 | return modlist(msg) 446 | end 447 | 448 | if matches[1] == 'adminprom' then 449 | if not is_admin(msg) then 450 | return "Only sudo can promote user as admin" 451 | end 452 | local member = string.gsub(matches[2], "@", "") 453 | chat_info(receiver, username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 454 | end 455 | 456 | if matches[1] == 'admindem' then 457 | if not is_admin(msg) then 458 | return "Only sudo can promote user as admin" 459 | end 460 | if string.match(matches[2], '^%d+$') then 461 | admin_demote(receiver, matches[2], matches[2]) 462 | else 463 | local member = string.gsub(matches[2], "@", "") 464 | chat_info(receiver, username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 465 | end 466 | end 467 | 468 | if matches[1] == 'adminlist' then 469 | if not is_admin(msg) then 470 | return 'Admin only!' 471 | end 472 | return admin_list(msg) 473 | end 474 | 475 | if matches[1] == 'chat_add_user' and msg.action.user.id == our_id then 476 | chat_del_user(receiver, 'user#id'..our_id, ok_cb, true) 477 | end 478 | 479 | if matches[1] == 'chat_created' and msg.from.id == 0 then 480 | --return automodadd(msg) 481 | end 482 | end 483 | if is_channel_msg(msg) then -- supergrouuuuppppppppp 484 | local get_cmd = matches[1] 485 | local receiver = get_receiver(msg) 486 | if matches[1] == 'modadd' then 487 | return modadd(msg) 488 | end 489 | 490 | if matches[1] == 'modrem' then 491 | return modrem(msg) 492 | end 493 | 494 | if matches[1] == 'promote' then 495 | if not is_momod(msg) then 496 | return 497 | end 498 | if not matches[2] and msg.reply_id then 499 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 500 | return 501 | end 502 | if not matches[2] then 503 | return 504 | end 505 | local member = string.gsub(matches[2], "@", "") 506 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 507 | end 508 | 509 | if matches[1] == 'demote' then 510 | if not is_momod(msg) then 511 | return "Only moderator can demote" 512 | end 513 | if not matches[2] and msg.reply_id then 514 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 515 | return 516 | end 517 | if not matches[2] then 518 | return 519 | end 520 | if string.gsub(matches[2], "@", "") == msg.from.username then 521 | return "You can't demote yourself" 522 | end 523 | local member = string.gsub(matches[2], "@", "") 524 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 525 | end 526 | 527 | if matches[1] == 'spromote' then 528 | if not is_admin(msg) then 529 | return "Only admin can promote moderator leader" 530 | end 531 | if not matches[2] and msg.reply_id then 532 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 533 | return 534 | end 535 | if not matches[2] then 536 | return 537 | end 538 | local member = string.gsub(matches[2], "@", "") 539 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 540 | end 541 | 542 | if matches[1] == 'sdemote' then 543 | if not is_admin(msg) then 544 | return "Only moderator can demote moderator leader" 545 | end 546 | if not matches[2] and msg.reply_id then 547 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 548 | return 549 | end 550 | if not matches[2] then 551 | return 552 | end 553 | if string.match(matches[2], '^%d+$') then 554 | return sdemote(receiver, matches[2], matches[2]) 555 | end 556 | local member = string.gsub(matches[2], "@", "") 557 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 558 | end 559 | 560 | if matches[1] == 'modlist' then 561 | return modlist(msg) 562 | end 563 | 564 | if matches[1] == 'upmanager' then 565 | if not is_admin(msg) then 566 | if not is_spromoted(msg.to.id, msg.from.id) then 567 | return "You're not a leader" 568 | end 569 | end 570 | if not matches[2] and msg.reply_id then 571 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 572 | return 573 | end 574 | if not matches[2] then 575 | return 576 | end 577 | if string.match(matches[2], '^%d+$') then 578 | return upmanager(receiver, matches[2], matches[2]) 579 | end 580 | local member = string.gsub(matches[2], "@", "") 581 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 582 | end 583 | 584 | if matches[1] == 'inmanager' then 585 | if not is_admin(msg) then 586 | if not is_spromoted(msg.to.id, msg.from.id) then 587 | return "You're not a leader" 588 | end 589 | end 590 | if not matches[2] and msg.reply_id then 591 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 592 | return 593 | end 594 | if not matches[2] then 595 | return 596 | end 597 | if string.match(matches[2], '^%d+$') then 598 | return sdemote(receiver, matches[2], matches[2]) 599 | end 600 | local member = string.gsub(matches[2], "@", "") 601 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 602 | end 603 | 604 | if matches[1] == 'adminprom' then 605 | if not is_admin(msg) then 606 | return "Only sudo can promote user as admin" 607 | end 608 | local member = string.gsub(matches[2], "@", "") 609 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 610 | end 611 | 612 | if matches[1] == 'admindem' then 613 | if not is_admin(msg) then 614 | return "Only sudo can promote user as admin" 615 | end 616 | if string.match(matches[2], '^%d+$') then 617 | admin_demote(receiver, matches[2], matches[2]) 618 | else 619 | local member = string.gsub(matches[2], "@", "") 620 | channel_get_users(receiver, channel_username_id, {get_cmd= get_cmd, receiver=receiver, member=member}) 621 | end 622 | end 623 | 624 | if matches[1] == 'adminlist' then 625 | if not is_admin(msg) then 626 | return 'Admin only!' 627 | end 628 | return admin_list(msg) 629 | end 630 | 631 | if matches[1] == 'chat_add_user' and msg.action.user.id == our_id then 632 | channel_kick_user(receiver, 'user#id'..our_id, ok_cb, true) 633 | end 634 | else 635 | return 636 | end 637 | end 638 | 639 | return { 640 | description = "Moderation plugin", 641 | usage = { 642 | user = { 643 | "/modlist : List of moderators", 644 | }, 645 | moderator = { 646 | "/promote : Promote user as moderator by username", 647 | "/promote (on reply) : Promote user as moderator by reply", 648 | "/demote : Demote user from moderator", 649 | "/demote (on reply) : demote user from moderator by reply", 650 | }, 651 | admin = { 652 | "/modadd : Add group to moderation list", 653 | "/modrem : Remove group from moderation list", 654 | "/spromote : Promote user as moderator leader by username", 655 | "/spromote (on reply) : Promote user as moderator leader by reply", 656 | "/sdemote : Demote user from being moderator leader by username", 657 | "/sdemote (on reply) : Demote user from being moderator leader by reply", 658 | }, 659 | sudo = { 660 | "/adminprom : Promote user as admin (must be done from a group)", 661 | "/admindem : Demote user from admin (must be done from a group)", 662 | "/admindem : Demote user from admin (must be done from a group)", 663 | }, 664 | }, 665 | patterns = { 666 | "^/(modadd)$", 667 | "^/(modrem)$", 668 | "^/(spromote) (.*)$", 669 | "^/(spromote)$", 670 | "^/(sdemote) (.*)$", 671 | "^/(sdemote)$", 672 | "^/(promote) (.*)$", 673 | "^/(promote)$", 674 | "^/(demote) (.*)$", 675 | "^/(demote)$", 676 | "^/(upmanager) (.*)$", 677 | "^/(upmanager)", 678 | "^/(inmanager) (.*)$", 679 | "^/(inmanager)", 680 | "^/(modlist)$", 681 | "^/(adminprom) (.*)$", -- sudoers only 682 | "^/(admindem) (.*)$", -- sudoers only 683 | "^/(adminlist)$", 684 | "^!!tgservice (chat_add_user)$", 685 | "^!!tgservice (chat_created)$", 686 | }, 687 | run = run, 688 | } 689 | 690 | end -------------------------------------------------------------------------------- /plugins/membercontrol.lua: -------------------------------------------------------------------------------- 1 | local function is_spromoted(chat_id, user_id) 2 | local hash = 'sprom:'..chat_id..':'..user_id 3 | local spromoted = redis:get(hash) 4 | return spromoted or false 5 | end 6 | 7 | local function kick_user(user_id, chat_id) 8 | local chat = 'chat#id'..chat_id 9 | local user = 'user#id'..user_id 10 | chat_del_user(chat, user, ok_cb, true) 11 | end 12 | 13 | local function kick_user_chan(user_id, chat_id) 14 | local channel = 'channel#id'..chat_id 15 | local user = 'user#id'..user_id 16 | channel_kick_user(channel, user, ok_cb, true) 17 | end 18 | 19 | local function ban_user(user_id, chat_id) 20 | local hash = 'banned:'..chat_id..':'..user_id 21 | redis:set(hash, true) 22 | kick_user(user_id, chat_id) 23 | end 24 | 25 | local function superban_user(user_id, chat_id) 26 | local hash = 'superbanned:'..user_id 27 | redis:set(hash, true) 28 | kick_user(user_id, chat_id) 29 | end 30 | 31 | local function superban_user_chan(user_id, chat_id) 32 | local hash = 'superbanned:'..user_id 33 | redis:set(hash, true) 34 | kick_user_chan(user_id, chat_id) 35 | end 36 | 37 | local function silent_user(user_id, chat_id) 38 | local hash = 'silent:'..chat_id..':'..user_id 39 | redis:set(hash, true) 40 | end 41 | 42 | local function is_banned(user_id, chat_id) 43 | local hash = 'banned:'..chat_id..':'..user_id 44 | local banned = redis:get(hash) 45 | return banned or false 46 | end 47 | 48 | local function is_super_banned(user_id) 49 | local hash = 'superbanned:'..user_id 50 | local superbanned = redis:get(hash) 51 | return superbanned or false 52 | end 53 | 54 | local function is_user_silent(user_id, chat_id) 55 | local hash = 'silent:'..chat_id..':'..user_id 56 | local silent = redis:get(hash) 57 | return silent or false 58 | end 59 | 60 | local function pre_process(msg) 61 | if msg.action and msg.action.type then 62 | local action = msg.action.type 63 | if action == 'chat_add_user' or action == 'chat_add_user_link' then 64 | local user_id 65 | if msg.action.link_issuer then 66 | user_id = msg.from.id 67 | else 68 | user_id = msg.action.user.id 69 | end 70 | print('Checking invited user '..user_id) 71 | local superbanned = is_super_banned(user_id) 72 | local banned = is_banned(user_id, msg.to.id) 73 | if superbanned or banned then 74 | print('User is banned!') 75 | if msg.to.type == 'chat' then 76 | kick_user(user_id, msg.to.id) 77 | end 78 | if msg.to.type == 'channel' then 79 | kick_user_chan(user_id, msg.to.id) 80 | end 81 | end 82 | end 83 | return msg 84 | end 85 | 86 | -- BANNED USER TALKING 87 | if msg.to.type == 'chat' then -- For chat 88 | local user_id = msg.from.id 89 | local chat_id = msg.to.id 90 | local superbanned = is_super_banned(user_id) 91 | local banned = is_banned(user_id, chat_id) 92 | if superbanned then 93 | print('SuperBanned user talking!') 94 | superban_user(user_id, chat_id) 95 | msg.text = '' 96 | end 97 | if banned then 98 | print('Banned user talking!') 99 | ban_user(user_id, chat_id) 100 | msg.text = '' 101 | end 102 | end 103 | if msg.to.type == 'channel' then -- For supergroup 104 | local user_id = msg.from.id 105 | local chat_id = msg.to.id 106 | if is_user_silent(user_id, chat_id) then -- is user allowed to talk? 107 | delete_msg(msg.id, ok_cb, false) 108 | return nil 109 | end 110 | local superbanned = is_super_banned(user_id) 111 | local banned = is_banned(user_id, chat_id) 112 | if superbanned then 113 | print('SuperBanned user talking!') 114 | superban_user_chan(user_id, chat_id) 115 | msg.text = '' 116 | end 117 | if banned then 118 | print('Banned user talking!') 119 | ban_user(user_id, chat_id) 120 | msg.text = '' 121 | end 122 | end 123 | return msg 124 | end 125 | 126 | local function username_id(cb_extra, success, result) 127 | local get_cmd = cb_extra.get_cmd 128 | local receiver = cb_extra.receiver 129 | local member = cb_extra.member 130 | local chat_id = string.gsub(receiver,'.+#id', '') 131 | local text = 'No user @'..member..' in this group.' 132 | for k,v in pairs(result.members) do 133 | vusername = v.username 134 | if vusername == member then 135 | member_username = member 136 | member_id = v.peer_id 137 | if get_cmd == 'kick' then 138 | if member_id == our_id then 139 | send_large_msg(receiver, 'Are you kidding?') 140 | return nil 141 | end 142 | local data = load_data(_config.moderation.data) 143 | if data[tostring('admins')] then 144 | if data[tostring('admins')][tostring(member_id)] then 145 | send_large_msg(receiver, 'You can\'t kick admin!') 146 | return nil 147 | end 148 | end 149 | if is_spromoted(chat_id, member_id) then 150 | return send_large_msg(receiver,'You can\'t kick leader') 151 | end 152 | return kick_user(member_id, chat_id) 153 | end 154 | if get_cmd == 'ban' then 155 | if member_id == our_id then 156 | send_large_msg(receiver, 'Are you kidding?') 157 | return nil 158 | end 159 | local data = load_data(_config.moderation.data) -- FLUX MOD 160 | if data[tostring('admins')] then 161 | if data[tostring('admins')][tostring(member_id)] then 162 | send_large_msg(receiver, 'You can\'t ban admin!') 163 | return nil 164 | end 165 | end 166 | if is_spromoted(chat_id, member_id) then 167 | return send_large_msg(receiver, 'You can\'t ban leader') 168 | end 169 | send_large_msg(receiver, 'User @'..member..' ['..member_id..'] banned') 170 | return ban_user(member_id, chat_id) 171 | end 172 | if get_cmd == 'sban' then 173 | if member_id == our_id then 174 | return nil 175 | end 176 | send_large_msg(receiver, 'User @'..member..' ['..member_id..'] globally banned!') 177 | return superban_user(member_id, chat_id) 178 | end 179 | end 180 | end 181 | return send_large_msg(receiver, text) 182 | end 183 | 184 | local function channel_username_id(cb_extra, success, result) 185 | local get_cmd = cb_extra.get_cmd 186 | local receiver = cb_extra.receiver 187 | local chat_id = string.gsub(receiver,'.+#id', '') 188 | local member = cb_extra.member 189 | local text = 'No user @'..member..' in this group.' 190 | for k,v in pairs(result) do 191 | vusername = v.username 192 | if vusername == member then 193 | member_username = member 194 | member_id = v.peer_id 195 | if get_cmd == 'kick' then 196 | if member_id == our_id then 197 | send_large_msg(receiver, 'Are you kidding?') 198 | return nil 199 | end 200 | local data = load_data(_config.moderation.data) -- FLUX MOD 201 | if data[tostring('admins')] then 202 | if data[tostring('admins')][tostring(member_id)] then 203 | send_large_msg(receiver, 'You can\'t kick admin!') 204 | return nil 205 | end 206 | end 207 | if is_spromoted(chat_id, member_id) then 208 | return send_large_msg(receiver,'You can\'t kick leader') 209 | end 210 | return kick_user_chan(member_id, chat_id) 211 | end 212 | if get_cmd == 'ban' then 213 | if member_id == our_id then 214 | send_large_msg(receiver, 'Are you kidding?') 215 | return nil 216 | end 217 | local data = load_data(_config.moderation.data) 218 | if data[tostring('admins')] then 219 | if data[tostring('admins')][tostring(member_id)] then 220 | send_large_msg(receiver, 'You can\'t ban admin!') 221 | return nil 222 | end 223 | end 224 | if is_spromoted(chat_id, member_id) then 225 | return send_large_msg(receiver, 'You can\'t ban leader') 226 | end 227 | send_large_msg(receiver, 'User @'..member..' ['..member_id..'] banned') 228 | return ban_user(member_id, chat_id) 229 | end 230 | if get_cmd == 'sban' then 231 | if member_id == our_id then 232 | return nil 233 | end 234 | send_large_msg(receiver, 'User @'..member..' ['..member_id..'] globally banned!') 235 | return superban_user_chan(member_id, chat_id) 236 | end 237 | if get_cmd == 'silent' then 238 | if member_id == our_id then 239 | return nil 240 | end 241 | local data = load_data(_config.moderation.data) 242 | if data[tostring('admins')] then 243 | if data[tostring('admins')][tostring(member_id)] then 244 | send_large_msg(receiver, 'You can\'t do this to admin!') 245 | return nil 246 | end 247 | end 248 | if is_spromoted(chat_id, member_id) then 249 | return send_large_msg(receiver, 'You can\'t do this to leader') 250 | end 251 | send_large_msg(receiver, 'User '..member_id..' not allowed to talk!') 252 | return silent_user(member_id, chat_id) 253 | end 254 | if get_cmd == 'unsilent' then 255 | if member_id == our_id then 256 | return nil 257 | end 258 | local data = load_data(_config.moderation.data) 259 | if data[tostring('admins')] then 260 | if data[tostring('admins')][tostring(member_id)] then 261 | send_large_msg(receiver, 'Admin always allowed to talk!') 262 | return nil 263 | end 264 | end 265 | local hash = 'silent:'..chat_id..':'..member_id 266 | redis:del(hash) 267 | return send_large_msg(receiver, 'User '..member_id..' allowed to talk') 268 | end 269 | end 270 | end 271 | return send_large_msg(receiver, text) 272 | end 273 | 274 | local function get_msg_callback(extra, success, result) 275 | if success ~= 1 then return end 276 | local get_cmd = extra.get_cmd 277 | local receiver = extra.receiver 278 | local user_id = result.from.peer_id 279 | local chat_id = string.gsub(receiver,'.+#id', '') 280 | if result.from.username then 281 | username = '@'..result.from.username 282 | else 283 | username = string.gsub(result.from.print_name, '_', ' ') 284 | end 285 | if string.find(receiver,'chat#id.+') then 286 | group_type = 'chat' 287 | else 288 | group_type = 'channel' 289 | end 290 | if get_cmd == 'kick' then 291 | if user_id == our_id then 292 | return nil 293 | end 294 | local data = load_data(_config.moderation.data) 295 | if data[tostring('admins')] then 296 | if data[tostring('admins')][tostring(user_id)] then 297 | return send_large_msg(receiver, 'You can\'t kick admin!') 298 | end 299 | end 300 | if is_spromoted(chat_id, user_id) then 301 | print('kick leader') 302 | return send_large_msg(receiver,'You can\'t kick leader') 303 | end 304 | if group_type == 'chat' then 305 | return kick_user(user_id, chat_id) 306 | else 307 | return kick_user_chan(user_id, chat_id) 308 | end 309 | end 310 | if get_cmd == 'ban' then 311 | if user_id == our_id then 312 | return nil 313 | end 314 | local data = load_data(_config.moderation.data) -- FLUX MOD 315 | if data[tostring('admins')] then 316 | if data[tostring('admins')][tostring(user_id)] then 317 | return send_large_msg(receiver, 'You can\'t ban admin!') 318 | end 319 | end 320 | if is_spromoted(chat_id, user_id) then 321 | return send_large_msg(receiver,'You can\'t ban leader') 322 | end 323 | send_large_msg(receiver, 'User '..username..' ['..user_id..'] banned') 324 | if group_type == 'chat' then 325 | return ban_user(user_id, chat_id) 326 | else 327 | return kick_user_chan(user_id, chat_id) 328 | end 329 | end 330 | if get_cmd == 'unban' then 331 | if user_id == our_id then 332 | return nil 333 | end 334 | local hash = 'banned:'..chat_id..':'..user_id 335 | redis:del(hash) 336 | return send_large_msg(receiver, 'User '..user_id..' unbanned') 337 | end 338 | if get_cmd == 'sban' then 339 | if user_id == our_id then 340 | return nil 341 | end 342 | send_large_msg(receiver, 'User '..username..' ['..user_id..'] globally banned!') 343 | return superban_user(member_id, chat_id) 344 | end 345 | if get_cmd == 'silent' then 346 | if user_id == our_id then 347 | return nil 348 | end 349 | local data = load_data(_config.moderation.data) 350 | if data[tostring('admins')] then 351 | if data[tostring('admins')][tostring(user_id)] then 352 | send_large_msg(receiver, 'You can\'t do this to admin!') 353 | return nil 354 | end 355 | end 356 | if is_spromoted(chat_id, user_id) then 357 | return send_large_msg(receiver, 'You can\'t do this to leader') 358 | end 359 | send_large_msg(receiver, 'User '..user_id..' not allowed to talk!') 360 | return silent_user(user_id, chat_id) 361 | end 362 | if get_cmd == 'unsilent' then 363 | if member_id == our_id then 364 | return nil 365 | end 366 | local data = load_data(_config.moderation.data) 367 | if data[tostring('admins')] then 368 | if data[tostring('admins')][tostring(user_id)] then 369 | send_large_msg(receiver, 'Admin always allowed to talk!') 370 | return nil 371 | end 372 | end 373 | local hash = 'silent:'..chat_id..':'..user_id 374 | redis:del(hash) 375 | return send_large_msg(receiver, 'User '..user_id..' allowed to talk') 376 | end 377 | end 378 | 379 | local function run(msg, matches) 380 | if matches[1] == 'kickme' then 381 | if is_chat_msg(msg) then 382 | kick_user(msg.from.id, msg.to.id) 383 | elseif is_channel_msg(msg) then 384 | kick_user_chan(msg.from.id, msg.to.id) 385 | else 386 | return 387 | end 388 | end 389 | if not is_momod(msg) then 390 | return nil 391 | end 392 | local receiver = get_receiver(msg) 393 | local get_cmd = matches[1] 394 | 395 | if is_channel_msg(msg) then -- SUPERGROUUUPPPPPPPPPPPP 396 | if matches[1] == 'ban' then 397 | if not matches[2] and msg.reply_id then 398 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 399 | end 400 | if not matches[2] then 401 | return 402 | end 403 | local user_id = matches[2] 404 | local chat_id = msg.to.id 405 | if string.match(matches[2], '^%d+$') then 406 | if matches[2] == our_id then 407 | return 408 | end 409 | local data = load_data(_config.moderation.data) -- FLUX MOD 410 | if data[tostring('admins')] then 411 | if data[tostring('admins')][tostring(user_id)] then 412 | return 'You can\'t ban admin!' 413 | end 414 | end 415 | if is_spromoted(msg.to.id, matches[2]) and not is_admin(msg) then 416 | return 'You can\'t ban leader' 417 | end 418 | ban_user(user_id, chat_id) 419 | send_large_msg(receiver, 'User '..user_id..' banned!') 420 | else 421 | local member = string.gsub(matches[2], '@', '') 422 | channel_get_users(receiver, chanel_username_id, {get_cmd=get_cmd, receiver=receiver, member=member}) 423 | end 424 | end 425 | if matches[1] == 'unban' then 426 | if not matches[2] and msg.reply_id then 427 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 428 | end 429 | if not matches[2] then 430 | return 431 | end 432 | local user_id = matches[2] 433 | local chat_id = msg.to.id 434 | if string.match(matches[2], '^%d+$') then 435 | local hash = 'banned:'..chat_id..':'..user_id 436 | redis:del(hash) 437 | return 'User '..user_id..' unbanned' 438 | else 439 | return 'Use user id only' 440 | end 441 | end 442 | if matches[1] == 'sban' and is_admin(msg) then 443 | if not matches[2] and msg.reply_id then 444 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 445 | end 446 | if not matches[2] then 447 | return 448 | end 449 | local user_id = matches[2] 450 | local chat_id = msg.to.id 451 | if string.match(matches[2], '^%d+$') then 452 | if matches[2] == our_id then 453 | return 454 | end 455 | local data = load_data(_config.moderation.data) -- FLUX MOD 456 | if data[tostring('admins')] then 457 | if data[tostring('admins')][tostring(user_id)] then 458 | return 'You can\'t ban admin!' 459 | end 460 | end 461 | if is_spromoted(msg.to.id, matches[2]) and not is_admin(msg) then 462 | return 'You can\'t ban leader' 463 | end 464 | ban_user(user_id, chat_id) 465 | send_large_msg(receiver, 'User '..user_id..' globally banned!') 466 | else 467 | local member = string.gsub(matches[2], '@', '') 468 | channel_get_users(receiver, chanel_username_id, {get_cmd=get_cmd, receiver=receiver, chat_id=chat_id, member=member}) 469 | end 470 | end 471 | if matches[1] == 'unsban' then 472 | local user_id = matches[2] 473 | local chat_id = msg.to.id 474 | local hash = 'superbanned:'..user_id 475 | redis:del(hash) 476 | return 'User '..user_id..' unbanned' 477 | end 478 | if matches[1] == 'kick' then 479 | if not matches[2] and msg.reply_id then 480 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 481 | return 482 | end 483 | if not matches[2] then 484 | return 485 | end 486 | if string.match(matches[2], '^%d+$') then 487 | if matches[2] == our_id and not is_admin(msg) then 488 | return 489 | end 490 | local data = load_data(_config.moderation.data) 491 | if data[tostring('admins')] then 492 | if data[tostring('admins')][tostring(matches[2])] then 493 | return 'You can\'t kick admin!' 494 | end 495 | end 496 | if is_spromoted(msg.to.id, matches[2]) and not is_admin(msg) then 497 | return 'You can\'t kick leader' 498 | end 499 | kick_user_chan(matches[2], msg.to.id) 500 | else 501 | local member = string.gsub(matches[2], '@', '') 502 | channel_get_users(receiver, channel_username_id, {get_cmd=get_cmd, receiver=receiver, chat_id=msg.to.id, member=member}) 503 | end 504 | end 505 | if matches[1] == 'silent' then 506 | if not matches[2] and msg.reply_id then 507 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 508 | return 509 | end 510 | if not matches[2] then 511 | return 512 | end 513 | if string.match(matches[2], '^%d+$') then 514 | if matches[2] == our_id then 515 | return 516 | end 517 | local data = load_data(_config.moderation.data) 518 | if data[tostring('admins')] then 519 | if data[tostring('admins')][tostring(matches[2])] then 520 | return 'You can\'t do this to admin!' 521 | end 522 | end 523 | if is_spromoted(msg.to.id, matches[2]) and not is_admin(msg) then 524 | return 'You can\'t do this to leader' 525 | end 526 | silent_user(matches[2], msg.to.id) 527 | send_large_msg(receiver, 'User '..matches[2]..' not allowed to talk!') 528 | else 529 | local member = string.gsub(matches[2], '@', '') 530 | channel_get_users(receiver, channel_username_id, {get_cmd=get_cmd, receiver=receiver, chat_id=msg.to.id, member=member}) 531 | end 532 | end 533 | if matches[1] == 'unsilent' then 534 | if not matches[2] and msg.reply_id then 535 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 536 | return 537 | end 538 | if not matches[2] then 539 | return 540 | end 541 | if string.match(matches[2], '^%d+$') then 542 | if matches[2] == our_id then 543 | return 544 | end 545 | local data = load_data(_config.moderation.data) 546 | if data[tostring('admins')] then 547 | if data[tostring('admins')][tostring(matches[2])] then 548 | return 'Admin always allowed to talk!' 549 | end 550 | end 551 | local hash = 'silent:'..msg.to.id..':'..matches[2] 552 | redis:del(hash) 553 | return 'User '..user_id..' allowed to talk' 554 | else 555 | local member = string.gsub(matches[2], '@', '') 556 | channel_get_users(receiver, channel_username_id, {get_cmd=get_cmd, receiver=receiver, chat_id=msg.to.id, member=member}) 557 | end 558 | end 559 | end 560 | 561 | if is_chat_msg(msg) then -- CHAAAAAAAAATTTTTTTTTTTTTTTTTTTTTT 562 | if matches[1] == 'ban' then 563 | if not matches[2] and msg.reply_id then 564 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 565 | end 566 | if not matches[2] then 567 | return 568 | end 569 | local user_id = matches[2] 570 | local chat_id = msg.to.id 571 | if string.match(matches[2], '^%d+$') then 572 | if matches[2] == our_id then 573 | return 574 | end 575 | local data = load_data(_config.moderation.data) -- FLUX MOD 576 | if data[tostring('admins')] then 577 | if data[tostring('admins')][tostring(user_id)] then 578 | return 'You can\'t ban admin!' 579 | end 580 | end 581 | if is_spromoted(msg.to.id, matches[2]) and not is_admin(msg) then 582 | return 'You can\'t ban leader' 583 | end 584 | ban_user(user_id, chat_id) 585 | send_large_msg(receiver, 'User '..user_id..' banned!') 586 | else 587 | local member = string.gsub(matches[2], '@', '') 588 | chat_info(receiver, username_id, {get_cmd=get_cmd, receiver=receiver, member=member}) 589 | end 590 | end 591 | if matches[1] == 'unban' then 592 | local user_id = matches[2] 593 | local chat_id = msg.to.id 594 | if string.match(matches[2], '^%d+$') then 595 | local hash = 'banned:'..chat_id..':'..user_id 596 | redis:del(hash) 597 | return 'User '..user_id..' unbanned' 598 | else 599 | return 'Use user id only' 600 | end 601 | end 602 | if matches[1] == 'sban' and is_admin(msg) then 603 | if not matches[2] and msg.reply_id then 604 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 605 | end 606 | if not matches[2] then 607 | return 608 | end 609 | local user_id = matches[2] 610 | local chat_id = msg.to.id 611 | if string.match(matches[2], '^%d+$') then 612 | if matches[2] == our_id then 613 | return 614 | end 615 | local data = load_data(_config.moderation.data) -- FLUX MOD 616 | if data[tostring('admins')] then 617 | if data[tostring('admins')][tostring(user_id)] then 618 | return 'You can\'t ban admin!' 619 | end 620 | end 621 | if is_spromoted(msg.to.id, matches[2]) and not is_admin(msg) then 622 | return 'You can\'t ban leader' 623 | end 624 | ban_user(user_id, chat_id) 625 | send_large_msg(receiver, 'User '..user_id..' globally banned!') 626 | else 627 | local member = string.gsub(matches[2], '@', '') 628 | chat_info(receiver, username_id, {get_cmd=get_cmd, receiver=receiver, member=member}) 629 | end 630 | end 631 | if matches[1] == 'unsban' then 632 | local hash = 'superbanned:'..user_id 633 | redis:del(hash) 634 | return 'User '..user_id..' unbanned' 635 | end 636 | if matches[1] == 'kick' then 637 | if not matches[2] and msg.reply_id then 638 | get_message(msg.reply_id, get_msg_callback, {get_cmd=get_cmd, receiver=receiver}) 639 | return 640 | end 641 | if not matches[2] then 642 | return 643 | end 644 | if string.match(matches[2], '^%d+$') then 645 | if matches[2] == our_id and not is_admin(msg) then 646 | return 647 | end 648 | local data = load_data(_config.moderation.data) 649 | if data[tostring('admins')] then 650 | if data[tostring('admins')][tostring(matches[2])] then 651 | return 'You can\'t kick admin!' 652 | end 653 | end 654 | if is_spromoted(msg.to.id, matches[2]) then 655 | if not is_admin(msg) then 656 | return 'You can\'t kick leader' 657 | end 658 | end 659 | kick_user(matches[2], msg.to.id) 660 | else 661 | local member = string.gsub(matches[2], '@', '') 662 | chat_info(receiver, username_id, {get_cmd=get_cmd, receiver=receiver, chat_id=chat_id, member=member}) 663 | end 664 | end 665 | end 666 | end 667 | 668 | return { 669 | description = "Plugin to manage bans, kicks and white/black lists.", 670 | usage = { 671 | user = "!kickme : Exit from group", 672 | moderator = { 673 | "!ban user : Kick user from chat and kicks it if joins chat again", 674 | "!ban user : Kick user from chat and kicks it if joins chat again", 675 | "!unban (on reply)", 676 | "!kick (on reply) : Kick user from chat group by reply", 677 | "!ban delete : Unban user", 678 | "!kick : Kick user from chat group by id", 679 | "!kick : Kick user from chat group by username", 680 | "!kick (on reply) : Kick user from chat group by reply", 681 | "!silent (username|id|reply) : make a user silent", 682 | }, 683 | admin = { 684 | "!banallgp user : Ban user from all chat by id", 685 | "!banallgp user : Ban user from all chat by username", 686 | "!banallgp (on reply) : Ban user from all chat by reply", 687 | "!banallgp delete : Unban user", 688 | }, 689 | }, 690 | patterns = { 691 | "^/(ban) (.*)$", 692 | "^/(unban) (.*)$", 693 | "^/(unban)$", 694 | "^/(ban)$", 695 | "^/(sban) (.*)$", 696 | "^/(unsban) (.*)$", 697 | "^/(sban)$", 698 | "^/(kick) (.*)$", 699 | "^/(kick)$", 700 | "^/(kickme)$", 701 | "^/(silent) (.*)$", --only for supergroup 702 | "^/(silent)$", 703 | "^/(unsilent) (.*)$", 704 | "^/(unsilent)$", --till here 705 | "^!!tgservice (.+)$", 706 | }, 707 | run = run, 708 | pre_process = pre_process 709 | } -------------------------------------------------------------------------------- /libs/dkjson.lua: -------------------------------------------------------------------------------- 1 | -- Module options: 2 | local always_try_using_lpeg = true 3 | local register_global_module_table = false 4 | local global_module_name = 'json' 5 | 6 | --[==[ 7 | 8 | David Kolf's JSON module for Lua 5.1/5.2 9 | ======================================== 10 | *Version 2.4* 11 | In the default configuration this module writes no global values, not even 12 | the module table. Import it using 13 | json = require ("dkjson") 14 | In environments where `require` or a similiar function are not available 15 | and you cannot receive the return value of the module, you can set the 16 | option `register_global_module_table` to `true`. The module table will 17 | then be saved in the global variable with the name given by the option 18 | `global_module_name`. 19 | Exported functions and values: 20 | `json.encode (object [, state])` 21 | -------------------------------- 22 | Create a string representing the object. `Object` can be a table, 23 | a string, a number, a boolean, `nil`, `json.null` or any object with 24 | a function `__tojson` in its metatable. A table can only use strings 25 | and numbers as keys and its values have to be valid objects as 26 | well. It raises an error for any invalid data types or reference 27 | cycles. 28 | `state` is an optional table with the following fields: 29 | - `indent` 30 | When `indent` (a boolean) is set, the created string will contain 31 | newlines and indentations. Otherwise it will be one long line. 32 | - `keyorder` 33 | `keyorder` is an array to specify the ordering of keys in the 34 | encoded output. If an object has keys which are not in this array 35 | they are written after the sorted keys. 36 | - `level` 37 | This is the initial level of indentation used when `indent` is 38 | set. For each level two spaces are added. When absent it is set 39 | to 0. 40 | - `buffer` 41 | `buffer` is an array to store the strings for the result so they 42 | can be concatenated at once. When it isn't given, the encode 43 | function will create it temporary and will return the 44 | concatenated result. 45 | - `bufferlen` 46 | When `bufferlen` is set, it has to be the index of the last 47 | element of `buffer`. 48 | - `tables` 49 | `tables` is a set to detect reference cycles. It is created 50 | temporary when absent. Every table that is currently processed 51 | is used as key, the value is `true`. 52 | 53 | When `state.buffer` was set, the return value will be `true` on 54 | success. Without `state.buffer` the return value will be a string. 55 | 56 | `json.decode (string [, position [, null]])` 57 | -------------------------------------------- 58 | 59 | Decode `string` starting at `position` or at 1 if `position` was 60 | omitted. 61 | 62 | `null` is an optional value to be returned for null values. The 63 | default is `nil`, but you could set it to `json.null` or any other 64 | value. 65 | 66 | The return values are the object or `nil`, the position of the next 67 | character that doesn't belong to the object, and in case of errors 68 | an error message. 69 | Two metatables are created. Every array or object that is decoded gets 70 | a metatable with the `__jsontype` field set to either `array` or 71 | `object`. If you want to provide your own metatables use the syntax 72 | json.decode (string, position, null, objectmeta, arraymeta) 73 | To prevent the assigning of metatables pass `nil`: 74 | json.decode (string, position, null, nil) 75 | `.__jsonorder` 76 | ------------------------- 77 | `__jsonorder` can overwrite the `keyorder` for a specific table. 78 | `.__jsontype` 79 | ------------------------ 80 | `__jsontype` can be either `"array"` or `"object"`. This value is only 81 | checked for empty tables. (The default for empty tables is `"array"`). 82 | `.__tojson (self, state)` 83 | ------------------------------------ 84 | You can provide your own `__tojson` function in a metatable. In this 85 | function you can either add directly to the buffer and return true, 86 | or you can return a string. On errors nil and a message should be 87 | returned. 88 | `json.null` 89 | ----------- 90 | You can use this value for setting explicit `null` values. 91 | `json.version` 92 | -------------- 93 | Set to `"dkjson 2.4"`. 94 | `json.quotestring (string)` 95 | --------------------------- 96 | Quote a UTF-8 string and escape critical characters using JSON 97 | escape sequences. This function is only necessary when you build 98 | your own `__tojson` functions. 99 | `json.addnewline (state)` 100 | ------------------------- 101 | When `state.indent` is set, add a newline to `state.buffer` and spaces 102 | according to `state.level`. 103 | LPeg support 104 | ------------ 105 | When the local configuration variable `always_try_using_lpeg` is set, 106 | this module tries to load LPeg to replace the `decode` function. The 107 | speed increase is significant. You can get the LPeg module at 108 | . 109 | When LPeg couldn't be loaded, the pure Lua functions stay active. 110 | 111 | In case you don't want this module to require LPeg on its own, 112 | disable the option `always_try_using_lpeg` in the options section at 113 | the top of the module. 114 | In this case you can later load LPeg support using 115 | ### `json.use_lpeg ()` 116 | Require the LPeg module and replace the functions `quotestring` and 117 | and `decode` with functions that use LPeg patterns. 118 | This function returns the module table, so you can load the module 119 | using: 120 | json = require "dkjson".use_lpeg() 121 | Alternatively you can use `pcall` so the JSON module still works when 122 | LPeg isn't found. 123 | 124 | json = require "dkjson" 125 | pcall (json.use_lpeg) 126 | 127 | ### `json.using_lpeg` 128 | 129 | This variable is set to `true` when LPeg was loaded successfully. 130 | 131 | --------------------------------------------------------------------- 132 | 133 | Contact 134 | ------- 135 | 136 | You can contact the author by sending an e-mail to 'david' at the 137 | domain 'dkolf.de'. 138 | 139 | --------------------------------------------------------------------- 140 | 141 | *Copyright (C) 2010-2013 David Heiko Kolf* 142 | 143 | Permission is hereby granted, free of charge, to any person obtaining 144 | a copy of this software and associated documentation files (the 145 | "Software"), to deal in the Software without restriction, including 146 | without limitation the rights to use, copy, modify, merge, publish, 147 | distribute, sublicense, and/or sell copies of the Software, and to 148 | permit persons to whom the Software is furnished to do so, subject to 149 | the following conditions: 150 | 151 | The above copyright notice and this permission notice shall be 152 | included in all copies or substantial portions of the Software. 153 | 154 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 155 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 156 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 157 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 158 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 159 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 160 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 161 | SOFTWARE. 162 | 163 | 168 | 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /libs/JSON.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- 3 | -- Simple JSON encoding and decoding in pure Lua. 4 | -- 5 | -- Copyright 2010-2014 Jeffrey Friedl 6 | -- http://regex.info/blog/ 7 | -- 8 | -- Latest version: http://regex.info/blog/lua/json 9 | -- 10 | -- This code is released under a Creative Commons CC-BY "Attribution" License: 11 | -- http://creativecommons.org/licenses/by/3.0/deed.en_US 12 | -- 13 | -- It can be used for any purpose so long as the copyright notice above, 14 | -- the web-page links above, and the 'AUTHOR_NOTE' string below are 15 | -- maintained. Enjoy. 16 | -- 17 | local VERSION = 20141223.14 -- version history at end of file 18 | local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20141223.14 ]-" 19 | 20 | -- 21 | -- The 'AUTHOR_NOTE' variable exists so that information about the source 22 | -- of the package is maintained even in compiled versions. It's also 23 | -- included in OBJDEF below mostly to quiet warnings about unused variables. 24 | -- 25 | local OBJDEF = { 26 | VERSION = VERSION, 27 | AUTHOR_NOTE = AUTHOR_NOTE, 28 | } 29 | 30 | 31 | -- 32 | -- Simple JSON encoding and decoding in pure Lua. 33 | -- http://www.json.org/ 34 | -- 35 | -- 36 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 37 | -- 38 | -- local lua_value = JSON:decode(raw_json_text) 39 | -- 40 | -- local raw_json_text = JSON:encode(lua_table_or_value) 41 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 42 | -- 43 | -- 44 | -- 45 | -- DECODING (from a JSON string to a Lua table) 46 | -- 47 | -- 48 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 49 | -- 50 | -- local lua_value = JSON:decode(raw_json_text) 51 | -- 52 | -- If the JSON text is for an object or an array, e.g. 53 | -- { "what": "books", "count": 3 } 54 | -- or 55 | -- [ "Larry", "Curly", "Moe" ] 56 | -- 57 | -- the result is a Lua table, e.g. 58 | -- { what = "books", count = 3 } 59 | -- or 60 | -- { "Larry", "Curly", "Moe" } 61 | -- 62 | -- 63 | -- The encode and decode routines accept an optional second argument, 64 | -- "etc", which is not used during encoding or decoding, but upon error 65 | -- is passed along to error handlers. It can be of any type (including nil). 66 | -- 67 | -- 68 | -- 69 | -- ERROR HANDLING 70 | -- 71 | -- With most errors during decoding, this code calls 72 | -- 73 | -- JSON:onDecodeError(message, text, location, etc) 74 | -- 75 | -- with a message about the error, and if known, the JSON text being 76 | -- parsed and the byte count where the problem was discovered. You can 77 | -- replace the default JSON:onDecodeError() with your own function. 78 | -- 79 | -- The default onDecodeError() merely augments the message with data 80 | -- about the text and the location if known (and if a second 'etc' 81 | -- argument had been provided to decode(), its value is tacked onto the 82 | -- message as well), and then calls JSON.assert(), which itself defaults 83 | -- to Lua's built-in assert(), and can also be overridden. 84 | -- 85 | -- For example, in an Adobe Lightroom plugin, you might use something like 86 | -- 87 | -- function JSON:onDecodeError(message, text, location, etc) 88 | -- LrErrors.throwUserError("Internal Error: invalid JSON data") 89 | -- end 90 | -- 91 | -- or even just 92 | -- 93 | -- function JSON.assert(message) 94 | -- LrErrors.throwUserError("Internal Error: " .. message) 95 | -- end 96 | -- 97 | -- If JSON:decode() is passed a nil, this is called instead: 98 | -- 99 | -- JSON:onDecodeOfNilError(message, nil, nil, etc) 100 | -- 101 | -- and if JSON:decode() is passed HTML instead of JSON, this is called: 102 | -- 103 | -- JSON:onDecodeOfHTMLError(message, text, nil, etc) 104 | -- 105 | -- The use of the fourth 'etc' argument allows stronger coordination 106 | -- between decoding and error reporting, especially when you provide your 107 | -- own error-handling routines. Continuing with the the Adobe Lightroom 108 | -- plugin example: 109 | -- 110 | -- function JSON:onDecodeError(message, text, location, etc) 111 | -- local note = "Internal Error: invalid JSON data" 112 | -- if type(etc) = 'table' and etc.photo then 113 | -- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') 114 | -- end 115 | -- LrErrors.throwUserError(note) 116 | -- end 117 | -- 118 | -- : 119 | -- : 120 | -- 121 | -- for i, photo in ipairs(photosToProcess) do 122 | -- : 123 | -- : 124 | -- local data = JSON:decode(someJsonText, { photo = photo }) 125 | -- : 126 | -- : 127 | -- end 128 | -- 129 | -- 130 | -- 131 | -- 132 | -- 133 | -- DECODING AND STRICT TYPES 134 | -- 135 | -- Because both JSON objects and JSON arrays are converted to Lua tables, 136 | -- it's not normally possible to tell which original JSON type a 137 | -- particular Lua table was derived from, or guarantee decode-encode 138 | -- round-trip equivalency. 139 | -- 140 | -- However, if you enable strictTypes, e.g. 141 | -- 142 | -- JSON = assert(loadfile "JSON.lua")() --load the routines 143 | -- JSON.strictTypes = true 144 | -- 145 | -- then the Lua table resulting from the decoding of a JSON object or 146 | -- JSON array is marked via Lua metatable, so that when re-encoded with 147 | -- JSON:encode() it ends up as the appropriate JSON type. 148 | -- 149 | -- (This is not the default because other routines may not work well with 150 | -- tables that have a metatable set, for example, Lightroom API calls.) 151 | -- 152 | -- 153 | -- ENCODING (from a lua table to a JSON string) 154 | -- 155 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 156 | -- 157 | -- local raw_json_text = JSON:encode(lua_table_or_value) 158 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 159 | -- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false }) 160 | -- 161 | -- On error during encoding, this code calls: 162 | -- 163 | -- JSON:onEncodeError(message, etc) 164 | -- 165 | -- which you can override in your local JSON object. 166 | -- 167 | -- The 'etc' in the error call is the second argument to encode() 168 | -- and encode_pretty(), or nil if it wasn't provided. 169 | -- 170 | -- 171 | -- PRETTY-PRINTING 172 | -- 173 | -- An optional third argument, a table of options, allows a bit of 174 | -- configuration about how the encoding takes place: 175 | -- 176 | -- pretty = JSON:encode(val, etc, { 177 | -- pretty = true, -- if false, no other options matter 178 | -- indent = " ", -- this provides for a three-space indent per nesting level 179 | -- align_keys = false, -- see below 180 | -- }) 181 | -- 182 | -- encode() and encode_pretty() are identical except that encode_pretty() 183 | -- provides a default options table if none given in the call: 184 | -- 185 | -- { pretty = true, align_keys = false, indent = " " } 186 | -- 187 | -- For example, if 188 | -- 189 | -- JSON:encode(data) 190 | -- 191 | -- produces: 192 | -- 193 | -- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} 194 | -- 195 | -- then 196 | -- 197 | -- JSON:encode_pretty(data) 198 | -- 199 | -- produces: 200 | -- 201 | -- { 202 | -- "city": "Kyoto", 203 | -- "climate": { 204 | -- "avg_temp": 16, 205 | -- "humidity": "high", 206 | -- "snowfall": "minimal" 207 | -- }, 208 | -- "country": "Japan", 209 | -- "wards": 11 210 | -- } 211 | -- 212 | -- The following three lines return identical results: 213 | -- JSON:encode_pretty(data) 214 | -- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " }) 215 | -- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " }) 216 | -- 217 | -- An example of setting your own indent string: 218 | -- 219 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " }) 220 | -- 221 | -- produces: 222 | -- 223 | -- { 224 | -- | "city": "Kyoto", 225 | -- | "climate": { 226 | -- | | "avg_temp": 16, 227 | -- | | "humidity": "high", 228 | -- | | "snowfall": "minimal" 229 | -- | }, 230 | -- | "country": "Japan", 231 | -- | "wards": 11 232 | -- } 233 | -- 234 | -- An example of setting align_keys to true: 235 | -- 236 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true }) 237 | -- 238 | -- produces: 239 | -- 240 | -- { 241 | -- "city": "Kyoto", 242 | -- "climate": { 243 | -- "avg_temp": 16, 244 | -- "humidity": "high", 245 | -- "snowfall": "minimal" 246 | -- }, 247 | -- "country": "Japan", 248 | -- "wards": 11 249 | -- } 250 | -- 251 | -- which I must admit is kinda ugly, sorry. This was the default for 252 | -- encode_pretty() prior to version 20141223.14. 253 | -- 254 | -- 255 | -- AMBIGUOUS SITUATIONS DURING THE ENCODING 256 | -- 257 | -- During the encode, if a Lua table being encoded contains both string 258 | -- and numeric keys, it fits neither JSON's idea of an object, nor its 259 | -- idea of an array. To get around this, when any string key exists (or 260 | -- when non-positive numeric keys exist), numeric keys are converted to 261 | -- strings. 262 | -- 263 | -- For example, 264 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 265 | -- produces the JSON object 266 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 267 | -- 268 | -- To prohibit this conversion and instead make it an error condition, set 269 | -- JSON.noKeyConversion = true 270 | -- 271 | 272 | 273 | 274 | 275 | -- 276 | -- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT 277 | -- 278 | -- assert 279 | -- onDecodeError 280 | -- onDecodeOfNilError 281 | -- onDecodeOfHTMLError 282 | -- onEncodeError 283 | -- 284 | -- If you want to create a separate Lua JSON object with its own error handlers, 285 | -- you can reload JSON.lua or use the :new() method. 286 | -- 287 | --------------------------------------------------------------------------- 288 | 289 | local default_pretty_indent = " " 290 | local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } 291 | 292 | local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray 293 | local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject 294 | 295 | 296 | function OBJDEF:newArray(tbl) 297 | return setmetatable(tbl or {}, isArray) 298 | end 299 | 300 | function OBJDEF:newObject(tbl) 301 | return setmetatable(tbl or {}, isObject) 302 | end 303 | 304 | local function unicode_codepoint_as_utf8(codepoint) 305 | -- 306 | -- codepoint is a number 307 | -- 308 | if codepoint <= 127 then 309 | return string.char(codepoint) 310 | 311 | elseif codepoint <= 2047 then 312 | -- 313 | -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 314 | -- 315 | local highpart = math.floor(codepoint / 0x40) 316 | local lowpart = codepoint - (0x40 * highpart) 317 | return string.char(0xC0 + highpart, 318 | 0x80 + lowpart) 319 | 320 | elseif codepoint <= 65535 then 321 | -- 322 | -- 1110yyyy 10yyyyxx 10xxxxxx 323 | -- 324 | local highpart = math.floor(codepoint / 0x1000) 325 | local remainder = codepoint - 0x1000 * highpart 326 | local midpart = math.floor(remainder / 0x40) 327 | local lowpart = remainder - 0x40 * midpart 328 | 329 | highpart = 0xE0 + highpart 330 | midpart = 0x80 + midpart 331 | lowpart = 0x80 + lowpart 332 | 333 | -- 334 | -- Check for an invalid character (thanks Andy R. at Adobe). 335 | -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 336 | -- 337 | if ( highpart == 0xE0 and midpart < 0xA0 ) or 338 | ( highpart == 0xED and midpart > 0x9F ) or 339 | ( highpart == 0xF0 and midpart < 0x90 ) or 340 | ( highpart == 0xF4 and midpart > 0x8F ) 341 | then 342 | return "?" 343 | else 344 | return string.char(highpart, 345 | midpart, 346 | lowpart) 347 | end 348 | 349 | else 350 | -- 351 | -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx 352 | -- 353 | local highpart = math.floor(codepoint / 0x40000) 354 | local remainder = codepoint - 0x40000 * highpart 355 | local midA = math.floor(remainder / 0x1000) 356 | remainder = remainder - 0x1000 * midA 357 | local midB = math.floor(remainder / 0x40) 358 | local lowpart = remainder - 0x40 * midB 359 | 360 | return string.char(0xF0 + highpart, 361 | 0x80 + midA, 362 | 0x80 + midB, 363 | 0x80 + lowpart) 364 | end 365 | end 366 | 367 | function OBJDEF:onDecodeError(message, text, location, etc) 368 | if text then 369 | if location then 370 | message = string.format("%s at char %d of: %s", message, location, text) 371 | else 372 | message = string.format("%s: %s", message, text) 373 | end 374 | end 375 | 376 | if etc ~= nil then 377 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 378 | end 379 | 380 | if self.assert then 381 | self.assert(false, message) 382 | else 383 | assert(false, message) 384 | end 385 | end 386 | 387 | OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError 388 | OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError 389 | 390 | function OBJDEF:onEncodeError(message, etc) 391 | if etc ~= nil then 392 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 393 | end 394 | 395 | if self.assert then 396 | self.assert(false, message) 397 | else 398 | assert(false, message) 399 | end 400 | end 401 | 402 | local function grok_number(self, text, start, etc) 403 | -- 404 | -- Grab the integer part 405 | -- 406 | local integer_part = text:match('^-?[1-9]%d*', start) 407 | or text:match("^-?0", start) 408 | 409 | if not integer_part then 410 | self:onDecodeError("expected number", text, start, etc) 411 | end 412 | 413 | local i = start + integer_part:len() 414 | 415 | -- 416 | -- Grab an optional decimal part 417 | -- 418 | local decimal_part = text:match('^%.%d+', i) or "" 419 | 420 | i = i + decimal_part:len() 421 | 422 | -- 423 | -- Grab an optional exponential part 424 | -- 425 | local exponent_part = text:match('^[eE][-+]?%d+', i) or "" 426 | 427 | i = i + exponent_part:len() 428 | 429 | local full_number_text = integer_part .. decimal_part .. exponent_part 430 | local as_number = tonumber(full_number_text) 431 | 432 | if not as_number then 433 | self:onDecodeError("bad number", text, start, etc) 434 | end 435 | 436 | return as_number, i 437 | end 438 | 439 | 440 | local function grok_string(self, text, start, etc) 441 | 442 | if text:sub(start,start) ~= '"' then 443 | self:onDecodeError("expected string's opening quote", text, start, etc) 444 | end 445 | 446 | local i = start + 1 -- +1 to bypass the initial quote 447 | local text_len = text:len() 448 | local VALUE = "" 449 | while i <= text_len do 450 | local c = text:sub(i,i) 451 | if c == '"' then 452 | return VALUE, i + 1 453 | end 454 | if c ~= '\\' then 455 | VALUE = VALUE .. c 456 | i = i + 1 457 | elseif text:match('^\\b', i) then 458 | VALUE = VALUE .. "\b" 459 | i = i + 2 460 | elseif text:match('^\\f', i) then 461 | VALUE = VALUE .. "\f" 462 | i = i + 2 463 | elseif text:match('^\\n', i) then 464 | VALUE = VALUE .. "\n" 465 | i = i + 2 466 | elseif text:match('^\\r', i) then 467 | VALUE = VALUE .. "\r" 468 | i = i + 2 469 | elseif text:match('^\\t', i) then 470 | VALUE = VALUE .. "\t" 471 | i = i + 2 472 | else 473 | local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 474 | if hex then 475 | i = i + 6 -- bypass what we just read 476 | 477 | -- We have a Unicode codepoint. It could be standalone, or if in the proper range and 478 | -- followed by another in a specific range, it'll be a two-code surrogate pair. 479 | local codepoint = tonumber(hex, 16) 480 | if codepoint >= 0xD800 and codepoint <= 0xDBFF then 481 | -- it's a hi surrogate... see whether we have a following low 482 | local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 483 | if lo_surrogate then 484 | i = i + 6 -- bypass the low surrogate we just read 485 | codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) 486 | else 487 | -- not a proper low, so we'll just leave the first codepoint as is and spit it out. 488 | end 489 | end 490 | VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) 491 | 492 | else 493 | 494 | -- just pass through what's escaped 495 | VALUE = VALUE .. text:match('^\\(.)', i) 496 | i = i + 2 497 | end 498 | end 499 | end 500 | 501 | self:onDecodeError("unclosed string", text, start, etc) 502 | end 503 | 504 | local function skip_whitespace(text, start) 505 | 506 | local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 507 | if match_end then 508 | return match_end + 1 509 | else 510 | return start 511 | end 512 | end 513 | 514 | local grok_one -- assigned later 515 | 516 | local function grok_object(self, text, start, etc) 517 | if text:sub(start,start) ~= '{' then 518 | self:onDecodeError("expected '{'", text, start, etc) 519 | end 520 | 521 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' 522 | 523 | local VALUE = self.strictTypes and self:newObject { } or { } 524 | 525 | if text:sub(i,i) == '}' then 526 | return VALUE, i + 1 527 | end 528 | local text_len = text:len() 529 | while i <= text_len do 530 | local key, new_i = grok_string(self, text, i, etc) 531 | 532 | i = skip_whitespace(text, new_i) 533 | 534 | if text:sub(i, i) ~= ':' then 535 | self:onDecodeError("expected colon", text, i, etc) 536 | end 537 | 538 | i = skip_whitespace(text, i + 1) 539 | 540 | local new_val, new_i = grok_one(self, text, i) 541 | 542 | VALUE[key] = new_val 543 | 544 | -- 545 | -- Expect now either '}' to end things, or a ',' to allow us to continue. 546 | -- 547 | i = skip_whitespace(text, new_i) 548 | 549 | local c = text:sub(i,i) 550 | 551 | if c == '}' then 552 | return VALUE, i + 1 553 | end 554 | 555 | if text:sub(i, i) ~= ',' then 556 | self:onDecodeError("expected comma or '}'", text, i, etc) 557 | end 558 | 559 | i = skip_whitespace(text, i + 1) 560 | end 561 | 562 | self:onDecodeError("unclosed '{'", text, start, etc) 563 | end 564 | 565 | local function grok_array(self, text, start, etc) 566 | if text:sub(start,start) ~= '[' then 567 | self:onDecodeError("expected '['", text, start, etc) 568 | end 569 | 570 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' 571 | local VALUE = self.strictTypes and self:newArray { } or { } 572 | if text:sub(i,i) == ']' then 573 | return VALUE, i + 1 574 | end 575 | 576 | local VALUE_INDEX = 1 577 | 578 | local text_len = text:len() 579 | while i <= text_len do 580 | local val, new_i = grok_one(self, text, i) 581 | 582 | -- can't table.insert(VALUE, val) here because it's a no-op if val is nil 583 | VALUE[VALUE_INDEX] = val 584 | VALUE_INDEX = VALUE_INDEX + 1 585 | 586 | i = skip_whitespace(text, new_i) 587 | 588 | -- 589 | -- Expect now either ']' to end things, or a ',' to allow us to continue. 590 | -- 591 | local c = text:sub(i,i) 592 | if c == ']' then 593 | return VALUE, i + 1 594 | end 595 | if text:sub(i, i) ~= ',' then 596 | self:onDecodeError("expected comma or '['", text, i, etc) 597 | end 598 | i = skip_whitespace(text, i + 1) 599 | end 600 | self:onDecodeError("unclosed '['", text, start, etc) 601 | end 602 | 603 | 604 | grok_one = function(self, text, start, etc) 605 | -- Skip any whitespace 606 | start = skip_whitespace(text, start) 607 | 608 | if start > text:len() then 609 | self:onDecodeError("unexpected end of string", text, nil, etc) 610 | end 611 | 612 | if text:find('^"', start) then 613 | return grok_string(self, text, start, etc) 614 | 615 | elseif text:find('^[-0123456789 ]', start) then 616 | return grok_number(self, text, start, etc) 617 | 618 | elseif text:find('^%{', start) then 619 | return grok_object(self, text, start, etc) 620 | 621 | elseif text:find('^%[', start) then 622 | return grok_array(self, text, start, etc) 623 | 624 | elseif text:find('^true', start) then 625 | return true, start + 4 626 | 627 | elseif text:find('^false', start) then 628 | return false, start + 5 629 | 630 | elseif text:find('^null', start) then 631 | return nil, start + 4 632 | 633 | else 634 | self:onDecodeError("can't parse JSON", text, start, etc) 635 | end 636 | end 637 | 638 | function OBJDEF:decode(text, etc) 639 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 640 | OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc) 641 | end 642 | 643 | if text == nil then 644 | self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc) 645 | elseif type(text) ~= 'string' then 646 | self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc) 647 | end 648 | 649 | if text:match('^%s*$') then 650 | return nil 651 | end 652 | 653 | if text:match('^%s*<') then 654 | -- Can't be JSON... we'll assume it's HTML 655 | self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc) 656 | end 657 | 658 | -- 659 | -- Ensure that it's not UTF-32 or UTF-16. 660 | -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), 661 | -- but this package can't handle them. 662 | -- 663 | if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then 664 | self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc) 665 | end 666 | 667 | local success, value = pcall(grok_one, self, text, 1, etc) 668 | 669 | if success then 670 | return value 671 | else 672 | -- if JSON:onDecodeError() didn't abort out of the pcall, we'll have received the error message here as "value", so pass it along as an assert. 673 | if self.assert then 674 | self.assert(false, value) 675 | else 676 | assert(false, value) 677 | end 678 | -- and if we're still here, return a nil and throw the error message on as a second arg 679 | return nil, value 680 | end 681 | end 682 | 683 | local function backslash_replacement_function(c) 684 | if c == "\n" then 685 | return "\\n" 686 | elseif c == "\r" then 687 | return "\\r" 688 | elseif c == "\t" then 689 | return "\\t" 690 | elseif c == "\b" then 691 | return "\\b" 692 | elseif c == "\f" then 693 | return "\\f" 694 | elseif c == '"' then 695 | return '\\"' 696 | elseif c == '\\' then 697 | return '\\\\' 698 | else 699 | return string.format("\\u%04x", c:byte()) 700 | end 701 | end 702 | 703 | local chars_to_be_escaped_in_JSON_string 704 | = '[' 705 | .. '"' -- class sub-pattern to match a double quote 706 | .. '%\\' -- class sub-pattern to match a backslash 707 | .. '%z' -- class sub-pattern to match a null 708 | .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters 709 | .. ']' 710 | 711 | local function json_string_literal(value) 712 | local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) 713 | return '"' .. newval .. '"' 714 | end 715 | 716 | local function object_or_array(self, T, etc) 717 | -- 718 | -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON 719 | -- object. If there are only numbers, it's a JSON array. 720 | -- 721 | -- If we'll be converting to a JSON object, we'll want to sort the keys so that the 722 | -- end result is deterministic. 723 | -- 724 | local string_keys = { } 725 | local number_keys = { } 726 | local number_keys_must_be_strings = false 727 | local maximum_number_key 728 | 729 | for key in pairs(T) do 730 | if type(key) == 'string' then 731 | table.insert(string_keys, key) 732 | elseif type(key) == 'number' then 733 | table.insert(number_keys, key) 734 | if key <= 0 or key >= math.huge then 735 | number_keys_must_be_strings = true 736 | elseif not maximum_number_key or key > maximum_number_key then 737 | maximum_number_key = key 738 | end 739 | else 740 | self:onEncodeError("can't encode table with a key of type " .. type(key), etc) 741 | end 742 | end 743 | 744 | if #string_keys == 0 and not number_keys_must_be_strings then 745 | -- 746 | -- An empty table, or a numeric-only array 747 | -- 748 | if #number_keys > 0 then 749 | return nil, maximum_number_key -- an array 750 | elseif tostring(T) == "JSON array" then 751 | return nil 752 | elseif tostring(T) == "JSON object" then 753 | return { } 754 | else 755 | -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects 756 | return nil 757 | end 758 | end 759 | 760 | table.sort(string_keys) 761 | 762 | local map 763 | if #number_keys > 0 then 764 | -- 765 | -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array 766 | -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object. 767 | -- 768 | 769 | if self.noKeyConversion then 770 | self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) 771 | end 772 | 773 | -- 774 | -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings 775 | -- 776 | map = { } 777 | for key, val in pairs(T) do 778 | map[key] = val 779 | end 780 | 781 | table.sort(number_keys) 782 | 783 | -- 784 | -- Throw numeric keys in there as strings 785 | -- 786 | for _, number_key in ipairs(number_keys) do 787 | local string_key = tostring(number_key) 788 | if map[string_key] == nil then 789 | table.insert(string_keys , string_key) 790 | map[string_key] = T[number_key] 791 | else 792 | self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc) 793 | end 794 | end 795 | end 796 | 797 | return string_keys, nil, map 798 | end 799 | 800 | -- 801 | -- Encode 802 | -- 803 | -- 'options' is nil, or a table with possible keys: 804 | -- pretty -- if true, return a pretty-printed version 805 | -- indent -- a string (usually of spaces) used to indent each nested level 806 | -- align_keys -- if true, align all the keys when formatting a table 807 | -- 808 | local encode_value -- must predeclare because it calls itself 809 | function encode_value(self, value, parents, etc, options, indent) 810 | 811 | if value == nil then 812 | return 'null' 813 | 814 | elseif type(value) == 'string' then 815 | return json_string_literal(value) 816 | 817 | elseif type(value) == 'number' then 818 | if value ~= value then 819 | -- 820 | -- NaN (Not a Number). 821 | -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. 822 | -- 823 | return "null" 824 | elseif value >= math.huge then 825 | -- 826 | -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should 827 | -- really be a package option. Note: at least with some implementations, positive infinity 828 | -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. 829 | -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" 830 | -- case first. 831 | -- 832 | return "1e+9999" 833 | elseif value <= -math.huge then 834 | -- 835 | -- Negative infinity. 836 | -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. 837 | -- 838 | return "-1e+9999" 839 | else 840 | return tostring(value) 841 | end 842 | 843 | elseif type(value) == 'boolean' then 844 | return tostring(value) 845 | 846 | elseif type(value) ~= 'table' then 847 | self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) 848 | 849 | else 850 | -- 851 | -- A table to be converted to either a JSON object or array. 852 | -- 853 | local T = value 854 | 855 | if type(options) ~= 'table' then 856 | options = {} 857 | end 858 | if type(indent) ~= 'string' then 859 | indent = "" 860 | end 861 | 862 | if parents[T] then 863 | self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) 864 | else 865 | parents[T] = true 866 | end 867 | 868 | local result_value 869 | 870 | local object_keys, maximum_number_key, map = object_or_array(self, T, etc) 871 | if maximum_number_key then 872 | -- 873 | -- An array... 874 | -- 875 | local ITEMS = { } 876 | for i = 1, maximum_number_key do 877 | table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) 878 | end 879 | 880 | if options.pretty then 881 | result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" 882 | else 883 | result_value = "[" .. table.concat(ITEMS, ",") .. "]" 884 | end 885 | 886 | elseif object_keys then 887 | -- 888 | -- An object 889 | -- 890 | local TT = map or T 891 | 892 | if options.pretty then 893 | 894 | local KEYS = { } 895 | local max_key_length = 0 896 | for _, key in ipairs(object_keys) do 897 | local encoded = encode_value(self, tostring(key), parents, etc, options, indent) 898 | if options.align_keys then 899 | max_key_length = math.max(max_key_length, #encoded) 900 | end 901 | table.insert(KEYS, encoded) 902 | end 903 | local key_indent = indent .. tostring(options.indent or "") 904 | local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "") 905 | local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" 906 | 907 | local COMBINED_PARTS = { } 908 | for i, key in ipairs(object_keys) do 909 | local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) 910 | table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) 911 | end 912 | result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" 913 | 914 | else 915 | 916 | local PARTS = { } 917 | for _, key in ipairs(object_keys) do 918 | local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) 919 | local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent) 920 | table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) 921 | end 922 | result_value = "{" .. table.concat(PARTS, ",") .. "}" 923 | 924 | end 925 | else 926 | -- 927 | -- An empty array/object... we'll treat it as an array, though it should really be an option 928 | -- 929 | result_value = "[]" 930 | end 931 | 932 | parents[T] = false 933 | return result_value 934 | end 935 | end 936 | 937 | 938 | function OBJDEF:encode(value, etc, options) 939 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 940 | OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) 941 | end 942 | return encode_value(self, value, {}, etc, options or nil) 943 | end 944 | 945 | function OBJDEF:encode_pretty(value, etc, options) 946 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 947 | OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) 948 | end 949 | return encode_value(self, value, {}, etc, options or default_pretty_options) 950 | end 951 | 952 | function OBJDEF.__tostring() 953 | return "JSON encode/decode package" 954 | end 955 | 956 | OBJDEF.__index = OBJDEF 957 | 958 | function OBJDEF:new(args) 959 | local new = { } 960 | 961 | if args then 962 | for key, val in pairs(args) do 963 | new[key] = val 964 | end 965 | end 966 | 967 | return setmetatable(new, OBJDEF) 968 | end 969 | 970 | return OBJDEF:new() 971 | 972 | -- 973 | -- Version history: 974 | -- 975 | -- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really 976 | -- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines 977 | -- more flexible, and changed the default encode_pretty() to be more generally useful. 978 | -- 979 | -- Added a third 'options' argument to the encode() and encode_pretty() routines, to control 980 | -- how the encoding takes place. 981 | -- 982 | -- Updated docs to add assert() call to the loadfile() line, just as good practice so that 983 | -- if there is a problem loading JSON.lua, the appropriate error message will percolate up. 984 | -- 985 | -- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string, 986 | -- so that the source of the package, and its version number, are visible in compiled copies. 987 | -- 988 | -- 20140911.12 Minor lua cleanup. 989 | -- Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'. 990 | -- (Thanks to SmugMug's David Parry for these.) 991 | -- 992 | -- 20140418.11 JSON nulls embedded within an array were being ignored, such that 993 | -- ["1",null,null,null,null,null,"seven"], 994 | -- would return 995 | -- {1,"seven"} 996 | -- It's now fixed to properly return 997 | -- {1, nil, nil, nil, nil, nil, "seven"} 998 | -- Thanks to "haddock" for catching the error. 999 | -- 1000 | -- 20140116.10 The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up. 1001 | -- 1002 | -- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2", 1003 | -- and this caused some problems. 1004 | -- 1005 | -- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate, 1006 | -- and had of course diverged (encode_pretty didn't get the fixes that encode got, so 1007 | -- sometimes produced incorrect results; thanks to Mattie for the heads up). 1008 | -- 1009 | -- Handle encoding tables with non-positive numeric keys (unlikely, but possible). 1010 | -- 1011 | -- If a table has both numeric and string keys, or its numeric keys are inappropriate 1012 | -- (such as being non-positive or infinite), the numeric keys are turned into 1013 | -- string keys appropriate for a JSON object. So, as before, 1014 | -- JSON:encode({ "one", "two", "three" }) 1015 | -- produces the array 1016 | -- ["one","two","three"] 1017 | -- but now something with mixed key types like 1018 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 1019 | -- instead of throwing an error produces an object: 1020 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 1021 | -- 1022 | -- To maintain the prior throw-an-error semantics, set 1023 | -- JSON.noKeyConversion = true 1024 | -- 1025 | -- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry. 1026 | -- 1027 | -- 20130120.6 Comment update: added a link to the specific page on my blog where this code can 1028 | -- be found, so that folks who come across the code outside of my blog can find updates 1029 | -- more easily. 1030 | -- 1031 | -- 20111207.5 Added support for the 'etc' arguments, for better error reporting. 1032 | -- 1033 | -- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. 1034 | -- 1035 | -- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: 1036 | -- 1037 | -- * When encoding lua for JSON, Sparse numeric arrays are now handled by 1038 | -- spitting out full arrays, such that 1039 | -- JSON:encode({"one", "two", [10] = "ten"}) 1040 | -- returns 1041 | -- ["one","two",null,null,null,null,null,null,null,"ten"] 1042 | -- 1043 | -- In 20100810.2 and earlier, only up to the first non-null value would have been retained. 1044 | -- 1045 | -- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". 1046 | -- Version 20100810.2 and earlier created invalid JSON in both cases. 1047 | -- 1048 | -- * Unicode surrogate pairs are now detected when decoding JSON. 1049 | -- 1050 | -- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding 1051 | -- 1052 | -- 20100731.1 initial public release 1053 | -- 1054 | --------------------------------------------------------------------------------