Files
Qlyra/lib/core/webpush/max_web_socket.dart
sevenhill f74057ce9b
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Rebrand application as Qlyra
2026-08-30 13:16:17 +03:00

194 lines
4.9 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import '../utils/logger.dart';
import 'max_web_protocol.dart';
class MaxWebException implements Exception {
final String message;
final String? code;
const MaxWebException(this.message, {this.code});
@override
String toString() => code == null ? message : '$code: $message';
}
class MaxWebDevice {
final String deviceId;
final String appVersion;
final String osVersion;
final String deviceName;
final String screen;
final String timezone;
final String locale;
final String userAgent;
const MaxWebDevice({
required this.deviceId,
this.appVersion = '26.8.8',
this.osVersion = 'iOS',
this.deviceName = 'Safari',
required this.screen,
this.timezone = 'Europe/Moscow',
this.locale = 'ru',
required this.userAgent,
});
Map<String, Object?> toHandshakeUserAgent() => <String, Object?>{
'deviceType': 'WEB',
'pushDeviceType': 'WEBPUSH',
'locale': locale,
'deviceLocale': locale,
'osVersion': osVersion,
'deviceName': deviceName,
'headerUserAgent': userAgent,
'isPwa': true,
'appVersion': appVersion,
'screen': screen,
'timezone': timezone,
};
}
class MaxWebSocketSession {
static const String endpoint = 'wss://api.oneme.ru/websocket';
static const String origin = 'https://web.max.ru';
static const Duration requestTimeout = Duration(seconds: 30);
static const int _opcodePing = 1;
static const int _opcodeSessionInit = 6;
final MaxWebDevice device;
WebSocket? _socket;
StreamSubscription<dynamic>? _subscription;
final Map<int, Completer<Object?>> _pending = <int, Completer<Object?>>{};
int _seq = 0;
bool _closed = false;
MaxWebSocketSession({required this.device});
Future<Object?> connect() async {
final socket = await WebSocket.connect(
endpoint,
headers: <String, Object>{
'Origin': origin,
'User-Agent': device.userAgent,
},
);
_socket = socket;
_subscription = socket.listen(
_onFrame,
onError: (Object error) => _failAll(MaxWebException('сокет: $error')),
onDone: () =>
_failAll(const MaxWebException('соединение закрыто сервером')),
cancelOnError: true,
);
return request(_opcodeSessionInit, <String, Object?>{
'userAgent': device.toHandshakeUserAgent(),
'deviceId': device.deviceId,
});
}
Future<Object?> request(int opcode, Object? payload) {
final socket = _socket;
if (socket == null || _closed) {
return Future<Object?>.error(
const MaxWebException('сессия не подключена'),
);
}
_seq += 1;
final seq = _seq;
final completer = Completer<Object?>();
_pending[seq] = completer;
socket.add(
MaxWebFraming.encode(
cmd: MaxWebCmd.request,
seq: seq,
opcode: opcode,
payload: payload,
),
);
return completer.future.timeout(
requestTimeout,
onTimeout: () {
_pending.remove(seq);
throw MaxWebException('опкод $opcode: сервер не ответил');
},
);
}
void _onFrame(dynamic raw) {
if (raw is! List<int>) return;
final MaxWebFrame frame;
try {
frame = MaxWebFraming.decode(Uint8List.fromList(raw));
} catch (e) {
logger.w('WebPush: не разобрал кадр ($e)');
return;
}
if (frame.cmd == MaxWebCmd.request) {
if (frame.opcode == _opcodePing) {
_socket?.add(
MaxWebFraming.encode(
cmd: MaxWebCmd.ok,
seq: frame.seq,
opcode: _opcodePing,
),
);
}
return;
}
final completer = _pending.remove(frame.seq);
if (completer == null || completer.isCompleted) return;
if (frame.isError) {
completer.completeError(_errorFrom(frame));
return;
}
completer.complete(frame.payload);
}
MaxWebException _errorFrom(MaxWebFrame frame) {
final payload = frame.payload;
if (payload is Map) {
final message = payload['localizedMessage'] ?? payload['message'];
final code = payload['error'];
if (message is String && message.isNotEmpty) {
return MaxWebException(message, code: code is String ? code : null);
}
if (code is String && code.isNotEmpty) {
return MaxWebException(code, code: code);
}
}
return MaxWebException(
'опкод ${frame.opcode}: ошибка сервера (cmd=${frame.cmd})',
);
}
void _failAll(MaxWebException error) {
for (final completer in _pending.values) {
if (!completer.isCompleted) completer.completeError(error);
}
_pending.clear();
}
Future<void> close() async {
if (_closed) return;
_closed = true;
_failAll(const MaxWebException('сессия закрыта'));
await _subscription?.cancel();
_subscription = null;
await _socket?.close();
_socket = null;
}
}