import 'api_service.dart'; import 'content_policy.dart'; class HomeBanner { const HomeBanner._({ required this.titleEn, required this.titleFa, required this.subtitleEn, required this.subtitleFa, required this.target, required this.isAdvertisement, this.image, }); final String titleEn, titleFa, subtitleEn, subtitleFa; final Uri target; final Uri? image; final bool isAdvertisement; String title(bool persian) => persian ? titleFa : titleEn; String subtitle(bool persian) => persian ? subtitleFa : subtitleEn; static final telegram = HomeBanner._( titleEn: 'ReelBox updates', titleFa: 'تازه‌های ریل‌باکس', subtitleEn: 'Updates and announcements', subtitleFa: 'به‌روزرسانی‌ها و اطلاعیه‌ها', target: Uri.parse('https://t.me/ReelBoxApp'), isAdvertisement: false, ); } /// Only configured advertisements are parsed. The official Telegram entry is /// fixed in the client and can never be replaced or relabelled by server data. List parseHomeBanners(dynamic raw) { final result = [HomeBanner.telegram]; if (raw is! Map || raw['banners'] is! List) return result; for (final row in (raw['banners'] as List).take(20)) { if (result.length == 6) break; if (row is! Map) continue; final en = _label(row['title_en'], 80); final fa = _label(row['title_fa'] ?? row['title_en'], 80); final subtitleEn = _label(row['subtitle_en'] ?? '', 180); final subtitleFa = _label( row['subtitle_fa'] ?? row['subtitle_en'] ?? '', 180, ); final target = _https(row['target_url']); final imageValue = row['image_url']; final image = imageValue == null || imageValue == '' ? null : _https(imageValue); if (en == null || en.isEmpty || fa == null || fa.isEmpty || subtitleEn == null || subtitleFa == null || target == null || (imageValue != null && imageValue != '' && image == null)) { continue; } result.add( HomeBanner._( titleEn: en, titleFa: fa, subtitleEn: subtitleEn, subtitleFa: subtitleFa, target: target, image: image, isAdvertisement: true, ), ); } return List.unmodifiable(result); } String? _label(dynamic value, int limit) { if (value is! String || value.length > limit || RegExp(r'[\x00-\x1f\x7f\u202a-\u202e\u2066-\u2069]').hasMatch(value) || !ContentPolicy.allowsText(value)) { return null; } return value.trim(); } Uri? _https(dynamic value) { if (value is! String || value.length > 2048 || RegExp(r'[\x00-\x20\x7f\\]').hasMatch(value) || RegExp( r'%(?![0-9a-fA-F]{2})|%(?:0[0-9a-f]|1[0-9a-f]|7f)', caseSensitive: false, ).hasMatch(value) || !ContentPolicy.allowsUrl(value)) { return null; } try { final uri = Uri.parse(value); if (uri.scheme != 'https' || uri.host.isEmpty || uri.userInfo.isNotEmpty || uri.port != 443 || uri.host.length > 253 || !uri.host .split('.') .every( (part) => RegExp(r'^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$') .hasMatch(part), )) { return null; } return uri; } on FormatException { return null; } } /// Uses only the existing trusted metadata transport, never a config URL. class HomeBannerService { const HomeBannerService(this.api); final ApiService api; Future> load() async { try { return parseHomeBanners(await api.get('banners/')); } catch (_) { // Announcements remain reachable when the metadata service is offline. return [HomeBanner.telegram]; } } }