Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-3157 Implement web socket push #3168

Merged
merged 5 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contact/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -652,10 +652,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
resolved-ref: b75666ba1ac351c7e7be7a1a8f95e58a860505fc
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
20 changes: 20 additions & 0 deletions docs/adr/0053-web-socket-data-synchronization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 53. Web socket data synchronization

Date: 2024-11-10

## Status

Accepted

## Context

- Currently Twake Mail web use Firebase Cloud Messaging to sync data on real time
- JMAP already implemented web socket push, which is more optimized for web

## Decision

- Web socket is implemented for real time update data for Twake Mail web

## Consequences

- Twake Mail web now no longer depends on Firebase Cloud Messaging, using web socket to update users' latest data
4 changes: 2 additions & 2 deletions email_recovery/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
resolved-ref: b75666ba1ac351c7e7be7a1a8f95e58a860505fc
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
8 changes: 4 additions & 4 deletions fcm/lib/model/type_name.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import 'package:equatable/equatable.dart';

class TypeName with EquatableMixin {
static final mailboxType = TypeName('Mailbox');
static final emailType = TypeName('Email');
static final emailDelivery = TypeName('EmailDelivery');
static const mailboxType = TypeName('Mailbox');
static const emailType = TypeName('Email');
static const emailDelivery = TypeName('EmailDelivery');

final String value;

TypeName(this.value);
const TypeName(this.value);

@override
List<Object?> get props => [value];
Expand Down
4 changes: 2 additions & 2 deletions fcm/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
resolved-ref: b75666ba1ac351c7e7be7a1a8f95e58a860505fc
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
4 changes: 2 additions & 2 deletions forward/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
resolved-ref: b75666ba1ac351c7e7be7a1a8f95e58a860505fc
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
6 changes: 2 additions & 4 deletions lib/features/base/action/ui_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ abstract class Action with EquatableMixin {}

abstract class UIAction extends Action {}

abstract class FcmAction extends Action {}

abstract class FcmStateChangeAction extends FcmAction {
abstract class PushNotificationStateChangeAction extends Action {
final TypeName typeName;
final jmap.State newState;

FcmStateChangeAction(this.typeName, this.newState);
PushNotificationStateChangeAction(this.typeName, this.newState);
dab246 marked this conversation as resolved.
Show resolved Hide resolved
}
29 changes: 28 additions & 1 deletion lib/features/base/base_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import 'package:forward/forward/capability_forward.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/account/authentication_type.dart';
import 'package:model/model.dart';
import 'package:rule_filter/rule_filter/capability_rule_filter.dart';
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart';
Expand All @@ -44,14 +45,17 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oi
import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/bindings/email_rules_interactor_bindings.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindings/forwarding_interactors_bindings.dart';
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart';
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart';
import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_firebase_registration_state.dart';
import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_firebase_registration_state.dart';
import 'package:tmail_ui_user/features/push_notification/domain/usecases/destroy_firebase_registration_interactor.dart';
import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_firebase_registration_interactor.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/config/fcm_configuration.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_token_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/web_socket_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart';
Expand Down Expand Up @@ -372,6 +376,29 @@ abstract class BaseController extends GetxController
}
}

void injectWebSocket(Session? session, AccountId? accountId) {
try {
requireCapability(
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
session!,
accountId!,
[
CapabilityIdentifier.jmapWebSocket,
CapabilityIdentifier.jmapWebSocketTicket
]
);
final wsCapability = session.getCapabilityProperties<WebSocketCapability>(
accountId,
CapabilityIdentifier.jmapWebSocket);
if (wsCapability?.supportsPush != true) {
throw WebSocketPushNotSupportedException();
}
WebSocketInteractorBindings().dependencies();
WebSocketController.instance.initialize(accountId: accountId, session: session);
} catch(e) {
logError('$runtimeType::injectWebSocket(): exception: $e');
}
}

AuthenticationType get authenticationType => authorizationInterceptors.authenticationType;

bool get isAuthenticatedWithOidc => authenticationType == AuthenticationType.oidc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,11 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
injectAutoCompleteBindings(session, currentAccountId);
injectRuleFilterBindings(session, currentAccountId);
injectVacationBindings(session, currentAccountId);
injectFCMBindings(session, currentAccountId);
if (PlatformInfo.isWeb) {
injectWebSocket(session, currentAccountId);
} else {
injectFCMBindings(session, currentAccountId);
}

_getVacationResponse();
spamReportController.getSpamReportStateAction();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

