/* * Copyright (c) 2020, 2021, 2024 Jonathan Schleifer <js@nil.im> * * https://fl.nil.im/objmatrix * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ #import "MTXClient.h" #import "MTXRequest.h" #import "MTXFetchRoomListFailedException.h" #import "MTXJoinRoomFailedException.h" #import "MTXLeaveRoomFailedException.h" #import "MTXLoginFailedException.h" #import "MTXLogoutFailedException.h" #import "MTXSendMessageFailedException.h" #import "MTXSyncFailedException.h" static void validateHomeserver(OFIRI *homeserver) { if (![homeserver.scheme isEqual: @"http"] && ![homeserver.scheme isEqual: @"https"]) @throw [OFUnsupportedProtocolException exceptionWithIRI: homeserver]; if (homeserver.path != nil && ![homeserver.path isEqual: @"/"]) @throw [OFInvalidArgumentException exception]; if (homeserver.user != nil || homeserver.password != nil || homeserver.query != nil || homeserver.fragment != nil) @throw [OFInvalidArgumentException exception]; } @implementation MTXClient { bool _syncing; } + (instancetype)clientWithUserID: (OFString *)userID deviceID: (OFString *)deviceID accessToken: (OFString *)accessToken homeserver: (OFIRI *)homeserver storage: (id <MTXStorage>)storage { return [[[self alloc] initWithUserID: userID deviceID: deviceID accessToken: accessToken homeserver: homeserver storage: storage] autorelease]; } + (void)logInWithUser: (OFString *)user password: (OFString *)password homeserver: (OFIRI *)homeserver storage: (id <MTXStorage>)storage block: (MTXClientLoginBlock)block { void *pool = objc_autoreleasePoolPush(); validateHomeserver(homeserver); MTXRequest *request = [MTXRequest requestWithPath: @"/_matrix/client/r0/login" accessToken: nil homeserver: homeserver]; request.method = OFHTTPRequestMethodPost; request.body = @{ @"type": @"m.login.password", @"identifier": @{ @"type": @"m.id.user", @"user": user }, @"password": password }; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { block(nil, exception); return; } if (statusCode != 200) { id exception = [MTXLoginFailedException exceptionWithUser: user homeserver: homeserver statusCode: statusCode response: response]; block(nil, exception); return; } OFString *userID = response[@"user_id"]; OFString *deviceID = response[@"device_id"]; OFString *accessToken = response[@"access_token"]; if (![userID isKindOfClass: OFString.class] || ![deviceID isKindOfClass: OFString.class] || ![accessToken isKindOfClass: OFString.class]) { block(nil, [OFInvalidServerResponseException exception]); return; } OFString *baseIRI = response[@"well_known"][@"m.homeserver"][@"base_url"]; if (baseIRI != nil && ![baseIRI isKindOfClass: OFString.class]) { block(nil, [OFInvalidServerResponseException exception]); return; } OFIRI *realHomeserver; if (baseIRI != nil) { @try { realHomeserver = [OFIRI IRIWithString: baseIRI]; } @catch (id e) { block(nil, e); return; } } else realHomeserver = homeserver; MTXClient *client = [MTXClient clientWithUserID: userID deviceID: deviceID accessToken: accessToken homeserver: realHomeserver storage: storage]; block(client, nil); }]; objc_autoreleasePoolPop(pool); } - (instancetype)initWithUserID: (OFString *)userID deviceID: (OFString *)deviceID accessToken: (OFString *)accessToken homeserver: (OFIRI *)homeserver storage: (id <MTXStorage>)storage { self = [super init]; @try { validateHomeserver(homeserver); _userID = [userID copy]; _deviceID = [deviceID copy]; _accessToken = [accessToken copy]; _homeserver = [homeserver copy]; _storage = [storage retain]; _syncTimeout = 300; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [_userID release]; [_deviceID release]; [_accessToken release]; [_homeserver release]; [_storage release]; [super dealloc]; } - (OFString *)description { return [OFString stringWithFormat: @"<%@\n" @"\tUser ID = %@\n" @"\tDevice ID = %@\n" @"\tAccess token = %@\n" @"\tHomeserver = %@\n" @">", self.class, _userID, _deviceID, _accessToken, _homeserver]; } - (MTXRequest *)requestWithPath: (OFString *)path { return [MTXRequest requestWithPath: path accessToken: _accessToken homeserver: _homeserver]; } - (void)startSyncLoop { if (_syncing) return; _syncing = true; void *pool = objc_autoreleasePoolPush(); MTXRequest *request = [self requestWithPath: @"/_matrix/client/r0/sync"]; unsigned long long timeoutMs = _syncTimeout * 1000; OFMutableArray<OFPair <OFString *, OFString *> *> *queryItems = [OFMutableArray array]; OFString *since = [_storage nextBatchForDeviceID: _deviceID]; [queryItems addObject: [OFPair pairWithFirstObject: @"timeout" secondObject: @(timeoutMs).stringValue]]; if (since != nil) [queryItems addObject: [OFPair pairWithFirstObject: @"since" secondObject: since]]; request.queryItems = queryItems; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { if (_syncExceptionHandler != NULL) _syncExceptionHandler(exception); return; } if (statusCode != 200) { if (_syncExceptionHandler != NULL) _syncExceptionHandler([MTXSyncFailedException exceptionWithStatusCode: statusCode response: response client: self]); return; } OFString *nextBatch = response[@"next_batch"]; if (![nextBatch isKindOfClass: OFString.class]) { if (_syncExceptionHandler != NULL) _syncExceptionHandler( [OFInvalidServerResponseException exception]); return; } @try { [_storage transactionWithBlock: ^ { [_storage setNextBatch: nextBatch forDeviceID: _deviceID]; [self processRoomsSync: response[@"rooms"]]; [self processPresenceSync: response[@"presence"]]; [self processAccountDataSync: response[@"account_data"]]; [self processToDeviceSync: response[@"to_device"]]; return true; }]; } @catch (id e) { if (_syncExceptionHandler != NULL) _syncExceptionHandler(e); return; } if (_syncing) [self startSyncLoop]; }]; objc_autoreleasePoolPop(pool); } - (void)stopSyncLoop { _syncing = false; } - (void)logOutWithBlock: (MTXClientResponseBlock)block { void *pool = objc_autoreleasePoolPush(); MTXRequest *request = [self requestWithPath: @"/_matrix/client/r0/logout"]; request.method = OFHTTPRequestMethodPost; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { block(exception); return; } if (statusCode != 200) { block([MTXLogoutFailedException exceptionWithStatusCode: statusCode response: response client: self]); return; } block(nil); }]; objc_autoreleasePoolPop(pool); } - (void)fetchRoomListWithBlock: (MTXClientRoomListBlock)block { void *pool = objc_autoreleasePoolPush(); MTXRequest *request = [self requestWithPath: @"/_matrix/client/r0/joined_rooms"]; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { block(nil, exception); return; } if (statusCode != 200) { block(nil, [MTXFetchRoomListFailedException exceptionWithStatusCode: statusCode response: response client: self]); return; } OFArray<OFString *> *joinedRooms = response[@"joined_rooms"]; if (![joinedRooms isKindOfClass: OFArray.class]) { block(nil, [OFInvalidServerResponseException exception]); return; } for (OFString *room in joinedRooms) { if (![room isKindOfClass: OFString.class]) { block(nil, [OFInvalidServerResponseException exception]); return; } } block(response[@"joined_rooms"], nil); }]; objc_autoreleasePoolPop(pool); } - (void)joinRoom: (OFString *)room block: (MTXClientRoomJoinBlock)block { void *pool = objc_autoreleasePoolPush(); MTXRequest *request = [self requestWithPath: [OFString stringWithFormat: @"/_matrix/client/r0/join/%@", room]]; request.method = OFHTTPRequestMethodPost; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { block(nil, exception); return; } if (statusCode != 200) { block(nil, [MTXJoinRoomFailedException exceptionWithRoom: room statusCode: statusCode response: response client: self]); return; } OFString *roomID = response[@"room_id"]; if (![roomID isKindOfClass: OFString.class]) { block(nil, [OFInvalidServerResponseException exception]); return; } block(roomID, nil); }]; objc_autoreleasePoolPop(pool); } - (void)leaveRoom: (OFString *)roomID block: (MTXClientResponseBlock)block { void *pool = objc_autoreleasePoolPush(); MTXRequest *request = [self requestWithPath: [OFString stringWithFormat: @"/_matrix/client/r0/rooms/%@/leave", roomID]]; request.method = OFHTTPRequestMethodPost; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { block(exception); return; } if (statusCode != 200) { block([MTXLeaveRoomFailedException exceptionWithRoomID: roomID statusCode: statusCode response: response client: self]); return; } block(nil); }]; objc_autoreleasePoolPop(pool); } - (void)sendMessage: (OFString *)message roomID: (OFString *)roomID block: (MTXClientResponseBlock)block { void *pool = objc_autoreleasePoolPush(); OFString *path = [OFString stringWithFormat: @"/_matrix/client/r0/rooms/%@/send/m.room.message", roomID]; MTXRequest *request = [self requestWithPath: path]; request.method = OFHTTPRequestMethodPost; request.body = @{ @"msgtype": @"m.text", @"body": message }; [request performWithBlock: ^ (MTXResponse response, int statusCode, id exception) { if (exception != nil) { block(exception); return; } if (statusCode != 200) { block([MTXSendMessageFailedException exceptionWithMessage: message roomID: roomID statusCode: statusCode response: response client: self]); return; } block(nil); }]; objc_autoreleasePoolPop(pool); } - (void)processRoomsSync: (OFDictionary<OFString *, id> *)rooms { [self processJoinedRooms: rooms[@"join"]]; [self processInvitedRooms: rooms[@"invite"]]; [self processLeftRooms: rooms[@"leave"]]; } - (void)processPresenceSync: (OFDictionary<OFString *, id> *)presence { } - (void)processAccountDataSync: (OFDictionary<OFString *, id> *)accountData { } - (void)processToDeviceSync: (OFDictionary<OFString *, id> *)toDevice { } - (void)processJoinedRooms: (OFDictionary<OFString *, id> *)rooms { if (rooms == nil) return; for (OFString *roomID in rooms) [_storage addJoinedRoom: roomID forUser: _userID]; } - (void)processInvitedRooms: (OFDictionary<OFString *, id> *)rooms { if (rooms == nil) return; } - (void)processLeftRooms: (OFDictionary<OFString *, id> *)rooms { if (rooms == nil) return; for (OFString *roomID in rooms) [_storage removeJoinedRoom: roomID forUser: _userID]; } @end