diff --git a/prosody-modules/mod_muc_poll/constants.lib.lua b/prosody-modules/mod_muc_poll/constants.lib.lua
new file mode 100644
index 00000000..759e274c
--- /dev/null
+++ b/prosody-modules/mod_muc_poll/constants.lib.lua
@@ -0,0 +1,9 @@
+-- SPDX-FileCopyrightText: 2024 John Livingston
+-- SPDX-License-Identifier: AGPL-3.0-only
+
+-- FIXME: create a XEP to standardize this, and remove the "x-".
+local xmlns_poll = "http://jabber.org/protocol/muc#x-poll";
+
+return {
+ xmlns_poll = xmlns_poll;
+};
diff --git a/prosody-modules/mod_muc_poll/form.lib.lua b/prosody-modules/mod_muc_poll/form.lib.lua
new file mode 100644
index 00000000..169f17eb
--- /dev/null
+++ b/prosody-modules/mod_muc_poll/form.lib.lua
@@ -0,0 +1,152 @@
+-- SPDX-FileCopyrightText: 2024 John Livingston
+-- SPDX-License-Identifier: AGPL-3.0-only
+
+local st = require "util.stanza";
+local dataform = require "util.dataforms";
+local get_form_type = require "util.dataforms".get_type;
+local xmlns_poll = module:require("constants").xmlns_poll;
+
+local end_current_poll = module:require("poll").end_current_poll;
+local create_poll = module:require("poll").create_poll;
+
+local function get_form_layout(room, stanza)
+ local form = dataform.new({
+ title = "New poll",
+ instructions = "Complete and submit this form to create a new poll. This will end and replace any existing poll.",
+ {
+ name = "FORM_TYPE";
+ type = "hidden";
+ value = xmlns_poll;
+ }
+ });
+
+ table.insert(form, {
+ name = "muc#roompoll_question";
+ type = "text-single";
+ label = "Question";
+ desc = "The poll question.";
+ value = "";
+ required = true;
+ });
+ table.insert(form, {
+ name = "muc#roompoll_duration";
+ type = "text-single";
+ datatype = "xs:integer";
+ range_min = 1;
+ label = "Poll duration (in minutes)";
+ desc = "The number of minutes to run the poll.";
+ value = "";
+ required = true;
+ });
+ table.insert(form, {
+ name = "muc#roompoll_anonymous";
+ type = "boolean";
+ label = "Anonymous results";
+ desc = "By enabling this, user's votes won't be publicly shown in the room.";
+ });
+ table.insert(form, {
+ name = "muc#roompoll_choice1";
+ type = "text-single";
+ label = "Choice 1";
+ desc = "";
+ value = "";
+ required = true;
+ });
+ table.insert(form, {
+ name = "muc#roompoll_choice2";
+ type = "text-single";
+ label = "Choice 2";
+ desc = "";
+ value = "";
+ required = true;
+ });
+ table.insert(form, {
+ name = "muc#roompoll_choice3";
+ type = "text-single";
+ label = "Choice 3";
+ desc = "";
+ value = "";
+ });
+ table.insert(form, {
+ name = "muc#roompoll_choice4";
+ type = "text-single";
+ label = "Choice 4";
+ desc = "";
+ value = "";
+ });
+ table.insert(form, {
+ name = "muc#roompoll_choice5";
+ type = "text-single";
+ label = "Choice 5";
+ desc = "";
+ value = "";
+ });
+
+ return form;
+end
+
+local function send_form(room, origin, stanza)
+ module:log("debug", "Sending the poll form");
+ origin.send(st.reply(stanza):query(xmlns_poll)
+ :add_child(get_form_layout(room, stanza.attr.from):form())
+) ;
+end
+
+local function dataform_error_message(err)
+ local out = {};
+ for field, errmsg in pairs(err) do
+ table.insert(out, ("%s: %s"):format(field, errmsg))
+ end
+ return table.concat(out, "; ");
+end
+
+
+local function process_form(room, origin, stanza)
+ if not stanza.tags[1] then
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ return true;
+ end
+ local form = stanza.tags[1]:get_child("x", "jabber:x:data");
+ if not form then
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ return true;
+ end
+
+ local form_type, err = get_form_type(form);
+ if not form_type then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
+ return true;
+ elseif form_type ~= xmlns_poll then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_poll.."'"));
+ return true;
+ end
+
+ if form.attr.type == "cancel" then
+ origin.send(st.reply(stanza));
+ return true;
+ elseif form.attr.type ~= "submit" then
+ origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form"));
+ return true;
+ end
+
+ -- form submitted
+ local fields, errors, present = get_form_layout(room, stanza.attr.from):data(form);
+ if errors then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(errors)));
+ return true;
+ end
+
+ -- stop any poll that is already here
+ end_current_poll(room);
+
+ -- create the new poll
+ create_poll(room, fields);
+
+ origin.send(st.reply(stanza));
+ return true;
+end
+
+return {
+ send_form = send_form;
+ process_form = process_form;
+};
diff --git a/prosody-modules/mod_muc_poll/mod_muc_poll.lua b/prosody-modules/mod_muc_poll/mod_muc_poll.lua
index 43e809ba..2af76e34 100644
--- a/prosody-modules/mod_muc_poll/mod_muc_poll.lua
+++ b/prosody-modules/mod_muc_poll/mod_muc_poll.lua
@@ -1,165 +1,23 @@
+-- mod_muc_poll
+--
+-- SPDX-FileCopyrightText: 2024 John Livingston
+-- SPDX-License-Identifier: AGPL-3.0-only
+--
+-- This file is AGPL-v3 licensed.
+-- Please see the Peertube livechat plugin copyright information.
+-- https://livingston.frama.io/peertube-plugin-livechat/credits/
+--
+-- Implements: XEP-????: MUC Poll (XEP to come).
+
local st = require "util.stanza";
-local dataform = require "util.dataforms";
-local get_form_type = require "util.dataforms".get_type;
local jid_bare = require "util.jid".bare;
-local get_time = require "util.time".now;
local mod_muc = module:depends"muc";
local get_room_from_jid = mod_muc.get_room_from_jid;
--- FIXME: create a XEP to standardize this, and remove the "x-".
-local xmlns_poll = "http://jabber.org/protocol/muc#x-poll";
-
-local function get_form_layout(room, stanza)
- local form = dataform.new({
- title = "New poll",
- instructions = "Complete and submit this form to create a new poll. This will end and replace any existing poll.",
- {
- name = "FORM_TYPE";
- type = "hidden";
- value = xmlns_poll;
- }
- });
-
- table.insert(form, {
- name = "muc#roompoll_question";
- type = "text-single";
- label = "Question";
- desc = "The poll question.";
- value = "";
- required = true;
- });
- table.insert(form, {
- name = "muc#roompoll_duration";
- type = "text-single";
- datatype = "xs:integer";
- range_min = 1;
- label = "Poll duration (in minutes)";
- desc = "The number of minutes to run the poll.";
- value = "";
- required = true;
- });
- table.insert(form, {
- name = "muc#roompoll_anonymous";
- type = "boolean";
- label = "Anonymous results";
- desc = "By enabling this, user's votes won't be publicly shown in the room.";
- });
- table.insert(form, {
- name = "muc#roompoll_choice1";
- type = "text-single";
- label = "Choice 1";
- desc = "";
- value = "";
- required = true;
- });
- table.insert(form, {
- name = "muc#roompoll_choice2";
- type = "text-single";
- label = "Choice 2";
- desc = "";
- value = "";
- required = true;
- });
- table.insert(form, {
- name = "muc#roompoll_choice3";
- type = "text-single";
- label = "Choice 3";
- desc = "";
- value = "";
- });
- table.insert(form, {
- name = "muc#roompoll_choice4";
- type = "text-single";
- label = "Choice 4";
- desc = "";
- value = "";
- });
- table.insert(form, {
- name = "muc#roompoll_choice5";
- type = "text-single";
- label = "Choice 5";
- desc = "";
- value = "";
- });
-
- return form;
-end
-
-local function send_form(room, origin, stanza)
- module:log("debug", "Sending the poll form");
- origin.send(st.reply(stanza):query(xmlns_poll)
- :add_child(get_form_layout(room, stanza.attr.from):form())
-) ;
-end
-
-local function dataform_error_message(err)
- local out = {};
- for field, errmsg in pairs(err) do
- table.insert(out, ("%s: %s"):format(field, errmsg))
- end
- return table.concat(out, "; ");
-end
-
-local function process_form(room, origin, stanza)
- if not stanza.tags[1] then
- origin.send(st.error_reply(stanza, "modify", "bad-request"));
- return true;
- end
- local form = stanza.tags[1]:get_child("x", "jabber:x:data");
- if not form then
- origin.send(st.error_reply(stanza, "modify", "bad-request"));
- return true;
- end
-
- local form_type, err = get_form_type(form);
- if not form_type then
- origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
- return true;
- elseif form_type ~= xmlns_poll then
- origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_poll.."'"));
- return true;
- end
-
- if form.attr.type == "cancel" then
- origin.send(st.reply(stanza));
- return true;
- elseif form.attr.type ~= "submit" then
- origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form"));
- return true;
- end
-
- -- form submitted
- local fields, errors, present = get_form_layout(room, stanza.attr.from):data(form);
- if errors then
- origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(errors)));
- return true;
- end
-
- -- stop any poll that is already here
- end_current_poll(room);
-
- -- create the new poll
- create_poll(room, fields);
-
- origin.send(st.reply(stanza));
- return true;
-end
-
-local function end_current_poll (room)
- if not room._data.current_poll then
- return;
- end
- -- TODO: compute and send last result.
- room._data.current_poll = nil;
-end
-
-local function create_poll(room, fields)
- room._data.current_poll = fields;
- room._data.current_poll.end_timestamp = now() + (60 * fields["muc#roompoll_duration"]);
- room._data.current_poll.votes = {};
- -- TODO: create and send poll message.
-end
+local xmlns_poll = module:require("constants").xmlns_poll;
+local send_form = module:require("form").send_form;
+local process_form = module:require("form").process_form;
-- new poll creation, get form
module:hook("iq-get/bare/" .. xmlns_poll .. ":query", function (event)
@@ -183,7 +41,6 @@ module:hook("iq-get/bare/" .. xmlns_poll .. ":query", function (event)
return true;
end);
-
-- new poll creation, form submission
module:hook("iq-set/bare/" .. xmlns_poll .. ":query", function (event)
local origin, stanza = event.origin, event.stanza;
diff --git a/prosody-modules/mod_muc_poll/poll.lib.lua b/prosody-modules/mod_muc_poll/poll.lib.lua
new file mode 100644
index 00000000..8b1607e8
--- /dev/null
+++ b/prosody-modules/mod_muc_poll/poll.lib.lua
@@ -0,0 +1,24 @@
+-- SPDX-FileCopyrightText: 2024 John Livingston
+-- SPDX-License-Identifier: AGPL-3.0-only
+
+local get_time = require "util.time".now;
+
+local function end_current_poll (room)
+ if not room._data.current_poll then
+ return;
+ end
+ -- TODO: compute and send last result.
+ room._data.current_poll = nil;
+end
+
+local function create_poll(room, fields)
+ room._data.current_poll = fields;
+ room._data.current_poll.end_timestamp = now() + (60 * fields["muc#roompoll_duration"]);
+ room._data.current_poll.votes = {};
+ -- TODO: create and send poll message.
+end
+
+return {
+ end_current_poll = end_current_poll;
+ create_poll = create_poll;
+};