Replay stealing (excluding web version)
This commit is contained in:
parent
b7dc7d33ca
commit
eca63a5288
|
@ -728,15 +728,17 @@ class TetraLeagueAlphaRecord{
|
||||||
late String replayId;
|
late String replayId;
|
||||||
late String ownId;
|
late String ownId;
|
||||||
late DateTime timestamp;
|
late DateTime timestamp;
|
||||||
|
late bool replayAvalable;
|
||||||
late List<EndContextMulti> endContext;
|
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) {
|
TetraLeagueAlphaRecord.fromJson(Map<String, dynamic> json) {
|
||||||
ownId = json['_id'];
|
ownId = json['_id'];
|
||||||
endContext = [EndContextMulti.fromJson(json['endcontext'][0]), EndContextMulti.fromJson(json['endcontext'][1])];
|
endContext = [EndContextMulti.fromJson(json['endcontext'][0]), EndContextMulti.fromJson(json['endcontext'][1])];
|
||||||
replayId = json['replayid'];
|
replayId = json['replayid'];
|
||||||
timestamp = DateTime.parse(json['ts']);
|
timestamp = DateTime.parse(json['ts']);
|
||||||
|
replayAvalable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
|
|
|
@ -24,8 +24,18 @@ class P1nkl0bst3rTooManyRequests implements Exception {}
|
||||||
|
|
||||||
class P1nkl0bst3rForbidden 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 P1nkl0bst3rInternalProblem implements Exception {}
|
||||||
|
|
||||||
|
class SzyInternalProblem implements Exception {}
|
||||||
|
|
||||||
class TetrioOskwareBridgeProblem implements Exception {}
|
class TetrioOskwareBridgeProblem implements Exception {}
|
||||||
|
|
||||||
class TetrioInternalProblem implements Exception {}
|
class TetrioInternalProblem implements Exception {}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer' as developer;
|
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:tetra_stats/main.dart' show packageInfo;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:tetra_stats/services/custom_http_client.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, List<News>> _newsCache = {};
|
||||||
final Map<String, Map<String, double?>> _topTRcache = {};
|
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 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();
|
static final TetrioService _shared = TetrioService._sharedInstance();
|
||||||
factory TetrioService() => _shared;
|
factory TetrioService() => _shared;
|
||||||
late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
|
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 {
|
Future<double?> fetchTopTR(String id) async {
|
||||||
try{
|
try{
|
||||||
var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id);
|
var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id);
|
||||||
|
@ -441,7 +480,7 @@ class TetrioService extends DB {
|
||||||
List<TetraLeagueAlphaRecord> matches = [];
|
List<TetraLeagueAlphaRecord> matches = [];
|
||||||
final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]);
|
final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]);
|
||||||
for (var match in results){
|
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;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -159,11 +159,12 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
|
||||||
List<TetrioPlayer> states = [];
|
List<TetrioPlayer> states = [];
|
||||||
TetraLeagueAlpha? compareWith;
|
TetraLeagueAlpha? compareWith;
|
||||||
var uniqueTL = <dynamic>{};
|
var uniqueTL = <dynamic>{};
|
||||||
|
tlMatches = tlStream.records;
|
||||||
if (isTracking){
|
if (isTracking){
|
||||||
await teto.storeState(me);
|
await teto.storeState(me);
|
||||||
await teto.saveTLMatchesFromStream(tlStream);
|
await teto.saveTLMatchesFromStream(tlStream);
|
||||||
tlMatches.addAll(await teto.getTLMatchesbyPlayerID(me.userId));
|
var storedRecords = await teto.getTLMatchesbyPlayerID(me.userId);
|
||||||
for (var match in tlStream.records) {
|
for (var match in storedRecords) {
|
||||||
if (!tlMatches.contains(match)) tlMatches.add(match);
|
if (!tlMatches.contains(match)) tlMatches.add(match);
|
||||||
}
|
}
|
||||||
tlMatches.sort((a, b) {
|
tlMatches.sort((a, b) {
|
||||||
|
@ -172,8 +173,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
|
||||||
if(a.timestamp.isAfter(b.timestamp)) return -1;
|
if(a.timestamp.isAfter(b.timestamp)) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
} else{
|
|
||||||
tlMatches = tlStream.records;
|
|
||||||
}
|
}
|
||||||
if(fetchHistory) await teto.fetchAndsaveTLHistory(_searchFor);
|
if(fetchHistory) await teto.fetchAndsaveTLHistory(_searchFor);
|
||||||
states.addAll(await teto.getPlayer(me.userId));
|
states.addAll(await teto.getPlayer(me.userId));
|
||||||
|
|
|
@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tetra_stats/gen/strings.g.dart';
|
import 'package:tetra_stats/gen/strings.g.dart';
|
||||||
import 'package:tetra_stats/services/crud_exceptions.dart';
|
import 'package:tetra_stats/services/crud_exceptions.dart';
|
||||||
import 'package:tetra_stats/services/tetrio_crud.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';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
late String oldWindowTitle;
|
late String oldWindowTitle;
|
||||||
|
@ -43,15 +43,6 @@ class SettingsState extends State<SettingsView> {
|
||||||
super.dispose();
|
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 {
|
Future<void> _getPreferences() async {
|
||||||
prefs = await SharedPreferences.getInstance();
|
prefs = await SharedPreferences.getInstance();
|
||||||
_setDefaultNickname(prefs.getString("player"));
|
_setDefaultNickname(prefs.getString("player"));
|
||||||
|
@ -248,7 +239,7 @@ class SettingsState extends State<SettingsView> {
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: (){
|
onTap: (){
|
||||||
_launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));
|
launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));
|
||||||
},
|
},
|
||||||
title: Text(t.aboutApp),
|
title: Text(t.aboutApp),
|
||||||
subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)),
|
subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)),
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
import 'dart:io';
|
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:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||||
import 'package:tetra_stats/gen/strings.g.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';
|
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();
|
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
|
||||||
|
@ -52,6 +60,52 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
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)}"),
|
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,
|
backgroundColor: Colors.black,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|
Loading…
Reference in New Issue