2024-06-30 13:30:33 +00:00
|
|
|
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
|
|
-- SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2024-06-30 15:19:14 +00:00
|
|
|
local id = require "util.id";
|
|
|
|
local st = require "util.stanza";
|
2024-06-30 15:57:33 +00:00
|
|
|
local timer = require "util.timer";
|
2024-06-30 15:19:14 +00:00
|
|
|
local xmlns_occupant_id = "urn:xmpp:occupant-id:0";
|
2024-06-30 15:57:33 +00:00
|
|
|
local xmlns_replace = "urn:xmpp:message-correct:0";
|
2024-07-01 10:36:32 +00:00
|
|
|
local xmlns_poll_message = module:require("constants").xmlns_poll_message;
|
|
|
|
local poll_message_tag = module:require("constants").poll_message_tag;
|
|
|
|
local poll_question_tag = module:require("constants").poll_question_tag;
|
|
|
|
local poll_choice_tag = module:require("constants").poll_choice_tag;
|
2024-06-30 15:19:14 +00:00
|
|
|
|
2024-06-30 15:57:33 +00:00
|
|
|
local mod_muc = module:depends"muc";
|
|
|
|
local get_room_from_jid = mod_muc.get_room_from_jid;
|
|
|
|
|
2024-07-01 10:04:03 +00:00
|
|
|
local debounce_delay = 5; -- number of seconds during which we must group votes to avoid flood.
|
2024-06-30 15:57:33 +00:00
|
|
|
local scheduled_updates = {};
|
|
|
|
|
|
|
|
-- construct the poll message stanza
|
|
|
|
local function build_poll_message(room, message_id, is_end_message)
|
2024-06-30 15:19:14 +00:00
|
|
|
local current_poll = room._data.current_poll;
|
|
|
|
if not current_poll then
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
local from = room.jid .. '/' .. current_poll.occupant_nick;
|
|
|
|
|
|
|
|
local content = current_poll["muc#roompoll_question"] .. "\n";
|
|
|
|
|
2024-06-30 15:57:33 +00:00
|
|
|
if is_end_message then
|
|
|
|
content = content .. "This poll is now over.\n";
|
|
|
|
end
|
|
|
|
|
2024-06-30 15:19:14 +00:00
|
|
|
local total = 0;
|
|
|
|
for choice, nb in pairs(current_poll.votes_by_choices) do
|
|
|
|
total = total + nb;
|
|
|
|
end
|
2024-07-01 10:04:03 +00:00
|
|
|
for _, choice_desc in ipairs(current_poll.choices_ordered) do
|
|
|
|
local choice, label = choice_desc.number, choice_desc.label;
|
2024-06-30 15:19:14 +00:00
|
|
|
content = content .. choice .. ': ' .. label;
|
|
|
|
if total > 0 then
|
2024-07-01 10:36:32 +00:00
|
|
|
local nb = current_poll.votes_by_choices[choice] or 0;
|
2024-06-30 15:57:33 +00:00
|
|
|
local percent = string.format("%.2f", nb * 100 / total);
|
2024-06-30 15:19:14 +00:00
|
|
|
content = content .. " (" .. nb .. "/" .. total .. " = " .. percent .. "%)";
|
|
|
|
end
|
|
|
|
content = content .. "\n";
|
|
|
|
end
|
2024-06-30 15:57:33 +00:00
|
|
|
|
|
|
|
if not is_end_message then
|
|
|
|
content = content .. "Send a message with an exclamation mark followed by your choice number to vote. Example: !1\n";
|
|
|
|
end
|
2024-06-30 15:19:14 +00:00
|
|
|
|
|
|
|
local msg = st.message({
|
|
|
|
type = "groupchat",
|
|
|
|
from = from,
|
|
|
|
id = message_id
|
|
|
|
}, content);
|
|
|
|
|
|
|
|
msg:tag("occupant-id", {
|
|
|
|
xmlns = xmlns_occupant_id,
|
|
|
|
id = current_poll.occupant_id
|
|
|
|
}):up();
|
|
|
|
|
2024-07-01 10:36:32 +00:00
|
|
|
-- now we must add some custom XML data, so that compatible clients can display the poll as they want:
|
2024-07-01 15:45:11 +00:00
|
|
|
-- <x-poll xmlns="http://jabber.org/protocol/muc#x-poll-message" id="I9UWyoxsz4BN" votes="1" end="1719842224" over="">
|
2024-07-01 10:36:32 +00:00
|
|
|
-- <x-poll-question>Poll question</x-poll-question>
|
|
|
|
-- <x-poll-choice choice="1" votes="0">Choice 1 label</x-poll-choice>
|
|
|
|
-- <x-poll-choice choice="2" votes="1">Choice 2 label</x-poll-choice>
|
|
|
|
-- <x-poll-choice choice="3" votes="0">Choice 3 label</x-poll-choice>
|
|
|
|
-- <x-poll-choice choice="4" votes="0">Choice 4 label</x-poll-choice>
|
|
|
|
-- </x-poll>
|
|
|
|
local message_attrs = {
|
|
|
|
xmlns = xmlns_poll_message,
|
|
|
|
id = current_poll.poll_id,
|
|
|
|
votes = "" .. total
|
|
|
|
};
|
2024-07-01 15:45:11 +00:00
|
|
|
message_attrs["end"] = string.format("%i", current_poll.end_timestamp);
|
2024-07-01 10:36:32 +00:00
|
|
|
if current_poll.already_ended then
|
|
|
|
message_attrs["over"] = "";
|
|
|
|
end
|
|
|
|
msg:tag(poll_message_tag, message_attrs):text_tag(poll_question_tag, current_poll["muc#roompoll_question"], {});
|
|
|
|
for _, choice_desc in ipairs(current_poll.choices_ordered) do
|
|
|
|
local choice, label = choice_desc.number, choice_desc.label;
|
|
|
|
local nb = current_poll.votes_by_choices[choice] or 0;
|
|
|
|
msg:text_tag(poll_choice_tag, label, {
|
|
|
|
votes = "" .. nb,
|
|
|
|
choice = choice
|
|
|
|
});
|
|
|
|
end
|
|
|
|
msg:up();
|
|
|
|
|
2024-06-30 15:19:14 +00:00
|
|
|
return msg;
|
|
|
|
end
|
|
|
|
|
2024-06-30 15:57:33 +00:00
|
|
|
-- sends a message when the poll starts.
|
2024-06-30 13:30:33 +00:00
|
|
|
local function poll_start_message(room)
|
2024-06-30 15:19:14 +00:00
|
|
|
if not room._data.current_poll then
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
module:log("debug", "Sending the start message for room %s poll", room.jid);
|
|
|
|
local message_id = id.medium();
|
2024-06-30 15:57:33 +00:00
|
|
|
local msg = build_poll_message(room, message_id, false);
|
2024-06-30 15:19:14 +00:00
|
|
|
room:broadcast_message(msg);
|
|
|
|
return message_id;
|
2024-06-30 13:30:33 +00:00
|
|
|
end
|
|
|
|
|
2024-06-30 15:57:33 +00:00
|
|
|
-- Send the poll update message
|
|
|
|
local function send_poll_update_message(room)
|
|
|
|
if not room._data.current_poll then
|
|
|
|
return nil;
|
|
|
|
end
|
2024-07-01 10:04:03 +00:00
|
|
|
if room._data.current_poll.already_ended then
|
|
|
|
module:log("debug", "Cancelling the update message for room %s poll, because already_ended==true.", room.jid);
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
|
2024-06-30 15:57:33 +00:00
|
|
|
module:log("debug", "Sending an update message for room %s poll", room.jid);
|
|
|
|
local message_id = id.medium(); -- generate a new id
|
|
|
|
local msg = build_poll_message(room, message_id, false);
|
|
|
|
|
|
|
|
-- the update message is a <replace> message (see XEP-0308).
|
|
|
|
msg:tag('replace', {
|
|
|
|
xmlns = xmlns_replace;
|
|
|
|
id = room._data.current_poll.start_message_id;
|
|
|
|
}):up();
|
|
|
|
|
|
|
|
room:broadcast_message(msg);
|
|
|
|
return message_id;
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Schedule an update of the start message.
|
|
|
|
-- We do not send this update each time someone vote,
|
|
|
|
-- to avoid flooding.
|
|
|
|
local function schedule_poll_update_message(room_jid)
|
|
|
|
if scheduled_updates[room_jid] then
|
|
|
|
-- already a running timer, we can ignore to debounce.
|
|
|
|
return;
|
|
|
|
end
|
|
|
|
scheduled_updates[room_jid] = timer.add_task(debounce_delay, function()
|
|
|
|
scheduled_updates[room_jid] = nil;
|
|
|
|
-- We dont pass room, because here it could have been removed from memory.
|
|
|
|
-- So we must relad the room from the JID in any case.
|
|
|
|
local room = get_room_from_jid(room_jid);
|
|
|
|
if not room then
|
|
|
|
return;
|
|
|
|
end
|
|
|
|
send_poll_update_message(room);
|
|
|
|
end);
|
2024-06-30 13:30:33 +00:00
|
|
|
end
|
|
|
|
|
2024-06-30 15:57:33 +00:00
|
|
|
-- Send a new message when the poll is over, with the result.
|
2024-06-30 13:30:33 +00:00
|
|
|
local function poll_end_message(room)
|
2024-06-30 15:57:33 +00:00
|
|
|
if not room._data.current_poll then
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
module:log("debug", "Sending the end message for room %s poll", room.jid);
|
|
|
|
local message_id = id.medium(); -- generate a new id
|
|
|
|
local msg = build_poll_message(room, message_id, true);
|
|
|
|
room:broadcast_message(msg);
|
|
|
|
return message_id;
|
2024-06-30 13:30:33 +00:00
|
|
|
end
|
|
|
|
|
2024-07-01 13:01:30 +00:00
|
|
|
-- security check: we must remove all specific tags, to be sure nobody tries to spoof polls!
|
|
|
|
local function remove_specific_tags_from_groupchat(event)
|
|
|
|
event.stanza:maptags(function (child)
|
|
|
|
if child.name == poll_message_tag then
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
if child.name == poll_question_tag then
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
if child.name == poll_choice_tag then
|
|
|
|
return nil;
|
|
|
|
end
|
|
|
|
return child;
|
|
|
|
end);
|
|
|
|
end
|
|
|
|
|
2024-07-01 15:45:11 +00:00
|
|
|
-- when a new session is opened, we must send the current poll to the client
|
|
|
|
local function handle_new_occupant_session(event)
|
|
|
|
local room = event.room;
|
|
|
|
if not room._data.current_poll then
|
|
|
|
return;
|
|
|
|
end
|
|
|
|
if room._data.current_poll.already_ended then
|
|
|
|
return;
|
|
|
|
end
|
|
|
|
schedule_poll_update_message(room.jid);
|
|
|
|
-- FIXME: for now we just schedule a new poll update. But we should only send a message to the new occupant.
|
|
|
|
end
|
|
|
|
|
2024-06-30 13:30:33 +00:00
|
|
|
return {
|
|
|
|
poll_start_message = poll_start_message;
|
|
|
|
poll_end_message = poll_end_message;
|
|
|
|
schedule_poll_update_message = schedule_poll_update_message;
|
2024-07-01 13:01:30 +00:00
|
|
|
remove_specific_tags_from_groupchat = remove_specific_tags_from_groupchat;
|
2024-07-01 15:45:11 +00:00
|
|
|
handle_new_occupant_session = handle_new_occupant_session;
|
2024-06-30 13:30:33 +00:00
|
|
|
};
|