186 lines
5.7 KiB
Dart
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;
|
|
}
|