mod_storage_memory: Also merged into core
[prosody-modules.git] / mod_pastebin / mod_pastebin.lua
blob560fb5515cdd2cff3270965c2d5a1fbd23953240
2 local st = require "util.stanza";
3 module:depends("http");
4 local uuid_new = require "util.uuid".generate;
5 local os_time = os.time;
6 local t_remove = table.remove;
7 local add_task = require "util.timer".add_task;
8 local jid_bare = require "util.jid".bare;
10 local function get_room_from_jid() end;
11 local is_component = module:get_host_type() == "component";
12 if is_component then
13 local mod_muc = module:depends "muc";
14 local muc_rooms = rawget(mod_muc, "rooms");
15 get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or
16 function (jid)
17 return muc_rooms[jid];
18 end
19 end
21 local utf8_pattern = "[\194-\244][\128-\191]*$";
22 local function drop_invalid_utf8(seq)
23 local start = seq:byte();
24 module:log("debug", "utf8: %d, %d", start, #seq);
25 if (start <= 223 and #seq < 2)
26 or (start >= 224 and start <= 239 and #seq < 3)
27 or (start >= 240 and start <= 244 and #seq < 4)
28 or (start > 244) then
29 return "";
30 end
31 return seq;
32 end
34 local function utf8_length(str)
35 local _, count = string.gsub(str, "[^\128-\193]", "");
36 return count;
37 end
39 local pastebin_private_messages = module:get_option_boolean("pastebin_private_messages", not is_component);
40 local length_threshold = module:get_option_number("pastebin_threshold", 500);
41 local line_threshold = module:get_option_number("pastebin_line_threshold", 4);
42 local max_summary_length = module:get_option_number("pastebin_summary_length", 150);
43 local html_preview = module:get_option_boolean("pastebin_html_preview", true);
45 local base_url = module:get_option_string("pastebin_url", module:http_url()):gsub("/$", "").."/";
47 -- Seconds a paste should live for in seconds (config is in hours), default 24 hours
48 local expire_after = math.floor(module:get_option_number("pastebin_expire_after", 24) * 3600);
50 local trigger_string = module:get_option_string("pastebin_trigger");
51 trigger_string = (trigger_string and trigger_string .. " ");
53 local pastes = {};
55 local xmlns_xhtmlim = "http://jabber.org/protocol/xhtml-im";
56 local xmlns_xhtml = "http://www.w3.org/1999/xhtml";
58 function pastebin_text(text)
59 local uuid = uuid_new();
60 pastes[uuid] = { body = text, time = os_time(), };
61 pastes[#pastes+1] = uuid;
62 if not pastes[2] then -- No other pastes, give the timer a kick
63 add_task(expire_after, expire_pastes);
64 end
65 return base_url..uuid;
66 end
68 function handle_request(event, pasteid)
69 event.response.headers.content_type = "text/plain; charset=utf-8";
71 if not pasteid then
72 return "Invalid paste id, perhaps it expired?";
73 end
75 --module:log("debug", "Received request, replying: %s", pastes[pasteid].text);
76 local paste = pastes[pasteid];
78 if not paste then
79 return "Invalid paste id, perhaps it expired?";
80 end
82 return paste.body;
83 end
85 local function replace_tag(s, replacement)
86 local once = false;
87 s:maptags(function (tag)
88 if tag.name == replacement.name and tag.attr.xmlns == replacement.attr.xmlns then
89 if not once then
90 once = true;
91 return replacement;
92 else
93 return nil;
94 end
95 end
96 return tag;
97 end);
98 if not once then
99 s:add_child(replacement);
103 local line_count_pattern = string.rep("[^\n]*\n", line_threshold + 1):sub(1,-2);
105 function check_message(data)
106 local stanza = data.stanza;
108 -- Only check for MUC presence when loaded on a component.
109 if is_component then
110 local room = get_room_from_jid(jid_bare(stanza.attr.to));
111 if not room then return; end
113 local nick = room._jid_nick[stanza.attr.from];
114 if not nick then return; end
117 local body = stanza:get_child_text();
119 if not body then return; end
121 --module:log("debug", "Body(%s) length: %d", type(body), #(body or ""));
123 if ( #body > length_threshold and utf8_length(body) > length_threshold ) or
124 (trigger_string and body:find(trigger_string, 1, true) == 1) or
125 body:find(line_count_pattern) then
126 if trigger_string and body:sub(1, #trigger_string) == trigger_string then
127 body = body:sub(#trigger_string+1);
129 local url = pastebin_text(body);
130 module:log("debug", "Pasted message as %s", url);
131 --module:log("debug", " stanza[bodyindex] = %q", tostring( stanza[bodyindex]));
132 local summary = (body:sub(1, max_summary_length):gsub(utf8_pattern, drop_invalid_utf8) or ""):match("[^\n]+") or "";
133 summary = summary:match("^%s*(.-)%s*$");
134 local summary_prefixed = summary:match("[,:]$");
135 replace_tag(stanza, st.stanza("body"):text(summary .. "\n" .. url));
137 stanza:add_child(st.stanza("query", { xmlns = "jabber:iq:oob" }):tag("url"):text(url));
139 if html_preview then
140 local line_count = select(2, body:gsub("\n", "%0")) + 1;
141 local link_text = ("[view %spaste (%d line%s)]"):format(summary_prefixed and "" or "rest of ", line_count, line_count == 1 and "" or "s");
142 local html = st.stanza("html", { xmlns = xmlns_xhtmlim }):tag("body", { xmlns = xmlns_xhtml });
143 html:tag("p"):text(summary.." "):up();
144 html:tag("a", { href = url }):text(link_text):up();
145 replace_tag(stanza, html);
150 module:hook("message/bare", check_message);
151 if pastebin_private_messages then
152 module:hook("message/full", check_message);
155 module:hook("muc-disco#info", function (event)
156 local reply, form, formdata = event.reply, event.form, event.formdata;
157 reply:tag("feature", { var = "https://modules.prosody.im/mod_pastebin" }):up();
158 table.insert(form, { name = "https://modules.prosody.im/mod_pastebin#max_lines", datatype = "xs:integer" });
159 table.insert(form, { name = "https://modules.prosody.im/mod_pastebin#max_characters", datatype = "xs:integer" });
160 formdata["https://modules.prosody.im/mod_pastebin#max_lines"] = tostring(line_threshold);
161 formdata["https://modules.prosody.im/mod_pastebin#max_characters"] = tostring(length_threshold);
162 end);
164 function expire_pastes(time)
165 time = time or os_time(); -- COMPAT with 0.5
166 if pastes[1] then
167 pastes[pastes[1]] = nil;
168 t_remove(pastes, 1);
169 if pastes[1] then
170 return (expire_after - (time - pastes[pastes[1]].time)) + 1;
176 module:provides("http", {
177 route = {
178 ["GET /*"] = handle_request;
182 local function set_pastes_metatable()
183 -- luacheck: ignore 212/pastes 431/pastes
184 if expire_after == 0 then
185 local dm = require "util.datamanager";
186 setmetatable(pastes, {
187 __index = function (pastes, id)
188 if type(id) == "string" then
189 return dm.load(id, module.host, "pastebin");
191 end;
192 __newindex = function (pastes, id, data)
193 if type(id) == "string" then
194 dm.store(id, module.host, "pastebin", data);
196 end;
198 else
199 setmetatable(pastes, nil);
203 module.load = set_pastes_metatable;
205 function module.save()
206 return { pastes = pastes };
209 function module.restore(data)
210 pastes = data.pastes or pastes;
211 set_pastes_metatable();