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 ' ;
import ' package:path_provider/path_provider.dart ' ;
2024-09-02 21:17:09 +00:00
import ' package:sqflite_common_ffi/sqflite_ffi.dart ' ;
2024-09-05 21:42:21 +00:00
import ' package:tetra_stats/data_objects/cutoff_tetrio.dart ' ;
import ' package:tetra_stats/data_objects/end_context_multi.dart ' ;
import ' package:tetra_stats/data_objects/news.dart ' ;
import ' package:tetra_stats/data_objects/p1nkl0bst3r.dart ' ;
import ' package:tetra_stats/data_objects/player_leaderboard_position.dart ' ;
import ' package:tetra_stats/data_objects/record_single.dart ' ;
import ' package:tetra_stats/data_objects/singleplayer_stream.dart ' ;
import ' package:tetra_stats/data_objects/summaries.dart ' ;
import ' package:tetra_stats/data_objects/tetra_league.dart ' ;
import ' package:tetra_stats/data_objects/tetra_league_alpha_record.dart ' ;
import ' package:tetra_stats/data_objects/tetra_league_alpha_stream.dart ' ;
import ' package:tetra_stats/data_objects/tetra_league_beta_stream.dart ' ;
import ' package:tetra_stats/data_objects/tetrio_constants.dart ' ;
2024-01-05 23:11:45 +00:00
import ' package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart ' ;
2024-09-05 21:42:21 +00:00
import ' package:tetra_stats/data_objects/tetrio_player.dart ' ;
import ' package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart ' ;
import ' package:tetra_stats/data_objects/tetrio_players_leaderboard.dart ' ;
import ' package:tetra_stats/data_objects/tetrio_zen.dart ' ;
import ' package:tetra_stats/data_objects/user_records.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 ' ;
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 " ;
2024-09-01 21:44:19 +00:00
const String tetrioLeagueTable = " tetrioLeague " ;
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-09-01 21:44:19 +00:00
const String createTetrioLeagueTable = '''
CREATE TABLE IF NOT EXISTS " tetrioLeague " (
" id " TEXT NOT NULL ,
" gamesplayed " INTEGER NOT NULL DEFAULT 0 ,
" gameswon " INTEGER NOT NULL DEFAULT 0 ,
" tr " REAL ,
" glicko " REAL ,
" rd " REAL ,
" gxe " REAL ,
" rank " TEXT NOT NULL DEFAULT ' z ' ,
" bestrank " TEXT NOT NULL DEFAULT ' z ' ,
" apm " REAL ,
" pps " REAL ,
" vs " REAL ,
" decaying " INTEGER NOT NULL DEFAULT 0 ,
" standing " INTEGER NOT NULL DEFAULT - 1 ,
" standing_local " INTEGER NOT NULL DEFAULT - 1 ,
" percentile " REAL NOT NULL ,
" prev_rank " TEXT ,
" prev_at " INTEGER NOT NULL DEFAULT - 1 ,
" next_rank " TEXT ,
" next_at " INTEGER NOT NULL DEFAULT - 1 ,
" percentile_rank " TEXT NOT NULL DEFAULT ' z ' ,
2024-09-02 21:17:09 +00:00
" season " INTEGER NOT NULL DEFAULT 1 ,
PRIMARY KEY ( " id " )
2024-09-01 21:44:19 +00:00
)
''' ;
2024-01-05 23:11:45 +00:00
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-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 ;
}
}
2024-07-31 21:50:15 +00:00
void store ( dynamic object , int cachedUntil ) async {
String key = _getObjectId ( object ) + cachedUntil . toString ( ) ;
2024-06-03 21:06:00 +00:00
_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 ;
2024-07-31 21:50:15 +00:00
case SingleplayerStream:
objectEntry = _cache . entries . firstWhere ( ( element ) = > element . key . startsWith ( id ) ) ;
2024-06-03 21:06:00 +00:00
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-09-04 21:12:26 +00:00
await db . delete ( tetrioLeagueTable , where: " id LIKE ? " , whereArgs: [ " $ id % " ] ) ;
2023-06-17 21:50:52 +00:00
}
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.
2024-07-31 21:50:15 +00:00
Future < SingleplayerStream > fetchStream ( String userID , String stream ) async {
SingleplayerStream ? cached = _cache . get ( stream + userID , SingleplayerStream ) ;
2024-06-12 22:32:45 +00:00
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 {
2024-07-31 21:50:15 +00:00
url = Uri . https ( ' ch.tetr.io ' , ' api/users/ ${ userID . toLowerCase ( ) . trim ( ) } /records/ $ stream ' ) ;
2024-06-12 22:32:45 +00:00
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
if ( jsonDecode ( response . body ) [ ' success ' ] ) {
2024-07-31 21:50:15 +00:00
SingleplayerStream records = SingleplayerStream . fromJson ( jsonDecode ( response . body ) [ ' data ' ] [ ' entries ' ] , userID , stream ) ;
2024-06-12 22:32:45 +00:00
_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-08-24 14:41:07 +00:00
Future < CutoffsTetrio ? > fetchCutoffsTetrio ( ) async {
2024-08-24 14:48:09 +00:00
CutoffsTetrio ? cached = _cache . get ( " league_ranks " , CutoffsTetrio ) ;
2024-08-24 14:41:07 +00:00
if ( cached ! = null ) return cached ;
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " cutoffs " } ) ;
} else {
url = Uri . https ( ' ch.tetr.io ' , ' api/labs/league_ranks ' ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
Map < String , dynamic > rawData = jsonDecode ( response . body ) ;
CutoffsTetrio result = CutoffsTetrio . fromJson ( rawData [ ' data ' ] ) ;
_cache . store ( result , rawData [ " cache " ] [ " cached_until " ] ) ;
return result ;
case 404 :
developer . log ( " fetchCutoffsTetrio: Cutoffs are gone " , name: " services/tetrio_crud " , error: response . statusCode ) ;
return null ;
// 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 :
developer . log ( " fetchCutoffsTetrio: Cutoffs are unavalable ( ${ response . statusCode } ) " , name: " services/tetrio_crud " , error: response . statusCode ) ;
return null ;
default :
developer . log ( " fetchCutoffsTetrio: 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
}
}
Future < Cutoffs ? > fetchCutoffsBeanserver ( ) async {
2024-10-16 23:10:08 +00:00
Cutoffs ? cached = _cache . get ( " CutoffsTetrioleague_ranks " , Cutoffs ) ;
2024-06-03 21:06:00 +00:00
if ( cached ! = null ) return cached ;
2024-05-02 22:26:12 +00:00
2024-08-18 22:02:04 +00:00
Uri url = Uri . https ( ' ts.dan63.by ' , ' beanserver_blaster/cutoffs.json ' ) ;
2024-05-02 22:26:12 +00:00
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
Map < String , dynamic > rawData = jsonDecode ( response . body ) ;
2024-08-17 23:39:20 +00:00
Map < String , dynamic > data = rawData [ " data " ] as Map < String , dynamic > ;
Cutoffs result = Cutoffs ( DateTime . fromMillisecondsSinceEpoch ( rawData [ " created " ] ) , { } , { } , { } ) ;
2024-05-02 22:26:12 +00:00
for ( String rank in data . keys ) {
2024-08-17 23:39:20 +00:00
result . tr [ rank ] = data [ rank ] [ " tr " ] ;
2024-06-03 21:06:00 +00:00
result . glicko [ rank ] = data [ rank ] [ " glicko " ] ;
2024-08-17 23:39:20 +00:00
result . gxe [ rank ] = data [ rank ] [ " gxe " ] ;
2024-05-02 22:26:12 +00:00
}
2024-08-17 23:39:20 +00:00
_cache . store ( result , rawData [ " cache_until " ] ) ;
2024-06-03 21:06:00 +00:00
return result ;
2024-05-02 22:26:12 +00:00
case 404 :
2024-08-24 14:41:07 +00:00
developer . log ( " fetchCutoffsBeanserver: 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-08-24 14:41:07 +00:00
developer . log ( " fetchCutoffsBeanserver: Cutoffs are unavalable ( ${ response . statusCode } ) " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2024-06-19 13:11:20 +00:00
return null ;
2024-05-02 22:26:12 +00:00
default :
2024-08-24 14:41:07 +00:00
developer . log ( " fetchCutoffsBeanserver: Failed to fetch top Cutoffs " , name: " services/tetrio_crud " , error: response . statusCode ) ;
2024-05-02 22:26:12 +00:00
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-09-12 20:28:55 +00:00
Future < List < Cutoffs > > fetchCutoffsHistory ( ) async {
Uri url = Uri . https ( ' ts.dan63.by ' , ' beanserver_blaster/history.csv ' ) ;
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
2024-09-13 22:00:11 +00:00
List < List < dynamic > > csv = const CsvToListConverter ( ) . convert ( response . body , eol: " \n " ) . . removeAt ( 0 ) ;
2024-09-12 20:28:55 +00:00
List < Cutoffs > history = [ ] ;
for ( List < dynamic > entry in csv ) {
Map < String , double > tr = { } ;
Map < String , double > glicko = { } ;
Map < String , double > gxe = { } ;
for ( int i = 0 ; i < ranks . length ; i + + ) {
2024-09-13 22:00:11 +00:00
tr [ ranks [ ranks . length - 1 - i ] ] = entry [ 1 + i * 3 ] ;
glicko [ ranks [ ranks . length - 1 - i ] ] = entry [ 2 + i * 3 ] ;
gxe [ ranks [ ranks . length - 1 - i ] ] = entry [ 3 + i * 3 ] ;
2024-09-12 20:28:55 +00:00
}
history . add (
Cutoffs (
2024-09-13 22:00:11 +00:00
DateTime . fromMillisecondsSinceEpoch ( entry [ 0 ] * 1000 ) ,
2024-09-12 20:28:55 +00:00
tr ,
glicko ,
gxe
)
) ;
}
return history ;
case 404 :
developer . log ( " fetchCutoffsHistory: Cutoffs are gone " , name: " services/tetrio_crud " , error: response . statusCode ) ;
return [ ] ;
// 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 :
developer . log ( " fetchCutoffsHistory: Cutoffs are unavalable ( ${ response . statusCode } ) " , name: " services/tetrio_crud " , error: response . statusCode ) ;
return [ ] ;
default :
developer . log ( " fetchCutoffsHistory: 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 {
2024-08-17 23:39:20 +00:00
url = Uri . https ( ' ch.tetr.io ' , ' api/users/by/league ' , { " after " : " 25000:0:0 " , " limit " : " 1 " } ) ;
2024-05-04 20:04:48 +00:00
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
var rawJson = jsonDecode ( response . body ) ;
2024-08-17 23:39:20 +00:00
TetrioPlayerFromLeaderboard result = TetrioPlayerFromLeaderboard . fromJson ( rawJson [ " data " ] [ " entries " ] [ 0 ] , DateTime . fromMillisecondsSinceEpoch ( rawJson [ " cache " ] [ " cached_at " ] ) ) ;
2024-06-03 21:06:00 +00:00
_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.
2024-09-03 21:07:27 +00:00
Future < List < TetraLeague > > 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-09-01 21:44:19 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
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 ) ;
2024-09-01 21:44:19 +00:00
List < TetraLeague > history = [ ] ;
2024-09-02 21:17:09 +00:00
Batch batch = db . batch ( ) ;
2024-02-01 00:15:32 +00:00
for ( List < dynamic > entry in csv ) { // each entry is one state
2024-09-01 21:44:19 +00:00
TetraLeague state = TetraLeague (
id: id ,
2024-02-01 00:15:32 +00:00
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 ,
2024-08-16 22:40:09 +00:00
tr: entry [ 3 ] ,
gxe: - 1 ,
2024-02-01 00:15:32 +00:00
rank: entry [ 5 ] ,
percentileRank: entry [ 5 ] ,
percentile: rankCutoffs [ entry [ 5 ] ] ! ,
standing: - 1 ,
standingLocal: - 1 ,
nextAt: - 1 ,
2024-09-01 21:44:19 +00:00
prevAt: - 1 ,
season: 1
2023-09-23 19:09:36 +00:00
) ;
history . add ( state ) ;
2024-09-02 21:17:09 +00:00
batch . insert ( tetrioLeagueTable , state . toJson ( ) , conflictAlgorithm: ConflictAlgorithm . replace ) ;
2023-09-23 19:09:36 +00:00
}
2024-09-02 21:17:09 +00:00
batch . commit ( ) ;
2023-09-23 19:09:36 +00:00
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
2024-07-27 19:10:45 +00:00
Future < TetraLeagueAlphaStream > fetchAndSaveOldTLmatches ( String userID ) async {
2024-02-03 13:02:58 +00:00
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " TLMatches " , " user " : userID } ) ;
} else {
2024-07-10 16:21:03 +00:00
url = Uri . https ( ' api.p1nkl0bst3r.xyz ' , ' tlmatches/ $ userID ' , { " before " : " 0 " , " count " : " 9000 " } ) ;
2024-02-03 13:02:58 +00:00
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
2024-07-10 15:31:00 +00:00
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream . fromJson ( jsonDecode ( response . body ) [ ' data ' ] [ ' records ' ] , userID ) ;
saveTLMatchesFromStream ( stream ) ;
2024-07-27 19:10:45 +00:00
return stream ;
2024-02-03 13:02:58 +00:00
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.
2024-08-19 17:59:25 +00:00
Future < TetrioPlayersLeaderboard > fetchTLLeaderboard ( ) async {
TetrioPlayersLeaderboard ? cached = _cache . get ( " league " , TetrioPlayersLeaderboard ) ;
2024-06-03 21:06:00 +00:00
if ( cached ! = null ) return cached ;
2024-08-19 17:59:25 +00:00
Uri url = Uri . https ( ' ts.dan63.by ' , ' beanserver_blaster/leaderboard.json ' ) ;
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-08-19 17:59:25 +00:00
TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard . fromJson ( rawJson [ ' data ' ] , " league " , DateTime . fromMillisecondsSinceEpoch ( rawJson [ ' created ' ] ) ) ;
developer . log ( " fetchTLLeaderboard: Leaderboard retrieved and cached " , name: " services/tetrio_crud " ) ;
_cache . store ( leaderboard , rawJson [ ' cache_until ' ] ) ;
return leaderboard ;
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-09-29 22:02:19 +00:00
Future < List < TetrioPlayerFromLeaderboard > > fetchTetrioLeaderboard ( { String ? prisecter , String ? lb , String ? country } ) async {
2024-09-14 22:05:50 +00:00
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
// if (cached != null) return cached;
2024-06-04 22:14:34 +00:00
2024-09-14 22:05:50 +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/by/ ${ lb ? ? " league " } ' , {
" limit " : " 100 " ,
2024-09-29 22:02:19 +00:00
if ( prisecter ! = null ) " after " : prisecter ,
if ( country ! = null ) " country " : country
2024-09-14 22:05:50 +00:00
} ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
_lbPositions . clear ( ) ;
var rawJson = jsonDecode ( response . body ) ;
if ( rawJson [ ' success ' ] ) { // if api confirmed that everything ok
List < TetrioPlayerFromLeaderboard > leaderboard = [ ] ;
for ( Map < String , dynamic > entry in rawJson [ ' data ' ] [ ' entries ' ] ) {
leaderboard . add ( TetrioPlayerFromLeaderboard . fromJson ( entry , DateTime . fromMillisecondsSinceEpoch ( rawJson [ ' cache ' ] [ ' cached_at ' ] ) ) ) ;
}
developer . log ( " fetchTLLeaderboard: Leaderboard retrieved and cached " , name: " services/tetrio_crud " ) ;
//_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard;
//_cache.store(leaderboard, rawJson['cache']['cached_until']);
return leaderboard ;
} else { // idk how to hit that one
developer . log ( " fetchTLLeaderboard: Bruh " , name: " services/tetrio_crud " , error: rawJson ) ;
throw Exception ( " Failed to get leaderboard (problems on the tetr.io side) " ) ; // will it be on tetr.io side?
2024-09-15 16:38:07 +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 " ) ;
}
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
}
}
2024-10-01 21:46:43 +00:00
Future < List < RecordSingle > > fetchTetrioRecordsLeaderboard ( { String ? prisecter , String ? lb , String ? country } ) async {
2024-09-15 16:38:07 +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/records/ ${ lb ? ? " 40l_global " } ' , {
" limit " : " 100 " ,
2024-10-01 21:46:43 +00:00
if ( prisecter ! = null ) " after " : prisecter ,
if ( country ! = null ) " country " : country
2024-09-15 16:38:07 +00:00
} ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
_lbPositions . clear ( ) ;
var rawJson = jsonDecode ( response . body ) ;
if ( rawJson [ ' success ' ] ) { // if api confirmed that everything ok
List < RecordSingle > leaderboard = [ ] ;
for ( Map < String , dynamic > entry in rawJson [ ' data ' ] [ ' entries ' ] ) {
leaderboard . add ( RecordSingle . fromJson ( entry , - 1 , - 1 ) ) ;
}
developer . log ( " fetchTLLeaderboard: Leaderboard retrieved and cached " , name: " services/tetrio_crud " ) ;
//_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard;
//_cache.store(leaderboard, rawJson['cache']['cached_until']);
return leaderboard ;
} else { // idk how to hit that one
developer . log ( " fetchTLLeaderboard: Bruh " , name: " services/tetrio_crud " , error: rawJson ) ;
throw Exception ( " Failed to get leaderboard (problems on the tetr.io side) " ) ; // will it be on tetr.io side?
2024-09-14 22:05:50 +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 " ) ;
}
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
}
}
2024-06-04 22:14:34 +00:00
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 {
2024-07-31 21:50:15 +00:00
News ? cached = _cache . get ( " user_ $ userID " , News ) ;
2024-06-03 21:06:00 +00:00
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.
2024-07-27 19:10:45 +00:00
Future < TetraLeagueBetaStream > fetchTLStream ( String userID ) async {
2024-07-31 21:50:15 +00:00
TetraLeagueBetaStream ? cached = _cache . get ( userID , TetraLeagueBetaStream ) ;
2024-06-03 21:06:00 +00:00
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 {
2024-07-27 19:10:45 +00:00
url = Uri . https ( ' ch.tetr.io ' , ' api/users/ ${ userID . toLowerCase ( ) . trim ( ) } /records/league/recent ' ) ;
2023-07-20 20:56:00 +00:00
}
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 ' ] ) {
2024-07-27 19:10:45 +00:00
TetraLeagueBetaStream stream = TetraLeagueBetaStream . fromJson ( jsonDecode ( response . body ) [ ' data ' ] [ ' entries ' ] , 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-07-29 20:58:17 +00:00
? RecordSingle . fromJson ( jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' record ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' rank ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' rank_local ' ] )
2024-02-01 00:15:32 +00:00
: null ;
2023-09-23 19:09:36 +00:00
var blitz = jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' record ' ] ! = null
2024-07-29 20:58:17 +00:00
? RecordSingle . fromJson ( jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' record ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' rank ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' rank_local ' ] )
2024-02-01 00:15:32 +00:00
: 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-07-27 19:10:45 +00:00
Future < Summaries > fetchSummaries ( String id ) async {
Summaries ? cached = _cache . get ( id , Summaries ) ;
if ( cached ! = null ) return cached ;
Uri url ;
if ( kIsWeb ) {
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " Summaries " , " id " : id } ) ;
} else {
url = Uri . https ( ' ch.tetr.io ' , ' api/users/ $ id /summaries ' ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
if ( jsonDecode ( response . body ) [ ' success ' ] ) {
developer . log ( " fetchSummaries: $ id summaries retrieved and cached " , name: " services/tetrio_crud " ) ;
2024-07-31 21:50:15 +00:00
Summaries summaries = Summaries . fromJson ( jsonDecode ( response . body ) [ ' data ' ] , id ) ;
_cache . store ( summaries , jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] ) ;
return summaries ;
2024-07-27 19:10:45 +00:00
} else {
developer . log ( " fetchSummaries: 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 " ) ;
}
} 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
/// 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 ( ) ;
}
2024-09-01 21:44:19 +00:00
await db . insert ( tetrioUsersTable , { idCol: tetrioPlayer . userId , nickCol: tetrioPlayer . username } , conflictAlgorithm: ConflictAlgorithm . replace ) ;
2023-06-17 21:50:52 +00:00
db . insert ( tetrioUsersToTrackTable , { idCol: tetrioPlayer . userId } ) ;
2024-09-01 21:44:19 +00:00
_players [ tetrioPlayer . userId ] = tetrioPlayer . username ;
_tetrioStreamController . add ( _players ) ;
2023-06-17 21:50:52 +00:00
}
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 ( ) ] ) ;
2024-09-01 21:44:19 +00:00
await db . delete ( tetrioUsersTable , where: ' $ idCol = ? ' , whereArgs: [ id . toLowerCase ( ) ] ) ;
2023-06-17 21:50:52 +00:00
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-09-03 21:07:27 +00:00
Future < List < TetraLeague > > getStates ( String userID , { int ? season } ) async {
2024-02-06 20:38:52 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
2024-09-03 21:07:27 +00:00
List < Map > query = await db . query ( tetrioLeagueTable , where: season ! = null ? ' "id" LIKE ? AND "season" = ? ' : ' "id" LIKE ? ' , whereArgs: season ! = null ? [ " ${ userID } % " , season ] : [ " ${ userID } % " ] , orderBy: ' "id" ASC ' ) ;
return [ for ( var entry in query ) TetraLeague . fromJson ( entry as Map < String , dynamic > , DateTime . fromMillisecondsSinceEpoch ( int . parse ( entry [ " id " ] . substring ( 24 ) , radix: 16 ) ) , entry [ " season " ] , entry [ " id " ] . substring ( 0 , 24 ) ) ] ;
2023-06-17 21:50:52 +00:00
}
2024-09-01 21:44:19 +00:00
/// Saves state (which is [TetraLeague]) to the local database.
Future < void > storeState ( TetraLeague league ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
final db = getDatabaseOrThrow ( ) ;
2024-09-02 21:17:09 +00:00
List < Map > test = await db . query ( tetrioLeagueTable , where: ' "id" LIKE ? AND "gamesplayed" = ? AND "rd" = ? ' , whereArgs: [ " ${ league . id } % " , league . gamesPlayed , league . rd ] ) ;
2024-09-01 21:44:19 +00:00
if ( test . isEmpty ) {
await db . insert ( tetrioLeagueTable , league . toJson ( ) ) ;
2023-06-17 21:50:52 +00:00
}
}
2024-09-04 21:12:26 +00:00
/// Remove state, which has [dbID] from the local database
/// ([dbid] is a concatenation of player id and UINX milliseconds in hex)
Future < void > deleteState ( String dbID ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
int result = await db . delete ( tetrioLeagueTable , where: " id = ? " , whereArgs: [ dbID ] ) ;
if ( result = = 0 ) throw Exception ( " Failed to remove a row $ dbID - it's probably not exist " ) ;
}
2023-06-17 21:50:52 +00:00
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 {
2024-09-08 22:07:59 +00:00
dUrl = Uri . https ( ' ch.tetr.io ' , ' api/users/search/discord: ${ user . toLowerCase ( ) . trim ( ) } ' ) ;
2023-07-29 18:01:49 +00:00
}
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 ( ) ;
2024-08-15 21:55:45 +00:00
case 404 :
throw TetrioPlayerNotExist ( ) ;
2023-09-23 19:09:36 +00:00
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 :
2024-09-29 22:02:19 +00:00
var json = jsonDecode ( utf8 . decode ( response . bodyBytes ) ) ;
2023-09-23 19:09:36 +00:00
if ( json [ ' success ' ] ) {
2024-02-01 00:15:32 +00:00
// parse and count stats
2024-07-27 19:10:45 +00:00
TetrioPlayer player = TetrioPlayer . fromJson ( json [ ' data ' ] , DateTime . fromMillisecondsSinceEpoch ( json [ ' cache ' ] [ ' cached_at ' ] , isUtc: true ) , json [ ' data ' ] [ ' _id ' ] , json [ ' data ' ] [ ' 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 ( ) ;
2024-07-31 21:50:15 +00:00
case 404 :
throw TetrioPlayerNotExist ( ) ;
2023-09-23 19:09:36 +00:00
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-09-04 21:12:26 +00:00
/// Retrieves whole [tetrioUsersTable] and returns Map {id: nickname} of everyone in database
Future < Map < String , String > > getAllPlayers ( ) async {
2023-06-17 21:50:52 +00:00
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final players = await db . query ( tetrioUsersTable ) ;
2024-09-04 21:12:26 +00:00
Map < String , String > data = { } ;
2024-02-06 20:38:52 +00:00
for ( var entry in players ) {
2024-09-04 21:12:26 +00:00
data [ entry [ idCol ] as String ] = entry [ nickCol ] as String ;
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
2024-07-27 19:10:45 +00:00
// Future<void> fetchTracked() async {
// for (String userID in (await getAllPlayerToTrack())) {
// TetrioPlayer player = await fetchPlayer(userID);
// storeState(player);
// sleep(Durations.extralong4);
// TetraLeagueBetaStream matches = await fetchTLStream(userID);
// saveTLMatchesFromStream(matches);
// sleep(Durations.extralong4);
// }
// }
2023-06-17 21:50:52 +00:00
}