2021-12-13 22:21:59 +01:00
import olm from "@matrix-org/olm/olm" ;
global . Olm = olm ;
2022-09-29 20:52:05 +02:00
import * as sdk from "matrix-js-sdk" ;
2021-05-06 13:23:17 +02:00
import { TimelineWindow , EventTimeline } from "matrix-js-sdk" ;
2020-12-14 16:11:38 +01:00
import util from "../plugins/utils" ;
2021-01-27 12:57:23 +01:00
import User from "../models/user" ;
2020-11-09 10:26:56 +01:00
2023-01-31 09:39:30 +00:00
const LocalStorageCryptoStore =
require ( "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store" ) . LocalStorageCryptoStore ;
2020-11-09 10:26:56 +01:00
2020-11-09 15:08:36 +01:00
export default {
2021-12-13 22:21:59 +01:00
install ( Vue , options ) {
if ( ! options || ! options . store ) {
throw new Error ( "Please initialise plugin with a Vuex store." ) ;
}
2021-04-14 12:37:42 +02:00
2021-12-13 22:21:59 +01:00
// Set User-Agent headers.
// Update: browser do not allow this, "Refused to set unsafe header "User-Agent""
// Keep this code around however, since it's an example of how to add headers to a request...
// sdk.wrapRequest((orig, opts, callback) => {
// opts.headers = opts.headers || {}
// opts.headers['User-Agent'] = "Keanu";
// var ret = orig(opts, callback);
// return ret;
// });
const store = options . store ;
const i18n = options . i18n ;
const matrixService = new Vue ( {
store ,
i18n ,
data ( ) {
return {
matrixClient : null ,
matrixClientReady : false ,
rooms : [ ] ,
userDisplayName : null ,
userAvatar : null ,
currentRoom : null ,
2023-03-16 15:23:26 +01:00
currentRoomIsReadOnlyForUser : false ,
2023-02-08 11:22:12 +01:00
currentRoomBeingPurged : false ,
2021-12-13 22:21:59 +01:00
notificationCount : 0 ,
} ;
} ,
mounted ( ) {
console . log ( "Matrix service mounted" ) ;
} ,
computed : {
ready ( ) {
return this . matrixClient != null && this . matrixClientReady ;
} ,
currentUser ( ) {
return this . $store . state . auth . user ;
} ,
currentUserId ( ) {
const user = this . currentUser || { } ;
return user . user _id ;
} ,
currentUserDisplayName ( ) {
if ( this . ready ) {
const user = this . matrixClient . getUser ( this . currentUserId ) || { } ;
return this . userDisplayName || user . displayName ;
}
return null ;
} ,
currentUserHomeServer ( ) {
2023-01-31 09:39:30 +00:00
return this . $config . homeServer ? this . $config . homeServer : User . serverName ( this . currentUserId ) ;
2021-12-13 22:21:59 +01:00
} ,
currentRoomId ( ) {
return this . $store . state . currentRoomId ;
} ,
joinedRooms ( ) {
return this . rooms . filter ( ( room ) => {
return room . selfMembership === "join" ;
} ) ;
} ,
invites ( ) {
return this . rooms . filter ( ( room ) => {
return room . selfMembership === "invite" ;
} ) ;
} ,
} ,
watch : {
currentRoomId : {
immediate : true ,
handler ( roomId ) {
this . currentRoom = this . getRoom ( roomId ) ;
} ,
} ,
2023-05-11 11:07:13 +02:00
currentRoom : {
immediate : true ,
handler ( room ) {
if ( room ) {
this . currentRoomIsReadOnlyForUser = this . isReadOnlyRoomForUser ( room . roomId , this . currentUserId ) ;
} else {
this . currentRoomIsReadOnlyForUser = false ;
}
}
}
2021-12-13 22:21:59 +01:00
} ,
methods : {
createCryptoStore ( ) {
console . log ( "create crypto store" ) ;
return new LocalStorageCryptoStore ( this . $store . getters . storage ) ;
} ,
2023-04-04 14:30:50 +00:00
login ( user , registrationFlowHandler ) {
const tempMatrixClient = sdk . createClient ( { baseUrl : user . home _server , idBaseUrl : this . $config . identityServer } ) ;
2021-12-13 22:21:59 +01:00
var promiseLogin ;
const self = this ;
if ( user . access _token ) {
// Logged in on "real" account
promiseLogin = Promise . resolve ( user ) ;
2023-04-04 14:30:50 +00:00
} else if ( user . is _guest && ( ! user . user _id || user . registration _session ) ) {
2021-12-13 22:21:59 +01:00
// Generate random username and password. We don't user REAL matrix
// guest accounts because 1. They are not allowed to post media, 2. They
// can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
//
// Instead, we use an ILAG approach, Improved Landing as Guest.
2023-04-04 14:30:50 +00:00
const userId = user . registration _session ? user . user _id : util . randomUser ( this . $config . userIdPrefix ) ;
const pass = user . registration _session ? user . password : util . randomPass ( ) ;
const extractAndSaveUser = ( response ) => {
var u = Object . assign ( { } , response ) ;
u . home _server = tempMatrixClient . baseUrl ; // Don't use deprecated field from response.
u . password = pass ;
u . is _guest = true ;
this . $store . commit ( "setUser" , u ) ;
return u ;
} ;
2021-12-13 22:21:59 +01:00
promiseLogin = tempMatrixClient
2023-04-04 14:30:50 +00:00
. register ( userId , pass , user . registration _session || null , {
2021-12-13 22:21:59 +01:00
type : "m.login.dummy" ,
initial _device _display _name : this . $config . appName ,
} )
. then ( ( response ) => {
2023-04-04 14:30:50 +00:00
return extractAndSaveUser ( response ) ;
} )
. catch ( error => {
if ( registrationFlowHandler && error . httpStatus == 401 && error . data ) {
const registrationSession = error . data . session ;
// Store user, pass and session, so we can resume if network failure occurs etc.
//
var u = { } ;
u . user _id = userId ;
u . home _server = tempMatrixClient . baseUrl ; // Don't use deprecated field from response.
u . password = pass ;
u . is _guest = true ;
u . registration _session = registrationSession ;
this . $store . commit ( "setUser" , u ) ;
return registrationFlowHandler ( tempMatrixClient , error . data ) . then ( ( response ) => extractAndSaveUser ( response ) ) ;
} else {
console . error ( error ) ;
}
throw error ;
2021-12-13 22:21:59 +01:00
} ) ;
} else {
var data = {
user : User . localPart ( user . user _id ) ,
password : user . password ,
type : "m.login.password" ,
initial _device _display _name : this . $config . appName ,
} ;
if ( user . device _id ) {
data . device _id = user . device _id ;
}
2023-01-31 09:39:30 +00:00
promiseLogin = tempMatrixClient . login ( "m.login.password" , data ) . then ( ( response ) => {
var u = Object . assign ( { } , response ) ;
if ( user . is _guest ) {
// Copy over needed properties
u = Object . assign ( user , response ) ;
}
u . home _server = tempMatrixClient . baseUrl ; // Don't use deprecated field from response.
this . $store . commit ( "setUser" , u ) ;
return u ;
} ) ;
2021-12-13 22:21:59 +01:00
}
return promiseLogin . then ( ( user ) => {
return self . getMatrixClient ( user ) ;
} ) ;
} ,
clearCryptoStore ( ) {
// Clear crypto related data
// TODO - for some reason "clearStores" called in "logout" only clears the "account" crypto
// data item, not all sessions etc. Why? We need to do that manually here!
const toRemove = [ ] ;
const storage = this . $store . getters . storage ;
for ( let i = 0 ; i < storage . length ; ++ i ) {
const key = storage . key ( i ) ;
if ( key . startsWith ( "crypto." ) ) toRemove . push ( key ) ;
}
for ( const key of toRemove ) {
storage . removeItem ( key ) ;
}
} ,
logout ( ) {
if ( this . matrixClient ) {
this . removeMatrixClientListeners ( this . matrixClient ) ;
this . matrixClient . stopClient ( ) ;
this . matrixClient . clearStores ( ) . then ( ( ) => {
this . clearCryptoStore ( ) ;
} ) ;
this . matrixClient = null ;
this . matrixClientReady = false ;
} else {
this . clearCryptoStore ( ) ;
}
this . $store . commit ( "setUser" , null ) ;
this . $store . commit ( "setCurrentRoomId" , null ) ;
this . rooms = [ ] ;
this . userDisplayName = null ;
this . userAvatar = null ;
this . currentRoom = null ;
this . notificationCount = 0 ;
} ,
initClient ( ) {
this . reloadRooms ( ) ;
this . matrixClientReady = true ;
this . matrixClient . emit ( "Matrix.initialized" , this . matrixClient ) ;
this . matrixClient
. getProfileInfo ( this . currentUserId )
. then ( ( info ) => {
console . log ( "Got user profile: " + JSON . stringify ( info ) ) ;
this . userDisplayName = info . displayname ;
this . userAvatar = info . avatar _url ;
} )
. catch ( ( err ) => {
console . log ( "Failed to get user profile: " , err ) ;
} ) ;
} ,
getMatrixClient ( user ) {
if ( user === undefined ) {
user = this . $store . state . auth . user ;
}
if ( this . matrixClientReady ) {
return new Promise ( ( resolve , ignoredreject ) => {
resolve ( user ) ;
} ) ;
} else if ( this . matrixClient ) {
return new Promise ( ( resolve , ignoredreject ) => {
this . matrixClient . once ( "Matrix.initialized" , ( ignoredclient ) => {
resolve ( user ) ;
} ) ;
} ) ;
}
const matrixStore = new sdk . MemoryStore ( this . $store . getters . storage ) ;
var opts = {
2022-02-25 14:43:36 -06:00
baseUrl : user . home _server ,
2021-12-13 22:21:59 +01:00
userId : user . user _id ,
store : matrixStore ,
deviceId : user . device _id ,
accessToken : user . access _token ,
timelineSupport : true ,
unstableClientRelationAggregation : true ,
//useAuthorizationHeader: true
} ;
this . matrixClient = sdk . createClient ( opts ) ;
// if (user.is_guest) {
// this.matrixClient.setGuest(true);
// }
return this . matrixClient
. initCrypto ( )
. then ( ( ) => {
console . log ( "Crypto initialized" ) ;
this . addMatrixClientListeners ( this . matrixClient ) ;
this . matrixClient . startClient ( ) ;
return this . matrixClient ;
} )
. then ( ( matrixClient ) => {
if ( matrixClient . isInitialSyncComplete ( ) ) {
console . log ( "Initial sync done already!" ) ;
return matrixClient ;
} else {
return new Promise ( ( resolve , reject ) => {
2023-01-31 09:39:30 +00:00
matrixClient . once ( "sync" , function ( state , ignoredprevState , ignoredres ) {
2021-12-13 22:21:59 +01:00
console . log ( state ) ; // state will be 'PREPARED' when the client is ready to use
if ( state == "PREPARED" ) {
resolve ( matrixClient ) ;
} else if ( state == "ERROR" ) {
reject ( "Error syncing" ) ;
2020-11-25 10:02:24 +01:00
}
2021-12-13 22:21:59 +01:00
} ) ;
} ) ;
}
} )
. then ( ( ) => {
// Ready to use! Start by loading rooms.
this . initClient ( ) ;
return user ;
} ) ;
} ,
/ * *
* Returns a promise that will log us into the Matrix .
*
* Will use a real account , if we have one , otherwise will create
* a random account .
* /
2023-04-04 14:30:50 +00:00
getLoginPromise ( registrationFlowHandler ) {
2021-12-13 22:21:59 +01:00
if ( this . ready ) {
return Promise . resolve ( this . currentUser ) ;
}
2023-04-04 14:30:50 +00:00
return this . $store . dispatch ( "login" , { user : this . currentUser || new User ( this . $config . defaultServer , "" , "" , true ) , registrationFlowHandler } ) ;
2021-12-13 22:21:59 +01:00
} ,
addMatrixClientListeners ( client ) {
if ( client ) {
2022-05-03 09:40:02 +00:00
client . setMaxListeners ( 100 ) ; // Increate max number of listeners.
2021-12-13 22:21:59 +01:00
client . on ( "event" , this . onEvent ) ;
client . on ( "Room" , this . onRoom ) ;
client . on ( "Session.logged_out" , this . onSessionLoggedOut ) ;
client . on ( "Room.myMembership" , this . onRoomMyMembership ) ;
}
} ,
removeMatrixClientListeners ( client ) {
if ( client ) {
client . off ( "event" , this . onEvent ) ;
client . off ( "Room" , this . onRoom ) ;
client . off ( "Session.logged_out" , this . onSessionLoggedOut ) ;
client . off ( "Room.myMembership" , this . onRoomMyMembership ) ;
}
} ,
onEvent ( event ) {
switch ( event . getType ( ) ) {
case "m.room.topic" :
{
const room = this . matrixClient . getRoom ( event . getRoomId ( ) ) ;
if ( room ) {
Vue . set ( room , "topic" , event . getContent ( ) . topic ) ;
}
}
break ;
case "m.room.avatar" :
{
const room = this . matrixClient . getRoom ( event . getRoomId ( ) ) ;
if ( room ) {
Vue . set (
room ,
"avatar" ,
2023-01-31 09:39:30 +00:00
room . getAvatarUrl ( this . matrixClient . getHomeserverUrl ( ) , 80 , 80 , "scale" , true )
2021-12-13 22:21:59 +01:00
) ;
}
}
break ;
2023-06-08 13:25:02 +00:00
case "m.room.power_levels" :
2023-03-16 15:23:26 +01:00
{
if ( this . currentRoom && event . getRoomId ( ) == this . currentRoom . roomId ) {
this . currentRoomIsReadOnlyForUser = this . isReadOnlyRoomForUser ( event . getRoomId ( ) , this . currentUserId ) ;
}
}
break ;
2021-12-13 22:21:59 +01:00
}
this . updateNotificationCount ( ) ;
} ,
onRoom ( ignoredroom ) {
this . reloadRooms ( ) ;
this . updateNotificationCount ( ) ;
} ,
onRoomMyMembership ( room ) {
if ( room . selfMembership === "invite" ) {
// Invitation. Need to call "recalculate" to pick
// up room name, not sure why exactly.
room . recalculate ( ) ;
}
2022-04-20 14:16:01 +00:00
this . reloadRooms ( ) ;
2021-12-13 22:21:59 +01:00
} ,
onSessionLoggedOut ( ) {
console . log ( "Logged out!" ) ;
if ( this . matrixClient ) {
this . removeMatrixClientListeners ( this . matrixClient ) ;
this . matrixClient . stopClient ( ) ;
this . matrixClient = null ;
this . matrixClientReady = false ;
}
// For "real" accounts we totally wipe the user object, but for "guest"
// accounts (i.e. created from random data and with password never changed)
// we need to hang on to the generated password and use that to login to a new
// session, so only wipe the token in s that case.
// Clear the access token
2022-04-21 09:41:52 +00:00
var user = this . $store . state . auth . user ;
2021-12-13 22:21:59 +01:00
if ( user . is _guest ) {
delete user . access _token ;
this . $store . commit ( "setUser" , user ) ;
2022-04-21 09:41:52 +00:00
2021-12-13 22:21:59 +01:00
// Login again
2023-01-31 09:39:30 +00:00
this . login ( user ) . catch ( ( error ) => {
if ( error . data . errcode === "M_FORBIDDEN" && this . currentUser . is _guest ) {
2022-04-21 09:41:52 +00:00
// Guest account and password don't work. We are in a strange state, probably because
// of server cleanup of accounts or similar. Wipe account and restart...
this . $store . commit ( "setUser" , null ) ;
}
this . $store . commit ( "setCurrentRoomId" , null ) ;
this . $navigation . push ( { path : "/login" } , - 1 ) ;
2023-01-31 09:39:30 +00:00
} ) ;
2021-12-13 22:21:59 +01:00
} else {
this . $store . commit ( "setUser" , null ) ;
this . $store . commit ( "setCurrentRoomId" , null ) ;
this . $navigation . push ( { path : "/login" } , - 1 ) ;
}
} ,
reloadRooms ( ) {
// TODO - do incremental update instead of replacing the whole array
// each time!
var updatedRooms = this . matrixClient . getVisibleRooms ( ) ;
updatedRooms = updatedRooms . filter ( ( room ) => {
2023-01-31 09:39:30 +00:00
return room . selfMembership && ( room . selfMembership == "invite" || room . selfMembership == "join" ) ;
2021-12-13 22:21:59 +01:00
} ) ;
updatedRooms . forEach ( ( room ) => {
if ( ! room . avatar ) {
2023-01-31 09:39:30 +00:00
Vue . set ( room , "avatar" , room . getAvatarUrl ( this . matrixClient . getHomeserverUrl ( ) , 80 , 80 , "scale" , true ) ) ;
2021-12-13 22:21:59 +01:00
}
} ) ;
Vue . set ( this , "rooms" , updatedRooms ) ;
2023-04-04 14:30:50 +00:00
const resolvedId = ( this . currentRoomId && this . currentRoomId . startsWith ( "#" ) ) ? this . matrixClient . resolveRoomAlias ( this . currentRoomId ) . then ( r => r . room _id ) : Promise . resolve ( this . currentRoomId ) ;
resolvedId . then ( roomId => {
const currentRoom = this . getRoom ( roomId ) ;
if ( this . currentRoom != currentRoom ) {
this . currentRoom = currentRoom ;
}
} ) . catch ( ignorederror => { } ) ;
2021-12-13 22:21:59 +01:00
} ,
setCurrentRoomId ( roomId ) {
this . $store . commit ( "setCurrentRoomId" , roomId ) ;
this . currentRoom = this . getRoom ( roomId ) ;
} ,
getRoom ( roomId ) {
if ( ! roomId ) {
return null ;
}
var room = null ;
if ( this . matrixClient ) {
const visibleRooms = this . matrixClient . getRooms ( ) ;
room = visibleRooms . find ( ( room ) => {
if ( roomId . startsWith ( "#" ) ) {
return room . getCanonicalAlias ( ) == roomId ;
}
return room . roomId == roomId ;
} ) ;
}
return room || null ;
} ,
/ * *
* Return all users we are in a "invite" only room with !
* /
getAllFriends ( ) {
var ids = { } ;
const ret = [ ] ;
for ( const room of this . rooms ) {
2023-01-31 09:39:30 +00:00
if ( room . selfMembership == "join" && this . getRoomJoinRule ( room ) == "invite" ) {
2021-12-13 22:21:59 +01:00
for ( const member of room . getJoinedMembers ( ) ) {
2023-01-31 09:39:30 +00:00
if ( member . userId != this . currentUserId && ! ids [ member . userId ] ) {
2021-12-13 22:21:59 +01:00
ids [ member . userId ] = member ;
ret . push ( member ) ;
}
}
}
}
ret . sort ( ( a , b ) => {
const aName = a . user ? a . user . displayName : a . name ;
const bName = b . user ? b . user . displayName : b . name ;
return aName . localeCompare ( bName ) ;
} ) ;
return ret ;
} ,
getRoomJoinRule ( room ) {
if ( room ) {
2023-01-31 09:39:30 +00:00
const joinRules = room . currentState . getStateEvents ( "m.room.join_rules" , "" ) ;
2021-12-13 22:21:59 +01:00
return joinRules && joinRules . getContent ( ) . join _rule ;
}
return null ;
} ,
getRoomHistoryVisibility ( room ) {
if ( room ) {
2023-01-31 09:39:30 +00:00
const historyVisibility = room . currentState . getStateEvents ( "m.room.history_visibility" , "" ) ;
return historyVisibility && historyVisibility . getContent ( ) . history _visibility ;
2021-12-13 22:21:59 +01:00
}
return null ;
} ,
leaveRoom ( roomId ) {
return this . matrixClient . leave ( roomId , undefined ) . then ( ( ) => {
2023-03-16 08:17:29 +00:00
this . $store . commit ( "setCurrentRoomId" , null ) ;
2021-12-13 22:21:59 +01:00
this . rooms = this . rooms . filter ( ( room ) => {
room . roomId != roomId ;
} ) ;
2023-03-16 08:17:29 +00:00
//this.matrixClient.store.removeRoom(roomId);
2021-12-13 22:21:59 +01:00
//this.matrixClient.forget(roomId, true, undefined);
} ) ;
} ,
2023-08-01 10:14:10 +00:00
/ * *
* Leave the room , and if this is the last room we are in , navigate to the "goodbye" page .
* Otherwise , navigate to home .
* @ param roomId
* /
leaveRoomAndNavigate ( roomId ) {
const joinedRooms = this . joinedRooms ;
const isLastRoomWeAreJoinedTo = (
joinedRooms &&
joinedRooms . length == 1 &&
joinedRooms [ 0 ] . roomId == roomId
) ;
return this . leaveRoom ( roomId )
. then ( ( ) => {
if ( isLastRoomWeAreJoinedTo ) {
this . $navigation . push ( { name : "Goodbye" } , - 1 ) ;
} else {
this . $navigation . push (
{ name : "Home" , params : { roomId : null } } ,
- 1
) ;
}
} )
} ,
2023-01-22 14:41:35 +01:00
kickUser ( roomId , userId ) {
if ( this . matrixClient && roomId && userId ) {
2023-01-31 09:39:30 +00:00
this . matrixClient . kick ( roomId , userId , "" ) ;
2023-01-22 14:41:35 +01:00
}
} ,
banUser ( roomId , userId ) {
if ( this . matrixClient && roomId && userId ) {
2023-01-31 09:39:30 +00:00
this . matrixClient . ban ( roomId , userId , "" ) ;
2023-01-22 14:41:35 +01:00
}
} ,
makeAdmin ( roomId , userId ) {
if ( this . matrixClient && roomId && userId ) {
const room = this . getRoom ( roomId ) ;
if ( room && room . currentState ) {
const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
if ( powerLevelEvent ) {
this . matrixClient . setPowerLevel ( roomId , userId , 100 , powerLevelEvent ) ;
}
2023-01-31 09:39:30 +00:00
}
2023-01-22 14:41:35 +01:00
}
} ,
makeModerator ( roomId , userId ) {
if ( this . matrixClient && roomId && userId ) {
const room = this . getRoom ( roomId ) ;
2023-01-31 09:39:30 +00:00
console . log ( "Room" , room ) ;
2023-01-22 14:41:35 +01:00
if ( room && room . currentState ) {
const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
if ( powerLevelEvent ) {
this . matrixClient . setPowerLevel ( roomId , userId , 50 , powerLevelEvent ) ;
}
2023-01-31 09:39:30 +00:00
}
2023-01-22 14:41:35 +01:00
}
} ,
revokeModerator ( roomId , userId ) {
if ( this . matrixClient && roomId && userId ) {
const room = this . getRoom ( roomId ) ;
if ( room && room . currentState ) {
const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
if ( powerLevelEvent ) {
this . matrixClient . setPowerLevel ( roomId , userId , 0 , powerLevelEvent ) ;
}
2023-01-31 09:39:30 +00:00
}
2023-01-22 14:41:35 +01:00
}
} ,
2023-04-04 14:30:50 +00:00
/ * *
* Returns true if the current user is joined to the given room .
* @ param roomIdOrAlias
* @ returns Promise < Bool > - Whether the user is joined to the room or not
* /
isJoinedToRoom ( roomIdOrAlias ) {
if ( roomIdOrAlias && this . matrixClient ) {
try {
const resolvedRoomId = roomIdOrAlias . startsWith ( "#" ) ? this . matrixClient . resolveRoomAlias ( roomIdOrAlias ) . then ( res => res . room _id ) : Promise . resolve ( roomIdOrAlias ) ;
return resolvedRoomId . then ( roomId => {
return this . matrixClient . getJoinedRooms ( ) . then ( rooms => {
return rooms . joined _rooms . includes ( roomId ) ;
} ) ;
} ) ;
} catch ( ignorederror ) {
console . error ( ignorederror ) ;
return Promise . resolve ( false ) ;
}
}
return Promise . resolve ( false ) ;
} ,
2023-03-16 15:23:26 +01:00
isReadOnlyRoom ( roomId ) {
if ( this . matrixClient && roomId ) {
const room = this . getRoom ( roomId ) ;
if ( room && room . currentState ) {
const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
if ( powerLevelEvent ) {
return powerLevelEvent . getContent ( ) . events _default > 0
}
}
}
return false ;
} ,
isReadOnlyRoomForUser ( roomId , userId ) {
if ( this . matrixClient && roomId && userId ) {
const room = this . getRoom ( roomId ) ;
if ( room && room . currentState ) {
return ! room . currentState . maySendMessage ( userId )
}
}
return false ;
} ,
setReadOnlyRoom ( roomId , readOnly ) {
if ( this . matrixClient && roomId ) {
const room = this . getRoom ( roomId ) ;
if ( room && room . currentState ) {
const powerLevelEvent = room . currentState . getStateEvents ( "m.room.power_levels" , "" ) ;
if ( powerLevelEvent ) {
let content = powerLevelEvent . getContent ( ) ;
content . events _default = readOnly ? 50 : 0 ;
this . matrixClient . sendStateEvent (
room . roomId ,
"m.room.power_levels" ,
content
) ;
}
}
}
} ,
2021-12-13 22:21:59 +01:00
/ * *
* Purge the room with the given id ! This means :
* - Make room invite only
* - Disallow guest access
* - Set history visibility to 'joined'
* - Redact all events
* - Kick all members
* @ param roomId
* /
purgeRoom ( roomId , statusCallback ) {
2023-02-08 11:22:12 +01:00
this . currentRoomBeingPurged = true ;
const sleep = ( ms ) => {
return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
} ;
const withRetry = ( codeBlock ) => {
return codeBlock ( ) . catch ( ( error ) => {
if ( error && error . errcode == "M_LIMIT_EXCEEDED" ) {
var retry = 1000 ;
if ( error . data ) {
const retryIn = error . data . retry _after _ms ;
retry = Math . max ( retry , retryIn ? retryIn : 0 ) ;
}
console . log ( "Rate limited, retry in" , retry ) ;
return sleep ( retry ) . then ( ( ) => withRetry ( codeBlock ) ) ;
} else {
return Promise . resolve ( ) ;
}
} ) ;
} ;
2021-12-13 22:21:59 +01:00
const oldGlobalErrorSetting = this . matrixClient . getGlobalErrorOnUnknownDevices ( ) ;
return new Promise ( ( resolve , reject ) => {
const room = this . getRoom ( roomId ) ;
if ( ! room ) {
reject ( "Room not found!" ) ;
return ;
}
2020-11-09 15:08:36 +01:00
2023-01-31 09:39:30 +00:00
const timelineWindow = new TimelineWindow ( this . matrixClient , room . getUnfilteredTimelineSet ( ) , { } ) ;
2021-12-13 22:21:59 +01:00
const self = this ;
//console.log("Purge: set invite only");
statusCallback ( this . $t ( "room.purge_set_room_state" ) ) ;
this . matrixClient
2023-01-31 09:39:30 +00:00
. sendStateEvent ( roomId , "m.room.join_rules" , { join _rule : "invite" } , "" )
2021-12-13 22:21:59 +01:00
. then ( ( ) => {
//console.log("Purge: forbid guest access");
return this . matrixClient . sendStateEvent (
roomId ,
"m.room.guest_access" ,
{ guest _access : "forbidden" } ,
""
) ;
} )
. then ( ( ) => {
//console.log("Purge: set history visibility to 'joined'");
2023-01-31 09:39:30 +00:00
return this . matrixClient . sendStateEvent ( roomId , "m.room.history_visibility" , {
history _visibility : "joined" ,
} ) ;
2021-12-13 22:21:59 +01:00
} )
. then ( ( ) => {
//console.log("Purge: create timeline");
return timelineWindow . load ( null , 100 ) ;
} )
. then ( ( ) => {
const getMoreIfAvailable = function _getMoreIfAvailable ( ) {
if ( timelineWindow . canPaginate ( EventTimeline . BACKWARDS ) ) {
//console.log("Purge: page back");
2023-01-31 09:39:30 +00:00
return timelineWindow . paginate ( EventTimeline . BACKWARDS , 100 , true , 5 ) . then ( ( gotmore ) => {
if ( gotmore ) {
2021-12-13 22:21:59 +01:00
return _getMoreIfAvailable . call ( self ) ;
2023-01-31 09:39:30 +00:00
}
return Promise . resolve ( "Done" ) ;
} ) ;
2021-12-13 22:21:59 +01:00
} else {
return Promise . resolve ( "Done" ) ;
}
} . bind ( self ) ;
return getMoreIfAvailable ( ) ;
} )
. then ( ( ) => {
//console.log("Purge: redact events");
statusCallback ( this . $t ( "room.purge_redacting_events" ) ) ;
// First ignore unknown device errors
this . matrixClient . setGlobalErrorOnUnknownDevices ( false ) ;
2023-02-08 11:22:12 +01:00
const allEvents = timelineWindow . getEvents ( ) . filter ( ( event ) => {
return (
! event . isRedacted ( ) &&
! event . isRedaction ( ) &&
! event . isState ( ) &&
( ! room . currentState || room . currentState . maySendRedactionForEvent ( event , this . currentUserId ) )
) ;
2021-12-13 22:21:59 +01:00
} ) ;
2023-02-08 11:22:12 +01:00
const redactFirstEvent = ( events ) => {
statusCallback (
this . $t ( "room.purge_redacting_events" , {
count : allEvents . length - events . length + 1 ,
total : allEvents . length ,
} )
) ;
if ( events . length == 0 ) {
return Promise . resolve ( true ) ;
}
const event = events [ 0 ] ;
return withRetry ( ( ) => this . matrixClient . redactEvent ( event . getRoomId ( ) , event . getId ( ) ) ) . then ( ( ) =>
redactFirstEvent ( events . slice ( 1 ) )
) ;
} ;
return redactFirstEvent ( allEvents ) ;
2021-12-13 22:21:59 +01:00
} )
. then ( ( ) => {
//console.log("Purge: kick members");
statusCallback ( this . $t ( "room.purge_removing_members" ) ) ;
var joined = room . getMembersWithMembership ( "join" ) ;
var invited = room . getMembersWithMembership ( "invite" ) ;
2023-01-31 09:39:30 +00:00
var allMembers = joined . concat ( invited ) ;
const kickFirstMember = ( members ) => {
//console.log(`Kicking ${members.length} members`);
2023-02-08 11:22:12 +01:00
statusCallback (
this . $t ( "room.purge_removing_members" , {
count : allMembers . length - members . length + 1 ,
total : allMembers . length ,
} )
) ;
2023-01-31 09:39:30 +00:00
if ( members . length == 0 ) {
return Promise . resolve ( true ) ;
2021-12-13 22:21:59 +01:00
}
2023-01-31 09:39:30 +00:00
const member = members [ 0 ] ;
if ( member . userId == self . currentUserId ) {
return kickFirstMember ( members . slice ( 1 ) ) ;
} else {
// Slight pause to avoid rate limiting.
return sleep ( 0.1 )
2023-02-08 11:22:12 +01:00
. then ( ( ) => withRetry ( ( ) => this . matrixClient . kick ( roomId , member . userId , "Room Deleted" ) ) )
. then ( ( ) => kickFirstMember ( members . slice ( 1 ) ) ) ;
2023-01-31 09:39:30 +00:00
}
} ;
return kickFirstMember ( allMembers ) ;
2021-12-13 22:21:59 +01:00
} )
. then ( ( ) => {
statusCallback ( null ) ;
2023-01-31 09:39:30 +00:00
this . matrixClient . setGlobalErrorOnUnknownDevices ( oldGlobalErrorSetting ) ;
2023-02-08 11:22:12 +01:00
return withRetry ( ( ) => this . leaveRoom ( roomId ) ) ;
2021-12-13 22:21:59 +01:00
} )
. then ( ( ) => {
2023-02-08 11:22:12 +01:00
this . currentRoomBeingPurged = false ;
2021-12-13 22:21:59 +01:00
resolve ( true ) ; // Done!
} )
. catch ( ( err ) => {
console . error ( "Error purging room" , err ) ;
2023-02-08 11:22:12 +01:00
this . currentRoomBeingPurged = false ;
2023-01-31 09:39:30 +00:00
this . matrixClient . setGlobalErrorOnUnknownDevices ( oldGlobalErrorSetting ) ;
2021-12-13 22:21:59 +01:00
reject ( err ) ;
} ) ;
} ) ;
} ,
/ * *
* Get a private chat room with the given user . Searches through our rooms to see
* if a suitable room already exists . If not , one is created .
* @ param { * } userId The user to chat with .
* /
getOrCreatePrivateChat ( userId ) {
return new Promise ( ( resolve , reject ) => {
for ( const room of this . rooms ) {
// Is the other member the one we are looking for?
if ( this . isDirectRoomWith ( room , userId ) ) {
2022-04-27 14:24:39 +00:00
if ( room . getMyMembership ( ) == "invite" ) {
this . matrixClient . joinRoom ( room . roomId ) ;
} else {
var member = room . getMember ( userId ) ;
if ( member && member . membership != "join" ) {
// Resend invite
this . matrixClient . invite ( room . roomId , userId ) ;
}
2020-12-16 15:57:44 +01:00
}
2021-12-13 22:21:59 +01:00
resolve ( room ) ;
return ;
}
}
2021-02-17 10:43:42 +01:00
2021-12-13 22:21:59 +01:00
// No room found, create one
//
const createRoomOptions = {
visibility : "private" , // Not listed!
preset : "private_chat" ,
2022-04-12 20:39:43 +00:00
is _direct : true ,
2021-12-13 22:21:59 +01:00
initial _state : [
{
type : "m.room.encryption" ,
state _key : "" ,
content : {
algorithm : "m.megolm.v1.aes-sha2" ,
} ,
} ,
{
type : "m.room.guest_access" ,
state _key : "" ,
content : {
guest _access : "forbidden" ,
} ,
} ,
{
type : "m.room.history_visibility" ,
state _key : "" ,
content : {
2023-05-11 13:55:43 +02:00
history _visibility : "invited" ,
} ,
} ,
{
type : "m.room.power_levels" ,
state _key : "" ,
content : {
users : {
[ this . currentUserId ] : 100 ,
[ userId ] : 100 ,
} ,
2021-12-13 22:21:59 +01:00
} ,
} ,
] ,
invite : [ userId ] ,
} ;
return this . matrixClient
. createRoom ( createRoomOptions )
. then ( ( { room _id , room _alias } ) => {
resolve ( this . getRoom ( room _alias || room _id ) ) ;
} )
. catch ( ( error ) => {
reject ( error ) ;
} ) ;
} ) ;
} ,
/ * *
* Return true if this room is a direct room with the given user .
* @ param { } room
* @ param { * } userId
* /
isDirectRoomWith ( room , userId ) {
2023-01-31 09:39:30 +00:00
if ( room . getJoinRule ( ) == "invite" && room . getMembers ( ) . length == 2 ) {
2022-04-27 14:24:39 +00:00
let other = room . getMember ( userId ) ;
if ( other ) {
if ( room . getMyMembership ( ) == "invite" && other . membership == "join" ) {
return true ;
} else if ( room . getMyMembership ( ) == "join" && room . canInvite ( this . currentUserId ) ) {
return true ;
} else if ( room . getMyMembership ( ) == "join" && other . membership == "join" ) {
return true ;
}
2021-12-13 22:21:59 +01:00
}
}
return false ;
} ,
2023-03-03 14:43:53 +00:00
/ * *
* Return true if this room is a direct room with one other user . NOTE : this currently
* only checks number of members , not any is _direct flag .
* @ param { } room
* /
isDirectRoom ( room ) {
// TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
// apprently need to set this...
if ( room . getJoinRule ( ) == "invite" && room . getMembers ( ) . length == 2 ) {
return true ;
}
return false ;
} ,
2021-12-13 22:21:59 +01:00
on ( event , handler ) {
if ( this . matrixClient ) {
this . matrixClient . on ( event , handler ) ;
}
} ,
off ( event , handler ) {
if ( this . matrixClient ) {
this . matrixClient . off ( event , handler ) ;
}
} ,
2023-06-07 12:55:12 +00:00
setUserDisplayName ( name ) {
if ( this . matrixClient ) {
return this . matrixClient . setDisplayName ( name || this . user . userId ) . then ( ( ) => this . userDisplayName = name ) . catch ( err => console . err ( "Failed to set display name" , err ) ) ;
} else {
return Promise . reject ( "No matrix client" ) ;
}
} ,
2021-12-13 22:21:59 +01:00
setPassword ( oldPassword , newPassword ) {
if ( this . matrixClient && this . currentUser ) {
const authDict = {
type : "m.login.password" ,
identifier : {
type : "m.id.user" ,
user : this . currentUser . user _id ,
} ,
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
user : this . currentUser . user _id ,
password : oldPassword ,
} ;
const self = this ;
return this . matrixClient
. setPassword ( authDict , newPassword )
. then ( ( ) => {
// Forget password and remove the 'is_guest' flag, we are now a "real" user!
self . currentUser . password = undefined ;
self . currentUser . is _guest = false ;
self . $store . commit ( "setUser" , self . currentUser ) ;
} )
. then ( ( ) => {
return true ;
} ) ;
}
return Promise . resolve ( false ) ;
} ,
uploadFile ( file , opts ) {
return this . matrixClient . uploadContent ( file , opts ) ;
} ,
2022-12-12 16:10:53 +00:00
/ * *
* Get a matrix client that can be used for public queries . If we are logged in , this is the normal
* matrix client . If not , we create a temp one with a temp password .
* @ returns A MatrixClient that can be used for public queries
* /
getPublicQueryMatrixClient ( ) {
2021-12-13 22:21:59 +01:00
var clientPromise ;
if ( this . matrixClient ) {
clientPromise = this . getMatrixClient ( ) . then ( ( ) => {
return this . matrixClient ;
} ) ;
} else {
2023-03-28 14:35:03 +02:00
const tempMatrixClient = sdk . createClient ( { baseUrl : this . $config . defaultServer } ) ;
2021-12-13 22:21:59 +01:00
var tempUserString = this . $store . state . tempuser ;
var tempUser = null ;
if ( tempUserString ) {
tempUser = JSON . parse ( tempUserString ) ;
2020-11-09 15:08:36 +01:00
}
2021-12-13 22:21:59 +01:00
// Need to create an account?
//
if ( tempUser ) {
clientPromise = Promise . resolve ( tempUser ) ;
} else {
2022-07-01 08:55:26 +00:00
const user = util . randomUser ( this . $config . userIdPrefix ) ;
2021-12-13 22:21:59 +01:00
const pass = util . randomPass ( ) ;
clientPromise = tempMatrixClient
. register ( user , pass , null , {
type : "m.login.dummy" ,
initial _device _display _name : this . $config . appName ,
} )
. then ( ( response ) => {
console . log ( "Response" , response ) ;
response . password = pass ;
response . is _guest = true ;
this . $store . commit ( "setTempUser" , response ) ;
return response ;
} ) ;
}
2021-05-10 11:13:22 +02:00
2021-12-13 22:21:59 +01:00
// Get an access token
clientPromise = clientPromise . then ( ( user ) => {
var data = {
user : User . localPart ( user . user _id ) ,
password : user . password ,
type : "m.login.password" ,
initial _device _display _name : this . $config . appName ,
} ;
if ( user . device _id ) {
data . device _id = user . device _id ;
}
return tempMatrixClient . login ( "m.login.password" , data ) ;
} ) ;
// Then login
//
// Create a slimmed down client, without crypto. This one is
// Only used to get public room info from.
clientPromise = clientPromise . then ( ( user ) => {
var opts = {
baseUrl : this . $config . defaultServer ,
userId : user . user _id ,
accessToken : user . access _token ,
timelineSupport : false ,
} ;
var matrixClient = sdk . createClient ( opts ) ;
matrixClient . startClient ( ) ;
return matrixClient ;
} ) ;
}
2022-12-12 16:10:53 +00:00
return clientPromise ;
} ,
getPublicUserInfo ( userId ) {
if ( ! userId ) {
return Promise . reject ( "Invalid parameters" ) ;
}
const parts = userId . split ( ":" ) ;
if ( parts . length != 2 ) {
return Promise . reject ( "Unknown home server" ) ;
}
const clientPromise = this . getPublicQueryMatrixClient ( ) ;
let matrixClient ;
return clientPromise
. then ( ( client ) => {
matrixClient = client ;
return client . getProfileInfo ( userId ) ;
} )
. then ( ( response ) => {
if ( response . avatar _url ) {
2023-01-31 09:39:30 +00:00
response . avatar = matrixClient . mxcUrlToHttp ( response . avatar _url , 80 , 80 , "scale" , true ) ;
2022-12-12 16:10:53 +00:00
}
return Promise . resolve ( response ) ;
} )
. catch ( ( err ) => {
return Promise . reject ( "Failed to find user info: " + err ) ;
} ) ;
} ,
getPublicRoomInfo ( roomId ) {
if ( ! roomId ) {
return Promise . reject ( "Invalid parameters" ) ;
}
const parts = roomId . split ( ":" ) ;
if ( parts . length != 2 ) {
return Promise . reject ( "Unknown room server" ) ;
}
const server = parts [ 1 ] ;
const clientPromise = this . getPublicQueryMatrixClient ( ) ;
2021-12-13 22:21:59 +01:00
const findOrGetMore = function _findOrGetMore ( client , response ) {
for ( var room of response . chunk ) {
if (
( roomId . startsWith ( "#" ) && room . canonical _alias == roomId ) ||
( roomId . startsWith ( "!" ) && room . room _id == roomId )
) {
if ( room . avatar _url ) {
2023-01-31 09:39:30 +00:00
room . avatar = client . mxcUrlToHttp ( room . avatar _url , 80 , 80 , "scale" , true ) ;
2021-12-13 22:21:59 +01:00
}
return Promise . resolve ( room ) ;
}
}
if ( response . next _batch ) {
return client
. publicRooms ( {
server : server ,
limit : 1000 ,
since : response . next _batch ,
} )
. then ( ( response ) => {
return _findOrGetMore ( client , response ) ;
} )
. catch ( ( err ) => {
return Promise . reject ( "Failed to find room: " + err ) ;
} ) ;
} else {
return Promise . reject ( "No more data" ) ;
}
} ;
var matrixClient ;
return clientPromise
. then ( ( client ) => {
matrixClient = client ;
return matrixClient . publicRooms ( { server : server , limit : 1000 } ) ;
} )
. then ( ( response ) => {
return findOrGetMore ( matrixClient , response ) ;
} )
. catch ( ( err ) => {
return Promise . reject ( "Failed to find room: " + err ) ;
} ) ;
} ,
updateNotificationCount ( ) {
var count = 0 ;
this . rooms . forEach ( ( room ) => {
count += room . getUnreadNotificationCount ( "total" ) || 0 ;
} ) ;
this . notificationCount = count ;
} ,
} ,
} ) ;
2023-01-31 09:39:30 +00:00
sdk . setCryptoStoreFactory ( matrixService . createCryptoStore . bind ( matrixService ) ) ;
2021-12-13 22:21:59 +01:00
Vue . prototype . $matrix = matrixService ;
} ,
} ;