🎨 Use feature based folder structure
This commit is contained in:
549
lib/activity/activity_rpc.dart
Normal file
549
lib/activity/activity_rpc.dart
Normal file
@@ -0,0 +1,549 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart' hide Response;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/core/models/activity.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
// Conditional imports for IPC server - use web stubs on web platform
|
||||
import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart';
|
||||
|
||||
part 'activity_rpc.g.dart';
|
||||
|
||||
const String kRpcLogPrefix = 'arRPC.websocket';
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
|
||||
// IPC Constants
|
||||
const String kIpcBasePath = 'discord-ipc';
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
|
||||
class ActivityRpcServer {
|
||||
static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
|
||||
Map<String, Function>
|
||||
handlers; // {connection: (socket), message: (socket, data), close: (socket)}
|
||||
HttpServer? _httpServer;
|
||||
IpcServer? _ipcServer;
|
||||
final List<WebSocketChannel> _wsSockets = [];
|
||||
|
||||
ActivityRpcServer(this.handlers);
|
||||
|
||||
void updateHandlers(Map<String, Function> newHandlers) {
|
||||
handlers = newHandlers;
|
||||
}
|
||||
|
||||
// Start the server
|
||||
Future<void> start() async {
|
||||
int port = portRange[0];
|
||||
bool wsSuccess = false;
|
||||
|
||||
// Start WebSocket server
|
||||
while (port <= portRange[1]) {
|
||||
talker.log('[$kRpcLogPrefix] Trying port $port');
|
||||
try {
|
||||
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
|
||||
talker.log('[$kRpcLogPrefix] Listening on $port');
|
||||
|
||||
shelf_io.serveRequests(_httpServer!, (Request request) async {
|
||||
talker.log('[$kRpcLogPrefix] New request');
|
||||
if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
|
||||
final handler = webSocketHandler((WebSocketChannel channel, _) {
|
||||
_wsSockets.add(channel);
|
||||
_onWsConnection(channel, request);
|
||||
});
|
||||
return handler(request);
|
||||
}
|
||||
talker.log('New request disposed due to not websocket');
|
||||
return Response.notFound('Not a WebSocket request');
|
||||
});
|
||||
wsSuccess = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e is SocketException && e.osError?.errorCode == 98) {
|
||||
talker.log('[$kRpcLogPrefix] $port in use!');
|
||||
} else {
|
||||
talker.log('[$kRpcLogPrefix] HTTP error: $e');
|
||||
}
|
||||
port++;
|
||||
await Future.delayed(Duration(milliseconds: 100)); // Add delay
|
||||
}
|
||||
}
|
||||
|
||||
if (!wsSuccess) {
|
||||
throw Exception(
|
||||
'Failed to bind to any port in range ${portRange[0]}–${portRange[1]}',
|
||||
);
|
||||
}
|
||||
|
||||
// Start IPC server
|
||||
final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
|
||||
if (shouldStartIpc) {
|
||||
try {
|
||||
_ipcServer = MultiPlatformIpcServer();
|
||||
|
||||
// Set up IPC handlers
|
||||
_ipcServer!.handlePacket = (socket, packet, _) {
|
||||
_handleIpcPacket(socket, packet);
|
||||
};
|
||||
|
||||
// Set up IPC close handler
|
||||
if (!kIsWeb) {
|
||||
(_ipcServer as dynamic).onSocketClose = (socket) {
|
||||
handlers['close']?.call(socket);
|
||||
};
|
||||
}
|
||||
|
||||
await _ipcServer!.start();
|
||||
} catch (e) {
|
||||
talker.log('[$kRpcLogPrefix] IPC server error: $e');
|
||||
}
|
||||
} else {
|
||||
talker.log('IPC server disabled on macOS or web');
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
Future<void> stop() async {
|
||||
// Stop WebSocket server
|
||||
for (var socket in _wsSockets) {
|
||||
try {
|
||||
await socket.sink.close();
|
||||
} catch (e) {
|
||||
talker.log('[$kRpcLogPrefix] Error closing WebSocket: $e');
|
||||
}
|
||||
}
|
||||
_wsSockets.clear();
|
||||
await _httpServer?.close(force: true);
|
||||
|
||||
// Stop IPC server
|
||||
await _ipcServer?.stop();
|
||||
|
||||
talker.log('[$kRpcLogPrefix] Servers stopped');
|
||||
}
|
||||
|
||||
// Handle new WebSocket connection
|
||||
void _onWsConnection(WebSocketChannel socket, Request request) {
|
||||
final uri = request.url;
|
||||
final params = uri.queryParameters;
|
||||
final ver = int.tryParse(params['v'] ?? '1') ?? 1;
|
||||
final encoding = params['encoding'] ?? 'json';
|
||||
final clientId = params['client_id'] ?? '';
|
||||
final origin = request.headers['origin'] ?? '';
|
||||
|
||||
talker.log('New WS connection! origin: $origin, params: $params');
|
||||
|
||||
if (origin.isNotEmpty &&
|
||||
![
|
||||
'https://discord.com',
|
||||
'https://ptb.discord.com',
|
||||
'https://canary.discord.com',
|
||||
].contains(origin)) {
|
||||
talker.log('[$kRpcLogPrefix] Disallowed origin: $origin');
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding != 'json') {
|
||||
talker.log('Unsupported encoding requested: $encoding');
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ver != 1) {
|
||||
talker.log('[$kRpcLogPrefix] Unsupported version requested: $ver');
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
|
||||
|
||||
socket.stream.listen(
|
||||
(data) => _onWsMessage(socketWithMeta, data),
|
||||
onError: (e) {
|
||||
talker.log('[$kRpcLogPrefix] WS socket error: $e');
|
||||
},
|
||||
onDone: () {
|
||||
talker.log('[$kRpcLogPrefix] WS socket closed');
|
||||
handlers['close']?.call(socketWithMeta);
|
||||
_wsSockets.remove(socket);
|
||||
},
|
||||
);
|
||||
|
||||
handlers['connection']?.call(socketWithMeta);
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket message
|
||||
Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
|
||||
if (data is! String) {
|
||||
talker.log('Invalid WebSocket message: not a string');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final jsonData = await compute(jsonDecode, data);
|
||||
if (jsonData is! Map<String, dynamic>) {
|
||||
talker.log('Invalid WebSocket message: not a JSON object');
|
||||
return;
|
||||
}
|
||||
talker.log('[$kRpcLogPrefix] WS message: $jsonData');
|
||||
handlers['message']?.call(socket, jsonData);
|
||||
} catch (e) {
|
||||
talker.log('[$kRpcLogPrefix] WS message parse error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IPC packet
|
||||
void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
|
||||
switch (packet.type) {
|
||||
case IpcTypes.ping:
|
||||
talker.log('[$kRpcLogPrefix] IPC ping received');
|
||||
socket.sendPong(packet.data);
|
||||
break;
|
||||
|
||||
case IpcTypes.pong:
|
||||
talker.log('[$kRpcLogPrefix] IPC pong received');
|
||||
break;
|
||||
|
||||
case IpcTypes.handshake:
|
||||
if (socket.handshook) {
|
||||
throw Exception('Already handshook');
|
||||
}
|
||||
socket.handshook = true;
|
||||
_onIpcHandshake(socket, packet.data);
|
||||
break;
|
||||
|
||||
case IpcTypes.frame:
|
||||
if (!socket.handshook) {
|
||||
throw Exception('Need to handshake first');
|
||||
}
|
||||
talker.log('[$kRpcLogPrefix] IPC frame: ${packet.data}');
|
||||
handlers['message']?.call(socket, packet.data);
|
||||
break;
|
||||
|
||||
case IpcTypes.close:
|
||||
socket.close();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception('Invalid packet type: ${packet.type}');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IPC handshake
|
||||
void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||
talker.log('[$kRpcLogPrefix] IPC handshake: $params');
|
||||
|
||||
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||
final clientId = params['client_id']?.toString() ?? '';
|
||||
|
||||
if (ver != 1) {
|
||||
talker.log('IPC unsupported version requested: $ver');
|
||||
socket.closeWithCode(IpcErrorCodes.invalidVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientId.isEmpty) {
|
||||
talker.log('[$kRpcLogPrefix] IPC client ID required');
|
||||
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.clientId = clientId;
|
||||
|
||||
handlers['connection']?.call(socket);
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket wrapper
|
||||
class _WsSocketWrapper {
|
||||
final WebSocketChannel channel;
|
||||
final String clientId;
|
||||
final String encoding;
|
||||
|
||||
_WsSocketWrapper(this.channel, this.clientId, this.encoding);
|
||||
|
||||
void send(Map<String, dynamic> msg) {
|
||||
talker.log('[$kRpcLogPrefix] WS sending: $msg');
|
||||
channel.sink.add(jsonEncode(msg));
|
||||
}
|
||||
}
|
||||
|
||||
// State management for server status and activities
|
||||
class ServerState {
|
||||
final String status;
|
||||
final List<String> activities;
|
||||
final String? currentActivityManualId;
|
||||
final Map<String, dynamic>? currentActivityData;
|
||||
|
||||
ServerState({
|
||||
required this.status,
|
||||
this.activities = const [],
|
||||
this.currentActivityManualId,
|
||||
this.currentActivityData,
|
||||
});
|
||||
|
||||
ServerState copyWith({
|
||||
String? status,
|
||||
List<String>? activities,
|
||||
String? currentActivityManualId,
|
||||
Map<String, dynamic>? currentActivityData,
|
||||
}) {
|
||||
return ServerState(
|
||||
status: status ?? this.status,
|
||||
activities: activities ?? this.activities,
|
||||
currentActivityManualId:
|
||||
currentActivityManualId ?? this.currentActivityManualId,
|
||||
currentActivityData: currentActivityData ?? this.currentActivityData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ServerStateNotifier extends Notifier<ServerState> {
|
||||
late final ActivityRpcServer server;
|
||||
late final Dio apiClient;
|
||||
Timer? _renewalTimer;
|
||||
|
||||
@override
|
||||
ServerState build() {
|
||||
apiClient = ref.watch(apiClientProvider);
|
||||
server = ActivityRpcServer({});
|
||||
_setupHandlers();
|
||||
ref.onDispose(() {
|
||||
_stopRenewal();
|
||||
server.stop();
|
||||
});
|
||||
return ServerState(status: 'Server not started');
|
||||
}
|
||||
|
||||
void _setupHandlers() {
|
||||
server.updateHandlers({
|
||||
'connection': (socket) {
|
||||
final clientId =
|
||||
socket is _WsSocketWrapper
|
||||
? socket.clientId
|
||||
: (socket as IpcSocketWrapper).clientId;
|
||||
updateStatus('Client connected (ID: $clientId)');
|
||||
socket.send({
|
||||
'cmd': 'DISPATCH',
|
||||
'data': {
|
||||
'v': 1,
|
||||
'config': {
|
||||
'cdn_host': 'fake.cdn',
|
||||
'api_endpoint': '//fake.api',
|
||||
'environment': 'dev',
|
||||
},
|
||||
'user': {
|
||||
'id': 'fake_user_id',
|
||||
'username': 'FakeUser',
|
||||
'discriminator': '0001',
|
||||
'avatar': null,
|
||||
'bot': false,
|
||||
},
|
||||
},
|
||||
'evt': 'READY',
|
||||
'nonce': '12345',
|
||||
});
|
||||
},
|
||||
'message': (socket, dynamic data) async {
|
||||
if (data['cmd'] == 'SET_ACTIVITY') {
|
||||
final activity = data['args']['activity'];
|
||||
final appId = 'rpc:${socket.clientId}';
|
||||
|
||||
final currentId = currentActivityManualId;
|
||||
if (currentId != null && currentId != appId) {
|
||||
talker.info(
|
||||
'Skipped the new SET_ACTIVITY command due to there is one existing...',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
addActivity('Activity: ${activity['details'] ?? 'Untitled'}');
|
||||
// https://discord.com/developers/docs/topics/rpc#setactivity-set-activity-argument-structure
|
||||
final type = switch (activity['type']) {
|
||||
0 => 1, // Discord Playing -> Playing
|
||||
2 => 2, // Discord Music -> Listening
|
||||
3 => 2, // Discord Watching -> Listening
|
||||
_ => 1, // Discord Competing (or null) -> Playing
|
||||
};
|
||||
final title = activity['name'] ?? activity['assets']?['small_text'];
|
||||
final subtitle =
|
||||
activity['details'] ?? activity['assets']?['large_text'];
|
||||
var imageSmall = activity['assets']?['small_image'];
|
||||
var imageLarge = activity['assets']?['large_image'];
|
||||
if (imageSmall != null && !imageSmall!.contains(':')) {
|
||||
imageSmall = 'discord:$imageSmall';
|
||||
}
|
||||
if (imageLarge != null && !imageLarge!.contains(':')) {
|
||||
imageLarge = 'discord:$imageLarge';
|
||||
}
|
||||
try {
|
||||
final activityData = {
|
||||
'type': type,
|
||||
'manual_id': appId,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'caption': activity['state'],
|
||||
'title_url': activity['assets']?['small_text_url'],
|
||||
'subtitle_url': activity['assets']?['large_text_url'],
|
||||
'small_image': imageSmall,
|
||||
'large_image': imageLarge,
|
||||
'meta': activity,
|
||||
'lease_minutes': kPresenceActivityLease,
|
||||
};
|
||||
|
||||
await apiClient.post('/pass/activities', data: activityData);
|
||||
setCurrentActivity(appId, activityData);
|
||||
} catch (e) {
|
||||
talker.log('Failed to set remote activity status: $e');
|
||||
}
|
||||
socket.send({
|
||||
'cmd': 'SET_ACTIVITY',
|
||||
'data': data['args']['activity'],
|
||||
'evt': null,
|
||||
'nonce': data['nonce'],
|
||||
});
|
||||
}
|
||||
},
|
||||
'close': (socket) async {
|
||||
updateStatus('Client disconnected');
|
||||
final currentId = currentActivityManualId;
|
||||
try {
|
||||
await apiClient.delete(
|
||||
'/pass/activities',
|
||||
queryParameters: {'manualId': currentId},
|
||||
);
|
||||
setCurrentActivity(null, null);
|
||||
} catch (e) {
|
||||
talker.log('Failed to unset remote activity status: $e');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
String? get currentActivityManualId => state.currentActivityManualId;
|
||||
|
||||
Future<void> start() async {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) {
|
||||
try {
|
||||
await server.start();
|
||||
state = state.copyWith(status: 'Server running');
|
||||
} catch (e) {
|
||||
state = state.copyWith(status: 'Server failed: $e');
|
||||
}
|
||||
} else {
|
||||
Future(() {
|
||||
state = state.copyWith(status: 'Server disabled on mobile/web');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void updateStatus(String status) {
|
||||
state = state.copyWith(status: status);
|
||||
}
|
||||
|
||||
void addActivity(String activity) {
|
||||
state = state.copyWith(activities: [...state.activities, activity]);
|
||||
}
|
||||
|
||||
void setCurrentActivity(String? id, Map<String, dynamic>? data) {
|
||||
state = state.copyWith(
|
||||
currentActivityManualId: id,
|
||||
currentActivityData: data,
|
||||
);
|
||||
if (id != null && data != null) {
|
||||
_startRenewal();
|
||||
} else {
|
||||
_stopRenewal();
|
||||
}
|
||||
}
|
||||
|
||||
void _startRenewal() {
|
||||
_renewalTimer?.cancel();
|
||||
const int renewalIntervalSeconds = kPresenceActivityLease * 60 - 30;
|
||||
_renewalTimer = Timer.periodic(Duration(seconds: renewalIntervalSeconds), (
|
||||
timer,
|
||||
) {
|
||||
_renewActivity();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopRenewal() {
|
||||
_renewalTimer?.cancel();
|
||||
_renewalTimer = null;
|
||||
}
|
||||
|
||||
Future<void> _renewActivity() async {
|
||||
if (state.currentActivityData != null) {
|
||||
try {
|
||||
await apiClient.post(
|
||||
'/pass/activities',
|
||||
data: state.currentActivityData,
|
||||
);
|
||||
talker.log('Activity lease renewed');
|
||||
} catch (e) {
|
||||
talker.log('Failed to renew activity lease: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kPresenceActivityLease = 5;
|
||||
|
||||
// Providers
|
||||
final rpcServerStateProvider =
|
||||
NotifierProvider<ServerStateNotifier, ServerState>(ServerStateNotifier.new);
|
||||
|
||||
final rpcServerProvider = Provider<ActivityRpcServer>((ref) {
|
||||
final notifier = ref.watch(rpcServerStateProvider.notifier);
|
||||
return notifier.server;
|
||||
});
|
||||
|
||||
@riverpod
|
||||
Future<List<SnPresenceActivity>> presenceActivities(
|
||||
Ref ref,
|
||||
String uname,
|
||||
) async {
|
||||
ref.keepAlive();
|
||||
final timer = Timer.periodic(
|
||||
const Duration(minutes: 1),
|
||||
(_) => ref.invalidateSelf(),
|
||||
);
|
||||
ref.onDispose(() => timer.cancel());
|
||||
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final response = await apiClient.get('/pass/activities/$uname');
|
||||
final data = response.data as List<dynamic>;
|
||||
return data.map((json) => SnPresenceActivity.fromJson(json)).toList();
|
||||
}
|
||||
88
lib/activity/activity_rpc.g.dart
Normal file
88
lib/activity/activity_rpc.g.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'activity_rpc.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(presenceActivities)
|
||||
final presenceActivitiesProvider = PresenceActivitiesFamily._();
|
||||
|
||||
final class PresenceActivitiesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnPresenceActivity>>,
|
||||
List<SnPresenceActivity>,
|
||||
FutureOr<List<SnPresenceActivity>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SnPresenceActivity>>,
|
||||
$FutureProvider<List<SnPresenceActivity>> {
|
||||
PresenceActivitiesProvider._({
|
||||
required PresenceActivitiesFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'presenceActivitiesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$presenceActivitiesHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'presenceActivitiesProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnPresenceActivity>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnPresenceActivity>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return presenceActivities(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PresenceActivitiesProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$presenceActivitiesHash() =>
|
||||
r'3bfaa638eeb961ecd62a32d6a7760a6a7e7bf6f2';
|
||||
|
||||
final class PresenceActivitiesFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<SnPresenceActivity>>, String> {
|
||||
PresenceActivitiesFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'presenceActivitiesProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
PresenceActivitiesProvider call(String uname) =>
|
||||
PresenceActivitiesProvider._(argument: uname, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'presenceActivitiesProvider';
|
||||
}
|
||||
286
lib/activity/ipc_server.dart
Normal file
286
lib/activity/ipc_server.dart
Normal file
@@ -0,0 +1,286 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dart_ipc/dart_ipc.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
// Abstract base class for IPC server
|
||||
abstract class IpcServer {
|
||||
final List<IpcSocketWrapper> _sockets = [];
|
||||
|
||||
// Encode IPC packet
|
||||
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
|
||||
final jsonData = jsonEncode(data);
|
||||
final dataBytes = utf8.encode(jsonData);
|
||||
final dataSize = dataBytes.length;
|
||||
|
||||
final buffer = ByteData(8 + dataSize);
|
||||
buffer.setInt32(0, type, Endian.little);
|
||||
buffer.setInt32(4, dataSize, Endian.little);
|
||||
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
|
||||
|
||||
return buffer.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<void> start();
|
||||
Future<void> stop();
|
||||
|
||||
void addSocket(IpcSocketWrapper socket) {
|
||||
_sockets.add(socket);
|
||||
}
|
||||
|
||||
void removeSocket(IpcSocketWrapper socket) {
|
||||
_sockets.remove(socket);
|
||||
}
|
||||
|
||||
List<IpcSocketWrapper> get sockets => _sockets;
|
||||
|
||||
void Function(
|
||||
IpcSocketWrapper socket,
|
||||
IpcPacket packet,
|
||||
Map<String, Function> handlers,
|
||||
)?
|
||||
handlePacket;
|
||||
|
||||
void Function(IpcSocketWrapper socket)? onSocketClose;
|
||||
}
|
||||
|
||||
// Abstract base class for IPC socket wrapper
|
||||
abstract class IpcSocketWrapper {
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
final List<int> _buffer = [];
|
||||
|
||||
void addData(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> msg);
|
||||
void sendPong(dynamic data);
|
||||
void close();
|
||||
void closeWithCode(int code, [String message = '']);
|
||||
|
||||
List<IpcPacket> readPackets() {
|
||||
final packets = <IpcPacket>[];
|
||||
|
||||
while (_buffer.length >= 8) {
|
||||
final buffer = Uint8List.fromList(_buffer);
|
||||
final byteData = ByteData.view(buffer.buffer);
|
||||
|
||||
final type = byteData.getInt32(0, Endian.little);
|
||||
final dataSize = byteData.getInt32(4, Endian.little);
|
||||
|
||||
if (_buffer.length < 8 + dataSize) break;
|
||||
|
||||
final dataBytes = _buffer.sublist(8, 8 + dataSize);
|
||||
final jsonStr = utf8.decode(dataBytes);
|
||||
final jsonData = jsonDecode(jsonStr);
|
||||
|
||||
packets.add(IpcPacket(type, jsonData));
|
||||
|
||||
_buffer.removeRange(0, 8 + dataSize);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiplatform IPC Server implementation using dart_ipc
|
||||
class MultiPlatformIpcServer extends IpcServer {
|
||||
StreamSubscription? _serverSubscription;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
try {
|
||||
final ipcPath =
|
||||
Platform.isWindows
|
||||
? r'\\.\pipe\discord-ipc-0'
|
||||
: await _findAvailableUnixIpcPath();
|
||||
|
||||
final serverSocket = await bind(ipcPath);
|
||||
talker.log('IPC listening at $ipcPath');
|
||||
|
||||
_serverSubscription = serverSocket.listen((socket) {
|
||||
final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
|
||||
addSocket(socketWrapper);
|
||||
talker.log('New IPC connection!');
|
||||
_handleIpcData(socketWrapper);
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to start IPC server: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
for (var socket in sockets) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (e) {
|
||||
talker.log('Error closing IPC socket: $e');
|
||||
}
|
||||
}
|
||||
sockets.clear();
|
||||
_serverSubscription?.cancel();
|
||||
}
|
||||
|
||||
// Handle incoming IPC data
|
||||
void _handleIpcData(MultiPlatformIpcSocketWrapper socket) {
|
||||
final startTime = DateTime.now();
|
||||
socket.socket.listen(
|
||||
(data) {
|
||||
final readStart = DateTime.now();
|
||||
socket.addData(data);
|
||||
final readDuration =
|
||||
DateTime.now().difference(readStart).inMicroseconds;
|
||||
talker.log('Read data took $readDuration microseconds');
|
||||
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
handlePacket?.call(socket, packet, {});
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
talker.log('IPC connection closed');
|
||||
removeSocket(socket);
|
||||
onSocketClose?.call(socket);
|
||||
socket.close();
|
||||
},
|
||||
onError: (e) {
|
||||
talker.log('IPC data error: $e');
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
},
|
||||
);
|
||||
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
|
||||
talker.log('_handleIpcData took $totalDuration microseconds');
|
||||
}
|
||||
|
||||
Future<String> _getMacOsSystemTmpDir() async {
|
||||
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
// Find available IPC socket path for Unix-like systems
|
||||
Future<String> _findAvailableUnixIpcPath() async {
|
||||
// Build list of directories to try, with macOS-specific handling
|
||||
final baseDirs = <String>[];
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final macTempDir = await _getMacOsSystemTmpDir();
|
||||
if (macTempDir.isNotEmpty) {
|
||||
baseDirs.add(macTempDir);
|
||||
}
|
||||
} catch (e) {
|
||||
talker.log('Failed to get macOS system temp dir: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Add other standard directories
|
||||
final otherDirs = [
|
||||
Platform.environment['XDG_RUNTIME_DIR'],
|
||||
Platform.environment['TMPDIR'],
|
||||
Platform.environment['TMP'],
|
||||
Platform.environment['TEMP'],
|
||||
'/tmp',
|
||||
];
|
||||
|
||||
baseDirs.addAll(
|
||||
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
|
||||
);
|
||||
|
||||
for (final baseDir in baseDirs) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final socketPath = path.join(baseDir, 'discord-ipc-$i');
|
||||
try {
|
||||
final socket = await bind(socketPath);
|
||||
socket.close();
|
||||
try {
|
||||
await File(socketPath).delete();
|
||||
} catch (_) {}
|
||||
talker.log('IPC socket will be created at: $socketPath');
|
||||
return socketPath;
|
||||
} catch (e) {
|
||||
if (i == 0) {
|
||||
talker.log('IPC path $socketPath not available: $e');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception(
|
||||
'No available IPC socket paths found in any temp directory',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Multiplatform IPC Socket Wrapper
|
||||
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {
|
||||
final dynamic socket;
|
||||
|
||||
MultiPlatformIpcSocketWrapper(this.socket);
|
||||
|
||||
@override
|
||||
void send(Map<String, dynamic> msg) {
|
||||
talker.log('IPC sending: $msg');
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendPong(dynamic data) {
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void closeWithCode(int code, [String message = '']) {
|
||||
final closeData = {'code': code, 'message': message};
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
||||
socket.add(packet);
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
61
lib/activity/ipc_server.web.dart
Normal file
61
lib/activity/ipc_server.web.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// Stub implementation for web platform
|
||||
// This file provides empty implementations to avoid import errors on web
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
class IpcServer {
|
||||
Future<void> start() async {}
|
||||
Future<void> stop() async {}
|
||||
void Function(dynamic, dynamic, dynamic)? handlePacket;
|
||||
void addSocket(dynamic socket) {}
|
||||
void removeSocket(dynamic socket) {}
|
||||
List<dynamic> get sockets => [];
|
||||
}
|
||||
|
||||
class IpcSocketWrapper {
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
|
||||
void addData(List<int> data) {}
|
||||
void send(Map<String, dynamic> msg) {}
|
||||
void sendPong(dynamic data) {}
|
||||
void close() {}
|
||||
void closeWithCode(int code, [String message = '']) {}
|
||||
List<dynamic> readPackets() => [];
|
||||
}
|
||||
|
||||
class MultiPlatformIpcServer extends IpcServer {}
|
||||
|
||||
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {}
|
||||
Reference in New Issue
Block a user