Migrate to singbox

This commit is contained in:
problematicconsumer
2023-08-19 22:27:23 +03:30
parent 14369d0a03
commit 684acc555d
124 changed files with 3408 additions and 2047 deletions

145
lib/data/api/clash_api.dart Normal file
View File

@@ -0,0 +1,145 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class ClashApi with InfraLogger {
ClashApi(int port) : address = "${Constants.localHost}:$port";
final String address;
late final _clashDio = Dio(
BaseOptions(
baseUrl: "http://$address",
connectTimeout: const Duration(seconds: 3),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 3),
),
);
TaskEither<String, List<ClashProxy>> getProxies() {
return TaskEither(
() async {
final response = await _clashDio.get("/proxies");
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
final proxies = (jsonDecode(response.data! as String)["proxies"]
as Map<String, dynamic>)
.entries
.map(
(e) {
final proxyMap = (e.value as Map<String, dynamic>)
..putIfAbsent('name', () => e.key);
return ClashProxy.fromJson(proxyMap);
},
);
return right(proxies.toList());
},
);
}
TaskEither<String, Unit> changeProxy(String selectorName, String proxyName) {
return TaskEither(
() async {
final response = await _clashDio.put(
"/proxies/$selectorName",
data: {"name": proxyName},
);
if (response.statusCode != HttpStatus.noContent) {
return left(response.statusMessage ?? "");
}
return right(unit);
},
);
}
TaskEither<String, int> getProxyDelay(
String name,
String url, {
Duration timeout = const Duration(seconds: 10),
}) {
return TaskEither(
() async {
final response = await _clashDio.get<Map>(
"/proxies/$name/delay",
queryParameters: {
"timeout": timeout.inMilliseconds,
"url": url,
},
);
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
return right(response.data!["delay"] as int);
},
);
}
TaskEither<String, ClashConfig> getConfigs() {
return TaskEither(
() async {
final response = await _clashDio.get("/configs");
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
final config =
ClashConfig.fromJson(response.data as Map<String, dynamic>);
return right(config);
},
);
}
TaskEither<String, Unit> updateConfigs(String path) {
return TaskEither.of(unit);
}
TaskEither<String, Unit> patchConfigs(ClashConfig config) {
return TaskEither(
() async {
final response = await _clashDio.patch(
"/configs",
data: config.toJson(),
);
if (response.statusCode != HttpStatus.noContent) {
return left(response.statusMessage ?? "");
}
return right(unit);
},
);
}
Stream<ClashLog> watchLogs(LogLevel level) {
return const Stream.empty();
}
Stream<ClashTraffic> watchTraffic() {
final channel = WebSocketChannel.connect(
Uri.parse("ws://$address/traffic"),
);
return channel.stream.map(
(event) {
return ClashTraffic.fromJson(
jsonDecode(event as String) as Map<String, dynamic>,
);
},
);
}
TaskEither<String, ClashTraffic> getTraffic() {
return TaskEither(
() async {
final response = await _clashDio.get<Map<String, dynamic>>("/traffic");
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
return right(ClashTraffic.fromJson(response.data!));
},
);
}
}

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:hiddify/data/api/clash_api.dart';
import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/local/database.dart';
import 'package:hiddify/data/repository/repository.dart';
import 'package:hiddify/data/repository/update_repository_impl.dart';
import 'package:hiddify/domain/app/app.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -28,21 +30,26 @@ ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao(
ref.watch(appDatabaseProvider),
);
@Riverpod(keepAlive: true)
ClashFacade clashFacade(ClashFacadeRef ref) => ClashFacadeImpl(
clashService: ref.watch(clashServiceProvider),
filesEditor: ref.watch(filesEditorServiceProvider),
);
@Riverpod(keepAlive: true)
ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) =>
ProfilesRepositoryImpl(
profilesDao: ref.watch(profilesDaoProvider),
filesEditor: ref.watch(filesEditorServiceProvider),
clashFacade: ref.watch(clashFacadeProvider),
singbox: ref.watch(coreFacadeProvider),
dio: ref.watch(dioProvider),
);
@Riverpod(keepAlive: true)
UpdateRepository updateRepository(UpdateRepositoryRef ref) =>
UpdateRepositoryImpl(ref.watch(dioProvider));
@Riverpod(keepAlive: true)
ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort);
@Riverpod(keepAlive: true)
CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
ref.watch(singboxServiceProvider),
ref.watch(filesEditorServiceProvider),
ref.watch(clashApiProvider),
ref.watch(connectivityServiceProvider),
);

