133 lines
3.8 KiB
Dart
133 lines
3.8 KiB
Dart
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<HomeBanner> parseHomeBanners(dynamic raw) {
|
|
final result = <HomeBanner>[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<List<HomeBanner>> load() async {
|
|
try {
|
|
return parseHomeBanners(await api.get('banners/'));
|
|
} catch (_) {
|
|
// Announcements remain reachable when the metadata service is offline.
|
|
return [HomeBanner.telegram];
|
|
}
|
|
}
|
|
}
|