Files
umbrix/lib/features/profile/data/profile_repository.dart

415 lines
12 KiB
Dart
Raw Normal View History

2023-09-15 16:41:20 +02:00
import 'dart:io';
2023-07-06 17:18:41 +03:30
import 'package:dio/dio.dart';
2023-11-26 21:20:58 +03:30
import 'package:drift/drift.dart';
2023-07-06 17:18:41 +03:30
import 'package:fpdart/fpdart.dart';
2023-12-01 12:56:24 +03:30
import 'package:hiddify/core/database/app_database.dart';
import 'package:hiddify/core/utils/exception_handler.dart';
2023-11-26 21:20:58 +03:30
import 'package:hiddify/features/profile/data/profile_data_mapper.dart';
import 'package:hiddify/features/profile/data/profile_data_source.dart';
import 'package:hiddify/features/profile/data/profile_parser.dart';
import 'package:hiddify/features/profile/data/profile_path_resolver.dart';
import 'package:hiddify/features/profile/model/profile_entity.dart';
import 'package:hiddify/features/profile/model/profile_failure.dart';
import 'package:hiddify/features/profile/model/profile_sort_enum.dart';
2023-12-01 12:56:24 +03:30
import 'package:hiddify/singbox/service/singbox_service.dart';
2023-11-26 21:20:58 +03:30
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:hiddify/utils/link_parsers.dart';
2023-07-06 17:18:41 +03:30
import 'package:meta/meta.dart';
2023-10-02 23:56:38 +03:30
import 'package:retry/retry.dart';
2023-07-26 14:17:11 +03:30
import 'package:uuid/uuid.dart';
2023-07-06 17:18:41 +03:30
2023-11-26 21:20:58 +03:30
abstract interface class ProfileRepository {
TaskEither<ProfileFailure, Unit> init();
TaskEither<ProfileFailure, ProfileEntity?> getById(String id);
Stream<Either<ProfileFailure, ProfileEntity?>> watchActiveProfile();
Stream<Either<ProfileFailure, bool>> watchHasAnyProfile();
Stream<Either<ProfileFailure, List<ProfileEntity>>> watchAll({
ProfilesSort sort = ProfilesSort.lastUpdate,
SortMode sortMode = SortMode.ascending,
});
TaskEither<ProfileFailure, Unit> addByUrl(
String url, {
bool markAsActive = false,
});
TaskEither<ProfileFailure, Unit> addByContent(
String content, {
required String name,
bool markAsActive = false,
});
TaskEither<ProfileFailure, Unit> add(RemoteProfileEntity baseProfile);
2023-12-01 12:56:24 +03:30
TaskEither<ProfileFailure, String> generateConfig(String id);
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, Unit> updateSubscription(
RemoteProfileEntity baseProfile,
);
TaskEither<ProfileFailure, Unit> patch(ProfileEntity profile);
TaskEither<ProfileFailure, Unit> setAsActive(String id);
TaskEither<ProfileFailure, Unit> deleteById(String id);
}
class ProfileRepositoryImpl
2023-07-06 17:18:41 +03:30
with ExceptionHandler, InfraLogger
2023-11-26 21:20:58 +03:30
implements ProfileRepository {
ProfileRepositoryImpl({
required this.profileDataSource,
required this.profilePathResolver,
2023-12-01 12:56:24 +03:30
required this.singbox,
2023-07-06 17:18:41 +03:30
required this.dio,
});
2023-11-26 21:20:58 +03:30
final ProfileDataSource profileDataSource;
final ProfilePathResolver profilePathResolver;
2023-12-01 12:56:24 +03:30
final SingboxService singbox;
2023-07-06 17:18:41 +03:30
final Dio dio;
@override
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, Unit> init() {
return exceptionHandler(
() async {
if (!await profilePathResolver.directory.exists()) {
await profilePathResolver.directory.create(recursive: true);
}
return right(unit);
},
ProfileUnexpectedFailure.new,
);
}
@override
TaskEither<ProfileFailure, ProfileEntity?> getById(String id) {
2023-07-06 17:18:41 +03:30
return TaskEither.tryCatch(
2023-11-26 21:20:58 +03:30
() => profileDataSource.getById(id).then((value) => value?.toEntity()),
2023-07-06 17:18:41 +03:30
ProfileUnexpectedFailure.new,
);
}
@override
2023-11-26 21:20:58 +03:30
Stream<Either<ProfileFailure, ProfileEntity?>> watchActiveProfile() {
return profileDataSource
.watchActiveProfile()
.map((event) => event?.toEntity())
.handleExceptions(
2023-08-23 00:06:51 +03:30
(error, stackTrace) {
2023-10-05 22:47:24 +03:30
loggy.error("error watching active profile", error, stackTrace);
2023-08-23 00:06:51 +03:30
return ProfileUnexpectedFailure(error, stackTrace);
},
);
2023-07-06 17:18:41 +03:30
}
@override
Stream<Either<ProfileFailure, bool>> watchHasAnyProfile() {
2023-11-26 21:20:58 +03:30
return profileDataSource
.watchProfilesCount()
2023-07-06 17:18:41 +03:30
.map((event) => event != 0)
.handleExceptions(ProfileUnexpectedFailure.new);
}
@override
2023-11-26 21:20:58 +03:30
Stream<Either<ProfileFailure, List<ProfileEntity>>> watchAll({
2023-07-26 16:42:31 +03:30
ProfilesSort sort = ProfilesSort.lastUpdate,
2023-11-26 21:20:58 +03:30
SortMode sortMode = SortMode.ascending,
2023-07-26 16:42:31 +03:30
}) {
2023-11-26 21:20:58 +03:30
return profileDataSource
.watchAll(sort: sort, sortMode: sortMode)
.map((event) => event.map((e) => e.toEntity()).toList())
2023-07-06 17:18:41 +03:30
.handleExceptions(ProfileUnexpectedFailure.new);
}
2023-07-26 14:17:11 +03:30
@override
TaskEither<ProfileFailure, Unit> addByUrl(
String url, {
bool markAsActive = false,
}) {
return exceptionHandler(
() async {
2023-11-26 21:20:58 +03:30
final existingProfile = await profileDataSource
.getByUrl(url)
.then((value) => value?.toEntity());
if (existingProfile case RemoteProfileEntity()) {
2023-10-03 21:12:14 +03:30
loggy.info("profile with same url already exists, updating");
final baseProfile = markAsActive
? existingProfile.copyWith(active: true)
: existingProfile;
2023-11-26 21:20:58 +03:30
return updateSubscription(baseProfile).run();
}
2023-07-26 14:17:11 +03:30
final profileId = const Uuid().v4();
return fetch(url, profileId)
.flatMap(
(profile) => TaskEither(
() async {
2023-11-26 21:20:58 +03:30
await profileDataSource.insert(
profile
.copyWith(id: profileId, active: markAsActive)
.toEntry(),
2023-07-26 14:17:11 +03:30
);
return right(unit);
},
),
)
.run();
},
2023-08-22 01:02:33 +03:30
(error, stackTrace) {
loggy.warning("error adding profile by url", error, stackTrace);
return ProfileUnexpectedFailure(error, stackTrace);
},
2023-07-26 14:17:11 +03:30
);
}
2023-12-01 12:56:24 +03:30
@visibleForTesting
TaskEither<ProfileFailure, Unit> validateConfig(
String path,
String tempPath,
bool debug,
) {
return exceptionHandler(
() {
return singbox
.validateConfigByPath(path, tempPath, debug)
.mapLeft(ProfileFailure.invalidConfig)
.run();
},
ProfileUnexpectedFailure.new,
);
}
2023-07-06 17:18:41 +03:30
@override
2023-10-02 18:51:14 +03:30
TaskEither<ProfileFailure, Unit> addByContent(
String content, {
required String name,
bool markAsActive = false,
}) {
return exceptionHandler(
() async {
final profileId = const Uuid().v4();
2023-11-26 21:20:58 +03:30
final file = profilePathResolver.file(profileId);
final tempFile = profilePathResolver.tempFile(profileId);
2023-10-02 18:51:14 +03:30
try {
2023-11-26 21:20:58 +03:30
await tempFile.writeAsString(content);
2023-12-01 12:56:24 +03:30
return await validateConfig(file.path, tempFile.path, false)
.andThen(
() => TaskEither(() async {
final profile = LocalProfileEntity(
id: profileId,
active: markAsActive,
name: name,
lastUpdate: DateTime.now(),
);
await profileDataSource.insert(profile.toEntry());
return right(unit);
}),
)
.run();
2023-10-02 18:51:14 +03:30
} finally {
2023-11-26 21:20:58 +03:30
if (tempFile.existsSync()) tempFile.deleteSync();
2023-10-02 18:51:14 +03:30
}
},
(error, stackTrace) {
loggy.warning("error adding profile by content", error, stackTrace);
return ProfileUnexpectedFailure(error, stackTrace);
},
);
}
@override
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, Unit> add(RemoteProfileEntity baseProfile) {
2023-07-06 17:18:41 +03:30
return exceptionHandler(
() async {
return fetch(baseProfile.url, baseProfile.id)
.flatMap(
2023-07-26 14:17:11 +03:30
(remoteProfile) => TaskEither(() async {
2023-11-26 21:20:58 +03:30
await profileDataSource.insert(
baseProfile
.copyWith(
subInfo: remoteProfile.subInfo,
lastUpdate: DateTime.now(),
)
.toEntry(),
2023-07-06 17:18:41 +03:30
);
return right(unit);
}),
)
.run();
},
2023-08-22 01:02:33 +03:30
(error, stackTrace) {
loggy.warning("error adding profile", error, stackTrace);
return ProfileUnexpectedFailure(error, stackTrace);
},
2023-07-06 17:18:41 +03:30
);
}
2023-12-01 12:56:24 +03:30
@override
TaskEither<ProfileFailure, String> generateConfig(String id) {
return TaskEither<ProfileFailure, String>.Do(
($) async {
final configFile = profilePathResolver.file(id);
// TODO pass options
return await $(
singbox
.generateFullConfigByPath(configFile.path)
.mapLeft(ProfileFailure.unexpected),
);
},
).handleExceptions(ProfileFailure.unexpected);
}
2023-07-06 17:18:41 +03:30
@override
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, Unit> updateSubscription(
RemoteProfileEntity baseProfile,
) {
2023-07-06 17:18:41 +03:30
return exceptionHandler(
() async {
2023-08-22 01:02:33 +03:30
loggy.debug(
"updating profile [${baseProfile.name} (${baseProfile.id})]",
);
2023-07-06 17:18:41 +03:30
return fetch(baseProfile.url, baseProfile.id)
.flatMap(
2023-07-26 14:17:11 +03:30
(remoteProfile) => TaskEither(() async {
2023-11-26 21:20:58 +03:30
await profileDataSource.edit(
baseProfile.id,
remoteProfile
.subInfoPatch()
.copyWith(lastUpdate: Value(DateTime.now())),
2023-07-06 17:18:41 +03:30
);
return right(unit);
}),
)
.run();
},
2023-08-22 01:02:33 +03:30
(error, stackTrace) {
loggy.warning("error updating profile", error, stackTrace);
return ProfileUnexpectedFailure(error, stackTrace);
},
2023-07-06 17:18:41 +03:30
);
}
2023-09-28 14:03:45 +03:30
@override
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, Unit> patch(ProfileEntity profile) {
2023-09-28 14:03:45 +03:30
return exceptionHandler(
() async {
loggy.debug(
"editing profile [${profile.name} (${profile.id})]",
);
2023-11-26 21:20:58 +03:30
await profileDataSource.edit(profile.id, profile.toEntry());
2023-09-28 14:03:45 +03:30
return right(unit);
},
(error, stackTrace) {
loggy.warning("error editing profile", error, stackTrace);
return ProfileUnexpectedFailure(error, stackTrace);
},
);
}
2023-07-06 17:18:41 +03:30
@override
TaskEither<ProfileFailure, Unit> setAsActive(String id) {
return TaskEither.tryCatch(
() async {
2023-11-26 21:20:58 +03:30
await profileDataSource.edit(
id,
const ProfileEntriesCompanion(active: Value(true)),
);
2023-07-06 17:18:41 +03:30
return unit;
},
ProfileUnexpectedFailure.new,
);
}
@override
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, Unit> deleteById(String id) {
2023-07-06 17:18:41 +03:30
return TaskEither.tryCatch(
() async {
2023-11-26 21:20:58 +03:30
await profileDataSource.deleteById(id);
await profilePathResolver.file(id).delete();
2023-07-06 17:18:41 +03:30
return unit;
},
ProfileUnexpectedFailure.new,
);
}
2023-09-16 00:30:21 +03:30
final _subInfoHeaders = [
'profile-title',
'content-disposition',
'subscription-userinfo',
'profile-update-interval',
'support-url',
'profile-web-page-url',
];
2023-07-06 17:18:41 +03:30
@visibleForTesting
2023-11-26 21:20:58 +03:30
TaskEither<ProfileFailure, RemoteProfileEntity> fetch(
2023-07-06 17:18:41 +03:30
String url,
String fileName,
) {
return TaskEither(
() async {
2023-11-26 21:20:58 +03:30
final file = profilePathResolver.file(fileName);
final tempFile = profilePathResolver.tempFile(fileName);
2023-09-22 23:52:20 +03:30
try {
2023-10-02 23:56:38 +03:30
final response = await retry(
2023-11-26 21:20:58 +03:30
() async => dio.download(url.trim(), tempFile.path),
2023-10-02 23:56:38 +03:30
maxAttempts: 3,
);
2023-09-22 23:52:20 +03:30
final headers =
2023-11-26 21:20:58 +03:30
await _populateHeaders(response.headers.map, tempFile.path);
2023-12-01 12:56:24 +03:30
return await validateConfig(file.path, tempFile.path, false)
.andThen(
() => TaskEither(() async {
final profile = ProfileParser.parse(url, headers);
return right(profile);
}),
)
.run();
2023-09-22 23:52:20 +03:30
} finally {
2023-11-26 21:20:58 +03:30
if (tempFile.existsSync()) tempFile.deleteSync();
2023-09-22 23:52:20 +03:30
}
2023-07-06 17:18:41 +03:30
},
);
}
2023-09-16 00:30:21 +03:30
Future<Map<String, List<String>>> _populateHeaders(
Map<String, List<String>> headers,
2023-09-16 00:30:21 +03:30
String path,
) async {
var headersFound = 0;
for (final key in _subInfoHeaders) {
if (headers.containsKey(key)) headersFound++;
}
if (headersFound >= 4) return headers;
loggy.debug(
"only [$headersFound] headers found, checking file content for possible information",
);
var content = await File(path).readAsString();
content = safeDecodeBase64(content);
final lines = content.split("\n");
2023-09-16 08:28:53 +02:00
final linesToProcess = lines.length < 10 ? lines.length : 10;
for (int i = 0; i < linesToProcess; i++) {
2023-09-16 00:30:21 +03:30
final line = lines[i];
if (line.startsWith("#") || line.startsWith("//")) {
final index = line.indexOf(':');
if (index == -1) continue;
2023-09-16 00:30:21 +03:30
final key = line
.substring(0, index)
.replaceFirst(RegExp("^#|//"), "")
.trim()
.toLowerCase();
2023-09-16 00:30:21 +03:30
final value = line.substring(index + 1).trim();
if (!headers.keys.contains(key) &&
_subInfoHeaders.contains(key) &&
value.isNotEmpty) {
headers[key] = [value];
}
}
}
return headers;
}
2023-07-06 17:18:41 +03:30
}