# HG changeset patch # User Nathan Whitehorn # Date 1523807143 25200 # Node ID 02fc3b64cbb757bbdb9922576f58dee418c76c23 # Parent d631fd9a3300dce661783e0afb0739e44101ad95 Initial commit of mod_slack_webhooks. This provides an HTTP-based interface to and from Prosody-hosted MUCs equivalent to Slack's incoming and outgoing webhook interfaces, allowing a variety of Slack integrations to be used with a Prosody MUC. diff -r d631fd9a3300 -r 02fc3b64cbb7 mod_slack_webhooks/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_slack_webhooks/README.markdown Sun Apr 15 08:45:43 2018 -0700 @@ -0,0 +1,87 @@ +--- +labels: +- 'Stage-Alpha' +summary: 'Allow Slack integrations to work with Prosody MUCs' +... + +Introduction +============ + +This module provides a Slack-compatible "web hook" interface to Prosody MUCs. +Both "incoming" web hooks, which allow Slack integrations to post messages +to Prosody MUCs, and "outgoing" web hooks, which copy messages from Prosody +MUCs to Slack-style integrations by HTTP, are supported. This can also be +used, in conjunction with various Slack inter-namespace bridging tools, to +provide a bidirectional bridge between a Prosody-hosted XMPP MUC and a Slack +channel. + +Usage +===== + +First copy the module to the prosody plugins directory. + +Then add "slack\_webhooks" to your modules\_enabled list: + +``` {.lua} +Component "conference.example.org" "muc" +modules_enabled = { + "slack_webhooks", +} +``` + +Configuration +============= + +The normal use for this module is to provide an incoming webhook to allow +integrations to post to prosody MUCs: + +``` {.lua} +incoming_webhook_path = "/msg/DFSDF56587658765NBDSA" +default_from_nick = "Bot" -- Unless otherwise specified, posts as "Bot" +``` + +This allows Slack-style JSON messages posted to http://conference.example.org/msg/DFSDF56587658765NBDSA/chat to appear in the MUC chat@conference.example.org. A username field in the message is honored as the nick attached to the message; if no username is specified, the message will use the value of default_from_nick. +Specifying a string of random gibberish in the URL is important to prevent spam. + +In addition, there is a second operating mode equivalent to Slack's outgoing +webhooks. This allows all messages from a set of specified chat rooms to be +routed to an external server over HTTP in the format used by Slack's +outgoing webhooks. +``` {.lua} +outgoing_webhook_routing = { + -- Send all messages from chat@conference.example.org to + -- a web server. + ["chat"] = "http://example.org/cgi-bin/messagedest", +} +``` + +Known Issues +============ + +The users from whom messages delivered from integrations are apparently +delivered are not, in general, members of the MUC. Other prosody modules +that try to look up information about the users who most messages, mostly +logging modules, may become confused and fail (clients all work fine because +replayed history also can come from non-present users). In at least some cases, +such as with mod_muc_mam, this can be fixed by hiding the JIDs of the +participants in the room configuration. + +There are a few smaller UI issues: + +* If an integration posts with the same username as a room member, there is + no indication (like Slack's [bot] suffix) that the message is not from that + room member. +* It is not currently possible to prevent posting to some MUCs (this is + also true of Slack). +* It should be possible to set the webhook configuration for a room in the + room configuration rather than statically in Prosody's configuration file. + +Compatibility +============= + + ------- ----------------- + trunk Untested + 0.10 Works + 0.9 Works + ------- ----------------- + diff -r d631fd9a3300 -r 02fc3b64cbb7 mod_slack_webhooks/mod_slack_webhooks.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_slack_webhooks/mod_slack_webhooks.lua Sun Apr 15 08:45:43 2018 -0700 @@ -0,0 +1,132 @@ +-- Allow Slack-style incoming and outgoing hooks to MUC rooms +-- Based on mod_muc_intercom and mod_post_msg +-- Copyright 2016-2017 Nathan Whitehorn +-- +-- This file is MIT/X11 licensed. + +module:depends"http" + +local host_session = prosody.hosts[module.host]; +local msg = require "util.stanza".message; +local jid = require "util.jid"; +local now = require "util.datetime".datetime; +local b64_decode = require "util.encodings".base64.decode; +local json = require "util.json" +local formdecode = require "net.http".formdecode; +local xml = require "util.xml"; +local http = require "net.http"; + +local function get_room_by_jid(mod_muc, jid) + if mod_muc.get_room_by_jid then + return mod_muc.get_room_by_jid(jid); + elseif mod_muc.rooms then + return mod_muc.rooms[jid]; -- COMPAT 0.9, 0.10 + end +end + +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(content, code) + module:log("debug", "HTTP result %d", code) +end + +function check_message(data) + local origin, stanza = data.origin, data.stanza; + local mod_muc = host_session.muc; + if not mod_muc then return; end + + local this_room = get_room_by_jid(mod_muc, 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 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); + 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 request = event.request; + local headers = request.headers; + local bare_room = jid.join(path, module.host); + local mod_muc = host_session.muc; + if not get_room_by_jid(mod_muc, 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 mod_muc = host_session.muc; + local request = event.request; + local response = event.response; + local headers = request.headers; + + local body_type = headers.content_type; + local message; + local post_body; + if body_type == "application/x-www-form-urlencoded" then + post_body = formdecode(request.body)["payload"]; + elseif body_type == "application/json" then + if not pcall(function() post_body = json.decode(request.body) end) then + return 420; + end + else + return 422; + end + local bare_room = jid.join(path, module.host); + local dest_room = get_room_by_jid(mod_muc, 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"]); + message = msg({ to = bare_room, from = sender, type = "groupchat", id="webhookbot" .. now()},post_body["text"]); + 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; + } +}); +