deleteState fix + new badges

Also crud was documented. Not sure if i doing everything correctly, maybe i should implement something intresting instead?
This commit is contained in:
dan63047 2024-02-01 03:15:32 +03:00
parent a3e09d1ca7
commit 97f5e95da3
24 changed files with 192 additions and 193 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# My shit
lib/views/tl_match_view web.dart

View File

@ -175,6 +175,7 @@ class TetrioService extends DB {
developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode);
_replaysCache[replayID] = [response.body, response.bodyBytes]; // Puts results into the cache
return [response.body, response.bodyBytes];
// if not 200 - throw a unique for each code exception
case 404:
throw SzyNotFound();
case 403:
@ -192,12 +193,14 @@ class TetrioService extends DB {
developer.log("szyDownload: Failed to download a replay", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) {
} on http.ClientException catch (e, s) { // If local http client fails
developer.log("$e, $s");
throw http.ClientException(e.message, e.uri);
throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet
}
}
/// Saves replay with given [replayID] to Download or Documents directory as [replayID].ttrm. Throws an exception,
/// if file with name [replayID].ttrm exist, if it fails to get replay or unable to save replay
Future<String> saveReplay(String replayID) async {
var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
@ -208,49 +211,57 @@ class TetrioService extends DB {
return replayFile.path;
}
/// Gets replay with given [replayID] and returns some stats about it. If [isAvailable] is false
/// or unable to get replay, it will throw an exception
Future<ReplayData> analyzeReplay(String replayID, bool isAvailable) async{
// trying retirieve existing stats from DB first
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
final results = await db.query(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [replayID]);
if (results.isNotEmpty) return ReplayData.fromJson(jsonDecode(results.first["data"].toString()));
if (!isAvailable) throw ReplayNotAvalable();
Map<String, dynamic> toAnalyze = jsonDecode((await szyGetReplay(replayID))[0]);
if (results.isNotEmpty) return ReplayData.fromJson(jsonDecode(results.first["data"].toString())); // if success
if (!isAvailable) throw ReplayNotAvalable(); // if replay too old
// otherwise, actually going to download a replay and analyze it
String replay = (await szyGetReplay(replayID))[0];
Map<String, dynamic> toAnalyze = jsonDecode(replay);
ReplayData data = ReplayData.fromJson(toAnalyze);
saveReplayStats(data);
saveReplayStats(data); // saving to DB for later
return data;
}
/// Gets and returns Top TR as a double for a player with given [id]. May return null if player top tr is unknown
/// or api is unavaliable (404). May throw an exception, if something else happens.
Future<double?> fetchTopTR(String id) async {
try{
try{ // read from cache
var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired
developer.log("fetchTopTR: Top TR retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value.values.first;
}else{
}else{ // if cache expired
_topTRcache.remove(cached.key);
developer.log("fetchTopTR: Top TR expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
}catch(e){ // actually going to obtain
developer.log("fetchTopTR: Trying to retrieve Top TR", name: "services/tetrio_crud");
}
Uri url;
if (kIsWeb) {
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR", "user": id});
} else {
} else { // Actually going to hit p1nkl0bst3r api
url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id');
}
try{
final response = await client.get(url);
switch (response.statusCode) {
case 200:
case 200: // ok - return the value
_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: double.tryParse(response.body)};
return double.tryParse(response.body);
case 404:
case 404: // not found - return null
developer.log("fetchTopTR: Probably, player doesn't have top TR", name: "services/tetrio_crud", error: response.statusCode);
_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null};
return null;
// if not 200 or 404 - throw a unique for each code exception
case 403:
throw P1nkl0bst3rForbidden();
case 429:
@ -266,12 +277,17 @@ class TetrioService extends DB {
developer.log("fetchTopTR: Failed to fetch top TR", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) {
} on http.ClientException catch (e, s) { // If local http client fails
developer.log("$e, $s");
throw http.ClientException(e.message, e.uri);
throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet
}
}
// Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above,
// so i'm going to document only unique differences between them
/// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states
/// (state = instance of player stats at some point of time). Can throw an exception if fails to retrieve data.
Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async {
Uri url;
if (kIsWeb) {
@ -284,10 +300,12 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200:
// that one api returns csv instead of json
List<List<dynamic>> csv = const CsvToListConverter().convert(response.body)..removeAt(0);
List<TetrioPlayer> history = [];
// doesn't return nickname, need to retrieve it separately
String nick = await getNicknameByID(id);
for (List<dynamic> entry in csv){
for (List<dynamic> entry in csv){ // each entry is one state
TetrioPlayer state = TetrioPlayer(
userId: id,
username: nick,
@ -302,27 +320,51 @@ class TetrioService extends DB {
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),
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);
}
// trying to dump it to local DB
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
late List<TetrioPlayer> states;
try{
// checking if tetra stats aware about that player TODO: is it necessary?
states = _players[id]!;
}catch(e){
// if somehow not - create it
var player = await fetchPlayer(id);
await createPlayer(player);
states = _players[id]!;
}
states.insertAll(0, history.reversed);
final Map<String, dynamic> statesJson = {};
for (var e in states) {
for (var e in states) { // making one big json out of this list
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
}
// and putting it to local DB
await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]);
_tetrioStreamController.add(_players);
return history;
@ -350,6 +392,7 @@ class TetrioService extends DB {
}
}
/// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve.
Future<TetrioPlayersLeaderboard> fetchTLLeaderboard() async {
try{
var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league");
@ -375,14 +418,14 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200:
var rawJson = jsonDecode(response.body);
if (rawJson['success']) {
if (rawJson['success']) { // if api confirmed that everything ok
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 {
} else { // idk how to hit that one
developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson);
throw Exception("Failed to get leaderboard (problems on the tetr.io side)");
throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side?
}
case 403:
throw TetrioForbidden();
@ -405,6 +448,7 @@ class TetrioService extends DB {
}
}
/// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve.
Future<List<News>> fetchNews(String userID) async{
try{
var cached = _newsCache.entries.firstWhere((element) => element.value[0].stream == "user_$userID");
@ -431,10 +475,10 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200:
var payload = jsonDecode(response.body);
if (payload['success']) {
if (payload['success']) { // if api confirmed that everything ok
List<News> news = [for (var entry in payload['data']['news']) News.fromJson(entry)];
developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud");
_newsCache[payload['cache']['cached_until'].toString()] = news;
developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud");
return news;
} else {
developer.log("fetchNews: User dosen't exist", name: "services/tetrio_crud", error: response.body);
@ -461,18 +505,20 @@ class TetrioService extends DB {
}
}
Future<TetraLeagueAlphaStream> getTLStream(String userID) async {
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve.
Future<TetraLeagueAlphaStream> fetchTLStream(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");
developer.log("fetchTLStream: 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");
developer.log("fetchTLStream: 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");
developer.log("fetchTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud");
}
Uri url;
@ -488,11 +534,11 @@ class TetrioService extends DB {
case 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;
developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
return stream;
} else {
developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body);
developer.log("fetchTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist();
}
case 403:
@ -507,7 +553,7 @@ class TetrioService extends DB {
case 504:
throw TetrioInternalProblem();
default:
developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode);
developer.log("fetchTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) {
@ -516,16 +562,26 @@ class TetrioService extends DB {
}
}
/// Saves Tetra League Matches from [stream] to the local DB.
Future<void> saveTLMatchesFromStream(TetraLeagueAlphaStream stream) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
for (TetraLeagueAlphaRecord match in stream.records) {
for (TetraLeagueAlphaRecord match in stream.records) { // putting then one by one
final results = await db.query(tetraLeagueMatchesTable, where: '$replayID = ?', whereArgs: [match.replayId]);
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())});
if (results.isNotEmpty) continue; // if match alreay exist - skip
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())
});
}
}
/// Deletes duplicate entries of Tetra League matches from local DB.
Future<void> removeDuplicatesFromTLMatches() async{
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
@ -548,17 +604,28 @@ class TetrioService extends DB {
""");
}
/// Gets and returns a list of matches from local DB for a given [playerID].
Future<List<TetraLeagueAlphaRecord>> getTLMatchesbyPlayerID(String playerID) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
List<TetraLeagueAlphaRecord> 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()))], replayAvalable: false));
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()))
],
replayAvalable: false
));
}
return matches;
}
/// Deletes match and stats of that match with given [matchID] from local DB. Throws an exception if fails.
Future<void> deleteTLMatch(String matchID) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
@ -570,6 +637,8 @@ class TetrioService extends DB {
await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]);
}
/// Retrieves Blitz, 40 Lines and Zen records for a given [playerID] from Tetra Channel api. Returns Map, which contains user id (`user`),
/// Blitz (`blitz`) and 40 Lines (`sprint`) record objects and Zen object (`zen`). Throws an exception if fails to retrieve.
Future<Map<String, dynamic>> fetchRecords(String userID) async {
try{
var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID);
@ -598,15 +667,15 @@ class TetrioService extends DB {
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'])]
: [];
? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])
: null;
var blitz = jsonRecords['data']['records']['blitz']['record'] != null
? [RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])]
: [];
? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])
: null;
var zen = TetrioZen.fromJson(jsonRecords['data']['zen']);
Map<String, dynamic> 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;
developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud");
return map;
} else {
developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body);
@ -633,13 +702,18 @@ class TetrioService extends DB {
}
}
/// Creates an entry in local DB for [tetrioPlayer]. Throws an exception if that player already here.
Future<void> createPlayer(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
// checking if its already here
final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId.toLowerCase()]);
if (results.isNotEmpty) {
throw TetrioPlayerAlreadyExist();
}
// converting to json and store
final Map<String, dynamic> statesJson = {(tetrioPlayer.state.millisecondsSinceEpoch ~/ 1000).toString(): tetrioPlayer.toJson()};
db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)});
_players.addEntries({
@ -648,6 +722,7 @@ class TetrioService extends DB {
_tetrioStreamController.add(_players);
}
/// Adds user id of [tetrioPlayer] to the [tetrioUsersToTrackTable] of database.
Future<void> addPlayerToTrack(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
@ -658,6 +733,7 @@ class TetrioService extends DB {
db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId});
}
/// Returns bool, which tells whether is given [id] is in [tetrioUsersToTrackTable].
Future<bool> isPlayerTracking(String id) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
@ -665,6 +741,7 @@ class TetrioService extends DB {
return results.isNotEmpty;
}
/// Returns Iterable with user ids of players who is tracked.
Future<Iterable<String>> getAllPlayerToTrack() async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
@ -673,6 +750,7 @@ class TetrioService extends DB {
return players.map((noteRow) => noteRow["id"].toString());
}
/// Removes user with given [id] from the [tetrioUsersToTrackTable] of database.
Future<void> deletePlayerToTrack(String id) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
@ -685,56 +763,71 @@ class TetrioService extends DB {
}
}
/// Saves state (which is [tetrioPlayer]) to the local database.
Future<void> storeState(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
late List<TetrioPlayer> states;
try {
try { // retrieveing previous states
states = _players[tetrioPlayer.userId]!;
} catch (e) {
} catch (e) { // nothing found - player not exist - create them
await createPlayer(tetrioPlayer);
states = await getPlayer(tetrioPlayer.userId);
}
// we not going to add state, that is same, as the previous
bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer);
if (test == false) states.add(tetrioPlayer);
// Making map of the states
final Map<String, dynamic> statesJson = {};
for (var e in states) {
// Saving in format: {"unix_seconds": json_of_state}
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
}
// Rewrite our database
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);
}
/// Remove state (which is [tetrioPlayer]) from the local database
Future<void> deleteState(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
late List<TetrioPlayer> states;
states = await getPlayer(tetrioPlayer.userId);
// removing state from map that contain every state of each user
_players[tetrioPlayer.userId]!.removeWhere((element) => element.state == tetrioPlayer.state);
states = _players[tetrioPlayer.userId]!;
// Making map of the states (without deleted one)
final Map<String, dynamic> statesJson = {};
for (var e in states) {
statesJson.addEntries({e.state.millisecondsSinceEpoch.toString(): e.toJson()}.entries);
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
}
// Rewriting database entry with new json
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);
}
/// Returns list of all states of player with given [id] from database. Can return empty list if player
/// was not found.
Future<List<TetrioPlayer>> getPlayer(String id) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
List<TetrioPlayer> states = [];
final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]);
if (results.isEmpty) {
return states;
return states; // it empty
} else {
dynamic rawStates = results.first['jsonStates'] as String;
rawStates = json.decode(rawStates);
// recreating objects of states
rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String)));
// updating the stream
_players.removeWhere((key, value) => key == id);
_players.addEntries({states.last.userId: states}.entries);
_tetrioStreamController.add(_players);
@ -742,6 +835,9 @@ class TetrioService extends DB {
}
}
/// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user.
/// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve.
Future<TetrioPlayer> fetchPlayer(String user, {bool isItDiscordID = false}) async {
try{
var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user);
@ -757,6 +853,7 @@ class TetrioService extends DB {
}
if (isItDiscordID){
// trying to find player with given discord id
Uri dUrl;
if (kIsWeb) {
dUrl = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()});
@ -770,12 +867,14 @@ class TetrioService extends DB {
case 200:
var json = jsonDecode(response.body);
if (json['success'] && json['data'] != null) {
// success - rewrite user with tetrio user id and going to obtain data about him
user = json['data']['user']['_id'];
} else {
} else { // fail - throw an exception
developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist();
}
break;
// more exceptions to god of exceptions
case 403:
throw TetrioForbidden();
case 429:
@ -797,6 +896,7 @@ class TetrioService extends DB {
}
}
// finally going to obtain
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()});
@ -810,9 +910,10 @@ class TetrioService extends DB {
case 200:
var json = jsonDecode(response.body);
if (json['success']) {
// parse and count stats
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;
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
return player;
} else {
developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body);
@ -839,6 +940,8 @@ class TetrioService extends DB {
}
}
/// Basucally, retrieves whole [tetrioUsersTable] and do stupud things idk
/// Returns god knows what. TODO: Rewrite this shit
Future<Iterable<Map<String, List<TetrioPlayer>>>> getAllPlayers() async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();

View File

@ -156,7 +156,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
late List<News> news;
late double? topTR;
requests = await Future.wait([ // all at once
teto.getTLStream(_searchFor),
teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor),
if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR
@ -309,8 +309,6 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
case ConnectionState.done:
//bool bigScreen = MediaQuery.of(context).size.width > 1024;
if (snapshot.hasData) {
List<dynamic> sprintRuns = snapshot.data![1]['sprint'];
List<dynamic> blitzRuns = snapshot.data![1]['blitz'];
return RefreshIndicator(
onRefresh: () {
return Future(() => changePlayer(snapshot.data![0].userId));
@ -356,8 +354,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
TLThingy(tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon"),
_TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]),
_History(states: snapshot.data![2], update: _justUpdate),
_RecordThingy(record: sprintRuns.elementAtOrNull(0)),
_RecordThingy(record: blitzRuns.elementAtOrNull(0)),
_RecordThingy(record: snapshot.data![1]['sprint']),
_RecordThingy(record: snapshot.data![1]['blitz']),
_OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
],
),

View File

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
@ -392,7 +393,14 @@ class UserThingy extends StatelessWidget {
width: 32,
errorBuilder: (context, error, stackTrace) {
developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace);
return Image.asset("res/icons/kagari.png", height: 32, width: 32);
return Image.network(
"https://tetr.io/res/badges/${badge.badgeId}.png",
height: 32,
width: 32,
errorBuilder:(context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 32, width: 32);
}
);
},
))
],

View File

@ -82,129 +82,10 @@ targets:
flutter:
uses-material-design: true
assets:
- res/avatars/tetrio_anon.png
- res/avatars/tetrio_banned.png
- res/icons/kagari.png
- res/icons/osk.svg
- res/icons/tetrio-logo.svg
- res/icons/improvement-local.png
- res/icons/supporter-tag.png
- res/tetrio_tl_alpha_ranks/x.png
- res/tetrio_tl_alpha_ranks/u.png
- res/tetrio_tl_alpha_ranks/ss.png
- res/tetrio_tl_alpha_ranks/s+.png
- res/tetrio_tl_alpha_ranks/s.png
- res/tetrio_tl_alpha_ranks/s-.png
- res/tetrio_tl_alpha_ranks/a+.png
- res/tetrio_tl_alpha_ranks/a.png
- res/tetrio_tl_alpha_ranks/a-.png
- res/tetrio_tl_alpha_ranks/b+.png
- res/tetrio_tl_alpha_ranks/b.png
- res/tetrio_tl_alpha_ranks/b-.png
- res/tetrio_tl_alpha_ranks/c+.png
- res/tetrio_tl_alpha_ranks/c.png
- res/tetrio_tl_alpha_ranks/c-.png
- res/tetrio_tl_alpha_ranks/d+.png
- res/tetrio_tl_alpha_ranks/d.png
- res/tetrio_tl_alpha_ranks/z.png
- res/tetrio_badges/5mblast_1.png
- res/tetrio_badges/5mblast_10.png
- res/tetrio_badges/5mblast_100.png
- res/tetrio_badges/5mblast_1000.png
- res/tetrio_badges/20tsd.png
- res/tetrio_badges/100player.png
- res/tetrio_badges/allclear.png
- res/tetrio_badges/bugbounty.png
- res/tetrio_badges/cometopen_1.png
- res/tetrio_badges/cometopen_2.png
- res/tetrio_badges/cometopen_3.png
- res/tetrio_badges/early-supporter.png
- res/tetrio_badges/founder.png
- res/tetrio_badges/galactic2x2_1.png
- res/tetrio_badges/galactic2x2_2.png
- res/tetrio_badges/galactic2x2_3.png
- res/tetrio_badges/ggc_1.png
- res/tetrio_badges/ggc_2.png
- res/tetrio_badges/ggc_3.png
- res/tetrio_badges/hdoxii_1.png
- res/tetrio_badges/hdoxii_2.png
- res/tetrio_badges/hdoxii_3.png
- res/tetrio_badges/heart.png
- res/tetrio_badges/hnprism_1.png
- res/tetrio_badges/hnprism_2.png
- res/tetrio_badges/hnprism_3.png
- res/tetrio_badges/hnstratosphere50_1.png
- res/tetrio_badges/hnstratosphere50_2.png
- res/tetrio_badges/hnstratosphere50_3.png
- res/tetrio_badges/ift_1.png
- res/tetrio_badges/ift_2.png
- res/tetrio_badges/ift_3.png
- res/tetrio_badges/infdev.png
- res/tetrio_badges/kod_by_founder.png
- res/tetrio_badges/kod_founder.png
- res/tetrio_badges/leaderboard1.png
- res/tetrio_badges/mmc_tabi_1.png
- res/tetrio_badges/mmc_tabi_2.png
- res/tetrio_badges/mmc_tabi_3.png
- res/tetrio_badges/mmc_tabi_superlobby.png
- res/tetrio_badges/mmc_tabi_superlobby2.png
- res/tetrio_badges/mmc_tabi_superlobby3.png
- res/tetrio_badges/mmc_tabi_superlobby4.png
- res/tetrio_badges/redgevo_1.png
- res/tetrio_badges/redgevo_2.png
- res/tetrio_badges/redgevo_3.png
- res/tetrio_badges/rengervl_1.png
- res/tetrio_badges/rengervl_2.png
- res/tetrio_badges/rengervl_3.png
- res/tetrio_badges/sakurablend_1.png
- res/tetrio_badges/sakurablend_2.png
- res/tetrio_badges/sakurablend_3.png
- res/tetrio_badges/scuncapped_1.png
- res/tetrio_badges/scuncapped_2.png
- res/tetrio_badges/scuncapped_3.png
- res/tetrio_badges/secretgrade.png
- res/tetrio_badges/sfu_raccoon_1.png
- res/tetrio_badges/sfu_raccoon_2.png
- res/tetrio_badges/sfu_raccoon_3.png
- res/tetrio_badges/streamersuperlobby.png
- res/tetrio_badges/superlobby.png
- res/tetrio_badges/superlobby2.png
- res/tetrio_badges/taws_u50_1.png
- res/tetrio_badges/taws_u50_2.png
- res/tetrio_badges/taws_u50_3.png
- res/tetrio_badges/tawshdsl_capped.png
- res/tetrio_badges/tawshdsl_uncapped.png
- res/tetrio_badges/tawsignite_expert.png
- res/tetrio_badges/tawslg.png
- res/tetrio_badges/tetralympic_masters.png
- res/tetrio_badges/thaitour_1.png
- res/tetrio_badges/thaitour_2.png
- res/tetrio_badges/thaitour_3.png
- res/tetrio_badges/ttsdpf_1.png
- res/tetrio_badges/ttsdpf_2.png
- res/tetrio_badges/ttsdpf_3.png
- res/tetrio_badges/ttsdtc_1.png
- res/tetrio_badges/ttsdtc_2.png
- res/tetrio_badges/ttsdtc_3.png
- res/tetrio_badges/twc23_1.png
- res/tetrio_badges/twc23_2.png
- res/tetrio_badges/twc23_3.png
- res/tetrio_badges/twc23_4.png
- res/tetrio_badges/ubcea_1.png
- res/tetrio_badges/ubcea_2.png
- res/tetrio_badges/ubcea_3.png
- res/tetrio_badges/underdog_1.png
- res/tetrio_badges/underdog_2.png
- res/tetrio_badges/underdog_3.png
- res/tetrio_badges/underdog_predict.png
- res/tetrio_badges/wpl_1.png
- res/tetrio_badges/wpl_2.png
- res/tetrio_badges/wpl_3.png
- res/tetrio_badges/wplc_1.png
- res/tetrio_badges/wplc_2.png
- res/tetrio_badges/wplc_3.png
- res/tetrio_badges/wplc_participation.png
- res/avatars/
- res/icons/
- res/tetrio_tl_alpha_ranks/
- res/tetrio_badges/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
res/tetrio_badges/sii_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
res/tetrio_badges/sii_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
res/tetrio_badges/sii_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -13,21 +13,27 @@ import 'package:tetra_stats/services/tetrio_crud.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
if (kIsWeb) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfiWeb;
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
late TetrioService teto;
setUp(() {
if (kIsWeb) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfiWeb;
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
teto = TetrioService();
});
test("Initialize TetrioServise", () async {
await TetrioService().open();
}, skip: true); // a fucking MissingPluginException how does that even happening?
teto.open();
}); // a fucking MissingPluginException how does that even happening?
// i guess i will be unable to test iteractions with DB
group("Test fetchPlayer with different players", () {
// those tests exist in order to detect a tiny little change in Tetra Channel API in case of some update.
test("dan63047 (user who have activity in tetra league)", () async {
TetrioPlayer dan63047 = await TetrioService().fetchPlayer("6098518e3d5155e6ec429cdc");
TetrioPlayer dan63047 = await teto.fetchPlayer("6098518e3d5155e6ec429cdc");
expect(dan63047.userId, "6098518e3d5155e6ec429cdc");
expect(dan63047.registrationTime != null, true);
expect(dan63047.avatarRevision != null, true);
@ -51,7 +57,7 @@ void main() {
expect(dan63047.tlSeason1.playstyle != null, true);
});
test("osk (sysop who have activity in tetra league)", () async {
TetrioPlayer osk = await TetrioService().fetchPlayer("5e32fc85ab319c2ab1beb07c");
TetrioPlayer osk = await teto.fetchPlayer("5e32fc85ab319c2ab1beb07c");
expect(osk.userId, "5e32fc85ab319c2ab1beb07c");
expect(osk.registrationTime, null);
expect(osk.country, "XM");
@ -79,7 +85,7 @@ void main() {
expect(osk.tlSeason1.playstyle != null, true);
});
test("kagari (sysop who have zero activity)", () async {
TetrioPlayer kagari = await TetrioService().fetchPlayer("5e331c3ce24a5a3e258f7a1b");
TetrioPlayer kagari = await teto.fetchPlayer("5e331c3ce24a5a3e258f7a1b");
expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b");
expect(kagari.registrationTime, null);
expect(kagari.country, "XM");
@ -112,7 +118,7 @@ void main() {
expect(kagari.tlSeason1.playstyle, null);
});
test("furry (banned account)", () async {
TetrioPlayer furry = await TetrioService().fetchPlayer("5eea0ff69a1ba76c20347086");
TetrioPlayer furry = await teto.fetchPlayer("5eea0ff69a1ba76c20347086");
expect(furry.userId, "5eea0ff69a1ba76c20347086");
expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z"));
expect(furry.role, "banned");
@ -143,7 +149,7 @@ void main() {
expect(furry.tlSeason1.playstyle, null);
});
test("oskwarefan (anon account)", () async {
TetrioPlayer oskwarefan = await TetrioService().fetchPlayer("646cb8273e887a054d64febe");
TetrioPlayer oskwarefan = await teto.fetchPlayer("646cb8273e887a054d64febe");
expect(oskwarefan.userId, "646cb8273e887a054d64febe");
expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z"));
expect(oskwarefan.role, "anon");
@ -174,7 +180,7 @@ void main() {
});
test("not existing account", () async {
var future = TetrioService().fetchPlayer("hasdbashdbs");
var future = teto.fetchPlayer("hasdbashdbs");
await expectLater(future, throwsA(isA<TetrioPlayerNotExist>()));
});
});