View File

@@ -5,8 +5,8 @@ import 'package:drift/native.dart';
import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/local/tables.dart';
import 'package:hiddify/data/local/type_converters.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
part 'database.g.dart';
@@ -22,8 +22,8 @@ class AppDatabase extends _$AppDatabase {
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
final dbDir = await FilesEditorService.getDatabaseDirectory();
final file = File(p.join(dbDir.path, 'db.sqlite'));
return NativeDatabase.createInBackground(file);
});
}

View File

@@ -1,116 +0,0 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/services/clash/clash.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/utils/utils.dart';
class ClashFacadeImpl
with ExceptionHandler, InfraLogger
implements ClashFacade {
ClashFacadeImpl({
required ClashService clashService,
required FilesEditorService filesEditor,
}) : _clash = clashService,
_filesEditor = filesEditor;
final ClashService _clash;
final FilesEditorService _filesEditor;
@override
TaskEither<ClashFailure, ClashConfig> getConfigs() {
return exceptionHandler(
() async => _clash.getConfigs().mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, bool> validateConfig(String configFileName) {
return exceptionHandler(
() async {
final path = _filesEditor.configPath(configFileName);
return _clash.validateConfig(path).mapLeft(ClashFailure.core).run();
},
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, Unit> changeConfigs(String configFileName) {
return exceptionHandler(
() async {
loggy.debug("changing config, file name: [$configFileName]");
final path = _filesEditor.configPath(configFileName);
return _clash.updateConfigs(path).mapLeft(ClashFailure.core).run();
},
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, Unit> patchOverrides(ClashConfig overrides) {
return exceptionHandler(
() async =>
_clash.patchConfigs(overrides).mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, List<ClashProxy>> getProxies() {
return exceptionHandler(
() async => _clash.getProxies().mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, Unit> changeProxy(
String selectorName,
String proxyName,
) {
return exceptionHandler(
() async => _clash
.changeProxy(selectorName, proxyName)
.mapLeft(ClashFailure.core)
.run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, ClashTraffic> getTraffic() {
return exceptionHandler(
() async => _clash.getTraffic().mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, int> testDelay(
String proxyName, {
String testUrl = Constants.delayTestUrl,
}) {
return exceptionHandler(
() async {
final result = _clash
.getProxyDelay(proxyName, testUrl)
.mapLeft(ClashFailure.core)
.run();
return result;
},
ClashFailure.unexpected,
);
}
@override
Stream<Either<ClashFailure, ClashLog>> watchLogs() {
return _clash
.watchLogs(LogLevel.info)
.handleExceptions(ClashFailure.unexpected);
}
}

View File

@@ -0,0 +1,187 @@
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/api/clash_api.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connection_status.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/services/connectivity/connectivity.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/services/singbox/singbox_service.dart';
import 'package:hiddify/utils/utils.dart';
class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
CoreFacadeImpl(this.singbox, this.filesEditor, this.clash, this.connectivity);
final SingboxService singbox;
final FilesEditorService filesEditor;
final ClashApi clash;
final ConnectivityService connectivity;
bool _initialized = false;
@override
TaskEither<CoreServiceFailure, Unit> setup() {
if (_initialized) return TaskEither.of(unit);
return exceptionHandler(
() {
loggy.debug("setting up singbox");
return singbox
.setup(
filesEditor.baseDir.path,
filesEditor.workingDir.path,
filesEditor.tempDir.path,
)
.map((r) {
loggy.debug("setup complete");
_initialized = true;
return r;
})
.mapLeft(CoreServiceFailure.other)
.run();
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> parseConfig(String path) {
return exceptionHandler(
() {
return singbox
.parseConfig(path)
.mapLeft(CoreServiceFailure.invalidConfig)
.run();
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> changeConfig(String fileName) {
return exceptionHandler(
() {
final configPath = filesEditor.configPath(fileName);
loggy.debug("changing config to: $configPath");
return setup()
.andThen(
() =>
singbox.create(configPath).mapLeft(CoreServiceFailure.create),
)
.run();
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> start() {
return exceptionHandler(
() => singbox.start().mapLeft(CoreServiceFailure.start).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> stop() {
return exceptionHandler(
() => singbox.stop().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
Stream<Either<CoreServiceFailure, String>> watchLogs() {
return singbox
.watchLogs(filesEditor.logsPath)
.handleExceptions(CoreServiceFailure.unexpected);
}
@override
TaskEither<CoreServiceFailure, ClashConfig> getConfigs() {
return exceptionHandler(
() async => clash.getConfigs().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> patchOverrides(ClashConfig overrides) {
return exceptionHandler(
() async =>
clash.patchConfigs(overrides).mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, List<ClashProxy>> getProxies() {
return exceptionHandler(
() async => clash.getProxies().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> changeProxy(
String selectorName,
String proxyName,
) {
return exceptionHandler(
() async => clash
.changeProxy(selectorName, proxyName)
.mapLeft(CoreServiceFailure.other)
.run(),
CoreServiceFailure.unexpected,
);
}
@override
Stream<Either<CoreServiceFailure, ClashTraffic>> watchTraffic() {
return clash.watchTraffic().handleExceptions(CoreServiceFailure.unexpected);
}
@override
TaskEither<CoreServiceFailure, int> testDelay(
String proxyName, {
String testUrl = Defaults.delayTestUrl,
}) {
return exceptionHandler(
() async {
final result = clash
.getProxyDelay(proxyName, testUrl)
.mapLeft(CoreServiceFailure.other)
.run();
return result;
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> connect() {
return exceptionHandler(
() async {
await connectivity.connect();
return right(unit);
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> disconnect() {
return exceptionHandler(
() async {
await connectivity.disconnect();
return right(unit);
},
CoreServiceFailure.unexpected,
);
}
@override
Stream<ConnectionStatus> watchConnectionStatus() =>
connectivity.watchConnectionStatus();
}

View File

@@ -4,9 +4,9 @@ import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/enums.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:meta/meta.dart';
@@ -18,13 +18,13 @@ class ProfilesRepositoryImpl
ProfilesRepositoryImpl({
required this.profilesDao,
required this.filesEditor,
required this.clashFacade,
required this.singbox,
required this.dio,
});
final ProfilesDao profilesDao;
final FilesEditorService filesEditor;
final ClashFacade clashFacade;
final SingboxFacade singbox;
final Dio dio;
@override
@@ -166,20 +166,17 @@ class ProfilesRepositoryImpl
() async {
final path = filesEditor.configPath(fileName);
final response = await dio.download(url, path);
if (response.statusCode != 200) {
await File(path).delete();
return left(const ProfileUnexpectedFailure());
}
final isValid = await clashFacade
.validateConfig(fileName)
.getOrElse((_) => false)
.run();
if (!isValid) {
await File(path).delete();
return left(const ProfileFailure.invalidConfig());
}
final profile = Profile.fromResponse(url, response.headers.map);
return right(profile);
final parseResult = await singbox.parseConfig(path).run();
return parseResult.fold(
(l) async {
await File(path).delete();
return left(ProfileFailure.invalidConfig(l.msg));
},
(_) {
final profile = Profile.fromResponse(url, response.headers.map);
return right(profile);
},
);
},
);
}

View File

@@ -1,2 +1,2 @@
export 'clash_facade_impl.dart';
export 'core_facade_impl.dart';
export 'profiles_repository_impl.dart';