import 'dart:io'; import 'dart:convert'; 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/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'; import 'package:uuid/uuid.dart'; class ProfilesRepositoryImpl with ExceptionHandler, InfraLogger implements ProfilesRepository { ProfilesRepositoryImpl({ required this.profilesDao, required this.filesEditor, required this.singbox, required this.dio, }); final ProfilesDao profilesDao; final FilesEditorService filesEditor; final SingboxFacade singbox; final Dio dio; @override TaskEither get(String id) { return TaskEither.tryCatch( () => profilesDao.getById(id), ProfileUnexpectedFailure.new, ); } @override Stream> watchActiveProfile() { return profilesDao.watchActiveProfile().handleExceptions( (error, stackTrace) { loggy.warning("error watching active profile", error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace); }, ); } @override Stream> watchHasAnyProfile() { return profilesDao .watchProfileCount() .map((event) => event != 0) .handleExceptions(ProfileUnexpectedFailure.new); } @override Stream>> watchAll({ ProfilesSort sort = ProfilesSort.lastUpdate, SortMode mode = SortMode.ascending, }) { return profilesDao .watchAll(sort: sort, mode: mode) .handleExceptions(ProfileUnexpectedFailure.new); } @override TaskEither addByUrl( String url, { bool markAsActive = false, }) { return exceptionHandler( () async { final profileId = const Uuid().v4(); return fetch(url, profileId) .flatMap( (profile) => TaskEither( () async { await profilesDao.create( profile.copyWith( id: profileId, active: markAsActive, ), ); return right(unit); }, ), ) .run(); }, (error, stackTrace) { loggy.warning("error adding profile by url", error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace); }, ); } @override TaskEither add(Profile baseProfile) { return exceptionHandler( () async { return fetch(baseProfile.url, baseProfile.id) .flatMap( (remoteProfile) => TaskEither(() async { await profilesDao.create( baseProfile.copyWith( subInfo: remoteProfile.subInfo, extra: remoteProfile.extra, lastUpdate: DateTime.now(), ), ); return right(unit); }), ) .run(); }, (error, stackTrace) { loggy.warning("error adding profile", error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace); }, ); } @override TaskEither update(Profile baseProfile) { return exceptionHandler( () async { loggy.debug( "updating profile [${baseProfile.name} (${baseProfile.id})]", ); return fetch(baseProfile.url, baseProfile.id) .flatMap( (remoteProfile) => TaskEither(() async { await profilesDao.edit( baseProfile.copyWith( subInfo: remoteProfile.subInfo, extra: remoteProfile.extra, lastUpdate: DateTime.now(), ), ); return right(unit); }), ) .run(); }, (error, stackTrace) { loggy.warning("error updating profile", error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace); }, ); } @override TaskEither setAsActive(String id) { return TaskEither.tryCatch( () async { await profilesDao.setAsActive(id); return unit; }, ProfileUnexpectedFailure.new, ); } @override TaskEither delete(String id) { return TaskEither.tryCatch( () async { await profilesDao.removeById(id); await filesEditor.deleteConfig(id); return unit; }, ProfileUnexpectedFailure.new, ); } @visibleForTesting TaskEither fetch( String url, String fileName, ) { return TaskEither( () async { final path = filesEditor.configPath(fileName); final response = await dio.download(url.trim(), path); final content = await File(path).readAsString(); final headers = addHeadersFromBody(response.headers.map, content); final parseResult = await singbox.parseConfig(path).run(); return parseResult.fold( (l) async { await File(path).delete(); loggy.warning("error parsing config: $l"); return left(ProfileFailure.invalidConfig(l.msg)); }, (_) async { final profile = Profile.fromResponse(url, headers); return right(profile); }, ); }, ); } Map> addHeadersFromBody( Map> headers, String responseString, ) { final content = safeDecodeBase64(responseString); final allowedHeaders = [ 'profile-title', 'content-disposition', 'subscription-userinfo', 'profile-update-interval', 'support-url', 'profile-web-page-url', ]; for (final text in content.split("\n")) { if (text.startsWith("#") || text.startsWith("//")) { final index = text.indexOf(':'); if (index == -1) continue; final headerTitle = text .substring(0, index) .replaceFirst(RegExp("^#|//"), "") .trim() .toLowerCase(); final headerValue = text.substring(index + 1).trim(); if (!headers.keys.contains(headerTitle) && allowedHeaders.contains(headerTitle) && headerValue.isNotEmpty) { headers[headerTitle] = [headerValue]; } } } return headers; } String safeDecodeBase64(String str) { try { return utf8.decode(base64.decode(str)); } catch (e) { return str; } } }