Poll WIP (#231):

* refactoring update messages, for a more efficient and proper way to
  handle it
This commit is contained in:
John Livingston 2024-07-05 10:39:38 +02:00
parent 0983c8ed57
commit 6dda0cc44f
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 64 additions and 39 deletions

View File

@ -74,8 +74,11 @@ converse.plugins.add('livechat-converse-poll', {
}
// We will also translate some strings here.
// eslint-disable-next-line no-undef
const body = (attrs.body ?? '').replace(LOC_poll_is_over, __(LOC_poll_is_over))
const body = (attrs.body ?? '')
// eslint-disable-next-line no-undef
.replace(LOC_poll_is_over, __(LOC_poll_is_over))
// eslint-disable-next-line no-undef
.replace(LOC_poll_vote_instructions_xmpp, __(LOC_poll_vote_instructions)) // changing instructions on the fly
return Object.assign(
attrs,
@ -93,15 +96,11 @@ converse.plugins.add('livechat-converse-poll', {
if (!attrs.current_poll) {
return this.__super__.onMessage(attrs)
}
// We intercept poll messages, to show the banner.
// Note: we also show poll end messages in the chat, so that the user don't loose the result.
if (attrs.is_delayed || attrs.is_archived) {
if (attrs.current_poll.over) {
console.info('Got a delayed/archived poll message for an poll that is over, just displaying in the chat')
return this.__super__.onMessage(attrs)
}
console.info('Got a delayed/archived poll message, just dropping')
return
// We just drop archived messages, to not show the banner for finished polls.
if (attrs.is_archived) {
return this.__super__.onMessage(attrs)
}
console.info('Got a poll message, setting it as the current_poll')
@ -109,11 +108,7 @@ converse.plugins.add('livechat-converse-poll', {
// which is inserted in the DOM by the muc.js template overload.
this.set('current_poll', attrs.current_poll)
if (attrs.current_poll.over) {
console.info('The poll is over, displaying the message in the chat')
return this.__super__.onMessage(attrs)
}
// Dropping the message.
return this.__super__.onMessage(attrs)
}
}
}

View File

@ -46,6 +46,7 @@ const locKeys = [
'poll_end',
'poll',
'poll_vote_instructions',
'poll_vote_instructions_xmpp',
'poll_is_over',
'poll_choice_invalid',
'poll_anonymous_vote_ok'

View File

@ -575,6 +575,8 @@ poll_choice_n: 'Choice {{N}}:'
poll_end: 'Poll ends at:'
poll_vote_instructions: |
To vote, click on your choice or send a message with an exclamation mark followed by your choice number (Example: !1).
poll_vote_instructions_xmpp: |
Send a message with an exclamation mark followed by your choice number to vote. Example: !1
poll_is_over: This poll is now over.
poll_choice_invalid: This choice is not valid.
poll_anonymous_vote_ok: Your vote is taken into account. Votes are anonymous, they will not be shown to other participants.

View File

@ -20,13 +20,12 @@ local scheduled_updates = {};
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";
-- construct the poll message stanza
local function build_poll_message(room, message_id, is_end_message)
-- Build the content for poll start and end messages (that will go to the message <body>)
local function build_poll_message_content(room, is_end_message)
local current_poll = room._data.current_poll;
if not current_poll then
return nil;
end
local from = current_poll.occupant_nick; -- this is in fact room.jid/nickname
local content = current_poll["muc#roompoll_question"] .. "\n";
@ -41,7 +40,8 @@ local function build_poll_message(room, message_id, is_end_message)
for _, choice_desc in ipairs(current_poll.choices_ordered) do
local choice, label = choice_desc.number, choice_desc.label;
content = content .. choice .. ': ' .. label;
if total > 0 then
-- if vote over, and at least 1 vote, we add the results.
if is_end_message and total > 0 then
local nb = current_poll.votes_by_choices[choice] or 0;
local percent = string.format("%.2f", nb * 100 / total);
content = content .. " (" .. nb .. "/" .. total .. " = " .. percent .. "%)";
@ -53,10 +53,23 @@ local function build_poll_message(room, message_id, is_end_message)
content = content .. string_poll_vote_instructions .. "\n";
end
return content;
end
-- construct the poll message stanza.
-- Note: content can be nil, for updates messages.
local function build_poll_message(room, content)
local current_poll = room._data.current_poll;
if not current_poll then
return nil;
end
local from = current_poll.occupant_nick; -- this is in fact room.jid/nickname
local msg = st.message({
type = "groupchat",
from = from,
id = message_id
id = id.long()
}, content);
msg:tag("occupant-id", {
@ -64,6 +77,14 @@ local function build_poll_message(room, message_id, is_end_message)
id = current_poll.occupant_id
}):up();
if content == nil then
-- No content, this is an update message.
-- Adding some hints (XEP-0334):
msg:tag("no-copy", { xmlns = "urn:xmpp:hints" }):up();
msg:tag("no-store", { xmlns = "urn:xmpp:hints" }):up();
msg:tag("no-permanent-store", { xmlns = "urn:xmpp:hints" }):up();
end
-- now we must add some custom XML data, so that compatible clients can display the poll as they want:
-- <x-poll xmlns="http://jabber.org/protocol/muc#x-poll-message" id="I9UWyoxsz4BN" votes="1" end="1719842224" over="">
-- <x-poll-question>Poll question</x-poll-question>
@ -72,6 +93,11 @@ local function build_poll_message(room, message_id, is_end_message)
-- <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 total = 0;
for choice, nb in pairs(current_poll.votes_by_choices) do
total = total + nb;
end
local message_attrs = {
xmlns = xmlns_poll_message,
id = current_poll.poll_id,
@ -85,6 +111,7 @@ local function build_poll_message(room, message_id, is_end_message)
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;
total = total + nb;
msg:text_tag(poll_choice_tag, label, {
votes = "" .. nb,
choice = choice
@ -101,10 +128,9 @@ local function poll_start_message(room)
return nil;
end
module:log("debug", "Sending the start message for room %s poll", room.jid);
local message_id = id.medium();
local msg = build_poll_message(room, message_id, false);
local content = build_poll_message_content(room, false);
local msg = build_poll_message(room, content);
room:broadcast_message(msg);
return message_id;
end
-- Send the poll update message
@ -118,17 +144,9 @@ local function send_poll_update_message(room)
end
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();
local msg = build_poll_message(room, nil);
room:broadcast_message(msg);
return message_id;
end
-- Schedule an update of the start message.
@ -162,10 +180,9 @@ local function poll_end_message(room)
timer.stop(scheduled_updates[room.jid]);
scheduled_updates[room.jid] = nil;
end
local message_id = id.medium(); -- generate a new id
local msg = build_poll_message(room, message_id, true);
local content = build_poll_message_content(room, true);
local msg = build_poll_message(room, content);
room:broadcast_message(msg);
return message_id;
end
-- security check: we must remove all specific tags, to be sure nobody tries to spoof polls!
@ -187,14 +204,23 @@ end
-- 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;
local occupant = event.occupant;
local origin = event.origin;
if not occupant then
return;
end
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.
-- Sending an update message to the new occupant.
module:log("debug", "Sending a poll update message to new occupant %s", occupant.jid);
local msg = build_poll_message(room, nil);
msg.attr.to = occupant.jid;
origin.send(msg);
end
return {

View File

@ -96,4 +96,4 @@ module:hook("muc-room-restored", room_restored);
-- when a new session is opened, we must send the current poll to the client
-- Note: it should be in the MAM. But it is easier for clients to ignore delayed messages
-- when displaying polls (to ignore old polls).
module:hook("muc-occupant-session-new", handle_new_occupant_session);
module:hook("muc-occupant-session-new", handle_new_occupant_session, 10); -- must be after subject (20, see Prosody code)

View File

@ -128,7 +128,7 @@ local function create_poll(room, fields, occupant)
return tonumber(a.number) < tonumber(b.number);
end);
room._data.current_poll.start_message_id = poll_start_message(room);
poll_start_message(room);
schedule_poll_end(room.jid, room._data.current_poll.end_timestamp);
end

View File

@ -540,6 +540,7 @@ class ProsodyConfigContent {
this.muc.set('poll_string_over', loc('poll_is_over'))
this.muc.set('poll_string_invalid_choice', loc('poll_choice_invalid'))
this.muc.set('poll_string_anonymous_vote_ok', loc('poll_anonymous_vote_ok'))
this.muc.set('poll_string_vote_instructions', loc('poll_vote_instructions_xmpp'))
}
addMucAdmins (jids: string[]): void {