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 = { } ;
2024-07-04 12:04:33 +00:00
local string_poll_over = module : get_option_string ( " poll_string_over " ) or " This poll is now over. " ;
local string_poll_vote_instructions = module : get_option_string ( " poll_string_vote_instructions " ) or " Send a message with an exclamation mark followed by your choice number to vote. Example: !1 " ;
2024-06-30 15:57:33 +00:00
-- 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
2024-07-04 12:04:33 +00:00
local from = current_poll.occupant_nick ; -- this is in fact room.jid/nickname
2024-06-30 15:19:14 +00:00
local content = current_poll [ " muc#roompoll_question " ] .. " \n " ;
2024-06-30 15:57:33 +00:00
if is_end_message then
2024-07-04 12:04:33 +00:00
content = content .. string_poll_over .. " \n " ;
2024-06-30 15:57:33 +00:00
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
2024-07-04 12:04:33 +00:00
content = content .. string_poll_vote_instructions .. " \n " ;
2024-06-30 15:57:33 +00:00
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 ) ;
2024-07-04 13:34:32 +00:00
if scheduled_updates [ room.jid ] then
module : log ( " debug " , " Cancelling an update message for the poll %s " , room.jid ) ;
timer.stop ( scheduled_updates [ room.jid ] ) ;
scheduled_updates [ room.jid ] = nil ;
end
2024-06-30 15:57:33 +00:00
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
} ;