import 'dart:convert'; import 'package:dartx/dartx.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:uuid/uuid.dart'; /// parse profile subscription url and headers for data /// /// ***name parser hierarchy:*** /// - `profile-title` header /// - `content-disposition` header /// - url fragment (example: `https://example.com/config#user`) -> name=`user` /// - url filename extension (example: `https://example.com/config.json`) -> name=`config` /// - if none of these methods return a non-blank string, fallback to `Remote Profile` abstract class ProfileParser { static const infiniteTrafficThreshold = 9223372036854775807; static const infiniteTimeThreshold = 92233720368; static RemoteProfileEntity parse( String url, Map> headers, ) { var name = ''; if (headers['profile-title'] case [final titleHeader]) { if (titleHeader.startsWith("base64:")) { name = utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); } else { name = titleHeader.trim(); } } 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) { name = match.group(1) ?? ''; } } if (Uri.parse(url).fragment case final fragment when name.isEmpty) { name = fragment; } if (url.split("/").lastOrNull case final part? when name.isEmpty) { final pattern = RegExp(r"\.(json|yaml|yml|txt)[\s\S]*"); name = part.replaceFirst(pattern, ""); } if (name.isBlank) name = "Remote Profile"; ProfileOptions? options; if (headers['profile-update-interval'] case [final updateIntervalStr]) { final updateInterval = Duration(hours: int.parse(updateIntervalStr)); options = ProfileOptions(updateInterval: updateInterval); } 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)) { subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl); } if (headers['support-url'] case [final profileSupportUrl] when isUrl(profileSupportUrl)) { subInfo = subInfo.copyWith(supportUrl: profileSupportUrl); } } return RemoteProfileEntity( id: const Uuid().v4(), active: false, name: name, url: url, lastUpdate: DateTime.now(), options: options, subInfo: subInfo, ); } 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(), }; 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( upload: upload, download: download, total: total, expire: DateTime.fromMillisecondsSinceEpoch(expire * 1000), ); } return null; } }