import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:csv/csv.dart'; const String dbName = "TetraStats.db"; const String tetrioUsersTable = "tetrioUsers"; const String tetrioUsersToTrackTable = "tetrioUsersToTrack"; const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces"; const String idCol = "id"; const String replayID = "replayId"; const String nickCol = "nickname"; const String timestamp = "timestamp"; const String endContext1 = "endContext1"; const String endContext2 = "endContext2"; const String statesCol = "jsonStates"; const String player1id = "player1id"; const String player2id = "player2id"; const String createTetrioUsersTable = ''' CREATE TABLE IF NOT EXISTS "tetrioUsers" ( "id" TEXT UNIQUE, "nickname" TEXT, "jsonStates" TEXT, PRIMARY KEY("id") );'''; const String createTetrioUsersToTrack = ''' CREATE TABLE IF NOT EXISTS "tetrioUsersToTrack" ( "id" TEXT NOT NULL UNIQUE, PRIMARY KEY("ID") ) '''; const String createTetrioTLRecordsTable = ''' CREATE TABLE IF NOT EXISTS "tetrioAlphaLeagueMathces" ( "id" TEXT NOT NULL UNIQUE, "replayId" TEXT, "player1id" TEXT, "player2id" TEXT, "timestamp" TEXT, "endContext1" TEXT, "endContext2" TEXT, PRIMARY KEY("id") ) '''; class TetrioService extends DB { Map> _players = {}; final Map _playersCache = {}; final Map> _recordsCache = {}; final Map _leaderboardsCache = {}; final Map _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; late final StreamController>> _tetrioStreamController; TetrioService._sharedInstance() { _tetrioStreamController = StreamController>>.broadcast(onListen: () { _tetrioStreamController.sink.add(_players); }); } @override Future open() async { await super.open(); await _loadPlayers(); } Stream>> get allPlayers => _tetrioStreamController.stream; Future _loadPlayers() async { final allPlayers = await getAllPlayers(); _players = allPlayers.toList().first; // ??? _tetrioStreamController.add(_players); } Future deletePlayer(String id) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final deletedPlayer = await db.delete(tetrioUsersTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (deletedPlayer != 1) { throw CouldNotDeletePlayer(); } else { _players.removeWhere((key, value) => key == id); _tetrioStreamController.add(_players); } } Future getNicknameByID(String id) async { if (id.length <= 16) return id; try{ return await getPlayer(id).then((value) => value.last.username); } catch (e){ return await fetchPlayer(id).then((value) => value.username); } } Future> fetchAndsaveTLHistory(String id) async { Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); } else { url = Uri.https('api.p1nkl0bst3r.xyz', 'tlhist/$id'); } final response = await http.get(url); if (response.statusCode == 200) { List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); List history = []; String nick = await getNicknameByID(id); for (List entry in csv){ TetrioPlayer state = TetrioPlayer( userId: id, username: nick, role: "p1nkl0bst3r", state: DateTime.parse(entry[9]), badges: [], friendCount: -1, gamesPlayed: -1, gamesWon: -1, gameTime: const Duration(seconds: -1), xp: -1, supporterTier: 0, verified: false, connections: null, tlSeason1: TetraLeagueAlpha(timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, vs: entry[8] != '' ? entry[8] : null, glicko: entry[4], rd: noTrRd, gamesPlayed: entry[1], gamesWon: entry[2], bestRank: "z", decaying: false, rating: entry[3], rank: entry[5], percentileRank: entry[5], percentile: rankCutoffs[entry[5]]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), sprint: [], blitz: [] ); history.add(state); } ensureDbIsOpen(); final db = getDatabaseOrThrow(); late List states; try{ states = _players[id]!; }catch(e){ var player = await fetchPlayer(id); await createPlayer(player); states = _players[id]!; } states.insertAll(0, history.reversed); final Map statesJson = {}; for (var e in states) { statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); } await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]); _tetrioStreamController.add(_players); return history; } else { developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); throw Exception('Failed to fetch player'); } } Future fetchTLLeaderboard() async { try{ var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league"); if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ developer.log("fetchTLLeaderboard: Leaderboard retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); return cached.value; }else{ _leaderboardsCache.remove(cached.key); developer.log("fetchTLLeaderboard: Leaderboard expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); } }catch(e){ developer.log("fetchTLLeaderboard: Trying to retrieve leaderboard", name: "services/tetrio_crud"); } Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); } else { url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); } final response = await http.get(url); if (response.statusCode == 200) { var rawJson = jsonDecode(response.body); if (rawJson['success']) { TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); _leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; return leaderboard; } else { developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); throw Exception("User doesn't exist"); } } else { developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); throw Exception('Failed to fetch player'); } } Future getTLStream(String userID) async { try{ var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID); if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ developer.log("getTLStream: Stream $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); return cached.value; }else{ _tlStreamsCache.remove(cached.key); developer.log("getTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); } }catch(e){ developer.log("getTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud"); } Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()}); } else { url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}'); } final response = await http.get(url); if (response.statusCode == 200) { if (jsonDecode(response.body)['success']) { TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson( jsonDecode(response.body)['data']['records'], userID); developer.log("getTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); _tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream; return stream; } else { developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body); throw Exception("User doesn't exist"); } } else { developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode); throw Exception('Failed to fetch player'); } } Future saveTLMatchesFromStream(TetraLeagueAlphaStream stream) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); for (TetraLeagueAlphaRecord match in stream.records) { final results = await db.query(tetraLeagueMatchesTable, where: '$idCol = ?', whereArgs: [match.ownId]); if (results.isNotEmpty) continue; db.insert(tetraLeagueMatchesTable, {idCol: match.ownId, replayID: match.replayId, timestamp: match.timestamp.toString(), player1id: match.endContext.first.userId, player2id: match.endContext.last.userId, endContext1: jsonEncode(match.endContext.first.toJson()), endContext2: jsonEncode(match.endContext.last.toJson())}); } } Future> getTLMatchesbyPlayerID(String playerID) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); List matches = []; final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]); for (var match in results){ matches.add(TetraLeagueAlphaRecord(ownId: match[idCol].toString(), replayId: match[replayID].toString(), timestamp: DateTime.parse(match[timestamp].toString()), endContext:[EndContextMulti.fromJson(jsonDecode(match[endContext1].toString())), EndContextMulti.fromJson(jsonDecode(match[endContext2].toString()))])); } return matches; } Future> fetchRecords(String userID) async { try{ var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID); if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ developer.log("fetchRecords: $userID records retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); return cached.value; }else{ _recordsCache.remove(cached.key); developer.log("fetchRecords: $userID records expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); } }catch(e){ developer.log("fetchRecords: Trying to retrieve $userID records", name: "services/tetrio_crud"); } Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()}); } else { url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records'); } final response = await http.get(url); if (response.statusCode == 200) { if (jsonDecode(response.body)['success']) { Map jsonRecords = jsonDecode(response.body); var sprint = jsonRecords['data']['records']['40l']['record'] != null ? [RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])] : []; var blitz = jsonRecords['data']['records']['blitz']['record'] != null ? [RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])] : []; var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); Map map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen}; developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud"); _recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map; return map; } else { developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body); throw Exception("User doesn't exist"); } } else { developer.log("fetchRecords Failed to fetch records", name: "services/tetrio_crud", error: response.statusCode); throw Exception('Failed to fetch player'); } } Future createPlayer(TetrioPlayer tetrioPlayer) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId.toLowerCase()]); if (results.isNotEmpty) { throw TetrioPlayerAlreadyExist(); } final Map statesJson = {(tetrioPlayer.state.millisecondsSinceEpoch ~/ 1000).toString(): tetrioPlayer.toJson()}; db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}); _players.addEntries({ tetrioPlayer.userId: [tetrioPlayer] }.entries); _tetrioStreamController.add(_players); } Future addPlayerToTrack(TetrioPlayer tetrioPlayer) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); final results = await db.query(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId.toLowerCase()]); if (results.isNotEmpty) { throw TetrioPlayerAlreadyExist(); } db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId}); } Future isPlayerTracking(String id) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); final results = await db.query(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (results.isEmpty) { return false; } else { return true; } } Future> getAllPlayerToTrack() async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersToTrackTable); developer.log("getAllPlayerToTrack: $players", name: "services/tetrio_crud"); return players.map((noteRow) => noteRow["id"].toString()); } Future deletePlayerToTrack(String id) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final deletedPlayer = await db.delete(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (deletedPlayer != 1) { throw CouldNotDeletePlayer(); } else { // _players.removeWhere((key, value) => key == id); // _tetrioStreamController.add(_players); } } Future storeState(TetrioPlayer tetrioPlayer) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); late List states; try { states = _players[tetrioPlayer.userId]!; //states = await getPlayer(tetrioPlayer.userId); } catch (e) { await createPlayer(tetrioPlayer); states = await getPlayer(tetrioPlayer.userId); } bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer); if (test == false) states.add(tetrioPlayer); final Map statesJson = {}; for (var e in states) { statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); } await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); _players[tetrioPlayer.userId]!.add(tetrioPlayer); _tetrioStreamController.add(_players); } Future deleteState(TetrioPlayer tetrioPlayer) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); late List states; states = await getPlayer(tetrioPlayer.userId); _players[tetrioPlayer.userId]!.removeWhere((element) => element.state == tetrioPlayer.state); states = _players[tetrioPlayer.userId]!; final Map statesJson = {}; for (var e in states) { statesJson.addEntries({e.state.millisecondsSinceEpoch.toString(): e.toJson()}.entries); } await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); _players[tetrioPlayer.userId]!.add(tetrioPlayer); _tetrioStreamController.add(_players); } Future> getPlayer(String id) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); List states = []; final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (results.isEmpty) { return states; } else { dynamic rawStates = results.first['jsonStates'] as String; rawStates = json.decode(rawStates); rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String))); _players.removeWhere((key, value) => key == id); _players.addEntries({states.last.userId: states}.entries); _tetrioStreamController.add(_players); return states; } } Future fetchPlayer(String user) async { try{ var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user); if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ developer.log("fetchPlayer: User $user retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); return cached.value; }else{ _playersCache.remove(cached.key); developer.log("fetchPlayer: Cached user $user expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); } }catch(e){ developer.log("fetchPlayer: Trying to retrieve $user", name: "services/tetrio_crud"); } Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()}); } else { url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}'); } final response = await http.get(url); if (response.statusCode == 200) { var json = jsonDecode(response.body); if (json['success']) { TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username']); developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); _playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player; return player; } else { developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); throw TetrioPlayerNotExist(); } } else { developer.log("fetchPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } } Future>>> getAllPlayers() async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersTable); Map> data = {}; //developer.log("getAllPlayers: $players", name: "services/tetrio_crud"); return players.map((row) { // what the fuck am i doing here? var test = json.decode(row['jsonStates'] as String); List states = []; test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), row[idCol] as String, row[nickCol] as String))); data.addEntries({states.last.userId: states}.entries); return data; }); } }