2020-09-29 14:14:51 -06:00
/*
2021-08-25 18:16:40 -06:00
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
2020-09-29 14:14:51 -06:00
*
* 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.
*/
2020-11-17 20:38:59 -07:00
import {
Capability ,
2020-11-25 14:40:01 -07:00
EventDirection ,
2020-11-23 14:10:14 -07:00
IOpenIDCredentials ,
IOpenIDUpdate ,
2020-11-17 20:38:59 -07:00
ISendEventDetails ,
2020-11-19 11:24:17 -07:00
MatrixCapabilities ,
2020-11-23 14:10:14 -07:00
OpenIDRequestState ,
SimpleObservable ,
2021-08-25 18:16:40 -06:00
Symbols ,
2020-11-19 11:24:17 -07:00
Widget ,
2020-11-17 20:38:59 -07:00
WidgetDriver ,
2020-11-25 14:40:01 -07:00
WidgetEventCapability ,
2020-11-19 11:24:17 -07:00
WidgetKind ,
2020-11-17 20:38:59 -07:00
} from "matrix-widget-api" ;
2022-03-11 09:04:22 +00:00
import { EventType } from "matrix-js-sdk/src/@types/event" ;
2021-12-09 09:10:23 +00:00
import { IContent , IEvent , MatrixEvent } from "matrix-js-sdk/src/models/event" ;
import { Room } from "matrix-js-sdk/src/models/room" ;
import { logger } from "matrix-js-sdk/src/logger" ;
2022-03-11 09:04:22 +00:00
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread" ;
2021-12-09 09:10:23 +00:00
2022-01-06 20:31:30 +01:00
import { iterableDiff , iterableIntersection } from "../../utils/iterables" ;
2020-11-02 15:17:05 -07:00
import { MatrixClientPeg } from "../../MatrixClientPeg" ;
2020-11-17 20:38:59 -07:00
import Modal from "../../Modal" ;
2020-11-23 14:10:14 -07:00
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog" ;
2021-08-25 22:30:57 -06:00
import WidgetCapabilitiesPromptDialog from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog" ;
2020-11-23 11:59:38 -07:00
import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions" ;
2020-11-25 18:39:11 -07:00
import { OIDCState , WidgetPermissionStore } from "./WidgetPermissionStore" ;
2020-11-25 14:40:01 -07:00
import { WidgetType } from "../../widgets/WidgetType" ;
2020-12-10 21:00:37 -07:00
import { CHAT_EFFECTS } from "../../effects" ;
import { containsEmoji } from "../../effects/utils" ;
import dis from "../../dispatcher/dispatcher" ;
2021-11-18 12:47:11 +00:00
import SettingsStore from "../../settings/SettingsStore" ;
2022-03-08 15:29:41 -07:00
import { RoomViewStore } from "../RoomViewStore" ;
2022-04-14 23:25:53 +02:00
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities" ;
2022-07-05 20:26:44 +02:00
import { navigateToPermalink } from "../../utils/permalinks/navigator" ;
2021-10-15 16:30:53 +02:00
2020-09-29 14:14:51 -06:00
// TODO: Purge this from the universe
2021-08-25 22:30:57 -06:00
function getRememberedCapabilitiesForWidget ( widget : Widget ) : Capability [] {
return JSON . parse ( localStorage . getItem ( `widget_ ${ widget . id } _approved_caps` ) || "[]" );
}
function setRememberedCapabilitiesForWidget ( widget : Widget , caps : Capability []) {
localStorage . setItem ( `widget_ ${ widget . id } _approved_caps` , JSON . stringify ( caps ));
}
2020-09-29 14:14:51 -06:00
export class StopGapWidgetDriver extends WidgetDriver {
2020-11-17 20:38:59 -07:00
private allowedCapabilities : Set < Capability >;
2020-11-19 11:24:17 -07:00
// TODO: Refactor widgetKind into the Widget class
2020-11-25 18:39:11 -07:00
constructor (
allowedCapabilities : Capability [],
private forWidget : Widget ,
private forWidgetKind : WidgetKind ,
private inRoomId? : string ,
) {
2020-09-29 14:14:51 -06:00
super ();
2020-11-17 20:38:59 -07:00
// Always allow screenshots to be taken because it's a client-induced flow. The widget can't
// spew screenshots at us and can't request screenshots of us, so it's up to us to provide the
// button if the widget says it supports screenshots.
2021-10-28 14:17:04 +02:00
this . allowedCapabilities = new Set ([... allowedCapabilities ,
MatrixCapabilities . Screenshots ,
2022-04-14 23:25:53 +02:00
ElementWidgetCapabilities . RequiresClient ]);
2020-11-25 14:40:01 -07:00
// Grant the permissions that are specific to given widget types
if ( WidgetType . JITSI . matches ( this . forWidget . type ) && forWidgetKind === WidgetKind . Room ) {
this . allowedCapabilities . add ( MatrixCapabilities . AlwaysOnScreen );
} else if ( WidgetType . STICKERPICKER . matches ( this . forWidget . type ) && forWidgetKind === WidgetKind . Account ) {
const stickerSendingCap = WidgetEventCapability . forRoomEvent ( EventDirection . Send , EventType . Sticker ). raw ;
this . allowedCapabilities . add ( MatrixCapabilities . StickerSending ); // legacy as far as MSC2762 is concerned
this . allowedCapabilities . add ( stickerSendingCap );
2021-01-27 14:22:55 -07:00
// Auto-approve the legacy visibility capability. We send it regardless of capability.
// Widgets don't technically need to request this capability, but Scalar still does.
this . allowedCapabilities . add ( "visibility" );
2020-11-25 14:40:01 -07:00
}
2020-09-29 14:14:51 -06:00
}
public async validateCapabilities ( requested : Set < Capability >) : Promise < Set < Capability >> {
2020-11-17 20:38:59 -07:00
// Check to see if any capabilities aren't automatically accepted (such as sticker pickers
// allowing stickers to be sent). If there are excess capabilities to be approved, the user
// will be prompted to accept them.
const diff = iterableDiff ( requested , this . allowedCapabilities );
const missing = new Set ( diff . removed ); // "removed" is "in A (requested) but not in B (allowed)"
const allowedSoFar = new Set ( this . allowedCapabilities );
2020-11-23 11:59:38 -07:00
getRememberedCapabilitiesForWidget ( this . forWidget ). forEach ( cap => {
allowedSoFar . add ( cap );
missing . delete ( cap );
});
if ( WidgetPermissionCustomisations . preapproveCapabilities ) {
const approved = await WidgetPermissionCustomisations . preapproveCapabilities ( this . forWidget , requested );
if ( approved ) {
approved . forEach ( cap => {
allowedSoFar . add ( cap );
missing . delete ( cap );
});
}
}
2020-11-17 20:38:59 -07:00
// TODO: Do something when the widget requests new capabilities not yet asked for
2021-08-25 22:30:57 -06:00
let rememberApproved = false ;
2020-11-17 20:38:59 -07:00
if ( missing . size > 0 ) {
try {
2022-06-14 17:51:51 +01:00
const [ result ] = await Modal . createDialog (
2020-11-17 20:38:59 -07:00
WidgetCapabilitiesPromptDialog ,
{
requestedCapabilities : missing ,
widget : this.forWidget ,
2020-11-19 11:24:17 -07:00
widgetKind : this.forWidgetKind ,
2020-11-17 20:38:59 -07:00
}). finished ;
( result . approved || []). forEach ( cap => allowedSoFar . add ( cap ));
2021-08-25 22:30:57 -06:00
rememberApproved = result . remember ;
2020-11-17 20:38:59 -07:00
} catch ( e ) {
2021-10-15 16:30:53 +02:00
logger . error ( "Non-fatal error getting capabilities: " , e );
2020-11-02 21:32:49 -07:00
}
2020-11-02 15:17:05 -07:00
}
2020-11-17 20:38:59 -07:00
2022-01-06 20:31:30 +01:00
// discard all previously allowed capabilities if they are not requested
// TODO: this results in an unexpected behavior when this function is called during the capabilities renegotiation of MSC2974 that will be resolved later.
const allAllowed = new Set ( iterableIntersection ( allowedSoFar , requested ));
2021-08-25 22:30:57 -06:00
if ( rememberApproved ) {
setRememberedCapabilitiesForWidget ( this . forWidget , Array . from ( allAllowed ));
}
return allAllowed ;
2020-09-29 14:14:51 -06:00
}
2020-11-02 21:32:49 -07:00
2021-08-25 18:25:20 -06:00
public async sendEvent (
eventType : string ,
2021-11-18 12:47:11 +00:00
content : IContent ,
2021-08-25 18:25:20 -06:00
stateKey : string = null ,
targetRoomId : string = null ,
) : Promise < ISendEventDetails > {
2020-11-02 21:32:49 -07:00
const client = MatrixClientPeg . get ();
2022-03-08 15:29:41 -07:00
const roomId = targetRoomId || RoomViewStore . instance . getRoomId ();
2020-11-02 21:32:49 -07:00
if ( ! client || ! roomId ) throw new Error ( "Not in a room or not attached to a client" );
2020-11-23 14:10:14 -07:00
let r : { event_id : string } = null ; // eslint-disable-line camelcase
2020-11-02 21:32:49 -07:00
if ( stateKey !== null ) {
// state event
r = await client . sendStateEvent ( roomId , eventType , content , stateKey );
2021-08-25 22:40:51 -06:00
} else if ( eventType === EventType . RoomRedaction ) {
// special case: extract the `redacts` property and call redact
r = await client . redactEvent ( roomId , content [ 'redacts' ]);
2020-11-02 21:32:49 -07:00
} else {
// message event
r = await client . sendEvent ( roomId , eventType , content );
2020-12-10 21:00:37 -07:00
if ( eventType === EventType . RoomMessage ) {
CHAT_EFFECTS . forEach (( effect ) => {
if ( containsEmoji ( content , effect . emojis )) {
2021-11-18 12:47:11 +00:00
// For initial threads launch, chat effects are disabled
// see #19731
2022-03-11 09:04:22 +00:00
const isNotThread = content [ "m.relates_to" ]. rel_type !== THREAD_RELATION_TYPE . name ;
2021-11-23 08:17:30 +00:00
if ( ! SettingsStore . getValue ( "feature_thread" ) || isNotThread ) {
2021-11-18 12:47:11 +00:00
dis . dispatch ({ action : `effects. ${ effect . command } ` });
}
2020-12-10 21:00:37 -07:00
}
});
}
2020-11-02 21:32:49 -07:00
}
2021-06-29 13:11:58 +01:00
return { roomId , eventId : r.event_id };
2020-11-02 21:32:49 -07:00
}
2020-11-23 14:10:14 -07:00
2021-08-25 18:16:40 -06:00
private pickRooms ( roomIds : ( string | Symbols . AnyRoom )[] = null ) : Room [] {
2021-05-03 21:50:25 -06:00
const client = MatrixClientPeg . get ();
2021-08-25 18:16:40 -06:00
if ( ! client ) throw new Error ( "Not attached to a client" );
2021-05-03 21:50:25 -06:00
2021-08-25 18:16:40 -06:00
const targetRooms = roomIds
? ( roomIds . includes ( Symbols . AnyRoom ) ? client . getVisibleRooms () : roomIds . map ( r => client . getRoom ( r )))
2022-03-08 15:29:41 -07:00
: [ client . getRoom ( RoomViewStore . instance . getRoomId ())];
2021-08-25 18:16:40 -06:00
return targetRooms . filter ( r => !! r );
2021-05-03 21:50:25 -06:00
}
2021-08-25 18:16:40 -06:00
public async readRoomEvents (
eventType : string ,
msgtype : string | undefined ,
limitPerRoom : number ,
roomIds : ( string | Symbols . AnyRoom )[] = null ,
) : Promise < object [] > {
2021-09-01 11:29:20 -06:00
limitPerRoom = limitPerRoom > 0 ? Math . min ( limitPerRoom , Number . MAX_SAFE_INTEGER ) : Number . MAX_SAFE_INTEGER ; // relatively arbitrary
2021-05-03 21:50:25 -06:00
2021-08-25 18:16:40 -06:00
const rooms = this . pickRooms ( roomIds );
const allResults : IEvent [] = [];
for ( const room of rooms ) {
const results : MatrixEvent [] = [];
const events = room . getLiveTimeline (). getEvents (); // timelines are most recent last
for ( let i = events . length - 1 ; i > 0 ; i -- ) {
if ( results . length >= limitPerRoom ) break ;
2021-05-03 21:50:25 -06:00
2021-08-25 18:16:40 -06:00
const ev = events [ i ];
if ( ev . getType () !== eventType || ev . isState ()) continue ;
if ( eventType === EventType . RoomMessage && msgtype && msgtype !== ev . getContent ()[ 'msgtype' ]) continue ;
results . push ( ev );
2021-05-03 21:50:25 -06:00
}
2021-08-25 18:16:40 -06:00
results . forEach ( e => allResults . push ( e . getEffectiveEvent ()));
}
return allResults ;
}
public async readStateEvents (
eventType : string ,
stateKey : string | undefined ,
limitPerRoom : number ,
roomIds : ( string | Symbols . AnyRoom )[] = null ,
) : Promise < object [] > {
2021-09-01 11:29:20 -06:00
limitPerRoom = limitPerRoom > 0 ? Math . min ( limitPerRoom , Number . MAX_SAFE_INTEGER ) : Number . MAX_SAFE_INTEGER ; // relatively arbitrary
2021-08-25 18:16:40 -06:00
const rooms = this . pickRooms ( roomIds );
const allResults : IEvent [] = [];
for ( const room of rooms ) {
const results : MatrixEvent [] = [];
const state : Map < string , MatrixEvent > = room . currentState . events . get ( eventType );
if ( state ) {
if ( stateKey === "" || !! stateKey ) {
const forKey = state . get ( stateKey );
if ( forKey ) results . push ( forKey );
} else {
results . push (... Array . from ( state . values ()));
}
}
results . slice ( 0 , limitPerRoom ). forEach ( e => allResults . push ( e . getEffectiveEvent ()));
}
return allResults ;
2021-05-03 21:50:25 -06:00
}
2020-11-23 14:10:14 -07:00
public async askOpenID ( observer : SimpleObservable < IOpenIDUpdate >) {
2020-11-25 18:39:11 -07:00
const oidcState = WidgetPermissionStore . instance . getOIDCState (
this . forWidget , this . forWidgetKind , this . inRoomId ,
);
2020-11-23 14:10:14 -07:00
const getToken = () : Promise < IOpenIDCredentials > => {
return MatrixClientPeg . get (). getOpenIdToken ();
};
2020-11-25 18:39:11 -07:00
if ( oidcState === OIDCState . Denied ) {
2021-06-29 13:11:58 +01:00
return observer . update ({ state : OpenIDRequestState.Blocked });
2020-11-23 14:10:14 -07:00
}
2020-11-25 18:39:11 -07:00
if ( oidcState === OIDCState . Allowed ) {
2021-06-29 13:11:58 +01:00
return observer . update ({ state : OpenIDRequestState.Allowed , token : await getToken () });
2020-11-23 14:10:14 -07:00
}
2021-06-29 13:11:58 +01:00
observer . update ({ state : OpenIDRequestState.PendingUserConfirmation });
2020-11-23 14:10:14 -07:00
2022-06-14 17:51:51 +01:00
Modal . createDialog ( WidgetOpenIDPermissionsDialog , {
2020-11-25 18:39:11 -07:00
widget : this.forWidget ,
widgetKind : this.forWidgetKind ,
inRoomId : this.inRoomId ,
2020-11-23 14:10:14 -07:00
onFinished : async ( confirm ) => {
if ( ! confirm ) {
2021-06-29 13:11:58 +01:00
return observer . update ({ state : OpenIDRequestState.Blocked });
2020-11-23 14:10:14 -07:00
}
2021-06-29 13:11:58 +01:00
return observer . update ({ state : OpenIDRequestState.Allowed , token : await getToken () });
2020-11-23 14:10:14 -07:00
},
});
}
2020-12-29 12:35:48 -07:00
public async navigate ( uri : string ) : Promise < void > {
2022-07-05 20:26:44 +02:00
navigateToPermalink ( uri );
2020-12-29 12:35:48 -07:00
}
2020-09-29 14:14:51 -06:00
}