@@ -20,13 +20,10 @@ limitations under the License.
2020import * as React from "react" ;
2121import { User } from "matrix-js-sdk/src/models/user" ;
2222import { Direction } from "matrix-js-sdk/src/models/event-timeline" ;
23- import { EventType } from "matrix-js-sdk/src/@types/event" ;
2423import * as ContentHelpers from "matrix-js-sdk/src/content-helpers" ;
2524import { logger } from "matrix-js-sdk/src/logger" ;
2625import { IContent } from "matrix-js-sdk/src/models/event" ;
2726import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic" ;
28- import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand" ;
29- import { MatrixClient } from "matrix-js-sdk/src/matrix" ;
3027
3128import dis from "./dispatcher/dispatcher" ;
3229import { _t , _td , UserFriendlyError } from "./languageHandler" ;
@@ -46,192 +43,31 @@ import BugReportDialog from "./components/views/dialogs/BugReportDialog";
4643import { ensureDMExists } from "./createRoom" ;
4744import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload" ;
4845import { Action } from "./dispatcher/actions" ;
49- import { EffectiveMembership , getEffectiveMembership } from "./utils/membership" ;
5046import SdkConfig from "./SdkConfig" ;
5147import SettingsStore from "./settings/SettingsStore" ;
5248import { UIComponent , UIFeature } from "./settings/UIFeature" ;
5349import { CHAT_EFFECTS } from "./effects" ;
5450import LegacyCallHandler from "./LegacyCallHandler" ;
5551import { guessAndSetDMRoom } from "./Rooms" ;
5652import { upgradeRoom } from "./utils/RoomUpgrade" ;
57- import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog" ;
5853import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog" ;
5954import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog" ;
6055import InfoDialog from "./components/views/dialogs/InfoDialog" ;
6156import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog" ;
6257import { shouldShowComponent } from "./customisations/helpers/UIComponents" ;
6358import { TimelineRenderingType } from "./contexts/RoomContext" ;
64- import { XOR } from "./@types/common" ;
65- import { PosthogAnalytics } from "./PosthogAnalytics" ;
6659import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload" ;
6760import VoipUserMapper from "./VoipUserMapper" ;
6861import { htmlSerializeFromMdIfNeeded } from "./editor/serialize" ;
6962import { leaveRoomBehaviour } from "./utils/leave-behaviour" ;
70- import { isLocalRoom } from "./utils/localRoom/isLocalRoom" ;
71- import { SdkContextClass } from "./contexts/SDKContext" ;
7263import { MatrixClientPeg } from "./MatrixClientPeg" ;
7364import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo" ;
65+ import { isCurrentLocalRoom , reject , singleMxcUpload , success , successSync } from "./slash-commands/utils" ;
66+ import { deop , op } from "./slash-commands/op" ;
67+ import { CommandCategories } from "./slash-commands/interface" ;
68+ import { Command } from "./slash-commands/command" ;
7469
75- // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
76- interface HTMLInputEvent extends Event {
77- target : HTMLInputElement & EventTarget ;
78- }
79-
80- const singleMxcUpload = async ( cli : MatrixClient ) : Promise < string | null > => {
81- return new Promise ( ( resolve ) => {
82- const fileSelector = document . createElement ( "input" ) ;
83- fileSelector . setAttribute ( "type" , "file" ) ;
84- fileSelector . onchange = ( ev : Event ) => {
85- const file = ( ev as HTMLInputEvent ) . target . files ?. [ 0 ] ;
86- if ( ! file ) return ;
87-
88- Modal . createDialog ( UploadConfirmDialog , {
89- file,
90- onFinished : async ( shouldContinue ) : Promise < void > => {
91- if ( shouldContinue ) {
92- const { content_uri : uri } = await cli . uploadContent ( file ) ;
93- resolve ( uri ) ;
94- } else {
95- resolve ( null ) ;
96- }
97- } ,
98- } ) ;
99- } ;
100-
101- fileSelector . click ( ) ;
102- } ) ;
103- } ;
104-
105- export const CommandCategories = {
106- messages : _td ( "Messages" ) ,
107- actions : _td ( "Actions" ) ,
108- admin : _td ( "Admin" ) ,
109- advanced : _td ( "Advanced" ) ,
110- effects : _td ( "Effects" ) ,
111- other : _td ( "Other" ) ,
112- } ;
113-
114- export type RunResult = XOR < { error : Error } , { promise : Promise < IContent | undefined > } > ;
115-
116- type RunFn = (
117- this : Command ,
118- matrixClient : MatrixClient ,
119- roomId : string ,
120- threadId : string | null ,
121- args ?: string ,
122- ) => RunResult ;
123-
124- interface ICommandOpts {
125- command : string ;
126- aliases ?: string [ ] ;
127- args ?: string ;
128- description : string ;
129- analyticsName ?: SlashCommandEvent [ "command" ] ;
130- runFn ?: RunFn ;
131- category : string ;
132- hideCompletionAfterSpace ?: boolean ;
133- isEnabled ?( matrixClient : MatrixClient | null ) : boolean ;
134- renderingTypes ?: TimelineRenderingType [ ] ;
135- }
136-
137- export class Command {
138- public readonly command : string ;
139- public readonly aliases : string [ ] ;
140- public readonly args ?: string ;
141- public readonly description : string ;
142- public readonly runFn ?: RunFn ;
143- public readonly category : string ;
144- public readonly hideCompletionAfterSpace : boolean ;
145- public readonly renderingTypes ?: TimelineRenderingType [ ] ;
146- public readonly analyticsName ?: SlashCommandEvent [ "command" ] ;
147- private readonly _isEnabled ?: ( matrixClient : MatrixClient | null ) => boolean ;
148-
149- public constructor ( opts : ICommandOpts ) {
150- this . command = opts . command ;
151- this . aliases = opts . aliases || [ ] ;
152- this . args = opts . args || "" ;
153- this . description = opts . description ;
154- this . runFn = opts . runFn ?. bind ( this ) ;
155- this . category = opts . category || CommandCategories . other ;
156- this . hideCompletionAfterSpace = opts . hideCompletionAfterSpace || false ;
157- this . _isEnabled = opts . isEnabled ;
158- this . renderingTypes = opts . renderingTypes ;
159- this . analyticsName = opts . analyticsName ;
160- }
161-
162- public getCommand ( ) : string {
163- return `/${ this . command } ` ;
164- }
165-
166- public getCommandWithArgs ( ) : string {
167- return this . getCommand ( ) + " " + this . args ;
168- }
169-
170- public run ( matrixClient : MatrixClient , roomId : string , threadId : string | null , args ?: string ) : RunResult {
171- // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
172- if ( ! this . runFn ) {
173- return reject ( new UserFriendlyError ( "Command error: Unable to handle slash command." ) ) ;
174- }
175-
176- const renderingType = threadId ? TimelineRenderingType . Thread : TimelineRenderingType . Room ;
177- if ( this . renderingTypes && ! this . renderingTypes ?. includes ( renderingType ) ) {
178- return reject (
179- new UserFriendlyError ( "Command error: Unable to find rendering type (%(renderingType)s)" , {
180- renderingType,
181- cause : undefined ,
182- } ) ,
183- ) ;
184- }
185-
186- if ( this . analyticsName ) {
187- PosthogAnalytics . instance . trackEvent < SlashCommandEvent > ( {
188- eventName : "SlashCommand" ,
189- command : this . analyticsName ,
190- } ) ;
191- }
192-
193- return this . runFn ( matrixClient , roomId , threadId , args ) ;
194- }
195-
196- public getUsage ( ) : string {
197- return _t ( "Usage" ) + ": " + this . getCommandWithArgs ( ) ;
198- }
199-
200- public isEnabled ( cli : MatrixClient | null ) : boolean {
201- return this . _isEnabled ?.( cli ) ?? true ;
202- }
203- }
204-
205- function reject ( error ?: any ) : RunResult {
206- return { error } ;
207- }
208-
209- function success ( promise : Promise < any > = Promise . resolve ( ) ) : RunResult {
210- return { promise } ;
211- }
212-
213- function successSync ( value : any ) : RunResult {
214- return success ( Promise . resolve ( value ) ) ;
215- }
216-
217- const isCurrentLocalRoom = ( cli : MatrixClient | null ) : boolean => {
218- const roomId = SdkContextClass . instance . roomViewStore . getRoomId ( ) ;
219- if ( ! roomId ) return false ;
220- const room = cli ?. getRoom ( roomId ) ;
221- if ( ! room ) return false ;
222- return isLocalRoom ( room ) ;
223- } ;
224-
225- const canAffectPowerlevels = ( cli : MatrixClient | null ) : boolean => {
226- const roomId = SdkContextClass . instance . roomViewStore . getRoomId ( ) ;
227- if ( ! cli || ! roomId ) return false ;
228- const room = cli ?. getRoom ( roomId ) ;
229- return ! ! room ?. currentState . maySendStateEvent ( EventType . RoomPowerLevels , cli . getSafeUserId ( ) ) && ! isLocalRoom ( room ) ;
230- } ;
231-
232- /* Disable the "unexpected this" error for these commands - all of the run
233- * functions are called with `this` bound to the Command instance.
234- */
70+ export { CommandCategories , Command } ;
23571
23672export const Commands = [
23773 new Command ( {
@@ -886,78 +722,8 @@ export const Commands = [
886722 } ,
887723 category : CommandCategories . actions ,
888724 } ) ,
889- new Command ( {
890- command : "op" ,
891- args : "<user-id> [<power-level>]" ,
892- description : _td ( "Define the power level of a user" ) ,
893- isEnabled : canAffectPowerlevels ,
894- runFn : function ( cli , roomId , threadId , args ) {
895- if ( args ) {
896- const matches = args . match ( / ^ ( \S + ?) ( + ( - ? \d + ) ) ? $ / ) ;
897- let powerLevel = 50 ; // default power level for op
898- if ( matches ) {
899- const userId = matches [ 1 ] ;
900- if ( matches . length === 4 && undefined !== matches [ 3 ] ) {
901- powerLevel = parseInt ( matches [ 3 ] , 10 ) ;
902- }
903- if ( ! isNaN ( powerLevel ) ) {
904- const room = cli . getRoom ( roomId ) ;
905- if ( ! room ) {
906- return reject (
907- new UserFriendlyError ( "Command failed: Unable to find room (%(roomId)s" , {
908- roomId,
909- cause : undefined ,
910- } ) ,
911- ) ;
912- }
913- const member = room . getMember ( userId ) ;
914- if (
915- ! member ?. membership ||
916- getEffectiveMembership ( member . membership ) === EffectiveMembership . Leave
917- ) {
918- return reject ( new UserFriendlyError ( "Could not find user in room" ) ) ;
919- }
920- const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
921- return success ( cli . setPowerLevel ( roomId , userId , powerLevel , powerLevelEvent ) ) ;
922- }
923- }
924- }
925- return reject ( this . getUsage ( ) ) ;
926- } ,
927- category : CommandCategories . admin ,
928- renderingTypes : [ TimelineRenderingType . Room ] ,
929- } ) ,
930- new Command ( {
931- command : "deop" ,
932- args : "<user-id>" ,
933- description : _td ( "Deops user with given id" ) ,
934- isEnabled : canAffectPowerlevels ,
935- runFn : function ( cli , roomId , threadId , args ) {
936- if ( args ) {
937- const matches = args . match ( / ^ ( \S + ) $ / ) ;
938- if ( matches ) {
939- const room = cli . getRoom ( roomId ) ;
940- if ( ! room ) {
941- return reject (
942- new UserFriendlyError ( "Command failed: Unable to find room (%(roomId)s" , {
943- roomId,
944- cause : undefined ,
945- } ) ,
946- ) ;
947- }
948-
949- const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
950- if ( ! powerLevelEvent ?. getContent ( ) . users [ args ] ) {
951- return reject ( new UserFriendlyError ( "Could not find user in room" ) ) ;
952- }
953- return success ( cli . setPowerLevel ( roomId , args , undefined , powerLevelEvent ) ) ;
954- }
955- }
956- return reject ( this . getUsage ( ) ) ;
957- } ,
958- category : CommandCategories . admin ,
959- renderingTypes : [ TimelineRenderingType . Room ] ,
960- } ) ,
725+ op ,
726+ deop ,
961727 new Command ( {
962728 command : "devtools" ,
963729 description : _td ( "Opens the Developer Tools dialog" ) ,
0 commit comments