├── var_nginx ├── logs │ └── .keep ├── run │ └── .keep ├── tmp │ └── .keep ├── content │ ├── html │ │ ├── favicon.ico │ │ ├── 404.html │ │ ├── logs.html │ │ ├── docs │ │ │ ├── websocket-workflow.md │ │ │ ├── index.md │ │ │ └── plugins.md │ │ ├── plugins.html │ │ └── index.html │ ├── img │ │ └── bot-engine-workflow.png │ ├── fonts │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── css │ │ ├── prettify.css │ │ └── bootstrap-theme.min.css │ └── js │ │ ├── sse.js │ │ ├── lang-lua.js │ │ ├── lb.js │ │ ├── underscore-min.js │ │ ├── underscore-min.map │ │ └── bootstrap.min.js ├── lubot_plugins │ ├── contrib │ │ └── .keep │ ├── user │ │ └── .keep │ ├── plugins.json │ ├── core │ │ ├── lubot_ping.lua │ │ ├── lubot_help.lua │ │ ├── lubot_image.lua │ │ ├── lubot_status.lua │ │ └── lubot_plugins.lua │ └── brainz │ │ ├── memory_brain.lua │ │ └── ngx_shared_brain.lua ├── local.conf.d │ ├── env.conf.d │ │ ├── slack.conf │ │ └── lubot.conf │ └── lua.conf.d │ │ ├── lubot.conf │ │ ├── plugin.conf │ │ └── slack.conf ├── lua │ ├── slackproxy.lua │ ├── ui.lua │ ├── utils │ │ ├── nginx.lua │ │ ├── brain.lua │ │ ├── log.lua │ │ ├── slack.lua │ │ ├── test.lua │ │ └── plugins.lua │ ├── sse.lua │ ├── slack.lua │ ├── markdown.lua │ ├── api.lua │ ├── router.lua │ └── init.lua ├── conf.d │ ├── 01-lua.conf │ ├── lubot.conf │ └── private.conf └── nginx.conf ├── .gitignore ├── Makefile ├── supervisord.conf ├── lua-releng ├── Dockerfile └── README.md /var_nginx/logs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var_nginx/run/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var_nginx/tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var_nginx/content/html/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/contrib/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/user/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var_nginx/local.conf.d/env.conf.d/slack.conf: -------------------------------------------------------------------------------- 1 | env SLACK_API_TOKEN; 2 | env SLACK_WEBHOOK_URL; 3 | -------------------------------------------------------------------------------- /var_nginx/content/html/404.html: -------------------------------------------------------------------------------- 1 |

Not Found

2 | The page you are looking for isn't here! 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | var_nginx/logs/*.log 2 | var_nginx/run/nginx.pid 3 | !var_nginx/lubot_plugins/user/.keep 4 | var_nginx/lubot_plugins/user/* 5 | -------------------------------------------------------------------------------- /var_nginx/content/img/bot-engine-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusis/lubot/HEAD/var_nginx/content/img/bot-engine-workflow.png -------------------------------------------------------------------------------- /var_nginx/local.conf.d/env.conf.d/lubot.conf: -------------------------------------------------------------------------------- 1 | env LUBOT_PLUGIN_CONFIG; 2 | env LUBOT_BOTNAME; 3 | env LUBOT_BRAIN; 4 | env LUBOT_BRAIN_OPTS; 5 | -------------------------------------------------------------------------------- /var_nginx/local.conf.d/lua.conf.d/lubot.conf: -------------------------------------------------------------------------------- 1 | lua_shared_dict lubot_config 10m; 2 | lua_shared_dict lubot_log 100m; 3 | lua_shared_dict lubot_brain 100m; 4 | -------------------------------------------------------------------------------- /var_nginx/content/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusis/lubot/HEAD/var_nginx/content/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /var_nginx/content/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusis/lubot/HEAD/var_nginx/content/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled":[ 3 | "status", 4 | "ping", 5 | "image", 6 | "plugins", 7 | "help" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /var_nginx/local.conf.d/lua.conf.d/plugin.conf: -------------------------------------------------------------------------------- 1 | lua_shared_dict plugin_active 1m; 2 | lua_shared_dict plugin_config 20m; 3 | lua_shared_dict plugin_log 20m; 4 | lua_shared_dict plugin_errors 20m; 5 | lua_shared_dict plugin_executions 20m; 6 | -------------------------------------------------------------------------------- /var_nginx/local.conf.d/lua.conf.d/slack.conf: -------------------------------------------------------------------------------- 1 | lua_shared_dict slack_running 100k; 2 | lua_shared_dict slack_users 20m; 3 | lua_shared_dict slack_groups 20m; 4 | lua_shared_dict slack_channels 20m; 5 | lua_shared_dict slack_ims 20m; 6 | lua_shared_dict slack_bots 20m; 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURDIR = `pwd` 2 | CONTENTDIR = $(CURDIR)/var_nginx 3 | 4 | 5 | all: image run 6 | 7 | image: 8 | docker build --rm -t lubot . 9 | 10 | run: 11 | docker run --rm --name lubot -p 3232:3232 -v $(CONTENTDIR):/var/nginx -i $(DOCKER_ENV) -t lubot 12 | 13 | .PHONY: image run all 14 | -------------------------------------------------------------------------------- /var_nginx/lua/slackproxy.lua: -------------------------------------------------------------------------------- 1 | local _VERSION = "0.0.1" 2 | 3 | local slack = require 'utils.slack' 4 | local pu = require 'utils.plugins' 5 | local ngu = require 'utils.nginx' 6 | 7 | ngx.req.read_body() 8 | local data = ngx.req.get_body_data() 9 | if not data or data == ngx.null then 10 | ngu.logerr("Post to slack proxy with empty body") 11 | return nil 12 | else 13 | local res, err = slack.post_chat_message(data) 14 | if res.status == 200 then 15 | return true 16 | else 17 | return nil 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /var_nginx/conf.d/01-lua.conf: -------------------------------------------------------------------------------- 1 | lua_shared_dict ng_shared_dict 1m; 2 | lua_shared_dict shared_locks 1m; 3 | 4 | include /var/nginx/local.conf.d/lua.conf.d/*.conf; 5 | 6 | lua_package_path '/var/nginx/lua/?.lua;/var/nginx/lubot_plugins/user/?.lua;/var/nginx/lubot_plugins/core/?.lua;/var/nginx/lubot_plugins/contrib/?.lua;/var/nginx/lubot_plugins/brainz/?.lua;;'; 7 | resolver 8.8.8.8; 8 | # We handle our socket errors ourself 9 | lua_socket_log_errors off; 10 | 11 | init_by_lua_file "/var/nginx/lua/init.lua"; 12 | init_worker_by_lua_file "/var/nginx/lua/slack.lua"; 13 | -------------------------------------------------------------------------------- /var_nginx/content/html/logs.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Logs

4 |

5 |
6 |
7 |
8 |
9 | 14 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | loglevel = error 3 | #logfile = /var/nginx/logs/supervisord.log 4 | pidfile = /tmp/supervisord.pid 5 | nodaemon = true 6 | 7 | [supervisorctl] 8 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket 9 | 10 | [program:lubot] 11 | command = /opt/openresty/nginx/sbin/nginx -c /var/nginx/nginx.conf 12 | stdout_logfile = /dev/stdout 13 | stdout_logfile_maxbytes = 0 14 | #stdout_logfile = /var/nginx/logs/%(program_name)s.log 15 | stderr_logfile = /dev/stderr 16 | stderr_logfile_maxbytes = 0 17 | #stderr_logfile = /var/nginx/logs/%(program_name)s.log 18 | autorestart = true 19 | -------------------------------------------------------------------------------- /var_nginx/content/html/docs/websocket-workflow.md: -------------------------------------------------------------------------------- 1 | # Nginx and Websocket Workflow 2 | The following convoluted diagram traces the flow from nginx startup to message proccessing. 3 | 4 | ![engine-workflow](/img/bot-engine-workflow.png) 5 | 6 | As stated previously, one worker is selected through a locking mechanism to actually connect to the Slack websocket. Since this a blocking behaviour, that worker is essentially removed from the pool for handling http requests. This isn't a big deal if lubot is only dealing with being a bot and responding to messages. It's a bigger deal if lubot is embedded in an existing nginx installation that normally services http requests. 7 | -------------------------------------------------------------------------------- /var_nginx/content/css/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} -------------------------------------------------------------------------------- /var_nginx/content/html/plugins.html: -------------------------------------------------------------------------------- 1 |

