UNPKG

matrix-react-sdk

Version:
1,310 lines (1,090 loc) 166 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.parseCommandString = parseCommandString; exports.getCommand = getCommand; exports.CommandMap = exports.Commands = exports.Command = exports.CommandCategories = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var React = _interopRequireWildcard(require("react")); var ContentHelpers = _interopRequireWildcard(require("matrix-js-sdk/src/content-helpers")); var _MatrixClientPeg = require("./MatrixClientPeg"); var _dispatcher = _interopRequireDefault(require("./dispatcher/dispatcher")); var sdk = _interopRequireWildcard(require("./index")); var _languageHandler = require("./languageHandler"); var _Modal = _interopRequireDefault(require("./Modal")); var _MultiInviter = _interopRequireDefault(require("./utils/MultiInviter")); var _HtmlUtils = require("./HtmlUtils"); var _QuestionDialog = _interopRequireDefault(require("./components/views/dialogs/QuestionDialog")); var _WidgetUtils = _interopRequireDefault(require("./utils/WidgetUtils")); var _colour = require("./utils/colour"); var _UserAddress = require("./UserAddress"); var _UrlUtils = require("./utils/UrlUtils"); var _IdentityServerUtils = require("./utils/IdentityServerUtils"); var _Permalinks = require("./utils/permalinks/Permalinks"); var _RoomInvite = require("./RoomInvite"); var _WidgetType = require("./widgets/WidgetType"); var _Jitsi = require("./widgets/Jitsi"); var _parse = require("parse5"); var _BugReportDialog = _interopRequireDefault(require("./components/views/dialogs/BugReportDialog")); var _createRoom = require("./createRoom"); var _actions = require("./dispatcher/actions"); var _membership = require("./utils/membership"); var _SdkConfig = _interopRequireDefault(require("./SdkConfig")); var _SettingsStore = _interopRequireDefault(require("./settings/SettingsStore")); var _UIFeature = require("./settings/UIFeature"); var _effects = require("./effects"); var _CallHandler = _interopRequireDefault(require("./CallHandler")); var _Rooms = require("./Rooms"); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } const singleMxcUpload = async () => /*: Promise<any>*/ { return new Promise(resolve => { const fileSelector = document.createElement('input'); fileSelector.setAttribute('type', 'file'); fileSelector.onchange = (ev /*: HTMLInputEvent*/ ) => { const file = ev.target.files[0]; const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); _Modal.default.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { file, onFinished: shouldContinue => { resolve(shouldContinue ? _MatrixClientPeg.MatrixClientPeg.get().uploadContent(file) : null); } }); }; fileSelector.click(); }); }; const CommandCategories = { "messages": (0, _languageHandler._td)("Messages"), "actions": (0, _languageHandler._td)("Actions"), "admin": (0, _languageHandler._td)("Admin"), "advanced": (0, _languageHandler._td)("Advanced"), "effects": (0, _languageHandler._td)("Effects"), "other": (0, _languageHandler._td)("Other") }; exports.CommandCategories = CommandCategories; class Command { constructor(opts /*: ICommandOpts*/ ) { (0, _defineProperty2.default)(this, "command", void 0); (0, _defineProperty2.default)(this, "aliases", void 0); (0, _defineProperty2.default)(this, "args", void 0); (0, _defineProperty2.default)(this, "description", void 0); (0, _defineProperty2.default)(this, "runFn", void 0); (0, _defineProperty2.default)(this, "category", void 0); (0, _defineProperty2.default)(this, "hideCompletionAfterSpace", void 0); (0, _defineProperty2.default)(this, "_isEnabled", void 0); this.command = opts.command; this.aliases = opts.aliases || []; this.args = opts.args || ""; this.description = opts.description; this.runFn = opts.runFn; this.category = opts.category || CommandCategories.other; this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; this._isEnabled = opts.isEnabled; } getCommand() { return `/${this.command}`; } getCommandWithArgs() { return this.getCommand() + " " + this.args; } run(roomId /*: string*/ , args /*: string*/ ) { // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` if (!this.runFn) return reject((0, _languageHandler._t)("Command error")); return this.runFn.bind(this)(roomId, args); } getUsage() { return (0, _languageHandler._t)('Usage') + ': ' + this.getCommandWithArgs(); } isEnabled() { return this._isEnabled ? this._isEnabled() : true; } } exports.Command = Command; function reject(error) { return { error }; } function success(promise /*: Promise<any>*/ ) { return { promise }; } /* Disable the "unexpected this" error for these commands - all of the run * functions are called with `this` bound to the Command instance. */ const Commands = [new Command({ command: 'spoiler', args: '<message>', description: (0, _languageHandler._td)('Sends the given message as a spoiler'), runFn: function (roomId, message) { return success(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`)); }, category: CommandCategories.messages }), new Command({ command: 'shrug', args: '<message>', description: (0, _languageHandler._td)('Prepends ¯\\_(ツ)_/¯ to a plain-text message'), runFn: function (roomId, args) { let message = '¯\\_(ツ)_/¯'; if (args) { message = message + ' ' + args; } return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages }), new Command({ command: 'tableflip', args: '<message>', description: (0, _languageHandler._td)('Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message'), runFn: function (roomId, args) { let message = '(╯°□°)╯︵ ┻━┻'; if (args) { message = message + ' ' + args; } return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages }), new Command({ command: 'unflip', args: '<message>', description: (0, _languageHandler._td)('Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message'), runFn: function (roomId, args) { let message = '┬──┬ ノ( ゜-゜ノ)'; if (args) { message = message + ' ' + args; } return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages }), new Command({ command: 'lenny', args: '<message>', description: (0, _languageHandler._td)('Prepends ( ͡° ͜ʖ ͡°) to a plain-text message'), runFn: function (roomId, args) { let message = '( ͡° ͜ʖ ͡°)'; if (args) { message = message + ' ' + args; } return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages }), new Command({ command: 'plain', args: '<message>', description: (0, _languageHandler._td)('Sends a message as plain text, without interpreting it as markdown'), runFn: function (roomId, messages) { return success(ContentHelpers.makeTextMessage(messages)); }, category: CommandCategories.messages }), new Command({ command: 'html', args: '<message>', description: (0, _languageHandler._td)('Sends a message as html, without interpreting it as markdown'), runFn: function (roomId, messages) { return success(ContentHelpers.makeHtmlMessage(messages, messages)); }, category: CommandCategories.messages }), new Command({ command: 'ddg', args: '<query>', description: (0, _languageHandler._td)('Searches DuckDuckGo for results'), runFn: function () { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. _Modal.default.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { title: (0, _languageHandler._t)('/ddg is not a command'), description: (0, _languageHandler._t)('To use it, just wait for autocomplete results to load and tab through them.') }); return success(); }, category: CommandCategories.actions, hideCompletionAfterSpace: true }), new Command({ command: 'upgraderoom', args: '<new_version>', description: (0, _languageHandler._td)('Upgrades a room to a new version'), runFn: function (roomId, args) { if (args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const room = cli.getRoom(roomId); if (!room.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { return reject((0, _languageHandler._t)("You do not have the required permissions to use this command.")); } const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog"); const { finished } = _Modal.default.createTrackedDialog('Slash Commands', 'upgrade room confirmation', RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/ null, /*isPriority=*/ false, /*isStatic=*/ true); return success(finished.then(async ([resp]) => { if (!resp.continue) return; let checkForUpgradeFn; try { const upgradePromise = cli.upgradeRoom(roomId, args); // We have to wait for the js-sdk to give us the room back so // we can more effectively abuse the MultiInviter behaviour // which heavily relies on the Room object being available. if (resp.invite) { checkForUpgradeFn = async newRoom => { // The upgradePromise should be done by the time we await it here. const { replacement_room: newRoomId } = await upgradePromise; if (newRoom.roomId !== newRoomId) return; const toInvite = [...room.getMembersWithMembership("join"), ...room.getMembersWithMembership("invite")].map(m => m.userId).filter(m => m !== cli.getUserId()); if (toInvite.length > 0) { // Errors are handled internally to this function await (0, _RoomInvite.inviteUsersToRoom)(newRoomId, toInvite); } cli.removeListener('Room', checkForUpgradeFn); }; cli.on('Room', checkForUpgradeFn); } // We have to await after so that the checkForUpgradesFn has a proper reference // to the new room's ID. await upgradePromise; } catch (e) { console.error(e); if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); _Modal.default.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { title: (0, _languageHandler._t)('Error upgrading room'), description: (0, _languageHandler._t)('Double check that your server supports the room version chosen and try again.') }); } })); } return reject(this.getUsage()); }, category: CommandCategories.admin }), new Command({ command: 'nick', args: '<display_name>', description: (0, _languageHandler._td)('Changes your display nickname'), runFn: function (roomId, args) { if (args) { return success(_MatrixClientPeg.MatrixClientPeg.get().setDisplayName(args)); } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: 'myroomnick', aliases: ['roomnick'], args: '<display_name>', description: (0, _languageHandler._td)('Changes your display nickname in the current room only'), runFn: function (roomId, args) { if (args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId()); const content = _objectSpread(_objectSpread({}, ev ? ev.getContent() : { membership: 'join' }), {}, { displayname: args }); return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId())); } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: 'roomavatar', args: '[<mxc_url>]', description: (0, _languageHandler._td)('Changes the avatar of the current room'), runFn: function (roomId, args) { let promise = Promise.resolve(args); if (!args) { promise = singleMxcUpload(); } return success(promise.then(url => { if (!url) return; return _MatrixClientPeg.MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.avatar', { url }, ''); })); }, category: CommandCategories.actions }), new Command({ command: 'myroomavatar', args: '[<mxc_url>]', description: (0, _languageHandler._td)('Changes your avatar in this current room only'), runFn: function (roomId, args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const room = cli.getRoom(roomId); const userId = cli.getUserId(); let promise = Promise.resolve(args); if (!args) { promise = singleMxcUpload(); } return success(promise.then(url => { if (!url) return; const ev = room.currentState.getStateEvents('m.room.member', userId); const content = _objectSpread(_objectSpread({}, ev ? ev.getContent() : { membership: 'join' }), {}, { avatar_url: url }); return cli.sendStateEvent(roomId, 'm.room.member', content, userId); })); }, category: CommandCategories.actions }), new Command({ command: 'myavatar', args: '[<mxc_url>]', description: (0, _languageHandler._td)('Changes your avatar in all rooms'), runFn: function (roomId, args) { let promise = Promise.resolve(args); if (!args) { promise = singleMxcUpload(); } return success(promise.then(url => { if (!url) return; return _MatrixClientPeg.MatrixClientPeg.get().setAvatarUrl(url); })); }, category: CommandCategories.actions }), new Command({ command: 'topic', args: '[<topic>]', description: (0, _languageHandler._td)('Gets or sets the room topic'), runFn: function (roomId, args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); if (args) { return success(cli.setRoomTopic(roomId, args)); } const room = cli.getRoom(roomId); if (!room) return reject((0, _languageHandler._t)("Failed to set topic")); const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); const topic = topicEvents && topicEvents.getContent().topic; const topicHtml = topic ? (0, _HtmlUtils.linkifyAndSanitizeHtml)(topic) : (0, _languageHandler._t)('This room has no topic.'); const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); _Modal.default.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { title: room.name, description: /*#__PURE__*/React.createElement("div", { dangerouslySetInnerHTML: { __html: topicHtml } }), hasCloseButton: true }); return success(); }, category: CommandCategories.admin }), new Command({ command: 'roomname', args: '<name>', description: (0, _languageHandler._td)('Sets the room name'), runFn: function (roomId, args) { if (args) { return success(_MatrixClientPeg.MatrixClientPeg.get().setRoomName(roomId, args)); } return reject(this.getUsage()); }, category: CommandCategories.admin }), new Command({ command: 'invite', args: '<user-id> [<reason>]', description: (0, _languageHandler._td)('Invites user with given id to current room'), runFn: function (roomId, args) { if (args) { const [address, reason] = args.split(/\s+(.+)/); if (address) { // We use a MultiInviter to re-use the invite logic, even though // we're only inviting one user. // If we need an identity server but don't have one, things // get a bit more complex here, but we try to show something // meaningful. let prom = Promise.resolve(); if ((0, _UserAddress.getAddressType)(address) === 'email' && !_MatrixClientPeg.MatrixClientPeg.get().getIdentityServerUrl()) { const defaultIdentityServerUrl = (0, _IdentityServerUtils.getDefaultIdentityServerUrl)(); if (defaultIdentityServerUrl) { const { finished } = _Modal.default.createTrackedDialog('Slash Commands', 'Identity server', _QuestionDialog.default, { title: (0, _languageHandler._t)("Use an identity server"), description: /*#__PURE__*/React.createElement("p", null, (0, _languageHandler._t)("Use an identity server to invite by email. " + "Click continue to use the default identity server " + "(%(defaultIdentityServerName)s) or manage in Settings.", { defaultIdentityServerName: (0, _UrlUtils.abbreviateUrl)(defaultIdentityServerUrl) })), button: (0, _languageHandler._t)("Continue") }); prom = finished.then(([useDefault]) => { if (useDefault) { (0, _IdentityServerUtils.useDefaultIdentityServer)(); return; } throw new Error((0, _languageHandler._t)("Use an identity server to invite by email. Manage in Settings.")); }); } else { return reject((0, _languageHandler._t)("Use an identity server to invite by email. Manage in Settings.")); } } const inviter = new _MultiInviter.default(roomId); return success(prom.then(() => { return inviter.invite([address], reason); }).then(() => { if (inviter.getCompletionState(address) !== "invited") { throw new Error(inviter.getErrorText(address)); } })); } } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: 'join', aliases: ['j', 'goto'], args: '<room-address>', description: (0, _languageHandler._td)('Joins room with given address'), runFn: function (_, args) { if (args) { // Note: we support 2 versions of this command. The first is // the public-facing one for most users and the other is a // power-user edition where someone may join via permalink or // room ID with optional servers. Practically, this results // in the following variations: // /join #example:example.org // /join !example:example.org // /join !example:example.org altserver.com elsewhere.ca // /join https://matrix.to/#/!example:example.org?via=altserver.com // The command also supports event permalinks transparently: // /join https://matrix.to/#/!example:example.org/$something:example.org // /join https://matrix.to/#/!example:example.org/$something:example.org?via=altserver.com const params = args.split(' '); if (params.length < 1) return reject(this.getUsage()); let isPermalink = false; if (params[0].startsWith("http:") || params[0].startsWith("https:")) { // It's at least a URL - try and pull out a hostname to check against the // permalink handler const parsedUrl = new URL(params[0]); const hostname = parsedUrl.host || parsedUrl.hostname; // takes first non-falsey value // if we're using a Element permalink handler, this will catch it before we get much further. // see below where we make assumptions about parsing the URL. if ((0, _Permalinks.isPermalinkHost)(hostname)) { isPermalink = true; } } if (params[0][0] === '#') { let roomAlias = params[0]; if (!roomAlias.includes(':')) { roomAlias += ':' + _MatrixClientPeg.MatrixClientPeg.get().getDomain(); } _dispatcher.default.dispatch({ action: 'view_room', room_alias: roomAlias, auto_join: true, _type: "slash_command" // instrumentation }); return success(); } else if (params[0][0] === '!') { const [roomId, ...viaServers] = params; _dispatcher.default.dispatch({ action: 'view_room', room_id: roomId, opts: { // These are passed down to the js-sdk's /join call viaServers: viaServers }, via_servers: viaServers, // for the rejoin button auto_join: true, _type: "slash_command" // instrumentation }); return success(); } else if (isPermalink) { const permalinkParts = (0, _Permalinks.parsePermalink)(params[0]); // This check technically isn't needed because we already did our // safety checks up above. However, for good measure, let's be sure. if (!permalinkParts) { return reject(this.getUsage()); } // If for some reason someone wanted to join a group or user, we should // stop them now. if (!permalinkParts.roomIdOrAlias) { return reject(this.getUsage()); } const entity = permalinkParts.roomIdOrAlias; const viaServers = permalinkParts.viaServers; const eventId = permalinkParts.eventId; const dispatch = { action: 'view_room', auto_join: true, _type: "slash_command" // instrumentation }; if (entity[0] === '!') dispatch["room_id"] = entity;else dispatch["room_alias"] = entity; if (eventId) { dispatch["event_id"] = eventId; dispatch["highlighted"] = true; } if (viaServers) { // For the join dispatch["opts"] = { // These are passed down to the js-sdk's /join call viaServers: viaServers }; // For if the join fails (rejoin button) dispatch['via_servers'] = viaServers; } _dispatcher.default.dispatch(dispatch); return success(); } } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: 'part', args: '[<room-address>]', description: (0, _languageHandler._td)('Leave room'), runFn: function (roomId, args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); let targetRoomId; if (args) { const matches = args.match(/^(\S+)$/); if (matches) { let roomAlias = matches[1]; if (roomAlias[0] !== '#') return reject(this.getUsage()); if (!roomAlias.includes(':')) { roomAlias += ':' + cli.getDomain(); } // Try to find a room with this alias const rooms = cli.getRooms(); for (let i = 0; i < rooms.length; i++) { const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases'); for (let j = 0; j < aliasEvents.length; j++) { const aliases = aliasEvents[j].getContent().aliases || []; for (let k = 0; k < aliases.length; k++) { if (aliases[k] === roomAlias) { targetRoomId = rooms[i].roomId; break; } } if (targetRoomId) break; } if (targetRoomId) break; } if (!targetRoomId) return reject((0, _languageHandler._t)('Unrecognised room address:') + ' ' + roomAlias); } } if (!targetRoomId) targetRoomId = roomId; return success((0, _membership.leaveRoomBehaviour)(targetRoomId)); }, category: CommandCategories.actions }), new Command({ command: 'kick', args: '<user-id> [reason]', description: (0, _languageHandler._td)('Kicks user with given id'), runFn: function (roomId, args) { if (args) { const matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { return success(_MatrixClientPeg.MatrixClientPeg.get().kick(roomId, matches[1], matches[3])); } } return reject(this.getUsage()); }, category: CommandCategories.admin }), new Command({ command: 'ban', args: '<user-id> [reason]', description: (0, _languageHandler._td)('Bans user with given id'), runFn: function (roomId, args) { if (args) { const matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { return success(_MatrixClientPeg.MatrixClientPeg.get().ban(roomId, matches[1], matches[3])); } } return reject(this.getUsage()); }, category: CommandCategories.admin }), new Command({ command: 'unban', args: '<user-id>', description: (0, _languageHandler._td)('Unbans user with given ID'), runFn: function (roomId, args) { if (args) { const matches = args.match(/^(\S+)$/); if (matches) { // Reset the user membership to "leave" to unban him return success(_MatrixClientPeg.MatrixClientPeg.get().unban(roomId, matches[1])); } } return reject(this.getUsage()); }, category: CommandCategories.admin }), new Command({ command: 'ignore', args: '<user-id>', description: (0, _languageHandler._td)('Ignores a user, hiding their messages from you'), runFn: function (roomId, args) { if (args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const matches = args.match(/^(@[^:]+:\S+)$/); if (matches) { const userId = matches[1]; const ignoredUsers = cli.getIgnoredUsers(); ignoredUsers.push(userId); // de-duped internally in the js-sdk return success(cli.setIgnoredUsers(ignoredUsers).then(() => { const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); _Modal.default.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, { title: (0, _languageHandler._t)('Ignored user'), description: /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", null, (0, _languageHandler._t)('You are now ignoring %(userId)s', { userId }))) }); })); } } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: 'unignore', args: '<user-id>', description: (0, _languageHandler._td)('Stops ignoring a user, showing their messages going forward'), runFn: function (roomId, args) { if (args) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const matches = args.match(/(^@[^:]+:\S+$)/); if (matches) { const userId = matches[1]; const ignoredUsers = cli.getIgnoredUsers(); const index = ignoredUsers.indexOf(userId); if (index !== -1) ignoredUsers.splice(index, 1); return success(cli.setIgnoredUsers(ignoredUsers).then(() => { const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); _Modal.default.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, { title: (0, _languageHandler._t)('Unignored user'), description: /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", null, (0, _languageHandler._t)('You are no longer ignoring %(userId)s', { userId }))) }); })); } } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: 'op', args: '<user-id> [<power-level>]', description: (0, _languageHandler._td)('Define the power level of a user'), runFn: function (roomId, 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 cli = _MatrixClientPeg.MatrixClientPeg.get(); const room = cli.getRoom(roomId); if (!room) return reject((0, _languageHandler._t)("Command failed")); const member = room.getMember(userId); if (!member || (0, _membership.getEffectiveMembership)(member.membership) === _membership.EffectiveMembership.Leave) { return reject((0, _languageHandler._t)("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 }), new Command({ command: 'deop', args: '<user-id>', description: (0, _languageHandler._td)('Deops user with given id'), runFn: function (roomId, args) { if (args) { const matches = args.match(/^(\S+)$/); if (matches) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const room = cli.getRoom(roomId); if (!room) return reject((0, _languageHandler._t)("Command failed")); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); if (!powerLevelEvent.getContent().users[args]) return reject((0, _languageHandler._t)("Could not find user in room")); return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); } } return reject(this.getUsage()); }, category: CommandCategories.admin }), new Command({ command: 'devtools', description: (0, _languageHandler._td)('Opens the Developer Tools dialog'), runFn: function (roomId) { const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); _Modal.default.createDialog(DevtoolsDialog, { roomId }); return success(); }, category: CommandCategories.advanced }), new Command({ command: 'addwidget', args: '<url | embed code | Jitsi url>', description: (0, _languageHandler._td)('Adds a custom widget by URL to the room'), isEnabled: () => _SettingsStore.default.getValue(_UIFeature.UIFeature.Widgets), runFn: function (roomId, widgetUrl) { if (!widgetUrl) { return reject((0, _languageHandler._t)("Please supply a widget URL or embed code")); } // Try and parse out a widget URL from iframes if (widgetUrl.toLowerCase().startsWith("<iframe ")) { // We use parse5, which doesn't render/create a DOM node. It instead runs // some superfast regex over the text so we don't have to. const embed = (0, _parse.parseFragment)(widgetUrl); if (embed && embed.childNodes && embed.childNodes.length === 1) { const iframe = embed.childNodes[0]; if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) { const srcAttr = iframe.attrs.find(a => a.name === 'src'); console.log("Pulling URL out of iframe (embed code)"); widgetUrl = srcAttr.value; } } } if (!widgetUrl.startsWith("https://") && !widgetUrl.startsWith("http://")) { return reject((0, _languageHandler._t)("Please supply a https:// or http:// widget URL")); } if (_WidgetUtils.default.canUserModifyWidgets(roomId)) { const userId = _MatrixClientPeg.MatrixClientPeg.get().getUserId(); const nowMs = new Date().getTime(); const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`); let type = _WidgetType.WidgetType.CUSTOM; let name = "Custom Widget"; let data = {}; // Make the widget a Jitsi widget if it looks like a Jitsi widget const jitsiData = _Jitsi.Jitsi.getInstance().parsePreferredConferenceUrl(widgetUrl); if (jitsiData) { console.log("Making /addwidget widget a Jitsi conference"); type = _WidgetType.WidgetType.JITSI; name = "Jitsi Conference"; data = jitsiData; widgetUrl = _WidgetUtils.default.getLocalJitsiWrapperUrl(); } return success(_WidgetUtils.default.setRoomWidget(roomId, widgetId, type, widgetUrl, name, data)); } else { return reject((0, _languageHandler._t)("You cannot modify widgets in this room.")); } }, category: CommandCategories.admin }), new Command({ command: 'verify', args: '<user-id> <device-id> <device-signing-key>', description: (0, _languageHandler._td)('Verifies a user, session, and pubkey tuple'), runFn: function (roomId, args) { if (args) { const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); if (matches) { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const userId = matches[1]; const deviceId = matches[2]; const fingerprint = matches[3]; return success((async () => { const device = cli.getStoredDevice(userId, deviceId); if (!device) { throw new Error((0, _languageHandler._t)('Unknown (user, session) pair:') + ` (${userId}, ${deviceId})`); } const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); if (deviceTrust.isVerified()) { if (device.getFingerprint() === fingerprint) { throw new Error((0, _languageHandler._t)('Session already verified!')); } else { throw new Error((0, _languageHandler._t)('WARNING: Session already verified, but keys do NOT MATCH!')); } } if (device.getFingerprint() !== fingerprint) { const fprint = device.getFingerprint(); throw new Error((0, _languageHandler._t)('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + '"%(fingerprint)s". This could mean your communications are being intercepted!', { fprint, userId, deviceId, fingerprint })); } await cli.setDeviceVerified(userId, deviceId, true); // Tell the user we verified everything const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); _Modal.default.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { title: (0, _languageHandler._t)('Verified key'), description: /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", null, (0, _languageHandler._t)('The signing key you provided matches the signing key you received ' + 'from %(userId)s\'s session %(deviceId)s. Session marked as verified.', { userId, deviceId }))) }); })()); } } return reject(this.getUsage()); }, category: CommandCategories.advanced }), new Command({ command: 'discardsession', description: (0, _languageHandler._td)('Forces the current outbound group session in an encrypted room to be discarded'), runFn: function (roomId) { try { _MatrixClientPeg.MatrixClientPeg.get().forceDiscardSession(roomId); } catch (e) { return reject(e.message); } return success(); }, category: CommandCategories.advanced }), new Command({ command: "rainbow", description: (0, _languageHandler._td)("Sends the given message coloured as a rainbow"), args: '<message>', runFn: function (roomId, args) { if (!args) return reject(this.getUserId()); return success(ContentHelpers.makeHtmlMessage(args, (0, _colour.textToHtmlRainbow)(args))); }, category: CommandCategories.messages }), new Command({ command: "rainbowme", description: (0, _languageHandler._td)("Sends the given emote coloured as a rainbow"), args: '<message>', runFn: function (roomId, args) { if (!args) return reject(this.getUserId()); return success(ContentHelpers.makeHtmlEmote(args, (0, _colour.textToHtmlRainbow)(args))); }, category: CommandCategories.messages }), new Command({ command: "help", description: (0, _languageHandler._td)("Displays list of commands with usages and descriptions"), runFn: function () { const SlashCommandHelpDialog = sdk.getComponent('dialogs.SlashCommandHelpDialog'); _Modal.default.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog); return success(); }, category: CommandCategories.advanced }), new Command({ command: "whois", description: (0, _languageHandler._td)("Displays information about a user"), args: "<user-id>", runFn: function (roomId, userId) { if (!userId || !userId.startsWith("@") || !userId.includes(":")) { return reject(this.getUsage()); } const member = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomId).getMember(userId); _dispatcher.default.dispatch({ action: _actions.Action.ViewUser, // XXX: We should be using a real member object and not assuming what the // receiver wants. member: member || { userId } }); return success(); }, category: CommandCategories.advanced }), new Command({ command: "rageshake", aliases: ["bugreport"], description: (0, _languageHandler._td)("Send a bug report with logs"), isEnabled: () => !!_SdkConfig.default.get().bug_report_endpoint_url, args: "<description>", runFn: function (roomId, args) { return success(_Modal.default.createTrackedDialog('Slash Commands', 'Bug Report Dialog', _BugReportDialog.default, { initialText: args }).finished); }, category: CommandCategories.advanced }), new Command({ command: "query", description: (0, _languageHandler._td)("Opens chat with the given user"), args: "<user-id>", runFn: function (roomId, userId) { // easter-egg for now: look up phone numbers through the thirdparty API // (very dumb phone number detection...) const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId); if (!userId || (!userId.startsWith("@") || !userId.includes(":")) && !isPhoneNumber) { return reject(this.getUsage()); } return success((async () => { if (isPhoneNumber) { const results = await _CallHandler.default.sharedInstance().pstnLookup(this.state.value); if (!results || results.length === 0 || !results[0].userid) { throw new Error("Unable to find Matrix ID for phone number"); } userId = results[0].userid; } const roomId = await (0, _createRoom.ensureDMExists)(_MatrixClientPeg.MatrixClientPeg.get(), userId); _dispatcher.default.dispatch({ action: 'view_room', room_id: roomId }); })()); }, category: CommandCategories.actions }), new Command({ command: "msg", description: (0, _languageHandler._td)("Sends a message to the given user"), args: "<user-id> <message>", runFn: function (_, args) { if (args) { // matches the first whitespace delimited group and then the rest of the string const matches = args.match(/^(\S+?)(?: +(.*))?$/s); if (matches) { const [userId, msg] = matches.slice(1); if (msg && userId && userId.startsWith("@") && userId.includes(":")) { return success((async () => { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const roomId = await (0, _createRoom.ensureDMExists)(cli, userId); _dispatcher.default.dispatch({ action: 'view_room', room_id: roomId }); cli.sendTextMessage(roomId, msg); })()); } } } return reject(this.getUsage()); }, category: CommandCategories.actions }), new Command({ command: "holdcall", description: (0, _languageHandler._td)("Places the call in the current room on hold"), category: CommandCategories.other, runFn: function (roomId, args) { const call = _CallHandler.default.sharedInstance().getCallForRoom(roomId); if (!call) { return reject("No active call in this room"); } call.setRemoteOnHold(true); return success(); } }), new Command({ command: "unholdcall", description: (0, _languageHandler._td)("Takes the call in the current room off hold"), category: CommandCategories.other, runFn: function (roomId, args) { const call = _CallHandler.default.sharedInstance().getCallForRoom(roomId); if (!call) { return reject("No active call in this room"); } call.setRemoteOnHold(false); return success(); } }), new Command({ command: "converttodm", description: (0, _languageHandler._td)("Converts the room to a DM"), category: CommandCategories.other, runFn: function (roomId, args) { const room = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomId); return success((0, _Rooms.guessAndSetDMRoom)(room, true)); } }), new Command({ command: "converttoroom", description: (0, _languageHandler._td)("Converts the DM to a room"), category: CommandCategories.other, runFn: function (roomId, args) { const room = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomId); return success((0, _Rooms.guessAndSetDMRoom)(room, false)); } }), // Command definitions for autocompletion ONLY: // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes new Command({ command: "me", args: '<message>', description: (0, _languageHandler._td)('Displays action'), category: CommandCategories.messages, hideCompletionAfterSpace: true }), ..._effects.CHAT_EFFECTS.map(effect => { return new Command({ command: effect.command, description: effect.description(), args: '<message>', runFn: function (roomId, args) { return success((async () => { if (!args) { args = effect.fallbackMessage(); _MatrixClientPeg.MatrixClientPeg.get().sendEmoteMessage(roomId, args); } else { const content = { msgtype: effect.msgType, body: args }; _MatrixClientPeg.MatrixClientPeg.get().sendMessage(roomId, content); } _dispatcher.default.dispatch({ action: `effects.${effect.command}` }); })()); }, category: CommandCategories.effects }); })]; // build a map from names and aliases to the Command objects. exports.Commands = Commands; const CommandMap = new Map(); exports.CommandMap = CommandMap; Commands.forEach(cmd => { CommandMap.set(cmd.command, cmd); cmd.aliases.forEach(alias => { CommandMap.set(alias, cmd); }); }); function parseCommandString(input /*: string*/ ) { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); if (input[0] !== '/') return {}; // not a command const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); let cmd; let args; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[2]; } else { cmd = input; } return { cmd, args }; } /** * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. * @param {string} input The raw text input by the user. * @return {null|function(): Object} Function returning an object with the property 'error' if there was an error * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ function getCommand(input /*: string*/ ) { const { cmd, args } = parseCommandString(input); if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { return { cmd: CommandMap.get(cmd), args }; } return {}; } //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9TbGFzaENvbW1hbmRzLnRzeCJdLCJuYW1lcyI6WyJzaW5nbGVNeGNVcGxvYWQiLCJQcm9taXNlIiwicmVzb2x2ZSIsImZpbGVTZWxlY3RvciIsImRvY3VtZW50IiwiY3JlYXRlRWxlbWVudCIsInNldEF0dHJpYnV0ZSIsIm9uY2hhbmdlIiwiZXYiLCJmaWxlIiwidGFyZ2V0IiwiZmlsZXMiLCJVcGxvYWRDb25maXJtRGlhbG9nIiwic2RrIiwiZ2V0Q29tcG9uZW50IiwiTW9kYWwiLCJjcmVhdGVUcmFja2VkRGlhbG9nIiwib25GaW5pc2hlZCIsInNob3VsZENvbnRpbnVlIiwiTWF0cml4Q2xpZW50UGVnIiwiZ2V0IiwidXBsb2FkQ29udGVudCIsImNsaWNrIiwiQ29tbWFuZENhdGVnb3JpZXMiLCJDb21tYW5kIiwiY29uc3RydWN0b3IiLCJvcHRzIiwiY29tbWFuZCIsImFsaWFzZXMiLCJhcmdzIiwiZGVzY3JpcHRpb24iLCJydW5GbiIsImNhdGVnb3J5Iiwib3RoZXIiLCJoaWRlQ29tcGxldGlvbkFmdGVyU3BhY2UiLCJfaXNFbmFibGVkIiwiaXNFbmFibGVkIiwiZ2V0Q29tbWFuZCIsImdldENvbW1hbmRXaXRoQXJncyIsInJ1biIsInJvb21JZCIsInJlamVjdCIsImJpbmQiLCJnZXRVc2FnZSIsImVycm9yIiwic3VjY2VzcyIsInByb21pc2UiLCJDb21tYW5kcyIsIm1lc3NhZ2UiLCJDb250ZW50SGVscGVycyIsIm1ha2VIdG1sTWVzc2FnZSIsIm1lc3NhZ2VzIiwibWFrZVRleHRNZXNzYWdlIiwiRXJyb3JEaWFsb2ciLCJ0aXRsZSIsImFjdGlvbnMiLCJjbGkiLCJyb29tIiwiZ2V0Um9vbSIsImN1cnJlbnRTdGF0ZSIsIm1heUNsaWVudFNlbmRTdGF0ZUV2ZW50IiwiUm9vbVVwZ3JhZGVXYXJuaW5nRGlhbG9nIiwiZmluaXNoZWQiLCJ0YXJnZXRWZXJzaW9uIiwidGhlbiIsInJlc3AiLCJjb250aW51ZSIsImNoZWNrRm9yVXBncmFkZUZuIiwidXBncmFkZVByb21pc2UiLCJ1cGdyYWRlUm9vbSIsImludml0ZSIsIm5ld1Jvb20iLCJyZXBsYWNlbWVudF9yb29tIiwibmV3Um9vbUlkIiwidG9JbnZpdGUiLCJnZXRNZW1iZXJzV2l0aE1lbWJlcnNoaXAiLCJtYXAiLCJtIiwidXNlcklkIiwiZmlsdGVyIiwiZ2V0VXNlcklkIiwibGVuZ3RoIiwicmVtb3ZlTGlzdGVuZXIiLCJvbiIsImUiLCJjb25zb2xlIiwiYWRtaW4iLCJzZXREaXNwbGF5TmFtZSIsImdldFN0YXRlRXZlbnRzIiwiY29udGVudCIsImdldENvbnRlbnQiLCJtZW1iZXJzaGlwIiwiZGlzcGxheW5hbWUiLCJzZW5kU3RhdGVFdmVudCIsInVybCIsImF2YXRhcl91cmwiLCJzZXRBdmF0YXJVcmwiLCJzZXRSb29tVG9waWMiLCJ0b3BpY0V2ZW50cyIsInRvcGljIiwidG9waWNIdG1sIiwiSW5mb0RpYWxvZyIsIm5hbWUiLCJfX2h0bWwiLCJoYXNDbG9zZUJ1dHRvbiIsInNldFJvb21OYW1lIiwiYWRkcmVzcyIsInJlYXNvbiIsInNwbGl0IiwicHJvbSIsImdldElkZW50aXR5U2VydmVyVXJsIiwiZGVmYXVsdElkZW50aXR5U2VydmVyVXJsIiwiUXVlc3Rpb25EaWFsb2ciLCJkZWZhdWx0SWRlbnRpdHlTZXJ2ZXJOYW1lIiwiYnV0dG9uIiwidXNlRGVmYXVsdCIsIkVycm9yIiwiaW52aXRlciIsIk11bHRpSW52aXRlciIsImdldENvbXBsZXRpb25TdGF0ZSIsImdldEVycm9yVGV4dCIsIl8iLCJwYXJhbXMiLCJpc1Blcm1hbGluayIsInN0YXJ0c1dpdGgiLCJwYXJzZWRVcmwiLCJVUkwiLCJob3N0bmFtZSIsImhvc3QiLCJyb29tQWxpYXMiLCJpbmNsdWRlcyIsImdldERvbWFpbiIsImRpcyIsImRpc3BhdGNoIiwiYWN0aW9uIiwicm9vbV9hbGlhcyIsImF1dG9fam9pbiIsIl90eXBlIiwidmlhU2VydmVycyIsInJvb21faWQiLCJ2aWFfc2VydmVycyIsInBlcm1hbGlua1BhcnRzIiwicm9vbUlkT3JBbGlhcyIsImVudGl0eSIsImV2ZW50SWQiLCJ0YXJnZXRSb29tSWQiLCJtYXRjaGVzIiwibWF0Y2giLCJyb29tcyIsImdldFJvb21zIiwiaSIsImFsaWFzRXZlbnRzIiwiaiIsImsiLCJraWNrIiwiYmFuIiwidW5iYW4iLCJpZ25vcmVkVXNlcnMiLCJnZXRJZ25vcmVkVXNlcnMiLCJwdXNoIiwic2V0SWdub3JlZFVzZXJzIiwiaW5kZXgiLCJpbmRleE9mIiwic3BsaWNlIiwicG93ZXJMZXZlbCIsInVuZGVmaW5lZCIsInBhcnNlSW50IiwiaXNOYU4iLCJtZW1iZXIiLCJnZXRNZW1iZXIiLCJFZmZlY3RpdmVNZW1iZXJzaGlwIiwiTGVhdmUiLCJwb3dlckxldmVsRXZlbnQiLCJzZXRQb3dlckxldmVsIiwidXNlcnMiLCJEZXZ0b29sc0RpYWxvZyIsImNyZWF0ZURpYWxvZyIsImFkdmFuY2VkIiwiU2V0dGluZ3NTdG9yZSIsImdldFZhbHVlIiwiVUlGZWF0dXJlIiwiV2lkZ2V0cyIsIndpZGdldFVybCIsInRvTG93ZXJDYXNlIiwiZW1iZWQiLCJjaGlsZE5vZGVzIiwiaWZyYW1lIiwidGFnTmFtZSIsImF0dHJzIiwic3JjQXR0ciIsImZpbmQiLCJhIiwibG9nIiwidmFsdWUiLCJXaWRnZXRVdGlscyIsImNhblVzZXJNb2RpZnlXaWRnZXRzIiwibm93TXMiLCJEYXRlIiwiZ2V0VGltZSIsIndpZGdldElkIiwiZW5jb2RlVVJJQ29tcG9uZW50IiwidHlwZSIsIldpZGdldFR5cGUiLCJDVVNUT00iLCJkYXRhIiwiaml0c2lEYXRhIiwiSml0c2kiLCJnZXRJbnN0YW5jZSIsInBhcnNlUHJlZmVycmVkQ29uZmVyZW5jZVVybCIsIkpJVFNJIiwiZ2V0TG9jYWxKaXRzaVdyYXBwZXJVcmwiLCJzZXRSb29tV2lkZ2V0IiwiZGV2aWNlSWQiLCJmaW5nZXJwcmludCIsImRldmljZSIsImdldFN0b3JlZERldmljZSIsImRldmljZVRydXN0IiwiY2hlY2tEZXZpY2VUcnVzdCIsImlzVmVyaWZpZWQiLCJnZXRGaW5nZXJwcmludCIsImZwcmludCIsInNldERldmljZVZlcmlmaWVkIiwiZm9yY2VEaXNjYXJkU2Vzc2lvbiIsIm1ha2VIdG1sRW1vdGUiLCJTbGFzaENvbW1hbmRIZWxwRGlhbG9nIiwiQWN0aW9uIiwiVmlld1VzZXIiLCJTZGtDb25maWciLCJidWdfcmVwb3J0X2VuZHBvaW50X3VybCIsIkJ1Z1JlcG9ydERpYWxvZyIsImluaXRpYWxUZXh0IiwiaXNQaG9uZU51bWJlciIsInRlc3QiLCJyZXN1bHRzIiwiQ2FsbEhhbmRsZXIiLCJzaGFyZWRJbnN0YW5jZSIsInBzdG5Mb29rdXAiLCJzdGF0ZSIsInVzZXJpZCIsIm1zZyIsInNsaWNlIiwic2VuZFRleHRNZXNzYWdlIiwiY2FsbCIsImdldENhbGxGb3JSb29tIiwic2V0UmVtb3RlT25Ib2xkIiwiQ0hBVF9FRkZFQ1RTIiwiZWZmZWN0IiwiZmFsbGJhY2tNZXNzYWdlIiwic2VuZEVtb3RlTWVzc2FnZSIsIm1zZ3R5cGUiLCJtc2dUeXBlIiwiYm9keSIsInNlbmRNZXNzYWdlIiwiZWZmZWN0cyIsIkNvbW1hbmRNYXAiLCJNYXAiLCJmb3JFYWNoIiwiY21kIiwic2V0IiwiYWxpYXMiLCJwYXJzZUNvbW1hbmRTdHJpbmciLCJpbnB1dCIsInJlcGxhY2UiLCJiaXRzIiwic3Vic3RyaW5nIiwiaGFzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7QUFvQkE7O0FBRUE7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBRUE7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7Ozs7OztBQU9BLE1BQU1BLGVBQWUsR0FBRztBQUFBO0FBQTBCO0FBQzlDLFNBQU8sSUFBSUMsT0FBSixDQUFhQyxPQUFELElBQWE7QUFDNUIsVUFBTUMsWUFBWSxHQUFHQyxRQUFRLENBQUNDLGFBQVQsQ0FBdUIsT0FBdkIsQ0FBckI7QUFDQUYsSUFBQUEsWUFBWSxDQUFDRyxZQUFiLENBQTBCLE1BQTFCLEVBQWtDLE1BQWxDOztBQUNBSCxJQUFBQSxZQUFZLENBQUNJLFFBQWIsR0FBd0IsQ0FBQ0M7QUFBRDtBQUFBLFNBQXdCO0FBQzVDLFlBQU1DLElBQUksR0FBR0QsRUFBRSxDQUFDRSxNQUFILENBQVVDLEtBQVYsQ0FBZ0IsQ0FBaEIsQ0FBYjtBQUVBLFlBQU1DLG1CQUFtQixHQUFHQyxHQUFHLENBQUNDLFlBQUosQ0FBaUIsNkJBQWpCLENBQTVCOztBQUNBQyxxQkFBTUMsbUJBQU4sQ0FBMEIsMkJBQTFCLEVBQXVELEVBQXZELEVBQTJESixtQkFBM0QsRUFBZ0Y7QUFDNUVILFFBQUFBLElBRDRFO0FBRTVFUSxRQUFBQSxVQUFVLEVBQUdDLGNBQUQsSUFBb0I7QUFDNUJoQixVQUFBQSxPQUFPLENBQ