Software /
code /
prosody-modules
File
mod_slack_webhooks/mod_slack_webhooks.lua @ 4515:2e33eeafe962
mod_muc_markers: Prevent any markers from reaching the archive, even if untracked
Original intention was to leave alone things that this module isn't
handling. However markers in archives are just problematic without
more advanced logic about what is markable and what is not. It also
requires a more advanced query in mod_muc_rai to determine the latest
markable message instead of the latest archived message.
I'd rather keep the "is archivable" and "is markable" definition the
same for simplicity. I don't want to introduce yet another set of rules
for no reason.
No markers in MAM.
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Mon, 22 Mar 2021 15:55:02 +0000 |
parent | 3443:284d8c18060e |
child | 4560:724003f24308 |
line wrap: on
line source
-- Allow Slack-style incoming and outgoing hooks to MUC rooms -- Based on mod_muc_intercom and mod_post_msg -- Copyright 2016-2017 Nathan Whitehorn <nwhitehorn@physics.ucla.edu> -- -- This file is MIT/X11 licensed. module:depends"http" local msg = require "util.stanza".message; local jid = require "util.jid"; local now = require "util.datetime".datetime; local json = require "util.json" local formdecode = require "net.http".formdecode; local http = require "net.http"; local dataform = require "util.dataforms"; local mod_muc = module:depends"muc"; local rooms = rawget(mod_muc, "rooms"); local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or function (room_jid) return rooms[room_jid]; end local button_ns = "xmpp:prosody.im/community/mod_slack_webhooks#buttons"; local routing = module:get_option("outgoing_webhook_routing") or {}; local listen_path = module:get_option("incoming_webhook_path") or "/webhook"; local default_from_nick = module:get_option("incoming_webhook_default_nick") or "Bot"; function postcallback(_, code) module:log("debug", "HTTP result %d", code) end function check_message(data) local stanza = data.stanza; local this_room = get_room_from_jid(stanza.attr.to); if not this_room then return; end -- no such room local from_room_jid = this_room._jid_nick[stanza.attr.from]; if not from_room_jid then return; end -- no such nick local from_room, from_host, from_nick = jid.split(from_room_jid); local body = stanza:get_child("body"); if not body then return; end -- No body, like topic changes body = body and body:get_text(); -- I feel like I want to do `or ""` there :/ if not routing[from_room] then return; end local json_out = { channel_name = from_room, timestamp = now(), text = body, team_domain = from_host, user_name = from_nick, }; local form = stanza:get_child("x", "jabber:x:form"); if form and form.attr.type == "submit" then local callback_id, button_name, button_value; for field in form:childtags("field") do if field.attr.var == "callback_id" then button_name = field:get_child_text("text"); elseif field.attr.var == "button_name" then button_name = field:get_child_text("text"); elseif field.attr.var ~= "FORM_TYPE" or field:get_child_text("text") ~= button_ns then callback_id, button_name, button_value = nil, nil, nil; break; end end if callback_id and button_name and button_value then json_out.callback_id = callback_id; json_out.actions = { { type = "button", name = button_name, value = button_value } }; end end local stanzaid = stanza:get_child("id"); if stanzaid and string.sub(stanzaid,1,string.len("webhookbot"))=="webhookbot" then json_out["bot_id"] = "webhookbot"; end json_out = json.encode(json_out) local url = routing[from_room]; module:log("debug", "message from %s in %s to %s", from_nick, from_room, url); if url == "DEBUG" then module:log("debug", "json_out = %s", json_out); return; end local headers = { ["Content-Type"] = "application/json", }; http.request(url, { method = "POST", body = json_out, headers = headers }, postcallback) end module:hook("message/bare", check_message, 10); local function route_post(f) return function(event, path) local bare_room = jid.join(path, module.host); if not get_room_from_jid(bare_room) then module:log("warn", "mod_slack_webhook: invalid JID: %s", bare_room); return 404; end -- Check secret? return f(event, path) end end local function handle_post(event, path) local request = event.request; local headers = request.headers; local body_type = headers.content_type; local post_body; if body_type == "application/x-www-form-urlencoded" then post_body = formdecode(request.body); elseif body_type == "application/json" then post_body = json.decode(request.body) if not post_body then return 420; end else return 422; end local bare_room = jid.join(path, module.host); local dest_room = get_room_from_jid(bare_room); local from_nick = default_from_nick; if post_body["username"] then from_nick = post_body["username"]; end local sender = jid.join(path, module.host, from_nick); module:log("debug", "message to %s from %s", bare_room, sender); module:log("debug", "body: %s", post_body["text"]); local message = msg({ to = bare_room, from = sender, type = "groupchat", id="webhookbot" .. now()},post_body["text"]); if type(post_body["attachments"]) == "table" then -- Buttons? -- luacheck: ignore 631 -- defensive against JSON having whatever data in it, enjoy for _, attachment in ipairs(post_body["attachments"]) do if type(attachment) == "table" and type(attachment.actions) == "table" and type(attachment.callback_id) == "string" then local buttons = {}; local button_name; for _, action in ipairs(attachment.actions) do if type(attachment.text) == "string" then buttons.label = attachment.text; end if type(action) == "table" and action.type == "button" and type(action.name) == "string" and type(action.value) == "string" then if not button_name then button_name = action.name; end if button_name == action.name then local button = { value = action.value; }; if type(action.text) == "string" then button.label = action.text; end table.insert(buttons, button); end end end if button_name then message:add_direct_child(dataform.new({ { type = "hidden", name = "FORM_TYPE", value = button_ns, }, { type = "hidden", name = "callback_id", value = attachment.callback_id, }, { type = "hidden", name = "button_name", value = button_name, }, { type = "list-single", name = "buttons", value = "", -- FIXME util.dataforms can't do options without a value options = buttons; } }):form()); break; end end end end dest_room:broadcast_message(message, true); return 201; end module:provides("http", { default_path = listen_path; route = { ["POST /*"] = route_post(handle_post); OPTIONS = function(e) local headers = e.response.headers; headers.allow = "POST"; headers.accept = "application/x-www-form-urlencoded, application/json"; return 200; end; } });