UNPKG

converse.js

Version:
390 lines (368 loc) 13.9 kB
/** * @typedef {import('@converse/headless/types/plugins/muc/muc.js').default} MUC * @typedef {import("shared/avatar/avatar").default} Avatar * @typedef {import("shared/autocomplete/suggestion").default} Suggestion */ import { html } from 'lit'; import { api, converse, log, constants } from '@converse/headless'; import './modals/occupant.js'; import './modals/moderator-tools.js'; import tplSpinner from 'templates/spinner.js'; import { __ } from 'i18n'; const { Strophe, u } = converse.env; const { CHATROOMS_TYPE } = constants; 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', }; /** * Presents a confirmation modal to the user asking them to accept or decline a * MUC invitation. * @async */ export function confirmDirectMUCInvitation({ contact, jid, reason }) { if (!reason) { return api.confirm( __('Invitation to a groupchat'), __('%1$s has invited you to join the groupchat "%2$s"', contact, jid) ); } else { return api.confirm( __('Invitation to a groupchat'), __( '%1$s has invited you to join the groupchat "%2$s", and left the following reason: "%3$s"', contact, jid, reason ) ); } } /** * @param {string} jid */ export function clearHistory(jid) { if (location.hash === `converse/room?jid=${jid}`) { history.pushState(null, '', window.location.pathname); } } /** * @param {MUC} model */ 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); } } /** * @param {MUC} model */ export function getNicknameRequiredTemplate(model) { const jid = model.get('jid'); if (api.settings.get('muc_show_logs_before_join')) { return html`<converse-muc-chatarea class="row g-0" jid="${jid}"></converse-muc-chatarea>`; } else { return html`<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>`; } } /** * @param {MUC} model */ export function getChatRoomBodyTemplate(model) { const view = model.session.get('view'); const jid = model.get('jid'); const RS = converse.ROOMSTATUS; const conn_status = model.session.get('connection_status'); if (view === converse.MUC.VIEWS.CONFIG) { return html`<converse-muc-config-form class="muc-form-container" jid="${jid}"></converse-muc-config-form>`; } else { return html` ${conn_status == RS.PASSWORD_REQUIRED ? html`<converse-muc-password-form class="muc-form-container" jid="${jid}" ></converse-muc-password-form>` : ''} ${conn_status == RS.ENTERED ? html`<converse-muc-chatarea class="row g-0" jid="${jid}"></converse-muc-chatarea>` : ''} ${conn_status == RS.CONNECTING ? tplSpinner({ class: 'vertically-centered' }) : ''} ${conn_status == RS.NICKNAME_REQUIRED ? getNicknameRequiredTemplate(model) : ''} ${conn_status == RS.DISCONNECTED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : ''} ${conn_status == RS.BANNED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : ''} ${conn_status == RS.DESTROYED ? html`<converse-muc-destroyed jid="${jid}"></converse-muc-destroyed>` : ''} `; } } /** * @param {MUC} muc * @param {Suggestion} text * @param {string} input * @returns {import('lit').TemplateResult} The rendered HTML for the item. */ export function getAutoCompleteListItem(muc, text, input) { input = input.trim(); let avatar_model; const show_avatar = api.settings.get('muc_mention_autocomplete_show_avatar'); if (show_avatar) { const t = text.label.toLowerCase(); avatar_model = muc.occupants.findWhere((o) => { if (o.getDisplayName()?.toLowerCase()?.startsWith(t)) { return o; } else if (o.get('nickname')?.toLowerCase()?.startsWith(t)) { return o; } else if (o.get('jid')?.toLowerCase()?.startsWith(t)) { return o; } }); } const regex = new RegExp('(' + input + ')', 'ig'); const parts = input ? text.split(regex) : [text]; return html` <li aria-selected="false"> ${show_avatar ? html`<converse-avatar .model=${avatar_model} name="${avatar_model.getDisplayName()}" height="22" width="22" class="avatar avatar-autocomplete" ></converse-avatar>` : ''} <span class="autocomplete-item"> ${parts.map((txt) => (input && txt.match(regex) ? html`<mark>${txt}</mark>` : txt))} </span> </li> `; } 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; } /** * @param {MUC} muc */ 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; } /** * @param {MUC} muc */ 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'); } u.muc .setAffiliation(affiliation, muc.get('jid'), [attrs]) .then(() => muc.occupants.fetchMembers()) .catch((err) => muc.onCommandError(err)); } /** * @param {MUC} muc * @param {string} [affiliation] */ 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') !== 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; } }