Software /
code /
prosody-modules
Diff
mod_muc_rai/mod_muc_rai.lua @ 3974:f14c862598a9
mod_muc_rai: New module to implement Room Activity Indicators
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Wed, 15 Apr 2020 21:19:45 +0100 |
child | 3997:0e72dd70afff |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_rai/mod_muc_rai.lua Wed Apr 15 21:19:45 2020 +0100 @@ -0,0 +1,215 @@ +local cache = require "util.cache"; +local jid = require "util.jid"; +local st = require "util.stanza"; + +local max_subscribers = module:get_option_number("muc_rai_max_subscribers", 1024); + +local muc_affiliation_store = module:open_store("config", "map"); +local muc_archive = module:open_store("muc_log", "archive"); + +local xmlns_rai = "xmpp:prosody.im/protocol/rai"; + +local muc_markers = module:depends("muc_markers"); + +-- subscriber_jid -> { [room_jid] = interested } +local subscribed_users = cache.new(max_subscribers, false); +-- room_jid -> { [user_jid] = interested } +local interested_users = {}; +-- room_jid -> last_id +local room_activity_cache = cache.new(1024); + +-- Send a single notification for a room, updating data structures as needed +local function send_single_notification(user_jid, room_jid) + local notification = st.message({ to = user_jid, from = module.host }) + :tag("rai", { xmlns = xmlns_rai }) + :text_tag("activity", room_jid) + :up(); + local interested_room_users = interested_users[room_jid]; + if interested_room_users then + interested_room_users[user_jid] = nil; + end + local interested_rooms = subscribed_users:get(user_jid); + if interested_rooms then + interested_rooms[room_jid] = nil; + end + module:log("debug", "Sending notification from %s to %s", room_jid, user_jid); + return module:send(notification); +end + +local function subscribe_room(user_jid, room_jid) + local interested_rooms = subscribed_users:get(user_jid); + if not interested_rooms then + return nil, "not-subscribed"; + end + module:log("debug", "Subscribed %s to %s", user_jid, room_jid); + interested_rooms[room_jid] = true; + + local interested_room_users = interested_users[room_jid]; + if not interested_room_users then + interested_room_users = {}; + interested_users[room_jid] = interested_room_users; + end + interested_room_users[user_jid] = true; + return true; +end + +local function unsubscribe_room(user_jid, room_jid) + local interested_rooms = subscribed_users:get(user_jid); + if not interested_rooms then + return nil, "not-subscribed"; + end + interested_rooms[room_jid] = nil; + + local interested_room_users = interested_users[room_jid]; + if not interested_room_users then + return true; + end + interested_room_users[user_jid] = nil; + return true; +end + +local function notify_interested_users(room_jid) + module:log("warn", "NOTIFYING FOR %s", room_jid) + local interested_room_users = interested_users[room_jid]; + if not interested_room_users then + module:log("debug", "Nobody interested in %s", room_jid); + return; + end + for user_jid in pairs(interested_room_users) do + send_single_notification(user_jid, room_jid); + end + return true; +end + +local function unsubscribe_user_from_all_rooms(user_jid) + local interested_rooms = subscribed_users:get(user_jid); + if not interested_rooms then + return nil, "not-subscribed"; + end + for room_jid in pairs(interested_rooms) do + unsubscribe_room(user_jid, room_jid); + end + return true; +end + +local function get_last_room_message_id(room_jid) + local last_room_message_id = room_activity_cache:get(room_jid); + if last_room_message_id then + return last_room_message_id; + end + + -- Load all the data! + local query = { + limit = 1; + reverse = true; + with = "message<groupchat"; + } + local data, err = muc_archive:find(jid.node(room_jid), query); + + if not data then + module:log("error", "Could not fetch history: %s", err); + return nil; + end + + local id = data(); + room_activity_cache:set(room_jid, id); + return id; +end + +local function update_room_activity(room_jid, last_id) + room_activity_cache:set(room_jid, last_id); +end + +local function get_last_user_read_id(user_jid, room_jid) + return muc_markers.get_user_read_marker(user_jid, room_jid); +end + +local function has_new_activity(room_jid, user_jid) + local last_room_message_id = get_last_room_message_id(room_jid); + local last_user_read_id = get_last_user_read_id(user_jid, room_jid); + return last_room_message_id ~= last_user_read_id; +end + +-- Returns a set of rooms that a user is interested in +local function get_interested_rooms(user_jid) + -- Use affiliation as an indication of interest, return + -- all rooms a user is affiliated + return muc_affiliation_store:get_all(jid.bare(user_jid)); +end + +-- Subscribes to all rooms that the user has an interest in +-- Returns a set of room JIDs that have already had activity (thus no subscription) +local function subscribe_all_rooms(user_jid) + -- Send activity notifications for all relevant rooms + local interested_rooms, err = get_interested_rooms(user_jid); + + if not interested_rooms then + if err then + return nil, "internal-server-error"; + end + interested_rooms = {}; + end + + if not subscribed_users:set(user_jid, interested_rooms) then + module:log("warn", "Subscriber limit (%d) reached, rejecting subscription from %s", max_subscribers, user_jid); + return nil, "resource-constraint"; + end + + local rooms_with_activity; + for room_name in pairs(interested_rooms) do + local room_jid = room_name.."@"..module.host; + if has_new_activity(room_jid, user_jid) then + -- There has already been activity, include this room + -- in the response + if not rooms_with_activity then + rooms_with_activity = {}; + end + rooms_with_activity[room_jid] = true; + else + -- Subscribe to any future activity + subscribe_room(user_jid, room_jid); + end + end + return rooms_with_activity; +end + +module:hook("presence/host", function (event) + local origin, stanza = event.origin, event.stanza; + local user_jid = stanza.attr.from; + + if stanza.attr.type == "unavailable" then -- User going offline + unsubscribe_user_from_all_rooms(user_jid); + return true; + end + + local rooms_with_activity, err = subscribe_all_rooms(user_jid); + + if not rooms_with_activity then + if not err then + module:log("debug", "No activity to notify"); + return true; + else + return origin.send(st.error_reply(stanza, "wait", "resource-constraint")); + end + end + + local reply = st.reply(stanza) + :tag("rai", { xmlns = xmlns_rai }); + for room_jid in pairs(rooms_with_activity) do + reply:text_tag("activity", room_jid); + end + return origin.send(reply); +end); + +module:hook("muc-broadcast-message", function (event) + local room, stanza = event.room, event.stanza; + local archive_id = stanza:get_child_text("stanza-id", "urn:xmpp:sid:0"); + if archive_id then + -- Remember the id of the last message so we can compare it + -- to the per-user marker (managed by mod_muc_markers) + update_room_activity(room.jid, archive_id); + -- Notify any users that need to be notified + notify_interested_users(room.jid); + end +end, -1); +