2020-09-29 14:14:51 -06:00
/*
2022-08-10 09:26:42 -04:00
* Copyright 2020 - 2022 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 ,
2022-08-10 09:26:42 -04:00
ITurnServer ,
2022-09-14 16:18:51 +02:00
IReadEventRelationsResult ,
2022-08-10 08:57:56 -04:00
IRoomEvent ,
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-08-10 09:26:42 -04:00
import { ClientEvent , ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client" ;
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" ;
2022-09-14 16:18:51 +02:00
import { Direction } from "matrix-js-sdk/src/matrix" ;
2021-12-09 09:10:23 +00:00
2022-10-07 21:55:48 +02:00
import SdkConfig , { DEFAULTS } from "../../SdkConfig" ;
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-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" ;
2022-10-19 13:07:03 +01:00
import { SdkContextClass } from "../../contexts/SDKContext" ;
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 ));
}
2022-08-10 09:26:42 -04:00
const normalizeTurnServer = ({ urls , username , credential } : IClientTurnServer ) : ITurnServer => ({
uris : urls ,
username ,
password : credential ,
});
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 ,
2022-09-16 11:12:27 -04:00
virtual : boolean ,
2020-11-25 18:39:11 -07:00
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" );
2022-10-07 21:55:48 +02:00
} else if (
virtual
&& new URL ( SdkConfig . get ( "element_call" ). url ?? DEFAULTS . element_call . url ). origin === this . forWidget . origin
) {
2022-09-16 11:12:27 -04:00
// This is a trusted Element Call widget that we control
this . allowedCapabilities . add ( MatrixCapabilities . AlwaysOnScreen );
this . allowedCapabilities . add ( MatrixCapabilities . MSC3846TurnServers );
this . allowedCapabilities . add ( `org.matrix.msc2762.timeline: ${ inRoomId } ` );
this . allowedCapabilities . add (
WidgetEventCapability . forStateEvent ( EventDirection . Receive , EventType . RoomMember ). raw ,
);
this . allowedCapabilities . add (
WidgetEventCapability . forStateEvent ( EventDirection . Send , "org.matrix.msc3401.call" ). raw ,
);
this . allowedCapabilities . add (
WidgetEventCapability . forStateEvent ( EventDirection . Receive , "org.matrix.msc3401.call" ). raw ,
);
this . allowedCapabilities . add (
WidgetEventCapability . forStateEvent (
EventDirection . Send , "org.matrix.msc3401.call.member" , MatrixClientPeg . get (). getUserId () ! ,
). raw ,
);
this . allowedCapabilities . add (
WidgetEventCapability . forStateEvent ( EventDirection . Receive , "org.matrix.msc3401.call.member" ). raw ,
);
const sendRecvToDevice = [
EventType . CallInvite ,
EventType . CallCandidates ,
EventType . CallAnswer ,
EventType . CallHangup ,
EventType . CallReject ,
EventType . CallSelectAnswer ,
EventType . CallNegotiate ,
EventType . CallSDPStreamMetadataChanged ,
EventType . CallSDPStreamMetadataChangedPrefix ,
EventType . CallReplaces ,
];
for ( const eventType of sendRecvToDevice ) {
this . allowedCapabilities . add (
WidgetEventCapability . forToDeviceEvent ( EventDirection . Send , eventType ). raw ,
);
this . allowedCapabilities . add (
WidgetEventCapability . forToDeviceEvent ( EventDirection . Receive , eventType ). raw ,
);
}
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-10-19 13:07:03 +01:00
const roomId = targetRoomId || SdkContextClass . instance . roomViewStore . 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
2022-08-10 08:57:56 -04:00
public async sendToDevice (
eventType : string ,
encrypted : boolean ,
contentMap : { [ userId : string ] : { [ deviceId : string ] : object } },
) : Promise < void > {
const client = MatrixClientPeg . get ();
if ( encrypted ) {
const deviceInfoMap = await client . crypto . deviceList . downloadKeys ( Object . keys ( contentMap ), false );
await Promise . all (
Object . entries ( contentMap ). flatMap (([ userId , userContentMap ]) =>
Object . entries ( userContentMap ). map ( async ([ deviceId , content ]) => {
if ( deviceId === "*" ) {
// Send the message to all devices we have keys for
await client . encryptAndSendToDevices (
Object . values ( deviceInfoMap [ userId ]). map ( deviceInfo => ({
userId , deviceInfo ,
})),
content ,
);
} else {
// Send the message to a specific device
await client . encryptAndSendToDevices (
[{ userId , deviceInfo : deviceInfoMap [ userId ][ deviceId ] }],
content ,
);
}
}),
),
);
} else {
await client . queueToDevice ({
eventType ,
batch : Object.entries ( contentMap ). flatMap (([ userId , userContentMap ]) =>
Object . entries ( userContentMap ). map (([ deviceId , content ]) =>
({ userId , deviceId , payload : content }),
),
),
});
}
}
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-10-19 13:07:03 +01:00
: [ client . getRoom ( SdkContextClass . instance . roomViewStore . 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 ,
2022-08-10 08:57:56 -04:00
) : Promise < IRoomEvent [] > {
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 ,
2022-08-10 08:57:56 -04:00
) : Promise < IRoomEvent [] > {
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
}
2022-08-10 09:26:42 -04:00
public async * getTurnServers () : AsyncGenerator < ITurnServer > {
const client = MatrixClientPeg . get ();
if ( ! client . pollingTurnServers || ! client . getTurnServers (). length ) return ;
let setTurnServer : ( server : ITurnServer ) => void ;
let setError : ( error : Error ) => void ;
const onTurnServers = ([ server ] : IClientTurnServer []) => setTurnServer ( normalizeTurnServer ( server ));
const onTurnServersError = ( error : Error , fatal : boolean ) => { if ( fatal ) setError ( error ); };
client . on ( ClientEvent . TurnServers , onTurnServers );
client . on ( ClientEvent . TurnServersError , onTurnServersError );
try {
const initialTurnServer = client . getTurnServers ()[ 0 ];
yield normalizeTurnServer ( initialTurnServer );
// Repeatedly listen for new TURN servers until an error occurs or
// the caller stops this generator
while ( true ) {
yield await new Promise < ITurnServer >(( resolve , reject ) => {
setTurnServer = resolve ;
setError = reject ;
});
}
} finally {
// The loop was broken - clean up
client . off ( ClientEvent . TurnServers , onTurnServers );
client . off ( ClientEvent . TurnServersError , onTurnServersError );
}
}
2022-09-14 16:18:51 +02:00
public async readEventRelations (
eventId : string ,
roomId? : string ,
relationType? : string ,
eventType? : string ,
from ?: string ,
to? : string ,
limit? : number ,
direction ?: 'f' | 'b' ,
) : Promise < IReadEventRelationsResult > {
const client = MatrixClientPeg . get ();
const dir = direction as Direction ;
2022-10-19 13:07:03 +01:00
roomId = roomId ?? SdkContextClass . instance . roomViewStore . getRoomId () ?? undefined ;
2022-09-14 16:18:51 +02:00
if ( typeof roomId !== "string" ) {
throw new Error ( 'Error while reading the current room' );
}
const {
events ,
nextBatch ,
prevBatch ,
} = await client . relations (
roomId ,
eventId ,
relationType ?? null ,
eventType ?? null ,
{
from ,
to ,
limit ,
2022-10-12 16:56:52 +02:00
dir ,
2022-09-14 16:18:51 +02:00
});
return {
chunk : events.map ( e => e . getEffectiveEvent ()),
nextBatch ,
prevBatch ,
};
}
2020-09-29 14:14:51 -06:00
}