view mod_slack_webhooks/mod_slack_webhooks.lua @ 6403:13c61a068c71

mod_pubsub_serverinfo: Include hostname in default pubsub node name This change ensures that every host publishes to a unique node by default, which is important when publishing info about multiple hosts. The old default node name was "serverinfo" The new default node name is in the format "serverinfo/example.com" where example.com is automatically replaced by the publishing host's name. This affects existing deployments in the following ways: - If you already configured pubsub_serverinfo_node explicitly in your config, then nothing will change, your current configuration will still be used. - If you did not specify it in your config (i.e. used the default) then the module will switch to publishing to the new node. If your configuration is correct this shouldn't be noticed, because the new node will automatically be created and configured by the module. If you want, you can clean up the old 'serverinfo' node using: prosodyctl shell pubsub delete_node pubsub.example.com serverinfo
author Matthew Wild <mwild1@gmail.com>
date Wed, 11 Feb 2026 09:40:30 +0000
parents c6b740ccf6ec
children
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 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 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 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"]);
	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;
	}
});