| 1 | local verse = require "verse"; |
| 2 | local st = require "prosody.util.stanza"; |
| 3 | |
| 4 | -- Global, so that plugins can add themselves to riddim.plugins |
| 5 | riddim = { plugins = {} }; |
| 6 | |
| 7 | local riddim_mt = {}; |
| 8 | riddim_mt.__index = riddim_mt; |
| 9 | |
| 10 | function riddim.new(stream, config) |
| 11 | if not stream then |
| 12 | error("riddim.new(): Verse stream required as first parameter", 2); |
| 13 | end |
| 14 | return setmetatable({ stream = stream, config = config or {}, plugins = {} }, riddim_mt); |
| 15 | end |
| 16 | |
| 17 | -- self.conn is ready for stanzas |
| 18 | function riddim_mt:start() |
| 19 | self.stream:hook("stanza", function (stanza) |
| 20 | local body = stanza:get_child("body"); |
| 21 | local event = { |
| 22 | sender = { jid = stanza.attr.from }; |
| 23 | body = (body and body:get_text()) or nil; |
| 24 | stanza = stanza; |
| 25 | }; |
| 26 | if stanza.name == "message" then |
| 27 | local replied; |
| 28 | local bot = self; |
| 29 | function event:reply(reply) |
| 30 | if replied then return false; end |
| 31 | replied = true; |
| 32 | return bot:send_message(stanza.attr.from, stanza.attr.type, reply); |
| 33 | end |
| 34 | end |
| 35 | local ret; |
| 36 | if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then |
| 37 | local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; |
| 38 | if xmlns then |
| 39 | event.xmlns = xmlns; |
| 40 | ret = self:event("iq/"..xmlns, event); |
| 41 | end |
| 42 | end |
| 43 | if not ret then |
| 44 | ret = self:event(stanza.name, event); |
| 45 | end |
| 46 | if ret and type(ret) == "table" and ret.name then |
| 47 | self:send(ret); |
| 48 | end |
| 49 | return ret; |
| 50 | end, 1); |
| 51 | self:event("started"); |
| 52 | end |
| 53 | |
| 54 | function riddim_mt:send(s) |
| 55 | return self.stream:send(s); |
| 56 | end |
| 57 | |
| 58 | function riddim_mt:send_iq(s, callback, errback) |
| 59 | return self.stream:send_iq(s, callback, errback); |
| 60 | end |
| 61 | |
| 62 | function riddim_mt:event(name, ...) |
| 63 | return self.stream:event("bot/"..name, ...); |
| 64 | end |
| 65 | |
| 66 | function riddim_mt:hook(name, ...) |
| 67 | return self.stream:hook("bot/"..name, ...); |
| 68 | end |
| 69 | |
| 70 | function riddim_mt:send_message(to, type, text) |
| 71 | self:send(st.message({ to = to, type = type }):tag("body"):text(text)); |
| 72 | end |
| 73 | |
| 74 | function riddim_mt:send_presence(to, type) |
| 75 | self:send(st.presence({ to = to, type = type })); |
| 76 | end |
| 77 | |
| 78 | function riddim_mt:add_plugin(name) |
| 79 | if not self.plugins[name] then |
| 80 | self.plugins[name] = require("riddim.plugins."..name); |
| 81 | return riddim.plugins[name](self); |
| 82 | end |
| 83 | end |
| 84 | |
| 85 | function riddim_mt:http_request(u, opt, cb) |
| 86 | local http = require "prosody.net.http"; |
| 87 | opt = opt or {}; |
| 88 | opt.sslctx = opt.sslctx or { |
| 89 | mode = "client"; |
| 90 | protocol = "sslv23"; |
| 91 | capath = self.config.capath or "/etc/ssl/certs"; |
| 92 | cafile = self.config.cafile; |
| 93 | verify = "peer"; |
| 94 | }; |
| 95 | return http.request(u, opt, cb); |
| 96 | end |
| 97 | |
| 98 | -- Built-in bot starter |
| 99 | if not (... and package.loaded[...] ~= nil) then |
| 100 | require "verse.client"; |
| 101 | |
| 102 | -- Config loading |
| 103 | local config = setmetatable({}, { __index = _G }); |
| 104 | local chunk, err = loadfile(arg[1] or "config.lua", "t", config); |
| 105 | if not chunk then |
| 106 | print("File or syntax error:", err); |
| 107 | return 1; |
| 108 | end |
| 109 | |
| 110 | local ok, err = pcall(chunk); |
| 111 | if not ok then |
| 112 | print("Error while processing config:", err); |
| 113 | return 1; |
| 114 | end |
| 115 | setmetatable(config, nil); |
| 116 | |
| 117 | if not config.jid then |
| 118 | io.write("Enter the bot's JID: "); |
| 119 | config.jid = io.read("*l"); |
| 120 | end |
| 121 | |
| 122 | if not config.password then |
| 123 | io.write("Enter the password for "..config.jid..": "); |
| 124 | config.password = io.read("*l"); |
| 125 | end |
| 126 | |
| 127 | -- Initialize the HTTP client |
| 128 | local http = require "prosody.net.http"; |
| 129 | http.default.options.sslctx = { |
| 130 | mode = "client"; |
| 131 | protocol = "sslv23"; |
| 132 | capath = config.capath or "/etc/ssl/certs"; |
| 133 | cafile = config.cafile; |
| 134 | verify = "peer"; |
| 135 | }; |
| 136 | |
| 137 | -- Create the stream object and bot object |
| 138 | local c = verse.new(); |
| 139 | local b = riddim.new(c, config); |
| 140 | |
| 141 | if config.debug then |
| 142 | c:hook("incoming-raw", print); |
| 143 | c:hook("outgoing-raw", print); |
| 144 | verse.set_log_handler(print); |
| 145 | else |
| 146 | verse.set_log_handler(print, {"info","warn","error"}); |
| 147 | end |
| 148 | |
| 149 | for _, plugin in ipairs(config.plugins or {}) do |
| 150 | b:add_plugin(plugin); |
| 151 | end |
| 152 | |
| 153 | for _, plugin in ipairs(config.stream_plugins or {}) do |
| 154 | c:add_plugin(plugin); |
| 155 | end |
| 156 | |
| 157 | b:hook("started", function () |
| 158 | local presence = verse.presence() |
| 159 | if b.caps then |
| 160 | presence:add_child(b:caps()) |
| 161 | end |
| 162 | b:send(presence); |
| 163 | end); |
| 164 | |
| 165 | c:hook("ready", function () b:start(); end); |
| 166 | |
| 167 | c:hook("stream-error", function () |
| 168 | print("Stream closed with error, riddim will exit now"); |
| 169 | os.exit(2); |
| 170 | end); |
| 171 | |
| 172 | local shutting_down = false; |
| 173 | c:hook("disconnected", function () |
| 174 | if shutting_down then |
| 175 | os.exit(0); |
| 176 | end |
| 177 | print("Disconnected from server, riddim will exit now"); |
| 178 | os.exit(1); |
| 179 | end, -1); |
| 180 | |
| 181 | -- Graceful shutdown on SIGTERM (docker stop) and SIGINT (Ctrl-C): |
| 182 | -- close the stream cleanly so the server sends unavailable presence |
| 183 | -- and MUCs see the bot leave, then exit once the server closes the |
| 184 | -- connection (or after a short deadline). |
| 185 | local have_signal, signal = pcall(require, "prosody.util.signal"); |
| 186 | local hook_signal = have_signal and verse.server.hook_signal; |
| 187 | if hook_signal then |
| 188 | local function shutdown(signame) |
| 189 | if shutting_down then return; end |
| 190 | shutting_down = true; |
| 191 | print("Received "..signame..", riddim will exit now"); |
| 192 | if c.conn then |
| 193 | c:close("shutdown"); |
| 194 | verse.add_task(5, function () os.exit(0); end); |
| 195 | else |
| 196 | os.exit(0); |
| 197 | end |
| 198 | end |
| 199 | for _, signame in ipairs({ "SIGTERM", "SIGINT" }) do |
| 200 | hook_signal(signal[signame], function () shutdown(signame); end); |
| 201 | end |
| 202 | end |
| 203 | |
| 204 | if config.connect_host then |
| 205 | c.connect_host = config.connect_host; |
| 206 | end |
| 207 | if config.connect_port then |
| 208 | c.connect_port = config.connect_port; |
| 209 | end |
| 210 | |
| 211 | c:connect_client(config.jid, config.password); |
| 212 | |
| 213 | verse.loop(); |
| 214 | end |
| 215 | |
| 216 | return riddim; |