1 -- Allow Slack-style incoming and outgoing hooks to MUC rooms
2 -- Based on mod_muc_intercom and mod_post_msg
3 -- Copyright 2016-2017 Nathan Whitehorn <nwhitehorn@physics.ucla.edu>
5 -- This file is MIT/X11 licensed.
9 local msg
= require
"util.stanza".message
;
10 local jid
= require
"util.jid";
11 local now
= require
"util.datetime".datetime
;
12 local json
= require
"util.json"
13 local formdecode
= require
"net.http".formdecode
;
14 local http
= require
"net.http";
15 local dataform
= require
"util.dataforms";
17 local mod_muc
= module
:depends
"muc";
18 local rooms
= rawget(mod_muc
, "rooms");
19 local get_room_from_jid
= rawget(mod_muc
, "get_room_from_jid") or
21 return rooms
[room_jid
];
24 local button_ns
= "xmpp:prosody.im/community/mod_slack_webhooks#buttons";
25 local routing
= module
:get_option("outgoing_webhook_routing") or {};
26 local listen_path
= module
:get_option("incoming_webhook_path") or "/webhook";
27 local default_from_nick
= module
:get_option("incoming_webhook_default_nick") or "Bot";
29 function postcallback(_
, code
)
30 module
:log("debug", "HTTP result %d", code
)
33 function check_message(data
)
34 local stanza
= data
.stanza
;
36 local this_room
= get_room_from_jid(stanza
.attr
.to
);
37 if not this_room
then return; end -- no such room
39 local from_room_jid
= this_room
._jid_nick
[stanza
.attr
.from
];
40 if not from_room_jid
then return; end -- no such nick
42 local from_room
, from_host
, from_nick
= jid
.split(from_room_jid
);
44 local body
= stanza
:get_child("body");
45 if not body
then return; end -- No body, like topic changes
46 body
= body
and body
:get_text(); -- I feel like I want to do `or ""` there :/
48 if not routing
[from_room
] then
53 channel_name
= from_room
,
56 team_domain
= from_host
,
57 user_name
= from_nick
,
60 local form
= stanza
:get_child("x", "jabber:x:form");
61 if form
and form
.attr
.type == "submit" then
62 local callback_id
, button_name
, button_value
;
63 for field
in form
:childtags("field") do
64 if field
.attr
.var
== "callback_id" then
65 button_name
= field
:get_child_text("text");
66 elseif field
.attr
.var
== "button_name" then
67 button_name
= field
:get_child_text("text");
68 elseif field
.attr
.var
~= "FORM_TYPE" or field
:get_child_text("text") ~= button_ns
then
69 callback_id
, button_name
, button_value
= nil, nil, nil;
73 if callback_id
and button_name
and button_value
then
74 json_out
.callback_id
= callback_id
;
76 { type = "button", name
= button_name
, value
= button_value
}
81 local stanzaid
= stanza
:get_child("id");
82 if stanzaid
and string.sub(stanzaid
,1,string.len("webhookbot"))=="webhookbot" then
83 json_out
["bot_id"] = "webhookbot";
86 json_out
= json
.encode(json_out
)
87 local url
= routing
[from_room
];
88 module
:log("debug", "message from %s in %s to %s", from_nick
, from_room
, url
);
89 if url
== "DEBUG" then
90 module
:log("debug", "json_out = %s", json_out
);
94 ["Content-Type"] = "application/json",
96 http
.request(url
, { method
= "POST", body
= json_out
, headers
= headers
}, postcallback
)
99 module
:hook("message/bare", check_message
, 10);
101 local function route_post(f
)
102 return function(event
, path
)
103 local bare_room
= jid
.join(path
, module
.host
);
104 if not get_room_from_jid(bare_room
) then
105 module
:log("warn", "mod_slack_webhook: invalid JID: %s", bare_room
);
109 return f(event
, path
)
113 local function handle_post(event
, path
)
114 local request
= event
.request
;
115 local headers
= request
.headers
;
117 local body_type
= headers
.content_type
;
119 if body_type
== "application/x-www-form-urlencoded" then
120 post_body
= formdecode(request
.body
);
121 elseif body_type
== "application/json" then
122 post_body
= json
.decode(request
.body
)
123 if not post_body
then
129 local bare_room
= jid
.join(path
, module
.host
);
130 local dest_room
= get_room_from_jid(bare_room
);
131 local from_nick
= default_from_nick
;
132 if post_body
["username"] then
133 from_nick
= post_body
["username"];
135 local sender
= jid
.join(path
, module
.host
, from_nick
);
136 module
:log("debug", "message to %s from %s", bare_room
, sender
);
137 module
:log("debug", "body: %s", post_body
["text"]);
138 local message
= msg({ to
= bare_room
, from
= sender
, type = "groupchat", id
="webhookbot" .. now()},post_body
["text"]);
140 if type(post_body
["attachments"]) == "table" then -- Buttons?
141 -- luacheck: ignore 631
142 -- defensive against JSON having whatever data in it, enjoy
144 for _
, attachment
in ipairs(post_body
["attachments"]) do
145 if type(attachment
) == "table" and type(attachment
.actions
) == "table" and type(attachment
.callback_id
) == "string" then
148 for _
, action
in ipairs(attachment
.actions
) do
149 if type(attachment
.text
) == "string" then
150 buttons
.label
= attachment
.text
;
152 if type(action
) == "table" and action
.type == "button" and type(action
.name
) == "string" and type(action
.value
) == "string" then
153 if not button_name
then
154 button_name
= action
.name
;
156 if button_name
== action
.name
then
158 value
= action
.value
;
160 if type(action
.text
) == "string" then
161 button
.label
= action
.text
;
163 table.insert(buttons
, button
);
168 message
:add_direct_child(dataform
.new({
170 type = "hidden", name
= "FORM_TYPE",
174 type = "hidden", name
= "callback_id",
175 value
= attachment
.callback_id
,
178 type = "hidden", name
= "button_name",
182 type = "list-single", name
= "buttons",
183 value
= "", -- FIXME util.dataforms can't do options without a value
192 dest_room
:broadcast_message(message
, true);
196 module
:provides("http", {
197 default_path
= listen_path
;
199 ["POST /*"] = route_post(handle_post
);
200 OPTIONS
= function(e
)
201 local headers
= e
.response
.headers
;
202 headers
.allow
= "POST";
203 headers
.accept
= "application/x-www-form-urlencoded, application/json";