2024-06-16 21:04:07 +00:00
// ignore_for_file: type_literal_in_constant_pattern
2023-06-17 21:50:52 +00:00
import ' dart:async ' ;
import ' dart:convert ' ;
import ' dart:developer ' as developer ;
2023-10-18 21:50:41 +00:00
import ' dart:io ' ;
2024-06-04 22:14:34 +00:00
import ' package:flutter/material.dart ' ;
2023-10-18 21:50:41 +00:00
import ' package:path_provider/path_provider.dart ' ;
2024-06-03 21:06:00 +00:00
import ' package:tetra_stats/data_objects/tetra_stats.dart ' ;
2024-01-05 23:11:45 +00:00
import ' package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart ' ;
2023-10-09 18:48:50 +00:00
import ' package:tetra_stats/main.dart ' show packageInfo ;
2023-07-20 20:56:00 +00:00
import ' package:flutter/foundation.dart ' ;
2023-09-23 19:09:36 +00:00
import ' package:tetra_stats/services/custom_http_client.dart ' ;
2023-06-17 21:50:52 +00:00
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 ' ;
2023-07-17 17:57:24 +00:00
import ' package:csv/csv.dart ' ;
2023-06-17 21:50:52 +00:00
const String dbName = " TetraStats.db " ;
const String tetrioUsersTable = " tetrioUsers " ;
const String tetrioUsersToTrackTable = " tetrioUsersToTrack " ;
2023-06-23 18:38:15 +00:00
const String tetraLeagueMatchesTable = " tetrioAlphaLeagueMathces " ;
2024-01-08 22:42:49 +00:00
const String tetrioTLReplayStatsTable = " tetrioTLReplayStats " ;
2023-06-17 21:50:52 +00:00
const String idCol = " id " ;
2023-06-23 18:38:15 +00:00
const String replayID = " replayId " ;
2023-06-17 21:50:52 +00:00
const String nickCol = " nickname " ;
2023-06-23 18:38:15 +00:00
const String timestamp = " timestamp " ;
const String endContext1 = " endContext1 " ;
const String endContext2 = " endContext2 " ;
2023-06-17 21:50:52 +00:00
const String statesCol = " jsonStates " ;
2023-06-23 18:38:15 +00:00
const String player1id = " player1id " ;
const String player2id = " player2id " ;
2024-02-06 20:38:52 +00:00
/// Table, that store players data, their stats at some moments of time
2023-06-17 21:50:52 +00:00
const String createTetrioUsersTable = '''
CREATE TABLE IF NOT EXISTS " tetrioUsers " (
" id " TEXT UNIQUE ,
" nickname " TEXT ,
" jsonStates " TEXT ,
PRIMARY KEY ( " id " )
) ; ''' ;
2024-01-29 21:13:07 +00:00
/// Table, that store ids of players we need keep track of
2023-06-17 21:50:52 +00:00
const String createTetrioUsersToTrack = '''
CREATE TABLE IF NOT EXISTS " tetrioUsersToTrack " (
" id " TEXT NOT NULL UNIQUE ,
PRIMARY KEY ( " ID " )
)
''' ;
2024-01-29 21:13:07 +00:00
/// Table of Tetra League matches. Each match corresponds with their own players and end contexts
2023-06-23 18:38:15 +00:00
const String createTetrioTLRecordsTable = '''
CREATE TABLE IF NOT EXISTS " tetrioAlphaLeagueMathces " (
2023-06-26 17:13:53 +00:00
" id " TEXT NOT NULL UNIQUE ,
2023-06-23 18:38:15 +00:00
" replayId " TEXT ,
" player1id " TEXT ,
" player2id " TEXT ,
" timestamp " TEXT ,
" endContext1 " TEXT ,
2023-06-26 17:13:53 +00:00
" endContext2 " TEXT ,
PRIMARY KEY ( " id " )
2023-06-23 18:38:15 +00:00
)
''' ;
2024-01-29 21:13:07 +00:00
/// Table, that contains results of replay analysis in order to not analyze it more, than one time.
2024-01-05 23:11:45 +00:00
const String createTetrioTLReplayStats = '''
2024-01-08 22:42:49 +00:00
CREATE TABLE IF NOT EXISTS " tetrioTLReplayStats " (
2024-01-05 23:11:45 +00:00
" id " TEXT NOT NULL ,
2024-01-08 22:42:49 +00:00
" data " TEXT NOT NULL ,
2024-01-22 19:39:28 +00:00
" freyhoe " TEXT ,
2024-01-05 23:11:45 +00:00
PRIMARY KEY ( " id " )
)
''' ;
2024-06-03 21:06:00 +00:00
class CacheController {
late Map < String , dynamic > _cache ;
late Map < String , String > _nicknames ;
CacheController . init ( ) {
_cache = { } ;
_nicknames = { } ;
}
String _getObjectId ( dynamic object ) {
switch ( object . runtimeType ) {
case TetrioPlayer:
object as TetrioPlayer ;
_nicknames [ object . username ] = object . userId ;
return object . userId ;
case TetrioPlayersLeaderboard:
return object . runtimeType . toString ( ) + object . type ;
case Cutoffs:
return object . runtimeType . toString ( ) ;
case TetrioPlayerFromLeaderboard: // i may be a little stupid
2024-06-16 21:04:07 +00:00
return " ${ object . runtimeType } topone " ;
2024-06-03 21:06:00 +00:00
case TetraLeagueAlphaStream:
return object . runtimeType . toString ( ) + object . userId ;
2024-06-12 22:32:45 +00:00
case SingleplayerStream:
return object . type + object . userId ;
2024-06-03 21:06:00 +00:00
default :
return object . runtimeType . toString ( ) + object . id ;
}
}
void store ( dynamic object , int ? cachedUntil ) async {
String key = _getObjectId ( object ) + cachedUntil ! . toString ( ) ;
_cache [ key ] = object ;
}
dynamic get ( String id , Type datatype ) {
if ( _cache . isEmpty ) return null ;
MapEntry < String , dynamic > ? objectEntry ;
try {
switch ( datatype ) {
case TetrioPlayer:
objectEntry = id . length < = 16 ? _cache . entries . firstWhere ( ( element ) = > element . key . startsWith ( _nicknames [ id ] ? ? " huh? " ) ) : _cache . entries . firstWhere ( ( element ) = > element . key . startsWith ( id ) ) ;
if ( id . length < = 16 ) id = _nicknames [ id ] ? ? " huh? " ;
break ;
default :
objectEntry = _cache . entries . firstWhere ( ( element ) = > element . key . startsWith ( datatype . toString ( ) + id ) ) ;
id = datatype . toString ( ) + id ;
break ;
}
} on StateError {
return null ;
}
if ( int . parse ( objectEntry . key . substring ( id . length ) ) < = DateTime . now ( ) . millisecondsSinceEpoch ) {
_cache . remove ( objectEntry . key ) ;
return null ;
} else {
return objectEntry . value ;
}
}
void removeOld ( ) async {
2024-06-03 23:42:44 +00:00
_cache . removeWhere ( ( key , value ) = > int . parse ( key . substring ( _getObjectId ( value ) . length ) ) < = DateTime . now ( ) . millisecondsSinceEpoch ) ;
2024-06-03 21:06:00 +00:00
}
void reset ( ) {
_cache . clear ( ) ;
}
}
2023-06-17 21:50:52 +00:00
class TetrioService extends DB {
2024-02-06 20:38:52 +00:00
final Map < String , String > _players = { } ;
2024-06-03 21:06:00 +00:00
final _cache = CacheController . init ( ) ; // I'm trying to send as less requests, as possible, so i'm caching the results of those requests.
final Map < String , PlayerLeaderboardPosition > _lbPositions = { } ; // separate one because attached to the leaderboard
2024-01-29 21:13:07 +00:00
/// Thing, that sends every request to the API endpoints
final client = kDebugMode ? UserAgentClient ( " Kagari-chan loves osk (Tetra Stats dev build) " , http . Client ( ) ) : UserAgentClient ( " Tetra Stats v ${ packageInfo . version } (dm @dan63047 if someone abuse that software) " , http . Client ( ) ) ;
/// We should have only one instanse of this service
2023-06-17 21:50:52 +00:00
static final TetrioService _shared = TetrioService . _sharedInstance ( ) ;
factory TetrioService ( ) = > _shared ;
2024-02-06 20:38:52 +00:00
late final StreamController < Map < String , String > > _tetrioStreamController ;
2023-06-17 21:50:52 +00:00
TetrioService . _sharedInstance ( ) {
2024-02-06 20:38:52 +00:00
_tetrioStreamController = StreamController < Map < String , String > > . broadcast ( onListen: ( ) {
2023-06-17 21:50:52 +00:00
_tetrioStreamController . sink . add ( _players ) ;
} ) ;
}
@ override
Future < void > open ( ) async {
await super . open ( ) ;
2023-06-21 19:17:39 +00:00
await _loadPlayers ( ) ;
2023-06-17 21:50:52 +00:00
}
2024-02-06 20:38:52 +00:00
Stream < Map < String , String > > get allPlayers = > _tetrioStreamController . stream ;
2023-06-17 21:50:52 +00:00
2024-01-29 21:13:07 +00:00
/// Loading and sending to the stream everyone.
2023-06-21 19:17:39 +00:00
Future < void > _loadPlayers ( ) async {
2024-02-06 20:38:52 +00:00
final allPlayers = await getAllPlayerToTrack ( ) ;
for ( var element in allPlayers ) {
_players [ element ] = await getNicknameByID ( element ) ;
2023-07-22 12:23:11 +00:00
}
2024-02-06 20:38:52 +00:00
developer . log ( " _loadPlayers: $ _players " , name: " services/tetrio_crud " ) ;
2023-06-17 21:50:52 +00:00
_tetrioStreamController . add ( _players ) ;
}
2024-01-29 21:13:07 +00:00
/// Removes player entry from tetrioUsersTable with given [id].
/// Can throw an error is player with this id is not exist
2023-06-17 21:50:52 +00:00
Future < void > 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 ) ;
}
}
2024-01-29 21:13:07 +00:00
/// Gets nickname from database or requests it from API if missing.
/// Throws an exception if user not exist or request failed.
2023-06-17 21:50:52 +00:00
Future < String > getNicknameByID ( String id ) async {
2024-01-29 21:13:07 +00:00
if ( id . length < = 16 ) return id ; // nicknames can be up to 16 symbols in length, that's how i'm differentiate nickname from ids
2023-07-17 17:57:24 +00:00
try {
2024-02-06 20:38:52 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
var request = await db . query ( tetrioUsersTable , limit: 1 , where: ' $ idCol = ? ' , whereArgs: [ id . toLowerCase ( ) ] ) ;
return request . first [ nickCol ] as String ;
2023-07-17 17:57:24 +00:00
} catch ( e ) {
return await fetchPlayer ( id ) . then ( ( value ) = > value . username ) ;
}
}
2024-01-29 21:13:07 +00:00
/// Puts results of replay analysis into a tetrioTLReplayStatsTable
2024-01-08 22:42:49 +00:00
Future < void > saveReplayStats ( ReplayData replay ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
db . insert ( tetrioTLReplayStatsTable , { idCol: replay . id , " data " : jsonEncode ( replay . toJson ( ) ) } ) ;
}
2024-03-06 22:34:15 +00:00
void cacheLeaderboardPositions ( String userID , PlayerLeaderboardPosition positions ) {
_lbPositions [ userID ] = positions ;
}
PlayerLeaderboardPosition ? getCachedLeaderboardPositions ( String userID ) {
return _lbPositions [ userID ] ;
}
2024-06-03 23:42:44 +00:00
void cacheRoutine ( ) {
_cache . removeOld ( ) ;
}
2024-01-29 21:13:07 +00:00
/// Downloads replay from inoue (szy API). Requiers [replayID]. If request have
/// different from 200 statusCode, it will throw an excepction. Returns list, that contains same replay
/// as string and as binary.
2024-06-03 21:06:00 +00:00
Future < RawReplay > szyGetReplay ( String replayID ) async {
// Trying to get it from cache first
RawReplay ? cached = _cache . get ( replayID , RawReplay ) ;
if ( cached ! = null ) return cached ;
// If failed, trying to obtain replay from download directory
if ( ! kIsWeb ) { // can't obtain download directory on web
var downloadPath = await getDownloadsDirectory ( ) ;
downloadPath ? ? = Platform . isAndroid ? Directory ( " /storage/emulated/0/Download " ) : await getApplicationDocumentsDirectory ( ) ;
var replayFile = File ( " ${ downloadPath . path } / $ replayID .ttrm " ) ;
if ( replayFile . existsSync ( ) ) return RawReplay ( replayID , replayFile . readAsBytesSync ( ) , replayFile . readAsStringSync ( ) ) ;
2024-01-06 22:54:00 +00:00
}
2024-01-22 18:00:24 +00:00
2024-06-03 21:06:00 +00:00
// If failed, actually trying to retrieve
2024-01-22 18:00:24 +00:00
Uri url ;
2024-01-29 21:13:07 +00:00
if ( kIsWeb ) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
2024-01-22 18:00:24 +00:00
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " tetrioReplay " , " replayid " : replayID } ) ;
2024-01-29 21:13:07 +00:00
} else { // Actually going to hit inoue
2024-01-22 18:00:24 +00:00
url = Uri . https ( ' inoue.szy.lol ' , ' /api/replay/ $ replayID ' ) ;
}
2023-10-18 21:50:41 +00:00
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
2024-06-03 21:06:00 +00:00
developer . log ( " szyDownload: Replay $ replayID downloaded " , name: " services/tetrio_crud " ) ;
RawReplay replay = RawReplay ( replayID , response . bodyBytes , response . body ) ;
DateTime now = DateTime . now ( ) ;
_cache . store ( replay , now . millisecondsSinceEpoch + 3600000 ) ;
return replay ;
2024-02-01 00:15:32 +00:00
// if not 200 - throw a unique for each code exception
2023-10-18 21:50:41 +00:00
case 404 :
throw SzyNotFound ( ) ;
case 403 :
throw SzyForbidden ( ) ;
case 429 :
throw SzyTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw SzyInternalProblem ( ) ;
default :
2024-06-03 21:06:00 +00:00
developer . log ( " szyDownload: Failed to download a replay $ replayID " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2023-10-18 21:50:41 +00:00
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
2024-02-01 00:15:32 +00:00
} on http . ClientException catch ( e , s ) { // If local http client fails
2023-10-18 21:50:41 +00:00
developer . log ( " $ e , $ s " ) ;
2024-02-01 00:15:32 +00:00
throw http . ClientException ( e . message , e . uri ) ; // just assuming, that our end user don't have acess to the internet
2023-10-18 21:50:41 +00:00
}
}
2024-02-01 00:15:32 +00:00
/// 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
2024-01-08 22:42:49 +00:00
Future < String > saveReplay ( String replayID ) async {
2024-01-05 23:11:45 +00:00
var downloadPath = await getDownloadsDirectory ( ) ;
downloadPath ? ? = Platform . isAndroid ? Directory ( " /storage/emulated/0/Download " ) : await getApplicationDocumentsDirectory ( ) ;
var replayFile = File ( " ${ downloadPath . path } / $ replayID .ttrm " ) ;
if ( replayFile . existsSync ( ) ) throw TetrioReplayAlreadyExist ( ) ;
2024-06-03 21:06:00 +00:00
RawReplay replay = await szyGetReplay ( replayID ) ;
await replayFile . writeAsBytes ( replay . asBytes ) ;
2024-01-05 23:11:45 +00:00
return replayFile . path ;
}
2024-02-01 00:15:32 +00:00
/// 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
2024-01-22 19:39:28 +00:00
Future < ReplayData > analyzeReplay ( String replayID , bool isAvailable ) async {
2024-02-01 00:15:32 +00:00
// trying retirieve existing stats from DB first
2024-01-08 22:42:49 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final results = await db . query ( tetrioTLReplayStatsTable , where: ' $ idCol = ? ' , whereArgs: [ replayID ] ) ;
2024-02-01 00:15:32 +00:00
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
2024-06-03 21:06:00 +00:00
String replay = ( await szyGetReplay ( replayID ) ) . asString ;
2024-02-01 00:15:32 +00:00
Map < String , dynamic > toAnalyze = jsonDecode ( replay ) ;
2024-01-08 22:42:49 +00:00
ReplayData data = ReplayData . fromJson ( toAnalyze ) ;
2024-02-01 00:15:32 +00:00
saveReplayStats ( data ) ; // saving to DB for later
2024-01-08 22:42:49 +00:00
return data ;
2024-01-05 23:11:45 +00:00
}
2024-02-01 14:38:11 +00:00
2024-06-12 22:32:45 +00:00
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve.
Future < SingleplayerStream > fetchSingleplayerStream ( String userID , String stream ) async {
SingleplayerStream ? cached = _cache . get ( userID , SingleplayerStream ) ;
if ( cached ! = null ) return cached ;
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " singleplayerStream " , " user " : userID . toLowerCase ( ) . trim ( ) , " stream " : stream } ) ;
} else {
url = Uri . https ( ' ch.tetr.io ' , ' api/streams/ ${ stream } _ ${ userID . toLowerCase ( ) . trim ( ) } ' ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
if ( jsonDecode ( response . body ) [ ' success ' ] ) {
SingleplayerStream records = SingleplayerStream . fromJson ( jsonDecode ( response . body ) [ ' data ' ] [ ' records ' ] , userID , stream ) ;
_cache . store ( records , jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] ) ;
developer . log ( " fetchSingleplayerStream: $ stream $ userID stream retrieved and cached " , name: " services/tetrio_crud " ) ;
return records ;
} else {
developer . log ( " fetchSingleplayerStream: User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw TetrioPlayerNotExist ( ) ;
}
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
developer . log ( " fetchSingleplayerStream: Failed to fetch stream $ stream $ userID " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
}
}
2024-02-01 14:38:11 +00:00
/// Gets and returns Top TR for a player with given [id]. May return null if player top tr is unknown
2024-02-01 00:15:32 +00:00
/// or api is unavaliable (404). May throw an exception, if something else happens.
2024-06-03 21:06:00 +00:00
Future < TopTr ? > fetchTopTR ( String id ) async {
// Trying to get it from cache first
TopTr ? cached = _cache . get ( id , TopTr ) ;
if ( cached ! = null ) return cached ;
2023-10-08 17:20:42 +00:00
Uri url ;
2024-02-01 00:15:32 +00:00
if ( kIsWeb ) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
2023-10-09 18:48:50 +00:00
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " PeakTR " , " user " : id } ) ;
2024-02-01 00:15:32 +00:00
} else { // Actually going to hit p1nkl0bst3r api
2023-10-08 17:20:42 +00:00
url = Uri . https ( ' api.p1nkl0bst3r.xyz ' , ' toptr/ $ id ' ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
2024-02-01 00:15:32 +00:00
case 200 : // ok - return the value
2024-06-03 21:06:00 +00:00
TopTr result = TopTr ( id , double . tryParse ( response . body ) ) ;
_cache . store ( result , DateTime . now ( ) . millisecondsSinceEpoch + 300000 ) ;
return result ;
2024-02-01 00:15:32 +00:00
case 404 : // not found - return null
2024-06-03 21:06:00 +00:00
TopTr result = TopTr ( id , null ) ;
2023-10-08 17:20:42 +00:00
developer . log ( " fetchTopTR: Probably, player doesn't have top TR " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2024-06-03 21:06:00 +00:00
_cache . store ( result , DateTime . now ( ) . millisecondsSinceEpoch + 300000 ) ;
return result ;
2024-02-01 00:15:32 +00:00
// if not 200 or 404 - throw a unique for each code exception
2023-10-08 17:20:42 +00:00
case 403 :
throw P1nkl0bst3rForbidden ( ) ;
case 429 :
throw P1nkl0bst3rTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
2024-06-19 13:11:20 +00:00
TopTr result = TopTr ( id , null ) ;
developer . log ( " fetchTopTR: API returned ${ response . statusCode } " , name: " services/tetrio_crud " , error: response . statusCode ) ;
//_cache.store(result, DateTime.now().millisecondsSinceEpoch + 300000);
return result ;
2023-10-08 17:20:42 +00:00
default :
developer . log ( " fetchTopTR: Failed to fetch top TR " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
2024-02-01 00:15:32 +00:00
} on http . ClientException catch ( e , s ) { // If local http client fails
2023-10-08 17:20:42 +00:00
developer . log ( " $ e , $ s " ) ;
2024-02-01 00:15:32 +00:00
throw http . ClientException ( e . message , e . uri ) ; // just assuming, that our end user don't have acess to the internet
2023-10-08 17:20:42 +00:00
}
}
2024-02-01 00:15:32 +00:00
// 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
2024-06-03 21:06:00 +00:00
Future < Cutoffs ? > fetchCutoffs ( ) async {
Cutoffs ? cached = _cache . get ( " " , Cutoffs ) ;
if ( cached ! = null ) return cached ;
2024-05-02 22:26:12 +00:00
Uri url ;
if ( kIsWeb ) {
2024-05-04 20:04:48 +00:00
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " TLCutoffs " } ) ;
2024-05-02 22:26:12 +00:00
} else {
url = Uri . https ( ' api.p1nkl0bst3r.xyz ' , ' rankcutoff ' , { " users " : null } ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
Map < String , dynamic > rawData = jsonDecode ( response . body ) ;
Map < String , dynamic > data = rawData [ " cutoffs " ] as Map < String , dynamic > ;
2024-06-03 21:06:00 +00:00
Cutoffs result = Cutoffs ( { } , { } ) ;
2024-05-02 22:26:12 +00:00
for ( String rank in data . keys ) {
2024-06-03 21:06:00 +00:00
result . tr [ rank ] = data [ rank ] [ " rating " ] ;
result . glicko [ rank ] = data [ rank ] [ " glicko " ] ;
2024-05-02 22:26:12 +00:00
}
2024-06-03 21:06:00 +00:00
_cache . store ( result , rawData [ " ts " ] + 300000 ) ;
return result ;
2024-05-02 22:26:12 +00:00
case 404 :
developer . log ( " fetchCutoffs: Cutoffs are gone " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2024-06-03 21:06:00 +00:00
return null ;
2024-05-02 22:26:12 +00:00
// if not 200 or 404 - throw a unique for each code exception
case 403 :
throw P1nkl0bst3rForbidden ( ) ;
case 429 :
throw P1nkl0bst3rTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
2024-06-19 13:11:20 +00:00
developer . log ( " fetchCutoffs: Cutoffs are unavalable ( ${ response . statusCode } ) " , name: " services/tetrio_crud " , error: response . statusCode ) ;
return null ;
2024-05-02 22:26:12 +00:00
default :
developer . log ( " fetchCutoffs: Failed to fetch top Cutoffs " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
} on http . ClientException catch ( e , s ) { // If local http client fails
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ; // just assuming, that our end user don't have acess to the internet
}
}
2024-05-04 20:04:48 +00:00
Future < TetrioPlayerFromLeaderboard > fetchTopOneFromTheLeaderboard ( ) async {
2024-06-03 21:06:00 +00:00
TetrioPlayerFromLeaderboard ? cached = _cache . get ( " topone " , TetrioPlayerFromLeaderboard ) ;
if ( cached ! = null ) return cached ;
2024-05-04 20:04:48 +00:00
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " TLTopOne " } ) ;
} else {
url = Uri . https ( ' ch.tetr.io ' , ' api/users/lists/league ' , { " after " : " 25000 " , " limit " : " 1 " } ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
var rawJson = jsonDecode ( response . body ) ;
2024-06-03 21:06:00 +00:00
TetrioPlayerFromLeaderboard result = TetrioPlayerFromLeaderboard . fromJson ( rawJson [ " data " ] [ " users " ] [ 0 ] , DateTime . fromMillisecondsSinceEpoch ( rawJson [ " cache " ] [ " cached_at " ] ) ) ;
_cache . store ( result , rawJson [ " cache " ] [ " cached_until " ] ) ;
return result ;
2024-05-04 20:04:48 +00:00
case 404 :
throw TetrioPlayerNotExist ( ) ;
// if not 200 or 404 - throw a unique for each code exception
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw P1nkl0bst3rInternalProblem ( ) ;
default :
developer . log ( " fetchTopOneFromTheLeaderboard: Failed to fetch top one " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
} on http . ClientException catch ( e , s ) { // If local http client fails
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ; // just assuming, that our end user don't have acess to the internet
}
}
2024-02-01 00:15:32 +00:00
/// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states
2024-02-01 14:38:11 +00:00
/// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data.
2023-07-17 17:57:24 +00:00
Future < List < TetrioPlayer > > fetchAndsaveTLHistory ( String id ) async {
2023-07-20 20:56:00 +00:00
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 ' ) ;
}
2023-09-23 19:09:36 +00:00
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
2024-02-01 00:15:32 +00:00
// that one api returns csv instead of json
2023-09-23 19:09:36 +00:00
List < List < dynamic > > csv = const CsvToListConverter ( ) . convert ( response . body ) . . removeAt ( 0 ) ;
List < TetrioPlayer > history = [ ] ;
2024-02-01 00:15:32 +00:00
// doesn't return nickname, need to retrieve it separately
2023-09-23 19:09:36 +00:00
String nick = await getNicknameByID ( id ) ;
2024-02-01 00:15:32 +00:00
for ( List < dynamic > entry in csv ) { // each entry is one state
2023-09-23 19:09:36 +00:00
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 ,
2024-02-01 00:15:32 +00:00
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
) ,
2023-09-23 19:09:36 +00:00
sprint: [ ] ,
blitz: [ ]
) ;
history . add ( state ) ;
}
2024-02-01 00:15:32 +00:00
// trying to dump it to local DB
2023-09-23 19:09:36 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
2024-02-06 20:38:52 +00:00
List < TetrioPlayer > states = await getPlayer ( id ) ;
if ( states . isEmpty ) await createPlayer ( history . first ) ;
2023-09-23 19:09:36 +00:00
states . insertAll ( 0 , history . reversed ) ;
final Map < String , dynamic > statesJson = { } ;
2024-02-01 00:15:32 +00:00
for ( var e in states ) { // making one big json out of this list
2023-09-23 19:09:36 +00:00
statesJson . addEntries ( { ( e . state . millisecondsSinceEpoch ~ / 1000 ) . toString ( ) : e . toJson ( ) } . entries ) ;
}
2024-02-01 00:15:32 +00:00
// and putting it to local DB
2023-09-23 19:09:36 +00:00
await db . update ( tetrioUsersTable , { idCol: id , nickCol: nick , statesCol: jsonEncode ( statesJson ) } , where: ' $ idCol = ? ' , whereArgs: [ id ] ) ;
return history ;
case 404 :
developer . log ( " fetchTLHistory: Probably, history doesn't exist " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw TetrioHistoryNotExist ( ) ;
case 403 :
throw P1nkl0bst3rForbidden ( ) ;
case 429 :
throw P1nkl0bst3rTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw P1nkl0bst3rInternalProblem ( ) ;
default :
developer . log ( " fetchTLHistory: Failed to fetch history " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
2023-07-18 17:53:43 +00:00
}
2023-09-23 19:09:36 +00:00
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
2023-07-17 17:57:24 +00:00
}
2023-06-17 21:50:52 +00:00
}
2024-02-03 13:02:58 +00:00
/// Docs later
Future < List < TetraLeagueAlphaRecord > > fetchAndSaveOldTLmatches ( String userID ) async {
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " TLMatches " , " user " : userID } ) ;
} else {
url = Uri . https ( ' api.p1nkl0bst3r.xyz ' , ' tlmatches/ $ userID ' ) ;
}
try {
final response = await client . get ( url ) ;
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 < TetraLeagueAlphaRecord > matches = [ ] ;
// parsing data into TetraLeagueAlphaRecord objects
for ( var entry in csv ) {
TetraLeagueAlphaRecord match = TetraLeagueAlphaRecord (
2024-03-24 16:38:06 +00:00
replayId: entry [ 0 ] . toString ( ) ,
ownId: entry [ 0 ] . toString ( ) , // i gonna disting p1nkl0bst3r entries with it
2024-02-03 13:02:58 +00:00
timestamp: DateTime . parse ( entry [ 1 ] ) ,
endContext: [
EndContextMulti (
2024-03-24 16:38:06 +00:00
userId: entry [ 2 ] . toString ( ) ,
2024-02-03 13:02:58 +00:00
username: entry [ 3 ] . toString ( ) ,
naturalOrder: 0 ,
inputs: - 1 ,
piecesPlaced: - 1 ,
handling: Handling ( arr: - 1 , das: - 1 , sdf: - 1 , dcd: 0 , cancel: true , safeLock: true ) ,
points: entry [ 4 ] ,
wins: entry [ 4 ] ,
secondary: entry [ 6 ] ,
secondaryTracking: [ ] ,
tertiary: entry [ 5 ] ,
tertiaryTracking: [ ] ,
extra: entry [ 7 ] ,
extraTracking: [ ] ,
success: true
) ,
EndContextMulti (
2024-03-24 16:38:06 +00:00
userId: entry [ 8 ] . toString ( ) ,
2024-02-03 13:02:58 +00:00
username: entry [ 9 ] . toString ( ) ,
naturalOrder: 1 ,
inputs: - 1 ,
piecesPlaced: - 1 ,
handling: Handling ( arr: - 1 , das: - 1 , sdf: - 1 , dcd: 0 , cancel: true , safeLock: true ) ,
points: entry [ 10 ] ,
wins: entry [ 10 ] ,
secondary: entry [ 12 ] ,
secondaryTracking: [ ] ,
tertiary: entry [ 11 ] ,
tertiaryTracking: [ ] ,
extra: entry [ 13 ] ,
extraTracking: [ ] ,
success: false
)
] ,
replayAvalable: false
) ;
matches . add ( match ) ;
}
// trying to dump it to local DB
TetraLeagueAlphaStream fakeStream = TetraLeagueAlphaStream ( userId: userID , records: matches ) ;
saveTLMatchesFromStream ( fakeStream ) ;
return matches ;
case 404 :
developer . log ( " fetchAndSaveOldTLmatches: Probably, history doesn't exist " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw TetrioHistoryNotExist ( ) ;
case 403 :
throw P1nkl0bst3rForbidden ( ) ;
case 429 :
throw P1nkl0bst3rTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw P1nkl0bst3rInternalProblem ( ) ;
default :
developer . log ( " fetchAndSaveOldTLmatches: Failed to fetch history " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
}
}
2024-02-01 00:15:32 +00:00
/// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve.
2023-07-07 20:32:57 +00:00
Future < TetrioPlayersLeaderboard > fetchTLLeaderboard ( ) async {
2024-06-03 21:06:00 +00:00
TetrioPlayersLeaderboard ? cached = _cache . get ( " league " , TetrioPlayersLeaderboard ) ;
if ( cached ! = null ) return cached ;
2023-07-20 20:56:00 +00:00
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 ' ) ;
}
2023-09-23 19:09:36 +00:00
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
2024-03-06 22:34:15 +00:00
_lbPositions . clear ( ) ;
2023-09-23 19:09:36 +00:00
var rawJson = jsonDecode ( response . body ) ;
2024-02-01 00:15:32 +00:00
if ( rawJson [ ' success ' ] ) { // if api confirmed that everything ok
2023-09-23 19:09:36 +00:00
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 " ) ;
2024-06-03 21:06:00 +00:00
//_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard;
_cache . store ( leaderboard , rawJson [ ' cache ' ] [ ' cached_until ' ] ) ;
2023-09-23 19:09:36 +00:00
return leaderboard ;
2024-02-01 00:15:32 +00:00
} else { // idk how to hit that one
2023-09-23 19:09:36 +00:00
developer . log ( " fetchTLLeaderboard: Bruh " , name: " services/tetrio_crud " , error: rawJson ) ;
2024-02-01 00:15:32 +00:00
throw Exception ( " Failed to get leaderboard (problems on the tetr.io side) " ) ; // will it be on tetr.io side?
2023-09-23 19:09:36 +00:00
}
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
developer . log ( " fetchTLLeaderboard: Failed to fetch leaderboard " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
2023-07-07 20:32:57 +00:00
}
2023-09-23 19:09:36 +00:00
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
2023-07-07 20:32:57 +00:00
}
}
2024-06-04 22:14:34 +00:00
// i want to know progress, so i trying to figure out this thing:
// Stream<TetrioPlayersLeaderboard> fetchTLLeaderboardAsStream() async {
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
// if (cached != null) return cached;
// 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');
// }
// Stream<TetrioPlayersLeaderboard> stream = http.StreamedRequest("GET", url);
// }
2024-03-04 22:05:59 +00:00
TetrioPlayersLeaderboard ? getCachedLeaderboard ( ) {
2024-06-03 21:06:00 +00:00
return _cache . get ( " league " , TetrioPlayersLeaderboard ) ;
2024-03-04 22:05:59 +00:00
}
2024-02-01 00:15:32 +00:00
/// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve.
2024-06-03 21:06:00 +00:00
Future < News > fetchNews ( String userID ) async {
News ? cached = _cache . get ( userID , News ) ;
if ( cached ! = null ) return cached ;
2023-10-07 16:44:54 +00:00
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " tetrioNews " , " user " : userID . toLowerCase ( ) . trim ( ) , " limit " : " 100 " } ) ;
} else {
url = Uri . https ( ' ch.tetr.io ' , ' api/news/user_ ${ userID . toLowerCase ( ) . trim ( ) } ' , { " limit " : " 100 " } ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
var payload = jsonDecode ( response . body ) ;
2024-02-01 00:15:32 +00:00
if ( payload [ ' success ' ] ) { // if api confirmed that everything ok
2024-06-03 21:06:00 +00:00
News news = News . fromJson ( payload [ ' data ' ] , userID ) ;
_cache . store ( news , payload [ ' cache ' ] [ ' cached_until ' ] ) ;
2024-02-01 00:15:32 +00:00
developer . log ( " fetchNews: $ userID news retrieved and cached " , name: " services/tetrio_crud " ) ;
2023-10-07 16:44:54 +00:00
return news ;
} else {
developer . log ( " fetchNews: User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw TetrioPlayerNotExist ( ) ;
}
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
developer . log ( " fetchNews: 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 ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
}
}
2024-02-01 00:15:32 +00:00
/// 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 {
2024-06-03 21:06:00 +00:00
TetraLeagueAlphaStream ? cached = _cache . get ( userID , TetraLeagueAlphaStream ) ;
if ( cached ! = null ) return cached ;
2023-06-21 19:17:39 +00:00
2023-07-20 20:56:00 +00:00
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 ( ) } ' ) ;
}
2023-09-23 19:09:36 +00:00
try {
final response = await client . get ( url ) ;
2023-06-20 20:53:28 +00:00
2023-09-23 19:09:36 +00:00
switch ( response . statusCode ) {
case 200 :
if ( jsonDecode ( response . body ) [ ' success ' ] ) {
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream . fromJson ( jsonDecode ( response . body ) [ ' data ' ] [ ' records ' ] , userID ) ;
2024-06-03 21:06:00 +00:00
_cache . store ( stream , jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] ) ;
2024-02-01 00:15:32 +00:00
developer . log ( " fetchTLStream: $ userID stream retrieved and cached " , name: " services/tetrio_crud " ) ;
2023-09-23 19:09:36 +00:00
return stream ;
} else {
2024-02-01 00:15:32 +00:00
developer . log ( " fetchTLStream User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
2023-09-23 19:09:36 +00:00
throw TetrioPlayerNotExist ( ) ;
}
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
2024-02-01 00:15:32 +00:00
developer . log ( " fetchTLStream Failed to fetch stream " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2023-09-23 19:09:36 +00:00
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
2023-06-20 20:53:28 +00:00
}
2023-09-23 19:09:36 +00:00
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
2023-06-20 20:53:28 +00:00
}
}
2024-02-01 00:15:32 +00:00
/// Saves Tetra League Matches from [stream] to the local DB.
2023-06-23 18:38:15 +00:00
Future < void > saveTLMatchesFromStream ( TetraLeagueAlphaStream stream ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-23 18:38:15 +00:00
final db = getDatabaseOrThrow ( ) ;
2024-02-01 00:15:32 +00:00
for ( TetraLeagueAlphaRecord match in stream . records ) { // putting then one by one
2023-10-16 21:41:45 +00:00
final results = await db . query ( tetraLeagueMatchesTable , where: ' $ replayID = ? ' , whereArgs: [ match . replayId ] ) ;
2024-02-01 00:15:32 +00:00
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 ( ) )
} ) ;
2023-06-23 18:38:15 +00:00
}
}
2024-02-01 00:15:32 +00:00
/// Deletes duplicate entries of Tetra League matches from local DB.
2023-10-16 21:41:45 +00:00
Future < void > removeDuplicatesFromTLMatches ( ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
await db . execute ( """
DELETE FROM $tetraLeagueMatchesTable
WHERE
$idCol IN (
SELECT
$idCol
FROM (
SELECT
$idCol ,
ROW_NUMBER ( ) OVER (
PARTITION BY $replayID
ORDER BY $replayID ) AS row_num
FROM $tetraLeagueMatchesTable
) t
WHERE row_num > 1
) ;
""" );
}
2024-02-01 00:15:32 +00:00
/// Gets and returns a list of matches from local DB for a given [playerID].
2023-06-23 18:38:15 +00:00
Future < List < TetraLeagueAlphaRecord > > getTLMatchesbyPlayerID ( String playerID ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-23 18:38:15 +00:00
final db = getDatabaseOrThrow ( ) ;
List < TetraLeagueAlphaRecord > matches = [ ] ;
final results = await db . query ( tetraLeagueMatchesTable , where: ' ( $ player1id = ?) OR ( $ player2id = ?) ' , whereArgs: [ playerID , playerID ] ) ;
for ( var match in results ) {
2024-02-01 00:15:32 +00:00
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
) ) ;
2023-06-23 18:38:15 +00:00
}
return matches ;
}
2024-03-13 22:44:53 +00:00
/// Gets and returns an amount of stored Tetra League mathes between [ourPlayerID] and [enemyPlayerID].
Future < int > getNumberOfTLMatchesBetweenPlayers ( String ourPlayerID , String enemyPlayerID ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final results = await db . rawQuery ( " SELECT COUNT(*) from tetrioAlphaLeagueMathces WHERE (player1id = $ ourPlayerID AND player2id = $ enemyPlayerID ) OR (player1id = $ enemyPlayerID AND player2id = $ ourPlayerID ) " ) ;
return results . first . values . first as int ;
}
2024-02-01 00:15:32 +00:00
/// Deletes match and stats of that match with given [matchID] from local DB. Throws an exception if fails.
2023-09-23 19:09:36 +00:00
Future < void > deleteTLMatch ( String matchID ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
2024-01-22 18:00:24 +00:00
final rID = ( await db . query ( tetraLeagueMatchesTable , where: ' $ idCol = ? ' , whereArgs: [ matchID ] ) ) . first [ replayID ] ;
2023-09-23 19:09:36 +00:00
final results = await db . delete ( tetraLeagueMatchesTable , where: ' $ idCol = ? ' , whereArgs: [ matchID ] ) ;
if ( results ! = 1 ) {
throw CouldNotDeleteMatch ( ) ;
}
2024-01-22 18:00:24 +00:00
await db . delete ( tetrioTLReplayStatsTable , where: ' $ idCol = ? ' , whereArgs: [ rID ] ) ;
2023-09-23 19:09:36 +00:00
}
2024-06-03 21:06:00 +00:00
/// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns `UserRecords`.
/// Throws an exception if fails to retrieve.
Future < UserRecords > fetchRecords ( String userID ) async {
UserRecords ? cached = _cache . get ( userID , UserRecords ) ;
if ( cached ! = null ) return cached ;
2023-06-21 22:05:14 +00:00
2023-07-20 20:56:00 +00:00
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 ' ) ;
}
2023-09-23 19:09:36 +00:00
try {
final response = await client . get ( url ) ;
2023-06-21 22:05:14 +00:00
2023-09-23 19:09:36 +00:00
switch ( response . statusCode ) {
case 200 :
if ( jsonDecode ( response . body ) [ ' success ' ] ) {
2024-06-03 21:06:00 +00:00
Map jsonRecords = jsonDecode ( response . body ) ;
2023-09-23 19:09:36 +00:00
var sprint = jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' record ' ] ! = null
2024-02-01 00:15:32 +00:00
? RecordSingle . fromJson ( jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' record ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' rank ' ] )
: null ;
2023-09-23 19:09:36 +00:00
var blitz = jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' record ' ] ! = null
2024-02-01 00:15:32 +00:00
? RecordSingle . fromJson ( jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' record ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' rank ' ] )
: null ;
2023-09-23 19:09:36 +00:00
var zen = TetrioZen . fromJson ( jsonRecords [ ' data ' ] [ ' zen ' ] ) ;
2024-06-03 21:06:00 +00:00
UserRecords result = UserRecords ( userID , sprint , blitz , zen ) ;
_cache . store ( result , jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] ) ;
2024-02-01 00:15:32 +00:00
developer . log ( " fetchRecords: $ userID records retrieved and cached " , name: " services/tetrio_crud " ) ;
2024-06-03 21:06:00 +00:00
return result ;
2023-09-23 19:09:36 +00:00
} else {
developer . log ( " fetchRecords User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw TetrioPlayerNotExist ( ) ;
}
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
developer . log ( " fetchRecords Failed to fetch records " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
2023-06-21 22:05:14 +00:00
}
2023-09-23 19:09:36 +00:00
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
2023-06-21 22:05:14 +00:00
}
}
2024-02-01 00:15:32 +00:00
/// Creates an entry in local DB for [tetrioPlayer]. Throws an exception if that player already here.
2023-06-17 21:50:52 +00:00
Future < void > createPlayer ( TetrioPlayer tetrioPlayer ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
final db = getDatabaseOrThrow ( ) ;
2024-02-01 00:15:32 +00:00
// checking if its already here
2023-06-17 21:50:52 +00:00
final results = await db . query ( tetrioUsersTable , limit: 1 , where: ' $ idCol = ? ' , whereArgs: [ tetrioPlayer . userId . toLowerCase ( ) ] ) ;
if ( results . isNotEmpty ) {
throw TetrioPlayerAlreadyExist ( ) ;
}
2024-02-01 00:15:32 +00:00
// converting to json and store
2023-07-18 17:53:43 +00:00
final Map < String , dynamic > statesJson = { ( tetrioPlayer . state . millisecondsSinceEpoch ~ / 1000 ) . toString ( ) : tetrioPlayer . toJson ( ) } ;
2023-06-17 21:50:52 +00:00
db . insert ( tetrioUsersTable , { idCol: tetrioPlayer . userId , nickCol: tetrioPlayer . username , statesCol: jsonEncode ( statesJson ) } ) ;
2024-02-06 20:38:52 +00:00
_players . addEntries ( { tetrioPlayer . userId: tetrioPlayer . username } . entries ) ;
2023-06-17 21:50:52 +00:00
_tetrioStreamController . add ( _players ) ;
}
2024-02-01 00:15:32 +00:00
/// Adds user id of [tetrioPlayer] to the [tetrioUsersToTrackTable] of database.
2023-06-17 21:50:52 +00:00
Future < void > addPlayerToTrack ( TetrioPlayer tetrioPlayer ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
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 } ) ;
}
2024-02-01 00:15:32 +00:00
/// Returns bool, which tells whether is given [id] is in [tetrioUsersToTrackTable].
2023-06-17 21:50:52 +00:00
Future < bool > isPlayerTracking ( String id ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
final db = getDatabaseOrThrow ( ) ;
final results = await db . query ( tetrioUsersToTrackTable , where: ' $ idCol = ? ' , whereArgs: [ id . toLowerCase ( ) ] ) ;
2023-09-23 19:09:36 +00:00
return results . isNotEmpty ;
2023-06-17 21:50:52 +00:00
}
2024-02-01 00:15:32 +00:00
/// Returns Iterable with user ids of players who is tracked.
2023-06-17 21:50:52 +00:00
Future < Iterable < String > > getAllPlayerToTrack ( ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final players = await db . query ( tetrioUsersToTrackTable ) ;
return players . map ( ( noteRow ) = > noteRow [ " id " ] . toString ( ) ) ;
}
2024-02-01 00:15:32 +00:00
/// Removes user with given [id] from the [tetrioUsersToTrackTable] of database.
2023-06-17 21:50:52 +00:00
Future < void > 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 {
2024-02-06 20:38:52 +00:00
_players . removeWhere ( ( key , value ) = > key = = id ) ;
_tetrioStreamController . add ( _players ) ;
2023-06-17 21:50:52 +00:00
}
}
2024-02-01 00:15:32 +00:00
/// Saves state (which is [tetrioPlayer]) to the local database.
2023-07-18 17:53:43 +00:00
Future < void > storeState ( TetrioPlayer tetrioPlayer ) async {
2024-02-06 20:38:52 +00:00
// if tetrio player doesn't have entry in database - just calling different function
List < TetrioPlayer > states = await getPlayer ( tetrioPlayer . userId ) ;
if ( states . isEmpty ) {
await createPlayer ( tetrioPlayer ) ;
return ;
2023-06-17 21:50:52 +00:00
}
2024-02-01 00:15:32 +00:00
// we not going to add state, that is same, as the previous
2024-06-03 21:06:00 +00:00
if ( ! states . last . isSameState ( tetrioPlayer ) ) states . add ( tetrioPlayer ) ;
2024-02-01 00:15:32 +00:00
// Making map of the states
2023-06-17 21:50:52 +00:00
final Map < String , dynamic > statesJson = { } ;
for ( var e in states ) {
2024-02-01 00:15:32 +00:00
// Saving in format: {"unix_seconds": json_of_state}
2023-07-18 17:53:43 +00:00
statesJson . addEntries ( { ( e . state . millisecondsSinceEpoch ~ / 1000 ) . toString ( ) : e . toJson ( ) } . entries ) ;
2023-06-17 21:50:52 +00:00
}
2024-02-06 20:38:52 +00:00
2024-02-01 00:15:32 +00:00
// Rewrite our database
2024-02-06 20:38:52 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
2023-07-17 17:57:24 +00:00
await db . update ( tetrioUsersTable , { idCol: tetrioPlayer . userId , nickCol: tetrioPlayer . username , statesCol: jsonEncode ( statesJson ) } ,
2023-06-17 21:50:52 +00:00
where: ' $ idCol = ? ' , whereArgs: [ tetrioPlayer . userId ] ) ;
}
2024-02-01 00:15:32 +00:00
/// Remove state (which is [tetrioPlayer]) from the local database
2023-06-17 21:50:52 +00:00
Future < void > deleteState ( TetrioPlayer tetrioPlayer ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
final db = getDatabaseOrThrow ( ) ;
2024-02-06 20:38:52 +00:00
List < TetrioPlayer > states = await getPlayer ( tetrioPlayer . userId ) ;
2024-02-01 00:15:32 +00:00
// removing state from map that contain every state of each user
2024-02-06 20:38:52 +00:00
states . removeWhere ( ( element ) = > element . state = = tetrioPlayer . state ) ;
2024-02-01 00:15:32 +00:00
// Making map of the states (without deleted one)
2023-06-17 21:50:52 +00:00
final Map < String , dynamic > statesJson = { } ;
for ( var e in states ) {
2024-02-01 00:15:32 +00:00
statesJson . addEntries ( { ( e . state . millisecondsSinceEpoch ~ / 1000 ) . toString ( ) : e . toJson ( ) } . entries ) ;
2023-06-17 21:50:52 +00:00
}
2024-02-01 00:15:32 +00:00
// Rewriting database entry with new json
2023-07-17 17:57:24 +00:00
await db . update ( tetrioUsersTable , { idCol: tetrioPlayer . userId , nickCol: tetrioPlayer . username , statesCol: jsonEncode ( statesJson ) } ,
2023-06-17 21:50:52 +00:00
where: ' $ idCol = ? ' , whereArgs: [ tetrioPlayer . userId ] ) ;
_tetrioStreamController . add ( _players ) ;
}
2024-02-01 00:15:32 +00:00
/// Returns list of all states of player with given [id] from database. Can return empty list if player
/// was not found.
2023-06-17 21:50:52 +00:00
Future < List < TetrioPlayer > > getPlayer ( String id ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
final db = getDatabaseOrThrow ( ) ;
List < TetrioPlayer > states = [ ] ;
final results = await db . query ( tetrioUsersTable , limit: 1 , where: ' $ idCol = ? ' , whereArgs: [ id . toLowerCase ( ) ] ) ;
if ( results . isEmpty ) {
2024-02-01 00:15:32 +00:00
return states ; // it empty
2023-06-17 21:50:52 +00:00
} else {
dynamic rawStates = results . first [ ' jsonStates ' ] as String ;
rawStates = json . decode ( rawStates ) ;
2024-02-01 00:15:32 +00:00
// recreating objects of states
2023-07-18 17:53:43 +00:00
rawStates . forEach ( ( k , v ) = > states . add ( TetrioPlayer . fromJson ( v , DateTime . fromMillisecondsSinceEpoch ( int . parse ( k ) * 1000 ) , id , results . first [ nickCol ] as String ) ) ) ;
2024-02-01 00:15:32 +00:00
// updating the stream
2023-06-17 21:50:52 +00:00
_players . removeWhere ( ( key , value ) = > key = = id ) ;
2024-02-06 20:38:52 +00:00
_players . addEntries ( { states . last . userId: states . last . username } . entries ) ;
2023-06-17 21:50:52 +00:00
_tetrioStreamController . add ( _players ) ;
return states ;
}
}
2024-02-01 00:15:32 +00:00
/// 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.
2023-07-29 18:01:49 +00:00
Future < TetrioPlayer > fetchPlayer ( String user , { bool isItDiscordID = false } ) async {
2024-06-03 21:06:00 +00:00
TetrioPlayer ? cached = _cache . get ( user , TetrioPlayer ) ;
if ( cached ! = null ) return cached ;
2023-07-29 18:01:49 +00:00
if ( isItDiscordID ) {
2024-02-01 00:15:32 +00:00
// trying to find player with given discord id
2023-07-29 18:01:49 +00:00
Uri dUrl ;
if ( kIsWeb ) {
dUrl = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " tetrioUserByDiscordID " , " user " : user . toLowerCase ( ) . trim ( ) } ) ;
} else {
dUrl = Uri . https ( ' ch.tetr.io ' , ' api/users/search/ ${ user . toLowerCase ( ) . trim ( ) } ' ) ;
}
2023-09-23 19:09:36 +00:00
try {
final response = await client . get ( dUrl ) ;
switch ( response . statusCode ) {
case 200 :
var json = jsonDecode ( response . body ) ;
if ( json [ ' success ' ] & & json [ ' data ' ] ! = null ) {
2024-02-01 00:15:32 +00:00
// success - rewrite user with tetrio user id and going to obtain data about him
2023-09-23 19:09:36 +00:00
user = json [ ' data ' ] [ ' user ' ] [ ' _id ' ] ;
2024-02-01 00:15:32 +00:00
} else { // fail - throw an exception
2023-09-23 19:09:36 +00:00
developer . log ( " fetchPlayer User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
2024-03-20 22:56:13 +00:00
throw TetrioDiscordNotExist ( ) ;
2023-09-23 19:09:36 +00:00
}
break ;
2024-02-01 00:15:32 +00:00
// more exceptions to god of exceptions
2023-09-23 19:09:36 +00:00
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
developer . log ( " fetchPlayer Failed to fetch player " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
2023-07-29 18:01:49 +00:00
}
2023-09-23 19:09:36 +00:00
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
2023-07-29 18:01:49 +00:00
}
}
2024-02-01 00:15:32 +00:00
// finally going to obtain
2023-07-20 20:56:00 +00:00
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 ( ) } ' ) ;
}
2023-09-23 19:09:36 +00:00
try {
final response = await client . get ( url ) ;
2023-06-17 21:50:52 +00:00
2023-09-23 19:09:36 +00:00
switch ( response . statusCode ) {
case 200 :
var json = jsonDecode ( response . body ) ;
if ( json [ ' success ' ] ) {
2024-02-01 00:15:32 +00:00
// parse and count stats
2024-06-03 23:42:44 +00:00
TetrioPlayer player = TetrioPlayer . fromJson ( json [ ' data ' ] [ ' user ' ] , DateTime . fromMillisecondsSinceEpoch ( json [ ' cache ' ] [ ' cached_at ' ] , isUtc: true ) , json [ ' data ' ] [ ' user ' ] [ ' _id ' ] , json [ ' data ' ] [ ' user ' ] [ ' username ' ] , DateTime . fromMillisecondsSinceEpoch ( json [ ' cache ' ] [ ' cached_until ' ] , isUtc: true ) ) ;
2024-06-03 21:06:00 +00:00
_cache . store ( player , json [ ' cache ' ] [ ' cached_until ' ] ) ;
2024-02-01 00:15:32 +00:00
developer . log ( " fetchPlayer: $ user retrieved and cached " , name: " services/tetrio_crud " ) ;
2023-09-23 19:09:36 +00:00
return player ;
} else {
developer . log ( " fetchPlayer User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw TetrioPlayerNotExist ( ) ;
}
case 403 :
throw TetrioForbidden ( ) ;
case 429 :
throw TetrioTooManyRequests ( ) ;
case 418 :
throw TetrioOskwareBridgeProblem ( ) ;
case 500 :
case 502 :
case 503 :
case 504 :
throw TetrioInternalProblem ( ) ;
default :
developer . log ( " fetchPlayer Failed to fetch player " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
2023-06-17 21:50:52 +00:00
}
2023-09-23 19:09:36 +00:00
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
2023-06-17 21:50:52 +00:00
}
}
2024-02-06 20:38:52 +00:00
/// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database
Future < Map < String , List < TetrioPlayer > > > getAllPlayers ( ) async {
2023-06-17 21:50:52 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final players = await db . query ( tetrioUsersTable ) ;
Map < String , List < TetrioPlayer > > data = { } ;
2024-02-06 20:38:52 +00:00
for ( var entry in players ) {
var test = json . decode ( entry [ ' jsonStates ' ] as String ) ;
2023-06-17 21:50:52 +00:00
List < TetrioPlayer > states = [ ] ;
2024-02-06 20:38:52 +00:00
test . forEach ( ( k , v ) = > states . add ( TetrioPlayer . fromJson ( v , DateTime . fromMillisecondsSinceEpoch ( int . parse ( k ) * 1000 ) , entry [ idCol ] as String , entry [ nickCol ] as String ) ) ) ;
2023-06-17 21:50:52 +00:00
data . addEntries ( { states . last . userId: states } . entries ) ;
2024-02-06 20:38:52 +00:00
}
return data ;
2023-06-17 21:50:52 +00:00
}
2024-06-04 22:14:34 +00:00
Future < void > fetchTracked ( ) async {
for ( String userID in ( await getAllPlayerToTrack ( ) ) ) {
TetrioPlayer player = await fetchPlayer ( userID ) ;
storeState ( player ) ;
sleep ( Durations . extralong4 ) ;
TetraLeagueAlphaStream matches = await fetchTLStream ( userID ) ;
saveTLMatchesFromStream ( matches ) ;
sleep ( Durations . extralong4 ) ;
}
}
2023-06-17 21:50:52 +00:00
}