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 {