2024-06-29 16:39:22 +00:00
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
-- SPDX-License-Identifier: AGPL-3.0-only
2024-07-01 10:36:32 +00:00
local id = require " util.id " ;
2024-06-29 17:33:37 +00:00
local st = require " util.stanza " ;
2024-06-29 16:39:22 +00:00
local get_time = require " util.time " . now ;
2024-06-30 13:30:33 +00:00
local timer = require " util.timer " ;
local mod_muc = module : depends " muc " ;
local get_room_from_jid = mod_muc.get_room_from_jid ;
local poll_start_message = module : require ( " message " ) . poll_start_message ;
local poll_end_message = module : require ( " message " ) . poll_end_message ;
local schedule_poll_update_message = module : require ( " message " ) . schedule_poll_update_message ;
2024-07-04 13:15:28 +00:00
local string_poll_invalid_choice = module : get_option_string ( " poll_string_invalid_choice " ) or " This choice is not valid. " ;
2024-07-04 15:39:40 +00:00
local string_poll_anonymous_vote_ok = module : get_option_string ( " poll_string_anonymous_vote_ok " ) or " Your vote is taken into account. Votes are anonymous, they will not be shown to other participants. " ;
2024-07-04 13:34:32 +00:00
local string_poll_over = module : get_option_string ( " poll_string_over " ) or " This poll is now over. " ;
2024-07-04 13:15:28 +00:00
2024-06-30 13:30:33 +00:00
local scheduled_end = { } ;
local function schedule_poll_purge ( room_jid )
module : log ( " debug " , " Scheduling a purge for poll %s " , room_jid ) ;
timer.add_task ( 30 , function ( )
module : log ( " info " , " Must purge poll for room %s " , room_jid ) ;
-- 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
module : log ( " debug " , " Room %s not found, was maybe destroyed " , room_jid ) ;
return ;
end
-- we must check if the poll is ended (could be a new poll!)
if not room._data . current_poll then
module : log ( " debug " , " Room %s has no current poll to purge " , room_jid ) ;
return ;
end
if not room._data . current_poll.already_ended then
module : log ( " debug " , " Room %s has has a poll that is not ended, must be a new one " , room_jid ) ;
return ;
end
module : log ( " info " , " Purging poll for room %s " , room_jid ) ;
room._data . current_poll = nil ;
end ) ;
end
2024-06-29 16:39:22 +00:00
local function end_current_poll ( room )
if not room._data . current_poll then
return ;
end
2024-06-30 13:30:33 +00:00
if room._data . current_poll.already_ended then
-- this can happen if the server was restarted before the purge
schedule_poll_purge ( room.jid ) ;
return ;
end
2024-06-29 17:33:37 +00:00
module : log ( " debug " , " Ending the current poll for room %s " , room.jid ) ;
2024-06-30 13:30:33 +00:00
room._data . current_poll.already_ended = true ;
if scheduled_end [ room.jid ] then
module : log ( " debug " , " Stopping the end timer for the poll " ) ;
timer.stop ( scheduled_end [ room_jid ] ) ;
scheduled_end [ room_jid ] = nil ;
end
2024-06-30 15:57:33 +00:00
2024-06-30 13:30:33 +00:00
poll_end_message ( room ) ;
2024-06-30 15:57:33 +00:00
2024-06-30 13:30:33 +00:00
-- TODO: store the result somewhere, to keep track?
-- We don't remove the poll immediatly. Indeed, if the vote is anonymous,
-- we don't want to expose votes from late users.
schedule_poll_purge ( room.jid ) ;
end
local function schedule_poll_end ( room_jid , timestamp )
2024-09-10 15:58:12 +00:00
local delay = timestamp - math.floor ( get_time ( ) ) ;
2024-06-30 13:30:33 +00:00
if delay <= 0 then
delay = 1 ;
end
module : log ( " debug " , " Must schedule a poll end in %i for room %s " , delay , room_jid ) ;
if scheduled_end [ room_jid ] then
module : log ( " debug " , " There is already a timer for the %s poll end, rescheduling " , room_jid ) ;
timer.reschedule ( scheduled_end [ room_jid ] , delay ) ;
return ;
end
scheduled_end [ room_jid ] = timer.add_task ( delay , function ( )
module : log ( " debug " , " Its time to end the poll for room %s " , room_jid ) ;
scheduled_end [ 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
module : log ( " debug " , " Room %s not found, was probably destroyed " , room_jid ) ;
return ; -- room was probably destroyed
end
end_current_poll ( room ) ;
end ) ;
2024-06-29 16:39:22 +00:00
end
2024-06-30 15:19:14 +00:00
local function create_poll ( room , fields , occupant )
module : log ( " debug " , " Creating a new poll for room %s, by %s " , room.jid , occupant.bare_jid ) ;
2024-06-29 16:39:22 +00:00
room._data . current_poll = fields ;
2024-07-01 10:36:32 +00:00
room._data . current_poll.poll_id = id.short ( ) ;
2024-09-10 15:58:12 +00:00
room._data . current_poll.end_timestamp = math.floor ( get_time ( ) ) + ( 60 * fields [ " muc#roompoll_duration " ] ) ;
2024-06-29 17:33:37 +00:00
room._data . current_poll.votes_by_occupant = { } ;
room._data . current_poll.votes_by_choices = { } ;
2024-07-01 10:04:03 +00:00
room._data . current_poll.choices_ordered = { } ; -- choices labels with numerical index, so we can have correct order
2024-06-30 13:30:33 +00:00
room._data . current_poll.already_ended = false ;
2024-06-30 15:19:14 +00:00
room._data . current_poll.occupant_bare_jid = occupant.bare_jid ;
room._data . current_poll.occupant_nick = occupant.nick ;
room._data . current_poll.occupant_id = room : get_occupant_id ( occupant ) ;
2024-06-29 17:33:37 +00:00
for field , _ in pairs ( fields ) do
local c = field : match ( " ^muc#roompoll_choice(%d+)$ " ) ;
if c then
if fields [ " muc#roompoll_choice " .. c ] : find ( " %S " ) then
room._data . current_poll.votes_by_choices [ c ] = 0 ;
2024-07-01 10:04:03 +00:00
table.insert ( room._data . current_poll.choices_ordered , {
number = c ;
label = fields [ " muc#roompoll_choice " .. c ] ;
} ) ;
2024-06-29 17:33:37 +00:00
end
end
end
2024-07-01 10:04:03 +00:00
table.sort ( room._data . current_poll.choices_ordered , function ( a , b )
if a.number == b.number then
return 0 ;
end
return tonumber ( a.number ) < tonumber ( b.number ) ;
end ) ;
2024-07-05 08:39:38 +00:00
poll_start_message ( room ) ;
2024-06-30 13:30:33 +00:00
schedule_poll_end ( room.jid , room._data . current_poll.end_timestamp ) ;
2024-06-29 16:39:22 +00:00
end
2024-06-29 17:33:37 +00:00
local function handle_groupchat ( event )
local origin , stanza = event.origin , event.stanza ;
local room = event.room ;
if not room._data . current_poll then
return ;
end
-- There is a current poll. Is this a vote?
local body = stanza : get_child_text ( " body " )
if not body or # body < 1 then
return ;
end
local choice = body : match ( " ^%s*!(%d+)%s*$ " ) ;
if not choice then
return ;
end
-- Ok, seems it is a vote.
2024-09-10 15:58:12 +00:00
if math.floor ( get_time ( ) ) >= room._data . current_poll.end_timestamp then
2024-06-29 17:33:37 +00:00
module : log ( " debug " , " Got a vote for a finished poll, not counting it. " ) ;
-- Note: we keep bouncing messages a few seconds/minutes after the poll end
-- to be sure any user that send the vote too late won't expose his choice.
origin.send ( st.error_reply (
stanza ,
-- error_type = 'cancel' (see descriptions in RFC 6120 https://xmpp.org/rfcs/rfc6120.html#stanzas-error-syntax)
" cancel " ,
-- error_condition = 'not-allowed' (see RFC 6120 Defined Error Conditions https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions)
" not-allowed " ,
2024-07-04 13:34:32 +00:00
string_poll_over
2024-06-29 17:33:37 +00:00
) ) ;
return true ; -- stop!
end
-- We must check that the choice is valid:
if room._data . current_poll.votes_by_choices [ choice ] == nil then
module : log ( " debug " , " Invalid vote, bouncing it. " ) ;
origin.send ( st.error_reply (
stanza ,
-- error_type = 'cancel' (see descriptions in RFC 6120 https://xmpp.org/rfcs/rfc6120.html#stanzas-error-syntax)
" cancel " ,
-- error_condition = 'not-allowed' (see RFC 6120 Defined Error Conditions https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions)
" bad-request " ,
2024-07-04 13:15:28 +00:00
string_poll_invalid_choice
2024-06-29 17:33:37 +00:00
) ) ;
return true ; -- stop!
end
-- Ok, we can count the vote.
local occupant = event.occupant ;
2024-06-30 13:30:33 +00:00
if not occupant then
module : log ( " warn " , " No occupant in the event, dont know how to count the vote " ) ;
return
end
2024-06-29 17:33:37 +00:00
2024-06-30 13:30:33 +00:00
local occupant_bare_id = occupant.bare_jid ;
module : log ( " debug " , " Counting a new vote for room %s: choice %i, voter %s " , room.jid , choice , occupant_bare_id ) ;
-- counting the vote:
if room._data . current_poll.votes_by_occupant [ occupant_bare_id ] ~= nil then
2024-06-30 15:57:33 +00:00
module : log ( " debug " , " Occupant %s has already voted for current room %s vote, reassigning his vote. " , occupant_bare_id , room.jid ) ;
2024-06-30 13:30:33 +00:00
room._data . current_poll.votes_by_choices [ room._data . current_poll.votes_by_occupant [ occupant_bare_id ] ] = room._data . current_poll.votes_by_choices [ room._data . current_poll.votes_by_occupant [ occupant_bare_id ] ] - 1 ;
end
room._data . current_poll.votes_by_choices [ choice ] = room._data . current_poll.votes_by_choices [ choice ] + 1 ;
room._data . current_poll.votes_by_occupant [ occupant_bare_id ] = choice ;
2024-06-30 15:57:33 +00:00
schedule_poll_update_message ( room.jid ) ;
2024-06-29 17:33:37 +00:00
-- When the poll is anonymous, we bounce the messages (but count the votes).
local must_bounce = room._data . current_poll [ " muc#roompoll_anonymous " ] == true ;
if must_bounce then
2024-06-30 15:57:33 +00:00
module : log ( " debug " , " Anonymous votes, bouncing it. " ) ;
2024-06-29 17:33:37 +00:00
origin.send ( st.error_reply (
stanza ,
-- error_type
" continue " ,
-- error_condition
" undefined-condition " ,
2024-07-04 13:15:28 +00:00
string_poll_anonymous_vote_ok
2024-06-29 17:33:37 +00:00
) ) ;
return true ; -- stop!
end
end
2024-06-30 13:30:33 +00:00
local function room_restored ( event )
local room = event.room ;
if not room._data . current_poll then
return ;
end
module : log ( " info " , " Restoring room %s with current ongoing poll. " , room.jid ) ;
2024-09-10 15:58:12 +00:00
local now = math.floor ( get_time ( ) ) ;
2024-06-30 13:30:33 +00:00
if now >= room._data . current_poll.end_timestamp then
module : log ( " info " , " Current poll is over for room %s, ending it " , room.jid ) ;
end_current_poll ( room ) ;
return ;
end
if scheduled_end [ room.jid ] then
module : log ( " info " , " Poll for room %s is not finished yet, the end is still scheduled " , room.jid ) ;
else
module : log ( " info " , " Poll for room %s is not finished yet, rescheduling the end " , room.jid ) ;
schedule_poll_end ( room.jid , room._data . current_poll.end_timestamp ) ;
end
-- just in case, we can also reschedule an update message
2024-06-30 15:57:33 +00:00
schedule_poll_update_message ( room.jid ) ;
2024-06-30 13:30:33 +00:00
end
2024-06-29 16:39:22 +00:00
return {
end_current_poll = end_current_poll ;
create_poll = create_poll ;
2024-06-29 17:33:37 +00:00
handle_groupchat = handle_groupchat ;
2024-06-30 13:30:33 +00:00
room_restored = room_restored ;
2024-06-29 16:39:22 +00:00
} ;