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 ' ;
import ' package:path_provider/path_provider.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 " )
)
''' ;
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-01-29 21:13:07 +00:00
// I'm trying to send as less requests, as possible, so i'm caching the results of those requests.
// Usually those maps looks like this: {"cached_until_unix_milliseconds": Object}
2023-06-21 19:17:39 +00:00
final Map < String , TetrioPlayer > _playersCache = { } ;
2023-06-21 22:05:14 +00:00
final Map < String , Map < String , dynamic > > _recordsCache = { } ;
2024-01-29 21:13:07 +00:00
final Map < String , dynamic > _replaysCache = { } ; // the only one is different: {"replayID": [replayString, replayBytes]}
2023-07-07 20:32:57 +00:00
final Map < String , TetrioPlayersLeaderboard > _leaderboardsCache = { } ;
2024-03-06 22:34:15 +00:00
final Map < String , PlayerLeaderboardPosition > _lbPositions = { } ;
2023-10-07 16:44:54 +00:00
final Map < String , List < News > > _newsCache = { } ;
2023-10-08 17:20:42 +00:00
final Map < String , Map < String , double ? > > _topTRcache = { } ;
2024-01-29 21:13:07 +00:00
final Map < String , TetraLeagueAlphaStream > _tlStreamsCache = { } ;
/// 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-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-01-05 23:11:45 +00:00
Future < List < dynamic > > szyGetReplay ( String replayID ) async {
2024-01-29 21:13:07 +00:00
try { // read from cache
2024-01-06 22:54:00 +00:00
var cached = _replaysCache . entries . firstWhere ( ( element ) = > element . key = = replayID ) ;
return cached . value ;
} catch ( e ) {
// actually going to obtain
}
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 ' ) ;
}
2024-01-29 21:13:07 +00:00
// Trying to obtain replay from download directory first
2024-01-22 18:00:24 +00:00
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 [ replayFile . readAsStringSync ( ) , replayFile . readAsBytesSync ( ) ] ;
}
2023-10-18 21:50:41 +00:00
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
2024-01-05 23:11:45 +00:00
developer . log ( " szyDownload: Replay downloaded " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2024-01-29 21:13:07 +00:00
_replaysCache [ replayID ] = [ response . body , response . bodyBytes ] ; // Puts results into the cache
2024-01-05 23:11:45 +00:00
return [ response . body , response . bodyBytes ] ;
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-01-05 23:11:45 +00:00
developer . log ( " szyDownload: Failed to download a replay " , 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 ( ) ;
var replay = await szyGetReplay ( replayID ) ;
await replayFile . writeAsBytes ( replay [ 1 ] ) ;
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
String replay = ( await szyGetReplay ( replayID ) ) [ 0 ] ;
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
/// 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.
2023-10-08 17:20:42 +00:00
Future < double ? > fetchTopTR ( String id ) async {
2024-02-01 00:15:32 +00:00
try { // read from cache
2023-10-08 17:20:42 +00:00
var cached = _topTRcache . entries . firstWhere ( ( element ) = > element . value . keys . first = = id ) ;
2024-02-01 00:15:32 +00:00
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) { // if not expired
2023-10-08 17:20:42 +00:00
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 ;
2024-02-01 00:15:32 +00:00
} else { // if cache expired
2023-10-08 17:20:42 +00:00
_topTRcache . remove ( cached . key ) ;
developer . log ( " fetchTopTR: Top TR expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
2024-02-01 00:15:32 +00:00
} catch ( e ) { // actually going to obtain
2023-10-08 17:20:42 +00:00
developer . log ( " fetchTopTR: Trying to retrieve Top TR " , name: " services/tetrio_crud " ) ;
}
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
2023-10-08 17:20:42 +00:00
_topTRcache [ ( DateTime . now ( ) . millisecondsSinceEpoch + 300000 ) . toString ( ) ] = { id: double . tryParse ( response . body ) } ;
return double . tryParse ( response . body ) ;
2024-02-01 00:15:32 +00:00
case 404 : // not found - return 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 ) ;
_topTRcache [ ( DateTime . now ( ) . millisecondsSinceEpoch + 300000 ) . toString ( ) ] = { id: null } ;
return null ;
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 :
throw P1nkl0bst3rInternalProblem ( ) ;
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
/// 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 (
replayId: entry [ 0 ] ,
ownId: entry [ 0 ] , // i gonna disting p1nkl0bst3r entries with it
timestamp: DateTime . parse ( entry [ 1 ] ) ,
endContext: [
EndContextMulti (
userId: entry [ 2 ] ,
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 (
userId: entry [ 8 ] ,
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 {
try {
var cached = _leaderboardsCache . entries . firstWhere ( ( element ) = > element . value . type = = " league " ) ;
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) {
developer . log ( " fetchTLLeaderboard: Leaderboard retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
return cached . value ;
} else {
_leaderboardsCache . remove ( cached . key ) ;
developer . log ( " fetchTLLeaderboard: Leaderboard expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
} catch ( e ) {
developer . log ( " fetchTLLeaderboard: Trying to retrieve leaderboard " , name: " services/tetrio_crud " ) ;
}
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 " ) ;
_leaderboardsCache [ rawJson [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = leaderboard ;
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-03-04 22:05:59 +00:00
TetrioPlayersLeaderboard ? getCachedLeaderboard ( ) {
return _leaderboardsCache . entries . firstOrNull ? . value ;
// That function will break if i decide to recive other leaderboards
// TODO: Think about better solution
}
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.
2023-10-07 16:44:54 +00:00
Future < List < News > > fetchNews ( String userID ) async {
try {
var cached = _newsCache . entries . firstWhere ( ( element ) = > element . value [ 0 ] . stream = = " user_ $ userID " ) ;
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) {
developer . log ( " fetchNews: News for $ userID retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
return cached . value ;
} else {
_newsCache . remove ( cached . key ) ;
developer . log ( " fetchNews: Cached news for $ userID expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
} catch ( e ) {
developer . log ( " fetchNews: Trying to retrieve news for $ userID " , name: " services/tetrio_crud " ) ;
}
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
2023-10-07 16:44:54 +00:00
List < News > news = [ for ( var entry in payload [ ' data ' ] [ ' news ' ] ) News . fromJson ( entry ) ] ;
_newsCache [ payload [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = news ;
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 {
2023-06-21 19:17:39 +00:00
try {
var cached = _tlStreamsCache . entries . firstWhere ( ( element ) = > element . value . userId = = userID ) ;
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) {
2024-02-01 00:15:32 +00:00
developer . log ( " fetchTLStream: Stream $ userID retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
2023-06-21 19:17:39 +00:00
return cached . value ;
} else {
_tlStreamsCache . remove ( cached . key ) ;
2024-02-01 00:15:32 +00:00
developer . log ( " fetchTLStream: Cached stream $ userID expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
2023-06-21 19:17:39 +00:00
}
} catch ( e ) {
2024-02-01 00:15:32 +00:00
developer . log ( " fetchTLStream: Trying to retrieve stream $ userID " , name: " services/tetrio_crud " ) ;
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 ) ;
_tlStreamsCache [ jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = stream ;
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-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-02-06 20:38:52 +00:00
/// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns Map, which contains user id (`user`),
2024-02-01 00:15:32 +00:00
/// Blitz (`blitz`) and 40 Lines (`sprint`) record objects and Zen object (`zen`). Throws an exception if fails to retrieve.
2023-06-21 22:05:14 +00:00
Future < Map < String , dynamic > > fetchRecords ( String userID ) async {
try {
var cached = _recordsCache . entries . firstWhere ( ( element ) = > element . value [ ' user ' ] = = userID ) ;
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) {
developer . log ( " fetchRecords: $ userID records retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
return cached . value ;
} else {
_recordsCache . remove ( cached . key ) ;
developer . log ( " fetchRecords: $ userID records expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
} catch ( e ) {
developer . log ( " fetchRecords: Trying to retrieve $ userID records " , name: " services/tetrio_crud " ) ;
}
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 ' ] ) {
Map jsonRecords = jsonDecode ( response . body ) ;
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 ' ] ) ;
Map < String , dynamic > map = { " user " : userID . toLowerCase ( ) . trim ( ) , " sprint " : sprint , " blitz " : blitz , " zen " : zen } ;
_recordsCache [ jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = map ;
2024-02-01 00:15:32 +00:00
developer . log ( " fetchRecords: $ userID records retrieved and cached " , name: " services/tetrio_crud " ) ;
2023-09-23 19:09:36 +00:00
return map ;
} 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-02-06 20:38:52 +00:00
bool test = states . last . isSameState ( tetrioPlayer ) ;
2023-07-18 17:53:43 +00:00
if ( test = = false ) 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 {
2023-06-21 19:17:39 +00:00
try {
var cached = _playersCache . entries . firstWhere ( ( element ) = > element . value . userId = = user | | element . value . username = = user ) ;
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) {
developer . log ( " fetchPlayer: User $ user retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
return cached . value ;
} else {
_playersCache . remove ( cached . key ) ;
developer . log ( " fetchPlayer: Cached user $ user expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
} catch ( e ) {
developer . log ( " fetchPlayer: Trying to retrieve $ user " , name: " services/tetrio_crud " ) ;
}
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 ) ;
throw TetrioPlayerNotExist ( ) ;
}
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
2023-09-23 19:09:36 +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 ' ] ) ;
_playersCache [ jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = player ;
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
}
}