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 ' ;
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 " ;
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 " ;
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 " )
) ; ''' ;
const String createTetrioUsersToTrack = '''
CREATE TABLE IF NOT EXISTS " tetrioUsersToTrack " (
" id " TEXT NOT NULL UNIQUE ,
PRIMARY KEY ( " ID " )
)
''' ;
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
)
''' ;
2023-06-17 21:50:52 +00:00
class TetrioService extends DB {
Map < String , List < TetrioPlayer > > _players = { } ;
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 = { } ;
2023-07-07 20:32:57 +00:00
final Map < String , TetrioPlayersLeaderboard > _leaderboardsCache = { } ;
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 = { } ;
2023-06-21 19:17:39 +00:00
final Map < String , TetraLeagueAlphaStream > _tlStreamsCache = { } ; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer}
2023-10-18 21:50:41 +00:00
// final client = UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client());
final client = UserAgentClient ( " Kagari-chan loves osk (Tetra Stats dev build) " , http . Client ( ) ) ;
2023-06-17 21:50:52 +00:00
static final TetrioService _shared = TetrioService . _sharedInstance ( ) ;
factory TetrioService ( ) = > _shared ;
late final StreamController < Map < String , List < TetrioPlayer > > > _tetrioStreamController ;
TetrioService . _sharedInstance ( ) {
_tetrioStreamController = StreamController < Map < String , List < TetrioPlayer > > > . broadcast ( onListen: ( ) {
_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
}
Stream < Map < String , List < TetrioPlayer > > > get allPlayers = > _tetrioStreamController . stream ;
2023-06-21 19:17:39 +00:00
Future < void > _loadPlayers ( ) async {
2023-06-17 21:50:52 +00:00
final allPlayers = await getAllPlayers ( ) ;
2023-07-22 12:23:11 +00:00
try {
_players = allPlayers . toList ( ) . first ; // ???
} catch ( e ) {
developer . log ( " _loadPlayers: allPlayers.toList().first did oopsie " , name: " services/tetrio_crud " , error: e ) ;
_players = { } ;
}
2023-06-17 21:50:52 +00:00
_tetrioStreamController . add ( _players ) ;
}
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 ) ;
}
}
Future < String > getNicknameByID ( String id ) async {
if ( id . length < = 16 ) return id ;
2023-07-17 17:57:24 +00:00
try {
return await getPlayer ( id ) . then ( ( value ) = > value . last . username ) ;
} catch ( e ) {
return await fetchPlayer ( id ) . then ( ( value ) = > value . username ) ;
}
}
2023-10-18 21:50:41 +00:00
Future < String > szyDownloadAndSaveReplay ( String replayID ) async {
Uri url = Uri . https ( ' inoue.szy.lol ' , ' /api/replay/ $ replayID ' ) ;
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 ( ) ;
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
await replayFile . writeAsBytes ( response . bodyBytes ) ;
return replayFile . path ;
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 :
developer . log ( " szyDownloadAndSaveReplay: Failed to download a replay " , name: " services/tetrio_crud " , error: response . statusCode ) ;
throw ConnectionIssue ( response . statusCode , response . reasonPhrase ? ? " No reason " ) ;
}
} on http . ClientException catch ( e , s ) {
developer . log ( " $ e , $ s " ) ;
throw http . ClientException ( e . message , e . uri ) ;
}
}
2023-10-08 17:20:42 +00:00
Future < double ? > fetchTopTR ( String id ) async {
try {
var cached = _topTRcache . entries . firstWhere ( ( element ) = > element . value . keys . first = = id ) ;
if ( DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) . isAfter ( DateTime . now ( ) ) ) {
developer . log ( " fetchTopTR: Top TR retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
return cached . value . values . first ;
} else {
_topTRcache . remove ( cached . key ) ;
developer . log ( " fetchTopTR: Top TR expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
} catch ( e ) {
developer . log ( " fetchTopTR: Trying to retrieve Top TR " , name: " services/tetrio_crud " ) ;
}
Uri url ;
if ( kIsWeb ) {
2023-10-09 18:48:50 +00:00
url = Uri . https ( ' ts.dan63.by ' , ' oskware_bridge.php ' , { " endpoint " : " PeakTR " , " user " : id } ) ;
2023-10-08 17:20:42 +00:00
} else {
url = Uri . https ( ' api.p1nkl0bst3r.xyz ' , ' toptr/ $ id ' ) ;
}
try {
final response = await client . get ( url ) ;
switch ( response . statusCode ) {
case 200 :
_topTRcache [ ( DateTime . now ( ) . millisecondsSinceEpoch + 300000 ) . toString ( ) ] = { id: double . tryParse ( response . body ) } ;
return double . tryParse ( response . body ) ;
case 404 :
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 ;
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 " ) ;
}
} 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
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 :
List < List < dynamic > > csv = const CsvToListConverter ( ) . convert ( response . body ) . . removeAt ( 0 ) ;
List < TetrioPlayer > history = [ ] ;
String nick = await getNicknameByID ( id ) ;
for ( List < dynamic > entry in csv ) {
TetrioPlayer state = TetrioPlayer (
userId: id ,
username: nick ,
role: " p1nkl0bst3r " ,
state: DateTime . parse ( entry [ 9 ] ) ,
badges: [ ] ,
friendCount: - 1 ,
gamesPlayed: - 1 ,
gamesWon: - 1 ,
gameTime: const Duration ( seconds: - 1 ) ,
xp: - 1 ,
supporterTier: 0 ,
verified: false ,
connections: null ,
tlSeason1: TetraLeagueAlpha ( timestamp: DateTime . parse ( entry [ 9 ] ) , apm: entry [ 6 ] ! = ' ' ? entry [ 6 ] : null , pps: entry [ 7 ] ! = ' ' ? entry [ 7 ] : null , vs: entry [ 8 ] ! = ' ' ? entry [ 8 ] : null , glicko: entry [ 4 ] , rd: noTrRd , gamesPlayed: entry [ 1 ] , gamesWon: entry [ 2 ] , bestRank: " z " , decaying: false , rating: entry [ 3 ] , rank: entry [ 5 ] , percentileRank: entry [ 5 ] , percentile: rankCutoffs [ entry [ 5 ] ] ! , standing: - 1 , standingLocal: - 1 , nextAt: - 1 , prevAt: - 1 ) ,
sprint: [ ] ,
blitz: [ ]
) ;
history . add ( state ) ;
}
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
late List < TetrioPlayer > states ;
try {
states = _players [ id ] ! ;
} catch ( e ) {
var player = await fetchPlayer ( id ) ;
await createPlayer ( player ) ;
states = _players [ id ] ! ;
}
states . insertAll ( 0 , history . reversed ) ;
final Map < String , dynamic > statesJson = { } ;
for ( var e in states ) {
statesJson . addEntries ( { ( e . state . millisecondsSinceEpoch ~ / 1000 ) . toString ( ) : e . toJson ( ) } . entries ) ;
}
await db . update ( tetrioUsersTable , { idCol: id , nickCol: nick , statesCol: jsonEncode ( statesJson ) } , where: ' $ idCol = ? ' , whereArgs: [ id ] ) ;
_tetrioStreamController . add ( _players ) ;
return history ;
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
}
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 :
var rawJson = jsonDecode ( response . body ) ;
if ( rawJson [ ' success ' ] ) {
TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard . fromJson ( rawJson [ ' data ' ] [ ' users ' ] , " league " , DateTime . fromMillisecondsSinceEpoch ( rawJson [ ' cache ' ] [ ' cached_at ' ] ) ) ;
developer . log ( " fetchTLLeaderboard: Leaderboard retrieved and cached " , name: " services/tetrio_crud " ) ;
_leaderboardsCache [ rawJson [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = leaderboard ;
return leaderboard ;
} else {
developer . log ( " fetchTLLeaderboard: Bruh " , name: " services/tetrio_crud " , error: rawJson ) ;
throw Exception ( " Failed to get leaderboard (problems on the tetr.io side) " ) ;
}
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
}
}
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 ) ;
if ( payload [ ' success ' ] ) {
List < News > news = [ for ( var entry in payload [ ' data ' ] [ ' news ' ] ) News . fromJson ( entry ) ] ;
developer . log ( " fetchNews: $ userID news retrieved and cached " , name: " services/tetrio_crud " ) ;
_newsCache [ payload [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = news ;
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 ) ;
}
}
2023-06-20 20:53:28 +00:00
Future < TetraLeagueAlphaStream > getTLStream ( 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 ( ) ) ) {
developer . log ( " getTLStream: Stream $ userID retrieved from cache, that expires ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } " , name: " services/tetrio_crud " ) ;
return cached . value ;
} else {
_tlStreamsCache . remove ( cached . key ) ;
developer . log ( " getTLStream: Cached stream $ userID expired ( ${ DateTime . fromMillisecondsSinceEpoch ( int . parse ( cached . key . toString ( ) ) , isUtc: true ) } ) " , name: " services/tetrio_crud " ) ;
}
} catch ( e ) {
developer . log ( " getTLStream: Trying to retrieve stream $ userID " , name: " services/tetrio_crud " ) ;
}
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 ) ;
developer . log ( " getTLStream: $ userID stream retrieved and cached " , name: " services/tetrio_crud " ) ;
_tlStreamsCache [ jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = stream ;
return stream ;
} else {
developer . log ( " getTLStream User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw 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 ( " getTLStream Failed to fetch stream " , name: " services/tetrio_crud " , error: response . statusCode ) ;
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
}
}
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 ( ) ;
for ( TetraLeagueAlphaRecord match in stream . records ) {
2023-10-16 21:41:45 +00:00
final results = await db . query ( tetraLeagueMatchesTable , where: ' $ replayID = ? ' , whereArgs: [ match . replayId ] ) ;
2023-06-23 18:38:15 +00:00
if ( results . isNotEmpty ) continue ;
db . insert ( tetraLeagueMatchesTable , { idCol: match . ownId , replayID: match . replayId , timestamp: match . timestamp . toString ( ) , player1id: match . endContext . first . userId , player2id: match . endContext . last . userId , endContext1: jsonEncode ( match . endContext . first . toJson ( ) ) , endContext2: jsonEncode ( match . endContext . last . toJson ( ) ) } ) ;
}
}
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
) ;
""" );
}
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 ) {
2023-10-18 21:50:41 +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 ;
}
2023-09-23 19:09:36 +00:00
Future < void > deleteTLMatch ( String matchID ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final results = await db . delete ( tetraLeagueMatchesTable , where: ' $ idCol = ? ' , whereArgs: [ matchID ] ) ;
if ( results ! = 1 ) {
throw CouldNotDeleteMatch ( ) ;
}
}
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
? [ RecordSingle . fromJson ( jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' record ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' 40l ' ] [ ' rank ' ] ) ]
: [ ] ;
var blitz = jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' record ' ] ! = null
? [ RecordSingle . fromJson ( jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' record ' ] , jsonRecords [ ' data ' ] [ ' records ' ] [ ' blitz ' ] [ ' rank ' ] ) ]
: [ ] ;
var zen = TetrioZen . fromJson ( jsonRecords [ ' data ' ] [ ' zen ' ] ) ;
Map < String , dynamic > map = { " user " : userID . toLowerCase ( ) . trim ( ) , " sprint " : sprint , " blitz " : blitz , " zen " : zen } ;
developer . log ( " fetchRecords: $ userID records retrieved and cached " , name: " services/tetrio_crud " ) ;
_recordsCache [ jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = map ;
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
}
}
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 ( ) ;
final results = await db . query ( tetrioUsersTable , limit: 1 , where: ' $ idCol = ? ' , whereArgs: [ tetrioPlayer . userId . toLowerCase ( ) ] ) ;
if ( results . isNotEmpty ) {
throw TetrioPlayerAlreadyExist ( ) ;
}
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 ) } ) ;
_players . addEntries ( {
tetrioPlayer . userId: [ tetrioPlayer ]
} . entries ) ;
_tetrioStreamController . add ( _players ) ;
}
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 } ) ;
}
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
}
Future < Iterable < String > > getAllPlayerToTrack ( ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final players = await db . query ( tetrioUsersToTrackTable ) ;
developer . log ( " getAllPlayerToTrack: $ players " , name: " services/tetrio_crud " ) ;
return players . map ( ( noteRow ) = > noteRow [ " id " ] . toString ( ) ) ;
}
Future < 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 {
// _players.removeWhere((key, value) => key == id);
// _tetrioStreamController.add(_players);
}
}
2023-07-18 17:53:43 +00:00
Future < void > storeState ( TetrioPlayer tetrioPlayer ) async {
2023-07-29 18:01:49 +00:00
await ensureDbIsOpen ( ) ;
2023-06-17 21:50:52 +00:00
final db = getDatabaseOrThrow ( ) ;
late List < TetrioPlayer > states ;
try {
2023-07-17 17:57:24 +00:00
states = _players [ tetrioPlayer . userId ] ! ;
} catch ( e ) {
2023-06-17 21:50:52 +00:00
await createPlayer ( tetrioPlayer ) ;
states = await getPlayer ( tetrioPlayer . userId ) ;
}
2023-07-18 17:53:43 +00:00
bool test = _players [ tetrioPlayer . userId ] ! . last . isSameState ( tetrioPlayer ) ;
if ( test = = false ) states . add ( tetrioPlayer ) ;
2023-06-17 21:50:52 +00:00
final Map < String , dynamic > statesJson = { } ;
for ( var e in states ) {
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
}
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 ] ) ;
2023-07-18 17:53:43 +00:00
_players [ tetrioPlayer . userId ] ! . add ( tetrioPlayer ) ;
2023-06-17 21:50:52 +00:00
_tetrioStreamController . add ( _players ) ;
}
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 ( ) ;
late List < TetrioPlayer > states ;
states = await getPlayer ( tetrioPlayer . userId ) ;
_players [ tetrioPlayer . userId ] ! . removeWhere ( ( element ) = > element . state = = tetrioPlayer . state ) ;
states = _players [ tetrioPlayer . userId ] ! ;
final Map < String , dynamic > statesJson = { } ;
for ( var e in states ) {
statesJson . addEntries ( { e . state . millisecondsSinceEpoch . toString ( ) : e . toJson ( ) } . entries ) ;
}
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 ] ) ;
_players [ tetrioPlayer . userId ] ! . add ( tetrioPlayer ) ;
_tetrioStreamController . add ( _players ) ;
}
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 ) {
2023-07-17 17:57:24 +00:00
return states ;
2023-06-17 21:50:52 +00:00
} else {
dynamic rawStates = results . first [ ' jsonStates ' ] as String ;
rawStates = json . decode ( rawStates ) ;
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 ) ) ) ;
2023-06-17 21:50:52 +00:00
_players . removeWhere ( ( key , value ) = > key = = id ) ;
_players . addEntries ( { states . last . userId: states } . entries ) ;
_tetrioStreamController . add ( _players ) ;
return states ;
}
}
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 ) {
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 ) {
user = json [ ' data ' ] [ ' user ' ] [ ' _id ' ] ;
} else {
developer . log ( " fetchPlayer User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw TetrioPlayerNotExist ( ) ;
}
break ;
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
}
}
2023-09-23 19:09:36 +00:00
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 ' ] ) {
TetrioPlayer player = TetrioPlayer . fromJson ( json [ ' data ' ] [ ' user ' ] , DateTime . fromMillisecondsSinceEpoch ( json [ ' cache ' ] [ ' cached_at ' ] , isUtc: true ) , json [ ' data ' ] [ ' user ' ] [ ' _id ' ] , json [ ' data ' ] [ ' user ' ] [ ' username ' ] ) ;
developer . log ( " fetchPlayer: $ user retrieved and cached " , name: " services/tetrio_crud " ) ;
_playersCache [ jsonDecode ( response . body ) [ ' cache ' ] [ ' cached_until ' ] . toString ( ) ] = player ;
return player ;
} else {
developer . log ( " fetchPlayer User dosen't exist " , name: " services/tetrio_crud " , error: response . body ) ;
throw TetrioPlayerNotExist ( ) ;
}
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
}
}
Future < Iterable < Map < String , List < TetrioPlayer > > > > getAllPlayers ( ) async {
await ensureDbIsOpen ( ) ;
final db = getDatabaseOrThrow ( ) ;
final players = await db . query ( tetrioUsersTable ) ;
Map < String , List < TetrioPlayer > > data = { } ;
//developer.log("getAllPlayers: $players", name: "services/tetrio_crud");
return players . map ( ( row ) {
// what the fuck am i doing here?
var test = json . decode ( row [ ' jsonStates ' ] as String ) ;
List < TetrioPlayer > states = [ ] ;
2023-07-18 17:53:43 +00:00
test . forEach ( ( k , v ) = > states . add ( TetrioPlayer . fromJson ( v , DateTime . fromMillisecondsSinceEpoch ( int . parse ( k ) * 1000 ) , row [ idCol ] as String , row [ nickCol ] as String ) ) ) ;
2023-06-17 21:50:52 +00:00
data . addEntries ( { states . last . userId: states } . entries ) ;
return data ;
} ) ;
}
}