mirror of
https://github.com/lichess-org/mobile.git
synced 2026-05-26 13:50:52 +00:00
More wip on game creation
This commit is contained in:
@@ -44,7 +44,6 @@ dart_code_metrics:
|
||||
- avoid-unnecessary-type-assertions
|
||||
- avoid-unnecessary-type-casts
|
||||
- avoid-unrelated-type-assertions
|
||||
- prefer-iterable-of
|
||||
- prefer-first
|
||||
- prefer-immediate-return
|
||||
- prefer-iterable-of
|
||||
@@ -61,7 +60,6 @@ dart_code_metrics:
|
||||
- avoid-shrink-wrap-in-lists
|
||||
- avoid-unnecessary-setstate
|
||||
- avoid-expanded-as-spacer
|
||||
- avoid-wrapping-in-padding
|
||||
- check-for-equals-in-render-object-setters
|
||||
- consistent-update-render-object
|
||||
- prefer-const-border-radius
|
||||
|
||||
@@ -8,6 +8,11 @@ const kLichessWSHost = String.fromEnvironment(
|
||||
defaultValue: 'wss://socket.lichess.org',
|
||||
);
|
||||
|
||||
const kLichessWSSecret = String.fromEnvironment(
|
||||
'LICHESS_WS_SECRET',
|
||||
defaultValue: 'somethingElseInProd',
|
||||
);
|
||||
|
||||
const kLichessDevUser =
|
||||
String.fromEnvironment('LICHESS_DEV_USER', defaultValue: 'lichess');
|
||||
const kLichessDevPassword = String.fromEnvironment('LICHESS_DEV_PASSWORD');
|
||||
|
||||
@@ -35,7 +35,8 @@ AuthClient authClient(AuthClientRef ref) {
|
||||
crashlytics,
|
||||
);
|
||||
ref.onDispose(() {
|
||||
authClient.close();
|
||||
logger.info('Disposing AuthClient.');
|
||||
httpClient.close();
|
||||
});
|
||||
return authClient;
|
||||
}
|
||||
@@ -83,18 +84,7 @@ class AuthClient {
|
||||
Result.capture(
|
||||
(retryOnError ? _retryClient : _client).get(url, headers: headers),
|
||||
).mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
if (kReleaseMode) {
|
||||
_crashlytics.recordError(
|
||||
error,
|
||||
stackTrace,
|
||||
reason: 'a non-fatal http request error',
|
||||
information: [
|
||||
'url: $url',
|
||||
'headers: $headers',
|
||||
],
|
||||
);
|
||||
}
|
||||
_recordError('GET', error, stackTrace, url, headers);
|
||||
return GenericIOException();
|
||||
}).flatMap(
|
||||
(response) => _validateResponseStatusResult('GET', url, response),
|
||||
@@ -111,18 +101,7 @@ class AuthClient {
|
||||
(retryOnError ? _retryClient : _client)
|
||||
.post(url, headers: headers, body: body, encoding: encoding),
|
||||
).mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
if (kReleaseMode) {
|
||||
_crashlytics.recordError(
|
||||
error,
|
||||
stackTrace,
|
||||
reason: 'a non-fatal http request error',
|
||||
information: [
|
||||
'url: $url',
|
||||
'headers: $headers',
|
||||
],
|
||||
);
|
||||
}
|
||||
_recordError('POST', error, stackTrace, url, headers);
|
||||
return GenericIOException();
|
||||
}).flatMap(
|
||||
(response) => _validateResponseStatusResult('POST', url, response),
|
||||
@@ -139,18 +118,7 @@ class AuthClient {
|
||||
(retryOnError ? _retryClient : _client)
|
||||
.delete(url, headers: headers, body: body, encoding: encoding),
|
||||
).mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
if (kReleaseMode) {
|
||||
_crashlytics.recordError(
|
||||
error,
|
||||
stackTrace,
|
||||
reason: 'a non-fatal http request error',
|
||||
information: [
|
||||
'url: $url',
|
||||
'headers: $headers',
|
||||
],
|
||||
);
|
||||
}
|
||||
_recordError('DELETE', error, stackTrace, url, headers);
|
||||
return GenericIOException();
|
||||
}).flatMap(
|
||||
(response) => _validateResponseStatusResult('DELETE', url, response),
|
||||
@@ -166,18 +134,7 @@ class AuthClient {
|
||||
}
|
||||
return Result.capture(_client.send(request))
|
||||
.mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
if (kReleaseMode) {
|
||||
_crashlytics.recordError(
|
||||
error,
|
||||
stackTrace,
|
||||
reason: 'a non-fatal http request error',
|
||||
information: [
|
||||
'url: $url',
|
||||
'headers: $headers',
|
||||
],
|
||||
);
|
||||
}
|
||||
_recordError('GET', error, stackTrace, url, headers);
|
||||
return GenericIOException();
|
||||
})
|
||||
.flatMap((r) => _validateResponseStatusResult('GET', url, r))
|
||||
@@ -223,8 +180,34 @@ class AuthClient {
|
||||
);
|
||||
}
|
||||
|
||||
void _recordError(
|
||||
String method,
|
||||
Object error,
|
||||
StackTrace? stackTrace,
|
||||
Uri url,
|
||||
Map<String, String>? headers,
|
||||
) {
|
||||
// Ignore canceling seek requests
|
||||
if (error is ClientException && url.path.contains('api/board/seek')) {
|
||||
return;
|
||||
}
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
if (kReleaseMode) {
|
||||
_crashlytics.recordError(
|
||||
error,
|
||||
stackTrace,
|
||||
reason: 'a non-fatal http request error',
|
||||
information: [
|
||||
'method: $method',
|
||||
'url: $url',
|
||||
'headers: $headers',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
_log.info('Closing AuthClient.');
|
||||
_retryClient.close();
|
||||
_client.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
@@ -25,6 +27,8 @@ AuthSocket authSocket(AuthSocketRef ref) {
|
||||
return authSocket;
|
||||
}
|
||||
|
||||
final hmacSha1 = Hmac(sha1, utf8.encode(kLichessWSSecret));
|
||||
|
||||
/// WebSocket channel wrapper to authenticate with lichess
|
||||
///
|
||||
/// It automatically generate a new SRI for each connection.
|
||||
@@ -55,9 +59,12 @@ class AuthSocket {
|
||||
final info = _ref.read(packageInfoProvider);
|
||||
final sri = genRandomString(12);
|
||||
final uri = Uri.parse('$kLichessWSHost$kWebSocketPath?sri=$sri');
|
||||
final bearer = session != null
|
||||
? '${session.token}:${hmacSha1.convert(utf8.encode(session.token))}'
|
||||
: '';
|
||||
final headers = session != null
|
||||
? {
|
||||
'Authorization': 'Bearer ${session.token}',
|
||||
'Authorization': 'Bearer $bearer',
|
||||
'User-Agent': AuthClient.userAgent(info, session.user),
|
||||
}
|
||||
: {
|
||||
@@ -83,9 +90,14 @@ class AuthSocket {
|
||||
///
|
||||
/// Will not do anything if the channel is not connected.
|
||||
void switchRoute(Uri route) {
|
||||
final msg = "{ t: 'switch', d: { uri: '${route.path}' }}";
|
||||
print('switch route: $msg');
|
||||
sink?.add(msg);
|
||||
sink?.add(
|
||||
jsonEncode({
|
||||
't': 'switch',
|
||||
'd': {
|
||||
'uri': '${route.path}?sri=$sri',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the current WebSocket sink
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:deep_pick/deep_pick.dart';
|
||||
|
||||
import 'package:lichess_mobile/src/model/common/id.dart';
|
||||
import 'package:lichess_mobile/src/model/common/perf.dart';
|
||||
import 'package:lichess_mobile/src/model/common/chess.dart';
|
||||
import 'package:lichess_mobile/src/model/common/speed.dart';
|
||||
import 'package:lichess_mobile/src/model/game/game.dart';
|
||||
import 'package:lichess_mobile/src/model/game/player.dart';
|
||||
import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart';
|
||||
import 'package:lichess_mobile/src/model/lobby/game_seek.dart';
|
||||
import 'package:lichess_mobile/src/model/auth/auth_client.dart';
|
||||
import 'package:lichess_mobile/src/model/auth/auth_socket.dart';
|
||||
import 'package:lichess_mobile/src/model/settings/play_preferences.dart';
|
||||
|
||||
@@ -32,28 +41,76 @@ class CreateGameService {
|
||||
final completer = Completer<PlayableGame>();
|
||||
|
||||
final stream = socket.connect();
|
||||
|
||||
_socketSubscription = stream.listen((event) {
|
||||
print('event: $event');
|
||||
debugPrint('event: $event');
|
||||
final msg = jsonDecode(event as String) as Map<String, dynamic>;
|
||||
switch (msg['t'] as String) {
|
||||
case 'redirect':
|
||||
// ignore: avoid_dynamic_calls
|
||||
final gameId = msg['d']['id'] as String;
|
||||
socket.switchRoute(Uri(path: '/play/$gameId'));
|
||||
|
||||
case 'full':
|
||||
final game =
|
||||
// ignore: avoid_dynamic_calls
|
||||
_playableGameFromJson(msg['d']['game'] as Map<String, dynamic>);
|
||||
completer.complete(game);
|
||||
}
|
||||
});
|
||||
socket.sink?.add('null');
|
||||
|
||||
socket.switchRoute(Uri(path: '/lobby/socket'));
|
||||
socket.switchRoute(Uri(path: '/lobby/socket/v5'));
|
||||
|
||||
// await Result.release(
|
||||
// lobbyRepo.createSeek(
|
||||
// GameSeek(
|
||||
// time: Duration(seconds: playPref.timeIncrement.time),
|
||||
// increment: Duration(seconds: playPref.timeIncrement.increment),
|
||||
// // TODO add rated choice
|
||||
// rated: true,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
_log.info('Creating new online game');
|
||||
|
||||
await Result.release(
|
||||
lobbyRepo.createSeek(
|
||||
GameSeek(
|
||||
time: Duration(seconds: playPref.timeIncrement.time),
|
||||
increment: Duration(seconds: playPref.timeIncrement.increment),
|
||||
// TODO add rated choice
|
||||
rated: true,
|
||||
),
|
||||
sri: socket.sri!,
|
||||
),
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
ref.invalidate(authClientProvider);
|
||||
_socketSubscription?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
PlayableGame _playableGameFromJson(Map<String, dynamic> json) =>
|
||||
_playableGameFromPick(pick(json).required());
|
||||
|
||||
PlayableGame _playableGameFromPick(RequiredPick pick) {
|
||||
return PlayableGame(
|
||||
id: pick('id').asGameIdOrThrow(),
|
||||
rated: pick('rated').asBoolOrThrow(),
|
||||
speed: pick('speed').asSpeedOrThrow(),
|
||||
perf: pick('perf').asPerfOrThrow(),
|
||||
player: pick('player').asSideOrThrow(),
|
||||
status: pick('status').asGameStatusOrThrow(),
|
||||
white: pick('players', 'white').letOrThrow(_playerFromUserGamePick),
|
||||
black: pick('players', 'black').letOrThrow(_playerFromUserGamePick),
|
||||
variant: pick('variant').asVariantOrThrow(),
|
||||
fen: pick('fen').asStringOrThrow(),
|
||||
initialFen: pick('initialFen').asStringOrNull(),
|
||||
);
|
||||
}
|
||||
|
||||
Player _playerFromUserGamePick(RequiredPick pick) {
|
||||
return Player(
|
||||
id: pick('user', 'id').asUserIdOrNull(),
|
||||
name: pick('user', 'name').asStringOrNull() ?? 'Stockfish',
|
||||
patron: pick('user', 'patron').asBoolOrNull(),
|
||||
title: pick('user', 'title').asStringOrNull(),
|
||||
rating: pick('rating').asIntOrNull(),
|
||||
ratingDiff: pick('ratingDiff').asIntOrNull(),
|
||||
aiLevel: pick('aiLevel').asIntOrNull(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ class PlayableGame with _$PlayableGame {
|
||||
required Variant variant,
|
||||
required Speed speed,
|
||||
required Perf perf,
|
||||
required String initialFen,
|
||||
required Side orientation,
|
||||
required String fen,
|
||||
String? initialFen,
|
||||
required Side player,
|
||||
required GameStatus status,
|
||||
Move? lastMove,
|
||||
int? turns,
|
||||
|
||||
@@ -21,10 +21,10 @@ class LobbyRepository {
|
||||
|
||||
final AuthClient authClient;
|
||||
|
||||
FutureResult<void> createSeek(GameSeek seek) {
|
||||
FutureResult<void> createSeek(GameSeek seek, {required String sri}) {
|
||||
return authClient.post(
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/board/seek',
|
||||
'$kLichessHost/api/board/seek?sri=$sri',
|
||||
),
|
||||
body: seek.requestBody,
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ import 'package:lichess_mobile/src/utils/immersive_mode.dart';
|
||||
part 'online_game_screen.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<PlayableGame> _playableGame(_PlayableGameRef ref) async {
|
||||
Future<PlayableGame> _playableGame(_PlayableGameRef ref) {
|
||||
final service = ref.watch(createGameServiceProvider);
|
||||
ref.onDispose(service.dispose);
|
||||
return service.newOnlineGame();
|
||||
|
||||
@@ -240,15 +240,19 @@ class _ChoiceChip extends StatelessWidget {
|
||||
? CupertinoColors.systemBackground
|
||||
: CupertinoColors.secondarySystemBackground
|
||||
.resolveFrom(context),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
|
||||
border: selected
|
||||
? Border.all(
|
||||
color: CupertinoColors.activeBlue.resolveFrom(context),
|
||||
width: 2.0,
|
||||
? Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: CupertinoColors.activeBlue.resolveFrom(context),
|
||||
width: 2.0,
|
||||
),
|
||||
)
|
||||
: Border.all(
|
||||
color: Colors.transparent,
|
||||
width: 2.0,
|
||||
: const Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
|
||||
@@ -60,6 +60,7 @@ dependencies:
|
||||
flutter_displaymode: ^0.6.0
|
||||
web_socket_channel: ^2.4.0
|
||||
device_info_plus: ^9.0.2
|
||||
crypto: ^3.0.3
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
|
||||
Reference in New Issue
Block a user