diff --git a/conversejs/custom/index.js b/conversejs/custom/index.js
index d5fda0a1..51d352c5 100644
--- a/conversejs/custom/index.js
+++ b/conversejs/custom/index.js
@@ -47,6 +47,7 @@ import './plugins/fullscreen/index.js'
import '../custom/plugins/size/index.js'
import '../custom/plugins/tasks/index.js'
import '../custom/plugins/terms/index.js'
+import '../custom/plugins/poll/index.js'
/* END: Removable components */
import { CORE_PLUGINS } from './headless/shared/constants.js'
@@ -55,6 +56,7 @@ import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
CORE_PLUGINS.push('livechat-converse-size')
CORE_PLUGINS.push('livechat-converse-tasks')
CORE_PLUGINS.push('livechat-converse-terms')
+CORE_PLUGINS.push('livechat-converse-poll')
// We must also add our custom ROOM_FEATURES, so that they correctly resets
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
diff --git a/conversejs/custom/plugins/poll/constants.js b/conversejs/custom/plugins/poll/constants.js
new file mode 100644
index 00000000..a7eceaf1
--- /dev/null
+++ b/conversejs/custom/plugins/poll/constants.js
@@ -0,0 +1,5 @@
+// SPDX-FileCopyrightText: 2024 John Livingston
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+
+export const XMLNS_POLL = 'http://jabber.org/protocol/muc#x-poll'
diff --git a/conversejs/custom/plugins/poll/index.js b/conversejs/custom/plugins/poll/index.js
new file mode 100644
index 00000000..798b1037
--- /dev/null
+++ b/conversejs/custom/plugins/poll/index.js
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: 2024 John Livingston
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { _converse, converse } from '../../../src/headless/core.js'
+import { getHeadingButtons } from './utils.js'
+// import { XMLNS_POLL } from './constants.js'
+
+converse.plugins.add('livechat-converse-poll', {
+ dependencies: ['converse-muc', 'converse-disco'],
+
+ initialize () {
+ // _converse.api.listen.on('chatRoomInitialized', muc => {
+ // muc.features.on('change:' + XMLNS_POLL, () => {
+ // // TODO: refresh headingbuttons?
+ // })
+ // })
+ // adding the poll actions in the MUC heading buttons:
+ _converse.api.listen.on('getHeadingButtons', getHeadingButtons)
+ }
+})
diff --git a/conversejs/custom/plugins/poll/utils.js b/conversejs/custom/plugins/poll/utils.js
new file mode 100644
index 00000000..acddfcab
--- /dev/null
+++ b/conversejs/custom/plugins/poll/utils.js
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2024 John Livingston
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { XMLNS_POLL } from './constants.js'
+import { converse, _converse, api } from '../../../src/headless/core.js'
+import { __ } from 'i18n'
+const $iq = converse.env.$iq
+
+function _fetchPollForm (mucModel) {
+ return api.sendIQ(
+ $iq({
+ to: mucModel.get('jid'),
+ type: 'get'
+ }).c('query', { xmlns: XMLNS_POLL })
+ )
+}
+
+export function getHeadingButtons (view, buttons) {
+ const muc = view.model
+ if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
+ // only on MUC.
+ return buttons
+ }
+
+ if (!muc.features?.get?.(XMLNS_POLL)) {
+ // Poll feature not available (can happen if the chat is remote, and the plugin not up to date)
+ return buttons
+ }
+
+ const myself = muc.getOwnOccupant()
+ if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
+ return buttons
+ }
+
+ // Adding a "New poll" button.
+ buttons.unshift({
+ // eslint-disable-next-line no-undef
+ i18n_text: __(LOC_new_poll),
+ handler: async (ev) => {
+ ev.preventDefault()
+ const r = await _fetchPollForm(muc)
+ // FIXME
+ console.info('Received poll form', r)
+ },
+ a_class: '',
+ icon_class: 'fa-list-check', // FIXME
+ name: 'muc-create-poll'
+ })
+
+ return buttons
+}
diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js
index 5baacb04..ba601544 100644
--- a/conversejs/loc.keys.js
+++ b/conversejs/loc.keys.js
@@ -35,7 +35,8 @@ const locKeys = [
'task_list_pick_title',
'task_list_pick_empty',
'task_list_pick_message',
- 'muted_anonymous_message'
+ 'muted_anonymous_message',
+ 'new_poll'
]
module.exports = locKeys
diff --git a/languages/en.yml b/languages/en.yml
index a05e26a2..b29d1491 100644
--- a/languages/en.yml
+++ b/languages/en.yml
@@ -563,3 +563,5 @@ livechat_configuration_channel_mute_anonymous_desc: |
livechat_configuration_channel_terms_label: "Channel's chat terms & conditions"
livechat_configuration_channel_terms_desc: |
You can configure a "terms & conditions" message that will be shown to users joining your chatrooms.
+
+new_poll: Create a new poll
diff --git a/prosody-modules/mod_muc_poll/README.md b/prosody-modules/mod_muc_poll/README.md
new file mode 100644
index 00000000..f29d7621
--- /dev/null
+++ b/prosody-modules/mod_muc_poll/README.md
@@ -0,0 +1,15 @@
+
+# mod_muc_slow_pool
+
+This module provide a way to create polls in MUC rooms.
+
+This module is part of peertube-plugin-livechat, and is under the same LICENSE.
+
+There will probably be a XEP proposal for this module behaviour. When done, this module will be published in the prosody-modules repository.
+
+## Configuration
+
+Just enable the module on your MUC component.
diff --git a/prosody-modules/mod_muc_poll/mod_muc_poll.lua b/prosody-modules/mod_muc_poll/mod_muc_poll.lua
new file mode 100644
index 00000000..6386d493
--- /dev/null
+++ b/prosody-modules/mod_muc_poll/mod_muc_poll.lua
@@ -0,0 +1,70 @@
+local st = require "util.stanza";
+local dataform = require "util.dataforms";
+local jid_bare = require "util.jid".bare;
+
+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";
+ label = "Question";
+ 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
+
+-- module:hook("iq/bare", function (event)
+-- local stanza = event.stanza;
+-- local type = stanza.attr.type;
+-- local child = stanza.tags[1];
+-- local xmlns = child.attr.xmlns or "jabber:client";
+-- module:log("info", "coucou %s %s %s", type, xmlns, child.name);
+-- end, 1000);
+
+-- new poll creation, get form
+module:hook("iq-get/bare/" .. xmlns_poll .. ":query", function (event)
+ local origin, stanza = event.origin, event.stanza;
+ local room_jid = stanza.attr.to;
+ module:log("debug", "Received a request for the poll form");
+ local room = get_room_from_jid(room_jid);
+ if not room then
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ return true;
+ end
+ local from = jid_bare(stanza.attr.from);
+
+ local from_affiliation = room:get_affiliation(from);
+ if (from_affiliation ~= "owner" and from_affiliation ~= "admin") then
+ origin.send(st.error_reply(stanza, "auth", "forbidden"))
+ return true;
+ end
+
+ send_form(room, origin, stanza);
+ return true;
+end);
+
+-- Discovering support
+module:hook("muc-disco#info", function (event)
+ event.reply:tag("feature", { var = xmlns_poll }):up();
+end);
diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts
index 9853c302..6aa8d99c 100644
--- a/server/lib/prosody/config/content.ts
+++ b/server/lib/prosody/config/content.ts
@@ -248,6 +248,8 @@ class ProsodyConfigContent {
if (chatTerms) {
this.muc.set('muc_terms_global', new ConfigEntryValueMultiLineString(chatTerms))
}
+
+ this.muc.add('modules_enabled', 'muc_poll')
}
useAnonymous (autoBanIP: boolean): void {