Replay stealing (excluding web version)

This commit is contained in:
dan63047 2023-10-19 00:50:41 +03:00
parent b7dc7d33ca
commit eca63a5288
7 changed files with 123 additions and 18 deletions

View File

@ -728,15 +728,17 @@ class TetraLeagueAlphaRecord{
late String replayId;
late String ownId;
late DateTime timestamp;
late bool replayAvalable;
late List<EndContextMulti> endContext;
TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext});
TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext, required this.replayAvalable});
TetraLeagueAlphaRecord.fromJson(Map<String, dynamic> json) {
ownId = json['_id'];
endContext = [EndContextMulti.fromJson(json['endcontext'][0]), EndContextMulti.fromJson(json['endcontext'][1])];
replayId = json['replayid'];
timestamp = DateTime.parse(json['ts']);
replayAvalable = true;
}
Map<String, dynamic> toJson() {

View File

@ -24,8 +24,18 @@ class P1nkl0bst3rTooManyRequests implements Exception {}
class P1nkl0bst3rForbidden implements Exception {}
class SzyTooManyRequests implements Exception {}
class SzyForbidden implements Exception {}
class SzyNotFound implements Exception {}
class TetrioReplayAlreadyExist implements Exception {}
class P1nkl0bst3rInternalProblem implements Exception {}
class SzyInternalProblem implements Exception {}
class TetrioOskwareBridgeProblem implements Exception {}
class TetrioInternalProblem implements Exception {}

View File

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:tetra_stats/main.dart' show packageInfo;
import 'package:flutter/foundation.dart';
import 'package:tetra_stats/services/custom_http_client.dart';
@ -57,7 +59,8 @@ class TetrioService extends DB {
final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer}
final client = UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client());
// 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());
static final TetrioService _shared = TetrioService._sharedInstance();
factory TetrioService() => _shared;
late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
@ -107,6 +110,42 @@ class TetrioService extends DB {
}
}
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);
}
}
Future<double?> fetchTopTR(String id) async {
try{
var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id);
@ -441,7 +480,7 @@ class TetrioService extends DB {
List<TetraLeagueAlphaRecord> matches = [];
final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]);
for (var match in results){
matches.add(TetraLeagueAlphaRecord(ownId: match[idCol].toString(), replayId: match[replayID].toString(), timestamp: DateTime.parse(match[timestamp].toString()), endContext:[EndContextMulti.fromJson(jsonDecode(match[endContext1].toString())), EndContextMulti.fromJson(jsonDecode(match[endContext2].toString()))]));
matches.add(TetraLeagueAlphaRecord(ownId: match[idCol].toString(), replayId: match[replayID].toString(), timestamp: DateTime.parse(match[timestamp].toString()), endContext:[EndContextMulti.fromJson(jsonDecode(match[endContext1].toString())), EndContextMulti.fromJson(jsonDecode(match[endContext2].toString()))], replayAvalable: false));
}
return matches;
}

View File

@ -0,0 +1,10 @@
import 'package:url_launcher/url_launcher.dart';
Future<void> launchInBrowser(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.externalApplication,
)) {
throw Exception('Could not launch $url');
}
}

View File

@ -159,11 +159,12 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
List<TetrioPlayer> states = [];
TetraLeagueAlpha? compareWith;
var uniqueTL = <dynamic>{};
tlMatches = tlStream.records;
if (isTracking){
await teto.storeState(me);
await teto.saveTLMatchesFromStream(tlStream);
tlMatches.addAll(await teto.getTLMatchesbyPlayerID(me.userId));
for (var match in tlStream.records) {
var storedRecords = await teto.getTLMatchesbyPlayerID(me.userId);
for (var match in storedRecords) {
if (!tlMatches.contains(match)) tlMatches.add(match);
}
tlMatches.sort((a, b) {
@ -172,8 +173,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
if(a.timestamp.isAfter(b.timestamp)) return -1;
return 0;
});
} else{
tlMatches = tlStream.records;
}
if(fetchHistory) await teto.fetchAndsaveTLHistory(_searchFor);
states.addAll(await teto.getPlayer(me.userId));

View File

@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:window_manager/window_manager.dart';
late String oldWindowTitle;
@ -43,15 +43,6 @@ class SettingsState extends State<SettingsView> {
super.dispose();
}
Future<void> _launchInBrowser(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.externalApplication,
)) {
throw Exception('Could not launch $url');
}
}
Future<void> _getPreferences() async {
prefs = await SharedPreferences.getInstance();
_setDefaultNickname(prefs.getString("player"));
@ -248,7 +239,7 @@ class SettingsState extends State<SettingsView> {
const Divider(),
ListTile(
onTap: (){
_launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));
launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));
},
title: Text(t.aboutApp),
subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)),

View File

@ -1,11 +1,19 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'main_view.dart' show teto;
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:window_manager/window_manager.dart';
// ignore: avoid_web_libraries_in_flutter
// import 'dart:html' show AnchorElement, document;
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
@ -52,6 +60,52 @@ class TlMatchResultState extends State<TlMatchResultView> {
return Scaffold(
appBar: AppBar(
title: Text("${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${dateFormat.format(widget.record.timestamp)}"),
actions: [
PopupMenuButton(
enabled: widget.record.replayAvalable,
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
const PopupMenuItem(
value: 1,
child: Text("Download le replay"),
),
const PopupMenuItem(
value: 2,
child: Text("Open le replay in TETR.IO"),
),
],
onSelected: (value) async {
switch (value) {
case 1:
if (kIsWeb){
// final _base64 = base64Encode([1,2,3,4,5]);
// final anchor = AnchorElement(href: 'data:application/octet-stream;base64,$_base64')..target = 'blank';
//final anchor = AnchorElement(href: 'https://inoue.szy.lol/api/replay/${widget.record.replayId}')..target = 'blank';
//anchor.download = "${widget.record.replayId}.ttrm";
//document.body!.append(anchor);
//anchor.click();
//anchor.remove();
} else{
try{
String path = await teto.szyDownloadAndSaveReplay(widget.record.replayId);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Replay saved to $path")));
} on TetrioReplayAlreadyExist{
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Replay already saved")));
} on SzyNotFound {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Replay expired (i think)")));
} on SzyForbidden {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Request has been rejected")));
} on SzyTooManyRequests {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.tooManyRequests)));
}
}
break;
case 2:
await launchInBrowser(Uri.parse("https://tetr.io/#r:${widget.record.replayId}"));
break;
default:
}
})
]
),
backgroundColor: Colors.black,
body: SafeArea(