213 lines
6.1 KiB
Dart
213 lines
6.1 KiB
Dart
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:html';
|
|
import 'dart:convert';
|
|
import 'dart:async';
|
|
|
|
class ChatInfo {
|
|
int id;
|
|
String name;
|
|
|
|
ChatInfo({required this.id, required this.name});
|
|
}
|
|
|
|
class Backend {
|
|
static WebSocket? _socket;
|
|
static String? _url;
|
|
static Map<int, Completer> _pendingActions = {};
|
|
static Map<int, StreamController<Map<String, dynamic>>> _chatControllers = {};
|
|
static int _nextNonce = 0; // Tracking nonce for each action
|
|
static StreamController<Map<String, dynamic>> _packetStreamController = StreamController.broadcast();
|
|
|
|
// Cached Data
|
|
static List<ChatInfo>? _chats;
|
|
|
|
// Authentication
|
|
static ({String username, String password})? _credentials;
|
|
static Completer<bool>? _loginCompleter;
|
|
static Completer? _credentialsCompleter;
|
|
|
|
static void setup(String url) {
|
|
_url = url;
|
|
connect();
|
|
}
|
|
|
|
static Future<void> signup(String username, String email, String password) async {
|
|
final response = await http.post(Uri.parse("$_url/user/signup"),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: jsonEncode({
|
|
'username': username,
|
|
'email': email,
|
|
'password': password,
|
|
})
|
|
);
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw Exception("Signup Status Code ${response.statusCode}");
|
|
}
|
|
}
|
|
|
|
static Future<bool> auth(String username, String password) {
|
|
if (_credentials != null) {
|
|
print("Cannot authenticate twice");
|
|
return Future.value(false);
|
|
}
|
|
_credentials = (username: username, password: password);
|
|
if (_credentialsCompleter != null) {
|
|
_credentialsCompleter!.complete(_credentials);
|
|
_credentialsCompleter = null;
|
|
}
|
|
_loginCompleter = Completer();
|
|
return _loginCompleter!.future;
|
|
}
|
|
|
|
static void connect() {
|
|
_socket = WebSocket(_url! + "/ws");
|
|
_socket!.onOpen.listen((event) async {
|
|
print('Connected to the WebSocket server');
|
|
Backend.invalidateCache(chat: true);
|
|
|
|
print('Logging in...');
|
|
// secure the listener for the response before sending credentials
|
|
var responseFuture = nextIncomingMessage();
|
|
if (_credentials != null) {
|
|
_socket!.send(json.encode({'username': _credentials!.username, 'password': _credentials!.password}));
|
|
} else {
|
|
_credentialsCompleter = Completer();
|
|
_credentialsCompleter!.future.then((credentials) {
|
|
_socket!.send(json.encode({'username': credentials.username, 'password': credentials.password}));
|
|
});
|
|
}
|
|
var response = await responseFuture;
|
|
if (response['error'] == null) {
|
|
_loginCompleter?.complete(true);
|
|
} else {
|
|
_loginCompleter?.complete(false);
|
|
_socket!.close(); // Retry
|
|
}
|
|
_loginCompleter = null;
|
|
});
|
|
_socket!.onError.listen((event) {
|
|
print('Error: $event');
|
|
_reconnect();
|
|
});
|
|
_socket!.onClose.listen((event) {
|
|
print('Disconnected. Attempting to reconnect...');
|
|
_reconnect();
|
|
});
|
|
_socket!.onMessage.listen((event) {
|
|
_handleIncomingMessage(event.data);
|
|
});
|
|
}
|
|
|
|
static void disconnect() {
|
|
_socket?.close();
|
|
}
|
|
|
|
static void _reconnect() {
|
|
Future.delayed(Duration(seconds: 1), () {
|
|
connect();
|
|
});
|
|
}
|
|
|
|
static Future<dynamic> sendAction(String action, Map<String, dynamic> body) {
|
|
int nonce = _nextNonce++;
|
|
Completer completer = Completer();
|
|
_pendingActions[nonce] = completer;
|
|
|
|
final packet = {
|
|
'action': action,
|
|
'nonce': nonce,
|
|
'body': body,
|
|
};
|
|
_socket!.send(json.encode(packet));
|
|
|
|
return completer.future.then((response) {
|
|
_pendingActions.remove(nonce); // Clean up the mapping
|
|
return response;
|
|
});
|
|
}
|
|
|
|
static void _handleIncomingMessage(String data) {
|
|
var jsonData = json.decode(data);
|
|
print("Incoming Packet: $jsonData");
|
|
|
|
if (jsonData['nonce'] != null) {
|
|
int nonce = jsonData['nonce'];
|
|
if (_pendingActions.containsKey(nonce)) {
|
|
// Complete the response
|
|
_pendingActions[nonce]!.complete(jsonData);
|
|
} else {
|
|
print("Orphan response for action nonce $nonce");
|
|
}
|
|
} else if (jsonData['action'] == 'chat-message') {
|
|
int chatId = jsonData['chatId'];
|
|
final controller = _chatControllers[chatId];
|
|
if (controller != null && !controller.isClosed) {
|
|
controller.add(jsonData);
|
|
} else {
|
|
print("No active listeners for chatId $chatId");
|
|
}
|
|
} else {
|
|
if (jsonData['error'] != null) {
|
|
print("Error: ${jsonData['error']}");
|
|
}
|
|
_packetStreamController.add(jsonData);
|
|
}
|
|
}
|
|
|
|
static Future<Map<String, dynamic>> nextIncomingMessage() {
|
|
return _packetStreamController.stream.first;
|
|
}
|
|
|
|
static Stream<Map<String, dynamic>> chatMessages(int chatId) {
|
|
if (!_chatControllers.containsKey(chatId) || _chatControllers[chatId]!.isClosed) {
|
|
_chatControllers[chatId] = StreamController<Map<String, dynamic>>.broadcast();
|
|
}
|
|
return _chatControllers[chatId]!.stream;
|
|
}
|
|
|
|
static void removeChatListener(int chatId) {
|
|
final controller = _chatControllers.remove(chatId);
|
|
if (controller != null && !controller.isClosed) {
|
|
controller.close();
|
|
}
|
|
}
|
|
|
|
static Future<List<ChatInfo>> getChats () async {
|
|
if (_chats == null) {
|
|
final response = await Backend.sendAction("get-chats", {});
|
|
List<dynamic> chatObjects = response['chats'];
|
|
_chats = chatObjects
|
|
.map((msg) => ChatInfo(
|
|
id: msg['id'],
|
|
name: msg['name']?.isNotEmpty == true ? msg['name'] : '[Chat ${msg['id']}]',
|
|
))
|
|
.toList();
|
|
}
|
|
return _chats!;
|
|
}
|
|
|
|
static Future<ChatInfo?> getChat (int chatId) async {
|
|
final chats = await getChats();
|
|
try {
|
|
return chats.firstWhere((chat) => chat.id == chatId);
|
|
} on StateError {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static void invalidateCache ({bool chat = false}) {
|
|
if (chat) { _chats = null; }
|
|
}
|
|
|
|
static void ping() {
|
|
if (_socket!= null && _socket!.readyState == WebSocket.OPEN) {
|
|
final data = json.encode({'path': 'ping'});
|
|
_socket!.send(data);
|
|
print('Ping sent: $data');
|
|
}
|
|
}
|
|
} |