Files
rely/lib/features/share_intake/domain/share_payload_parser.dart
T
2026-05-19 19:17:35 +02:00

186 lines
5.7 KiB
Dart

import 'package:relationship_saver/features/local/local_models.dart';
import 'package:relationship_saver/features/share_intake/domain/shared_message_datetime_extractor.dart';
import 'package:relationship_saver/features/share_intake/domain/signal_share_parser.dart';
import 'package:relationship_saver/features/share_intake/whatsapp_share_parser.dart';
class SharePayloadParser {
const SharePayloadParser._();
static final RegExp _urlPattern = RegExp(
r'(https?:\/\/[^\s]+|www\.[^\s]+)',
caseSensitive: false,
);
static SharedPayload parse({
required String rawText,
required String sourceApp,
required String platform,
DateTime? createdAt,
DateTime? receivedAt,
}) {
final DateTime now = DateTime.now();
final String normalizedSourceApp = _normalizeSourceApp(sourceApp);
final String trimmed = rawText.trim();
final DateTime? sharedMessageDateTime =
SharedMessageDateTimeExtractor.extract(trimmed, now: receivedAt ?? now);
final _ParsedSourceMetadata? sourceMetadata = _parseSourceMetadata(
sourceApp: normalizedSourceApp,
rawText: trimmed,
);
final String candidateText = sourceMetadata?.messageText.trim() ?? trimmed;
final RegExpMatch? urlMatch = _urlPattern.firstMatch(candidateText);
final String? url = urlMatch == null
? null
: _normalizeUrl(urlMatch.group(0));
final String cleanedText = url == null
? candidateText
: candidateText.replaceFirst(urlMatch!.group(0)!, '').trim();
final SharedPayloadType payloadType;
if (url != null && cleanedText.isNotEmpty) {
payloadType = SharedPayloadType.textWithUrl;
} else if (url != null) {
payloadType = SharedPayloadType.url;
} else {
payloadType = SharedPayloadType.text;
}
return SharedPayload(
sourceApp: normalizedSourceApp,
payloadType: payloadType,
rawText: cleanedText,
url: url,
createdAt: createdAt ?? now,
receivedAt: receivedAt ?? now,
sharedMessageDateTime: sharedMessageDateTime,
platform: platform,
sourceDisplayName: sourceMetadata?.sourceDisplayName,
sourceUserId: sourceMetadata?.sourceUserId,
sourceThreadId: sourceMetadata?.sourceThreadId,
parsingHints: <String>[
if (normalizedSourceApp != 'share_sheet')
'source_app_$normalizedSourceApp',
if (url != null) 'contains_url',
if (sharedMessageDateTime != null) 'shared_message_datetime_detected',
if (sourceMetadata?.sourceDisplayName case final String _)
'sender_detected',
],
metadata: <String, String>{
if (url case final String value) 'primaryUrl': value,
if (sharedMessageDateTime case final DateTime value)
'shared_message_datetime': value.toUtc().toIso8601String(),
},
);
}
static String detectSourceApp({
required String rawText,
String? sourceAppHint,
}) {
final String normalizedHint = _normalizeSourceApp(sourceAppHint);
if (normalizedHint != 'share_sheet') {
return normalizedHint;
}
final String trimmed = rawText.trim();
if (_shouldUseWhatsAppParser(normalizedHint, trimmed)) {
return 'whatsapp';
}
return 'share_sheet';
}
static bool _shouldUseWhatsAppParser(String sourceApp, String rawText) {
if (sourceApp.contains('whatsapp')) {
return true;
}
if (sourceApp != 'share_sheet') {
return false;
}
if (rawText.contains(': ') && rawText.length < 4000) {
return WhatsAppShareParser.looksLikeWhatsAppMessage(rawText);
}
return false;
}
static bool _shouldUseSignalParser(String sourceApp) {
return sourceApp.contains('signal');
}
static _ParsedSourceMetadata? _parseSourceMetadata({
required String sourceApp,
required String rawText,
}) {
if (_shouldUseWhatsAppParser(sourceApp, rawText)) {
final WhatsAppSharePayload payload = WhatsAppShareParser.parse(rawText);
return _ParsedSourceMetadata(
messageText: payload.messageText,
sourceDisplayName: payload.sourceDisplayName,
sourceUserId: payload.sourceUserId,
sourceThreadId: payload.sourceThreadId,
);
}
if (_shouldUseSignalParser(sourceApp) &&
SignalShareParser.looksLikeSignalMessage(rawText)) {
final SignalSharePayload payload = SignalShareParser.parse(rawText);
return _ParsedSourceMetadata(
messageText: payload.messageText,
sourceDisplayName: payload.sourceDisplayName,
sourceUserId: payload.sourceUserId,
sourceThreadId: payload.sourceThreadId,
);
}
return null;
}
static String _normalizeSourceApp(String? raw) {
final String value = raw?.trim().toLowerCase() ?? '';
if (value.isEmpty ||
value == 'generic' ||
value == 'text' ||
value == 'share sheet') {
return 'share_sheet';
}
if (value.contains('whatsapp')) {
return 'whatsapp';
}
if (value.contains('signal')) {
return 'signal';
}
if (value.contains('note')) {
return 'notes';
}
return value;
}
static String? _normalizeUrl(String? raw) {
if (raw == null) {
return null;
}
final String value = raw.trim();
if (value.isEmpty) {
return null;
}
if (value.toLowerCase().startsWith('http://') ||
value.toLowerCase().startsWith('https://')) {
return value;
}
return 'https://$value';
}
}
class _ParsedSourceMetadata {
const _ParsedSourceMetadata({
required this.messageText,
this.sourceDisplayName,
this.sourceUserId,
this.sourceThreadId,
});
final String messageText;
final String? sourceDisplayName;
final String? sourceUserId;
final String? sourceThreadId;
}