websocket works

This commit is contained in:
otsmr 2025-01-21 00:45:30 +01:00
parent 3b3c5e61b9
commit e358cf2e57
5 changed files with 108 additions and 209 deletions

View file

@ -1,5 +1,4 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart'; import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,6 +9,7 @@ import 'src/settings/settings_controller.dart';
import 'src/settings/settings_service.dart'; import 'src/settings/settings_service.dart';
late DbProvider dbProvider; late DbProvider dbProvider;
late ApiProvider apiProvider;
void main() async { void main() async {
// Set up the SettingsController, which will glue user settings to multiple // 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 // check if release build or debug build
final kDebugMode = true; 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) { Logger.root.onRecord.listen((record) {
// if (kDebugMode) { // if (kDebugMode) {
// ignore: avoid_print // ignore: avoid_print
@ -36,25 +36,31 @@ void main() async {
}); });
var cameras = await availableCameras(); var cameras = await availableCameras();
t
// Create or open the database // Create or open the database
dbProvider = DbProvider(); dbProvider = DbProvider();
await dbProvider.ready; await dbProvider.ready;
// Create an option to select different servers. // Create an option to select different servers.
var apiUrl = "ws://api.theconnectapp.de/v-1/"; var apiUrl = "ws://api.theconnectapp.de/v-1/";
if (true) { if (true) {
// Overwrite the domain in your local network so you can test the app locally // 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 { // Workmanager.executeTask((task, inputData) async {
// await _HomeState().manager(); // await _HomeState().manager();
// print('Background Services are Working!');//This is Working // print('Background Services are Working!');//This is Working
// return true; // return true;
// }); // });
runApp(ChangeNotifierProvider<ApiProvider>( // Run the app and pass in the SettingsController. The app listens to the
child: MyApp(settingsController: settingsController, cameras: cameras), // SettingsController for changes, then passes it further down to the
create: (_) => ApiProvider(apiUrl: apiUrl, backupApiUrl: apiUrl))); // SettingsView.
runApp(MyApp(settingsController: settingsController, cameras: cameras));
} }

View file

@ -1,5 +1,6 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'views/home_view.dart'; import 'views/home_view.dart';
import 'views/register_view.dart'; import 'views/register_view.dart';
@ -26,6 +27,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
Future<bool> _isUserCreated = isUserCreated(); Future<bool> _isUserCreated = isUserCreated();
bool _isConnected = false;
int redColorOpacity = 0; // Start with dark red int redColorOpacity = 0; // Start with dark red
bool redColorGoUp = true; bool redColorGoUp = true;
bool isConnected = false; bool isConnected = false;
@ -35,6 +37,11 @@ class _MyAppState extends State<MyApp> {
super.initState(); super.initState();
// Start the color animation // Start the color animation
_startColorAnimation(); _startColorAnimation();
apiProvider.connect((isConnected) {
setState(() {
_isConnected = isConnected;
});
});
} }
void _startColorAnimation() { void _startColorAnimation() {
@ -60,7 +67,7 @@ class _MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width; double screenWidth = MediaQuery.of(context).size.width;
var isConnected = context.watch<ApiProvider>().isConnected; // var isConnected = context.watch<ApiProvider>().isConnected;
// Glue the SettingsController to the MaterialApp. // Glue the SettingsController to the MaterialApp.
// //
// The ListenableBuilder Widget listens to the SettingsController for changes. // The ListenableBuilder Widget listens to the SettingsController for changes.
@ -97,29 +104,24 @@ class _MyAppState extends State<MyApp> {
home: Stack( home: Stack(
children: [ children: [
FutureBuilder<bool>( FutureBuilder<bool>(
future: context.watch<ApiProvider>().startBackend(), future: _isUserCreated,
builder: (context, snapshot) { builder: (context, snapshot) {
return FutureBuilder<bool>( if (snapshot.hasData) {
future: _isUserCreated, return snapshot.data!
builder: (context, snapshot) { ? HomeView(
if (snapshot.hasData) { settingsController: widget.settingsController,
return snapshot.data! cameras: widget.cameras)
? HomeView( : RegisterView(
settingsController: callbackOnSuccess: () {
widget.settingsController, _isUserCreated = isUserCreated();
cameras: widget.cameras) setState(() {});
: RegisterView( },
callbackOnSuccess: () { ); // Show the red line if not connected
_isUserCreated = isUserCreated(); } else {
setState(() {}); return Container();
}, }
); // Show the red line if not connected
} else {
return Container();
}
});
}), }),
if (!isConnected) if (!_isConnected)
Positioned( Positioned(
top: 3, // Position it at the top top: 3, // Position it at the top
left: (screenWidth * 0.5) / 2, // Center it horizontally left: (screenWidth * 0.5) / 2, // Center it horizontally

View file

@ -1,17 +1,15 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.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/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.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:twonly/src/signal/signal_helper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
@ -27,170 +25,13 @@ class Result<T, E> {
Result.error(this.error) : value = null; Result.error(this.error) : value = null;
} }
enum ExchangeKind { connectionStateChange, sendRequestV0 } class ApiProvider {
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 {
ApiProvider({required this.apiUrl, required this.backupApiUrl}); ApiProvider({required this.apiUrl, required this.backupApiUrl});
final String apiUrl;
final String backupApiUrl;
bool _isConnected = false;
final Map<int, ServerToClient> _sendRequestV0Resp = HashMap();
final log = Logger("twonly::ApiProvider");
final ReceivePort _receivePort = ReceivePort();
SendPort? _sendPort;
Future<ServerToClient?> _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<bool> 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<Result> 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 apiUrl;
final String? backupApiUrl; final String? backupApiUrl;
final log = Logger("twonly::backend"); final log = Logger("connect::ApiProvider");
final HashMap<String, List<Function>> _callbacks = HashMap();
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap(); final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
@ -213,18 +54,20 @@ class Backend {
} }
} }
Future<bool> connect() async { Future<bool> connect(Function(bool)? callBack) async {
print("Trying to connect to the backend $apiUrl!"); print("Trying to connect to the backend $apiUrl!");
if (_channel != null && _channel!.closeCode != null) { if (_channel != null && _channel!.closeCode != null) {
return true; return true;
} }
log.info("Trying to connect to the backend $apiUrl!"); log.info("Trying to connect to the backend $apiUrl!");
if (await _connectTo(apiUrl)) { if (await _connectTo(apiUrl)) {
if (callBack != null) callBack(true);
return true; return true;
} }
if (backupApiUrl != null) { if (backupApiUrl != null) {
log.info("Trying to connect to the backup backend $backupApiUrl!"); log.info("Trying to connect to the backup backend $backupApiUrl!");
if (await _connectTo(backupApiUrl!)) { if (await _connectTo(backupApiUrl!)) {
if (callBack != null) callBack(true);
return 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 :/ // TODO: There must be a smarter move to do that :/
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async { Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
final startTime = DateTime.now(); final startTime = DateTime.now();
@ -266,8 +116,7 @@ class Backend {
} }
} }
Future<server.ServerToClient?> _sendRequestV0( Future<server.ServerToClient?> _sendRequestV0(ClientToServer request) async {
c.ClientToServer request) async {
var seq = Int64(Random().nextInt(4294967296)); var seq = Int64(Random().nextInt(4294967296));
while (messagesV0.containsKey(seq)) { while (messagesV0.containsKey(seq)) {
seq = Int64(Random().nextInt(4294967296)); seq = Int64(Random().nextInt(4294967296));
@ -278,7 +127,7 @@ class Backend {
log.info("Check if is connected?"); log.info("Check if is connected?");
// check if it is connected to the backend. if not try to reconnect. // check if it is connected to the backend. if not try to reconnect.
if (!await connect()) { if (!await connect(null)) {
return null; return null;
} }
@ -287,6 +136,18 @@ class Backend {
return await _waitForResponse(seq); 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) { static String getLocalizedString(BuildContext context, ErrorCode code) {
switch (code.toString()) { switch (code.toString()) {
case "Unknown": case "Unknown":
@ -319,4 +180,41 @@ class Backend {
return code.toString(); // Fallback for unrecognized keys 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<Result> 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);
}
} }

View file

@ -1,11 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/signal/signal_helper.dart'; import 'package:twonly/src/signal/signal_helper.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'model/user_data_json.dart'; import 'model/user_data_json.dart';
import 'package:provider/provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
// Just a helper function to get the secure storage // Just a helper function to get the secure storage
@ -46,18 +44,13 @@ Future<bool> deleteLocalUserData() async {
return true; return true;
} }
Future<Result> createNewUser( Future<Result> createNewUser(String username, String inviteCode) async {
BuildContext context, String username, String inviteCode) async {
final storage = getSecureStorage(); final storage = getSecureStorage();
if (!context.mounted) {
return Result.error("not mounted");
}
await SignalHelper.createIfNotExistsSignalIdentity(); await SignalHelper.createIfNotExistsSignalIdentity();
// TODO: API call to server to check username and inviteCode // TODO: API call to server to check username and inviteCode
// final res = await apiProvider.register(username, inviteCode); final res = await apiProvider.register(username, inviteCode);
final res = await context.watch<ApiProvider>().register(username, inviteCode);
if (res.isSuccess) { if (res.isSuccess) {
print("Got user_id ${res.value}"); print("Got user_id ${res.value}");

View file

@ -120,7 +120,7 @@ class _RegisterViewState extends State<RegisterView> {
setState(() { setState(() {
_isTryingToRegister = true; _isTryingToRegister = true;
}); });
final res = await createNewUser(context, final res = await createNewUser(
usernameController.text, inviteCodeController.text); usernameController.text, inviteCodeController.text);
setState(() { setState(() {
_isTryingToRegister = false; _isTryingToRegister = false;
@ -130,7 +130,7 @@ class _RegisterViewState extends State<RegisterView> {
return; return;
} }
final errMsg = final errMsg =
Backend.getLocalizedString(context, res.error); ApiProvider.getLocalizedString(context, res.error);
showAlertDialog(context, "Oh no!", errMsg); showAlertDialog(context, "Oh no!", errMsg);
}, },
style: ButtonStyle( style: ButtonStyle(