Files
jitsi-meet/resources/prosody-plugins/mod_polls.lua
damencho a76f9d548b feat: Move to use cjson everywhere.
We were using prosody,util.json and cjson at the same time, but the latter is more performant.
Adds some error handling which were missing with the prosody util json one.
2024-04-05 11:26:51 -05:00

212 lines
6.9 KiB
Lua

-- This module provides persistence for the "polls" feature,
-- by keeping track of the state of polls in each room, and sending
-- that state to new participants when they join.
local json = require 'cjson.safe';
local st = require("util.stanza");
local jid = require "util.jid";
local util = module:require("util");
local muc = module:depends("muc");
local NS_NICK = 'http://jabber.org/protocol/nick';
local is_healthcheck_room = util.is_healthcheck_room;
-- Checks if the given stanza contains a JSON message,
-- and that the message type pertains to the polls feature.
-- If yes, returns the parsed message. Otherwise, returns nil.
local function get_poll_message(stanza)
if stanza.attr.type ~= "groupchat" then
return nil;
end
local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet");
if json_data == nil then
return nil;
end
local data, error = json.decode(json_data);
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
if error then
module:log('error', 'Error decoding data error:%s', error);
end
return nil;
end
return data;
end
-- Logs a warning and returns true if a room does not
-- have poll data associated with it.
local function check_polls(room)
if room.polls == nil then
module:log("warn", "no polls data in room");
return true;
end
return false;
end
--- Returns a table having occupant id and occupant name.
--- If the id cannot be extracted from nick a nil value is returned
--- if the occupant name cannot be extracted from presence the Fellow Jitster
--- name is used
local function get_occupant_details(occupant)
if not occupant then
return nil
end
local presence = occupant:get_presence();
local occupant_name;
if presence then
occupant_name = presence:get_child("nick", NS_NICK) and presence:get_child("nick", NS_NICK):get_text() or 'Fellow Jitster';
else
occupant_name = 'Fellow Jitster'
end
local _, _, occupant_id = jid.split(occupant.nick)
if not occupant_id then
return nil
end
return { ["occupant_id"] = occupant_id, ["occupant_name"] = occupant_name }
end
-- Sets up poll data in new rooms.
module:hook("muc-room-created", function(event)
local room = event.room;
if is_healthcheck_room(room.jid) then return end
module:log("debug", "setting up polls in room %s", room.jid);
room.polls = {
by_id = {};
order = {};
};
end);
-- Keeps track of the current state of the polls in each room,
-- by listening to "new-poll" and "answer-poll" messages,
-- and updating the room poll data accordingly.
-- This mirrors the client-side poll update logic.
module:hook("message/bare", function(event)
local data = get_poll_message(event.stanza);
if data == nil then return end
local room = muc.get_room_from_jid(event.stanza.attr.to);
if data.type == "new-poll" then
if check_polls(room) then return end
local occupant_jid = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid)
return
end
local poll_creator = get_occupant_details(occupant)
if not poll_creator then
module:log("error", "Cannot retrieve poll creator id and name for %s from %s", occupant.jid, room.jid)
return
end
local answers = {}
local compact_answers = {}
for i, name in ipairs(data.answers) do
table.insert(answers, { name = name, voters = {} });
table.insert(compact_answers, { key = i, name = name});
end
local poll = {
id = data.pollId,
sender_id = poll_creator.occupant_id,
sender_name = poll_creator.occupant_name,
question = data.question,
answers = answers
};
room.polls.by_id[data.pollId] = poll
table.insert(room.polls.order, poll)
local pollData = {
event = event,
room = room,
poll = {
pollId = data.pollId,
senderId = poll_creator.occupant_id,
senderName = poll_creator.occupant_name,
question = data.question,
answers = compact_answers
}
}
module:fire_event("poll-created", pollData);
elseif data.type == "answer-poll" then
if check_polls(room) then return end
local occupant_jid = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant %s does not exists for room %s", occupant_jid, room.jid)
return
end
local poll = room.polls.by_id[data.pollId];
if poll == nil then
module:log("warn", "answering inexistent poll");
return;
end
local voter = get_occupant_details(occupant)
if not voter then
module:log("error", "Cannot retrieve voter id and name for %s from %s", occupant.jid, room.jid)
return
end
local answers = {};
for vote_option_idx, vote_flag in ipairs(data.answers) do
table.insert(answers, {
key = vote_option_idx,
value = vote_flag,
name = poll.answers[vote_option_idx].name,
});
poll.answers[vote_option_idx].voters[voter.occupant_id] = vote_flag and voter.occupant_name or nil;
end
local answerData = {
event = event,
room = room,
pollId = poll.id,
voterName = voter.occupant_name,
voterId = voter.occupant_id,
answers = answers
}
module:fire_event("answer-poll", answerData);
end
end);
-- Sends the current poll state to new occupants after joining a room.
module:hook("muc-occupant-joined", function(event)
local room = event.room;
if is_healthcheck_room(room.jid) then return end
if room.polls == nil or #room.polls.order == 0 then
return
end
local data = {
type = "old-polls",
polls = {},
};
for i, poll in ipairs(room.polls.order) do
data.polls[i] = {
id = poll.id,
senderId = poll.sender_id,
senderName = poll.sender_name,
question = poll.question,
answers = poll.answers
};
end
local json_msg_str, error = json.encode(data);
if not json_msg_str then
module:log('error', 'Error encoding data room:%s error:%s', room.jid, error);
end
local stanza = st.message({
from = room.jid,
to = event.occupant.jid
})
:tag("json-message", { xmlns = "http://jitsi.org/jitmeet" })
:text(json_msg_str)
:up();
room:route_stanza(stanza);
end);