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
345 lines
9.5 KiB
Dart
345 lines
9.5 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'
|
|
show RustStreamSink;
|
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'
|
|
show ExternalLibrary;
|
|
|
|
import 'src/rust/api/session.dart' hide authMode;
|
|
import 'src/rust/api/session.dart' as _rust;
|
|
import 'src/rust/frb_generated.dart';
|
|
|
|
export 'src/rust/api/session.dart'
|
|
show
|
|
KolibriSession,
|
|
SessionOptions,
|
|
HandshakeInfo,
|
|
PushEvent,
|
|
WireLogEvent,
|
|
UploadEvent,
|
|
UploadEvent_Progress,
|
|
UploadEvent_Done,
|
|
UploadEvent_Error;
|
|
|
|
export 'src/rust/api/tls.dart' show setTrustMincifryCa, trustMincifryCa;
|
|
|
|
export 'src/rust/api/calls.dart'
|
|
show
|
|
CallSignaling,
|
|
CallParams,
|
|
IceServer,
|
|
ConnectionInfo,
|
|
TransmittedData,
|
|
decodeVcp,
|
|
connectCallSignaling,
|
|
parseConnection,
|
|
parseTransmittedData;
|
|
|
|
bool _initialized = false;
|
|
|
|
/// Load the native library and init the bindings; call once. On Flutter omit
|
|
/// [libraryPath] (bundled library used); for `dart run` pass the built dylib path.
|
|
Future<void> initKolibri({String? libraryPath}) async {
|
|
if (_initialized) return;
|
|
await RustLib.init(
|
|
externalLibrary:
|
|
libraryPath == null ? null : ExternalLibrary.open(libraryPath),
|
|
);
|
|
_initialized = true;
|
|
}
|
|
|
|
SessionOptions _options({
|
|
required String host,
|
|
required int port,
|
|
required String deviceId,
|
|
required String instanceId,
|
|
required String appVersion,
|
|
required int buildNumber,
|
|
required String deviceType,
|
|
required String osVersion,
|
|
required String timezone,
|
|
required String screen,
|
|
required String pushDeviceType,
|
|
required String arch,
|
|
required String locale,
|
|
required String deviceName,
|
|
required String deviceLocale,
|
|
required int clientSessionId,
|
|
required int pingIntervalSecs,
|
|
required bool pingInteractive,
|
|
required bool autoReconnect,
|
|
required bool insecureTls,
|
|
required String? proxy,
|
|
}) {
|
|
return SessionOptions(
|
|
host: host,
|
|
port: port,
|
|
deviceId: deviceId,
|
|
instanceId: instanceId,
|
|
appVersion: appVersion,
|
|
buildNumber: buildNumber,
|
|
deviceType: deviceType,
|
|
osVersion: osVersion,
|
|
timezone: timezone,
|
|
screen: screen,
|
|
pushDeviceType: pushDeviceType,
|
|
arch: arch,
|
|
locale: locale,
|
|
deviceName: deviceName,
|
|
deviceLocale: deviceLocale,
|
|
clientSessionId: clientSessionId,
|
|
pingIntervalSecs: BigInt.from(pingIntervalSecs),
|
|
pingInteractive: pingInteractive,
|
|
autoReconnect: autoReconnect,
|
|
insecureTls: insecureTls,
|
|
proxy: proxy,
|
|
);
|
|
}
|
|
|
|
/// Create a session with sensible defaults; override any device field to spoof.
|
|
KolibriSession openSession({
|
|
required String host,
|
|
int port = 443,
|
|
String deviceId = 'kolibri-dart',
|
|
String instanceId = 'kolibri-dart',
|
|
String appVersion = '26.20.2',
|
|
int buildNumber = 6758,
|
|
String deviceType = 'ANDROID',
|
|
String osVersion = 'Android 14',
|
|
String timezone = 'Europe/Moscow',
|
|
String screen = '420dpi 420dpi 1080x2340',
|
|
String pushDeviceType = 'GCM',
|
|
String arch = 'arm64-v8a',
|
|
String locale = 'ru',
|
|
String deviceName = 'Dart',
|
|
String deviceLocale = 'ru',
|
|
int clientSessionId = 1700000000,
|
|
int pingIntervalSecs = 30,
|
|
bool pingInteractive = true,
|
|
bool autoReconnect = true,
|
|
bool insecureTls = false,
|
|
String? proxy,
|
|
}) {
|
|
return KolibriSession(
|
|
options: _options(
|
|
host: host,
|
|
port: port,
|
|
deviceId: deviceId,
|
|
instanceId: instanceId,
|
|
appVersion: appVersion,
|
|
buildNumber: buildNumber,
|
|
deviceType: deviceType,
|
|
osVersion: osVersion,
|
|
timezone: timezone,
|
|
screen: screen,
|
|
pushDeviceType: pushDeviceType,
|
|
arch: arch,
|
|
locale: locale,
|
|
deviceName: deviceName,
|
|
deviceLocale: deviceLocale,
|
|
clientSessionId: clientSessionId,
|
|
pingIntervalSecs: pingIntervalSecs,
|
|
pingInteractive: pingInteractive,
|
|
autoReconnect: autoReconnect,
|
|
insecureTls: insecureTls,
|
|
proxy: proxy,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Like [openSession], but also returns a stream of every packet in both
|
|
/// directions (requests, pushes, handshake, ping) rendered for logging.
|
|
(KolibriSession, Stream<WireLogEvent>) openSessionWithWireLog({
|
|
required String host,
|
|
int port = 443,
|
|
String deviceId = 'kolibri-dart',
|
|
String instanceId = 'kolibri-dart',
|
|
String appVersion = '26.20.2',
|
|
int buildNumber = 6758,
|
|
String deviceType = 'ANDROID',
|
|
String osVersion = 'Android 14',
|
|
String timezone = 'Europe/Moscow',
|
|
String screen = '420dpi 420dpi 1080x2340',
|
|
String pushDeviceType = 'GCM',
|
|
String arch = 'arm64-v8a',
|
|
String locale = 'ru',
|
|
String deviceName = 'Dart',
|
|
String deviceLocale = 'ru',
|
|
int clientSessionId = 1700000000,
|
|
int pingIntervalSecs = 30,
|
|
bool pingInteractive = true,
|
|
bool autoReconnect = true,
|
|
bool insecureTls = false,
|
|
String? proxy,
|
|
}) {
|
|
final sink = RustStreamSink<WireLogEvent>();
|
|
final session = KolibriSession(
|
|
options: _options(
|
|
host: host,
|
|
port: port,
|
|
deviceId: deviceId,
|
|
instanceId: instanceId,
|
|
appVersion: appVersion,
|
|
buildNumber: buildNumber,
|
|
deviceType: deviceType,
|
|
osVersion: osVersion,
|
|
timezone: timezone,
|
|
screen: screen,
|
|
pushDeviceType: pushDeviceType,
|
|
arch: arch,
|
|
locale: locale,
|
|
deviceName: deviceName,
|
|
deviceLocale: deviceLocale,
|
|
clientSessionId: clientSessionId,
|
|
pingIntervalSecs: pingIntervalSecs,
|
|
pingInteractive: pingInteractive,
|
|
autoReconnect: autoReconnect,
|
|
insecureTls: insecureTls,
|
|
proxy: proxy,
|
|
),
|
|
wireLog: sink,
|
|
);
|
|
return (session, sink.stream);
|
|
}
|
|
|
|
/// Build a request from a Dart `Map`; the core does the msgpack. A `Uint8List`
|
|
/// value is sent as a binary field; an `int` map key is sent as an integer key
|
|
/// (JSON can't hold either, so both travel tagged and the core untags them).
|
|
extension KolibriRequestMap on KolibriSession {
|
|
Future<Map<String, dynamic>> requestMap(
|
|
int opcode,
|
|
Map<String, dynamic> payload,
|
|
) async {
|
|
final out = await requestJson(
|
|
opcode: opcode,
|
|
jsonIn: jsonEncode(_escapeBinary(payload)),
|
|
);
|
|
return _asMap(out);
|
|
}
|
|
}
|
|
|
|
/// A full response: packet command plus decoded payload. A server error is
|
|
/// reported here (isError/errorKey), not thrown.
|
|
class KolibriResponse {
|
|
KolibriResponse({
|
|
required this.cmd,
|
|
required this.opcode,
|
|
required this.payload,
|
|
this.errorMessage,
|
|
this.errorKey,
|
|
});
|
|
|
|
final int cmd;
|
|
final int opcode;
|
|
final Map<String, dynamic> payload;
|
|
final String? errorMessage;
|
|
final String? errorKey;
|
|
|
|
bool get isOk => cmd == 1;
|
|
bool get isNotFound => cmd == 2;
|
|
bool get isError => cmd == 3;
|
|
}
|
|
|
|
extension KolibriRequestMapFull on KolibriSession {
|
|
Future<KolibriResponse> requestMapFull(
|
|
int opcode,
|
|
Map<String, dynamic> payload,
|
|
) async {
|
|
final r = await requestFull(
|
|
opcode: opcode,
|
|
jsonIn: jsonEncode(_escapeBinary(payload)),
|
|
);
|
|
return KolibriResponse(
|
|
cmd: r.cmd,
|
|
opcode: r.opcode,
|
|
payload: _asMap(r.payloadJson),
|
|
errorMessage: r.errorMessage,
|
|
errorKey: r.errorKey,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handshake payload decoded as a map.
|
|
extension KolibriHandshakeMap on HandshakeInfo {
|
|
Map<String, dynamic> get payloadMap => _asMap(payloadJson);
|
|
}
|
|
|
|
/// The push payload decoded as a map.
|
|
extension KolibriPushMap on PushEvent {
|
|
Map<String, dynamic> get payloadMap => _asMap(payloadJson);
|
|
}
|
|
|
|
/// Server pushes with their payloads already decoded to maps.
|
|
extension KolibriPushesMap on KolibriSession {
|
|
Stream<(int, Map<String, dynamic>)> pushesMap() =>
|
|
pushes().map((e) => (e.opcode, e.payloadMap));
|
|
}
|
|
|
|
Map<String, dynamic> _asMap(String jsonStr) {
|
|
final decoded = _unescapeBinary(jsonDecode(jsonStr));
|
|
return decoded is Map ? Map<String, dynamic>.from(decoded) : <String, dynamic>{};
|
|
}
|
|
|
|
/// Inverse of [_escapeBinary]: `{"$bin":"<base64>"}` -> `Uint8List`.
|
|
dynamic _unescapeBinary(dynamic value) {
|
|
if (value is Map) {
|
|
if (value.length == 1 && value[r'$bin'] is String) {
|
|
return base64.decode(value[r'$bin'] as String);
|
|
}
|
|
return value.map((k, v) => MapEntry(k, _unescapeBinary(v)));
|
|
}
|
|
if (value is List) {
|
|
return value.map(_unescapeBinary).toList();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
dynamic _escapeBinary(dynamic value) {
|
|
if (value is Uint8List) {
|
|
return {r'$bin': base64.encode(value)};
|
|
}
|
|
if (value is Map) {
|
|
return value.map(
|
|
(k, v) => MapEntry(k is int ? '\$int:$k' : k, _escapeBinary(v)),
|
|
);
|
|
}
|
|
if (value is List) {
|
|
return value.map(_escapeBinary).toList();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/// 96-byte anti-spoof fingerprint (authRequest `mode` / login `chatCacheFingerprint`).
|
|
/// signature/dex/so default to the known app values; override (raw bytes) per app build.
|
|
Uint8List authMode(
|
|
int callsSeed,
|
|
String deviceId, {
|
|
List<int>? signature,
|
|
List<int>? dex,
|
|
List<int>? so,
|
|
}) {
|
|
return _rust.authMode(
|
|
signature: signature ?? _defaultSignatureDigest,
|
|
dex: dex ?? _defaultDexDigest,
|
|
so: so ?? _defaultSoDigest,
|
|
callsSeed: callsSeed,
|
|
deviceId: deviceId,
|
|
);
|
|
}
|
|
|
|
final Uint8List _defaultSignatureDigest = _hex(
|
|
'1684414033eb263e2c615f8b7df5ed8793850a07656304997fbf07e9e21e1e93');
|
|
final Uint8List _defaultSoDigest = _hex(
|
|
'90e2fb8745b17b42a10182f8d8ac590e3fca5b311e2ce2d5144fa2c18cb3090d');
|
|
final Uint8List _defaultDexDigest = _hex(
|
|
'0a6265f6e5d8231b9cba641f8c40475e6f3baeb06ed41b804b9bf7307aa4214e');
|
|
|
|
Uint8List _hex(String s) {
|
|
final out = Uint8List(s.length ~/ 2);
|
|
for (var i = 0; i < out.length; i++) {
|
|
out[i] = int.parse(s.substring(i * 2, i * 2 + 2), radix: 16);
|
|
}
|
|
return out;
|
|
}
|