import './modals/occupant.js'; import './modals/moderator-tools.js'; import log from "@converse/headless/log"; import tplSpinner from 'templates/spinner.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; import { html } from "lit"; import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js'; const { Strophe, u } = converse.env; const COMMAND_TO_AFFILIATION = { 'admin': 'admin', 'ban': 'outcast', 'member': 'member', 'owner': 'owner', 'revoke': 'none' }; const COMMAND_TO_ROLE = { 'deop': 'participant', 'kick': 'none', 'mute': 'visitor', 'op': 'moderator', 'voice': 'participant' }; /** * @async * Presents a confirmation modal to the user asking them to accept or decline a * MUC invitation. */ export function confirmDirectMUCInvitation ({ contact, jid, reason }) { if (!reason) { return api.confirm(__('%1$s has invited you to join a groupchat: %2$s', contact, jid)); } else { return api.confirm( __( '%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"', contact, jid, reason ) ); } } export function clearHistory (jid) { if (_converse.router.history.getFragment() === `converse/room?jid=${jid}`) { _converse.router.navigate(''); } } export async function destroyMUC (model) { const messages = [__('Are you sure you want to destroy this groupchat?')]; let fields = [ { 'name': 'challenge', 'label': __('Please enter the XMPP address of this groupchat to confirm'), 'challenge': model.get('jid'), 'placeholder': __('name@example.org'), 'required': true }, { 'name': 'reason', 'label': __('Optional reason for destroying this groupchat'), 'placeholder': __('Reason') }, { 'name': 'newjid', 'label': __('Optional XMPP address for a new groupchat that replaces this one'), 'placeholder': __('replacement@example.org') } ]; try { fields = await api.confirm(__('Confirm'), messages, fields); const reason = fields.filter(f => f.name === 'reason').pop()?.value; const newjid = fields.filter(f => f.name === 'newjid').pop()?.value; return model.sendDestroyIQ(reason, newjid).then(() => model.close()); } catch (e) { log.error(e); } } export function getNicknameRequiredTemplate (model) { const jid = model.get('jid'); if (api.settings.get('muc_show_logs_before_join')) { return html``; } else { return html``; } } export function getChatRoomBodyTemplate (o) { const view = o.model.session.get('view'); const jid = o.model.get('jid'); const RS = converse.ROOMSTATUS; const conn_status = o.model.session.get('connection_status'); if (view === converse.MUC.VIEWS.CONFIG) { return html``; } else { return html` ${ conn_status == RS.PASSWORD_REQUIRED ? html`` : '' } ${ conn_status == RS.ENTERED ? html`` : '' } ${ conn_status == RS.CONNECTING ? tplSpinner() : '' } ${ conn_status == RS.NICKNAME_REQUIRED ? getNicknameRequiredTemplate(o.model) : '' } ${ conn_status == RS.DISCONNECTED ? html`` : '' } ${ conn_status == RS.BANNED ? html`` : '' } ${ conn_status == RS.DESTROYED ? html`` : '' } `; } } export function getAutoCompleteListItem (text, input) { input = input.trim(); const element = document.createElement('li'); element.setAttribute('aria-selected', 'false'); if (api.settings.get('muc_mention_autocomplete_show_avatar')) { const img = document.createElement('img'); let dataUri = 'data:' + _converse.DEFAULT_IMAGE_TYPE + ';base64,' + _converse.DEFAULT_IMAGE; if (_converse.vcards) { const vcard = _converse.vcards.findWhere({ 'nickname': text }); if (vcard) dataUri = 'data:' + vcard.get('image_type') + ';base64,' + vcard.get('image'); } img.setAttribute('src', dataUri); img.setAttribute('width', '22'); img.setAttribute('class', 'avatar avatar-autocomplete'); element.appendChild(img); } const regex = new RegExp('(' + input + ')', 'ig'); const parts = input ? text.split(regex) : [text]; parts.forEach(txt => { if (input && txt.match(regex)) { const match = document.createElement('mark'); match.textContent = txt; element.appendChild(match); } else { element.appendChild(document.createTextNode(txt)); } }); return element; } export async function getAutoCompleteList () { const models = [...(await api.rooms.get()), ...(await api.contacts.get())]; const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))]; return jids; } function setRole (muc, command, args, required_affiliations = [], required_roles = []) { const role = COMMAND_TO_ROLE[command]; if (!role) { throw Error(`ChatRoomView#setRole called with invalid command: ${command}`); } if (!muc.verifyAffiliations(required_affiliations) || !muc.verifyRoles(required_roles)) { return false; } if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) { return false; } const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args); if (!nick_or_jid) { return false; } const reason = args.split(nick_or_jid, 2)[1].trim(); // We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs const occupant = muc.getOccupant(nick_or_jid); muc.setRole(occupant, role, reason, undefined, e => muc.onCommandError(e)); return true; } function verifyAndSetAffiliation (muc, command, args, required_affiliations) { const affiliation = COMMAND_TO_AFFILIATION[command]; if (!affiliation) { throw Error(`verifyAffiliations called with invalid command: ${command}`); } if (!muc.verifyAffiliations(required_affiliations)) { return false; } if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) { return false; } const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args); if (!nick_or_jid) { return false; } let jid; const reason = args.split(nick_or_jid, 2)[1].trim(); const occupant = muc.getOccupant(nick_or_jid); if (occupant) { jid = occupant.get('jid'); } else { if (u.isValidJID(nick_or_jid)) { jid = nick_or_jid; } else { const message = __( "Couldn't find a participant with that nickname. " + 'They might have left the groupchat.' ); muc.createMessage({ message, 'type': 'error' }); return; } } const attrs = { jid, reason }; if (occupant && api.settings.get('auto_register_muc_nickname')) { attrs['nick'] = occupant.get('nick'); } setAffiliation(affiliation, muc.get('jid'), [attrs]) .then(() => muc.occupants.fetchMembers()) .catch(err => muc.onCommandError(err)); } export function showModeratorToolsModal (muc, affiliation) { if (!muc.verifyRoles(['moderator'])) { return; } let modal = api.modal.get('converse-modtools-modal'); if (modal) { modal.affiliation = affiliation; modal.render(); } else { modal = api.modal.create('converse-modtools-modal', { affiliation, 'jid': muc.get('jid') }); } modal.show(); } export function showOccupantModal (ev, occupant) { api.modal.show('converse-muc-occupant-modal', { 'model': occupant }, ev); } export function parseMessageForMUCCommands (data, handled) { const model = data.model; if (handled || model.get('type') !== _converse.CHATROOMS_TYPE || ( api.settings.get('muc_disable_slash_commands') && !Array.isArray(api.settings.get('muc_disable_slash_commands')) )) { return handled; } let text = data.text; text = text.replace(/^\s*/, ''); const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase(); if (!command) { return false; } const args = text.slice(('/' + command).length + 1).trim(); const allowed_commands = model.getAllowedCommands() ?? []; if (command === 'admin' && allowed_commands.includes(command)) { verifyAndSetAffiliation(model, command, args, ['owner']); return true; } else if (command === 'ban' && allowed_commands.includes(command)) { verifyAndSetAffiliation(model, command, args, ['admin', 'owner']); return true; } else if (command === 'modtools' && allowed_commands.includes(command)) { showModeratorToolsModal(model, args); return true; } else if (command === 'deop' && allowed_commands.includes(command)) { // FIXME: /deop only applies to setting a moderators // role to "participant" (which only admin/owner can // do). Moderators can however set non-moderator's role // to participant (e.g. visitor => participant). // Currently we don't distinguish between these two // cases. setRole(model, command, args, ['admin', 'owner']); return true; } else if (command === 'destroy' && allowed_commands.includes(command)) { if (!model.verifyAffiliations(['owner'])) { return true; } destroyMUC(model).catch(e => model.onCommandError(e)); return true; } else if (command === 'help' && allowed_commands.includes(command)) { model.set({ 'show_help_messages': false }, { 'silent': true }); model.set({ 'show_help_messages': true }); return true; } else if (command === 'kick' && allowed_commands.includes(command)) { setRole(model, command, args, [], ['moderator']); return true; } else if (command === 'mute' && allowed_commands.includes(command)) { setRole(model, command, args, [], ['moderator']); return true; } else if (command === 'member' && allowed_commands.includes(command)) { verifyAndSetAffiliation(model, command, args, ['admin', 'owner']); return true; } else if (command === 'nick' && allowed_commands.includes(command)) { if (!model.verifyRoles(['visitor', 'participant', 'moderator'])) { return true; } else if (args.length === 0) { // e.g. Your nickname is "coolguy69" const message = __('Your nickname is "%1$s"', model.get('nick')); model.createMessage({ message, 'type': 'error' }); } else { model.setNickname(args); } return true; } else if (command === 'owner' && allowed_commands.includes(command)) { verifyAndSetAffiliation(model, command, args, ['owner']); return true; } else if (command === 'op' && allowed_commands.includes(command)) { setRole(model, command, args, ['admin', 'owner']); return true; } else if (command === 'register' && allowed_commands.includes(command)) { if (args.length > 1) { model.createMessage({ 'message': __('Error: invalid number of arguments'), 'type': 'error' }); } else { model.registerNickname().then(err_msg => { err_msg && model.createMessage({ 'message': err_msg, 'type': 'error' }); }); } return true; } else if (command === 'revoke' && allowed_commands.includes(command)) { verifyAndSetAffiliation(model, command, args, ['admin', 'owner']); return true; } else if (command === 'topic' && allowed_commands.includes(command) || command === 'subject' && allowed_commands.includes(command)) { model.setSubject(args); return true; } else if (command === 'voice' && allowed_commands.includes(command)) { setRole(model, command, args, [], ['moderator']); return true; } else { return false; } }