abstract class WebSocketDatasource {
Future<WebSocketChannel> getWebSocketChannel(
Session session,
AccountId accountId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

import 'dart:async';

import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart';
import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart';
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

class WebSocketDatasourceImpl implements WebSocketDatasource {
final WebSocketApi _webSocketApi;
final ExceptionThrower _exceptionThrower;

const WebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower);

@override
Future<WebSocketChannel> getWebSocketChannel(Session session, AccountId accountId) {
return Future.sync(() async {
_verifyWebSocketCapabilities(session, accountId);
final webSocketTicket = await _webSocketApi.getWebSocketTicket(session, accountId);
final webSocketUri = _getWebSocketUri(session, accountId);
final webSocketChannel = WebSocketChannel.connect(
Uri.parse('$webSocketUri?ticket=$webSocketTicket'),
protocols: ["jmap"],
);

await webSocketChannel.ready;

return webSocketChannel;
}).catchError(_exceptionThrower.throwException);
}

void _verifyWebSocketCapabilities(Session session, AccountId accountId) {
if (!CapabilityIdentifier.jmapWebSocket.isSupported(session, accountId)
|| !CapabilityIdentifier.jmapWebSocketTicket.isSupported(session, accountId)
|| session.getCapabilityProperties<WebSocketCapability>(
accountId,
CapabilityIdentifier.jmapWebSocket)?.supportsPush != true
) {
throw WebSocketPushNotSupportedException();
}
}

Uri _getWebSocketUri(Session session, AccountId accountId) {
final webSocketCapability = session.getCapabilityProperties<WebSocketCapability>(
accountId,
CapabilityIdentifier.jmapWebSocket);
if (webSocketCapability?.supportsPush != true) {
throw WebSocketPushNotSupportedException();
}
final webSocketUri = webSocketCapability?.url;
if (webSocketUri == null) throw WebSocketUriUnavailableException();

return webSocketUri;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:tmail_ui_user/features/push_notification/domain/model/web_socket_action.dart';

part 'connect_web_socket_message.g.dart';

@JsonSerializable()
class ConnectWebSocketMessage with EquatableMixin {
@JsonKey(name: 'action')
final WebSocketAction webSocketAction;
@JsonKey(name: 'url')
final String webSocketUrl;
@JsonKey(name: 'ticket')
final String webSocketTicket;

ConnectWebSocketMessage({
required this.webSocketAction,
required this.webSocketUrl,
required this.webSocketTicket,
});

factory ConnectWebSocketMessage.fromJson(Map<String, dynamic> json)
=> _$ConnectWebSocketMessageFromJson(json);
Map<String, dynamic> toJson() => _$ConnectWebSocketMessageToJson(this);

@override
List<Object?> get props => [webSocketAction, webSocketUrl, webSocketTicket];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:tmail_ui_user/features/push_notification/data/model/web_socket_request.dart';

class WebSocketEchoRequest extends WebSocketRequest {
static const String type = 'Request';
static const String id = 'R1';
static final CapabilityIdentifier usingCapability = CapabilityIdentifier.jmapCore;
static const String method = 'Core/echo';

@override
Map<String, dynamic> toJson() {
return {
'@type': type,
'id': id,
'using': [usingCapability.value.toString()],
'methodCalls': [[method, {}, 'c0']],
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:fcm/model/type_name.dart';

class WebSocketPushEnableRequest {
static const String type = 'WebSocketPushEnable';

static Map<String, dynamic> toJson({
required List<TypeName> dataTypes,
}) {
return {
'@type': type,
'dataTypes': dataTypes.map((typeName) => typeName.value).toList(),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
abstract class WebSocketRequest {
const WebSocketRequest();

Map<String, dynamic> toJson();
}
33 changes: 33 additions & 0 deletions lib/features/push_notification/data/model/web_socket_ticket.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'web_socket_ticket.g.dart';

@JsonSerializable(includeIfNull: false)
class WebSocketTicket with EquatableMixin {
final String? value;
final String? clientAddress;
final DateTime? generatedOn;
final DateTime? validUntil;
final String? username;

WebSocketTicket({
required this.value,
required this.clientAddress,
required this.generatedOn,
required this.validUntil,
required this.username,
});

factory WebSocketTicket.fromJson(Map<String, dynamic> json) => _$WebSocketTicketFromJson(json);
Map<String, dynamic> toJson() => _$WebSocketTicketToJson(this);

@override
List<Object?> get props => [
value,
clientAddress,
generatedOn,
validUntil,
username,
];
}
Loading
Loading