new: add custom url-test for each repository

This commit is contained in:
hiddify-com
2024-07-29 13:11:51 +02:00
parent ccbc416c49
commit b9afe586bf
14 changed files with 417 additions and 157 deletions

View File

@@ -21,7 +21,7 @@ class AppDatabase extends _$AppDatabase with InfraLogger {
AppDatabase.connect() : super(openConnection());
@override
int get schemaVersion => 3;
int get schemaVersion => 4;
@override
MigrationStrategy get migration {
@@ -48,6 +48,9 @@ class AppDatabase extends _$AppDatabase with InfraLogger {
await m.createTable(schema.geoAssetEntries);
await _prePopulateGeoAssets();
},
from3To4: (m, schema) async {
await m.addColumn(profileEntries, profileEntries.testUrl);
},
),
beforeOpen: (details) async {
if (kDebugMode) {

View File

@@ -39,78 +39,38 @@ final class _S2 extends i0.VersionedSchema {
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get active =>
columnsByName['active']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get url =>
columnsByName['url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get lastUpdate =>
columnsByName['last_update']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get updateInterval =>
columnsByName['update_interval']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get upload =>
columnsByName['upload']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get download =>
columnsByName['download']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get total =>
columnsByName['total']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get expire =>
columnsByName['expire']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get webPageUrl =>
columnsByName['web_page_url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get supportUrl =>
columnsByName['support_url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get id => columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type => columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get active => columnsByName['active']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get name => columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get url => columnsByName['url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get lastUpdate => columnsByName['last_update']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get updateInterval => columnsByName['update_interval']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get upload => columnsByName['upload']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get download => columnsByName['download']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get total => columnsByName['total']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get expire => columnsByName['expire']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get webPageUrl => columnsByName['web_page_url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get supportUrl => columnsByName['support_url']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
i1.GeneratedColumn<String>('id', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('type', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_2(String aliasedName) =>
i1.GeneratedColumn<bool>('active', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("active" IN (0, 1))'));
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
additionalChecks: i1.GeneratedColumn.checkTextLength(
minTextLength: 1,
),
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
i1.GeneratedColumn<String>('url', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
i1.GeneratedColumn<DateTime>('last_update', aliasedName, false,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
i1.GeneratedColumn<int>('update_interval', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
i1.GeneratedColumn<int>('upload', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_8(String aliasedName) =>
i1.GeneratedColumn<int>('download', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_9(String aliasedName) =>
i1.GeneratedColumn<int>('total', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
i1.GeneratedColumn<DateTime>('expire', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_11(String aliasedName) =>
i1.GeneratedColumn<String>('web_page_url', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
i1.GeneratedColumn<String>('support_url', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_0(String aliasedName) => i1.GeneratedColumn<String>('id', aliasedName, false, type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_1(String aliasedName) => i1.GeneratedColumn<String>('type', aliasedName, false, type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_2(String aliasedName) => i1.GeneratedColumn<bool>('active', aliasedName, false, type: i1.DriftSqlType.bool, defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))'));
i1.GeneratedColumn<String> _column_3(String aliasedName) => i1.GeneratedColumn<String>('name', aliasedName, false,
additionalChecks: i1.GeneratedColumn.checkTextLength(
minTextLength: 1,
),
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_4(String aliasedName) => i1.GeneratedColumn<String>('url', aliasedName, true, type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) => i1.GeneratedColumn<DateTime>('last_update', aliasedName, false, type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<int> _column_6(String aliasedName) => i1.GeneratedColumn<int>('update_interval', aliasedName, true, type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_7(String aliasedName) => i1.GeneratedColumn<int>('upload', aliasedName, true, type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_8(String aliasedName) => i1.GeneratedColumn<int>('download', aliasedName, true, type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_9(String aliasedName) => i1.GeneratedColumn<int>('total', aliasedName, true, type: i1.DriftSqlType.int);
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) => i1.GeneratedColumn<DateTime>('expire', aliasedName, true, type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_11(String aliasedName) => i1.GeneratedColumn<String>('web_page_url', aliasedName, true, type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_12(String aliasedName) => i1.GeneratedColumn<String>('support_url', aliasedName, true, type: i1.DriftSqlType.string);
final class _S3 extends i0.VersionedSchema {
_S3({required super.database}) : super(version: 3);
@@ -170,37 +130,26 @@ final class _S3 extends i0.VersionedSchema {
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get active =>
columnsByName['active']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get providerName =>
columnsByName['provider_name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get version =>
columnsByName['version']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get lastCheck =>
columnsByName['last_check']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get id => columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type => columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get active => columnsByName['active']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get name => columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get providerName => columnsByName['provider_name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get version => columnsByName['version']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get lastCheck => columnsByName['last_check']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
i1.GeneratedColumn<String>('provider_name', aliasedName, false,
additionalChecks: i1.GeneratedColumn.checkTextLength(
minTextLength: 1,
),
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
i1.GeneratedColumn<String>('version', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_15(String aliasedName) =>
i1.GeneratedColumn<DateTime>('last_check', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_13(String aliasedName) => i1.GeneratedColumn<String>('provider_name', aliasedName, false,
additionalChecks: i1.GeneratedColumn.checkTextLength(
minTextLength: 1,
),
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_14(String aliasedName) => i1.GeneratedColumn<String>('version', aliasedName, true, type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_15(String aliasedName) => i1.GeneratedColumn<DateTime>('last_check', aliasedName, true, type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
required Future<void> Function(i1.Migrator m, _S3 schema) from3To4,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -214,6 +163,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema);
return 3;
case 3:
final schema = _S3(database: database);
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -223,9 +177,11 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
required Future<void> Function(i1.Migrator m, _S3 schema) from3To4,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
from3To4: from3To4,
));

View File

@@ -0,0 +1,296 @@
{
"_meta": {
"description": "This file contains a serialized version of schema entities for drift.",
"version": "1.1.0"
},
"options": {
"store_date_time_values_as_text": true
},
"entities": [
{
"id": 0,
"references": [],
"type": "table",
"data": {
"name": "profile_entries",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"getter_name": "id",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "type",
"getter_name": "type",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "const EnumNameConverter<ProfileType>(ProfileType.values)",
"dart_type_name": "ProfileType"
}
},
{
"name": "active",
"getter_name": "active",
"moor_type": "bool",
"nullable": false,
"customConstraints": null,
"defaultConstraints": "CHECK (\"active\" IN (0, 1))",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "name",
"getter_name": "name",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
{
"allowed-lengths": {
"min": 1,
"max": null
}
}
]
},
{
"name": "url",
"getter_name": "url",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_update",
"getter_name": "lastUpdate",
"moor_type": "dateTime",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "update_interval",
"getter_name": "updateInterval",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "DurationTypeConverter()",
"dart_type_name": "Duration"
}
},
{
"name": "upload",
"getter_name": "upload",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "download",
"getter_name": "download",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "total",
"getter_name": "total",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "expire",
"getter_name": "expire",
"moor_type": "dateTime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "web_page_url",
"getter_name": "webPageUrl",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "support_url",
"getter_name": "supportUrl",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "test_url",
"getter_name": "testUrl",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"without_rowid": false,
"constraints": [],
"explicit_pk": [
"id"
]
}
},
{
"id": 1,
"references": [],
"type": "table",
"data": {
"name": "geo_asset_entries",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"getter_name": "id",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "type",
"getter_name": "type",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "const EnumNameConverter<GeoAssetType>(GeoAssetType.values)",
"dart_type_name": "GeoAssetType"
}
},
{
"name": "active",
"getter_name": "active",
"moor_type": "bool",
"nullable": false,
"customConstraints": null,
"defaultConstraints": "CHECK (\"active\" IN (0, 1))",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "name",
"getter_name": "name",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
{
"allowed-lengths": {
"min": 1,
"max": null
}
}
]
},
{
"name": "provider_name",
"getter_name": "providerName",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
{
"allowed-lengths": {
"min": 1,
"max": null
}
}
]
},
{
"name": "version",
"getter_name": "version",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_check",
"getter_name": "lastCheck",
"moor_type": "dateTime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"without_rowid": false,
"constraints": [],
"explicit_pk": [
"id"
],
"unique_keys": [
[
"name",
"provider_name"
]
]
}
}
]
}

View File

@@ -11,14 +11,14 @@ class ProfileEntries extends Table {
TextColumn get name => text().withLength(min: 1)();
TextColumn get url => text().nullable()();
DateTimeColumn get lastUpdate => dateTime()();
IntColumn get updateInterval =>
integer().nullable().map(DurationTypeConverter())();
IntColumn get updateInterval => integer().nullable().map(DurationTypeConverter())();
IntColumn get upload => integer().nullable()();
IntColumn get download => integer().nullable()();
IntColumn get total => integer().nullable()();
DateTimeColumn get expire => dateTime().nullable()();
TextColumn get webPageUrl => text().nullable()();
TextColumn get supportUrl => text().nullable()();
TextColumn get testUrl => text().nullable()();
@override
Set<Column> get primaryKey => {id};

View File

@@ -22,12 +22,14 @@ abstract interface class ConnectionRepository {
String fileName,
String profileName,
bool disableMemoryLimit,
String? testUrl,
);
TaskEither<ConnectionFailure, Unit> disconnect();
TaskEither<ConnectionFailure, Unit> reconnect(
String fileName,
String profileName,
bool disableMemoryLimit,
String? testUrl,
);
}
@@ -101,11 +103,16 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
@visibleForTesting
TaskEither<ConnectionFailure, Unit> applyConfigOption(
SingboxConfigOption options,
String? testUrl,
) {
return exceptionHandler(
() {
_configOptionsSnapshot = options;
return singbox.changeOptions(options).mapLeft(InvalidConfigOption.new).run();
var newOptions = options;
if (testUrl != null) {
newOptions = options.copyWith(connectionTestUrl: testUrl);
}
return singbox.changeOptions(newOptions).mapLeft(InvalidConfigOption.new).run();
},
UnexpectedConnectionFailure.new,
);
@@ -138,10 +145,11 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
String fileName,
String profileName,
bool disableMemoryLimit,
String? testUrl,
) {
return TaskEither<ConnectionFailure, Unit>.Do(
($) async {
final options = await $(getConfigOption());
var options = await $(getConfigOption());
loggy.info(
"config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}",
);
@@ -159,7 +167,7 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
}),
);
await $(setup());
await $(applyConfigOption(options));
await $(applyConfigOption(options, testUrl));
return await $(
singbox
.start(
@@ -203,23 +211,26 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
String fileName,
String profileName,
bool disableMemoryLimit,
String? testUrl,
) {
return exceptionHandler(
() async {
return getConfigOption()
.flatMap((options) => applyConfigOption(options))
.andThen(
() => singbox
.restart(
profilePathResolver.file(fileName).path,
profileName,
disableMemoryLimit,
)
.mapLeft(UnexpectedConnectionFailure.new),
)
.run();
return TaskEither<ConnectionFailure, Unit>.Do(
($) async {
var options = await $(getConfigOption());
loggy.info(
"config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}",
);
await $(applyConfigOption(options, testUrl));
return await $(
singbox
.restart(
profilePathResolver.file(fileName).path,
profileName,
disableMemoryLimit,
)
.mapLeft(UnexpectedConnectionFailure.new),
);
},
UnexpectedConnectionFailure.new,
);
).handleExceptions(UnexpectedConnectionFailure.new);
}
}

View File

@@ -103,6 +103,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
profile.id,
profile.name,
ref.read(Preferences.disableMemoryLimit),
profile.testUrl,
)
.mapLeft((err) {
loggy.warning("error reconnecting", err);
@@ -133,6 +134,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
activeProfile.id,
activeProfile.name,
ref.read(Preferences.disableMemoryLimit),
activeProfile.testUrl,
)
.mapLeft((err) async {
loggy.warning("error connecting", err);

View File

@@ -5,8 +5,7 @@ import 'package:hiddify/features/profile/model/profile_entity.dart';
extension ProfileEntityMapper on ProfileEntity {
ProfileEntriesCompanion toEntry() {
return switch (this) {
RemoteProfileEntity(:final url, :final options, :final subInfo) =>
ProfileEntriesCompanion.insert(
RemoteProfileEntity(:final url, :final options, :final subInfo) => ProfileEntriesCompanion.insert(
id: id,
type: ProfileType.remote,
active: active,
@@ -20,6 +19,7 @@ extension ProfileEntityMapper on ProfileEntity {
expire: Value(subInfo?.expire),
webPageUrl: Value(subInfo?.webPageUrl),
supportUrl: Value(subInfo?.supportUrl),
testUrl: Value(testUrl),
),
LocalProfileEntity() => ProfileEntriesCompanion.insert(
id: id,
@@ -41,6 +41,7 @@ extension RemoteProfileEntityMapper on RemoteProfileEntity {
expire: Value(subInfo?.expire),
webPageUrl: Value(subInfo?.webPageUrl),
supportUrl: Value(subInfo?.supportUrl),
testUrl: Value(testUrl),
);
}
}
@@ -73,12 +74,14 @@ extension ProfileEntryMapper on ProfileEntry {
lastUpdate: lastUpdate,
options: options,
subInfo: subInfo,
testUrl: testUrl,
),
ProfileType.local => LocalProfileEntity(
id: id,
active: active,
name: name,
lastUpdate: lastUpdate,
testUrl: testUrl,
),
};
}

View File

@@ -24,14 +24,12 @@ abstract class ProfileParser {
var name = '';
if (headers['profile-title'] case [final titleHeader]) {
if (titleHeader.startsWith("base64:")) {
name =
utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", "")));
name = utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", "")));
} else {
name = titleHeader.trim();
}
}
if (headers['content-disposition'] case [final contentDispositionHeader]
when name.isEmpty) {
if (headers['content-disposition'] case [final contentDispositionHeader] when name.isEmpty) {
final regExp = RegExp('filename="([^"]*)"');
final match = regExp.firstMatch(contentDispositionHeader);
if (match != null && match.groupCount >= 1) {
@@ -52,19 +50,20 @@ abstract class ProfileParser {
final updateInterval = Duration(hours: int.parse(updateIntervalStr));
options = ProfileOptions(updateInterval: updateInterval);
}
String? testUrl;
if (headers['test-url'] case [final testUrl_] when isUrl(testUrl_)) {
testUrl = testUrl_;
}
SubscriptionInfo? subInfo;
if (headers['subscription-userinfo'] case [final subInfoStr]) {
subInfo = parseSubscriptionInfo(subInfoStr);
}
if (subInfo != null) {
if (headers['profile-web-page-url'] case [final profileWebPageUrl]
when isUrl(profileWebPageUrl)) {
if (headers['profile-web-page-url'] case [final profileWebPageUrl] when isUrl(profileWebPageUrl)) {
subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl);
}
if (headers['support-url'] case [final profileSupportUrl]
when isUrl(profileSupportUrl)) {
if (headers['support-url'] case [final profileSupportUrl] when isUrl(profileSupportUrl)) {
subInfo = subInfo.copyWith(supportUrl: profileSupportUrl);
}
}
@@ -77,23 +76,16 @@ abstract class ProfileParser {
lastUpdate: DateTime.now(),
options: options,
subInfo: subInfo,
testUrl: testUrl,
);
}
static SubscriptionInfo? parseSubscriptionInfo(String subInfoStr) {
final values = subInfoStr.split(';');
final map = {
for (final v in values)
v.split('=').first.trim():
num.tryParse(v.split('=').second.trim())?.toInt(),
for (final v in values) v.split('=').first.trim(): num.tryParse(v.split('=').second.trim())?.toInt(),
};
if (map
case {
"upload": final upload?,
"download": final download?,
"total": var total,
"expire": var expire
}) {
if (map case {"upload": final upload?, "download": final download?, "total": var total, "expire": var expire}) {
total = (total == null || total == 0) ? infiniteTrafficThreshold : total;
expire = (expire == null || expire == 0) ? infiniteTimeThreshold : expire;
return SubscriptionInfo(

View File

@@ -282,6 +282,7 @@ class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements Profil
? profilePatch.copyWith(
name: Value(baseProfile.name),
url: Value(baseProfile.url),
testUrl: Value(baseProfile.testUrl),
updateInterval: Value(baseProfile.options?.updateInterval),
)
: profilePatch,
@@ -349,6 +350,7 @@ class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements Profil
'profile-update-interval',
'support-url',
'profile-web-page-url',
'test-url',
];
@visibleForTesting

View File

@@ -15,6 +15,7 @@ sealed class ProfileEntity with _$ProfileEntity {
required String name,
required String url,
required DateTime lastUpdate,
String? testUrl,
ProfileOptions? options,
SubscriptionInfo? subInfo,
}) = RemoteProfileEntity;
@@ -24,6 +25,7 @@ sealed class ProfileEntity with _$ProfileEntity {
required bool active,
required String name,
required DateTime lastUpdate,
String? testUrl,
}) = LocalProfileEntity;
}

View File

@@ -17,15 +17,11 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
static const channelPrefix = "com.hiddify.app";
static const methodChannel = MethodChannel("$channelPrefix/method");
static const statusChannel =
EventChannel("$channelPrefix/service.status", JSONMethodCodec());
static const alertsChannel =
EventChannel("$channelPrefix/service.alerts", JSONMethodCodec());
static const statsChannel =
EventChannel("$channelPrefix/stats", JSONMethodCodec());
static const statusChannel = EventChannel("$channelPrefix/service.status", JSONMethodCodec());
static const alertsChannel = EventChannel("$channelPrefix/service.alerts", JSONMethodCodec());
static const statsChannel = EventChannel("$channelPrefix/stats", JSONMethodCodec());
static const groupsChannel = EventChannel("$channelPrefix/groups");
static const activeGroupsChannel =
EventChannel("$channelPrefix/active-groups");
static const activeGroupsChannel = EventChannel("$channelPrefix/active-groups");
static const logsChannel = EventChannel("$channelPrefix/service.logs");
late final ValueStream<SingboxStatus> _status;
@@ -33,10 +29,8 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
@override
Future<void> init() async {
loggy.debug("initializing");
final status =
statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
final alerts =
alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
final status = statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
final alerts = alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
_status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect();
await _status.first;
@@ -250,9 +244,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
@override
Stream<List<String>> watchLogs(String path) async* {
yield* logsChannel
.receiveBroadcastStream()
.map((event) => (event as List).map((e) => e as String).toList());
yield* logsChannel.receiveBroadcastStream().map((event) => (event as List).map((e) => e as String).toList());
}
@override

View File

@@ -1,6 +1,7 @@
/// https://gist.github.com/dperini/729294
final _urlRegex = RegExp(
r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$",
// r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$",
r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+',
);
/// https://stackoverflow.com/a/12968117