/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { l10nHelper } from "resource:///modules/imXPCOMUtils.sys.mjs"; import { MatrixSDK } from "resource:///modules/matrix-sdk.sys.mjs"; const lazy = {}; XPCOMUtils.defineLazyGetter(lazy, "_", () => l10nHelper("chrome://chat/locale/matrix.properties") ); ChromeUtils.defineESModuleGetters(lazy, { MatrixPowerLevels: "resource:///modules/matrixPowerLevels.sys.mjs", }); /** * Shared handler for verification requests. We need it twice because the * request can be the msgtype of a normal message or its own event. * * @param {MatrixEvent} matrixEvent - Matrix Event this is handling. * @param {{sender: string, content: object}} param1 - handler context. * @returns {string} */ const keyVerificationRequest = (matrixEvent, { sender, content }) => { return lazy._("message.verification.request2", sender, content.to); }; /** * Shared handler for room messages, since those come in the plain text and * encrypted form. */ const roomMessage = { pivot: "msgtype", handlers: { [MatrixSDK.MsgType.KeyVerificationRequest]: keyVerificationRequest, "m.bad.encrypted": () => lazy._("message.decryptionError"), }, }; /** * Functions returning notices to display when matrix events are received. * Top level key is the matrix event type. * * If the object then has a "pivot" key, the value of that key is used to get * the value of the key with that name from the event content. This value from * event content picks the handler from handlers. * * If there is no pivot, the method on the key "handler" is called. * * Handlers are called with the following arguments: * - matrixEvent: MatrixEvent * - context: { * sender: user ID of the sender, * content: event content object, * } * A handler is expected to return a string that is displayed as notice. If * nothing is returned, no notice will be shown. The formatContext function * optionally adds values to the context argument for the handler. */ const MATRIX_EVENT_HANDLERS = { [MatrixSDK.EventType.RoomMember]: { pivot: "membership", formatContext(matrixEvent, { sender, content }) { return { sender, content, target: matrixEvent.target, prevContent: matrixEvent.getPrevContent(), reason: content.reason, withReasonKey: content.reason ? "WithReason" : "", }; }, handlers: { ban(matrixEvent, { sender, target, reason, withReasonKey }) { return lazy._( "message.banned" + withReasonKey, sender, target.userId, reason ); }, invite(matrixEvent, { sender, content, target }) { const thirdPartyInvite = content.third_party_invite; if (thirdPartyInvite) { if (thirdPartyInvite.display_name) { return lazy._( "message.acceptedInviteFor", target.userId, thirdPartyInvite.display_name ); } return lazy._("message.acceptedInvite", target.userId); } return lazy._("message.invited", sender, target.userId); }, join(matrixEvent, { sender, content, prevContent, target }) { if (prevContent && prevContent.membership == "join") { if ( prevContent.displayname && content.displayname && prevContent.displayname != content.displayname ) { return lazy._( "message.displayName.changed", sender, prevContent.displayname, content.displayname ); } else if (!prevContent.displayname && content.displayname) { return lazy._( "message.displayName.set", sender, content.displayname ); } else if (prevContent.displayname && !content.displayname) { return lazy._( "message.displayName.remove", sender, prevContent.displayname ); } return null; } return lazy._("message.joined", target.userId); }, leave( matrixEvent, { sender, prevContent, target, reason, withReasonKey } ) { // kick and unban just change the membership to "leave". // So we need to look at each transition to what happened to the user. if (matrixEvent.getSender() === target.userId) { if (prevContent.membership === "invite") { return lazy._("message.rejectedInvite", target.userId); } return lazy._("message.left", target.userId); } else if (prevContent.membership === "ban") { return lazy._("message.unbanned", sender, target.userId); } else if (prevContent.membership === "join") { return lazy._( "message.kicked" + withReasonKey, sender, target.userId, reason ); } else if (prevContent.membership === "invite") { return lazy._( "message.withdrewInvite" + withReasonKey, sender, target.userId, reason ); } // ignore rest of the cases. return null; }, }, }, [MatrixSDK.EventType.RoomPowerLevels]: { handler(matrixEvent, { sender, content }) { const prevContent = matrixEvent.getPrevContent(); if (!prevContent?.users) { return null; } const userDefault = content.users_default || lazy.MatrixPowerLevels.user; const prevDefault = prevContent.users_default || lazy.MatrixPowerLevels.user; // Construct set of userIds. let users = new Set( Object.keys(content.users).concat(Object.keys(prevContent.users)) ); const changes = Array.from(users) .map(userId => { const prevPowerLevel = prevContent.users[userId] ?? prevDefault; const currentPowerLevel = content.users[userId] ?? userDefault; if (prevPowerLevel !== currentPowerLevel) { // Handling the case where there are multiple changes. // Example : "@Mr.B:matrix.org changed the power level of // @Mr.B:matrix.org from Default (0) to Moderator (50)." return lazy._( "message.powerLevel.fromTo", userId, lazy.MatrixPowerLevels.toText(prevPowerLevel, prevDefault), lazy.MatrixPowerLevels.toText(currentPowerLevel, userDefault) ); } return null; }) .filter(change => Boolean(change)); // Since the power levels event also contains role power levels, not // every event update will affect user power levels. if (!changes.length) { return null; } return lazy._("message.powerLevel.changed", sender, changes.join(", ")); }, }, [MatrixSDK.EventType.RoomName]: { handler(matrixEvent, { sender, content }) { let roomName = content.name; if (!roomName) { return lazy._("message.roomName.remove", sender); } return lazy._("message.roomName.changed", sender, roomName); }, }, [MatrixSDK.EventType.RoomGuestAccess]: { pivot: "guest_access", handlers: { [MatrixSDK.GuestAccess.Forbidden](matrixEvent, { sender }) { return lazy._("message.guest.prevented", sender); }, [MatrixSDK.GuestAccess.CanJoin](matrixEvent, { sender }) { return lazy._("message.guest.allowed", sender); }, }, }, [MatrixSDK.EventType.RoomHistoryVisibility]: { pivot: "history_visibility", handlers: { [MatrixSDK.HistoryVisibility.WorldReadable](matrixEvent, { sender }) { return lazy._("message.history.anyone", sender); }, [MatrixSDK.HistoryVisibility.Shared](matrixEvent, { sender }) { return lazy._("message.history.shared", sender); }, [MatrixSDK.HistoryVisibility.Invited](matrixEvent, { sender }) { return lazy._("message.history.invited", sender); }, [MatrixSDK.HistoryVisibility.Joined](matrixEvent, { sender }) { return lazy._("message.history.joined", sender); }, }, }, [MatrixSDK.EventType.RoomCanonicalAlias]: { handler(matrixEvent, { sender, content }) { const prevContent = matrixEvent.getPrevContent(); if (content.alias != prevContent.alias) { return lazy._( "message.alias.main", sender, prevContent.alias, content.alias ); } const prevAliases = prevContent.alt_aliases || []; const aliases = content.alt_aliases || []; const addedAliases = aliases .filter(alias => !prevAliases.includes(alias)) .join(", "); const removedAliases = prevAliases .filter(alias => !aliases.includes(alias)) .join(", "); if (addedAliases && removedAliases) { return lazy._( "message.alias.removedAndAdded", sender, removedAliases, addedAliases ); } else if (removedAliases) { return lazy._("message.alias.removed", sender, removedAliases); } else if (addedAliases) { return lazy._("message.alias.added", sender, addedAliases); } // No discernible changes to aliases return null; }, }, [MatrixSDK.EventType.RoomMessage]: roomMessage, [MatrixSDK.EventType.RoomMessageEncrypted]: roomMessage, [MatrixSDK.EventType.KeyVerificationRequest]: { handler: keyVerificationRequest, }, [MatrixSDK.EventType.KeyVerificationCancel]: { handler(matrixEvent, { sender, content }) { return lazy._("message.verification.cancel2", sender, content.reason); }, }, [MatrixSDK.EventType.KeyVerificationDone]: { handler(matrixEvent, { sender, content }) { return lazy._("message.verification.done"); }, }, [MatrixSDK.EventType.RoomEncryption]: { handler(matrixEvent, { sender, content }) { return lazy._("message.encryptionStart"); }, }, // TODO : Events to be handled: // 'm.call.invite' // 'm.call.answer' // 'm.call.hangup' // 'm.room.third_party_invite' // NOTE : No need to add string messages for 'm.room.topic' events, // as setTopic is used which handles the messages too. }; /** * Generates a notice string for a matrix event. May return null if no notice * should be shown. * * @param {MatrixEvent} matrixEvent - Matrix event to generate a notice for. * @returns {string?} Text to display as notice for the given event. */ export function getMatrixTextForEvent(matrixEvent) { const context = { sender: matrixEvent.getSender(), content: matrixEvent.getContent(), }; const eventHandlingInformation = MATRIX_EVENT_HANDLERS[matrixEvent.getType()]; if (!eventHandlingInformation) { return null; } const details = eventHandlingInformation.formatContext?.(matrixEvent, context) ?? context; if (eventHandlingInformation.pivot) { const pivotValue = context.content[eventHandlingInformation.pivot]; return ( eventHandlingInformation.handlers[pivotValue]?.(matrixEvent, details) ?? null ); } return eventHandlingInformation.handler(matrixEvent, details); }