From e358cf2e5702e5608f56c514ffa898b51a3dae69 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 21 Jan 2025 00:45:30 +0100 Subject: [PATCH] websocket works --- lib/main.dart | 24 +-- lib/src/app.dart | 46 +++--- lib/src/providers/api_provider.dart | 232 ++++++++-------------------- lib/src/utils.dart | 11 +- lib/src/views/register_view.dart | 4 +- 5 files changed, 108 insertions(+), 209 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e354c5d..df6fbdc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:camera/camera.dart'; -import 'package:provider/provider.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/db_provider.dart'; import 'package:flutter/material.dart'; @@ -10,6 +9,7 @@ import 'src/settings/settings_controller.dart'; import 'src/settings/settings_service.dart'; late DbProvider dbProvider; +late ApiProvider apiProvider; void main() async { // Set up the SettingsController, which will glue user settings to multiple @@ -27,7 +27,7 @@ void main() async { // check if release build or debug build final kDebugMode = true; - Logger.root.level = Level.FINEST; // defaults to Level.INFO + Logger.root.level = Level.ALL; // defaults to Level.INFO Logger.root.onRecord.listen((record) { // if (kDebugMode) { // ignore: avoid_print @@ -36,25 +36,31 @@ void main() async { }); var cameras = await availableCameras(); - t - // Create or open the database - dbProvider = DbProvider(); + + // Create or open the database + dbProvider = DbProvider(); await dbProvider.ready; // Create an option to select different servers. var apiUrl = "ws://api.theconnectapp.de/v-1/"; if (true) { // Overwrite the domain in your local network so you can test the app locally - apiUrl = "ws://9.99.0.6:3030/api/client"; + apiUrl = "ws://10.99.0.6:3030/api/client"; } + apiProvider = ApiProvider(apiUrl: apiUrl, backupApiUrl: null); + + // TODO: Open the connection in the background so the app launch is not delayed. + //await apiProvider.connect(); + // Workmanager.executeTask((task, inputData) async { // await _HomeState().manager(); // print('Background Services are Working!');//This is Working // return true; // }); - runApp(ChangeNotifierProvider( - child: MyApp(settingsController: settingsController, cameras: cameras), - create: (_) => ApiProvider(apiUrl: apiUrl, backupApiUrl: apiUrl))); + // Run the app and pass in the SettingsController. The app listens to the + // SettingsController for changes, then passes it further down to the + // SettingsView. + runApp(MyApp(settingsController: settingsController, cameras: cameras)); } diff --git a/lib/src/app.dart b/lib/src/app.dart index c9cc0fd..22d30fd 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,5 +1,6 @@ import 'package:camera/camera.dart'; import 'package:path/path.dart'; +import 'package:twonly/main.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'views/home_view.dart'; import 'views/register_view.dart'; @@ -26,6 +27,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { Future _isUserCreated = isUserCreated(); + bool _isConnected = false; int redColorOpacity = 0; // Start with dark red bool redColorGoUp = true; bool isConnected = false; @@ -35,6 +37,11 @@ class _MyAppState extends State { super.initState(); // Start the color animation _startColorAnimation(); + apiProvider.connect((isConnected) { + setState(() { + _isConnected = isConnected; + }); + }); } void _startColorAnimation() { @@ -60,7 +67,7 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; - var isConnected = context.watch().isConnected; + // var isConnected = context.watch().isConnected; // Glue the SettingsController to the MaterialApp. // // The ListenableBuilder Widget listens to the SettingsController for changes. @@ -97,29 +104,24 @@ class _MyAppState extends State { home: Stack( children: [ FutureBuilder( - future: context.watch().startBackend(), + future: _isUserCreated, builder: (context, snapshot) { - return FutureBuilder( - future: _isUserCreated, - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data! - ? HomeView( - settingsController: - widget.settingsController, - cameras: widget.cameras) - : RegisterView( - callbackOnSuccess: () { - _isUserCreated = isUserCreated(); - setState(() {}); - }, - ); // Show the red line if not connected - } else { - return Container(); - } - }); + if (snapshot.hasData) { + return snapshot.data! + ? HomeView( + settingsController: widget.settingsController, + cameras: widget.cameras) + : RegisterView( + callbackOnSuccess: () { + _isUserCreated = isUserCreated(); + setState(() {}); + }, + ); // Show the red line if not connected + } else { + return Container(); + } }), - if (!isConnected) + if (!_isConnected) Positioned( top: 3, // Position it at the top left: (screenWidth * 0.5) / 2, // Center it horizontally diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 6177fd2..dcb39a7 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -1,17 +1,15 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:ffi'; -import 'dart:isolate'; import 'dart:math'; import 'dart:typed_data'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:twonly/src/proto/api/client_to_server.pb.dart' as c; +import 'package:twonly/src/proto/api/client_to_server.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; -import 'package:twonly/src/proto/api/server_to_client.pbserver.dart'; import 'package:twonly/src/signal/signal_helper.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -27,170 +25,13 @@ class Result { Result.error(this.error) : value = null; } -enum ExchangeKind { connectionStateChange, sendRequestV0 } - -class Exchange { - Exchange({required this.kind, required this.body, required this.seq}); - final int seq; - final ExchangeKind kind; - final dynamic body; -} - -c.ClientToServer createClientToServerFromHandshake(c.Handshake handshake) { - // Create the V0 message - var v0 = c.V0() - ..seq = Int64(0) // You can set this to the appropriate sequence number - ..handshake = handshake; - - // Create the ClientToServer message - var clientToServer = c.ClientToServer()..v0 = v0; - - return clientToServer; -} - -Result asResult(server.ServerToClient msg) { - if (msg.v0.response.hasOk()) { - return Result.success(msg.v0.response.ok); - } else { - return Result.error(msg.v0.response.error); - } -} - -class BackendIsolatedArgs { - BackendIsolatedArgs( - {required this.apiUrl, - required this.backupApiUrl, - required this.receivePort, - required this.sendPort}); - final String apiUrl; - final String backupApiUrl; - final ReceivePort receivePort; - final SendPort sendPort; -} - -class ApiProvider with ChangeNotifier { +class ApiProvider { ApiProvider({required this.apiUrl, required this.backupApiUrl}); - final String apiUrl; - final String backupApiUrl; - bool _isConnected = false; - final Map _sendRequestV0Resp = HashMap(); - final log = Logger("twonly::ApiProvider"); - - final ReceivePort _receivePort = ReceivePort(); - SendPort? _sendPort; - - Future _waitForResponse(int seq) async { - final startTime = DateTime.now(); - - final timeout = Duration(seconds: 5); - - while (true) { - if (_sendRequestV0Resp[seq] != null) { - final tmp = _sendRequestV0Resp[seq]; - _sendRequestV0Resp.remove(seq); - return tmp; - } - if (DateTime.now().difference(startTime) > timeout) { - log.shout("Timeout for message $seq"); - return null; - } - await Future.delayed(Duration(milliseconds: 10)); - } - } - - static void _startBackendIsolated(BackendIsolatedArgs args) async { - final backend = - Backend(apiUrl: args.apiUrl, backupApiUrl: args.backupApiUrl); - - await backend.connect(); - - args.receivePort.listen((msg) async { - switch (msg.kind) { - case ExchangeKind.sendRequestV0: - final resp = await backend._sendRequestV0(msg.body); - args.sendPort.send(Exchange( - kind: ExchangeKind.sendRequestV0, body: resp, seq: msg.seq)); - break; - default: - } - }); - args.sendPort.send( - Exchange(kind: ExchangeKind.connectionStateChange, body: true, seq: 0)); - } - - Future startBackend() async { - ReceivePort port = ReceivePort(); - _sendPort = port.sendPort; - - await Isolate.spawn( - _startBackendIsolated, - BackendIsolatedArgs( - sendPort: _receivePort.sendPort, - receivePort: port, - apiUrl: apiUrl, - backupApiUrl: backupApiUrl)); - - _receivePort.listen((msg) { - switch (msg.kind) { - case ExchangeKind.sendRequestV0: - _sendRequestV0Resp[msg.seq] = msg.body; - // final resp = await backend._sendRequestV0(msg.body); - // args.sendPort.send(Exchange( - // kind: ExchangeKind.sendRequestV0, body: resp, seq: msg.seq)); - break; - case ExchangeKind.connectionStateChange: - _isConnected = msg.body; - default: - } - }); - return true; - } - - bool get isConnected => _isConnected; - - Future register(String username, String? inviteCode) async { - if (_sendPort == null) return Result.error("Unknown error"); - final reqSignal = await SignalHelper.getRegisterData(); - - if (reqSignal == null) { - return Result.error( - "There was an fatal error. Try reinstalling the app."); - } - - var register = c.Handshake_Register() - ..username = username - ..publicIdentityKey = reqSignal["identityKey"] - ..signedPrekey = reqSignal["signedPreKey"]?["key"] - ..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"] - ..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]); - - if (inviteCode != null && inviteCode != "") { - register.inviteCode = inviteCode; - } - // Create the Handshake message - var handshake = c.Handshake()..register = register; - var req = createClientToServerFromHandshake(handshake); - - var seq = Random().nextInt(4294967296); - final tmp = Exchange(seq: seq, kind: ExchangeKind.sendRequestV0, body: req); - _sendPort!.send(tmp); - - final resp = await _waitForResponse(seq); - - if (resp == null) { - return Result.error("Server is not reachable!"); - } - return asResult(resp); - } -} - -class Backend { - Backend({required this.apiUrl, required this.backupApiUrl}); - final String apiUrl; final String? backupApiUrl; - final log = Logger("twonly::backend"); + final log = Logger("connect::ApiProvider"); + final HashMap> _callbacks = HashMap(); final HashMap messagesV0 = HashMap(); @@ -213,18 +54,20 @@ class Backend { } } - Future connect() async { + Future connect(Function(bool)? callBack) async { print("Trying to connect to the backend $apiUrl!"); if (_channel != null && _channel!.closeCode != null) { return true; } log.info("Trying to connect to the backend $apiUrl!"); if (await _connectTo(apiUrl)) { + if (callBack != null) callBack(true); return true; } if (backupApiUrl != null) { log.info("Trying to connect to the backup backend $backupApiUrl!"); if (await _connectTo(backupApiUrl!)) { + if (callBack != null) callBack(true); return true; } } @@ -246,6 +89,13 @@ class Backend { } } + void addNotifier(String messageType, Function callBackFunction) { + if (!_callbacks.containsKey(messageType)) { + _callbacks[messageType] = []; + } + _callbacks[messageType]!.add(callBackFunction); + } + // TODO: There must be a smarter move to do that :/ Future _waitForResponse(Int64 seq) async { final startTime = DateTime.now(); @@ -266,8 +116,7 @@ class Backend { } } - Future _sendRequestV0( - c.ClientToServer request) async { + Future _sendRequestV0(ClientToServer request) async { var seq = Int64(Random().nextInt(4294967296)); while (messagesV0.containsKey(seq)) { seq = Int64(Random().nextInt(4294967296)); @@ -278,7 +127,7 @@ class Backend { log.info("Check if is connected?"); // check if it is connected to the backend. if not try to reconnect. - if (!await connect()) { + if (!await connect(null)) { return null; } @@ -287,6 +136,18 @@ class Backend { return await _waitForResponse(seq); } + ClientToServer createClientToServerFromHandshake(Handshake handshake) { + // Create the V0 message + var v0 = V0() + ..seq = Int64(0) // You can set this to the appropriate sequence number + ..handshake = handshake; + + // Create the ClientToServer message + var clientToServer = ClientToServer()..v0 = v0; + + return clientToServer; + } + static String getLocalizedString(BuildContext context, ErrorCode code) { switch (code.toString()) { case "Unknown": @@ -319,4 +180,41 @@ class Backend { return code.toString(); // Fallback for unrecognized keys } } + + Result _asResult(server.ServerToClient msg) { + if (msg.v0.response.hasOk()) { + return Result.success(msg.v0.response.ok); + } else { + return Result.error(msg.v0.response.error); + } + } + + Future register(String username, String? inviteCode) async { + final reqSignal = await SignalHelper.getRegisterData(); + + if (reqSignal == null) { + return Result.error( + "There was an fatal error. Try reinstalling the app."); + } + + var register = Handshake_Register() + ..username = username + ..publicIdentityKey = reqSignal["identityKey"] + ..signedPrekey = reqSignal["signedPreKey"]?["key"] + ..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"] + ..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]); + + if (inviteCode != null && inviteCode != "") { + register.inviteCode = inviteCode; + } + // Create the Handshake message + var handshake = Handshake()..register = register; + var req = createClientToServerFromHandshake(handshake); + + final resp = await _sendRequestV0(req); + if (resp == null) { + return Result.error("Server is not reachable!"); + } + return _asResult(resp); + } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 852bffd..f7af799 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,11 +1,9 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/signal/signal_helper.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'model/user_data_json.dart'; -import 'package:provider/provider.dart'; import 'package:logging/logging.dart'; // Just a helper function to get the secure storage @@ -46,18 +44,13 @@ Future deleteLocalUserData() async { return true; } -Future createNewUser( - BuildContext context, String username, String inviteCode) async { +Future createNewUser(String username, String inviteCode) async { final storage = getSecureStorage(); - if (!context.mounted) { - return Result.error("not mounted"); - } await SignalHelper.createIfNotExistsSignalIdentity(); // TODO: API call to server to check username and inviteCode - // final res = await apiProvider.register(username, inviteCode); - final res = await context.watch().register(username, inviteCode); + final res = await apiProvider.register(username, inviteCode); if (res.isSuccess) { print("Got user_id ${res.value}"); diff --git a/lib/src/views/register_view.dart b/lib/src/views/register_view.dart index 319c8ca..6577594 100644 --- a/lib/src/views/register_view.dart +++ b/lib/src/views/register_view.dart @@ -120,7 +120,7 @@ class _RegisterViewState extends State { setState(() { _isTryingToRegister = true; }); - final res = await createNewUser(context, + final res = await createNewUser( usernameController.text, inviteCodeController.text); setState(() { _isTryingToRegister = false; @@ -130,7 +130,7 @@ class _RegisterViewState extends State { return; } final errMsg = - Backend.getLocalizedString(context, res.error); + ApiProvider.getLocalizedString(context, res.error); showAlertDialog(context, "Oh no!", errMsg); }, style: ButtonStyle(