Plugin Status

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
nameversionerrorsexecutionstesthelp
15 | 21 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/core/lubot_ping.lua: -------------------------------------------------------------------------------- 1 | local id = "ping" 2 | local version = "0.0.1" 3 | local regex = [[ping]] 4 | local p = require 'utils.plugins' 5 | local slack = require 'utils.slack' 6 | 7 | local function run(data) 8 | local tstamp = ngx.now() 9 | 10 | local text = "pong ["..tstamp.."]" 11 | return slack.say(text) 12 | end 13 | 14 | local function test(data) 15 | local res = run(data) 16 | local expects = [=[^pong .*$]=] 17 | local params = { 18 | mock_data = data, 19 | expects = expects, 20 | run_data = res 21 | } 22 | local t = require('utils.test').new(params) 23 | t:add("responds_text") 24 | t:add("response_contains") 25 | t:run() 26 | return t:report() 27 | end 28 | 29 | local plugin = { 30 | run = run, 31 | id = id, 32 | version = version, 33 | regex = regex, 34 | test = test 35 | } 36 | 37 | return plugin 38 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/brainz/memory_brain.lua: -------------------------------------------------------------------------------- 1 | local _brain = {} 2 | local _B = {} 3 | _brain._VERSION = "0.0.1" 4 | _brain.name = "memory" 5 | _brain.persistent = false 6 | local inspect = require 'inspect' 7 | 8 | function _brain.new(...) 9 | local self = {} 10 | setmetatable(self, {__index = _B}) 11 | self._brainpan = {} 12 | return self 13 | end 14 | 15 | function _B:save() 16 | -- noop 17 | return true 18 | end 19 | 20 | function _B:keys() 21 | local t = {} 22 | for k, v in pairs(self._brainpan) do 23 | table.insert(t, k) 24 | end 25 | return t 26 | end 27 | 28 | function _B:export() 29 | -- noop 30 | end 31 | 32 | function _B:populate(t) 33 | self._brainpan = t 34 | return self._brainpan 35 | end 36 | 37 | function _B:get(k) 38 | return self._brainpan[k] 39 | end 40 | 41 | function _B:set(k, v) 42 | self._brainpan[k] = v 43 | return 1 44 | end 45 | 46 | function _B:safe_set(k, v) 47 | if self._brainpan[k] then 48 | return nil 49 | else 50 | self._brainpan[k] = v 51 | return 1 52 | end 53 | end 54 | 55 | function _B:delete(k) 56 | self._brainpan[k] = nil 57 | end 58 | 59 | function _B:flush() 60 | self._brainpan = {} 61 | end 62 | return _brain 63 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/core/lubot_help.lua: -------------------------------------------------------------------------------- 1 | _plugin = {} 2 | 3 | _plugin.id = "help" 4 | _plugin.version = "0.0.1" 5 | _plugin.regex = [=[help\s?(?\w+)?$]=] 6 | local p = require 'utils.plugins' 7 | local chat = require 'utils.slack' 8 | local ngu = require 'utils.nginx' 9 | local log = require 'utils.log' 10 | 11 | function _plugin.run(data) 12 | if not data.text then 13 | return nil, "Missing message text" 14 | end 15 | local m, err = ngx.re.match(data.text, _plugin.regex, 'jo') 16 | if not m then 17 | return nil, "Unable to match '"..data.text.."' to '".._plugin.regex.."'" 18 | else 19 | if not m.plugin_name then 20 | return chat.say(_plugin.help()) 21 | else 22 | return chat.say(p.plugin_help(m.plugin_name)) 23 | end 24 | end 25 | end 26 | 27 | function _plugin.help() 28 | local h = [[ 29 | help : Displays help for a given plugin 30 | ]] 31 | return h 32 | end 33 | 34 | function _plugin.test(...) 35 | -- use our own data here 36 | local data = {user = "foo", channel = "bar", text = "plugin help help"} 37 | local res, err = _plugin.run(data) 38 | local params = { 39 | mock_data = data, 40 | run_data = res 41 | } 42 | local t = require('utils.test').new(params) 43 | t:add("responds_text") 44 | t:add("response_contains", "Displays help for a given plugin") 45 | t:run() 46 | return t:report() 47 | end 48 | 49 | return _plugin 50 | -------------------------------------------------------------------------------- /var_nginx/lua/ui.lua: -------------------------------------------------------------------------------- 1 | local _VERSION = "0.0.1" 2 | local template = require 'resty.template' 3 | template.caching(false) 4 | 5 | local shared_dict = ngx.shared.ng_shared_dict 6 | local botname, err = shared_dict:get("bot_name") 7 | 8 | local function landing_page() 9 | local content = template.compile("

"..botname.." - the openresty chatbot

") 10 | template.render("index.html", { 11 | ngx = ngx, 12 | botname = botname, 13 | content = content, 14 | menubar = menubar 15 | }) 16 | end 17 | 18 | local function get_page(regex) 19 | local pagename = regex['path'] or '404' 20 | local plu = require 'utils.plugins' 21 | local active_plugins = plu.dicts.active:get_keys() 22 | local plugins = {} 23 | for _, k in pairs(active_plugins) do 24 | plugins[k] = { 25 | name = k, 26 | errors = plu.dicts.errors:get(k) or 0, 27 | config = plu.dicts.config:get(k) or "none", 28 | executions = plu.dicts.executions:get(k) or 0 29 | } 30 | end 31 | local subcontent = template.compile(pagename..".html"){active_plugins = plugins} 32 | if subcontent == pagename..".html" then 33 | subcontent = template.compile('404.html') 34 | end 35 | template.render("index.html", { 36 | menubar = menubar, 37 | ngx = ngx, 38 | botname = botname, 39 | content = subcontent 40 | }) 41 | end 42 | 43 | local m, err = ngx.re.match(ngx.var.uri, [=[^/(?\w+)(?/.*$)?]=]) 44 | if not m then 45 | return landing_page() 46 | else 47 | return get_page(m) 48 | end 49 | -------------------------------------------------------------------------------- /var_nginx/lua/utils/nginx.lua: -------------------------------------------------------------------------------- 1 | local m = {} 2 | m._VERSION = "0.0.1" 3 | 4 | function m.logerr(...) 5 | local caller = debug.getinfo(2).name 6 | if not caller then 7 | ngx.log(ngx.ERR, ...) 8 | else 9 | ngx.log(ngx.ERR,"["..caller.."] ", ...) 10 | end 11 | end 12 | 13 | function m.logwarn(...) 14 | local caller = debug.getinfo(2).name 15 | if not caller then 16 | ngx.log(ngx.WARN, ...) 17 | else 18 | ngx.log(ngx.WARN,"["..caller.."] ", ...) 19 | end 20 | end 21 | 22 | function m.loginfo(...) 23 | local caller = debug.getinfo(2).name 24 | if not caller then 25 | ngx.log(ngx.INFO, ...) 26 | else 27 | ngx.log(ngx.INFO,"["..caller.."] ", ...) 28 | end 29 | end 30 | 31 | function m.logdebug(...) 32 | local caller = debug.getinfo(2).name 33 | if not caller then 34 | ngx.log(ngx.DEBUG, ...) 35 | else 36 | ngx.log(ngx.DEBUG,"["..caller.."] ", ...) 37 | end 38 | end 39 | 40 | function m.lognotice(...) 41 | local caller = debug.getinfo(2).name 42 | if not caller then 43 | ngx.log(ngx.NOTICE, ...) 44 | else 45 | ngx.log(ngx.NOTICE,"["..caller.."] ", ...) 46 | end 47 | end 48 | 49 | function m.logalert(...) 50 | local caller = debug.getinfo(2).name 51 | if not caller then 52 | ngx.log(ngx.ALERT,...) 53 | else 54 | ngx.log(ngx.ALERT,"["..caller.."] ",...) 55 | end 56 | end 57 | 58 | function m.inspect(...) 59 | local caller = debug.getinfo(2).name 60 | local inspect = require 'inspect' 61 | if not caller then 62 | ngx.log(ngx.ALERT,"Inspecting: ",inspect(...)) 63 | else 64 | ngx.log(ngx.ALERT, "Inspecting "..caller..":", inspect(...)) 65 | end 66 | end 67 | 68 | return m 69 | -------------------------------------------------------------------------------- /var_nginx/content/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | {{botname}} 21 | 22 | 23 | 45 |
46 |
47 | {*content*} 48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /var_nginx/lua/utils/brain.lua: -------------------------------------------------------------------------------- 1 | local _brain = {} 2 | local _B = {} 3 | _brain._VERSION = "0.0.1" 4 | 5 | local log = require 'utils.log' 6 | 7 | function _brain.new(driver, driver_opts) 8 | local self = {} 9 | setmetatable(self, {__index = _B}) 10 | local d 11 | if not driver then 12 | print('No driver specified. Using memory') 13 | d = 'memory_brain' 14 | else 15 | print('Using driver: '..driver) 16 | d = driver..'_brain' 17 | end 18 | local p_ok, p = pcall(require, d) 19 | if not p_ok then 20 | print("Got error requiring brain module: "..p) 21 | return nil 22 | else 23 | self.__driver = p 24 | self.driver_name = p.name 25 | if p.persistent == false then 26 | print("Warning! Using a non-persistent brain. Data will be lost at shutdown") 27 | end 28 | end 29 | self.driver = p.new(unpack{driver_opts}) 30 | if not self.driver then 31 | print("Could not instantiate new brain") 32 | return nil 33 | end 34 | return self 35 | end 36 | 37 | function _B:save(...) 38 | return self.driver:save(unpack({...})) 39 | end 40 | 41 | function _B:keys(...) 42 | return self.driver:keys(unpack({...})) 43 | end 44 | 45 | function _B:export(...) 46 | return self.driver:export(unpack({...})) 47 | end 48 | 49 | function _B:populate(...) 50 | return self.driver:populate(unpack({...})) 51 | end 52 | 53 | function _B:get(...) 54 | return self.driver:get(unpack({...})) 55 | end 56 | 57 | function _B:set(...) 58 | return self.driver:set(unpack({...})) 59 | end 60 | 61 | function _B:safe_set(...) 62 | return self.driver:safe_set(unpack({...})) 63 | end 64 | 65 | function _B:delete(...) 66 | return self.driver:delete(unpack({...})) 67 | end 68 | 69 | function _B:flush(...) 70 | return self.driver:flush(unpack({...})) 71 | end 72 | 73 | 74 | return _brain 75 | -------------------------------------------------------------------------------- /var_nginx/conf.d/lubot.conf: -------------------------------------------------------------------------------- 1 | upstream private_api { 2 | server unix:/var/nginx/tmp/ngx.private.sock; 3 | #server 127.0.0.1:3131; 4 | } 5 | 6 | server { 7 | listen 3232; 8 | server_name 127.0.0.1 localhost; 9 | lua_need_request_body on; 10 | client_max_body_size 2048M; 11 | default_type text/html; 12 | underscores_in_headers on; 13 | set $template_root '/var/nginx/content/html'; 14 | 15 | location /capture { 16 | internal; 17 | proxy_buffering off; 18 | proxy_max_temp_file_size 0; 19 | resolver 8.8.8.8; 20 | set_unescape_uri $clean_url $arg_url; 21 | proxy_pass $clean_url; 22 | } 23 | 24 | #location /slack-sse { 25 | # chunked_transfer_encoding off; 26 | # proxy_pass http://127.0.0.1:3131/slack; 27 | # proxy_buffering off; 28 | # proxy_cache off; 29 | # proxy_set_header Connection ''; 30 | # proxy_http_version 1.1; 31 | #} 32 | 33 | location /docs { 34 | lua_code_cache off; 35 | content_by_lua_file '/var/nginx/lua/markdown.lua'; 36 | } 37 | 38 | location /api/logs { 39 | lua_code_cache off; 40 | postpone_output 0; 41 | lua_check_client_abort on; 42 | content_by_lua_file '/var/nginx/lua/sse.lua'; 43 | } 44 | 45 | location /api { 46 | lua_code_cache off; 47 | proxy_pass_request_headers on; 48 | proxy_redirect off; 49 | proxy_buffering off; 50 | proxy_cache off; 51 | rewrite ^/api/(.*) /_private/api/$1 break; 52 | proxy_pass http://private_api; 53 | } 54 | 55 | location /fonts { 56 | alias /var/nginx/content/fonts/; 57 | } 58 | 59 | location /css { 60 | alias /var/nginx/content/css/; 61 | } 62 | 63 | location /js { 64 | alias /var/nginx/content/js/; 65 | } 66 | 67 | location /img { 68 | alias /var/nginx/content/img/; 69 | } 70 | 71 | location / { 72 | lua_code_cache off; 73 | content_by_lua_file '/var/nginx/lua/ui.lua'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /var_nginx/lua/utils/log.lua: -------------------------------------------------------------------------------- 1 | local m = {} 2 | m._VERSION = "0.0.1" 3 | local n = require 'utils.nginx' 4 | 5 | function m.err(...) 6 | local caller = debug.getinfo(2).name 7 | if not caller then 8 | ngx.log(ngx.ERR, ...) 9 | else 10 | ngx.log(ngx.ERR,"["..caller.."] ", ...) 11 | end 12 | end 13 | 14 | function m.warn(...) 15 | local caller = debug.getinfo(2).name 16 | if not caller then 17 | ngx.log(ngx.WARN, ...) 18 | else 19 | ngx.log(ngx.WARN,"["..caller.."] ", ...) 20 | end 21 | end 22 | 23 | function m.info(...) 24 | local caller = debug.getinfo(2).name 25 | if not caller then 26 | ngx.log(ngx.INFO, ...) 27 | else 28 | ngx.log(ngx.INFO,"["..caller.."] ", ...) 29 | end 30 | end 31 | 32 | function m.debug(...) 33 | local caller = debug.getinfo(2).name 34 | if not caller then 35 | ngx.log(ngx.DEBUG, ...) 36 | else 37 | ngx.log(ngx.DEBUG,"["..caller.."] ", ...) 38 | end 39 | end 40 | 41 | function m.notice(...) 42 | local caller = debug.getinfo(2).name 43 | if not caller then 44 | ngx.log(ngx.NOTICE, ...) 45 | else 46 | ngx.log(ngx.NOTICE,"["..caller.."] ", ...) 47 | end 48 | end 49 | 50 | function m.alert(...) 51 | local caller = debug.getinfo(2).name 52 | if not caller then 53 | ngx.log(ngx.ALERT,...) 54 | else 55 | ngx.log(ngx.ALERT,"["..caller.."] ",...) 56 | end 57 | end 58 | 59 | function m.inspect(t, ...) 60 | local inspect = require 'inspect' 61 | local caller = debug.getinfo(2).name 62 | local args = {...} 63 | if #args > 0 then 64 | if not caller then 65 | m.alert(table.concat(args, " "), inspect(t)) 66 | else 67 | m.alert(caller..":", table.concat(args, " "), inspect(t)) 68 | end 69 | else 70 | if not caller then 71 | m.alert("inspecting :", inspect(t)) 72 | else 73 | m.alert("inspecting "..caller..":", inspect(t)) 74 | end 75 | end 76 | end 77 | 78 | return m 79 | -------------------------------------------------------------------------------- /var_nginx/lua/sse.lua: -------------------------------------------------------------------------------- 1 | local shared_dict = ngx.shared.ng_shared_dict 2 | local log_dict = ngx.shared.plugin_log 3 | 4 | -- following helper function was cribbed from the fine folks at 3scale 5 | -- https://github.com/3scale/nginx-oauth-templates/blob/master/oauth2/authorization-code-flow/no-token-generation/nginx.lua#L76-L90 6 | function string:split(delimiter) 7 | local result = { } 8 | local from = 1 9 | local delim_from, delim_to = string.find( self, delimiter, from ) 10 | 11 | while delim_from do 12 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 13 | from = delim_to + 1 14 | delim_from, delim_to = string.find( self, delimiter, from ) 15 | end 16 | 17 | table.insert( result, string.sub( self, from ) ) 18 | 19 | return result 20 | end 21 | 22 | local function start_log_tailer() 23 | ngx.log(ngx.ALERT, "Client connected to sse") 24 | ngx.header.content_type = 'text/event-stream' 25 | ngx.say("event: keepalive\ndata: "..ngx.utctime().."\n") 26 | while true do 27 | local log_entries = log_dict:get_keys() 28 | for _, k in ipairs(log_entries) do 29 | local e = log_dict:get(k) 30 | local str_t = k:split(":") 31 | local msg = { 32 | id = k, 33 | timestamp = str_t[2], 34 | sender = str_t[1], 35 | message = e 36 | } 37 | local json = safe_json_encode(msg) 38 | if not json then 39 | ngx.say("data: ["..k.."] "..e.."\n") 40 | else 41 | ngx.say("id: "..k.."\nevent: logevent\ndata: "..json.."\n") 42 | end 43 | end 44 | ngx.say("event: keepalive\ndata: "..ngx.utctime().."\n") 45 | ngx.flush(true) 46 | ngx.sleep(15) 47 | end 48 | end 49 | local ok, err = ngx.on_abort(function () 50 | ngx.log(ngx.ALERT, "Client disconnected from sse stream") 51 | ngx.exit(499) 52 | end) 53 | if not ok then 54 | ngx.log(ngx.ERR, "Can't register on_abort function.") 55 | ngx.exit(500) 56 | end 57 | start_log_tailer() 58 | -------------------------------------------------------------------------------- /var_nginx/conf.d/private.conf: -------------------------------------------------------------------------------- 1 | server { 2 | access_log off; 3 | # Internal services that shouldn't be exposed externally 4 | # we only listen on a domain socket now 5 | # listen 3131; 6 | listen unix:/var/nginx/tmp/ngx.private.sock; 7 | server_name 127.0.0.1 localhost; 8 | lua_need_request_body on; 9 | client_max_body_size 2048M; 10 | default_type text/html; 11 | index index.html; 12 | underscores_in_headers on; 13 | 14 | # A private proxy pass for ngx.location.capture calls 15 | location /capture { 16 | internal; 17 | proxy_buffering off; 18 | proxy_max_temp_file_size 0; 19 | resolver 8.8.8.8; 20 | set_unescape_uri $clean_url $arg_url; 21 | proxy_pass $clean_url; 22 | } 23 | 24 | # Private internal-only API 25 | # This should never be exposed directly 26 | location /_private/api { 27 | lua_code_cache off; 28 | content_by_lua_file '/var/nginx/lua/api.lua'; 29 | } 30 | 31 | # Private slack chat.postMessage proxy 32 | location /_private/slackpost { 33 | lua_code_cache off; 34 | content_by_lua_file '/var/nginx/lua/slackproxy.lua'; 35 | } 36 | } 37 | 38 | server { 39 | access_log off; 40 | # wss proxy only 41 | # as soon as lua-resty-websocket supports ssl, this will go away 42 | listen 3131; 43 | server_name 127.0.0.1 localhost; 44 | lua_need_request_body on; 45 | client_max_body_size 2048M; 46 | default_type text/html; 47 | index index.html; 48 | underscores_in_headers on; 49 | # Private websocket ssl proxy 50 | location /wssproxy { 51 | resolver 8.8.8.8; 52 | proxy_max_temp_file_size 0; 53 | proxy_buffering off; 54 | proxy_connect_timeout 1d; 55 | proxy_read_timeout 1d; 56 | proxy_send_timeout 1d; 57 | set_unescape_uri $clean_url $arg_url; 58 | proxy_pass $clean_url; 59 | proxy_set_header X-Real-IP $remote_addr; 60 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 61 | proxy_http_version 1.1; 62 | proxy_set_header Upgrade $http_upgrade; 63 | proxy_set_header Connection "upgrade"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /var_nginx/content/js/sse.js: -------------------------------------------------------------------------------- 1 | var evtSource; 2 | var sse_url = window.location.protocol+"//"+window.location.hostname+":"+window.location.port+"/api/logs"; 3 | if (!String.prototype.startsWith) { 4 | Object.defineProperty(String.prototype, 'startsWith', { 5 | enumerable: false, 6 | configurable: false, 7 | writable: false, 8 | value: function(searchString, position) { 9 | position = position || 0; 10 | return this.lastIndexOf(searchString, position) === position; 11 | } 12 | }); 13 | } 14 | 15 | function sseConnect() { 16 | evtSource = new EventSource(sse_url); 17 | $("#sse_connect_status").attr("onclick", "sseDisconnect();"); 18 | evtSource.addEventListener("keepalive", function(e) { 19 | console.log("Keepalive from server: "+e.data); 20 | $("#sse_connect_status").css("color", "green"); 21 | }) 22 | evtSource.addEventListener("logevent", function(e) { 23 | var evt = JSON.parse(e.data); 24 | sseLog("["+evt.timestamp+"] ("+evt.sender+") "+evt.message); 25 | }) 26 | evtSource.onerror = function(e) { 27 | switch(evtSource.readyState) { 28 | case 0: 29 | $("#sse_connect_status").attr("onclick", "sseDisconnect();"); 30 | $("#sse_connect_status").css("color", "yellow"); 31 | break; 32 | case 1: 33 | $("#sse_connect_status").attr("onclick", "sseDisconnect();"); 34 | $("#sse_connect_status").css("color", "green"); 35 | break; 36 | case 2: 37 | $("#sse_connect_status").attr("onclick", "sseConnect();"); 38 | $("#sse_connect_status").css("color", "red"); 39 | break; 40 | default: 41 | $("#sse_connect_status").attr("onclick", "sseDisconnect();"); 42 | $("#sse_connect_status").css("color", "green"); 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | 49 | function sseDisconnect() { 50 | $("#sse_connect_status").css("color", "red"); 51 | $("#sse_connect_status").attr("onclick", "sseConnect();"); 52 | evtSource.close(); 53 | } 54 | 55 | function sseLog(text) { 56 | $("#logwindow").append(document.createTextNode(text+"\n")); 57 | $("#logwindow").scrollTop = $("#logwindow").scrollHeight; 58 | console.log(text); 59 | return false; 60 | } 61 | -------------------------------------------------------------------------------- /var_nginx/content/html/docs/index.md: -------------------------------------------------------------------------------- 1 | # Lubot Documentation 2 | Lubot is an experimental [Slack](https://slack.com) [chatbot](https://www.youtube.com/watch?v=NST3u-GjjFw) written in Lua hosted inside [OpenResty](http://openresty.org) 3 | 4 | ## Usage 5 | To use lubot, you'll need four things: 6 | 7 | - A slack bot integration 8 | - A slack incoming webhook API key 9 | - Docker 10 | - This repo cloned locally 11 | 12 | ## Building 13 | The repository ships with a `Makefile` you can use to run everything inside Docker. 14 | From the root of the repository run the following: 15 | 16 | ``` 17 | make image 18 | ``` 19 | 20 | This will build a Docker image named `lubot` for you. 21 | 22 | or to use a different name for the image: 23 | 24 | ``` 25 | docker build --rm -t 26 | ``` 27 | 28 | You should never blindly trust a random `Dockerfile` so look closely at the one shipped with the repo. Feel free to make changes but do so AFTER the core stuff is built. 29 | 30 | ## Running 31 | If you used the `Makefile` to build, then you can start it up with: 32 | 33 | ``` 34 | DOCKER_ENV='--env LUBOT_PLUGIN_CONFIG=/var/nginx/lubot_plugins/lubot/plugins/plugins.json \ 35 | --env SLACK_API_TOKEN= \ 36 | --env SLACK_WEBHOOK_URL=""' \ 37 | make run 38 | ``` 39 | If you build the container with a different name, the run command in the `Makefile` can be used as a starting point. 40 | 41 | At this point you should see your bot visible in slack. Three basic example plugins ship out of the box: 42 | 43 | - `ping`: Works similar to hubot's ping 44 | - `status`: Returns some basic stats about the bot and slack account being used 45 | - `image`: Basic version of the hubot `image me` plugin 46 | - `help`: Help support for plugins (yes you can call `help` on the `help` plugin) 47 | - `plugins`: Plugin management itself is a plugin 48 | 49 | There is also a [webui](/docs/webui) available with some basic functionality and also an [api](/docs/api) that you can hit with curl. 50 | 51 | ## Nginx and Websocket internals 52 | See [workflow](/docs/websocket-workflow) 53 | 54 | ## Customizing 55 | See [customization](/docs/customizing) 56 | 57 | ## Logging 58 | See [logging](/docs/logging) 59 | 60 | ## Persistence 61 | See [brains](/docs/brains) 62 | 63 | ## Additional plugin documentation 64 | See [plugins](/docs/plugins) 65 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/core/lubot_image.lua: -------------------------------------------------------------------------------- 1 | local id = "image" 2 | local version = "0.0.1" 3 | local regex = [=[(image|img)( me)? (?.*)$]=] 4 | 5 | local p = require 'utils.plugins' 6 | local slack = require 'utils.slack' 7 | local ngu = require 'utils.nginx' 8 | local log = require 'utils.log' 9 | 10 | local function query_google(str) 11 | local gurl = "http://ajax.googleapis.com/ajax/services/search/images" 12 | local params = { 13 | v = "1.0", 14 | rsz = "8", 15 | q = str, 16 | safe = "active" 17 | } 18 | local cjson = require 'cjson' 19 | local hc = require("httpclient").new('httpclient.ngx_driver') 20 | local res = hc:get(gurl, {params = params}) 21 | if res.err then 22 | return nil 23 | else 24 | 25 | local b = safe_json_decode(res.body) 26 | if not b then return nil end 27 | local candidates = b.responseData.results 28 | return p.pick_random(candidates).unescapedUrl 29 | end 30 | end 31 | 32 | local function match(str) 33 | local m, err = ngx.re.match(str, regex, 'jo') 34 | if not m then 35 | log.err(err or "No match for "..str) 36 | return nil 37 | else 38 | return m['search_string'] 39 | end 40 | end 41 | 42 | local function run(data) 43 | if not data.text then 44 | return nil, "Missing message text" 45 | end 46 | local m = match(data.text) 47 | if not m then 48 | log.err("Missing string to search for") 49 | return nil, "Missing string to search for" 50 | else 51 | local img = query_google(m) 52 | if not img then 53 | log.err("No image found") 54 | return nil, "No image found" 55 | end 56 | return slack.say(img) 57 | end 58 | end 59 | 60 | local function test(data) 61 | local res = run(data) 62 | -- response should be a url 63 | local expects = [=[^http.*$]=] 64 | local params = { 65 | mock_data = data, 66 | run_data = res 67 | } 68 | local t = require('utils.test').new(params) 69 | -- basic tests 70 | t:add("responds_text") 71 | t:add("response_contains", expects) 72 | t:add("parses_text", regex, 'search_string') 73 | t:add("captures_value", data.expects, regex, 'search_string') 74 | t:run() 75 | return t:report() 76 | end 77 | 78 | local plugin = { 79 | run = run, 80 | id = id, 81 | version = version, 82 | regex = regex, 83 | test = test 84 | } 85 | 86 | return plugin 87 | -------------------------------------------------------------------------------- /var_nginx/lua/slack.lua: -------------------------------------------------------------------------------- 1 | local _VERSION = "0.0.1" 2 | local ngu = require 'utils.nginx' 3 | 4 | -- shared dicts 5 | local shared_dict = ngx.shared.ng_shared_dict 6 | local locks = ngx.shared.shared_locks 7 | 8 | local slack_token = shared_dict:get('slack_token') 9 | 10 | local function start_rtm(premature) 11 | if premature then return nil end 12 | local slock = require 'resty.lock' 13 | local mylock = slock:new("shared_locks",{timeout = 60, exptime = 120}) 14 | local locked, err = mylock:lock("slack_polling") 15 | if err then 16 | ngx.sleep(60) 17 | else 18 | local slack_running = ngx.shared.slack_running 19 | local ok, err = slack_running:add("locked", ngx.worker.pid()) 20 | if err then 21 | if slack_running:get("locked") == ngx.worker.pid() then 22 | -- we couldn't get a lock to run but we're listed as the pid owner 23 | -- this likely means our thread crashed 24 | -- let's clear this lock and start fresh 25 | ngu.logwarn("previous lock holder thread likely crashed. starting over") 26 | slack_running:delete("locked") 27 | end 28 | local ok, err = mylock:unlock() 29 | if not ok then 30 | ngu.logerr("Unable to clear lock: ", err) 31 | else 32 | ngu.logdebug("Locked held for ", locked) 33 | end 34 | ngx.sleep(60) 35 | else 36 | ngu.logdebug(ngx.worker.pid(), " set the internal lock") 37 | -- we can clear the lock now as we've set our pid 38 | local ok, err = mylock:unlock() 39 | if not ok then 40 | ngu.logerr("Unable to clear lock: ", err) 41 | else 42 | ngu.logdebug("Locked held for ", locked) 43 | end 44 | res = slackbot() 45 | if res == false then 46 | slack_running:delete("locked") 47 | ngu.logerr("Slackbot function exited for some reason") 48 | end 49 | end 50 | end 51 | -- wait before starting the loop again 52 | -- previous we relied on lock waits but 53 | -- since we release after setting our other lock 54 | -- we have to emulate that 55 | start_rtm(nil) 56 | end 57 | 58 | -- main entry 59 | if not slack_token then 60 | ngu.logerr("Gotta set a slack token") 61 | return nil 62 | else 63 | local ok, err = ngx.timer.at(1, start_rtm) 64 | if not ok then 65 | -- skip 66 | else 67 | ngu.logdebug("Initial timer started") 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /var_nginx/lua/markdown.lua: -------------------------------------------------------------------------------- 1 | local _VERSION = "0.0.1" 2 | -- lua-httpclient ships with a better url/uri parser 3 | local neturl = require 'httpclient.neturl' 4 | 5 | -- load the template stuff 6 | local template = require 'resty.template' 7 | 8 | -- We need to read files with something that reads our files and then wraps the response 9 | -- in the markdown parsing syntax 10 | -- based off https://github.com/bungle/lua-resty-template/blob/master/lib/resty/template.lua#L44-L50 11 | local function load_wrapper(s, template_instance) 12 | local md_file = ngx.var.template_root.."/docs/"..s..".md" 13 | local file = io.open(md_file, "rb") 14 | if not file then ngx.log(ngx.ERR, "Unable to open file: ", md_file); return nil end 15 | local content = file:read("*a") 16 | file:close() 17 | if not content or content == '' then content = [[# Missing file]] end 18 | --local compiled_content = template_instance.compile(content) 19 | --local prestyle = "" 20 | return "{*markdown([["..content.."]], {extensions = {'fenced_code', 'tables', 'quote'}, renderer = 'html', nesting = 2})*}" 21 | end 22 | --template.load = load_wrapper 23 | template.caching(false) 24 | template.markdown = require "resty.hoedown" 25 | 26 | -- following helper function was cribbed from the fine folks at 3scale 27 | -- https://github.com/3scale/nginx-oauth-templates/blob/master/oauth2/authorization-code-flow/no-token-generation/nginx.lua#L76-L90 28 | function string:split(delimiter) 29 | local result = { } 30 | local from = 1 31 | local delim_from, delim_to = string.find( self, delimiter, from ) 32 | 33 | while delim_from do 34 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 35 | from = delim_to + 1 36 | delim_from, delim_to = string.find( self, delimiter, from ) 37 | end 38 | 39 | table.insert( result, string.sub( self, from ) ) 40 | 41 | return result 42 | end 43 | 44 | -- split the path into parts and then get the last element 45 | local paths = string.split(neturl.parse(ngx.var.uri).path, "/") 46 | local doc = paths[#paths] 47 | local content = load_wrapper(doc, template) 48 | if not content then 49 | content = nil 50 | end 51 | local shared_dict = ngx.shared.ng_shared_dict 52 | local botname, err = shared_dict:get("bot_name") 53 | if not botname then botname = 'lubot' end 54 | template.render("index.html", { menubar = menubar, ngx = ngx, botname = botname, content = template.compile(content)}) 55 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/core/lubot_status.lua: -------------------------------------------------------------------------------- 1 | local id = "status" 2 | local version = "0.0.1" 3 | local regex = [[status]] 4 | 5 | local p = require 'utils.plugins' 6 | local slack = require 'utils.slack' 7 | 8 | local function run(data) 9 | 10 | local slack_running = ngx.shared.slack_running 11 | local slack_users = ngx.shared.slack_users 12 | local slack_groups = ngx.shared.slack_groups 13 | local slack_channels = ngx.shared.slack_channels 14 | local slack_ims = ngx.shared.slack_ims 15 | local slack_bots = ngx.shared.slack_bots 16 | local lubot_config = ngx.shared.lubot_config 17 | 18 | local worker_pid = slack_running:get("locked") 19 | local users = #slack_users:get_keys() 20 | local groups = #slack_groups:get_keys() 21 | local channels = #slack_channels:get_keys() 22 | local ims = #slack_ims:get_keys() 23 | local bots = #slack_bots:get_keys() 24 | local config_file = lubot_config:get("config_file") 25 | 26 | 27 | fields = { 28 | { 29 | title = "Current worker pid", 30 | value = worker_pid, 31 | short = true 32 | }, 33 | { 34 | title = "Config file", 35 | value = "`"..config_file.."`", 36 | short = true, 37 | mrkdwn = true 38 | }, 39 | { 40 | title = "User count", 41 | value = users, 42 | short = true 43 | }, 44 | { 45 | title = "My Private Groups", 46 | value = groups, 47 | short = true 48 | }, 49 | { 50 | title = "Public Channels", 51 | value = channels, 52 | short = true 53 | }, 54 | { 55 | title = "IMs", 56 | value = ims, 57 | short = true 58 | }, 59 | { 60 | title = "Total integrations", 61 | value = bots, 62 | short = true 63 | } 64 | } 65 | local t = { 66 | text = "Current Status", 67 | fields = fields, 68 | channel = data.channel, 69 | username = p.get_botname() 70 | } 71 | local response = slack.to_rich_message(t) 72 | if not response then 73 | return nil 74 | else 75 | return response 76 | end 77 | end 78 | 79 | local function test(data) 80 | local res = run(data) 81 | local params = { 82 | mock_data = data, 83 | run_data = res 84 | } 85 | local t = require('utils.test').new(params) 86 | t:add('is_valid_rich_text') 87 | t:run() 88 | return t:report() 89 | end 90 | 91 | local plugin = { 92 | run = run, 93 | id = id, 94 | version = version, 95 | regex = regex, 96 | test = test 97 | } 98 | 99 | return plugin 100 | -------------------------------------------------------------------------------- /var_nginx/content/js/lang-lua.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2008 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | 17 | /** 18 | * @fileoverview 19 | * Registers a language handler for Lua. 20 | * 21 | * 22 | * To use, include prettify.js and this file in your HTML page. 23 | * Then put your code in an HTML tag like 24 | *
(my Lua code)
25 | * 26 | * 27 | * I used http://www.lua.org/manual/5.1/manual.html#2.1 28 | * Because of the long-bracket concept used in strings and comments, Lua does 29 | * not have a regular lexical grammar, but luckily it fits within the space 30 | * of irregular grammars supported by javascript regular expressions. 31 | * 32 | * @author mikesamuel@gmail.com 33 | */ 34 | 35 | PR['registerLangHandler']( 36 | PR['createSimpleLexer']( 37 | [ 38 | // Whitespace 39 | [PR['PR_PLAIN'], /^[\t\n\r \xA0]+/, null, '\t\n\r \xA0'], 40 | // A double or single quoted, possibly multi-line, string. 41 | [PR['PR_STRING'], /^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])*(?:\'|$))/, null, '"\''] 42 | ], 43 | [ 44 | // A comment is either a line comment that starts with two dashes, or 45 | // two dashes preceding a long bracketed block. 46 | [PR['PR_COMMENT'], /^--(?:\[(=*)\[[\s\S]*?(?:\]\1\]|$)|[^\r\n]*)/], 47 | // A long bracketed block not preceded by -- is a string. 48 | [PR['PR_STRING'], /^\[(=*)\[[\s\S]*?(?:\]\1\]|$)/], 49 | [PR['PR_KEYWORD'], /^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/, null], 50 | // A number is a hex integer literal, a decimal real literal, or in 51 | // scientific notation. 52 | [PR['PR_LITERAL'], 53 | /^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i], 54 | // An identifier 55 | [PR['PR_PLAIN'], /^[a-z_]\w*/i], 56 | // A run of punctuation 57 | [PR['PR_PUNCTUATION'], /^[^\w\t\n\r \xA0][^\w\t\n\r \xA0\"\'\-\+=]*/] 58 | ]), 59 | ['lua']); 60 | -------------------------------------------------------------------------------- /var_nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | daemon off; 3 | worker_processes 5; 4 | include /var/nginx/local.conf.d/env.conf.d/*.conf; 5 | error_log /dev/stderr warn; 6 | #error_log /var/nginx/logs/nginx.log warn; 7 | 8 | pid /var/nginx/run/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | ssl_session_cache shared:SSL:10m; 18 | server_tokens off; 19 | more_set_headers 'Server: Lubot 0.0.1'; 20 | ssl_protocols TLSv1.2; 21 | ssl_prefer_server_ciphers on; 22 | ssl_session_timeout 5m; 23 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kE 24 | DH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA- 25 | AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA:AES256 26 | -SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 27 | include /opt/openresty/nginx/conf/mime.types; 28 | default_type application/octet-stream; 29 | 30 | log_format main '$remote_addr - $http_remote_user [$time_local] "$request" ' 31 | '$status $body_bytes_sent "$http_referer" ' 32 | '"$http_user_agent" "$http_x_forwarded_for"'; 33 | 34 | #access_log /var/nginx/logs/access.log main; 35 | access_log off; 36 | 37 | sendfile on; 38 | tcp_nopush on; 39 | tcp_nodelay on; 40 | 41 | #keepalive_timeout 65; 42 | 43 | 44 | gzip on; 45 | gzip_http_version 1.0; 46 | gzip_comp_level 2; 47 | gzip_proxied any; 48 | gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript application/json; 49 | 50 | server_names_hash_bucket_size 1024; 51 | variables_hash_bucket_size 1024; 52 | client_header_buffer_size 64k; 53 | large_client_header_buffers 4 64k; 54 | 55 | server { 56 | listen 22002; 57 | access_log off; 58 | 59 | location /_status/http { 60 | check_status; 61 | } 62 | location /_status/stub { 63 | stub_status on; 64 | } 65 | location /_status/tcp { 66 | tcp_check_status; 67 | } 68 | } 69 | include /var/nginx/conf.d/*.conf; 70 | } 71 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/brainz/ngx_shared_brain.lua: -------------------------------------------------------------------------------- 1 | local _brain = {} 2 | local _B = {} 3 | _brain._VERSION = "0.0.1" 4 | _brain.name = "ngx_shared" 5 | _brain.persistent = false 6 | 7 | local log = require 'utils.log' 8 | 9 | function _brain.new(...) 10 | local self = {} 11 | local args = ... or {} 12 | setmetatable(self, {__index = _B}) 13 | if not args.shared_dict then 14 | print("No shared dictionary specified. Using default.") 15 | self._shared_dict = ngx.shared.lubot_brain 16 | else 17 | self._shared_dict = ngx.shared[args.shared_dict] 18 | end 19 | local success, err, forcible = self._shared_dict:set('activated_brain', true) 20 | if not success then 21 | print("Could not set test data in brain: "..err) 22 | return nil 23 | else 24 | print("Successfully wrote to the brain") 25 | end 26 | return self 27 | end 28 | 29 | local function json_decode(str) 30 | local cjson = require 'cjson' 31 | local json_ok, json = pcall(cjson.decode, str) 32 | if not json_ok then 33 | return nil 34 | else 35 | return json 36 | end 37 | end 38 | 39 | local function json_encode(t) 40 | local cjson = require 'cjson' 41 | local json_ok, json = pcall(cjson.encode, t) 42 | if not json_ok then 43 | return nil 44 | else 45 | return json 46 | end 47 | end 48 | 49 | function _B:save() 50 | -- noop for now 51 | end 52 | 53 | function _B:keys() 54 | local keys = self._shared_dict:get_keys(0) 55 | return keys 56 | end 57 | 58 | function _B:export() 59 | local t = {} 60 | end 61 | 62 | function _B:populate() 63 | end 64 | 65 | function _B:get(k) 66 | local value = self._shared_dict:get(k) 67 | if not value then 68 | return nil 69 | else 70 | local dec = json_decode(value) 71 | if not dec then 72 | return nil 73 | else 74 | return dec 75 | end 76 | end 77 | end 78 | 79 | function _B:set(k, v) 80 | local enc = json_encode(v) 81 | if enc then 82 | local ok, err, force = self._shared_dict:set(k, enc) 83 | if not ok then 84 | log.err("Unable to set key in shared_dict") 85 | return nil 86 | else 87 | return 1 88 | end 89 | else 90 | log.err("Unable to encode value for shared_dict") 91 | return nil 92 | end 93 | end 94 | 95 | function _B:safe_set(k, v) 96 | local encoded_value 97 | local e = json_encode(v) 98 | if not e then 99 | log.err("Unable to encode data to json for key: ", k) 100 | return nil 101 | else 102 | encoded_value = e 103 | end 104 | local success, err, forcible = self._shared_dict:safe_add(k, encoded_value) 105 | if not success then 106 | log.err("Unable to insert value for key "..k.." into dict") 107 | return nil 108 | else 109 | return encoded_value 110 | end 111 | end 112 | 113 | function _B:delete(k) 114 | self._shared_dict:delete(k) 115 | end 116 | 117 | function _B:flush() 118 | self._shared_dict:flush_all() 119 | self._shared_dict:flush_expired() 120 | end 121 | 122 | return _brain 123 | -------------------------------------------------------------------------------- /lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Getopt::Std; 7 | 8 | my (@luas, @tests); 9 | 10 | my %opts; 11 | getopts('Lse', \%opts) or die "Usage: lua-releng [-L] [-s] [-e] [files]\n"; 12 | 13 | my $silent = $opts{s}; 14 | my $stop_on_error = $opts{e}; 15 | my $no_long_line_check = $opts{L}; 16 | 17 | if ($#ARGV != -1) { 18 | @luas = @ARGV; 19 | 20 | } else { 21 | @luas = map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }; 22 | if (-d 't') { 23 | @tests = map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }; 24 | } 25 | } 26 | 27 | for my $f (sort @luas) { 28 | process_file($f); 29 | } 30 | 31 | for my $t (@tests) { 32 | blank(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $t}); 33 | } 34 | # p: prints a string to STDOUT appending \n 35 | # w: prints a string to STDERR appending \n 36 | # Both respect the $silent value 37 | sub p { print "$_[0]\n" if (!$silent) } 38 | sub w { warn "$_[0]\n" if (!$silent) } 39 | 40 | # blank: runs a command and looks at the output. If the output is not 41 | # blank it is printed (and the program dies if stop_on_error is 1) 42 | sub blank { 43 | my ($command) = @_; 44 | if ($stop_on_error) { 45 | my $output = `$command`; 46 | if ($output ne '') { 47 | die $output; 48 | } 49 | } else { 50 | system($command); 51 | } 52 | } 53 | 54 | my $version; 55 | sub process_file { 56 | my $file = shift; 57 | # Check the sanity of each .lua file 58 | open my $in, $file or 59 | die "ERROR: Can't open $file for reading: $!\n"; 60 | my $found_ver; 61 | while (<$in>) { 62 | my ($ver, $skipping); 63 | if (/(?x) (?:_VERSION|version) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { 64 | my $orig_ver = $ver = $1; 65 | $found_ver = 1; 66 | $skipping = $2; 67 | $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; 68 | w("$file: $orig_ver ($ver)"); 69 | last; 70 | 71 | } elsif (/(?x) (?:_VERSION|version) \s* = \s* ([a-zA-Z_]\S*)/) { 72 | w("$file: $1"); 73 | $found_ver = 1; 74 | last; 75 | } 76 | 77 | if ($ver and $version and !$skipping) { 78 | if ($version ne $ver) { 79 | die "$file: $ver != $version\n"; 80 | } 81 | } elsif ($ver and !$version) { 82 | $version = $ver; 83 | } 84 | } 85 | if (!$found_ver) { 86 | w("WARNING: No \"_VERSION\" or \"version\" field found in `$file`."); 87 | } 88 | close $in; 89 | 90 | p("Checking use of Lua global variables in file $file..."); 91 | p("\top no.\tline\tinstruction\targs\t; code"); 92 | blank("luac -p -l $file | grep -E '[GS]ETGLOBAL' | grep -vE '\\<(require|type|tostring|error|ngx|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|rawget|rawset|rawlen)\\>'"); 93 | unless ($no_long_line_check) { 94 | p("Checking line length exceeding 80..."); 95 | blank("grep -H -n -E --color '.{81}' $file"); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /var_nginx/lua/utils/slack.lua: -------------------------------------------------------------------------------- 1 | local ngu = require 'utils.nginx' 2 | local pu = require 'utils.plugins' 3 | local log = require 'utils.log' 4 | local m = {} 5 | m._VERSION = "0.0.1" 6 | 7 | local function fields_to_fallback(fields) 8 | local t = {} 9 | for k, v in pairs(fields) do 10 | local txt = string.lower(v.title:gsub("%s+","_")) 11 | table.insert(t,txt..": "..v.value) 12 | end 13 | return table.concat(t, " | ") 14 | end 15 | 16 | m.users_dict = ngx.shared.slack_users 17 | m.groups_dict = ngx.shared.slack_groups 18 | m.channels_dict = ngx.shared.slack_channels 19 | m.ims_dict = ngx.shared.slack_ims 20 | m.bots_dict = ngx.shared.slack_bots 21 | 22 | function m.make_slack_attachment(text, fields, fallback) 23 | local t = {} 24 | t.text = text 25 | t.fields = fields 26 | t.mrkdwn_in = {"pretext", "text", "title", "fields", "fallback"} 27 | if not fallback then 28 | t.fallback = fields_to_fallback(fields) 29 | else 30 | t.fallback = fallback 31 | end 32 | return t 33 | end 34 | 35 | function m.to_rts_message(text, channel) 36 | local t = { 37 | ["type"] = "message", 38 | channel = channel, 39 | id = pu.generate_id(), 40 | text = text 41 | } 42 | return t 43 | end 44 | 45 | function m.say(...) 46 | return {text = table.concat({...}," ")} 47 | end 48 | 49 | function m.to_rich_message(...) 50 | local required_fields = {"text", "fields", "channel", "username"} 51 | local args = ... 52 | for _,k in pairs(required_fields) do 53 | if not args[k] then return nil end 54 | end 55 | local fallback = args.fallback or nil 56 | local attachments = m.make_slack_attachment(args.text, args.fields, fallback) 57 | local t = { 58 | channel = args.channel, 59 | username = args.username, 60 | attachments = {attachments} 61 | } 62 | local shared_dict = ngx.shared.ng_shared_dict 63 | local slack_webhook_url = shared_dict:get('slack_webhook_url') 64 | if not slack_webhook_url then 65 | ngu.logwarn("No slack webhook url, converting message to rts fallback") 66 | return m.to_rts_message(attachments.fallback, args.channel) 67 | else 68 | return t 69 | end 70 | end 71 | 72 | function m.post_chat_message(...) 73 | local args = ... 74 | local hc = require 'httpclient'.new('httpclient.ngx_driver') 75 | local shared_dict = ngx.shared.ng_shared_dict 76 | local webhook_url = shared_dict:get('slack_webhook_url') 77 | if not webhook_url then return nil, "Slack webhook url missing" end 78 | local res = hc:post(webhook_url, args, {headers = {accept = "application/json"}, content_type = "application/json"}) 79 | return res 80 | end 81 | 82 | function m.lookup_by_id(c) 83 | local users = m.users_dict 84 | local groups = m.groups_dict 85 | local channels = m.channels_dict 86 | local ims = m.ims_dict 87 | local bots = m.bots_dict 88 | 89 | local result 90 | local match, err = ngx.re.match(c, "^([A-Z]).*", "jo") 91 | if not match then 92 | log.err("Unable to match id to type") 93 | result = nil 94 | else 95 | if match[1] == 'D' then result = safe_json_decode(ims:get(c)) end 96 | if match[1] == 'C' then result = safe_json_decode(channels:get(c)) end 97 | if match[1] == 'G' then result = safe_json_decode(groups:get(c)) end 98 | if match[1] == 'B' then result = safe_json_decode(bots:get(c)) end 99 | if match[1] == 'U' then result = safe_json_decode(users:get(c)) end 100 | end 101 | return result 102 | end 103 | 104 | function m.id_to_user(u) 105 | 106 | end 107 | 108 | function m.id_to_channel(c) 109 | end 110 | 111 | function m.user_is_admin(u) 112 | end 113 | 114 | function m.user_is_restricted(u) 115 | end 116 | 117 | function m.user_id_ultra_restricted(u) 118 | end 119 | return m 120 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | 3 | MAINTAINER John E Vincent 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | ENV LANGUAGE en_US.UTF-8 7 | ENV LANG en_US.UTF-8 8 | ENV LC_ALL en_US.UTF-8 9 | 10 | 11 | 12 | RUN apt-get update && apt-get install -y \ 13 | supervisor \ 14 | git \ 15 | curl \ 16 | build-essential \ 17 | ruby1.9.1-full \ 18 | libssl-dev \ 19 | libreadline-dev \ 20 | libxslt1-dev \ 21 | libxml2-dev \ 22 | libcurl4-openssl-dev \ 23 | zlib1g-dev \ 24 | libexpat1-dev \ 25 | libicu-dev \ 26 | unzip \ 27 | libpcre3-dev && \ 28 | apt-get clean && \ 29 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 30 | 31 | RUN curl -L https://github.com/coreos/etcd/releases/download/v0.4.6/etcd-v0.4.6-linux-amd64.tar.gz -o etcd-v0.4.6-linux-amd64.tar.gz && \ 32 | tar -zxvf etcd-v0.4.6-linux-amd64.tar.gz && \ 33 | cp etcd-v0.4.6-linux-amd64/etcd / 34 | 35 | RUN mkdir /build && \ 36 | curl -LO http://openresty.org/download/ngx_openresty-1.7.7.1.tar.gz && \ 37 | cd /build/ && \ 38 | tar ozxf /ngx_openresty-1.7.7.1.tar.gz 39 | 40 | # Get some additional patches mostly proxy related (in its own group to optimize) 41 | RUN cd /build && \ 42 | git clone https://github.com/yaoweibin/nginx_upstream_check_module && \ 43 | git clone https://github.com/gnosek/nginx-upstream-fair && \ 44 | git clone https://github.com/lusis/nginx-sticky-module && \ 45 | git clone https://github.com/yaoweibin/nginx_tcp_proxy_module 46 | 47 | # Patch and build 48 | RUN cd /build/ngx_openresty-1.7.7.1/bundle/nginx-1.7.7 && \ 49 | patch -p1 < /build/nginx_upstream_check_module/check_1.7.5+.patch && \ 50 | patch -p1 < /build/nginx_tcp_proxy_module/tcp.patch 51 | 52 | RUN mkdir /tmp/client_body_tmp && mkdir /tmp/proxy_temp && cd /build/ngx_openresty-1.7.7.1 && \ 53 | ./configure --prefix=/opt/openresty \ 54 | --with-luajit \ 55 | --with-luajit-xcflags=-DLUAJIT_ENABLE_LUA52COMPAT \ 56 | --http-client-body-temp-path=/tmp/client_body_temp \ 57 | --http-proxy-temp-path=/tmp/proxy_temp \ 58 | --http-log-path=/var/nginx/logs/access.log \ 59 | --error-log-path=/var/nginx/logs/error.log \ 60 | --pid-path=/var/nginx/run/nginx.pid \ 61 | --lock-path=/var/nginx/run/nginx.lock \ 62 | --with-http_stub_status_module \ 63 | --with-http_ssl_module \ 64 | --with-http_secure_link_module \ 65 | --with-http_gzip_static_module \ 66 | --with-http_sub_module \ 67 | --with-http_realip_module \ 68 | --without-http_scgi_module \ 69 | --with-md5-asm \ 70 | --with-sha1-asm \ 71 | --with-file-aio \ 72 | --with-pcre \ 73 | --with-pcre-jit \ 74 | --add-module=/build/nginx_upstream_check_module \ 75 | --add-module=/build/nginx-upstream-fair \ 76 | --add-module=/build/nginx-sticky-module \ 77 | --add-module=/build/nginx_tcp_proxy_module 78 | 79 | RUN cd /build/ngx_openresty-1.7.7.1 && \ 80 | make && \ 81 | make install && \ 82 | cd / && \ 83 | rm -rf /build && \ 84 | ln -sf /opt/openresty/luajit/bin/luajit-2.1.0-alpha /opt/openresty/luajit/bin/lua 85 | 86 | # Hoedown needed for the resty-template markdown rendering 87 | RUN cd /tmp && \ 88 | git clone https://github.com/hoedown/hoedown.git && \ 89 | cd hoedown && \ 90 | make all install && \ 91 | cd / && \ 92 | rm -rf /tmp/hoedown && \ 93 | ldconfig 94 | 95 | # Now the luarocks stuff 96 | RUN curl -s http://luarocks.org/releases/luarocks-2.2.0.tar.gz | tar xvz -C /tmp/ \ 97 | && cd /tmp/luarocks-* \ 98 | && ./configure --with-lua=/opt/openresty/luajit \ 99 | --with-lua-include=/opt/openresty/luajit/include/luajit-2.1 \ 100 | --with-lua-lib=/opt/openresty/lualib \ 101 | && make && make install \ 102 | && ln -sf /opt/openresty/luajit/bin/lua /usr/local/bin/lua \ 103 | && rm -rf /tmp/luarocks-* 104 | 105 | RUN /usr/local/bin/luarocks install luasec && \ 106 | /usr/local/bin/luarocks install lua-resty-template && \ 107 | /usr/local/bin/luarocks install httpclient && \ 108 | /usr/local/bin/luarocks install lua-resty-http && \ 109 | /usr/local/bin/luarocks install inspect && \ 110 | /usr/local/bin/luarocks install lua-resty-hoedown && \ 111 | /usr/local/bin/luarocks install xml 112 | 113 | RUN useradd -r -d /var/nginx nginx && chown -R nginx:nginx /var/nginx/ /tmp/client_body_tmp /tmp/proxy_temp 114 | 115 | EXPOSE 3232 116 | 117 | ADD ./supervisord.conf /supervisord.conf 118 | CMD /usr/bin/supervisord -c /supervisord.conf 119 | -------------------------------------------------------------------------------- /var_nginx/lua/utils/test.lua: -------------------------------------------------------------------------------- 1 | local m = {} 2 | local _M = {} 3 | 4 | m._VERSION = "0.0.1" 5 | 6 | function m.new(...) 7 | local self = {} 8 | local args = ... or {} 9 | self.mock_data = args.mock_data or nil 10 | self.expects = args.expects or nil 11 | self.run_data = args.run_data or nil 12 | self.failures = {} 13 | self.tests = {} 14 | setmetatable(self, {__index = _M}) 15 | return self 16 | end 17 | 18 | function _M:set_expects(str) 19 | self.expects = str 20 | end 21 | 22 | function _M:set_mock_data(t) 23 | self.mock_data = t 24 | end 25 | 26 | function _M:set_rundata(t) 27 | self.run_data = t 28 | end 29 | 30 | -- Verify the slack attachment response is correctly formatted 31 | function _M:is_valid_rich_text(...) 32 | if not self.run_data then 33 | self:fail("Missing run data") 34 | else 35 | if not self.run_data.attachments then self:fail("Missing attachments") end 36 | if not self.run_data.attachments[1].fallback then self:fail("Missing fallback text") end 37 | if self.run_data.channel ~= self.mock_data.channel then 38 | self:fail("Channel mismatch in rich response") 39 | end 40 | end 41 | end 42 | 43 | -- Verify the plugin response contains a text element 44 | function _M:responds_text() 45 | if not self.run_data then 46 | self:fail("Missing run data") 47 | else 48 | if not self.run_data.text then 49 | self:fail("No text in response") 50 | end 51 | end 52 | end 53 | 54 | -- Verify the plugin parses a message correctly 55 | function _M:parses_text(regex, named_captures) 56 | local captures 57 | if not self.run_data then 58 | self:fail("Missing run data") 59 | else 60 | if type(named_captures) == 'string' then 61 | captures = {named_captures} 62 | elseif type(named_captures) == 'table' then 63 | captures = named_captures 64 | else 65 | self:fail("Named captures was not provided as a table or string") 66 | end 67 | local m, err = ngx.re.match(self.mock_data.text, regex, 'jo') 68 | if not m then 69 | self:fail("Got no matches for "..regex) 70 | else 71 | for _, c in ipairs(captures) do 72 | if not m[c] then 73 | self:fail("Did not get named capture "..c) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | 80 | -- Verify a specific value was captured 81 | function _M:captures_value(str, regex, capture) 82 | if not self.run_data then 83 | self:fail("Missing run data") 84 | else 85 | local m, err = ngx.re.match(self.mock_data.text, regex, 'jo') 86 | if not m then 87 | self:fail("Did not get any matches") 88 | else 89 | if not m[capture] then 90 | self:fail("Capture element "..capture.." was not found") 91 | else 92 | if m[capture] ~= str then 93 | self:fail("Expected "..str.." but got "..m[capture]) 94 | end 95 | end 96 | end 97 | end 98 | end 99 | 100 | -- Verify the plugin reponse contains specific text 101 | function _M:response_contains(...) 102 | local expects = self.expects or ... 103 | if not self.run_data then 104 | self:fail("Missing run data") 105 | elseif not expects then 106 | self:fail("Missing expectation") 107 | else 108 | local m, err = ngx.re.match(self.run_data.text, expects, 'jo') 109 | if not m then 110 | self:fail("Expected "..expects.." but got no matches") 111 | end 112 | end 113 | end 114 | 115 | function _M:reset() 116 | self.failures = {} 117 | self.tests = {} 118 | end 119 | 120 | function _M:fail(msg) 121 | table.insert(self.failures, msg) 122 | end 123 | 124 | function _M:add(func, ...) 125 | if ... then 126 | t = { 127 | name = func, 128 | args = {...} 129 | } 130 | table.insert(self.tests, t) 131 | else 132 | table.insert(self.tests, func) 133 | end 134 | end 135 | 136 | function _M:run(...) 137 | local args = ... 138 | if not self.tests then 139 | self:pass("No tests to run") 140 | else 141 | for _, t in ipairs(self.tests) do 142 | local test_ok, test_res, test_name 143 | if type(t) == 'string' then 144 | test_name = t 145 | test_ok, test_res = pcall(_M[t], self) 146 | else 147 | test_name = t.name 148 | test_ok, test_res = pcall(_M[t.name], self, unpack(t.args)) 149 | end 150 | if not test_ok then 151 | self:fail(test_name.." failed to run: "..test_res) 152 | end 153 | end 154 | end 155 | end 156 | 157 | function _M:report() 158 | local report = {} 159 | local failed = #self.failures 160 | if failed > 0 then 161 | report.tests = {} 162 | report.passed = false 163 | report.failures = self.failures 164 | for _, t in ipairs(self.tests) do 165 | if type(t) == 'string' then table.insert(report.tests, t) end 166 | if type(t) == 'table' then table.insert(report.tests, t.name) end 167 | end 168 | report.msg = "Failed "..failed.."/"..#report.tests 169 | else 170 | report.passed = true 171 | report.tests = self.tests 172 | report.msg = "All tests passed" 173 | report.response = self.run_data.text 174 | end 175 | return report 176 | end 177 | 178 | return m 179 | -------------------------------------------------------------------------------- /var_nginx/lua/api.lua: -------------------------------------------------------------------------------- 1 | local _VERSION = "0.0.1" 2 | local router = require 'router' 3 | local ngu = require 'utils.nginx' 4 | local plugutils = require 'utils.plugins' 5 | local log = require 'utils.log' 6 | 7 | local r = router.new() 8 | 9 | r:get('/_private/api/brain/inspect', function(params) 10 | local inspect = require('inspect') 11 | ngx.header.content_type = "text/plain" 12 | local response = inspect(robot.brain) 13 | ngx.say(response) 14 | ngx.exit(ngx.HTTP_OK) 15 | end) 16 | 17 | r:get('/_private/api/brain/all', function(params) 18 | local data = robot.brain:keys() 19 | if not data then 20 | log.alert("No data in brain") 21 | plugutils.respond_as_json({count = 0, msg = "No data found in brain"}) 22 | else 23 | local t = {} 24 | for _, k in ipairs(data) do 25 | local v = robot.brain:get(k) 26 | if not v then 27 | --log.alert("Missing data for key ", k) 28 | else 29 | t[k] = v 30 | end 31 | end 32 | plugutils.respond_as_json(t) 33 | end 34 | end) 35 | 36 | r:get('/_private/api/brain/:key', function(params) 37 | local data = robot.brain:get(params.key) 38 | if not data then return {} end 39 | local t = {} 40 | t[params.key] = data 41 | plugutils.respond_as_json(t) 42 | end) 43 | 44 | r:post('/_private/api/plugins/find_match', function(params) 45 | local data = params.data 46 | local plugin = plugutils.find_plugin_for(data) 47 | if not plugin then 48 | plugutils.respond_as_json({count = 0}) 49 | else 50 | local results = {} 51 | for _, k in ipairs(plugin) do table.insert(results, k.id) end 52 | plugutils.respond_as_json({count = #plugin, results = results}) 53 | end 54 | end) 55 | 56 | r:get('/_private/api/plugins/last_error/:plugin_name', function(params) 57 | local plugin = plugutils.get_last_error(params.plugin_name) 58 | if not plugin then 59 | plugutils.respond_as_json({ 60 | msg = "no logs available for plugin "..params.plugin_name 61 | }) 62 | else 63 | plugutils.respond_as_json(plugin) 64 | end 65 | end) 66 | 67 | r:get('/_private/api/plugins/logs/:plugin_name', function(params) 68 | local plugin = plugutils.get_logs(params.plugin_name) 69 | if not plugin then 70 | plugutils.respond_as_json({ 71 | msg = "no logs available for plugin "..params.plugin_name 72 | }) 73 | else 74 | plugutils.respond_as_json(plugin) 75 | end 76 | end) 77 | 78 | r:get('/_private/api/plugins/help/:plugin_name', function(params) 79 | local plugin = plugutils.plugin_help(params.plugin_name) 80 | if not plugin then 81 | plugutils.respond_as_json({ 82 | msg = "no help available for plugin "..params.plugin_name 83 | }) 84 | else 85 | plugutils.respond_as_json({msg = plugin}) 86 | end 87 | end) 88 | 89 | r:post('/_private/api/plugins/run/:plugin_name', function(params) 90 | local data 91 | if params.data then 92 | data = safe_json_decode(params.data) 93 | end 94 | local plugin = plugutils.safe_plugin_run(params.plugin_name, data) 95 | plugutils.respond_as_json(plugin) 96 | end) 97 | 98 | r:post('/_private/api/plugins/test/:plugin_name', function(params) 99 | local data 100 | if params.data then 101 | data = safe_json_decode(params.data) 102 | end 103 | local plugin = plugutils.plugin_test(params.plugin_name, data) 104 | plugutils.respond_as_json(plugin) 105 | end) 106 | 107 | r:get('/_private/api/plugins/stats/all', function(params) 108 | local list = plugutils.get_active() 109 | local resp = {} 110 | if not list then plugutils.respond_as_json({}) end 111 | for _, p in ipairs(list) do 112 | local stats = plugutils.plugin_stats(p) 113 | if not stats then 114 | resp[p] = {} 115 | else 116 | resp[p] = {errors = stats.errors, executions = stats.executions} 117 | end 118 | end 119 | plugutils.respond_as_json(resp) 120 | end) 121 | 122 | r:get('/_private/api/plugins/stats/:plugin_name', function(params) 123 | local plugin = plugutils.plugin_stats(params.plugin_name) 124 | plugutils.respond_as_json(plugin) 125 | end) 126 | 127 | r:get('/_private/api/plugins/details/all', function(params) 128 | local list = plugutils.get_active() 129 | local resp = {} 130 | if not list then plugutils.respond_as_json({}) end 131 | for _, p in ipairs(list) do 132 | local details = plugutils.plugin_details(p) 133 | if not details or details.err then 134 | resp[p] = {} 135 | else 136 | resp[p] = details 137 | end 138 | end 139 | plugutils.respond_as_json(resp) 140 | end) 141 | 142 | r:get('/_private/api/plugins/details/:plugin_name', function(params) 143 | local plugin = plugutils.plugin_details(params.plugin_name) 144 | plugutils.respond_as_json(plugin) 145 | end) 146 | 147 | r:get('/_private/api/plugins/list', function(params) 148 | local active_plugins = plugutils.get_active() 149 | plugutils.respond_as_json(active_plugins) 150 | end) 151 | 152 | r:get('/_private/api/lubot/botname', function(params) 153 | local botname = robot.brain:get('botname') 154 | plugutils.respond_as_json({botname = botname}) 155 | end) 156 | 157 | local method = string.lower(ngx.req.get_method()) 158 | local path = ngx.var.uri 159 | local query_params = {} 160 | ngx.req.read_body() 161 | 162 | if method == "post" or method == "put" then 163 | local q, err = ngx.req.get_body_data() 164 | if not q then 165 | log.warn("POST/PUT with no body data") 166 | else 167 | query_params.data = q 168 | end 169 | else 170 | query_params = ngx.var.args 171 | end 172 | r:execute(method, path, query_params) 173 | -------------------------------------------------------------------------------- /var_nginx/lua/router.lua: -------------------------------------------------------------------------------- 1 | local router = { 2 | _VERSION = 'router.lua v0.6', 3 | _DESCRIPTION = 'A simple router for Lua', 4 | _LICENSE = [[ 5 | MIT LICENSE 6 | 7 | * table_copyright (c) 2013 Enrique García Cota 8 | * table_copyright (c) 2013 Raimon Grau 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | table_copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, table_copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above table_copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR table_copyRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local function split(str, delimiter) 32 | local result = {} 33 | delimiter = delimiter or " " 34 | for chunk in str:gmatch("[^".. delimiter .. "]+") do 35 | result[#result + 1] = chunk 36 | end 37 | return result 38 | end 39 | 40 | local function array_get_head_and_tail(t) 41 | local tail = {} 42 | for i=2, #t do tail[i-1] = t[i] end 43 | return t[1], tail 44 | end 45 | 46 | local function table_merge(dest, src) 47 | if not src then return end 48 | for k,v in pairs(src) do 49 | dest[k] = tostring(v) 50 | end 51 | end 52 | 53 | local function table_copy(t) 54 | local result = {} 55 | for k,v in pairs(t) do result[k] = v end 56 | return result 57 | end 58 | 59 | local function resolve_rec(remaining_path, node, params) 60 | if not node then return nil end 61 | 62 | -- node is a _LEAF and no remaining tokens; found end 63 | if #remaining_path == 0 then return node[router._LEAF], params end 64 | 65 | local current_token, child_path = array_get_head_and_tail(remaining_path) 66 | 67 | -- always resolve static strings first 68 | for key, child in pairs(node) do 69 | if key == current_token then 70 | local f, bindings = resolve_rec(child_path, child, params) 71 | if f then return f, bindings end 72 | end 73 | end 74 | 75 | -- then resolve parameters 76 | for key, child in pairs(node) do 77 | if type(key) == "table" and key.param then 78 | local child_params = table_copy(params) 79 | child_params[key.param] = current_token 80 | local f, bindings = resolve_rec(child_path, child, child_params) 81 | if f then return f, bindings end 82 | end 83 | end 84 | 85 | return false 86 | end 87 | 88 | local function find_key_for(token, node) 89 | local param_name = token:match("^:(.+)$") 90 | -- if token is not a param( it does not begin with :) then return the token 91 | if not param_name then return token end 92 | 93 | -- otherwise, it's a param, like :id. If it exists as a child of the node, we return it 94 | for key,_ in pairs(node) do 95 | if type(key) == 'table' and key.param == param_name then return key end 96 | end 97 | 98 | -- otherwise, it's a new key to be inserted 99 | return {param = param_name} 100 | end 101 | 102 | 103 | local function match_one_path(self, method, path, f) 104 | self._tree[method] = self._tree[method] or {} 105 | local node = self._tree[method] 106 | for _,token in ipairs(split(path, "/")) do 107 | local key = find_key_for(token, node) 108 | node[key] = node[key] or {} 109 | node = node[key] 110 | end 111 | node[router._LEAF] = f 112 | end 113 | 114 | ------------------------------ INSTANCE METHODS ------------------------------------ 115 | 116 | local Router = {} 117 | 118 | function Router:resolve(method, path) 119 | return resolve_rec(split(path, "/"), self._tree[method] , {}) 120 | end 121 | 122 | function Router:execute(method, path, query_params) 123 | local f,params = self:resolve(method, path) 124 | if not f then return nil, ('Could not resolve %s %s'):format(method, path) end 125 | 126 | table_merge(params, query_params) 127 | 128 | return true, f(params) 129 | end 130 | 131 | function Router:match(method, path, f) 132 | if type(method) == 'table' then 133 | local t = method 134 | for method, routes in pairs(t) do 135 | for path, f in pairs(routes) do 136 | match_one_path(self, method, path, f) 137 | end 138 | end 139 | else 140 | match_one_path(self, method, path, f) 141 | end 142 | end 143 | 144 | for _,http_method in ipairs({'get', 'post', 'put', 'delete', 'trace', 'connect', 'options', 'head'}) do 145 | Router[http_method] = function(self, path, f) -- Router.get = function(self, path, f) 146 | return self:match(http_method, path, f) -- return self:match('get', path, f) 147 | end -- end 148 | end 149 | 150 | local router_mt = { __index = Router } 151 | 152 | ------------------------------ PUBLIC INTERFACE ------------------------------------ 153 | router._LEAF = {} 154 | 155 | router.new = function() 156 | return setmetatable({ _tree = {} }, router_mt) 157 | end 158 | 159 | return router 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lubot 2 | Proof of concept Slack chatbot hosted entirely inside nginx 3 | 4 | ## Motivation 5 | The main motivation for this was an extension of work I did for my [SysAdvent 2014 post](http://sysadvent.blogspot.com/2014/12/day-22-largely-unappreciated.html) on Nginx, Lua and OpenResty. 6 | For that post I created a [docker container](https://github.com/lusis/sysadvent-2014) to run the examples/tutorials. Part of those tutorials were a couple of Slack RTM clients - one of which was nginx/openresty operating as a [websocket client](https://github.com/lusis/sysadvent-2014/blob/master/var_nginx/lua/slack.lua). 7 | 8 | I got it in my head that I could write a [hubot clone](https://github.com/github/hubot) using that. 9 | 10 | ## How it works 11 | This is a prefab container like the previous example. A `Makefile` is provided to set everything up. You'll need to create a [Slack Bot User](https://api.slack.com/bot-users) and optionally you'll need an incoming webhook url (if you want to use rich formatting) as the [RTM Api doesn't support those yet](https://twitter.com/slackapi/status/542912319957643265) 12 | 13 | Build and start the container like so: 14 | `DOCKER_ENV='--env SLACK_API_TOKEN=xoxb-XXXXXXX --env SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXXXXX"' make all` 15 | 16 | (future invocations after the initial build can use `make run` in place of `make all`) 17 | 18 | You can watch the logs via `tail -f var_nginx/logs/error.log`. 19 | 20 | # How it works 21 | During startup, one of the nginx worker threads will connect as a websocket client to the RTM api. During the authentication response from Slack, the user/group/etc data is loaded into a shared dict. This is largely unused right now. 22 | 23 | The bot will be auto-joined to the `#general` channel. I'd suggest either opening a private message session with it or a dedicated private channel. 24 | 25 | ## Plugins 26 | Hubot has plugins. Lubot has plugins but they're "different". The way lubot plugins work are: 27 | 28 | - A message prefixed with the botname (default `lubot`) is noted. 29 | - The first word after the botname is considered the command (this will change) 30 | - The command is parsed and an http request is made to `http://127.0.0.1:3131/lubot_plugin?plugin=` (this is a `content_by_lua_file` script - `var_nginx/lua/lubot_plugin.lua`) 31 | - The plugin is executed in a fairly "safe" manner and the response is returned to be sent to slack via the existing websocket connection 32 | - If the result has an attachment element, it attempts to send that over the incoming webhook. If you've not provided a webhook url the `fallback` text required by the slack api is used instead and sent over websockets. If you do not provide a fallback yourself, the fields of your attachment will be converted to build a fallback message. 33 | 34 | ### Plugin location 35 | The plugins are located in the `var_nginx/lubot_plugins/` directory. There are three sub-directories: 36 | - `core`: core plugins 37 | - `contrib`: third-party plugins 38 | - `user`: local plugins 39 | 40 | The lua search path for lubot will look in the following order: user -> core -> contrib. This feels like the sanest mechanism for overrides. 41 | 42 | They currently have the following restrictions: 43 | - Must be named `lubot_.lua` and must return a table matching an RTM message object 44 | - If returning an attachment, you must follow the slack formatting rules for attachments returned as a table of attachments 45 | 46 | For examples see the three existing plugins. The `status` plugin sends an attachment. 47 | 48 | One nice thing about these plugins is that you can test them with curl: 49 | 50 | ``` 51 | jvbuntu :: ~ » curl -XPOST -d'{"channel":"foo","user":"test","text":"lubot image me foobar","expects":"foobar"}' http://localhost:3232/api/plugins/test/image 52 | 53 | {"expected":"foobar","results":"http:\/\/khromov.files.wordpress.com\/2011\/02\/foobar_cover.png","passed":true,"got":"foobar"} 54 | ``` 55 | 56 | Some plugins don't have any assertions you can provide. Take the ping plugin: 57 | 58 | ``` 59 | jvbuntu :: ~ » curl -XPOST -d'{"channel":"foo","user":"test"}' http://localhost:3232/api/plugins/test/ping 60 | {"expected":"^pong .*$","passed":true,"got":"pong (1420583382)"} 61 | ``` 62 | 63 | ## API 64 | You may have noticed in the plugin testing section, the call to `/api/plugins`. Pretty much everything inside lubot is an api call to itself. This provides the benefit of being able to use it with multiple tools. Lubot listens on two ports - `3232` and `3131`. Public communications are handled over `3232`. However internally, all api calls go to `3131`. You should never expose `3131` to the public. Instead you should `proxy_pass` requests to the private port. The api called for testing does just that (`var_nginx/conf.d/lubot.conf`): 65 | 66 | ``` 67 | location /api { 68 | lua_code_cache off; 69 | proxy_pass_request_headers on; 70 | proxy_redirect off; 71 | proxy_buffering off; 72 | proxy_cache off; 73 | rewrite ^/api/(.*) /_private/api/$1 break; 74 | proxy_pass http://private_api; 75 | } 76 | ``` 77 | 78 | The corresponding private api config (in `var_nginx/conf.d/private.conf`): 79 | 80 | ``` 81 | location /_private/api { 82 | lua_code_cache off; 83 | content_by_lua_file '/var/nginx/lua/api.lua'; 84 | } 85 | ``` 86 | 87 | # Customization 88 | More customization information is available from the web ui inside lubot. These are just markdown files served by lubot and they are available in `var_nginx/content/html/docs` 89 | 90 | The general idea for customization is a combination of: 91 | 92 | - nginx includes at critical points 93 | - environment variables 94 | - predefined site-specific directories on the lua load path before core load paths 95 | 96 | In general you should not need to touch ANY shipped files unless you are developing core functionality. 97 | 98 | # Production ready 99 | Actually...yeah...kinda. I'll probably move the websocket connection back out of the `init_by_lua` and into the worker the way it works in the sysadvent container. 100 | Also note that having a worker handling the websocket stuff means that worker cannot service nginx requests because it's being blocked. 101 | 102 | # TODO 103 | - Create a management API and page to be served 104 | - Possibly migrate the websocket logic back into `init_worker_by_lua` 105 | - Maybe consider porting this to hipchat or something 106 | 107 | -------------------------------------------------------------------------------- /var_nginx/lubot_plugins/core/lubot_plugins.lua: -------------------------------------------------------------------------------- 1 | _plugin = {} 2 | 3 | _plugin.id = "plugins" 4 | _plugin.version = "0.0.1" 5 | _plugin.regex = [=[(plugins|plugin) (?(enable|disable|list|stats|logs|last_error))?\s?(?\w+)?$]=] 6 | 7 | local p = require 'utils.plugins' 8 | local chat = require 'utils.slack' 9 | local ngu = require 'utils.nginx' 10 | local log = require 'utils.log' 11 | 12 | local function process_action(...) 13 | local args = ... 14 | if args.plugin_action == "list" then 15 | local active = p.get_active() 16 | return "active plugins: "..table.concat(active, " | ") 17 | end 18 | if args.plugin_action == "stats" then 19 | if not args.plugin_name then 20 | return "Missing plugin name" 21 | else 22 | if args.plugin_name == "all" then 23 | local errors = 0 24 | local executions = 0 25 | local active = p.get_active() 26 | for _, v in ipairs(active) do 27 | local stats = p.plugin_stats(v) 28 | if stats then 29 | errors = errors + stats.errors 30 | executions = executions + stats.executions 31 | end 32 | end 33 | return "stats for all plugins: errors = "..errors.." | executions = "..executions 34 | else 35 | local stats = p.plugin_stats(args.plugin_name) 36 | if not stats then 37 | return "no stats for "..args.plugin_name 38 | else 39 | local s = {} 40 | for k,v in pairs(stats) do if k ~= "plugin" then table.insert(s, k.."="..v) end end 41 | return "stats for plugin '"..stats.plugin.."': "..table.concat(s, " | ") 42 | end 43 | end 44 | end 45 | end 46 | if args.plugin_action == "disable" then 47 | if not args.plugin_name then 48 | return "Missing plugin name" 49 | end 50 | if args.plugin_name == _plugin.id then 51 | return "You can't disable this plugin this way" 52 | end 53 | p.disable_plugin(args.plugin_name) 54 | if args.data and args.data.user and args.data.channel then 55 | local who = args.data.user 56 | local where = args.data.channel 57 | local user = chat.lookup_by_id(args.data.user) 58 | if type(user) == 'table' then 59 | who = user.name 60 | end 61 | local channel = chat.lookup_by_id(args.data.channel) 62 | if type(channel) == 'table' then 63 | if channel.is_group then where = "private group "..channel.name end 64 | if channel.is_channel then where = "public channel "..channel.name end 65 | if channel.is_im then where = "a private chat" end 66 | end 67 | p.plog(_plugin.id, "Plugin "..args.plugin_name.." disabled by "..who.." in "..where) 68 | end 69 | return "Disabled plugin "..args.plugin_name 70 | end 71 | if args.plugin_action == "enable" then 72 | if not args.plugin_name then 73 | return "Missing plugin name" 74 | end 75 | local active = p.get_active() 76 | 77 | if args.plugin_name == _plugin.id or active[args.plugin_name] then 78 | return "plugin already active" 79 | end 80 | local enable, err = p.enable_plugin(args.plugin_name) 81 | if not enable then 82 | if err then return err end 83 | end 84 | if args.data and args.data.user and args.data.channel then 85 | local who = args.data.user 86 | local where = args.data.channel 87 | local user = chat.lookup_by_id(args.data.user) 88 | if type(user) == 'table' then 89 | who = user.name 90 | end 91 | local channel = chat.lookup_by_id(args.data.channel) 92 | if type(channel) == 'table' then 93 | if channel.is_group then where = "private group "..channel.name end 94 | if channel.is_channel then where = "public channel "..channel.name end 95 | if channel.is_im then where = "a private chat" end 96 | end 97 | p.plog(_plugin.id, "Plugin "..args.plugin_name.." enabled by "..who.." in "..where) 98 | end 99 | return "Enabled plugin "..args.plugin_name 100 | end 101 | if args.plugin_action == "logs" then 102 | if not args.plugin_name then 103 | return "Missing plugin name" 104 | end 105 | local logs = p.get_logs(args.plugin_name) 106 | if #logs == 0 then return "No logs" end 107 | local lines = {} 108 | for _, k in ipairs(logs) do 109 | table.insert(lines, "["..ngx.http_time(k.timestamp).."] "..k.msg) 110 | end 111 | return table.concat(lines, "\n") 112 | end 113 | if args.plugin_action == "last_error" then 114 | if not args.plugin_name then 115 | return "Missing plugin name" 116 | end 117 | local logs = p.get_last_error(args.plugin_name) 118 | if not logs then return "No errors" end 119 | return "["..logs.tstamp.."] "..logs.msg 120 | end 121 | end 122 | 123 | function _plugin.run(data) 124 | log.inspect(data) 125 | if not data.text then 126 | return nil, "Missing message text" 127 | end 128 | local m, err = ngx.re.match(data.text, _plugin.regex, 'jo') 129 | if not m then 130 | return nil, "Unable to match '"..data.text.."' to '".._plugin.regex.."'" 131 | else 132 | if not m.plugin_action then 133 | return nil, "Unable to find an action to take" 134 | end 135 | local params = { 136 | plugin_name = m.plugin_name, 137 | plugin_action = m.plugin_action, 138 | data = data 139 | } 140 | local resp = process_action(params) 141 | return chat.say(resp) 142 | end 143 | end 144 | 145 | function _plugin.help() 146 | local h = [[ 147 | [plugin|plugins] enable : enables the specified plugin 148 | [plugin|plugins] disable : disabled the specified plugin 149 | [plugin|plugins] list: lists all active plugins 150 | [plugin|plugins] stats : returns cumulative stats for all plugins or just the specified plugin 151 | [plugin|plugins] last_error : returns last error generated by the specified plugin 152 | [plugin|plugins] logs : returns any logs generated by the specified plugin 153 | ]] 154 | return h 155 | end 156 | 157 | function _plugin.test(...) 158 | -- use our own data here 159 | local data = {user = "foo", channel = "bar", text = "plugin stats ping"} 160 | local res, err = _plugin.run(data) 161 | local params = { 162 | mock_data = data, 163 | run_data = res 164 | } 165 | local t = require('utils.test').new(params) 166 | t:add("responds_text") 167 | t:add("response_contains", [=[(^stats for plugin '\w+': errors=\d+ | executions=\d+)$]=]) 168 | t:run() 169 | return t:report() 170 | end 171 | 172 | return _plugin 173 | -------------------------------------------------------------------------------- /var_nginx/content/html/docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | Plugins in lubot are pretty straight forward. The boilerplate required is very minimal. In general a lubot plugin has the following required structure: 3 | 4 | ```lua 5 | local plugin = {} 6 | 7 | plugin.id = "something" -- The name of your plugin. Will be concatenated with `lubot_` to locate the module 8 | plugin.version = "0.0.1" -- A version string - arbitrary text 9 | plugin.regex = [[something]] -- The keyword that will route the message to this plugin. Never use a start-of-string token here 10 | 11 | -- internal requirements/shipped helpers. You probably want these. 12 | local plugins = require 'utils.plugins' 13 | local slack = require 'utils.slack' 14 | 15 | function plugin.run(data) 16 | local text = "Hello from lubot" 17 | return slack.say(text) 18 | end 19 | 20 | function plugin.help() 21 | local h = [[This plugin says things]] 22 | return h 23 | end 24 | 25 | -- optional test support 26 | function plugin.test(data) 27 | local res = plugin.run(data) 28 | local expects = [=[^Hello from .*$]=] 29 | local params = { 30 | mock_data = data, 31 | run_data = res 32 | } 33 | local t = require('utils.test').new(params) 34 | -- does it contain a text key (required for slack RTS responses) 35 | t:add("responds_text") 36 | -- does the response match a given regex or string 37 | t:add("response_contains", expects) 38 | -- run the test 39 | t:run() 40 | -- return the report 41 | return t:report() 42 | end 43 | 44 | return plugin 45 | ``` 46 | 47 | ## Breakdown 48 | Plugin routing uses the following flow (using the above plugin as an example) 49 | 50 | - match msg text (without the botname prefix) against the regex for each active plugin (this needs instrumenting) 51 | - makes an internal http api request (over unix domain socket) to `/_private/api/plugins/run/` passing in the message data 52 | 53 | Once the api request takes over, the following happens: 54 | 55 | - concatenate the id of plugin with `lubot_` (i.e `lubot_something` using the above example) 56 | - safely load the plugin 57 | - call the `run` function on the plugin with the message data as the argument 58 | 59 | The api passes the json response back up to the websocket session. If the message was a rich text message, it's sent via webhook otherwise response is over websockets 60 | 61 | It seems complicated but the benefit here is that by internally using an HTTP api, the same flow can be used for testing entirely outside of the chat session. No need for a console plugin of any kind. 62 | 63 | In general, all you need to know is that plugins must define the following: 64 | 65 | - `id`: used to concatenate and load the module as `lubot_` 66 | - `regex`: used to match the text addressed to the botname (excluding the botname) 67 | - `version`: unused for now but still required 68 | - a run function 69 | 70 | ## Activating the plugin 71 | Save the above plugin as `var_nginx/lubot_plugins/user/lubot_something.lua`. 72 | Activate the plugin either via API (`/api/plugins/activate/`) or chat (`lubot plugins activate `) 73 | 74 | ## Running the plugin 75 | You can use plugins with curl fairly easily: 76 | 77 | ``` 78 | » curl -s -XPOST -d'{"channel":"foo","user":"test"}' http://localhost:3232/api/plugins/run/something | python -mjson.tool 79 | { 80 | "text": "Hello from lubot" 81 | } 82 | ``` 83 | 84 | or you can use the chat interface. 85 | 86 | ### Debugging 87 | If your plugin failed to run for whatever reason, you can always use the `plugins` plugin to get the last error generated like so: 88 | 89 | ``` 90 | plugins last_error 91 | ``` 92 | 93 | Additionally inside your plugin, you can always call `plugins.log("some message")` assuming you required the `utils.plugins` module as `plugins`. Otherwise use the name you chose when including the module. 94 | 95 | When you log via `plugins.log()`, you can get to these log entries via chat: 96 | 97 | ``` 98 | plugins logs 99 | ``` 100 | 101 | ## Plugin tests 102 | Lubot plugins have optional support for testing using a mini-test suite approach. It's a tad bit verbose but as this test actually runs the plugin, it allows you to define precisely how far you want to test. Using the following tests for our sample plugin above: 103 | 104 | ```lua 105 | function plugin.test(data) 106 | local res = plugin.run(data) 107 | local expects = [=[^Hello from .*$]=] 108 | local params = { 109 | mock_data = data, 110 | run_data = res 111 | } 112 | local t = require('utils.test').new(params) 113 | -- does it contain a text key (required for slack RTS responses) 114 | t:add("responds_text") 115 | -- does the response match a given regex or string 116 | t:add("response_contains", expects) 117 | -- run the test 118 | t:run() 119 | -- return the report 120 | return t:report() 121 | end 122 | ``` 123 | 124 | You can test the plugin with curl like so: 125 | 126 | ``` 127 | » curl -s -XPOST -d'{"channel":"foo","user":"test", "text":"lubot something"}' http://localhost:3232/api/plugins/test/something | python -mjson.tool 128 | { 129 | "msg": "All tests passed", 130 | "passed": true, 131 | "response": "Hello from lubot", 132 | "tests": [ 133 | "responds_text", 134 | { 135 | "args": [ 136 | "^Hello from .*$" 137 | ], 138 | "name": "response_contains" 139 | } 140 | ] 141 | } 142 | ``` 143 | 144 | Tests with failures look like this: 145 | 146 | ``` 147 | » curl -s -XPOST -d'{"channel":"foo","user":"test","text":"lubot something"}' http://localhost:3232/api/plugins/test/something | python -mjson.tool 148 | { 149 | "failures": [ 150 | "Expected ^foobar but got no matches" 151 | ], 152 | "msg": "Failed 1/2", 153 | "passed": false, 154 | "tests": [ 155 | "responds_text", 156 | "response_contains" 157 | ] 158 | } 159 | ``` 160 | 161 | ### Test Methods 162 | The following methods are available for use in tests: 163 | 164 | - `is_valid_rich_text()`: is the response valid for sending as a webhook with attachments 165 | - `responds_text()`: does the response contain a text response 166 | - `parses_text(regex, named_captures)`: does `regex` result contain the `named_captures`. `named_captures` can be either a string or a table containing multiple named captures 167 | - `captures_value(str, regex, capture)`: does running `regex` against `str` result in `capture`. `capture` can either be an integer (for an index) or a named capture. Reminder lua indexes start at `1` not `0` 168 | - `response_contains(str)`: does the response contain `str`. `str` is not required. If not provided, the value of `t.expects` will be used. 169 | 170 | 171 | -------------------------------------------------------------------------------- /var_nginx/content/js/lb.js: -------------------------------------------------------------------------------- 1 | var plugin_base_url = window.location.protocol+"//"+window.location.hostname+":"+window.location.port+"/api/plugins"; 2 | var run_api_url = plugin_base_url+"/run/"; 3 | var help_api_url = plugin_base_url+"/help/"; 4 | var plugin_list_url = plugin_base_url+"/list"; 5 | var plugin_stats_url = plugin_base_url+"/stats/"; 6 | var plugin_details_url = plugin_base_url+"/details/"; 7 | 8 | function getPlugins(callback) { 9 | console.log("Getting plugin list"); 10 | $.ajax({ 11 | type: "GET", 12 | url: plugin_details_url+"all", 13 | success: callback, 14 | error: function(res){ console.log("failed to pull list of plugins: "+JSON.stringify(res));} 15 | }); 16 | } 17 | 18 | function getPluginStats(plugin, callback) { 19 | console.log("Getting stats for plugin: "+plugin); 20 | $.ajax({ 21 | type: "GET", 22 | url: plugin_stats_url+plugin, 23 | success: callback, 24 | error: function(res){ console.log("failed to pull stats for plugin "+plugin+": "+JSON.stringify(res));} 25 | }); 26 | } 27 | 28 | function toggleTestRow(plugin) { 29 | $("#plugin_"+plugin+"_test_input_row").toggle(); 30 | $("#plugin_"+plugin+"_test_output").toggle(); 31 | } 32 | 33 | function toggleHelpRow(plugin) { 34 | $("#plugin_"+plugin+"_help_output_row").toggle(); 35 | runPluginHelp(plugin); 36 | } 37 | function getPluginsCallback(res) { 38 | _.each(res, function(thing) { 39 | var rowId = "plugin_"+thing.id 40 | var nameId = "plugin_name_"+thing.id 41 | var versionId = "plugin_version_"+thing.id 42 | var errorsId = "plugin_errors_"+thing.id 43 | var execsId = "plugin_executions_"+thing.id 44 | var testId = "plugin_test_"+thing.id 45 | var helpId = "plugin_help_"+thing.id 46 | var testInputRowId = "plugin_"+thing.id+"_test_input_row" 47 | var testInputId = "plugin_"+thing.id+"_test_input" 48 | var testOutputRowId = "plugin_"+thing.id+"_test_output_row" 49 | var testOutputId = "plugin_"+thing.id+"_test_output" 50 | var helpOutputId = "plugin_"+thing.id+"_help_output" 51 | var helpOutputRowId = "plugin_"+thing.id+"_help_output_row" 52 | var plugin_row = [ 53 | "", 54 | ""+thing.id+"", 55 | "", 56 | "", 57 | "", 58 | "", 59 | "", 60 | "", 61 | "", 62 | "", 63 | "", 64 | "", 65 | "", 66 | "", 67 | "" 68 | ].join("\n"); 69 | if ($("#"+rowId).length) { 70 | console.log("plugin row exists"); 71 | } else { 72 | $("#active_plugins").append(plugin_row); 73 | } 74 | $("#"+versionId).html(thing.version); 75 | getPluginStats(thing.id, function(res){ 76 | console.log("Got stats results: "+JSON.stringify(res)); 77 | $("#"+errorsId).html(res.errors); 78 | $("#"+execsId).html(res.executions); 79 | }); 80 | }) 81 | } 82 | 83 | function populatePluginTable() { 84 | } 85 | 86 | function getPluginLogs(plugin) { 87 | 88 | } 89 | 90 | function getPluginDetails(plugin) { 91 | } 92 | 93 | function runPlugin(plugin, text) { 94 | var plugin_url = run_api_url+plugin; 95 | $.ajax({ 96 | type: "POST", 97 | data: JSON.stringify({channel: "webconsole", user: "consoleuser", text: text}), 98 | url: plugin_url, 99 | success: function(res){ populatePluginResults(plugin, res);}, 100 | error: function(res){ populatePluginResults(plugin, "failed to run plugin: "+plugin, true);}, 101 | }); 102 | } 103 | 104 | function populatePluginResults(plugin, res, err) { 105 | var runOutputElement = "#plugin_"+plugin+"_test_output"; 106 | $(runOutputElement).empty(); 107 | $(runOutputElement+"_row").show(); 108 | var response = ""; 109 | if(res.attachments) { 110 | response = res.attachments[0].fallback; 111 | } else { 112 | response = res.text; 113 | } 114 | if(err) { 115 | $(runOutputElement).addClass("alert alert-danger"); 116 | $(runOutputElement).attr("role", "alert"); 117 | }else{ 118 | $(runOutputElement).addClass("alert alert-success"); 119 | $(runOutputElement).removeClass("alert-danger"); 120 | } 121 | $(runOutputElement).append("
"+response+"
"); 122 | } 123 | 124 | function displayPluginRun(plugin) { 125 | var runInputElement = "#plugin_"+plugin+"_test_input"; 126 | var runOutputElement = "#plugin_"+plugin+"_test_output"; 127 | var command = $(runInputElement).val(); 128 | console.log("Input: "+$(runInputElement).val()); 129 | runPlugin(plugin, command); 130 | } 131 | 132 | function displayPluginHelp(text, plugin) { 133 | $("#plugin_"+plugin+"_help_output").append("
"+text+"
"); 134 | } 135 | 136 | function runPluginHelp(plugin) { 137 | if($("#plugin_"+plugin+"_help_output").is(":visible")) { 138 | console.log("Ajax call happening"); 139 | $("#plugin_"+plugin+"_help_output").empty(); 140 | var plugin_url = help_api_url+plugin; 141 | $.ajax({ 142 | type: "GET", 143 | async: false, 144 | url: plugin_url, 145 | success: function(res){ displayPluginHelp(res.msg, plugin)}, 146 | error: function(res){console.log("failed to run plugin: "+plugin)}, 147 | }); 148 | } 149 | } 150 | 151 | function connect() { 152 | if (ws !== null) return log('already connected'); 153 | ws = new WebSocket(lbws_url); 154 | ws.onopen = function () { 155 | log('connected'); 156 | }; 157 | ws.onerror = function (error) { 158 | log(error); 159 | }; 160 | ws.onmessage = function (e) { 161 | log('recv: ' + e.data); 162 | var tableData = new Array(); 163 | if (e.data === "connected") { 164 | //Do nothing 165 | } else { 166 | if (_.isUndefined(e.data)) { 167 | return false; 168 | } else { 169 | var j = JSON.parse(e.data); 170 | _.each(j, function(value, key) { 171 | log(key+" = "+value); 172 | updateTable(key, value); 173 | }) 174 | return false; 175 | } 176 | } 177 | }; 178 | ws.onclose = function () { 179 | log('disconnected'); 180 | ws = null; 181 | }; 182 | return false; 183 | } 184 | function disconnect() { 185 | if (ws === null) return log('already disconnected'); 186 | ws.close(); 187 | return false; 188 | } 189 | function send() { 190 | if (ws === null) return log('please connect first'); 191 | ws.send(text); 192 | return false; 193 | } 194 | 195 | function updateTable(k,v) { 196 | // Check if there's an existing row 197 | var tdid = k.split('/').slice(-1).pop() 198 | if ($('tr#'+tdid).length > 0) { 199 | console.log("Updating existing row: "+k); 200 | if ($("#current_"+tdid).length > 0) { 201 | console.log("This is the active backend: "+tdid) 202 | $('tr#'+tdid).addClass('success'); 203 | } 204 | $('td#'+tdid+"_name").text(k); 205 | $('td#'+tdid+"_address").text(v); 206 | } else { 207 | if ($("#current_"+tdid).length > 0) { 208 | $('#loadbalancers').append(""+k+""+v+""); 209 | } else { 210 | $('#loadbalancers').append(""+k+""+v+""); 211 | } 212 | } 213 | } 214 | 215 | function log(text) { 216 | console.log(text); 217 | return false; 218 | } 219 | -------------------------------------------------------------------------------- /var_nginx/lua/utils/plugins.lua: -------------------------------------------------------------------------------- 1 | local ngu = require 'utils.nginx' 2 | local log = require 'utils.log' 3 | 4 | local m = {} 5 | m._VERSION = "0.0.1" 6 | 7 | m.dicts = { 8 | active = ngx.shared.plugin_active, 9 | config = ngx.shared.plugin_config, 10 | log = ngx.shared.plugin_log, 11 | errors = ngx.shared.plugin_errors, 12 | executions = ngx.shared.plugin_executions 13 | } 14 | 15 | function m.plugin_active(p) 16 | local d = m.dicts.active:get_keys() 17 | if not d[p] then 18 | return false 19 | else 20 | return true 21 | end 22 | end 23 | 24 | function m.find_plugin_for(text) 25 | local matches = {} 26 | local active_plugins = m.dicts.active:get_keys() 27 | if not active_plugins then 28 | return nil 29 | else 30 | for _, k in ipairs(active_plugins) do 31 | local plugin = m.safe_plugin_load(k) 32 | if plugin then 33 | local m, err = ngx.re.match(text, "^"..plugin.regex, 'jo') 34 | if m then 35 | -- return the plugin function itself to save another safeload call 36 | table.insert(matches, plugin) 37 | end 38 | end 39 | end 40 | end 41 | return matches 42 | end 43 | 44 | function m.generate_id() 45 | local worker_id = ngx.worker.pid() 46 | local tstamp = ngx.time() 47 | return worker_id..tstamp 48 | end 49 | 50 | function m.pass_test(...) 51 | local args = ... 52 | local resp = { 53 | passed = true 54 | } 55 | if args then 56 | for k, v in pairs(args) do 57 | resp[k] = v 58 | end 59 | end 60 | return resp 61 | end 62 | 63 | function m.fail_test(msg, ...) 64 | local args = ... 65 | local resp = { 66 | passed = false, 67 | failure = msg 68 | } 69 | if args then 70 | for k, v in pairs(args) do 71 | resp[k] = v 72 | end 73 | end 74 | return resp 75 | end 76 | 77 | function m.plugin_error(p, msg) 78 | local plugin_log = m.dicts.log 79 | local plugin_errors = m.dicts.errors 80 | local success = m.safe_incr(plugin_errors, p) 81 | if not success then 82 | log.warn("Unable to increment counter") 83 | end 84 | local perr = "Plugin '"..p.."' errored with message: "..msg 85 | m.set_last_error(p, perr) 86 | log.err(perr) 87 | ngx.status = ngx.HTTP_NOT_ALLOWED 88 | ngx.header.content_type = "application/json" 89 | ngx.say(safe_json_encode({err = true, msg = msg})) 90 | ngx.exit(ngx.HTTP_NOT_ALLOWED) 91 | end 92 | 93 | function m.get_botname() 94 | local shared_dict = ngx.shared.ng_shared_dict 95 | local botname, err = shared_dict:get("bot_name") 96 | if not botname then 97 | return "lubot" 98 | else 99 | return botname 100 | end 101 | end 102 | 103 | function m.set_last_error(p, msg) 104 | local d = m.dicts.log 105 | ngx.update_time() 106 | local keyname = p..":last_error" 107 | local perr = { 108 | tstamp = ngx.utctime(), 109 | msg = msg 110 | } 111 | local json = safe_json_encode(perr) 112 | if not json then 113 | m.err("Unable to encode json before insert") 114 | else 115 | local success, err, forcible = d:set(keyname, json) 116 | if not success then 117 | log.err("Unable to write plugin log entry for '"..p.."': ", err) 118 | end 119 | if forcible then 120 | log.warn("Keys evicted to write log message for '"..p.."'") 121 | end 122 | end 123 | end 124 | 125 | function m.get_last_error(p) 126 | local plugin_log = m.dicts.log 127 | local last_error = plugin_log:get(p..":last_error") 128 | local resp = safe_json_decode(last_error) 129 | if not resp then 130 | log.err("Unable to decode last_error for "..p) 131 | else 132 | return resp 133 | end 134 | end 135 | 136 | function m.get_logs(p) 137 | local regex = [=[(?^\w+)\:(?\d+.*)$]=] 138 | local plugin_log = m.dicts.log 139 | local entries = {} 140 | local keys = plugin_log:get_keys() 141 | for _, k in ipairs(keys) do 142 | local match, e = ngx.re.match(k, regex, 'jo') 143 | if not match then 144 | log.info("Regex failed with error: ", e) 145 | else 146 | if match.plugin_name == p then 147 | local line = plugin_log:get(k) 148 | local t = { 149 | timestamp = match.tstamp, 150 | msg = line 151 | } 152 | table.insert(entries, t) 153 | end 154 | end 155 | end 156 | return entries 157 | end 158 | 159 | function m.plog(p, msg) 160 | local d = m.dicts.log 161 | ngx.update_time() 162 | local keyname = p..":"..ngx.now() 163 | local success, err, forcible = d:set(keyname, msg) 164 | if not success then 165 | log.err("Unable to write plugin log entry for '"..p.."': ", err) 166 | end 167 | if forcible then 168 | log.warn("Keys evicted to write log message for '"..p.."'") 169 | end 170 | return success 171 | end 172 | 173 | function m.safe_incr(dict, key) 174 | local d = dict 175 | local success, err, forcible = d:add(key, 0) 176 | if not success and err ~= "exists" then 177 | return nil 178 | end 179 | local isuccess, ierror, iforcible = d:incr(key, 1) 180 | if not isuccess then return nil end 181 | if ierror then return nil end 182 | return true 183 | end 184 | 185 | function m.safe_plugin_load(p) 186 | local plugin_ok, plugin = pcall(require, "lubot_"..p) 187 | if not plugin_ok then 188 | log.err("Unable to load the plugin: ", p) 189 | return nil 190 | else 191 | if not plugin.id or not plugin.version or not plugin.regex then 192 | log.err("Plugin missing required metadata (id, version or regex): ", plugin.id) 193 | return nil 194 | else 195 | return plugin 196 | end 197 | end 198 | end 199 | 200 | function m.safe_plugin_run(p, d) 201 | local plugin = m.safe_plugin_load(p) 202 | if not plugin then return m.plugin_error(p, "Plugin failed to load cleanly. Check logs for errors") end 203 | local run_ok, res, err = pcall(plugin.run, d) 204 | if not run_ok then return m.plugin_error(p, "Plugin did not run safely: "..res) end 205 | if err then return m.plugin_error(p, "Plugin returned an error: "..err) end 206 | local incr_exec = m.safe_incr(m.dicts.executions, p) 207 | if not incr_exec then log.warn("Unable to increment execution counter for "..p) end 208 | return res 209 | end 210 | 211 | function m.respond_as_json(t) 212 | ngx.header.content_type = "application/json" 213 | local response = safe_json_encode(t) 214 | if not response then 215 | log.alert("JSON encode failed: ", response) 216 | else 217 | ngx.say(response) 218 | ngx.exit(ngx.HTTP_OK) 219 | end 220 | end 221 | 222 | function m.plugin_help(p) 223 | local plugin = m.safe_plugin_load(p) 224 | if not plugin.help then 225 | local resp = [[Plugin has no help. Displaying regex used instead. 226 | regex: `^]]..plugin.regex.."`" 227 | log.warn("plugin ", plugin.id, " has no help output") 228 | return resp 229 | else 230 | local help_ok, help = pcall(plugin.help) 231 | if not help_ok then 232 | m.plugin_error(plugin.id, " help errored: ", help) 233 | else 234 | return help 235 | end 236 | end 237 | end 238 | 239 | function m.plugin_test(p, body) 240 | local d = body or {} 241 | local plugin = m.safe_plugin_load(p) 242 | if not plugin.test then 243 | m.plugin_error(plugin.id, "No test defined for plugin "..plugin.id) 244 | else 245 | local str = d.text 246 | if str then 247 | local regex = [=[(^]=]..m.get_botname()..[=[)\s+(?.*$)]=] 248 | local b, e = ngx.re.match(d.text, regex, 'jo') 249 | if b and b.remainder then 250 | str = b.remainder 251 | end 252 | d.text = str 253 | end 254 | local test_ok, test = pcall(plugin.test, d) 255 | if not test_ok then 256 | return m.fail_test("Test failed to run: "..test) 257 | else 258 | return test 259 | end 260 | end 261 | end 262 | 263 | function m.plugin_stats(p) 264 | local resp 265 | local plugin_errors = ngx.shared.plugin_errors 266 | local plugin_executions = ngx.shared.plugin_executions 267 | local plugin_logs = ngx.shared.plugin_logs 268 | local errcount, err = plugin_errors:get(p) 269 | if not errcount then 270 | resp = {plugin = p, errors = 0, msg = err} 271 | else 272 | resp = {plugin = p, errors = errcount} 273 | end 274 | local excount, exerr = plugin_executions:get(p) 275 | if not excount then 276 | resp.executions = 0 277 | else 278 | resp.executions = excount 279 | end 280 | return resp 281 | end 282 | 283 | function m.plugin_details(p) 284 | local plugin = m.safe_plugin_load(p) 285 | if not plugin then 286 | return m.plugin_error(p, "Plugin details not found for "..p) 287 | else 288 | local t = { 289 | version = plugin.version, 290 | id = plugin.id, 291 | regex = plugin.regex 292 | } 293 | return t 294 | end 295 | end 296 | 297 | function m.enable_plugin(p) 298 | local success, err, forcible = m.dicts.active:set(p, true) 299 | if not success and err ~= "exists" then 300 | return nil, "Plugin already exists" 301 | elseif forcible then 302 | return true, "An existing plugin was evicted to make room. This could be bad" 303 | else 304 | return true, nil 305 | end 306 | end 307 | 308 | function m.disable_plugin(p) 309 | m.dicts.active:delete(p) 310 | end 311 | 312 | function m.get_active() 313 | return m.dicts.active:get_keys() 314 | end 315 | 316 | function m.load_config() 317 | local inspect = require 'inspect' 318 | 319 | local defaults = '{"enabled":["status","ping","image","plugins","help"]}' 320 | local lubot_config = ngx.shared.lubot_config 321 | local plugin_config_file = lubot_config:get("config_file") 322 | local config 323 | if not plugin_config_file then 324 | log.err("Missing config file, loading defaults") 325 | config = safe_json_decode(defaults) 326 | else 327 | log.alert("Loading config from ", plugin_config_file) 328 | local file = io.open(plugin_config_file) 329 | if not file then 330 | log.err("Unable to read file. Loading defaults") 331 | config = safe_json_decode(defaults) 332 | else 333 | local content = file:read("*a") 334 | file:close() 335 | config = safe_json_decode(content) 336 | end 337 | end 338 | for idx, plugin in pairs(config.enabled) do 339 | log.alert("Enabling plugin: ", plugin) 340 | m.dicts.active:set(plugin, true) 341 | if config[plugin] then 342 | local plugin_settings = safe_json_encode(config[plugin]) 343 | m.dicts.config:set(plugin, plugin_settings) 344 | log.alert("Loading ", plugin, " settings from config: ", inspect(config[plugin])) 345 | end 346 | end 347 | return config 348 | end 349 | 350 | function m.pick_random(t) 351 | math.randomseed(ngx.time()) 352 | local candidates = t 353 | return candidates[math.random(#candidates)] 354 | end 355 | 356 | function m.match(...) 357 | local s, err = ngx.re.match(unpack({...}), 'jo') 358 | end 359 | return m 360 | -------------------------------------------------------------------------------- /var_nginx/content/js/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.7.0 2 | // http://underscorejs.org 3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /var_nginx/lua/init.lua: -------------------------------------------------------------------------------- 1 | local _VERSION = "0.0.1" 2 | -- These variables are all local to this file 3 | local lubot_plugin_config = os.getenv("LUBOT_PLUGIN_CONFIG") or "/var/nginx/lubot_plugins/plugins.json" 4 | local botname = os.getenv("LUBOT_BOTNAME") or "lubot" 5 | local slack_token = os.getenv("SLACK_API_TOKEN") or nil 6 | local slack_webhook_url = os.getenv("SLACK_WEBHOOK_URL") 7 | -- default brain is a shared dictionary 8 | local lubot_brain = os.getenv("LUBOT_BRAIN") or "ngx_shared" 9 | local lubot_brain_opts = os.getenv("LUBOT_BRAIN_OPTS") 10 | 11 | local shared_dict = ngx.shared.ng_shared_dict 12 | local lubot_config = ngx.shared.lubot_config 13 | lubot_config:set("config_file", lubot_plugin_config) 14 | shared_dict:set("startup_time", startup_time) 15 | shared_dict:set("slack_token", slack_token) 16 | shared_dict:set("slack_webhook_url", slack_webhook_url) 17 | shared_dict:set("bot_name", botname) 18 | 19 | local ngu = require 'utils.nginx' 20 | local pu = require 'utils.plugins' 21 | local slack = require 'utils.slack' 22 | local log = require 'utils.log' 23 | 24 | local inspect = require 'inspect' 25 | 26 | menubar = { 27 | ["Slack"] = "/slack", 28 | ["Plugins"] = "/plugins", 29 | ["Logs"] = "/logs", 30 | ["Docs"] = "/docs/index" 31 | } 32 | 33 | robot = {} 34 | 35 | if lubot_brain == 'memory' then 36 | print("Memory brain is invalid for nginx. Switching to ngx_shared") 37 | lubot_brain = 'ngx_shared' 38 | end 39 | local brain_ok, brain = pcall(require, 'utils.brain') 40 | if not brain_ok then 41 | print("Failed to load brain. This won't work....") 42 | else 43 | robot.brain = brain.new(lubot_brain, lubot_brain_opts) 44 | robot.brain:set('botname', botname) 45 | robot.brain:set('config_file', lubot_plugin_config) 46 | end 47 | 48 | function safe_json_decode(str) 49 | if not str then 50 | log.err("no string passed in for decoding") 51 | return nil 52 | end 53 | local caller = debug.getinfo(2).name 54 | local cjson = require 'cjson' 55 | local ok, data = pcall(cjson.decode, str) 56 | if not ok then 57 | log.err("unable to decode json from "..caller..": ", data) 58 | return nil 59 | else 60 | return data 61 | end 62 | end 63 | 64 | function safe_json_encode(t) 65 | local cjson = require 'cjson' 66 | local ok, data = pcall(cjson.encode, t) 67 | if not ok then 68 | log.err("unable to encode json: ", data) 69 | return nil 70 | else 71 | return data 72 | end 73 | end 74 | 75 | function slackbot(premature) 76 | if premature then return nil end 77 | -- shared dicts 78 | local slack_running = ngx.shared.slack_running 79 | local shared_dict = ngx.shared.ng_shared_dict 80 | local locks = ngx.shared.shared_locks 81 | 82 | local function fill_slack_dicts(data) 83 | if not data then return false, "Missing data" end 84 | local errors = {} 85 | local slack_users = ngx.shared.slack_users 86 | local slack_groups = ngx.shared.slack_groups 87 | local slack_channels = ngx.shared.slack_channels 88 | local slack_ims = ngx.shared.slack_ims 89 | local slack_bots = ngx.shared.slack_bots 90 | 91 | log.alert("Filling shared dict with initial details") 92 | 93 | for k, v in pairs(data.users) do 94 | local str = safe_json_encode(v) 95 | if not str then break end 96 | local o, e = slack_users:safe_set(v.id, str) 97 | if not o then 98 | log.err("Unable to add user to shared_dict: ", e) 99 | errors.insert(v) 100 | end 101 | local bo = robot.brain:safe_set(v.id, v) 102 | if not bo then 103 | log.err("failed to add data to brain") 104 | end 105 | robot.brain:save() 106 | end 107 | for k, v in pairs(data.groups) do 108 | local str = safe_json_encode(v) 109 | if not str then break end 110 | local o, e = slack_groups:safe_set(v.id, str) 111 | if not o then 112 | log.err("Unable to add group to shared_dict: ", e) 113 | errors.insert(v) 114 | end 115 | local bo = robot.brain:safe_set(v.id, v) 116 | if not bo then 117 | log.err("failed to add data to brain") 118 | end 119 | robot.brain:save() 120 | end 121 | for k, v in pairs(data.channels) do 122 | local str = safe_json_encode(v) 123 | if not str then break end 124 | local o, e = slack_channels:safe_set(v.id, str) 125 | if not o then 126 | log.err("Unable to add channel to shared_dict: ", e) 127 | errors.insert(v) 128 | end 129 | local bo = robot.brain:safe_set(v.id, v) 130 | if not bo then 131 | log.err("failed to add data to brain") 132 | end 133 | robot.brain:save() 134 | end 135 | for k, v in pairs(data.bots) do 136 | local str = safe_json_encode(v) 137 | if not str then break end 138 | local o, e = slack_bots:safe_set(v.id, str) 139 | if not o then 140 | log.err("Unable to add bots to shared_dict: ", e) 141 | errors.insert(v) 142 | end 143 | local bo = robot.brain:safe_set(v.id, v) 144 | if not bo then 145 | log.err("failed to add data to brain") 146 | end 147 | robot.brain:save() 148 | end 149 | for k, v in pairs(data.ims) do 150 | local str = safe_json_encode(v) 151 | if not str then break end 152 | local o, e = slack_ims:safe_set(v.id, str) 153 | if not o then 154 | log.err("Unable to add ims to shared_dict: ", e) 155 | errors.insert(v) 156 | end 157 | local bo = robot.brain:safe_set(v.id, v) 158 | if not bo then 159 | log.err("failed to add data to brain") 160 | end 161 | robot.brain:save() 162 | end 163 | log.alert("Filled shared dict") 164 | if #errors > 0 then 165 | return false, errors 166 | else 167 | return true, nil 168 | end 169 | end 170 | 171 | local function wait(url) 172 | if not robot.brain then 173 | return false 174 | end 175 | local plugin_config = pu.load_config() 176 | local slack_webhook_url = shared_dict:get('slack_webhook_url') 177 | if not slack_webhook_url then log.alert("No webhook url. Some plugins may not work") end 178 | local slack_token = shared_dict:get('slack_token') 179 | local botname = shared_dict:get('bot_name') 180 | if not slack_token then return false end 181 | local u = url or "https://slack.com/api/rtm.start?token="..slack_token 182 | -- we use httpclient here because we don't have 183 | -- ngx.location.capture available in this context 184 | -- and resty-http has stupid SSL issues 185 | -- It's okay to block this request anyway...sort of 186 | local hc = require 'httpclient'.new() 187 | local reqheaders = { 188 | ["User-Agent"] = botname.." 0.0.1", 189 | ["Accept"] = "application/json", 190 | ["Content-Type"] = "application/json" 191 | } 192 | local res = hc:get(u,{ headers = reqheaders}) 193 | if res.err then 194 | log.err('failed to connect to slack: '..err) 195 | return false 196 | end 197 | local data 198 | local body = res.body 199 | if not body then 200 | log.err("Missing body", res.status) 201 | return false 202 | else 203 | data = safe_json_decode(body) 204 | if not data then return false end 205 | end 206 | -- Schedule a fill of shared dicts with the slack data from the initial auth 207 | -- local pok, perr = ngx.timer.at(0, fill_slack_dicts, data) 208 | local pok, perr = fill_slack_dicts(data) 209 | if not pok then 210 | log.err("Failed to schedule filling of shared dicts with slack data: ", inspect(perr)) 211 | end 212 | local rewrite_url_t = hc:urlparse(data.url) 213 | -- proxy_pass doesn't understand ws[s] urls so we fake it 214 | local rewrite_url = "https://"..rewrite_url_t.host..rewrite_url_t.path 215 | local proxy_url = 'ws://127.0.0.1:3131/wssproxy?url='..rewrite_url 216 | local ws = require 'resty.websocket.client' 217 | local wsc, wscerr = ws:new() 218 | local ok, connerr = wsc:connect(proxy_url) 219 | if not ok then 220 | log.err("[failed to connect] ", connerr) 221 | return false 222 | end 223 | 224 | local function parse_command(cmd, msg_data) 225 | -- switch to the cosocket library here since we want better perf 226 | local data = safe_json_encode(msg_data) 227 | if not data then return nil end 228 | local channel = msg_data.channel 229 | local http = require 'resty.http' 230 | local httpc = http.new() 231 | httpc:connect("unix:/var/nginx/tmp/ngx.private.sock") 232 | httpc:set_timeout(5000) 233 | local res, err = httpc:request{ 234 | method = "POST", 235 | path = "/_private/api/plugins/run/"..cmd, 236 | headers = {["Host"] = "localhost", ["Content-Type"] = "application/json"}, 237 | body = data 238 | } 239 | if not res then 240 | log.err("Got no response from request. That's bad") 241 | httpc:set_keepalive() 242 | return nil 243 | end 244 | httpc:set_keepalive() 245 | if err or res.status == 405 then 246 | log.err("error running plugin: ", res.status) 247 | httpc:set_keepalive() 248 | return nil 249 | else 250 | local body = res:read_body() 251 | local decoded_body = safe_json_decode(body) 252 | if not decoded_body then 253 | log.alert([[plugin response does not appear to be json]]) 254 | httpc:set_keepalive() 255 | return nil 256 | end 257 | if decoded_body.attachments then 258 | httpc:set_timeout(10000) 259 | local res, err = httpc:request{ 260 | method = "POST", 261 | path = "/_private/slackpost", 262 | headers = {["Host"] = "localhost", ["Content-Type"] = "application/json"}, 263 | body = body 264 | } 265 | httpc:set_keepalive() 266 | if not res or res.status ~= 200 then 267 | -- chat message failed 268 | httpc:set_keepalive() 269 | log.err("Unable to rich message to slack api: "..err) 270 | return nil 271 | else 272 | return true 273 | end 274 | else 275 | httpc:set_keepalive() 276 | local text = decoded_body.text 277 | local msg = slack.to_rts_message(text, channel) 278 | return msg 279 | end 280 | end 281 | end 282 | 283 | local users = ngx.shared.slack_users 284 | local groups = ngx.shared.slack_groups 285 | local channels = ngx.shared.slack_channels 286 | local ims = ngx.shared.slack_ims 287 | local bots = ngx.shared.slack_bots 288 | local function get_source(c) 289 | local m, err = ngx.re.match(c, "^([A-Z]).*", "jo") 290 | if not m then log.err("Error attempting match: ", err); return c end 291 | --log(INFO, "Matched ", c, " as type ", m[1]) 292 | if m[1] == 'D' then return safe_json_decode(ims:get(c)) end 293 | if m[1] == 'C' then return safe_json_decode(channels:get(c)) end 294 | if m[1] == 'G' then return safe_json_decode(groups:get(c)) end 295 | if m[1] == 'B' then return safe_json_decode(bots:get(c)) end 296 | if m[1] == 'U' then return safe_json_decode(users:get(c)) end 297 | end 298 | while true do 299 | local data, typ, err = wsc:recv_frame() 300 | if wsc.fatal then 301 | log.err("[failed to recieve the frame] ", err) 302 | break 303 | end 304 | if not data then 305 | log.alert("[sending wss ping] ", typ) 306 | local bytes, err = wsc:send_ping() 307 | if not bytes then 308 | log.err("[failed to send wss ping] ", err) 309 | break 310 | end 311 | elseif typ == "close" then break 312 | elseif typ == "ping" then 313 | log.alert("[wss ping] ", typ, " ("..data..")") 314 | local bytes, err = wsc:send_pong() 315 | if not bytes then 316 | log.err("[failed to send wss pong] ", err) 317 | break 318 | end 319 | elseif typ == "text" then 320 | local res = safe_json_decode(data) 321 | if res then 322 | if res['type'] == 'message' and res['subtype'] == 'message_changed' then 323 | -- ignore it 324 | elseif res['type'] == 'message' and res['subtype'] == 'bot_message' then 325 | -- ignore it 326 | elseif res.reply_to then 327 | -- ignore it 328 | elseif res['type'] == 'message' then 329 | local channel 330 | local c = get_source(res.channel) or nil 331 | if not c then 332 | channel = res.channel 333 | else 334 | if c.is_im then channel = 'private' else channel = c.name end 335 | end 336 | local user 337 | local u = get_source(res.user) or nil 338 | if not u then 339 | user = res.user 340 | else 341 | user = u.name 342 | end 343 | local m, err = ngx.re.match(res.text, "^"..botname.." (.*)$", "jo") 344 | if not m then 345 | -- we don't care if it's not directed at us 346 | else 347 | local command = m[1] 348 | if not command then 349 | log.warn("No command found") 350 | else 351 | if command == "die" then 352 | if not u.is_admin then 353 | log.alert("Ignoring non-admin user") 354 | else 355 | local m = { 356 | ["type"] = "message", 357 | text = "I am slain!", 358 | channel = res.channel, 359 | id = ngx.worker.pid()..ngx.time() 360 | } 361 | local bytes, err = wsc:send_text(safe_json_encode(m)) 362 | if err then 363 | log.err("Got an error sending die response: ", err) 364 | end 365 | break 366 | end 367 | end 368 | local candidates = pu.find_plugin_for(command) 369 | if #candidates == 1 then 370 | local response = parse_command(candidates[1].id, res) 371 | if not response then 372 | log.warn("empty response") 373 | elseif response == true then 374 | -- post message via webhook successfully 375 | else 376 | local reply = safe_json_encode(response) 377 | if not reply then 378 | log.err("Got an error encoding json for reply") 379 | else 380 | local bytes, err = wsc:send_text(reply) 381 | if err then log.err("Got an error responding: ", err) end 382 | end 383 | end 384 | elseif #candidates > 1 then 385 | log.alert("Multiple candidates for command. This should never happen") 386 | else 387 | -- do nothing 388 | end 389 | end 390 | end 391 | elseif res['type'] == 'hello' then 392 | log.alert("Connected!") 393 | elseif res['type'] == 'group_joined' then 394 | -- handle getting add to a new group 395 | local groups = ngx.shared.slack_groups 396 | local data = safe_json_encode(res.channel) 397 | local o, e = groups:safe_set(res.channel.id, data) 398 | if e then 399 | log.warn("Unable to add new group to shared_dict: ", e) 400 | end 401 | elseif res['type'] == 'user_typing' then 402 | -- skip it 403 | elseif res['type'] == 'presence_change' then 404 | -- skip it 405 | elseif res['reply_to'] then 406 | -- skip it 407 | elseif res['type'] == 'file_change' then 408 | -- skip it 409 | elseif res['type'] == 'file_shared' then 410 | -- skip it 411 | else 412 | log.notice("[unknown type] ", data) 413 | end 414 | else 415 | log.err("Error decoding json: ", inspect(data)) 416 | end 417 | end 418 | end 419 | log.notice("WSS Loop broke out") 420 | return false 421 | end 422 | 423 | -- lua-resty-lock locks will always expire 424 | -- not suited for long-running locks 425 | -- instead we will gate the concurrent access to this function 426 | -- with a resty-lock and then use our own tracking 427 | -- internally 428 | local working_worker_pid = slack_running:get("locked") 429 | local asking_worker_pid = ngx.worker.pid() 430 | if asking_worker_pid ~= working_worker_pid then 431 | return false 432 | else 433 | log.alert("I am the current holder. Continuing") 434 | -- keys 435 | local slack_token = shared_dict:get('slack_token') 436 | -- third party 437 | local wok, res = pcall(wait) 438 | local werr = wok or res 439 | if not wok or res == false then 440 | log.err("Res loop exited: ", werr) 441 | else 442 | log.alert("You...shouldn't get here but I'm playing it safe") 443 | end 444 | return res 445 | end 446 | end 447 | -------------------------------------------------------------------------------- /var_nginx/content/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-default .badge,.btn-primary .badge,.btn-success .badge,.btn-info .badge,.btn-warning .badge,.btn-danger .badge{text-shadow:none}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:hover,.btn-primary:focus{background-color:#265a88;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#265a88;border-color:#245580}.btn-primary:disabled,.btn-primary[disabled]{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:hover .badge,.list-group-item.active:focus .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /var_nginx/content/js/underscore-min.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"underscore-min.js","sources":["underscore.js"],"names":["root","this","previousUnderscore","_","ArrayProto","Array","prototype","ObjProto","Object","FuncProto","Function","push","slice","concat","toString","hasOwnProperty","nativeIsArray","isArray","nativeKeys","keys","nativeBind","bind","obj","_wrapped","exports","module","VERSION","createCallback","func","context","argCount","value","call","other","index","collection","accumulator","apply","arguments","iteratee","identity","isFunction","isObject","matches","property","each","forEach","i","length","map","collect","currentKey","results","reduceError","reduce","foldl","inject","memo","TypeError","reduceRight","foldr","find","detect","predicate","result","some","list","filter","select","reject","negate","every","all","any","contains","include","target","values","indexOf","invoke","method","args","isFunc","pluck","key","where","attrs","findWhere","max","computed","Infinity","lastComputed","min","shuffle","rand","set","shuffled","random","sample","n","guard","Math","sortBy","criteria","sort","left","right","a","b","group","behavior","groupBy","has","indexBy","countBy","sortedIndex","array","low","high","mid","toArray","size","partition","pass","fail","first","head","take","initial","last","rest","tail","drop","compact","flatten","input","shallow","strict","output","isArguments","without","difference","uniq","unique","isSorted","isBoolean","seen","union","intersection","argsLength","item","j","zip","object","lastIndexOf","from","idx","range","start","stop","step","ceil","Ctor","bound","self","partial","boundArgs","position","bindAll","Error","memoize","hasher","cache","address","delay","wait","setTimeout","defer","throttle","options","timeout","previous","later","leading","now","remaining","clearTimeout","trailing","debounce","immediate","timestamp","callNow","wrap","wrapper","compose","after","times","before","once","pairs","invert","functions","methods","names","extend","source","prop","pick","omit","String","defaults","clone","tap","interceptor","eq","aStack","bStack","className","aCtor","constructor","bCtor","pop","isEqual","isEmpty","isString","isElement","nodeType","type","name","isFinite","isNaN","parseFloat","isNumber","isNull","isUndefined","noConflict","constant","noop","pair","accum","floor","Date","getTime","escapeMap","&","<",">","\"","'","`","unescapeMap","createEscaper","escaper","match","join","testRegexp","RegExp","replaceRegexp","string","test","replace","escape","unescape","idCounter","uniqueId","prefix","id","templateSettings","evaluate","interpolate","noMatch","escapes","\\","\r","\n","
","
","escapeChar","template","text","settings","oldSettings","matcher","offset","variable","render","e","data","argument","chain","instance","_chain","mixin","define","amd"],"mappings":";;;;CAKC,WAMC,GAAIA,GAAOC,KAGPC,EAAqBF,EAAKG,EAG1BC,EAAaC,MAAMC,UAAWC,EAAWC,OAAOF,UAAWG,EAAYC,SAASJ,UAIlFK,EAAmBP,EAAWO,KAC9BC,EAAmBR,EAAWQ,MAC9BC,EAAmBT,EAAWS,OAC9BC,EAAmBP,EAASO,SAC5BC,EAAmBR,EAASQ,eAK5BC,EAAqBX,MAAMY,QAC3BC,EAAqBV,OAAOW,KAC5BC,EAAqBX,EAAUY,KAG7BlB,EAAI,SAASmB,GACf,MAAIA,aAAenB,GAAUmB,EACvBrB,eAAgBE,QACtBF,KAAKsB,SAAWD,GADiB,GAAInB,GAAEmB,GAOlB,oBAAZE,UACa,mBAAXC,SAA0BA,OAAOD,UAC1CA,QAAUC,OAAOD,QAAUrB,GAE7BqB,QAAQrB,EAAIA,GAEZH,EAAKG,EAAIA,EAIXA,EAAEuB,QAAU,OAKZ,IAAIC,GAAiB,SAASC,EAAMC,EAASC,GAC3C,GAAID,QAAiB,GAAG,MAAOD,EAC/B,QAAoB,MAAZE,EAAmB,EAAIA,GAC7B,IAAK,GAAG,MAAO,UAASC,GACtB,MAAOH,GAAKI,KAAKH,EAASE,GAE5B,KAAK,GAAG,MAAO,UAASA,EAAOE,GAC7B,MAAOL,GAAKI,KAAKH,EAASE,EAAOE,GAEnC,KAAK,GAAG,MAAO,UAASF,EAAOG,EAAOC,GACpC,MAAOP,GAAKI,KAAKH,EAASE,EAAOG,EAAOC,GAE1C,KAAK,GAAG,MAAO,UAASC,EAAaL,EAAOG,EAAOC,GACjD,MAAOP,GAAKI,KAAKH,EAASO,EAAaL,EAAOG,EAAOC,IAGzD,MAAO,YACL,MAAOP,GAAKS,MAAMR,EAASS,YAO/BnC,GAAEoC,SAAW,SAASR,EAAOF,EAASC,GACpC,MAAa,OAATC,EAAsB5B,EAAEqC,SACxBrC,EAAEsC,WAAWV,GAAeJ,EAAeI,EAAOF,EAASC,GAC3D3B,EAAEuC,SAASX,GAAe5B,EAAEwC,QAAQZ,GACjC5B,EAAEyC,SAASb,IASpB5B,EAAE0C,KAAO1C,EAAE2C,QAAU,SAASxB,EAAKiB,EAAUV,GAC3C,GAAW,MAAPP,EAAa,MAAOA,EACxBiB,GAAWZ,EAAeY,EAAUV,EACpC,IAAIkB,GAAGC,EAAS1B,EAAI0B,MACpB,IAAIA,KAAYA,EACd,IAAKD,EAAI,EAAOC,EAAJD,EAAYA,IACtBR,EAASjB,EAAIyB,GAAIA,EAAGzB,OAEjB,CACL,GAAIH,GAAOhB,EAAEgB,KAAKG,EAClB,KAAKyB,EAAI,EAAGC,EAAS7B,EAAK6B,OAAYA,EAAJD,EAAYA,IAC5CR,EAASjB,EAAIH,EAAK4B,IAAK5B,EAAK4B,GAAIzB,GAGpC,MAAOA,IAITnB,EAAE8C,IAAM9C,EAAE+C,QAAU,SAAS5B,EAAKiB,EAAUV,GAC1C,GAAW,MAAPP,EAAa,QACjBiB,GAAWpC,EAAEoC,SAASA,EAAUV,EAKhC,KAAK,GADDsB,GAHAhC,EAAOG,EAAI0B,UAAY1B,EAAI0B,QAAU7C,EAAEgB,KAAKG,GAC5C0B,GAAU7B,GAAQG,GAAK0B,OACvBI,EAAU/C,MAAM2C,GAEXd,EAAQ,EAAWc,EAARd,EAAgBA,IAClCiB,EAAahC,EAAOA,EAAKe,GAASA,EAClCkB,EAAQlB,GAASK,EAASjB,EAAI6B,GAAaA,EAAY7B,EAEzD,OAAO8B,GAGT,IAAIC,GAAc,6CAIlBlD,GAAEmD,OAASnD,EAAEoD,MAAQpD,EAAEqD,OAAS,SAASlC,EAAKiB,EAAUkB,EAAM5B,GACjD,MAAPP,IAAaA,MACjBiB,EAAWZ,EAAeY,EAAUV,EAAS,EAC7C,IAEesB,GAFXhC,EAAOG,EAAI0B,UAAY1B,EAAI0B,QAAU7C,EAAEgB,KAAKG,GAC5C0B,GAAU7B,GAAQG,GAAK0B,OACvBd,EAAQ,CACZ,IAAII,UAAUU,OAAS,EAAG,CACxB,IAAKA,EAAQ,KAAM,IAAIU,WAAUL,EACjCI,GAAOnC,EAAIH,EAAOA,EAAKe,KAAWA,KAEpC,KAAec,EAARd,EAAgBA,IACrBiB,EAAahC,EAAOA,EAAKe,GAASA,EAClCuB,EAAOlB,EAASkB,EAAMnC,EAAI6B,GAAaA,EAAY7B,EAErD,OAAOmC,IAITtD,EAAEwD,YAAcxD,EAAEyD,MAAQ,SAAStC,EAAKiB,EAAUkB,EAAM5B,GAC3C,MAAPP,IAAaA,MACjBiB,EAAWZ,EAAeY,EAAUV,EAAS,EAC7C,IAEIsB,GAFAhC,EAAOG,EAAI0B,UAAa1B,EAAI0B,QAAU7C,EAAEgB,KAAKG,GAC7CY,GAASf,GAAQG,GAAK0B,MAE1B,IAAIV,UAAUU,OAAS,EAAG,CACxB,IAAKd,EAAO,KAAM,IAAIwB,WAAUL,EAChCI,GAAOnC,EAAIH,EAAOA,IAAOe,KAAWA,GAEtC,KAAOA,KACLiB,EAAahC,EAAOA,EAAKe,GAASA,EAClCuB,EAAOlB,EAASkB,EAAMnC,EAAI6B,GAAaA,EAAY7B,EAErD,OAAOmC,IAITtD,EAAE0D,KAAO1D,EAAE2D,OAAS,SAASxC,EAAKyC,EAAWlC,GAC3C,GAAImC,EAQJ,OAPAD,GAAY5D,EAAEoC,SAASwB,EAAWlC,GAClC1B,EAAE8D,KAAK3C,EAAK,SAASS,EAAOG,EAAOgC,GACjC,MAAIH,GAAUhC,EAAOG,EAAOgC,IAC1BF,EAASjC,GACF,GAFT,SAKKiC,GAKT7D,EAAEgE,OAAShE,EAAEiE,OAAS,SAAS9C,EAAKyC,EAAWlC,GAC7C,GAAIuB,KACJ,OAAW,OAAP9B,EAAoB8B,GACxBW,EAAY5D,EAAEoC,SAASwB,EAAWlC,GAClC1B,EAAE0C,KAAKvB,EAAK,SAASS,EAAOG,EAAOgC,GAC7BH,EAAUhC,EAAOG,EAAOgC,IAAOd,EAAQzC,KAAKoB,KAE3CqB,IAITjD,EAAEkE,OAAS,SAAS/C,EAAKyC,EAAWlC,GAClC,MAAO1B,GAAEgE,OAAO7C,EAAKnB,EAAEmE,OAAOnE,EAAEoC,SAASwB,IAAalC,IAKxD1B,EAAEoE,MAAQpE,EAAEqE,IAAM,SAASlD,EAAKyC,EAAWlC,GACzC,GAAW,MAAPP,EAAa,OAAO,CACxByC,GAAY5D,EAAEoC,SAASwB,EAAWlC,EAClC,IAEIK,GAAOiB,EAFPhC,EAAOG,EAAI0B,UAAY1B,EAAI0B,QAAU7C,EAAEgB,KAAKG,GAC5C0B,GAAU7B,GAAQG,GAAK0B,MAE3B,KAAKd,EAAQ,EAAWc,EAARd,EAAgBA,IAE9B,GADAiB,EAAahC,EAAOA,EAAKe,GAASA,GAC7B6B,EAAUzC,EAAI6B,GAAaA,EAAY7B,GAAM,OAAO,CAE3D,QAAO,GAKTnB,EAAE8D,KAAO9D,EAAEsE,IAAM,SAASnD,EAAKyC,EAAWlC,GACxC,GAAW,MAAPP,EAAa,OAAO,CACxByC,GAAY5D,EAAEoC,SAASwB,EAAWlC,EAClC,IAEIK,GAAOiB,EAFPhC,EAAOG,EAAI0B,UAAY1B,EAAI0B,QAAU7C,EAAEgB,KAAKG,GAC5C0B,GAAU7B,GAAQG,GAAK0B,MAE3B,KAAKd,EAAQ,EAAWc,EAARd,EAAgBA,IAE9B,GADAiB,EAAahC,EAAOA,EAAKe,GAASA,EAC9B6B,EAAUzC,EAAI6B,GAAaA,EAAY7B,GAAM,OAAO,CAE1D,QAAO,GAKTnB,EAAEuE,SAAWvE,EAAEwE,QAAU,SAASrD,EAAKsD,GACrC,MAAW,OAAPtD,GAAoB,GACpBA,EAAI0B,UAAY1B,EAAI0B,SAAQ1B,EAAMnB,EAAE0E,OAAOvD,IACxCnB,EAAE2E,QAAQxD,EAAKsD,IAAW,IAInCzE,EAAE4E,OAAS,SAASzD,EAAK0D,GACvB,GAAIC,GAAOrE,EAAMoB,KAAKM,UAAW,GAC7B4C,EAAS/E,EAAEsC,WAAWuC,EAC1B,OAAO7E,GAAE8C,IAAI3B,EAAK,SAASS,GACzB,OAAQmD,EAASF,EAASjD,EAAMiD,IAAS3C,MAAMN,EAAOkD,MAK1D9E,EAAEgF,MAAQ,SAAS7D,EAAK8D,GACtB,MAAOjF,GAAE8C,IAAI3B,EAAKnB,EAAEyC,SAASwC,KAK/BjF,EAAEkF,MAAQ,SAAS/D,EAAKgE,GACtB,MAAOnF,GAAEgE,OAAO7C,EAAKnB,EAAEwC,QAAQ2C,KAKjCnF,EAAEoF,UAAY,SAASjE,EAAKgE,GAC1B,MAAOnF,GAAE0D,KAAKvC,EAAKnB,EAAEwC,QAAQ2C,KAI/BnF,EAAEqF,IAAM,SAASlE,EAAKiB,EAAUV,GAC9B,GACIE,GAAO0D,EADPzB,GAAU0B,IAAUC,GAAgBD,GAExC,IAAgB,MAAZnD,GAA2B,MAAPjB,EAAa,CACnCA,EAAMA,EAAI0B,UAAY1B,EAAI0B,OAAS1B,EAAMnB,EAAE0E,OAAOvD,EAClD,KAAK,GAAIyB,GAAI,EAAGC,EAAS1B,EAAI0B,OAAYA,EAAJD,EAAYA,IAC/ChB,EAAQT,EAAIyB,GACRhB,EAAQiC,IACVA,EAASjC,OAIbQ,GAAWpC,EAAEoC,SAASA,EAAUV,GAChC1B,EAAE0C,KAAKvB,EAAK,SAASS,EAAOG,EAAOgC,GACjCuB,EAAWlD,EAASR,EAAOG,EAAOgC,IAC9BuB,EAAWE,GAAgBF,KAAcC,KAAY1B,KAAY0B,OACnE1B,EAASjC,EACT4D,EAAeF,IAIrB,OAAOzB,IAIT7D,EAAEyF,IAAM,SAAStE,EAAKiB,EAAUV,GAC9B,GACIE,GAAO0D,EADPzB,EAAS0B,IAAUC,EAAeD,GAEtC,IAAgB,MAAZnD,GAA2B,MAAPjB,EAAa,CACnCA,EAAMA,EAAI0B,UAAY1B,EAAI0B,OAAS1B,EAAMnB,EAAE0E,OAAOvD,EAClD,KAAK,GAAIyB,GAAI,EAAGC,EAAS1B,EAAI0B,OAAYA,EAAJD,EAAYA,IAC/ChB,EAAQT,EAAIyB,GACAiB,EAARjC,IACFiC,EAASjC,OAIbQ,GAAWpC,EAAEoC,SAASA,EAAUV,GAChC1B,EAAE0C,KAAKvB,EAAK,SAASS,EAAOG,EAAOgC,GACjCuB,EAAWlD,EAASR,EAAOG,EAAOgC,IACnByB,EAAXF,GAAwCC,MAAbD,GAAoCC,MAAX1B,KACtDA,EAASjC,EACT4D,EAAeF,IAIrB,OAAOzB,IAKT7D,EAAE0F,QAAU,SAASvE,GAInB,IAAK,GAAewE,GAHhBC,EAAMzE,GAAOA,EAAI0B,UAAY1B,EAAI0B,OAAS1B,EAAMnB,EAAE0E,OAAOvD,GACzD0B,EAAS+C,EAAI/C,OACbgD,EAAW3F,MAAM2C,GACZd,EAAQ,EAAiBc,EAARd,EAAgBA,IACxC4D,EAAO3F,EAAE8F,OAAO,EAAG/D,GACf4D,IAAS5D,IAAO8D,EAAS9D,GAAS8D,EAASF,IAC/CE,EAASF,GAAQC,EAAI7D,EAEvB,OAAO8D,IAMT7F,EAAE+F,OAAS,SAAS5E,EAAK6E,EAAGC,GAC1B,MAAS,OAALD,GAAaC,GACX9E,EAAI0B,UAAY1B,EAAI0B,SAAQ1B,EAAMnB,EAAE0E,OAAOvD,IACxCA,EAAInB,EAAE8F,OAAO3E,EAAI0B,OAAS,KAE5B7C,EAAE0F,QAAQvE,GAAKV,MAAM,EAAGyF,KAAKb,IAAI,EAAGW,KAI7ChG,EAAEmG,OAAS,SAAShF,EAAKiB,EAAUV,GAEjC,MADAU,GAAWpC,EAAEoC,SAASA,EAAUV,GACzB1B,EAAEgF,MAAMhF,EAAE8C,IAAI3B,EAAK,SAASS,EAAOG,EAAOgC,GAC/C,OACEnC,MAAOA,EACPG,MAAOA,EACPqE,SAAUhE,EAASR,EAAOG,EAAOgC,MAElCsC,KAAK,SAASC,EAAMC,GACrB,GAAIC,GAAIF,EAAKF,SACTK,EAAIF,EAAMH,QACd,IAAII,IAAMC,EAAG,CACX,GAAID,EAAIC,GAAKD,QAAW,GAAG,MAAO,EAClC,IAAQC,EAAJD,GAASC,QAAW,GAAG,OAAQ,EAErC,MAAOH,GAAKvE,MAAQwE,EAAMxE,QACxB,SAIN,IAAI2E,GAAQ,SAASC,GACnB,MAAO,UAASxF,EAAKiB,EAAUV,GAC7B,GAAImC,KAMJ,OALAzB,GAAWpC,EAAEoC,SAASA,EAAUV,GAChC1B,EAAE0C,KAAKvB,EAAK,SAASS,EAAOG,GAC1B,GAAIkD,GAAM7C,EAASR,EAAOG,EAAOZ,EACjCwF,GAAS9C,EAAQjC,EAAOqD,KAEnBpB,GAMX7D,GAAE4G,QAAUF,EAAM,SAAS7C,EAAQjC,EAAOqD,GACpCjF,EAAE6G,IAAIhD,EAAQoB,GAAMpB,EAAOoB,GAAKzE,KAAKoB,GAAaiC,EAAOoB,IAAQrD,KAKvE5B,EAAE8G,QAAUJ,EAAM,SAAS7C,EAAQjC,EAAOqD,GACxCpB,EAAOoB,GAAOrD,IAMhB5B,EAAE+G,QAAUL,EAAM,SAAS7C,EAAQjC,EAAOqD,GACpCjF,EAAE6G,IAAIhD,EAAQoB,GAAMpB,EAAOoB,KAAapB,EAAOoB,GAAO,IAK5DjF,EAAEgH,YAAc,SAASC,EAAO9F,EAAKiB,EAAUV,GAC7CU,EAAWpC,EAAEoC,SAASA,EAAUV,EAAS,EAGzC,KAFA,GAAIE,GAAQQ,EAASjB,GACjB+F,EAAM,EAAGC,EAAOF,EAAMpE,OACbsE,EAAND,GAAY,CACjB,GAAIE,GAAMF,EAAMC,IAAS,CACrB/E,GAAS6E,EAAMG,IAAQxF,EAAOsF,EAAME,EAAM,EAAQD,EAAOC,EAE/D,MAAOF,IAITlH,EAAEqH,QAAU,SAASlG,GACnB,MAAKA,GACDnB,EAAEc,QAAQK,GAAaV,EAAMoB,KAAKV,GAClCA,EAAI0B,UAAY1B,EAAI0B,OAAe7C,EAAE8C,IAAI3B,EAAKnB,EAAEqC,UAC7CrC,EAAE0E,OAAOvD,OAIlBnB,EAAEsH,KAAO,SAASnG,GAChB,MAAW,OAAPA,EAAoB,EACjBA,EAAI0B,UAAY1B,EAAI0B,OAAS1B,EAAI0B,OAAS7C,EAAEgB,KAAKG,GAAK0B,QAK/D7C,EAAEuH,UAAY,SAASpG,EAAKyC,EAAWlC,GACrCkC,EAAY5D,EAAEoC,SAASwB,EAAWlC,EAClC,IAAI8F,MAAWC,IAIf,OAHAzH,GAAE0C,KAAKvB,EAAK,SAASS,EAAOqD,EAAK9D,IAC9ByC,EAAUhC,EAAOqD,EAAK9D,GAAOqG,EAAOC,GAAMjH,KAAKoB,MAE1C4F,EAAMC,IAShBzH,EAAE0H,MAAQ1H,EAAE2H,KAAO3H,EAAE4H,KAAO,SAASX,EAAOjB,EAAGC,GAC7C,MAAa,OAATgB,MAA2B,GACtB,MAALjB,GAAaC,EAAcgB,EAAM,GAC7B,EAAJjB,KACGvF,EAAMoB,KAAKoF,EAAO,EAAGjB,IAO9BhG,EAAE6H,QAAU,SAASZ,EAAOjB,EAAGC,GAC7B,MAAOxF,GAAMoB,KAAKoF,EAAO,EAAGf,KAAKb,IAAI,EAAG4B,EAAMpE,QAAe,MAALmD,GAAaC,EAAQ,EAAID,MAKnFhG,EAAE8H,KAAO,SAASb,EAAOjB,EAAGC,GAC1B,MAAa,OAATgB,MAA2B,GACtB,MAALjB,GAAaC,EAAcgB,EAAMA,EAAMpE,OAAS,GAC7CpC,EAAMoB,KAAKoF,EAAOf,KAAKb,IAAI4B,EAAMpE,OAASmD,EAAG,KAOtDhG,EAAE+H,KAAO/H,EAAEgI,KAAOhI,EAAEiI,KAAO,SAAShB,EAAOjB,EAAGC,GAC5C,MAAOxF,GAAMoB,KAAKoF,EAAY,MAALjB,GAAaC,EAAQ,EAAID,IAIpDhG,EAAEkI,QAAU,SAASjB,GACnB,MAAOjH,GAAEgE,OAAOiD,EAAOjH,EAAEqC,UAI3B,IAAI8F,GAAU,SAASC,EAAOC,EAASC,EAAQC,GAC7C,GAAIF,GAAWrI,EAAEoE,MAAMgE,EAAOpI,EAAEc,SAC9B,MAAOJ,GAAOwB,MAAMqG,EAAQH,EAE9B,KAAK,GAAIxF,GAAI,EAAGC,EAASuF,EAAMvF,OAAYA,EAAJD,EAAYA,IAAK,CACtD,GAAIhB,GAAQwG,EAAMxF,EACb5C,GAAEc,QAAQc,IAAW5B,EAAEwI,YAAY5G,GAE7ByG,EACT7H,EAAK0B,MAAMqG,EAAQ3G,GAEnBuG,EAAQvG,EAAOyG,EAASC,EAAQC,GAJ3BD,GAAQC,EAAO/H,KAAKoB,GAO7B,MAAO2G,GAITvI,GAAEmI,QAAU,SAASlB,EAAOoB,GAC1B,MAAOF,GAAQlB,EAAOoB,GAAS,OAIjCrI,EAAEyI,QAAU,SAASxB,GACnB,MAAOjH,GAAE0I,WAAWzB,EAAOxG,EAAMoB,KAAKM,UAAW,KAMnDnC,EAAE2I,KAAO3I,EAAE4I,OAAS,SAAS3B,EAAO4B,EAAUzG,EAAUV,GACtD,GAAa,MAATuF,EAAe,QACdjH,GAAE8I,UAAUD,KACfnH,EAAUU,EACVA,EAAWyG,EACXA,GAAW,GAEG,MAAZzG,IAAkBA,EAAWpC,EAAEoC,SAASA,EAAUV,GAGtD,KAAK,GAFDmC,MACAkF,KACKnG,EAAI,EAAGC,EAASoE,EAAMpE,OAAYA,EAAJD,EAAYA,IAAK,CACtD,GAAIhB,GAAQqF,EAAMrE,EAClB,IAAIiG,EACGjG,GAAKmG,IAASnH,GAAOiC,EAAOrD,KAAKoB,GACtCmH,EAAOnH,MACF,IAAIQ,EAAU,CACnB,GAAIkD,GAAWlD,EAASR,EAAOgB,EAAGqE,EAC9BjH,GAAE2E,QAAQoE,EAAMzD,GAAY,IAC9ByD,EAAKvI,KAAK8E,GACVzB,EAAOrD,KAAKoB,QAEL5B,GAAE2E,QAAQd,EAAQjC,GAAS,GACpCiC,EAAOrD,KAAKoB,GAGhB,MAAOiC,IAKT7D,EAAEgJ,MAAQ,WACR,MAAOhJ,GAAE2I,KAAKR,EAAQhG,WAAW,GAAM,QAKzCnC,EAAEiJ,aAAe,SAAShC,GACxB,GAAa,MAATA,EAAe,QAGnB,KAAK,GAFDpD,MACAqF,EAAa/G,UAAUU,OAClBD,EAAI,EAAGC,EAASoE,EAAMpE,OAAYA,EAAJD,EAAYA,IAAK,CACtD,GAAIuG,GAAOlC,EAAMrE,EACjB,KAAI5C,EAAEuE,SAASV,EAAQsF,GAAvB,CACA,IAAK,GAAIC,GAAI,EAAOF,EAAJE,GACTpJ,EAAEuE,SAASpC,UAAUiH,GAAID,GADAC,KAG5BA,IAAMF,GAAYrF,EAAOrD,KAAK2I,IAEpC,MAAOtF,IAKT7D,EAAE0I,WAAa,SAASzB,GACtB,GAAIc,GAAOI,EAAQ1H,EAAMoB,KAAKM,UAAW,IAAI,GAAM,KACnD,OAAOnC,GAAEgE,OAAOiD,EAAO,SAASrF,GAC9B,OAAQ5B,EAAEuE,SAASwD,EAAMnG,MAM7B5B,EAAEqJ,IAAM,SAASpC,GACf,GAAa,MAATA,EAAe,QAGnB,KAAK,GAFDpE,GAAS7C,EAAEqF,IAAIlD,UAAW,UAAUU,OACpCI,EAAU/C,MAAM2C,GACXD,EAAI,EAAOC,EAAJD,EAAYA,IAC1BK,EAAQL,GAAK5C,EAAEgF,MAAM7C,UAAWS,EAElC,OAAOK,IAMTjD,EAAEsJ,OAAS,SAASvF,EAAMW,GACxB,GAAY,MAARX,EAAc,QAElB,KAAK,GADDF,MACKjB,EAAI,EAAGC,EAASkB,EAAKlB,OAAYA,EAAJD,EAAYA,IAC5C8B,EACFb,EAAOE,EAAKnB,IAAM8B,EAAO9B,GAEzBiB,EAAOE,EAAKnB,GAAG,IAAMmB,EAAKnB,GAAG,EAGjC,OAAOiB,IAOT7D,EAAE2E,QAAU,SAASsC,EAAOkC,EAAMN,GAChC,GAAa,MAAT5B,EAAe,OAAQ,CAC3B,IAAIrE,GAAI,EAAGC,EAASoE,EAAMpE,MAC1B,IAAIgG,EAAU,CACZ,GAAuB,gBAAZA,GAIT,MADAjG,GAAI5C,EAAEgH,YAAYC,EAAOkC,GAClBlC,EAAMrE,KAAOuG,EAAOvG,GAAK,CAHhCA,GAAe,EAAXiG,EAAe3C,KAAKb,IAAI,EAAGxC,EAASgG,GAAYA,EAMxD,KAAWhG,EAAJD,EAAYA,IAAK,GAAIqE,EAAMrE,KAAOuG,EAAM,MAAOvG,EACtD,QAAQ,GAGV5C,EAAEuJ,YAAc,SAAStC,EAAOkC,EAAMK,GACpC,GAAa,MAATvC,EAAe,OAAQ,CAC3B,IAAIwC,GAAMxC,EAAMpE,MAIhB,KAHmB,gBAAR2G,KACTC,EAAa,EAAPD,EAAWC,EAAMD,EAAO,EAAItD,KAAKT,IAAIgE,EAAKD,EAAO,MAEhDC,GAAO,GAAG,GAAIxC,EAAMwC,KAASN,EAAM,MAAOM,EACnD,QAAQ,GAMVzJ,EAAE0J,MAAQ,SAASC,EAAOC,EAAMC,GAC1B1H,UAAUU,QAAU,IACtB+G,EAAOD,GAAS,EAChBA,EAAQ,GAEVE,EAAOA,GAAQ,CAKf,KAAK,GAHDhH,GAASqD,KAAKb,IAAIa,KAAK4D,MAAMF,EAAOD,GAASE,GAAO,GACpDH,EAAQxJ,MAAM2C,GAET4G,EAAM,EAAS5G,EAAN4G,EAAcA,IAAOE,GAASE,EAC9CH,EAAMD,GAAOE,CAGf,OAAOD,GAOT,IAAIK,GAAO,YAKX/J,GAAEkB,KAAO,SAASO,EAAMC,GACtB,GAAIoD,GAAMkF,CACV,IAAI/I,GAAcQ,EAAKP,OAASD,EAAY,MAAOA,GAAWiB,MAAMT,EAAMhB,EAAMoB,KAAKM,UAAW,GAChG,KAAKnC,EAAEsC,WAAWb,GAAO,KAAM,IAAI8B,WAAU,oCAW7C,OAVAuB,GAAOrE,EAAMoB,KAAKM,UAAW,GAC7B6H,EAAQ,WACN,KAAMlK,eAAgBkK,IAAQ,MAAOvI,GAAKS,MAAMR,EAASoD,EAAKpE,OAAOD,EAAMoB,KAAKM,YAChF4H,GAAK5J,UAAYsB,EAAKtB,SACtB,IAAI8J,GAAO,GAAIF,EACfA,GAAK5J,UAAY,IACjB,IAAI0D,GAASpC,EAAKS,MAAM+H,EAAMnF,EAAKpE,OAAOD,EAAMoB,KAAKM,YACrD,OAAInC,GAAEuC,SAASsB,GAAgBA,EACxBoG,IAQXjK,EAAEkK,QAAU,SAASzI,GACnB,GAAI0I,GAAY1J,EAAMoB,KAAKM,UAAW,EACtC,OAAO,YAGL,IAAK,GAFDiI,GAAW,EACXtF,EAAOqF,EAAU1J,QACZmC,EAAI,EAAGC,EAASiC,EAAKjC,OAAYA,EAAJD,EAAYA,IAC5CkC,EAAKlC,KAAO5C,IAAG8E,EAAKlC,GAAKT,UAAUiI,KAEzC,MAAOA,EAAWjI,UAAUU,QAAQiC,EAAKtE,KAAK2B,UAAUiI,KACxD,OAAO3I,GAAKS,MAAMpC,KAAMgF,KAO5B9E,EAAEqK,QAAU,SAASlJ,GACnB,GAAIyB,GAA8BqC,EAA3BpC,EAASV,UAAUU,MAC1B,IAAc,GAAVA,EAAa,KAAM,IAAIyH,OAAM,wCACjC,KAAK1H,EAAI,EAAOC,EAAJD,EAAYA,IACtBqC,EAAM9C,UAAUS,GAChBzB,EAAI8D,GAAOjF,EAAEkB,KAAKC,EAAI8D,GAAM9D,EAE9B,OAAOA,IAITnB,EAAEuK,QAAU,SAAS9I,EAAM+I,GACzB,GAAID,GAAU,SAAStF,GACrB,GAAIwF,GAAQF,EAAQE,MAChBC,EAAUF,EAASA,EAAOtI,MAAMpC,KAAMqC,WAAa8C,CAEvD,OADKjF,GAAE6G,IAAI4D,EAAOC,KAAUD,EAAMC,GAAWjJ,EAAKS,MAAMpC,KAAMqC,YACvDsI,EAAMC,GAGf,OADAH,GAAQE,SACDF,GAKTvK,EAAE2K,MAAQ,SAASlJ,EAAMmJ,GACvB,GAAI9F,GAAOrE,EAAMoB,KAAKM,UAAW,EACjC,OAAO0I,YAAW,WAChB,MAAOpJ,GAAKS,MAAM,KAAM4C,IACvB8F,IAKL5K,EAAE8K,MAAQ,SAASrJ,GACjB,MAAOzB,GAAE2K,MAAMzI,MAAMlC,GAAIyB,EAAM,GAAGf,OAAOD,EAAMoB,KAAKM,UAAW,MAQjEnC,EAAE+K,SAAW,SAAStJ,EAAMmJ,EAAMI,GAChC,GAAItJ,GAASoD,EAAMjB,EACfoH,EAAU,KACVC,EAAW,CACVF,KAASA,KACd,IAAIG,GAAQ,WACVD,EAAWF,EAAQI,WAAY,EAAQ,EAAIpL,EAAEqL,MAC7CJ,EAAU,KACVpH,EAASpC,EAAKS,MAAMR,EAASoD,GACxBmG,IAASvJ,EAAUoD,EAAO,MAEjC,OAAO,YACL,GAAIuG,GAAMrL,EAAEqL,KACPH,IAAYF,EAAQI,WAAY,IAAOF,EAAWG,EACvD,IAAIC,GAAYV,GAAQS,EAAMH,EAY9B,OAXAxJ,GAAU5B,KACVgF,EAAO3C,UACU,GAAbmJ,GAAkBA,EAAYV,GAChCW,aAAaN,GACbA,EAAU,KACVC,EAAWG,EACXxH,EAASpC,EAAKS,MAAMR,EAASoD,GACxBmG,IAASvJ,EAAUoD,EAAO,OACrBmG,GAAWD,EAAQQ,YAAa,IAC1CP,EAAUJ,WAAWM,EAAOG,IAEvBzH,IAQX7D,EAAEyL,SAAW,SAAShK,EAAMmJ,EAAMc,GAChC,GAAIT,GAASnG,EAAMpD,EAASiK,EAAW9H,EAEnCsH,EAAQ,WACV,GAAIrD,GAAO9H,EAAEqL,MAAQM,CAEVf,GAAP9C,GAAeA,EAAO,EACxBmD,EAAUJ,WAAWM,EAAOP,EAAO9C,IAEnCmD,EAAU,KACLS,IACH7H,EAASpC,EAAKS,MAAMR,EAASoD,GACxBmG,IAASvJ,EAAUoD,EAAO,QAKrC,OAAO,YACLpD,EAAU5B,KACVgF,EAAO3C,UACPwJ,EAAY3L,EAAEqL,KACd,IAAIO,GAAUF,IAAcT,CAO5B,OANKA,KAASA,EAAUJ,WAAWM,EAAOP,IACtCgB,IACF/H,EAASpC,EAAKS,MAAMR,EAASoD,GAC7BpD,EAAUoD,EAAO,MAGZjB,IAOX7D,EAAE6L,KAAO,SAASpK,EAAMqK,GACtB,MAAO9L,GAAEkK,QAAQ4B,EAASrK,IAI5BzB,EAAEmE,OAAS,SAASP,GAClB,MAAO,YACL,OAAQA,EAAU1B,MAAMpC,KAAMqC,aAMlCnC,EAAE+L,QAAU,WACV,GAAIjH,GAAO3C,UACPwH,EAAQ7E,EAAKjC,OAAS,CAC1B,OAAO,YAGL,IAFA,GAAID,GAAI+G,EACJ9F,EAASiB,EAAK6E,GAAOzH,MAAMpC,KAAMqC,WAC9BS,KAAKiB,EAASiB,EAAKlC,GAAGf,KAAK/B,KAAM+D,EACxC,OAAOA,KAKX7D,EAAEgM,MAAQ,SAASC,EAAOxK,GACxB,MAAO,YACL,QAAMwK,EAAQ,EACLxK,EAAKS,MAAMpC,KAAMqC,WAD1B,SAOJnC,EAAEkM,OAAS,SAASD,EAAOxK,GACzB,GAAI6B,EACJ,OAAO,YAML,QALM2I,EAAQ,EACZ3I,EAAO7B,EAAKS,MAAMpC,KAAMqC,WAExBV,EAAO,KAEF6B,IAMXtD,EAAEmM,KAAOnM,EAAEkK,QAAQlK,EAAEkM,OAAQ,GAO7BlM,EAAEgB,KAAO,SAASG,GAChB,IAAKnB,EAAEuC,SAASpB,GAAM,QACtB,IAAIJ,EAAY,MAAOA,GAAWI,EAClC,IAAIH,KACJ,KAAK,GAAIiE,KAAO9D,GAASnB,EAAE6G,IAAI1F,EAAK8D,IAAMjE,EAAKR,KAAKyE,EACpD,OAAOjE,IAIThB,EAAE0E,OAAS,SAASvD,GAIlB,IAAK,GAHDH,GAAOhB,EAAEgB,KAAKG,GACd0B,EAAS7B,EAAK6B,OACd6B,EAASxE,MAAM2C,GACVD,EAAI,EAAOC,EAAJD,EAAYA,IAC1B8B,EAAO9B,GAAKzB,EAAIH,EAAK4B,GAEvB,OAAO8B,IAIT1E,EAAEoM,MAAQ,SAASjL,GAIjB,IAAK,GAHDH,GAAOhB,EAAEgB,KAAKG,GACd0B,EAAS7B,EAAK6B,OACduJ,EAAQlM,MAAM2C,GACTD,EAAI,EAAOC,EAAJD,EAAYA,IAC1BwJ,EAAMxJ,IAAM5B,EAAK4B,GAAIzB,EAAIH,EAAK4B,IAEhC,OAAOwJ,IAITpM,EAAEqM,OAAS,SAASlL,GAGlB,IAAK,GAFD0C,MACA7C,EAAOhB,EAAEgB,KAAKG,GACTyB,EAAI,EAAGC,EAAS7B,EAAK6B,OAAYA,EAAJD,EAAYA,IAChDiB,EAAO1C,EAAIH,EAAK4B,KAAO5B,EAAK4B,EAE9B,OAAOiB,IAKT7D,EAAEsM,UAAYtM,EAAEuM,QAAU,SAASpL,GACjC,GAAIqL,KACJ,KAAK,GAAIvH,KAAO9D,GACVnB,EAAEsC,WAAWnB,EAAI8D,KAAOuH,EAAMhM,KAAKyE,EAEzC,OAAOuH,GAAMnG,QAIfrG,EAAEyM,OAAS,SAAStL,GAClB,IAAKnB,EAAEuC,SAASpB,GAAM,MAAOA,EAE7B,KAAK,GADDuL,GAAQC,EACH/J,EAAI,EAAGC,EAASV,UAAUU,OAAYA,EAAJD,EAAYA,IAAK,CAC1D8J,EAASvK,UAAUS,EACnB,KAAK+J,IAAQD,GACP9L,EAAeiB,KAAK6K,EAAQC,KAC5BxL,EAAIwL,GAAQD,EAAOC,IAI3B,MAAOxL,IAITnB,EAAE4M,KAAO,SAASzL,EAAKiB,EAAUV,GAC/B,GAAiBuD,GAAbpB,IACJ,IAAW,MAAP1C,EAAa,MAAO0C,EACxB,IAAI7D,EAAEsC,WAAWF,GAAW,CAC1BA,EAAWZ,EAAeY,EAAUV,EACpC,KAAKuD,IAAO9D,GAAK,CACf,GAAIS,GAAQT,EAAI8D,EACZ7C,GAASR,EAAOqD,EAAK9D,KAAM0C,EAAOoB,GAAOrD,QAE1C,CACL,GAAIZ,GAAON,EAAOwB,SAAUzB,EAAMoB,KAAKM,UAAW,GAClDhB,GAAM,GAAId,QAAOc,EACjB,KAAK,GAAIyB,GAAI,EAAGC,EAAS7B,EAAK6B,OAAYA,EAAJD,EAAYA,IAChDqC,EAAMjE,EAAK4B,GACPqC,IAAO9D,KAAK0C,EAAOoB,GAAO9D,EAAI8D,IAGtC,MAAOpB,IAIT7D,EAAE6M,KAAO,SAAS1L,EAAKiB,EAAUV,GAC/B,GAAI1B,EAAEsC,WAAWF,GACfA,EAAWpC,EAAEmE,OAAO/B,OACf,CACL,GAAIpB,GAAOhB,EAAE8C,IAAIpC,EAAOwB,SAAUzB,EAAMoB,KAAKM,UAAW,IAAK2K,OAC7D1K,GAAW,SAASR,EAAOqD,GACzB,OAAQjF,EAAEuE,SAASvD,EAAMiE,IAG7B,MAAOjF,GAAE4M,KAAKzL,EAAKiB,EAAUV,IAI/B1B,EAAE+M,SAAW,SAAS5L,GACpB,IAAKnB,EAAEuC,SAASpB,GAAM,MAAOA,EAC7B,KAAK,GAAIyB,GAAI,EAAGC,EAASV,UAAUU,OAAYA,EAAJD,EAAYA,IAAK,CAC1D,GAAI8J,GAASvK,UAAUS,EACvB,KAAK,GAAI+J,KAAQD,GACXvL,EAAIwL,SAAe,KAAGxL,EAAIwL,GAAQD,EAAOC,IAGjD,MAAOxL,IAITnB,EAAEgN,MAAQ,SAAS7L,GACjB,MAAKnB,GAAEuC,SAASpB,GACTnB,EAAEc,QAAQK,GAAOA,EAAIV,QAAUT,EAAEyM,UAAWtL,GADtBA,GAO/BnB,EAAEiN,IAAM,SAAS9L,EAAK+L,GAEpB,MADAA,GAAY/L,GACLA,EAIT,IAAIgM,GAAK,SAAS3G,EAAGC,EAAG2G,EAAQC,GAG9B,GAAI7G,IAAMC,EAAG,MAAa,KAAND,GAAW,EAAIA,IAAM,EAAIC,CAE7C,IAAS,MAALD,GAAkB,MAALC,EAAW,MAAOD,KAAMC,CAErCD,aAAaxG,KAAGwG,EAAIA,EAAEpF,UACtBqF,YAAazG,KAAGyG,EAAIA,EAAErF,SAE1B,IAAIkM,GAAY3M,EAASkB,KAAK2E,EAC9B,IAAI8G,IAAc3M,EAASkB,KAAK4E,GAAI,OAAO,CAC3C,QAAQ6G,GAEN,IAAK,kBAEL,IAAK,kBAGH,MAAO,GAAK9G,GAAM,GAAKC,CACzB,KAAK,kBAGH,OAAKD,KAAOA,GAAWC,KAAOA,EAEhB,KAAND,EAAU,GAAKA,IAAM,EAAIC,GAAKD,KAAOC,CAC/C,KAAK,gBACL,IAAK,mBAIH,OAAQD,KAAOC,EAEnB,GAAgB,gBAALD,IAA6B,gBAALC,GAAe,OAAO,CAIzD,KADA,GAAI5D,GAASuK,EAAOvK,OACbA,KAGL,GAAIuK,EAAOvK,KAAY2D,EAAG,MAAO6G,GAAOxK,KAAY4D,CAItD,IAAI8G,GAAQ/G,EAAEgH,YAAaC,EAAQhH,EAAE+G,WACrC,IACED,IAAUE,GAEV,eAAiBjH,IAAK,eAAiBC,MACrCzG,EAAEsC,WAAWiL,IAAUA,YAAiBA,IACxCvN,EAAEsC,WAAWmL,IAAUA,YAAiBA,IAE1C,OAAO,CAGTL,GAAO5M,KAAKgG,GACZ6G,EAAO7M,KAAKiG,EACZ,IAAIa,GAAMzD,CAEV,IAAkB,mBAAdyJ,GAIF,GAFAhG,EAAOd,EAAE3D,OACTgB,EAASyD,IAASb,EAAE5D,OAGlB,KAAOyE,MACCzD,EAASsJ,EAAG3G,EAAEc,GAAOb,EAAEa,GAAO8F,EAAQC,WAG3C,CAEL,GAAsBpI,GAAlBjE,EAAOhB,EAAEgB,KAAKwF,EAIlB,IAHAc,EAAOtG,EAAK6B,OAEZgB,EAAS7D,EAAEgB,KAAKyF,GAAG5D,SAAWyE,EAE5B,KAAOA,MAELrC,EAAMjE,EAAKsG,GACLzD,EAAS7D,EAAE6G,IAAIJ,EAAGxB,IAAQkI,EAAG3G,EAAEvB,GAAMwB,EAAExB,GAAMmI,EAAQC,OAOjE,MAFAD,GAAOM,MACPL,EAAOK,MACA7J,EAIT7D,GAAE2N,QAAU,SAASnH,EAAGC,GACtB,MAAO0G,GAAG3G,EAAGC,UAKfzG,EAAE4N,QAAU,SAASzM,GACnB,GAAW,MAAPA,EAAa,OAAO,CACxB,IAAInB,EAAEc,QAAQK,IAAQnB,EAAE6N,SAAS1M,IAAQnB,EAAEwI,YAAYrH,GAAM,MAAsB,KAAfA,EAAI0B,MACxE,KAAK,GAAIoC,KAAO9D,GAAK,GAAInB,EAAE6G,IAAI1F,EAAK8D,GAAM,OAAO,CACjD,QAAO,GAITjF,EAAE8N,UAAY,SAAS3M,GACrB,SAAUA,GAAwB,IAAjBA,EAAI4M,WAKvB/N,EAAEc,QAAUD,GAAiB,SAASM,GACpC,MAA8B,mBAAvBR,EAASkB,KAAKV,IAIvBnB,EAAEuC,SAAW,SAASpB,GACpB,GAAI6M,SAAc7M,EAClB,OAAgB,aAAT6M,GAAgC,WAATA,KAAuB7M,GAIvDnB,EAAE0C,MAAM,YAAa,WAAY,SAAU,SAAU,OAAQ,UAAW,SAASuL,GAC/EjO,EAAE,KAAOiO,GAAQ,SAAS9M,GACxB,MAAOR,GAASkB,KAAKV,KAAS,WAAa8M,EAAO,OAMjDjO,EAAEwI,YAAYrG,aACjBnC,EAAEwI,YAAc,SAASrH,GACvB,MAAOnB,GAAE6G,IAAI1F,EAAK,YAKH,kBAAR,MACTnB,EAAEsC,WAAa,SAASnB,GACtB,MAAqB,kBAAPA,KAAqB,IAKvCnB,EAAEkO,SAAW,SAAS/M,GACpB,MAAO+M,UAAS/M,KAASgN,MAAMC,WAAWjN,KAI5CnB,EAAEmO,MAAQ,SAAShN,GACjB,MAAOnB,GAAEqO,SAASlN,IAAQA,KAASA,GAIrCnB,EAAE8I,UAAY,SAAS3H,GACrB,MAAOA,MAAQ,GAAQA,KAAQ,GAAgC,qBAAvBR,EAASkB,KAAKV,IAIxDnB,EAAEsO,OAAS,SAASnN,GAClB,MAAe,QAARA,GAITnB,EAAEuO,YAAc,SAASpN,GACvB,MAAOA,SAAa,IAKtBnB,EAAE6G,IAAM,SAAS1F,EAAK8D,GACpB,MAAc,OAAP9D,GAAeP,EAAeiB,KAAKV,EAAK8D,IAQjDjF,EAAEwO,WAAa,WAEb,MADA3O,GAAKG,EAAID,EACFD,MAITE,EAAEqC,SAAW,SAAST,GACpB,MAAOA,IAGT5B,EAAEyO,SAAW,SAAS7M,GACpB,MAAO,YACL,MAAOA,KAIX5B,EAAE0O,KAAO,aAET1O,EAAEyC,SAAW,SAASwC,GACpB,MAAO,UAAS9D,GACd,MAAOA,GAAI8D,KAKfjF,EAAEwC,QAAU,SAAS2C,GACnB,GAAIiH,GAAQpM,EAAEoM,MAAMjH,GAAQtC,EAASuJ,EAAMvJ,MAC3C,OAAO,UAAS1B,GACd,GAAW,MAAPA,EAAa,OAAQ0B,CACzB1B,GAAM,GAAId,QAAOc,EACjB,KAAK,GAAIyB,GAAI,EAAOC,EAAJD,EAAYA,IAAK,CAC/B,GAAI+L,GAAOvC,EAAMxJ,GAAIqC,EAAM0J,EAAK,EAChC,IAAIA,EAAK,KAAOxN,EAAI8D,MAAUA,IAAO9D,IAAM,OAAO,EAEpD,OAAO,IAKXnB,EAAEiM,MAAQ,SAASjG,EAAG5D,EAAUV,GAC9B,GAAIkN,GAAQ1O,MAAMgG,KAAKb,IAAI,EAAGW,GAC9B5D,GAAWZ,EAAeY,EAAUV,EAAS,EAC7C,KAAK,GAAIkB,GAAI,EAAOoD,EAAJpD,EAAOA,IAAKgM,EAAMhM,GAAKR,EAASQ,EAChD,OAAOgM,IAIT5O,EAAE8F,OAAS,SAASL,EAAKJ,GAKvB,MAJW,OAAPA,IACFA,EAAMI,EACNA,EAAM,GAEDA,EAAMS,KAAK2I,MAAM3I,KAAKJ,UAAYT,EAAMI,EAAM,KAIvDzF,EAAEqL,IAAMyD,KAAKzD,KAAO,WAClB,OAAO,GAAIyD,OAAOC,UAIpB,IAAIC,IACFC,IAAK,QACLC,IAAK,OACLC,IAAK,OACLC,IAAK,SACLC,IAAK,SACLC,IAAK,UAEHC,EAAcvP,EAAEqM,OAAO2C,GAGvBQ,EAAgB,SAAS1M,GAC3B,GAAI2M,GAAU,SAASC,GACrB,MAAO5M,GAAI4M,IAGThD,EAAS,MAAQ1M,EAAEgB,KAAK8B,GAAK6M,KAAK,KAAO,IACzCC,EAAaC,OAAOnD,GACpBoD,EAAgBD,OAAOnD,EAAQ,IACnC,OAAO,UAASqD,GAEd,MADAA,GAAmB,MAAVA,EAAiB,GAAK,GAAKA,EAC7BH,EAAWI,KAAKD,GAAUA,EAAOE,QAAQH,EAAeL,GAAWM,GAG9E/P,GAAEkQ,OAASV,EAAcR,GACzBhP,EAAEmQ,SAAWX,EAAcD,GAI3BvP,EAAE6D,OAAS,SAASyF,EAAQ7G,GAC1B,GAAc,MAAV6G,EAAgB,WAAY,EAChC,IAAI1H,GAAQ0H,EAAO7G,EACnB,OAAOzC,GAAEsC,WAAWV,GAAS0H,EAAO7G,KAAcb,EAKpD,IAAIwO,GAAY,CAChBpQ,GAAEqQ,SAAW,SAASC,GACpB,GAAIC,KAAOH,EAAY,EACvB,OAAOE,GAASA,EAASC,EAAKA,GAKhCvQ,EAAEwQ,kBACAC,SAAc,kBACdC,YAAc,mBACdR,OAAc,mBAMhB,IAAIS,GAAU,OAIVC,GACFvB,IAAU,IACVwB,KAAU,KACVC,KAAU,IACVC,KAAU,IACVC,SAAU,QACVC,SAAU,SAGRxB,EAAU,4BAEVyB,EAAa,SAASxB,GACxB,MAAO,KAAOkB,EAAQlB,GAOxB1P,GAAEmR,SAAW,SAASC,EAAMC,EAAUC,IAC/BD,GAAYC,IAAaD,EAAWC,GACzCD,EAAWrR,EAAE+M,YAAasE,EAAUrR,EAAEwQ,iBAGtC,IAAIe,GAAU1B,SACXwB,EAASnB,QAAUS,GAASjE,QAC5B2E,EAASX,aAAeC,GAASjE,QACjC2E,EAASZ,UAAYE,GAASjE,QAC/BiD,KAAK,KAAO,KAAM,KAGhB5N,EAAQ,EACR2K,EAAS,QACb0E,GAAKnB,QAAQsB,EAAS,SAAS7B,EAAOQ,EAAQQ,EAAaD,EAAUe,GAanE,MAZA9E,IAAU0E,EAAK3Q,MAAMsB,EAAOyP,GAAQvB,QAAQR,EAASyB,GACrDnP,EAAQyP,EAAS9B,EAAM7M,OAEnBqN,EACFxD,GAAU,cAAgBwD,EAAS,iCAC1BQ,EACThE,GAAU,cAAgBgE,EAAc,uBAC/BD,IACT/D,GAAU,OAAS+D,EAAW,YAIzBf,IAEThD,GAAU,OAGL2E,EAASI,WAAU/E,EAAS,mBAAqBA,EAAS,OAE/DA,EAAS,2CACP,oDACAA,EAAS,eAEX,KACE,GAAIgF,GAAS,GAAInR,UAAS8Q,EAASI,UAAY,MAAO,IAAK/E,GAC3D,MAAOiF,GAEP,KADAA,GAAEjF,OAASA,EACLiF,EAGR,GAAIR,GAAW,SAASS,GACtB,MAAOF,GAAO7P,KAAK/B,KAAM8R,EAAM5R,IAI7B6R,EAAWR,EAASI,UAAY,KAGpC,OAFAN,GAASzE,OAAS,YAAcmF,EAAW,OAASnF,EAAS,IAEtDyE,GAITnR,EAAE8R,MAAQ,SAAS3Q,GACjB,GAAI4Q,GAAW/R,EAAEmB,EAEjB,OADA4Q,GAASC,QAAS,EACXD,EAUT,IAAIlO,GAAS,SAAS1C,GACpB,MAAOrB,MAAKkS,OAAShS,EAAEmB,GAAK2Q,QAAU3Q,EAIxCnB,GAAEiS,MAAQ,SAAS9Q,GACjBnB,EAAE0C,KAAK1C,EAAEsM,UAAUnL,GAAM,SAAS8M,GAChC,GAAIxM,GAAOzB,EAAEiO,GAAQ9M,EAAI8M,EACzBjO,GAAEG,UAAU8N,GAAQ,WAClB,GAAInJ,IAAQhF,KAAKsB,SAEjB,OADAZ,GAAK0B,MAAM4C,EAAM3C,WACV0B,EAAOhC,KAAK/B,KAAM2B,EAAKS,MAAMlC,EAAG8E,QAM7C9E,EAAEiS,MAAMjS,GAGRA,EAAE0C,MAAM,MAAO,OAAQ,UAAW,QAAS,OAAQ,SAAU,WAAY,SAASuL,GAChF,GAAIpJ,GAAS5E,EAAWgO,EACxBjO,GAAEG,UAAU8N,GAAQ,WAClB,GAAI9M,GAAMrB,KAAKsB,QAGf,OAFAyD,GAAO3C,MAAMf,EAAKgB,WACJ,UAAT8L,GAA6B,WAATA,GAAqC,IAAf9M,EAAI0B,cAAqB1B,GAAI,GACrE0C,EAAOhC,KAAK/B,KAAMqB,MAK7BnB,EAAE0C,MAAM,SAAU,OAAQ,SAAU,SAASuL,GAC3C,GAAIpJ,GAAS5E,EAAWgO,EACxBjO,GAAEG,UAAU8N,GAAQ,WAClB,MAAOpK,GAAOhC,KAAK/B,KAAM+E,EAAO3C,MAAMpC,KAAKsB,SAAUe,eAKzDnC,EAAEG,UAAUyB,MAAQ,WAClB,MAAO9B,MAAKsB,UAUQ,kBAAX8Q,SAAyBA,OAAOC,KACzCD,OAAO,gBAAkB,WACvB,MAAOlS,OAGX6B,KAAK/B"} -------------------------------------------------------------------------------- /var_nginx/content/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.1",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.1",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.1",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c="prev"==a?-1:1,d=this.getItemIndex(b),e=(d+c)%this.$items.length;return this.$items.eq(e)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i="next"==b?"first":"last",j=this;if(!f.length){if(!this.options.wrap)return;f=this.$element.find(".item")[i]()}if(f.hasClass("active"))return this.sliding=!1;var k=f[0],l=a.Event("slide.bs.carousel",{relatedTarget:k,direction:h});if(this.$element.trigger(l),!l.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var m=a(this.$indicators.children()[this.getItemIndex(f)]);m&&m.addClass("active")}var n=a.Event("slid.bs.carousel",{relatedTarget:k,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),j.sliding=!1,setTimeout(function(){j.$element.trigger(n)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(n)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.1",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.find("> .panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.1",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('