element-ios/Riot/Modules/Integrations/IntegrationManagerViewContr...

769 lines
26 KiB
Objective-C

/*
Copyright 2019-2024 New Vector Ltd.
Copyright 2017 Vector Creations Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "IntegrationManagerViewController.h"
#import "WidgetManager.h"
#import "GeneratedInterface-Swift.h"
NSString *const kIntegrationManagerMainScreen = nil;
NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ";
@interface IntegrationManagerViewController () <ServiceTermsModalCoordinatorBridgePresenterDelegate>
{
MXSession *mxSession;
NSString *roomId;
NSString *screen;
NSString *widgetId;
NSString *scalarToken;
MXHTTPOperation *operation;
}
@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter;
@property (nonatomic) BOOL isViewAppearedOnce;
@end
@implementation IntegrationManagerViewController
- (instancetype)initForMXSession:(MXSession *)theMXSession inRoom:(NSString *)theRoomId screen:(NSString *)theScreen widgetId:(NSString *)theWidgetId
{
self = [super init];
if (self)
{
mxSession = theMXSession;
roomId = theRoomId;
screen = theScreen;
widgetId = theWidgetId;
}
return self;
}
+ (NSString*)screenForWidget:(NSString*)widgetType
{
return [NSString stringWithFormat:@"type_%@", widgetType];
}
- (void)destroy
{
[super destroy];
[operation cancel];
operation = nil;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.isViewAppearedOnce)
{
self.isViewAppearedOnce = YES;
[self loadData];
}
}
- (void)loadData
{
RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:mxSession];
if (!sharedSettings.hasIntegrationProvisioningEnabled)
{
[self showDisabledIntegrationManagerError];
return;
}
if (!self.URL && !operation)
{
[self startActivityIndicator];
// Make sure we have a scalar token
MXWeakify(self);
operation = [[WidgetManager sharedManager] getScalarTokenForMXSession:mxSession validate:YES success:^(NSString *theScalarToken) {
MXStrongifyAndReturnIfNil(self);
self->operation = nil;
self->scalarToken = theScalarToken;
// Launch the webview on the right modular webapp page
self.URL = [self interfaceUrl];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[IntegraionManagerVS] Cannot open due to missing scalar token. Error: %@", error);
self->operation = nil;
[self stopActivityIndicator];
if ([error.domain isEqualToString:WidgetManagerErrorDomain]
&& error.code == WidgetManagerErrorCodeTermsNotSigned)
{
[self presentTerms];
}
else
{
[self withdrawViewControllerAnimated:YES completion:^{
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
}
}
#pragma mark - Private methods
/**
Build the URL to use in the Modular interface webapp.
*/
- (NSString *)interfaceUrl
{
NSMutableString *url;
NSString *integrationsUiUrl = [[WidgetManager sharedManager] configForUser:mxSession.myUser.userId].uiUrl;
if (scalarToken)
{
url = [NSMutableString stringWithFormat:@"%@?scalar_token=%@&room_id=%@",
integrationsUiUrl,
[MXTools encodeURIComponent:scalarToken],
[MXTools encodeURIComponent:roomId]
];
if (screen)
{
[url appendString:@"&screen="];
[url appendString:[MXTools encodeURIComponent:screen]];
}
if (widgetId)
{
[url appendString:@"&integ_id="];
[url appendString:[MXTools encodeURIComponent:widgetId]];
}
}
return url;
}
#pragma mark - Modular postMessage API implementation
- (void)onPostMessageRequest:(NSString*)requestId data:(NSDictionary*)requestData
{
NSString *roomIdInEvent, *userId, *action;
MXJSONModelSetString(roomIdInEvent, requestData[@"room_id"]);
MXJSONModelSetString(userId, requestData[@"user_id"]);
MXJSONModelSetString(action, requestData[@"action"]);
if ([action isEqualToString:@"close_scalar"])
{
[self withdrawViewControllerAnimated:YES completion:nil];
return;
}
if (!roomIdInEvent)
{
// These APIs don't require roomId
// Get and set user widgets (not associated with a specific room)
// If roomId is specified, it must be validated, so room-based widgets agreed
// handled further down.
if ([@"set_widget" isEqualToString:action])
{
[self setWidget:requestId data:requestData];
return;
}
else if ([@"get_widgets" isEqualToString:action])
{
[self getWidgets:requestId data:requestData];
return;
}
else
{
[self sendError:[VectorL10n widgetIntegrationMissingRoomId] toRequest:requestId];
return;
}
}
if (![roomIdInEvent isEqualToString:roomId])
{
[self sendError:[VectorL10n widgetIntegrationRoomNotVisible:roomIdInEvent] toRequest:requestId];
return;
}
// Get and set room-based widgets
if ([@"set_widget" isEqualToString:action])
{
[self setWidget:requestId data:requestData];
return;
}
else if ([@"get_widgets" isEqualToString:action])
{
[self getWidgets:requestId data:requestData];
return;
}
// These APIs don't require userId
if ([@"join_rules_state" isEqualToString:action])
{
[self getJoinRules:requestId data:requestData];
return;
}
else if ([@"set_plumbing_state" isEqualToString:action])
{
[self setPlumbingState:requestId data:requestData];
return;
}
else if ([@"get_membership_count" isEqualToString:action])
{
[self getMembershipCount:requestId data:requestData];
return;
}
else if ([@"get_room_enc_state" isEqualToString:action])
{
[self getRoomEncState:requestId data:requestData];
return;
}
else if ([@"can_send_event" isEqualToString:action])
{
[self canSendEvent:requestId data:requestData];
return;
}
if (!userId)
{
[self sendError:[VectorL10n widgetIntegrationMissingUserId] toRequest:requestId];
return;
}
if ([@"membership_state" isEqualToString:action])
{
[self getMembershipState:userId request:requestId data:requestData];
}
else if ([@"invite" isEqualToString:action])
{
[self inviteUser:userId request:requestId data:requestData];
}
else if ([@"bot_options" isEqualToString:action])
{
[self getBotOptions:userId request:requestId data:requestData];
}
else if ([@"set_bot_options" isEqualToString:action])
{
[self setBotOptions:userId request:requestId data:requestData];
}
else if ([@"set_bot_power" isEqualToString:action])
{
[self setBotPower:userId request:requestId data:requestData];
}
else
{
MXLogDebug(@"[IntegrationManagerViewControllerVC] Unhandled postMessage event with action %@: %@", action, requestData);
}
}
#pragma mark - Private methods
- (void)roomCheckForRequest:(NSString*)requestId data:(NSDictionary*)requestData onComplete:(void (^)(MXRoom *room, MXRoomState *roomState))onComplete
{
MXRoom *room = [mxSession roomWithRoomId:roomId];
if (room)
{
[room state:^(MXRoomState *roomState) {
onComplete(room, roomState);
}];
}
else
{
[self sendError:[VectorL10n widgetIntegrationRoomNotRecognised] toRequest:requestId];
}
}
- (void)inviteUser:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] Received request to invite %@ into room %@.", userId, roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
MXRoomMember *member = [roomState.members memberWithUserId:userId];
if (member && member.membership == MXMembershipJoin)
{
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
}
else
{
MXWeakify(self);
[room inviteUser:userId success:^{
MXStrongifyAndReturnIfNil(self);
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
[self sendError:[VectorL10n widgetIntegrationNeedToBeAbleToInvite] toRequest:requestId];
}];
}
}];
}
- (void)setWidget:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] Received request to set widget");
NSString *widget_id, *widgetType, *widgetUrl;
NSString *widgetName; // optional
NSDictionary *widgetData ; // optional
BOOL userWidget = NO;
MXJSONModelSetString(widget_id, requestData[@"widget_id"]);
MXJSONModelSetString(widgetType, requestData[@"type"]);
MXJSONModelSetString(widgetUrl, requestData[@"url"]);
MXJSONModelSetString(widgetName, requestData[@"name"]);
MXJSONModelSetDictionary(widgetData, requestData[@"data"]);
MXJSONModelSetBoolean(userWidget, requestData[@"userWidget"]);
if (!widget_id)
{
[self sendError:[VectorL10n widgetIntegrationUnableToCreate] toRequest:requestId];
return;
}
if (!widgetType)
{
[self sendError:[VectorL10n widgetIntegrationUnableToCreate] toRequest:requestId];
return;
}
NSMutableDictionary *widgetEventContent = [NSMutableDictionary dictionary];
if (widgetUrl)
{
widgetEventContent[@"type"] = widgetType;
widgetEventContent[@"url"] = widgetUrl;
if (widgetName)
{
widgetEventContent[@"name"] = widgetName;
}
if (widgetData)
{
widgetEventContent[@"data"] = widgetData;
}
}
// else this is a deletion
__weak __typeof__(self) weakSelf = self;
if (userWidget)
{
// Update the user account data
NSMutableDictionary *userWidgets = [NSMutableDictionary dictionaryWithDictionary:[mxSession.accountData accountDataForEventType:kMXAccountDataTypeUserWidgets]];
// Delete existing widget with ID
[userWidgets removeObjectForKey:widget_id];
// Add new widget / update
if (widgetUrl)
{
userWidgets[widget_id] = @{
@"content": widgetEventContent,
@"sender": mxSession.myUser.userId,
@"state_key": widget_id,
@"type": kWidgetMatrixEventTypeString,
@"id": widget_id,
};
}
[mxSession setAccountData:userWidgets forType:kMXAccountDataTypeUserWidgets success:^{
typeof(self) self = weakSelf;
if (self)
{
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
}
} failure:^(NSError *error) {
typeof(self) self = weakSelf;
if (self)
{
[self sendError:[VectorL10n widgetIntegrationUnableToCreate] toRequest:requestId];
}
}];
}
else
{
// Room widget
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
// TODO: Move to kWidgetMatrixEventTypeString ("m.widget") type but when?
[room sendStateEventOfType:kWidgetModularEventTypeString
content:widgetEventContent
stateKey:widget_id
success:^(NSString *eventId) {
typeof(self) self = weakSelf;
if (self)
{
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
}
}
failure:^(NSError *error) {
typeof(self) self = weakSelf;
if (self)
{
[self sendError:[VectorL10n widgetIntegrationFailedToSendRequest] toRequest:requestId];
}
}];
}];
}
}
- (void)getWidgets:(NSString*)requestId data:(NSDictionary*)requestData
{
MXWeakify(self);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
NSMutableArray<NSDictionary*> *widgetStateEvents = [NSMutableArray array];
NSArray<Widget*> *widgets = [[WidgetManager sharedManager] widgetsInRoom:room withRoomState:roomState];
for (Widget *widget in widgets)
{
[widgetStateEvents addObject:widget.widgetEvent.JSONDictionary];
}
// Add user widgets (not linked to a specific room)
for (Widget *widget in [[WidgetManager sharedManager] userWidgets:self->mxSession])
{
[widgetStateEvents addObject:widget.widgetEvent.JSONDictionary];
}
[self sendNSObjectResponse:widgetStateEvents toRequest:requestId];
}];
}
- (void)getRoomEncState:(NSString*)requestId data:(NSDictionary*)requestData
{
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
[self sendBoolResponse:room.summary.isEncrypted toRequest:requestId];
}];
}
- (void)canSendEvent:(NSString*)requestId data:(NSDictionary*)requestData
{
MXWeakify(self);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
NSString *eventType;
BOOL isState = NO;
if (room.summary.membership != MXMembershipJoin)
{
[self sendError:[VectorL10n widgetIntegrationMustBeInRoom] toRequest:requestId];
return;
}
MXJSONModelSetString(eventType, requestData[@"event_type"]);
MXJSONModelSetBoolean(isState, requestData[@"is_state"]);
MXRoomPowerLevels *powerLevels = roomState.powerLevels;
NSInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:self->mxSession.myUser.userId];
BOOL canSend = NO;
if (isState)
{
canSend = (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:eventType]);
}
else
{
canSend = (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsMessage:eventType]);
}
if (canSend)
{
[self sendBoolResponse:YES toRequest:requestId];
}
else
{
[self sendError:[VectorL10n widgetIntegrationNoPermissionInRoom] toRequest:requestId];
}
}];
}
- (void)getMembershipState:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] membership_state of %@ in room %@ requested.", userId, roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
MXRoomMember *member = [roomState.members memberWithUserId:userId];
[self sendNSObjectResponse:member.originalEvent.content toRequest:requestId];
}];
}
- (void)getJoinRules:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] join_rules of %@ requested.", roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
MXEvent *event = [roomState stateEventsWithType:kMXEventTypeStringRoomJoinRules].lastObject;
[self sendNSObjectResponse:event.JSONDictionary toRequest:requestId];
}];
}
- (void)setPlumbingState:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] Received request to set plumbing state to status %@ in room %@.", requestData[@"status"], roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
NSString *status;
MXJSONModelSetString(status, requestData[@"status"]);
if (status)
{
__weak __typeof__(self) weakSelf = self;
[room sendStateEventOfType:kMXEventTypeStringRoomPlumbing
content:@{
@"status": status
}
stateKey:nil
success:^(NSString *eventId) {
typeof(self) self = weakSelf;
if (self)
{
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
}
}
failure:^(NSError *error) {
typeof(self) self = weakSelf;
if (self)
{
[self sendError:[VectorL10n widgetIntegrationFailedToSendRequest] toRequest:requestId];
}
}];
}
else
{
MXLogDebug(@"[IntegrationManagerVC] setPlumbingState. Error: Plumbing state status should be a string.");
}
}];
}
- (void)getBotOptions:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] Received request to get options for bot %@ in room %@", userId, roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
NSString *stateKey = [NSString stringWithFormat:@"_%@", userId];
NSArray<MXEvent*> *stateEvents = [roomState stateEventsWithType:kMXEventTypeStringRoomBotOptions];
MXEvent *botOptionsEvent;
for (MXEvent *stateEvent in stateEvents)
{
if ([stateEvent.stateKey isEqualToString:stateKey])
{
if (!botOptionsEvent || stateEvent.ageLocalTs > botOptionsEvent.ageLocalTs)
{
botOptionsEvent = stateEvent;
}
}
}
[self sendNSObjectResponse:botOptionsEvent.JSONDictionary toRequest:requestId];
}];
}
- (void)setBotOptions:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] Received request to set options for bot %@ in room %@", userId, roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
NSDictionary *content;
MXJSONModelSetDictionary(content, requestData[@"content"]);
if (content)
{
__weak __typeof__(self) weakSelf = self;
NSString *stateKey = [NSString stringWithFormat:@"_%@", userId];
[room sendStateEventOfType:kMXEventTypeStringRoomBotOptions
content:content
stateKey:stateKey
success:^(NSString *eventId) {
typeof(self) self = weakSelf;
if (self)
{
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
}
}
failure:^(NSError *error) {
typeof(self) self = weakSelf;
if (self)
{
[self sendError:[VectorL10n widgetIntegrationFailedToSendRequest] toRequest:requestId];
}
}];
}
else
{
MXLogDebug(@"[IntegrationManagerVC] setBotOptions. Error: options should be a dict.");
}
}];
}
- (void)setBotPower:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
MXLogDebug(@"[IntegrationManagerVC] Received request to set power level to %@ for bot %@ in room %@.", requestData[@"level"], userId, roomId);
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
NSInteger level = -1;
MXJSONModelSetInteger(level, requestData[@"level"]);
if (level >= 0)
{
__weak __typeof__(self) weakSelf = self;
[room setPowerLevelOfUserWithUserID:userId powerLevel:level success:^{
typeof(self) self = weakSelf;
if (self)
{
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toRequest:requestId];
}
} failure:^(NSError *error) {
typeof(self) self = weakSelf;
if (self)
{
[self sendError:[VectorL10n widgetIntegrationFailedToSendRequest] toRequest:requestId];
}
}];
}
else
{
MXLogDebug(@"[IntegrationManagerVC] setBotPower. Power level must be positive integer.");
[self sendError:[VectorL10n widgetIntegrationPositivePowerLevel] toRequest:requestId];
}
}];
}
- (void)getMembershipCount:(NSString*)requestId data:(NSDictionary*)requestData
{
[self roomCheckForRequest:requestId data:requestData onComplete:^(MXRoom *room, MXRoomState *roomState) {
NSUInteger membershipCount = room.summary.membersCount.joined;
[self sendIntegerResponse:membershipCount toRequest:requestId];
}];
}
#pragma mark - Widget Permission
- (void)checkWidgetPermissionWithCompletion:(void (^)(BOOL granted))completion
{
// The integration manager widget has its own terms
completion(YES);
}
#pragma mark - Disabled Integrations
- (void)showDisabledIntegrationManagerError
{
NSString *message = [VectorL10n widgetIntegrationManagerDisabled];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self withdrawViewControllerAnimated:YES completion:nil];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - Service terms
- (void)presentTerms
{
WidgetManagerConfig *config = [[WidgetManager sharedManager] configForUser:mxSession.myUser.userId];
MXLogDebug(@"[IntegrationManagerVC] presentTerms for %@", config.baseUrl);
ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession baseUrl:config.baseUrl
serviceType:MXServiceTypeIntegrationManager
accessToken:config.scalarToken];
serviceTermsModalCoordinatorBridgePresenter.delegate = self;
[serviceTermsModalCoordinatorBridgePresenter presentFrom:self animated:YES];
self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter;
}
- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
[self loadData];
}];
self.serviceTermsModalCoordinatorBridgePresenter = nil;
}
- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
[self withdrawViewControllerAnimated:YES completion:nil];
}];
self.serviceTermsModalCoordinatorBridgePresenter = nil;
}
- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter
{
self.serviceTermsModalCoordinatorBridgePresenter = nil;
}
@end