From a97656e37fff40f201673e8f1599b121be821ed1 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 12 Jul 2023 11:19:13 +0100 Subject: [PATCH] Merge to fix package.json conflict --- .github/workflows/cypress.yaml | 2 +- cypress.config.ts | 4 + cypress/e2e/crypto/verification.spec.ts | 51 +++- cypress/plugins/index.ts | 4 + cypress/support/bot.ts | 39 ++- cypress/support/e2e.ts | 18 ++ package.json | 3 +- src/SlashCommands.tsx | 248 +----------------- .../views/emojipicker/EmojiPicker.tsx | 4 + src/components/views/right_panel/UserInfo.tsx | 2 +- .../views/rooms/SendMessageComposer.tsx | 8 +- src/i18n/strings/en_EN.json | 24 +- src/settings/Settings.tsx | 9 +- src/slash-commands/command.ts | 116 ++++++++ src/slash-commands/interface.ts | 34 +++ src/slash-commands/op.ts | 101 +++++++ src/slash-commands/utils.ts | 83 ++++++ test/ContentMessages-test.ts | 2 +- test/SlashCommands-test.tsx | 90 ++++++- .../views/rooms/EditMessageComposer-test.tsx | 44 ++-- .../views/rooms/SendMessageComposer-test.tsx | 58 ++-- .../rooms/VoiceRecordComposerTile-test.tsx | 4 +- .../pushrules_bug_botnotices.json | 8 +- .../pushrules_bug_keyword_only.json | 8 +- .../pushrules_default.json | 8 +- .../pushrules_default_new.json | 8 +- .../pushrules_sample.json | 8 +- test/test-utils/pushRules.ts | 8 +- yarn.lock | 38 ++- 29 files changed, 674 insertions(+), 360 deletions(-) create mode 100644 src/slash-commands/command.ts create mode 100644 src/slash-commands/interface.ts create mode 100644 src/slash-commands/op.ts create mode 100644 src/slash-commands/utils.ts diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 546c39ae7573..23d020acc55a 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -174,7 +174,7 @@ jobs: record: true parallel: true command-prefix: "yarn percy exec --parallel --" - config: '{"reporter":"cypress-multi-reporters", "reporterOptions": { "configFile": "cypress-ci-reporter-config.json" } }' + config: '{"reporter":"cypress-multi-reporters", "reporterOptions": { "configFile": "cypress-ci-reporter-config.json" }, "morgan": false }' ci-build-id: ${{ needs.prepare.outputs.uuid }} env: # pass the Dashboard record key as an environment variable diff --git a/cypress.config.ts b/cypress.config.ts index b57fe7f6c4ae..c3403838e60d 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -38,4 +38,8 @@ export default defineConfig({ runMode: 4, openMode: 0, }, + + // disable logging of HTTP requests made to the Cypress server. They are noisy and not very helpful. + // @ts-ignore https://github.com/cypress-io/cypress/issues/26284 + morgan: false, }); diff --git a/cypress/e2e/crypto/verification.spec.ts b/cypress/e2e/crypto/verification.spec.ts index b6ec5f0fbb91..d9b4bb6e5310 100644 --- a/cypress/e2e/crypto/verification.spec.ts +++ b/cypress/e2e/crypto/verification.spec.ts @@ -36,7 +36,11 @@ describe("Device verification", () => { cy.window({ log: false }).should("have.property", "matrixcs"); // Create a new device for alice - cy.getBot(homeserver, { rustCrypto: true, bootstrapCrossSigning: true }).then((bot) => { + cy.getBot(homeserver, { + rustCrypto: true, + bootstrapCrossSigning: true, + bootstrapSecretStorage: true, + }).then((bot) => { aliceBotClient = bot; }); }); @@ -87,6 +91,51 @@ describe("Device verification", () => { checkDeviceIsCrossSigned(); }); + it("Verify device during login with Security Phrase", () => { + logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password); + + // Select the security phrase + cy.get(".mx_AuthPage").within(() => { + cy.findByRole("button", { name: "Verify with Security Key or Phrase" }).click(); + }); + + // Fill the passphrase + cy.get(".mx_Dialog").within(() => { + cy.get("input").type("new passphrase"); + cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click(); + }); + + cy.get(".mx_AuthPage").within(() => { + cy.findByRole("button", { name: "Done" }).click(); + }); + + // Check that our device is now cross-signed + checkDeviceIsCrossSigned(); + }); + + it("Verify device during login with Security Key", () => { + logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password); + + // Select the security phrase + cy.get(".mx_AuthPage").within(() => { + cy.findByRole("button", { name: "Verify with Security Key or Phrase" }).click(); + }); + + // Fill the security key + cy.get(".mx_Dialog").within(() => { + cy.findByRole("button", { name: "use your Security Key" }).click(); + cy.get("#mx_securityKey").type(aliceBotClient.__cypress_recovery_key.encodedPrivateKey); + cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click(); + }); + + cy.get(".mx_AuthPage").within(() => { + cy.findByRole("button", { name: "Done" }).click(); + }); + + // Check that our device is now cross-signed + checkDeviceIsCrossSigned(); + }); + it("Handle incoming verification request with SAS", () => { logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index 1971a70c5b05..8ef9aac7883a 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -15,6 +15,7 @@ limitations under the License. */ /// +import installLogsPrinter from "cypress-terminal-report/src/installLogsPrinter"; import PluginEvents = Cypress.PluginEvents; import PluginConfigOptions = Cypress.PluginConfigOptions; @@ -35,4 +36,7 @@ export default function (on: PluginEvents, config: PluginConfigOptions) { slidingSyncProxyDocker(on, config); webserver(on, config); log(on, config); + installLogsPrinter(on, { + // printLogsToConsole: "always", + }); } diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts index 5806bab7ef45..34e5c858a983 100644 --- a/cypress/support/bot.ts +++ b/cypress/support/bot.ts @@ -17,6 +17,8 @@ limitations under the License. /// import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; +import type { AddSecretStorageKeyOpts } from "matrix-js-sdk/src/secret-storage"; import { HomeserverInstance } from "../plugins/utils/homeserver"; import { Credentials } from "./homeserver"; import Chainable = Cypress.Chainable; @@ -47,6 +49,10 @@ interface CreateBotOpts { * Whether to use the rust crypto impl. Defaults to false (for now!) */ rustCrypto?: boolean; + /** + * Whether or not to bootstrap the secret storage + */ + bootstrapSecretStorage?: boolean; } const defaultCreateBotOptions = { @@ -58,6 +64,7 @@ const defaultCreateBotOptions = { export interface CypressBot extends MatrixClient { __cypress_password: string; + __cypress_recovery_key: GeneratedSecretStorageKey; } declare global { @@ -143,6 +150,24 @@ function setupBotClient( Object.assign(keys, k); }; + // Store the cached secret storage key and return it when `getSecretStorageKey` is called + let cachedKey: { keyId: string; key: Uint8Array }; + const cacheSecretStorageKey = (keyId: string, keyInfo: AddSecretStorageKeyOpts, key: Uint8Array) => { + cachedKey = { + keyId, + key, + }; + }; + + const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]); + + const cryptoCallbacks = { + getCrossSigningKey, + saveCrossSigningKeys, + cacheSecretStorageKey, + getSecretStorageKey, + }; + const cli = new win.matrixcs.MatrixClient({ baseUrl: homeserver.baseUrl, userId: credentials.userId, @@ -151,7 +176,7 @@ function setupBotClient( store: new win.matrixcs.MemoryStore(), scheduler: new win.matrixcs.MatrixScheduler(), cryptoStore: new win.matrixcs.MemoryCryptoStore(), - cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys }, + cryptoCallbacks, }); if (opts.autoAcceptInvites) { @@ -192,6 +217,18 @@ function setupBotClient( }, }); } + + if (opts.bootstrapSecretStorage) { + const passphrase = "new passphrase"; + const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase); + Object.assign(cli, { __cypress_recovery_key: recoveryKey }); + + await cli.getCrypto()!.bootstrapSecretStorage({ + setupNewSecretStorage: true, + createSecretStorageKey: () => Promise.resolve(recoveryKey), + }); + } + return cli; }, ); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 4f268966a357..2ff0197ba65e 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -19,6 +19,7 @@ limitations under the License. import "@percy/cypress"; import "cypress-real-events"; import "@testing-library/cypress/add-commands"; +import installLogsCollector from "cypress-terminal-report/src/installLogsCollector"; import "./config.json"; import "./homeserver"; @@ -39,3 +40,20 @@ import "./network"; import "./composer"; import "./proxy"; import "./axe"; + +installLogsCollector({ + // specify the types of logs to collect (and report to the node console at the end of the test) + collectTypes: [ + "cons:log", + "cons:info", + "cons:warn", + "cons:error", + // "cons:debug", + "cy:log", + "cy:xhr", + "cy:fetch", + "cy:request", + "cy:intercept", + "cy:command", + ], +}); diff --git a/package.json b/package.json index bdfa62f489d7..df29cd7e5093 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.5.0", "@matrix-org/matrix-wysiwyg": "^2.3.1", - "@matrix-org/react-sdk-module-api": "^0.0.6", + "@matrix-org/react-sdk-module-api": "^1.0.0", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", @@ -184,6 +184,7 @@ "cypress-each": "^1.13.3", "cypress-multi-reporters": "^1.6.1", "cypress-real-events": "^1.7.1", + "cypress-terminal-report": "^5.3.2", "eslint": "8.43.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^8.5.0", diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index de2ec6adbc68..929d6e6e6f94 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -20,13 +20,10 @@ limitations under the License. import * as React from "react"; import { User } from "matrix-js-sdk/src/models/user"; import { Direction } from "matrix-js-sdk/src/models/event-timeline"; -import { EventType } from "matrix-js-sdk/src/@types/event"; import * as ContentHelpers from "matrix-js-sdk/src/content-helpers"; import { logger } from "matrix-js-sdk/src/logger"; import { IContent } from "matrix-js-sdk/src/models/event"; import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic"; -import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; import dis from "./dispatcher/dispatcher"; import { _t, _td, UserFriendlyError } from "./languageHandler"; @@ -46,7 +43,6 @@ import BugReportDialog from "./components/views/dialogs/BugReportDialog"; import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; -import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; import SdkConfig from "./SdkConfig"; import SettingsStore from "./settings/SettingsStore"; import { UIComponent, UIFeature } from "./settings/UIFeature"; @@ -54,184 +50,24 @@ import { CHAT_EFFECTS } from "./effects"; import LegacyCallHandler from "./LegacyCallHandler"; import { guessAndSetDMRoom } from "./Rooms"; import { upgradeRoom } from "./utils/RoomUpgrade"; -import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"; import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog"; import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; import InfoDialog from "./components/views/dialogs/InfoDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; import { TimelineRenderingType } from "./contexts/RoomContext"; -import { XOR } from "./@types/common"; -import { PosthogAnalytics } from "./PosthogAnalytics"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import VoipUserMapper from "./VoipUserMapper"; import { htmlSerializeFromMdIfNeeded } from "./editor/serialize"; import { leaveRoomBehaviour } from "./utils/leave-behaviour"; -import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; -import { SdkContextClass } from "./contexts/SDKContext"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo"; +import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils"; +import { deop, op } from "./slash-commands/op"; +import { CommandCategories } from "./slash-commands/interface"; +import { Command } from "./slash-commands/command"; -// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 -interface HTMLInputEvent extends Event { - target: HTMLInputElement & EventTarget; -} - -const singleMxcUpload = async (cli: MatrixClient): Promise => { - return new Promise((resolve) => { - const fileSelector = document.createElement("input"); - fileSelector.setAttribute("type", "file"); - fileSelector.onchange = (ev: Event) => { - const file = (ev as HTMLInputEvent).target.files?.[0]; - if (!file) return; - - Modal.createDialog(UploadConfirmDialog, { - file, - onFinished: async (shouldContinue): Promise => { - if (shouldContinue) { - const { content_uri: uri } = await cli.uploadContent(file); - resolve(uri); - } else { - resolve(null); - } - }, - }); - }; - - fileSelector.click(); - }); -}; - -export const CommandCategories = { - messages: _td("Messages"), - actions: _td("Actions"), - admin: _td("Admin"), - advanced: _td("Advanced"), - effects: _td("Effects"), - other: _td("Other"), -}; - -export type RunResult = XOR<{ error: Error }, { promise: Promise }>; - -type RunFn = ( - this: Command, - matrixClient: MatrixClient, - roomId: string, - threadId: string | null, - args?: string, -) => RunResult; - -interface ICommandOpts { - command: string; - aliases?: string[]; - args?: string; - description: string; - analyticsName?: SlashCommandEvent["command"]; - runFn?: RunFn; - category: string; - hideCompletionAfterSpace?: boolean; - isEnabled?(matrixClient: MatrixClient | null): boolean; - renderingTypes?: TimelineRenderingType[]; -} - -export class Command { - public readonly command: string; - public readonly aliases: string[]; - public readonly args?: string; - public readonly description: string; - public readonly runFn?: RunFn; - public readonly category: string; - public readonly hideCompletionAfterSpace: boolean; - public readonly renderingTypes?: TimelineRenderingType[]; - public readonly analyticsName?: SlashCommandEvent["command"]; - private readonly _isEnabled?: (matrixClient: MatrixClient | null) => boolean; - - public constructor(opts: ICommandOpts) { - this.command = opts.command; - this.aliases = opts.aliases || []; - this.args = opts.args || ""; - this.description = opts.description; - this.runFn = opts.runFn?.bind(this); - this.category = opts.category || CommandCategories.other; - this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; - this._isEnabled = opts.isEnabled; - this.renderingTypes = opts.renderingTypes; - this.analyticsName = opts.analyticsName; - } - - public getCommand(): string { - return `/${this.command}`; - } - - public getCommandWithArgs(): string { - return this.getCommand() + " " + this.args; - } - - public run(matrixClient: MatrixClient, roomId: string, threadId: string | null, args?: string): RunResult { - // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!this.runFn) { - return reject(new UserFriendlyError("Command error: Unable to handle slash command.")); - } - - const renderingType = threadId ? TimelineRenderingType.Thread : TimelineRenderingType.Room; - if (this.renderingTypes && !this.renderingTypes?.includes(renderingType)) { - return reject( - new UserFriendlyError("Command error: Unable to find rendering type (%(renderingType)s)", { - renderingType, - cause: undefined, - }), - ); - } - - if (this.analyticsName) { - PosthogAnalytics.instance.trackEvent({ - eventName: "SlashCommand", - command: this.analyticsName, - }); - } - - return this.runFn(matrixClient, roomId, threadId, args); - } - - public getUsage(): string { - return _t("Usage") + ": " + this.getCommandWithArgs(); - } - - public isEnabled(cli: MatrixClient | null): boolean { - return this._isEnabled?.(cli) ?? true; - } -} - -function reject(error?: any): RunResult { - return { error }; -} - -function success(promise: Promise = Promise.resolve()): RunResult { - return { promise }; -} - -function successSync(value: any): RunResult { - return success(Promise.resolve(value)); -} - -const isCurrentLocalRoom = (cli: MatrixClient | null): boolean => { - const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); - if (!roomId) return false; - const room = cli?.getRoom(roomId); - if (!room) return false; - return isLocalRoom(room); -}; - -const canAffectPowerlevels = (cli: MatrixClient | null): boolean => { - const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); - if (!cli || !roomId) return false; - const room = cli?.getRoom(roomId); - return !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getSafeUserId()) && !isLocalRoom(room); -}; - -/* Disable the "unexpected this" error for these commands - all of the run - * functions are called with `this` bound to the Command instance. - */ +export { CommandCategories, Command }; export const Commands = [ new Command({ @@ -886,78 +722,8 @@ export const Commands = [ }, category: CommandCategories.actions, }), - new Command({ - command: "op", - args: " []", - description: _td("Define the power level of a user"), - isEnabled: canAffectPowerlevels, - runFn: function (cli, roomId, threadId, args) { - if (args) { - const matches = args.match(/^(\S+?)( +(-?\d+))?$/); - let powerLevel = 50; // default power level for op - if (matches) { - const userId = matches[1]; - if (matches.length === 4 && undefined !== matches[3]) { - powerLevel = parseInt(matches[3], 10); - } - if (!isNaN(powerLevel)) { - const room = cli.getRoom(roomId); - if (!room) { - return reject( - new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", { - roomId, - cause: undefined, - }), - ); - } - const member = room.getMember(userId); - if ( - !member?.membership || - getEffectiveMembership(member.membership) === EffectiveMembership.Leave - ) { - return reject(new UserFriendlyError("Could not find user in room")); - } - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); - } - } - } - return reject(this.getUsage()); - }, - category: CommandCategories.admin, - renderingTypes: [TimelineRenderingType.Room], - }), - new Command({ - command: "deop", - args: "", - description: _td("Deops user with given id"), - isEnabled: canAffectPowerlevels, - runFn: function (cli, roomId, threadId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - const room = cli.getRoom(roomId); - if (!room) { - return reject( - new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", { - roomId, - cause: undefined, - }), - ); - } - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent?.getContent().users[args]) { - return reject(new UserFriendlyError("Could not find user in room")); - } - return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); - } - } - return reject(this.getUsage()); - }, - category: CommandCategories.admin, - renderingTypes: [TimelineRenderingType.Room], - }), + op, + deop, new Command({ command: "devtools", description: _td("Opens the Developer Tools dialog"), diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 3158d52e553e..edb5e427a3ad 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -306,6 +306,10 @@ class EmojiPicker extends React.Component { }; private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => { + // If the query is an emoji containing a variation then strip it to provide more useful matches + if (filter.includes(ZERO_WIDTH_JOINER)) { + filter = filter.split(ZERO_WIDTH_JOINER, 2)[0]; + } return ( emoji.label.toLowerCase().includes(filter) || (Array.isArray(emoji.emoticon) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index aa06ef3cc709..72768064ca04 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -513,7 +513,7 @@ export const UserOptionsSection: React.FC<{ ); }; -const warnSelfDemote = async (isSpace: boolean): Promise => { +export const warnSelfDemote = async (isSpace: boolean): Promise => { const { finished } = Modal.createDialog(QuestionDialog, { title: _t("Demote yourself?"), description: ( diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index d7326ee913b1..bc4a6ce74795 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -89,7 +89,7 @@ export function attachMentions( } // The mentions property *always* gets included to disable legacy push rules. - const mentions: IMentions = (content["org.matrix.msc3952.mentions"] = {}); + const mentions: IMentions = (content["m.mentions"] = {}); const userMentions = new Set(); let roomMention = false; @@ -100,7 +100,7 @@ export function attachMentions( userMentions.add(replyToEvent.sender!.userId); // TODO What do we do if the reply event *doeesn't* have this property? // Try to fish out replies from the contents? - const userIds = replyToEvent.getContent()["org.matrix.msc3952.mentions"]?.user_ids; + const userIds = replyToEvent.getContent()["m.mentions"]?.user_ids; if (Array.isArray(userIds)) { userIds.forEach((userId) => userMentions.add(userId)); } @@ -127,7 +127,7 @@ export function attachMentions( if (editedContent) { // First, the new event content gets the *full* set of users. const newContent = content["m.new_content"]; - const newMentions: IMentions = (newContent["org.matrix.msc3952.mentions"] = {}); + const newMentions: IMentions = (newContent["m.mentions"] = {}); // Only include the users/room if there is any content. if (userMentions.size) { @@ -139,7 +139,7 @@ export function attachMentions( // Fetch the mentions from the original event and remove any previously // mentioned users. - const prevMentions = editedContent["org.matrix.msc3952.mentions"]; + const prevMentions = editedContent["m.mentions"]; if (Array.isArray(prevMentions?.user_ids)) { prevMentions!.user_ids.forEach((userId) => userMentions.delete(userId)); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 45b3d453ff34..4b3d180041ff 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -409,14 +409,6 @@ "Go Back": "Go Back", "Cancel": "Cancel", "Setting up keys": "Setting up keys", - "Messages": "Messages", - "Actions": "Actions", - "Advanced": "Advanced", - "Effects": "Effects", - "Other": "Other", - "Command error: Unable to handle slash command.": "Command error: Unable to handle slash command.", - "Command error: Unable to find rendering type (%(renderingType)s)": "Command error: Unable to find rendering type (%(renderingType)s)", - "Usage": "Usage", "Sends the given message as a spoiler": "Sends the given message as a spoiler", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message", @@ -455,10 +447,6 @@ "Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward", "Unignored user": "Unignored user", "You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s", - "Define the power level of a user": "Define the power level of a user", - "Command failed: Unable to find room (%(roomId)s": "Command failed: Unable to find room (%(roomId)s", - "Could not find user in room": "Could not find user in room", - "Deops user with given id": "Deops user with given id", "Opens the Developer Tools dialog": "Opens the Developer Tools dialog", "Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room", "Please supply a widget URL or embed code": "Please supply a widget URL or embed code", @@ -938,6 +926,18 @@ "Unsent": "Unsent", "unknown": "unknown", "Change notification settings": "Change notification settings", + "Command error: Unable to handle slash command.": "Command error: Unable to handle slash command.", + "Command error: Unable to find rendering type (%(renderingType)s)": "Command error: Unable to find rendering type (%(renderingType)s)", + "Usage": "Usage", + "Messages": "Messages", + "Actions": "Actions", + "Advanced": "Advanced", + "Effects": "Effects", + "Other": "Other", + "Command failed: Unable to find room (%(roomId)s": "Command failed: Unable to find room (%(roomId)s", + "Could not find user in room": "Could not find user in room", + "Define the power level of a user": "Define the power level of a user", + "Deops user with given id": "Deops user with given id", "Messaging": "Messaging", "Profile": "Profile", "Spaces": "Spaces", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 96594e90c901..3f32d27d777a 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -552,9 +552,12 @@ export const SETTINGS: { [setting: string]: ISetting } = { displayName: _td("Enable intentional mentions"), labsGroup: LabGroup.Rooms, default: false, - controller: new ServerSupportUnstableFeatureController("feature_intentional_mentions", defaultWatchManager, [ - ["org.matrix.msc3952_intentional_mentions"], - ]), + controller: new ServerSupportUnstableFeatureController( + "feature_intentional_mentions", + defaultWatchManager, + [["org.matrix.msc3952_intentional_mentions"]], + "v1.7", + ), }, "feature_ask_to_join": { default: false, diff --git a/src/slash-commands/command.ts b/src/slash-commands/command.ts new file mode 100644 index 000000000000..30fa7732f9b2 --- /dev/null +++ b/src/slash-commands/command.ts @@ -0,0 +1,116 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020, 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand"; + +import { TimelineRenderingType } from "../contexts/RoomContext"; +import { reject } from "./utils"; +import { _t, UserFriendlyError } from "../languageHandler"; +import { PosthogAnalytics } from "../PosthogAnalytics"; +import { CommandCategories, RunResult } from "./interface"; + +type RunFn = ( + this: Command, + matrixClient: MatrixClient, + roomId: string, + threadId: string | null, + args?: string, +) => RunResult; + +interface ICommandOpts { + command: string; + aliases?: string[]; + args?: string; + description: string; + analyticsName?: SlashCommandEvent["command"]; + runFn?: RunFn; + category: string; + hideCompletionAfterSpace?: boolean; + isEnabled?(matrixClient: MatrixClient | null): boolean; + renderingTypes?: TimelineRenderingType[]; +} + +export class Command { + public readonly command: string; + public readonly aliases: string[]; + public readonly args?: string; + public readonly description: string; + public readonly runFn?: RunFn; + public readonly category: string; + public readonly hideCompletionAfterSpace: boolean; + public readonly renderingTypes?: TimelineRenderingType[]; + public readonly analyticsName?: SlashCommandEvent["command"]; + private readonly _isEnabled?: (matrixClient: MatrixClient | null) => boolean; + + public constructor(opts: ICommandOpts) { + this.command = opts.command; + this.aliases = opts.aliases || []; + this.args = opts.args || ""; + this.description = opts.description; + this.runFn = opts.runFn?.bind(this); + this.category = opts.category || CommandCategories.other; + this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; + this._isEnabled = opts.isEnabled; + this.renderingTypes = opts.renderingTypes; + this.analyticsName = opts.analyticsName; + } + + public getCommand(): string { + return `/${this.command}`; + } + + public getCommandWithArgs(): string { + return this.getCommand() + " " + this.args; + } + + public run(matrixClient: MatrixClient, roomId: string, threadId: string | null, args?: string): RunResult { + // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` + if (!this.runFn) { + return reject(new UserFriendlyError("Command error: Unable to handle slash command.")); + } + + const renderingType = threadId ? TimelineRenderingType.Thread : TimelineRenderingType.Room; + if (this.renderingTypes && !this.renderingTypes?.includes(renderingType)) { + return reject( + new UserFriendlyError("Command error: Unable to find rendering type (%(renderingType)s)", { + renderingType, + cause: undefined, + }), + ); + } + + if (this.analyticsName) { + PosthogAnalytics.instance.trackEvent({ + eventName: "SlashCommand", + command: this.analyticsName, + }); + } + + return this.runFn(matrixClient, roomId, threadId, args); + } + + public getUsage(): string { + return _t("Usage") + ": " + this.getCommandWithArgs(); + } + + public isEnabled(cli: MatrixClient | null): boolean { + return this._isEnabled?.(cli) ?? true; + } +} diff --git a/src/slash-commands/interface.ts b/src/slash-commands/interface.ts new file mode 100644 index 000000000000..932072718130 --- /dev/null +++ b/src/slash-commands/interface.ts @@ -0,0 +1,34 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020, 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IContent } from "matrix-js-sdk/src/matrix"; + +import { _td } from "../languageHandler"; +import { XOR } from "../@types/common"; + +export const CommandCategories = { + messages: _td("Messages"), + actions: _td("Actions"), + admin: _td("Admin"), + advanced: _td("Advanced"), + effects: _td("Effects"), + other: _td("Other"), +}; + +export type RunResult = XOR<{ error: Error }, { promise: Promise }>; diff --git a/src/slash-commands/op.ts b/src/slash-commands/op.ts new file mode 100644 index 000000000000..8af22edba44e --- /dev/null +++ b/src/slash-commands/op.ts @@ -0,0 +1,101 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020, 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; + +import { _td, UserFriendlyError } from "../languageHandler"; +import { EffectiveMembership, getEffectiveMembership } from "../utils/membership"; +import { warnSelfDemote } from "../components/views/right_panel/UserInfo"; +import { TimelineRenderingType } from "../contexts/RoomContext"; +import { canAffectPowerlevels, success, reject } from "./utils"; +import { CommandCategories, RunResult } from "./interface"; +import { Command } from "./command"; + +const updatePowerLevel = async (room: Room, member: RoomMember, powerLevel: number | undefined): Promise => { + // Only warn if the target is ourselves and the power level is decreasing or being unset + if (member.userId === room.client.getUserId() && (powerLevel === undefined || member.powerLevel > powerLevel)) { + const ok = await warnSelfDemote(room.isSpaceRoom()); + if (!ok) return; // Nothing to do + } + return room.client.setPowerLevel(room.roomId, member.userId, powerLevel); +}; + +const updatePowerLevelHelper = ( + client: MatrixClient, + roomId: string, + userId: string, + powerLevel: number | undefined, +): RunResult => { + const room = client.getRoom(roomId); + if (!room) { + return reject( + new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", { + roomId, + cause: undefined, + }), + ); + } + const member = room.getMember(userId); + if (!member?.membership || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) { + return reject(new UserFriendlyError("Could not find user in room")); + } + + return success(updatePowerLevel(room, member, powerLevel)); +}; + +export const op = new Command({ + command: "op", + args: " []", + description: _td("Define the power level of a user"), + isEnabled: canAffectPowerlevels, + runFn: function (cli, roomId, threadId, args) { + if (args) { + const matches = args.match(/^(\S+?)( +(-?\d+))?$/); + let powerLevel = 50; // default power level for op + if (matches) { + const userId = matches[1]; + if (matches.length === 4 && undefined !== matches[3]) { + powerLevel = parseInt(matches[3], 10); + } + return updatePowerLevelHelper(cli, roomId, userId, powerLevel); + } + } + return reject(this.getUsage()); + }, + category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], +}); + +export const deop = new Command({ + command: "deop", + args: "", + description: _td("Deops user with given id"), + isEnabled: canAffectPowerlevels, + runFn: function (cli, roomId, threadId, args) { + if (args) { + const matches = args.match(/^(\S+)$/); + if (matches) { + return updatePowerLevelHelper(cli, roomId, args, undefined); + } + } + return reject(this.getUsage()); + }, + category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], +}); diff --git a/src/slash-commands/utils.ts b/src/slash-commands/utils.ts new file mode 100644 index 000000000000..122a90db1b76 --- /dev/null +++ b/src/slash-commands/utils.ts @@ -0,0 +1,83 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020, 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { SdkContextClass } from "../contexts/SDKContext"; +import { isLocalRoom } from "../utils/localRoom/isLocalRoom"; +import Modal from "../Modal"; +import UploadConfirmDialog from "../components/views/dialogs/UploadConfirmDialog"; +import { RunResult } from "./interface"; + +export function reject(error?: any): RunResult { + return { error }; +} + +export function success(promise: Promise = Promise.resolve()): RunResult { + return { promise }; +} + +export function successSync(value: any): RunResult { + return success(Promise.resolve(value)); +} + +export const canAffectPowerlevels = (cli: MatrixClient | null): boolean => { + const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); + if (!cli || !roomId) return false; + const room = cli?.getRoom(roomId); + return !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getSafeUserId()) && !isLocalRoom(room); +}; + +// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 +interface HTMLInputEvent extends Event { + target: HTMLInputElement & EventTarget; +} + +export const singleMxcUpload = async (cli: MatrixClient): Promise => { + return new Promise((resolve) => { + const fileSelector = document.createElement("input"); + fileSelector.setAttribute("type", "file"); + fileSelector.onchange = (ev: Event) => { + const file = (ev as HTMLInputEvent).target.files?.[0]; + if (!file) return; + + Modal.createDialog(UploadConfirmDialog, { + file, + onFinished: async (shouldContinue): Promise => { + if (shouldContinue) { + const { content_uri: uri } = await cli.uploadContent(file); + resolve(uri); + } else { + resolve(null); + } + }, + }); + }; + + fileSelector.click(); + }); +}; + +export const isCurrentLocalRoom = (cli: MatrixClient | null): boolean => { + const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); + if (!roomId) return false; + const room = cli?.getRoom(roomId); + if (!room) return false; + return isLocalRoom(room); +}; diff --git a/test/ContentMessages-test.ts b/test/ContentMessages-test.ts index e725d4d9ce27..64bd3c845cc9 100644 --- a/test/ContentMessages-test.ts +++ b/test/ContentMessages-test.ts @@ -245,7 +245,7 @@ describe("ContentMessages", () => { expect.objectContaining({ "url": "mxc://server/file", "msgtype": "m.image", - "org.matrix.msc3952.mentions": { + "m.mentions": { user_ids: ["@bob:test"], }, }), diff --git a/test/SlashCommands-test.tsx b/test/SlashCommands-test.tsx index fcd6d6e4c88d..26820386e25c 100644 --- a/test/SlashCommands-test.tsx +++ b/test/SlashCommands-test.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import { Command, Commands, getCommand } from "../src/SlashCommands"; @@ -26,6 +26,9 @@ import { SdkContextClass } from "../src/contexts/SDKContext"; import Modal from "../src/Modal"; import WidgetUtils from "../src/utils/WidgetUtils"; import { WidgetType } from "../src/widgets/WidgetType"; +import { warnSelfDemote } from "../src/components/views/right_panel/UserInfo"; + +jest.mock("../src/components/views/right_panel/UserInfo"); describe("SlashCommands", () => { let client: MatrixClient; @@ -47,7 +50,7 @@ describe("SlashCommands", () => { }); }; - const setCurrentLocalRoon = (): void => { + const setCurrentLocalRoom = (): void => { mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(localRoomId); mocked(client.getRoom).mockImplementation((rId: string): Room | null => { if (rId === localRoomId) return localRoom; @@ -60,8 +63,8 @@ describe("SlashCommands", () => { client = createTestClient(); - room = new Room(roomId, client, client.getUserId()!); - localRoom = new LocalRoom(localRoomId, client, client.getUserId()!); + room = new Room(roomId, client, client.getSafeUserId()); + localRoom = new LocalRoom(localRoomId, client, client.getSafeUserId()); jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId"); }); @@ -116,12 +119,73 @@ describe("SlashCommands", () => { }); it("should return false for LocalRoom", () => { - setCurrentLocalRoon(); + setCurrentLocalRoom(); expect(command.isEnabled(client)).toBe(false); }); }); }); + describe("/op", () => { + beforeEach(() => { + command = findCommand("op")!; + }); + + it("should return usage if no args", () => { + expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage()); + }); + + it("should reject with usage if given an invalid power level value", () => { + expect(command.run(client, roomId, null, "@bob:server Admin").error).toBe(command.getUsage()); + }); + + it("should reject with usage for invalid input", () => { + expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage()); + }); + + it("should warn about self demotion", async () => { + setCurrentRoom(); + const member = new RoomMember(roomId, client.getSafeUserId()); + member.membership = "join"; + member.powerLevel = 100; + room.getMember = () => member; + command.run(client, roomId, null, `${client.getUserId()} 0`); + expect(warnSelfDemote).toHaveBeenCalled(); + }); + + it("should default to 50 if no powerlevel specified", async () => { + setCurrentRoom(); + const member = new RoomMember(roomId, "@user:server"); + member.membership = "join"; + room.getMember = () => member; + command.run(client, roomId, null, member.userId); + expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, member.userId, 50); + }); + }); + + describe("/deop", () => { + beforeEach(() => { + command = findCommand("deop")!; + }); + + it("should return usage if no args", () => { + expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage()); + }); + + it("should warn about self demotion", async () => { + setCurrentRoom(); + const member = new RoomMember(roomId, client.getSafeUserId()); + member.membership = "join"; + member.powerLevel = 100; + room.getMember = () => member; + command.run(client, roomId, null, client.getSafeUserId()); + expect(warnSelfDemote).toHaveBeenCalled(); + }); + + it("should reject with usage for invalid input", () => { + expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage()); + }); + }); + describe("/tovirtual", () => { beforeEach(() => { command = findCommand("tovirtual")!; @@ -139,7 +203,7 @@ describe("SlashCommands", () => { }); it("should return false for LocalRoom", () => { - setCurrentLocalRoon(); + setCurrentLocalRoom(); expect(command.isEnabled(client)).toBe(false); }); }); @@ -155,7 +219,7 @@ describe("SlashCommands", () => { }); it("should return false for LocalRoom", () => { - setCurrentLocalRoon(); + setCurrentLocalRoom(); expect(command.isEnabled(client)).toBe(false); }); }); @@ -181,7 +245,7 @@ describe("SlashCommands", () => { }); it("should return false for LocalRoom", () => { - setCurrentLocalRoon(); + setCurrentLocalRoom(); expect(command.isEnabled(client)).toBe(false); }); }); @@ -199,7 +263,7 @@ describe("SlashCommands", () => { }); it("should return false for LocalRoom", () => { - setCurrentLocalRoon(); + setCurrentLocalRoom(); expect(command.isEnabled(client)).toBe(false); }); }); @@ -208,9 +272,9 @@ describe("SlashCommands", () => { describe("/part", () => { it("should part room matching alias if found", async () => { - const room1 = new Room("room-id", client, client.getUserId()!); + const room1 = new Room("room-id", client, client.getSafeUserId()); room1.getCanonicalAlias = jest.fn().mockReturnValue("#foo:bar"); - const room2 = new Room("other-room", client, client.getUserId()!); + const room2 = new Room("other-room", client, client.getSafeUserId()); room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar"); mocked(client.getRooms).mockReturnValue([room1, room2]); @@ -222,9 +286,9 @@ describe("SlashCommands", () => { }); it("should part room matching alt alias if found", async () => { - const room1 = new Room("room-id", client, client.getUserId()!); + const room1 = new Room("room-id", client, client.getSafeUserId()); room1.getAltAliases = jest.fn().mockReturnValue(["#foo:bar"]); - const room2 = new Room("other-room", client, client.getUserId()!); + const room2 = new Room("other-room", client, client.getSafeUserId()); room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]); mocked(client.getRooms).mockReturnValue([room1, room2]); diff --git a/test/components/views/rooms/EditMessageComposer-test.tsx b/test/components/views/rooms/EditMessageComposer-test.tsx index 64425d8f9149..89cb09dd7274 100644 --- a/test/components/views/rooms/EditMessageComposer-test.tsx +++ b/test/components/views/rooms/EditMessageComposer-test.tsx @@ -69,7 +69,7 @@ describe("", () => { "format": "org.matrix.custom.html", "formatted_body": 'hey Bob and Charlie', - "org.matrix.msc3952.mentions": { + "m.mentions": { user_ids: ["@bob:server.org", "@charlie:server.org"], }, }, @@ -303,8 +303,8 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // both content.mentions and new_content.mentions are empty - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({}); - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.mentions"]).toEqual({}); + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({}); }); it("should retain mentions in the original message that are not removed by the edit", async () => { @@ -319,9 +319,9 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // no new mentions were added, so nothing in top level mentions - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.mentions"]).toEqual({}); // bob is still mentioned, charlie removed - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: ["@bob:server.org"], }); }); @@ -338,9 +338,9 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // no new mentions were added, so nothing in top level mentions - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.mentions"]).toEqual({}); // bob is not longer mentioned in the edited message, so empty mentions in new_content - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({}); }); it("should add mentions that were added in the edit", async () => { @@ -357,10 +357,10 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // new mention in the edit - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.mentions"]).toEqual({ user_ids: ["@dan:server.org"], }); - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: ["@dan:server.org"], }); }); @@ -380,11 +380,11 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // new mention in the edit - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.mentions"]).toEqual({ user_ids: ["@dan:server.org"], }); // all mentions in the edited version of the event - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: ["@bob:server.org", "@dan:server.org"], }); }); @@ -411,7 +411,7 @@ describe("", () => { event_id: originalEvent.getId(), }, }, - "org.matrix.msc3952.mentions": { + "m.mentions": { user_ids: [originalEvent.getSender()!], }, }, @@ -430,7 +430,7 @@ describe("", () => { event_id: originalEvent.getId(), }, }, - "org.matrix.msc3952.mentions": { + "m.mentions": { user_ids: [ // sender of event we replied to originalEvent.getSender()!, @@ -457,9 +457,9 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // no new mentions from edit - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.mentions"]).toEqual({}); // edited reply still mentions the parent event sender - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: [originalEvent.getSender()], }); }); @@ -476,12 +476,12 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // new mention in edit - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.mentions"]).toEqual({ user_ids: ["@dan:server.org"], }); // edited reply still mentions the parent event sender // plus new mention @dan - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: [originalEvent.getSender(), "@dan:server.org"], }); }); @@ -497,10 +497,10 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // no mentions in edit - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.mentions"]).toEqual({}); // edited reply still mentions the parent event sender // existing @bob mention removed - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: [originalEvent.getSender()], }); }); @@ -518,7 +518,7 @@ describe("", () => { event_id: originalEvent.getId(), }, }, - "org.matrix.msc3952.mentions": { + "m.mentions": { user_ids: [ // sender of event we replied to originalEvent.getSender()!, @@ -537,9 +537,9 @@ describe("", () => { const messageContent = mockClient.sendMessage.mock.calls[0][2]; // no mentions in edit - expect(messageContent["org.matrix.msc3952.mentions"]).toEqual({}); + expect(messageContent["m.mentions"]).toEqual({}); // edited reply still mentions the parent event sender - expect(messageContent["m.new_content"]["org.matrix.msc3952.mentions"]).toEqual({ + expect(messageContent["m.new_content"]["m.mentions"]).toEqual({ user_ids: [originalEvent.getSender()], }); }); diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index bf9fb0e1851a..39489b3dd278 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -178,7 +178,7 @@ describe("", () => { const content: IContent = {}; attachMentions("@alice:test", content, model, undefined); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, + "m.mentions": {}, }); }); @@ -187,7 +187,7 @@ describe("", () => { const content: IContent = {}; attachMentions("@alice:test", content, model, undefined); expect(content).toEqual({ - "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] }, + "m.mentions": { user_ids: ["@bob:test"] }, }); }); @@ -198,13 +198,13 @@ describe("", () => { type: "m.room.message", user: "@bob:test", room: "!abc:test", - content: { "org.matrix.msc3952.mentions": {} }, + content: { "m.mentions": {} }, event: true, }); let content: IContent = {}; attachMentions("@alice:test", content, model, replyToEvent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] }, + "m.mentions": { user_ids: ["@bob:test"] }, }); // It also adds any other mentioned users, but removes yourself. @@ -212,13 +212,13 @@ describe("", () => { type: "m.room.message", user: "@bob:test", room: "!abc:test", - content: { "org.matrix.msc3952.mentions": { user_ids: ["@alice:test", "@charlie:test"] } }, + content: { "m.mentions": { user_ids: ["@alice:test", "@charlie:test"] } }, event: true, }); content = {}; attachMentions("@alice:test", content, model, replyToEvent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": { user_ids: ["@bob:test", "@charlie:test"] }, + "m.mentions": { user_ids: ["@bob:test", "@charlie:test"] }, }); }); @@ -227,7 +227,7 @@ describe("", () => { const content: IContent = {}; attachMentions("@alice:test", content, model, undefined); expect(content).toEqual({ - "org.matrix.msc3952.mentions": { room: true }, + "m.mentions": { room: true }, }); }); @@ -238,13 +238,13 @@ describe("", () => { type: "m.room.message", user: "@alice:test", room: "!abc:test", - content: { "org.matrix.msc3952.mentions": { room: true } }, + content: { "m.mentions": { room: true } }, event: true, }); const content: IContent = {}; attachMentions("@alice:test", content, model, replyToEvent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, + "m.mentions": {}, }); }); @@ -256,13 +256,13 @@ describe("", () => { user: "@alice:test", room: "!abc:test", // @ts-ignore - Purposefully testing invalid data. - content: { "org.matrix.msc3952.mentions": { user_ids: "@bob:test" } }, + content: { "m.mentions": { user_ids: "@bob:test" } }, event: true, }); const content: IContent = {}; attachMentions("@alice:test", content, model, replyToEvent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, + "m.mentions": {}, }); }); @@ -273,8 +273,8 @@ describe("", () => { const prevContent: IContent = {}; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, - "m.new_content": { "org.matrix.msc3952.mentions": {} }, + "m.mentions": {}, + "m.new_content": { "m.mentions": {} }, }); }); @@ -282,12 +282,12 @@ describe("", () => { const model = new EditorModel([], partsCreator); const content: IContent = { "m.new_content": {} }; const prevContent: IContent = { - "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"], room: true }, + "m.mentions": { user_ids: ["@bob:test"], room: true }, }; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, - "m.new_content": { "org.matrix.msc3952.mentions": {} }, + "m.mentions": {}, + "m.new_content": { "m.mentions": {} }, }); }); @@ -297,19 +297,19 @@ describe("", () => { const prevContent: IContent = {}; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] }, - "m.new_content": { "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] } }, + "m.mentions": { user_ids: ["@bob:test"] }, + "m.new_content": { "m.mentions": { user_ids: ["@bob:test"] } }, }); }); it("test prev user mentions", () => { const model = new EditorModel([partsCreator.userPill("Bob", "@bob:test")], partsCreator); const content: IContent = { "m.new_content": {} }; - const prevContent: IContent = { "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] } }; + const prevContent: IContent = { "m.mentions": { user_ids: ["@bob:test"] } }; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, - "m.new_content": { "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] } }, + "m.mentions": {}, + "m.new_content": { "m.mentions": { user_ids: ["@bob:test"] } }, }); }); @@ -319,19 +319,19 @@ describe("", () => { const prevContent: IContent = {}; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": { room: true }, - "m.new_content": { "org.matrix.msc3952.mentions": { room: true } }, + "m.mentions": { room: true }, + "m.new_content": { "m.mentions": { room: true } }, }); }); it("test prev room mention", () => { const model = new EditorModel([partsCreator.atRoomPill("@room")], partsCreator); const content: IContent = { "m.new_content": {} }; - const prevContent: IContent = { "org.matrix.msc3952.mentions": { room: true } }; + const prevContent: IContent = { "m.mentions": { room: true } }; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, - "m.new_content": { "org.matrix.msc3952.mentions": { room: true } }, + "m.mentions": {}, + "m.new_content": { "m.mentions": { room: true } }, }); }); @@ -340,11 +340,11 @@ describe("", () => { const model = new EditorModel([], partsCreator); const content: IContent = { "m.new_content": {} }; // @ts-ignore - Purposefully testing invalid data. - const prevContent: IContent = { "org.matrix.msc3952.mentions": { user_ids: "@bob:test" } }; + const prevContent: IContent = { "m.mentions": { user_ids: "@bob:test" } }; attachMentions("@alice:test", content, model, undefined, prevContent); expect(content).toEqual({ - "org.matrix.msc3952.mentions": {}, - "m.new_content": { "org.matrix.msc3952.mentions": {} }, + "m.mentions": {}, + "m.new_content": { "m.mentions": {} }, }); }); }); diff --git a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx index fa0b61ef1dfe..6171fd6bd2c8 100644 --- a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx +++ b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx @@ -135,7 +135,7 @@ describe("", () => { "org.matrix.msc1767.text": "Voice message", "org.matrix.msc3245.voice": {}, "url": "mxc://example.com/voice", - "org.matrix.msc3952.mentions": {}, + "m.mentions": {}, }); }); @@ -189,7 +189,7 @@ describe("", () => { event_id: replyToEvent.getId(), }, }, - "org.matrix.msc3952.mentions": { user_ids: ["@bob:test"] }, + "m.mentions": { user_ids: ["@bob:test"] }, }); }); }); diff --git a/test/models/notificationsettings/pushrules_bug_botnotices.json b/test/models/notificationsettings/pushrules_bug_botnotices.json index 7956afb99c70..6ae0350140af 100644 --- a/test/models/notificationsettings/pushrules_bug_botnotices.json +++ b/test/models/notificationsettings/pushrules_bug_botnotices.json @@ -345,13 +345,13 @@ "enabled": true }, { - "rule_id": ".org.matrix.msc3952.is_user_mention", + "rule_id": ".m.rule.is_user_mention", "default": true, "enabled": true, "conditions": [ { "kind": "event_property_contains", - "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "key": "content.m\\.mentions.user_ids", "value": "@jannemk:element.io" } ], @@ -363,13 +363,13 @@ ] }, { - "rule_id": ".org.matrix.msc3952.is_room_mention", + "rule_id": ".m.rule.is_room_mention", "default": true, "enabled": true, "conditions": [ { "kind": "event_property_is", - "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "key": "content.m\\.mentions.room", "value": true }, { diff --git a/test/models/notificationsettings/pushrules_bug_keyword_only.json b/test/models/notificationsettings/pushrules_bug_keyword_only.json index 172957e53ddd..1526681a634a 100644 --- a/test/models/notificationsettings/pushrules_bug_keyword_only.json +++ b/test/models/notificationsettings/pushrules_bug_keyword_only.json @@ -315,13 +315,13 @@ "enabled": true }, { - "rule_id": ".org.matrix.msc3952.is_user_mention", + "rule_id": ".m.rule.is_user_mention", "default": true, "enabled": false, "conditions": [ { "kind": "event_property_contains", - "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "key": "content.m\\.mentions.user_ids", "value": "@jannemk:element.io" } ], @@ -333,13 +333,13 @@ ] }, { - "rule_id": ".org.matrix.msc3952.is_room_mention", + "rule_id": ".m.rule.is_room_mention", "default": true, "enabled": true, "conditions": [ { "kind": "event_property_is", - "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "key": "content.m\\.mentions.room", "value": true }, { diff --git a/test/models/notificationsettings/pushrules_default.json b/test/models/notificationsettings/pushrules_default.json index 1f6252410a16..a50531a99ded 100644 --- a/test/models/notificationsettings/pushrules_default.json +++ b/test/models/notificationsettings/pushrules_default.json @@ -259,7 +259,7 @@ "conditions": [ { "kind": "event_property_contains", - "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "key": "content.m\\.mentions.user_ids", "value": "@jannetestuser:beta.matrix.org" } ], @@ -269,7 +269,7 @@ "set_tweak": "highlight" } ], - "rule_id": ".org.matrix.msc3952.is_user_mention", + "rule_id": ".m.rule.is_user_mention", "default": true, "enabled": true, "kind": "override" @@ -299,7 +299,7 @@ "conditions": [ { "kind": "event_property_is", - "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "key": "content.m\\.mentions.room", "value": true }, { @@ -313,7 +313,7 @@ "set_tweak": "highlight" } ], - "rule_id": ".org.matrix.msc3952.is_room_mention", + "rule_id": ".m.rule.is_room_mention", "default": true, "enabled": true, "kind": "override" diff --git a/test/models/notificationsettings/pushrules_default_new.json b/test/models/notificationsettings/pushrules_default_new.json index 379e2d222dc8..c405f41fa336 100644 --- a/test/models/notificationsettings/pushrules_default_new.json +++ b/test/models/notificationsettings/pushrules_default_new.json @@ -265,7 +265,7 @@ "conditions": [ { "kind": "event_property_contains", - "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "key": "content.m\\.mentions.user_ids", "value": "@jannetestuser:beta.matrix.org" } ], @@ -275,7 +275,7 @@ "set_tweak": "highlight" } ], - "rule_id": ".org.matrix.msc3952.is_user_mention", + "rule_id": ".m.rule.is_user_mention", "default": true, "enabled": true, "kind": "override" @@ -305,7 +305,7 @@ "conditions": [ { "kind": "event_property_is", - "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "key": "content.m\\.mentions.room", "value": true }, { @@ -319,7 +319,7 @@ "set_tweak": "highlight" } ], - "rule_id": ".org.matrix.msc3952.is_room_mention", + "rule_id": ".m.rule.is_room_mention", "default": true, "enabled": true, "kind": "override" diff --git a/test/models/notificationsettings/pushrules_sample.json b/test/models/notificationsettings/pushrules_sample.json index 2c9f4b2af7b4..7c8819e2b070 100644 --- a/test/models/notificationsettings/pushrules_sample.json +++ b/test/models/notificationsettings/pushrules_sample.json @@ -501,13 +501,13 @@ "kind": "override" }, { - "rule_id": ".org.matrix.msc3952.is_user_mention", + "rule_id": ".m.rule.is_user_mention", "default": true, "enabled": true, "conditions": [ { "kind": "event_property_contains", - "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "key": "content.m\\.mentions.user_ids", "value": "@jannemk:element.io" } ], @@ -520,13 +520,13 @@ "kind": "override" }, { - "rule_id": ".org.matrix.msc3952.is_room_mention", + "rule_id": ".m.rule.is_room_mention", "default": true, "enabled": true, "conditions": [ { "kind": "event_property_is", - "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "key": "content.m\\.mentions.room", "value": true }, { diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts index cffc423d1c69..a50f7dd36d89 100644 --- a/test/test-utils/pushRules.ts +++ b/test/test-utils/pushRules.ts @@ -237,12 +237,12 @@ export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ conditions: [ { kind: "event_property_contains", - key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + key: "content.m\\.mentions.user_ids", value_type: "user_id", }, ], actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], - rule_id: ".org.matrix.msc3952.is_user_mention", + rule_id: ".m.rule.is_user_mention", default: true, enabled: true, }, @@ -255,11 +255,11 @@ export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ }, { conditions: [ - { kind: "event_property_is", key: "content.org\\.matrix\\.msc3952\\.mentions.room", value: true }, + { kind: "event_property_is", key: "content.m\\.mentions.room", value: true }, { kind: "sender_notification_permission", key: "room" }, ], actions: ["notify", { set_tweak: "highlight" }], - rule_id: ".org.matrix.msc3952.is_room_mention", + rule_id: ".m.rule.is_room_mention", default: true, enabled: true, }, diff --git a/yarn.lock b/yarn.lock index be910e5aaa0f..0e842a1bf391 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1619,10 +1619,10 @@ version "3.2.14" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" -"@matrix-org/react-sdk-module-api@^0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.6.tgz#941872ed081acdca9d247ccd6e146265aa24010b" - integrity sha512-FydbJYSMecpDIGk4fVQ9djjckQdbJPV9bH3px78TQ+MX/WHmzPmjEpMPTeP3uDSeg0EWmfoIFdNypJglMqAHpw== +"@matrix-org/react-sdk-module-api@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-1.0.0.tgz#de73e163a439fe330f6971a6a0cef2ccb090d616" + integrity sha512-drhPkoPWitAv9bXS2q8cyaqPta/KGF+Ph3aZSmaYiOPyY5S84e4Ju3JI6/HExqF8+HyBsajlCKtyvTZsMsTIFA== dependencies: "@babel/runtime" "^7.17.9" @@ -3618,6 +3618,17 @@ cypress-real-events@^1.7.1: resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.8.1.tgz#d00c7fe93124bbe7c0f27296684838614d24a840" integrity sha512-8fFnA8EzS3EVbAmpSEUf3A8yZCmfU3IPOSGUDVFCdE1ke1gYL1A+gvXXV6HKUbTPRuvKKt2vpaMbUwYLpDRswQ== +cypress-terminal-report@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/cypress-terminal-report/-/cypress-terminal-report-5.3.2.tgz#3a6b1cbda6101498243d17c5a2a646cb69af0336" + integrity sha512-0Gf/pXjrYpTkf2aR3LAFGoxEM0KulWsMKCu+52YJB6l7GEP2RLAOAr32tcZHZiL2EWnS0vE4ollomMzGvCci0w== + dependencies: + chalk "^4.0.0" + fs-extra "^10.1.0" + safe-json-stringify "^1.2.0" + semver "^7.3.5" + tv4 "^1.3.0" + cypress@^12.0.0: version "12.16.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.16.0.tgz#d0dcd0725a96497f4c60cf54742242259847924c" @@ -4781,6 +4792,15 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^11.0.0: version "11.1.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" @@ -7829,6 +7849,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-json-stringify@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -8581,6 +8606,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +tv4@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" + integrity sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"