Mercurial > prosody-modules
view mod_mam_muc_sql/mod_mam_muc_sql.lua @ 1268:854a3933cfcd
mod_muc_log_http: URL-encode room names. This allows special characters in room names to work. Ideally this escaping shouldn’t be done in the user visible content, but the module’s template system doesn’t currently allow that.
author | Waqas Hussain <waqas20@gmail.com> |
---|---|
date | Sat, 04 Jan 2014 16:50:57 -0500 |
parents | 87e847280aef |
children | 7dbde05b48a9 |
line wrap: on
line source
-- XEP-0313: Message Archive Management for Prosody -- Copyright (C) 2011-2012 Kim Alvefur -- -- This file is MIT/X11 licensed. local xmlns_mam = "urn:xmpp:mam:tmp"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_forward = "urn:xmpp:forward:0"; local st = require "util.stanza"; local rsm = module:require "mod_mam/rsm"; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local jid_prep = require "util.jid".prep; local host = module.host; local serialize, deserialize = require"util.json".encode, require"util.json".decode; local unpack = unpack; local tostring = tostring; local time_now = os.time; local t_insert = table.insert; local m_min = math.min; local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse; local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); --local rooms_to_archive = module:get_option_set("rooms_to_archive",{}); local sql, setsql, getsql = {}; do -- SQL stuff local dburi; local connection; local connections = module:shared "/*/sql/connection-cache"; local build_url = require"socket.url".build; local resolve_relative_path = require "core.configmanager".resolve_relative_path; local params = module:get_option("mam_sql", module:get_option("sql")); local function db2uri(params) return build_url{ scheme = params.driver, user = params.username, password = params.password, host = params.host, port = params.port, path = params.database, }; end local function test_connection() if not connection then return nil; end if connection:ping() then return true; else module:log("debug", "Database connection closed"); connection = nil; connections[dburi] = nil; end end local function connect() if not test_connection() then prosody.unlock_globals(); local dbh, err = DBI.Connect( params.driver, params.database, params.username, params.password, params.host, params.port ); prosody.lock_globals(); if not dbh then module:log("debug", "Database connection failed: %s", tostring(err)); return nil, err; end module:log("debug", "Successfully connected to database"); dbh:autocommit(false); -- don't commit automatically connection = dbh; connections[dburi] = dbh; end return connection; end do -- process options to get a db connection local ok; prosody.unlock_globals(); ok, DBI = pcall(require, "DBI"); if not ok then package.loaded["DBI"] = {}; module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI); module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi"); end prosody.lock_globals(); if not ok or not DBI.Connect then return; -- Halt loading of this module end params = params or { driver = "SQLite3" }; if params.driver == "SQLite3" then params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); end assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); dburi = db2uri(params); connection = connections[dburi]; assert(connect()); end function getsql(sql, ...) if params.driver == "PostgreSQL" then sql = sql:gsub("`", "\""); end -- do prepared statement stuff local stmt, err = connection:prepare(sql); if not stmt and not test_connection() then error("connection failed"); end if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end -- run query local ok, err = stmt:execute(...); if not ok and not test_connection() then error("connection failed"); end if not ok then return nil, err; end return stmt; end function setsql(sql, ...) local stmt, err = getsql(sql, ...); if not stmt then return stmt, err; end return stmt:affected(); end function sql.rollback(...) if connection then connection:rollback(); end -- FIXME check for rollback error? return ...; end function sql.commit(...) if not connection:commit() then return nil, "SQL commit failed"; end return ...; end end local archive_store = "archive2"; -- Handle archive queries module:hook("iq/bare/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; local room = jid_split(stanza.attr.to); local query = stanza.tags[1]; local room_obj = hosts[module.host].modules.muc.rooms[jid_bare(stanza.attr.to)]; if not room_obj then return -- FIXME not found end local from = jid_bare(stanza.attr.from); if room_obj._affiliations[from] == "outcast" or room_obj._data.members_only and not room_obj._affiliations[from] then return -- FIXME unauth end if stanza.attr.type == "get" then local qid = query.attr.queryid; -- Search query parameters local qwith = query:get_child_text("with"); local qstart = query:get_child_text("start"); local qend = query:get_child_text("end"); local qset = rsm.get(query); module:log("debug", "Archive query, id %s with %s from %s until %s)", tostring(qid), qwith or "anyone", qstart or "the dawn of time", qend or "now"); if qstart or qend then -- Validate timestamps local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend)) if (qstart and not vstart) or (qend and not vend) then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp")) return true end qstart, qend = vstart, vend; end local qres; if qwith then -- Validate the 'with' jid local pwith = qwith and jid_prep(qwith); if pwith and not qwith then -- it failed prepping origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid JID")) return true end local _, _, resource = jid_split(qwith); qwith = jid_bare(pwith); qres = resource; end -- RSM stuff local qmax = m_min(qset and qset.max or default_max_items, max_max_items); local first, last; local n = 0; local sql_query = ([[ SELECT `id`, `when`, `stanza` FROM `prosodyarchive` WHERE `host` = ? AND `user` = ? AND `store` = ? AND `when` BETWEEN ? AND ? %s AND `id` > ? LIMIT ?; ]]):format(qres and [[AND `resource` = ?]] or "") local p = { host, room, archive_store, qstart or 0, qend or time_now(), qset and tonumber(qset.after) or 0, qmax }; if qres then t_insert(p, 6, qres); end module:log("debug", "getsql(sql_query, "..table.concat(p, ", ")); local data, err = getsql(sql_query, unpack(p)); if not data then origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error loading archive: "..tostring(err))); return true end for item in data:rows() do local id, when, orig_stanza = unpack(item); --module:log("debug", "id is %s", id); local fwd_st = st.message{ to = stanza.attr.from, from = stanza.attr.to } :tag("result", { xmlns = xmlns_mam, queryid = qid, id = id }):up() :tag("forwarded", { xmlns = xmlns_forward }) :tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up(); orig_stanza = st.deserialize(deserialize(orig_stanza)); orig_stanza.attr.xmlns = "jabber:client"; fwd_st:add_child(orig_stanza); origin.send(fwd_st); last = id; n = n + 1; if not first then first = id; end end -- That's all folks! module:log("debug", "Archive query %s completed", tostring(qid)); local reply = st.reply(stanza); if last then -- This is a bit redundant, isn't it? reply:query(xmlns_mam):add_child(rsm.generate{first = first, last = last, count = n}); end origin.send(reply); return true end end); -- Handle messages local function message_handler(event) local origin, stanza = event.origin, event.stanza; local orig_type = stanza.attr.type or "normal"; local orig_to = stanza.attr.to; local orig_from = stanza.attr.from; -- Still needed? if not orig_from then orig_from = origin.full_jid; end -- Only store groupchat messages if not (orig_type == "groupchat" and (stanza:get_child("body") or stanza:get_child("subject"))) then return; end local room = jid_split(orig_to); local room_obj = hosts[host].modules.muc.rooms[orig_to] if not room_obj then return end local when = time_now(); local stanza = st.clone(stanza); -- Private copy --stanza.attr.to = nil; local nick = room_obj._jid_nick[orig_from]; if not nick then return end stanza.attr.from = nick; local _, _, nick = jid_split(nick); -- And stash it local ok, err = setsql([[ INSERT INTO `prosodyarchive` (`host`, `user`, `store`, `when`, `resource`, `stanza`) VALUES (?, ?, ?, ?, ?, ?); ]], host, room, archive_store, when, nick, serialize(st.preserialize(stanza))) if ok then sql.commit(); else module:log("error", "SQL error: %s", err); sql.rollback(); end --[[ This was dropped from the spec if ok then stanza:tag("archived", { xmlns = xmlns_mam, by = host, id = id }):up(); end --]] end module:hook("message/bare", message_handler, 2); module:add_feature(xmlns_mam);