LCOV - code coverage report
Current view: top level - lib/models - message.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 144 0.0 %
Date: 2024-04-05 23:55:39 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:convert';
       2             : import 'package:cwtch/config.dart';
       3             : import 'package:cwtch/cwtch/cwtch.dart';
       4             : import 'package:flutter/material.dart';
       5             : import 'package:flutter/widgets.dart';
       6             : import 'package:provider/provider.dart';
       7             : 
       8             : import '../main.dart';
       9             : import 'messagecache.dart';
      10             : import 'messages/filemessage.dart';
      11             : import 'messages/invitemessage.dart';
      12             : import 'messages/malformedmessage.dart';
      13             : import 'messages/quotedmessage.dart';
      14             : import 'messages/textmessage.dart';
      15             : import 'profile.dart';
      16             : 
      17             : // Define the overlays
      18             : const TextMessageOverlay = 1;
      19             : const QuotedMessageOverlay = 10;
      20             : const SuggestContactOverlay = 100;
      21             : const InviteGroupOverlay = 101;
      22             : const FileShareOverlay = 200;
      23             : 
      24             : // Defines the length of the tor v3 onion address. Code using this constant will
      25             : // need to updated when we allow multiple different identifiers. At which time
      26             : // it will likely be prudent to define a proper Contact wrapper.
      27             : const TorV3ContactHandleLength = 56;
      28             : 
      29             : // Defines the length of a Cwtch v2 Group.
      30             : const GroupConversationHandleLength = 32;
      31             : 
      32             : abstract class Message {
      33             :   MessageMetadata getMetadata();
      34             : 
      35             :   Widget getWidget(BuildContext context, Key key, int index);
      36             : 
      37             :   Widget getPreviewWidget(BuildContext context, {BoxConstraints? constraints});
      38             : }
      39             : 
      40           0 : Message compileOverlay(MessageInfo messageInfo) {
      41             :   try {
      42           0 :     dynamic message = jsonDecode(messageInfo.wrapper);
      43           0 :     var content = message['d'] as dynamic;
      44           0 :     var overlay = int.parse(message['o'].toString());
      45             : 
      46             :     switch (overlay) {
      47           0 :       case TextMessageOverlay:
      48           0 :         return TextMessage(messageInfo.metadata, content);
      49           0 :       case SuggestContactOverlay:
      50           0 :       case InviteGroupOverlay:
      51           0 :         return InviteMessage(overlay, messageInfo.metadata, content);
      52           0 :       case QuotedMessageOverlay:
      53           0 :         return QuotedMessage(messageInfo.metadata, content);
      54           0 :       case FileShareOverlay:
      55           0 :         return FileMessage(messageInfo.metadata, content);
      56             :       default:
      57             :         // Metadata is valid, content is not..
      58           0 :         return MalformedMessage(messageInfo.metadata);
      59             :     }
      60             :   } catch (e) {
      61           0 :     return MalformedMessage(messageInfo.metadata);
      62             :   }
      63             : }
      64             : 
      65             : abstract class CacheHandler {
      66             :   Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
      67             :   Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
      68             : }
      69             : 
      70             : class ByIndex implements CacheHandler {
      71             :   int index;
      72             : 
      73           0 :   ByIndex(this.index);
      74             : 
      75           0 :   Future<MessageInfo?> lookup(MessageCache cache) async {
      76           0 :     var msg = cache.getByIndex(index);
      77             :     return msg;
      78             :   }
      79             : 
      80           0 :   Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
      81             :     // if in cache, get. But if the cache has unsynced or not in cache, we'll have to do a fetch
      82           0 :     if (index < cache.cacheByIndex.length) {
      83           0 :       return cache.getByIndex(index);
      84             :     }
      85             : 
      86             :     // otherwise we are going to fetch, so we'll fetch a chunk of messages
      87             :     // observationally flutter future builder seemed to be reaching for 20-40 message on pane load, so we start trying to load up to that many messages in one request
      88             :     var amount = 40;
      89           0 :     var start = index;
      90             :     // we have to keep the indexed cache contiguous so reach back to the end of it and start the fetch from there
      91           0 :     if (index > cache.cacheByIndex.length) {
      92           0 :       start = cache.cacheByIndex.length;
      93           0 :       amount += index - start;
      94             :     }
      95             : 
      96             :     // check that we aren't asking for messages beyond stored messages
      97           0 :     if (start + amount >= cache.storageMessageCount) {
      98           0 :       amount = cache.storageMessageCount - start;
      99           0 :       if (amount <= 0) {
     100           0 :         return Future.value(null);
     101             :       }
     102             :     }
     103             : 
     104           0 :     cache.lockIndexes(start, start + amount);
     105           0 :     await fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
     106             : 
     107           0 :     return cache.getByIndex(index);
     108             :   }
     109             : 
     110           0 :   void loadUnsynced(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
     111             :     // return if inadvertently called when no unsynced messages
     112           0 :     if (cache.indexUnsynced == 0) {
     113             :       return;
     114             :     }
     115             : 
     116             :     // otherwise we are going to fetch, so we'll fetch a chunk of messages
     117             :     var start = 0;
     118           0 :     var amount = cache.indexUnsynced;
     119             : 
     120           0 :     cache.lockIndexes(start, start + amount);
     121           0 :     fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
     122             :     return;
     123             :   }
     124             : 
     125           0 :   Future<void> fetchAndProcess(int start, int amount, Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
     126           0 :     var msgs = await cwtch.GetMessages(profileOnion, conversationIdentifier, start, amount);
     127             :     int i = 0; // i used to loop through returned messages. if doesn't reach the requested count, we will use it in the finally stanza to error out the remaining asked for messages in the cache
     128             :     try {
     129           0 :       List<dynamic> messagesWrapper = jsonDecode(msgs);
     130             : 
     131           0 :       for (; i < messagesWrapper.length; i++) {
     132           0 :         var messageInfo = MessageWrapperToInfo(profileOnion, conversationIdentifier, messagesWrapper[i]);
     133           0 :         messageInfo.metadata.lastChecked = DateTime.now();
     134           0 :         cache.addIndexed(messageInfo, start + i);
     135             :       }
     136             :     } catch (e, stacktrace) {
     137           0 :       EnvironmentConfig.debugLog("Error: Getting indexed messages $start to ${start + amount} failed parsing: " + e.toString() + " " + stacktrace.toString());
     138             :     } finally {
     139           0 :       if (i != amount) {
     140           0 :         cache.malformIndexes(start + i, start + amount);
     141             :       }
     142             :     }
     143             :   }
     144             : 
     145           0 :   void add(MessageCache cache, MessageInfo messageInfo) {
     146           0 :     cache.addIndexed(messageInfo, index);
     147             :   }
     148             : 
     149           0 :   @override
     150             :   Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
     151           0 :     EnvironmentConfig.debugLog("performing a resync on message ${index}");
     152           0 :     fetchAndProcess(index, 1, cwtch, profileOnion, conversationIdentifier, cache);
     153           0 :     return get(cwtch, profileOnion, conversationIdentifier, cache);
     154             :   }
     155             : }
     156             : 
     157             : class ById implements CacheHandler {
     158             :   int id;
     159             : 
     160           0 :   ById(this.id);
     161             : 
     162           0 :   Future<MessageInfo?> lookup(MessageCache cache) {
     163           0 :     return Future<MessageInfo?>.value(cache.getById(id));
     164             :   }
     165             : 
     166           0 :   Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
     167           0 :     var rawMessageEnvelope = await cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
     168           0 :     var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
     169             :     if (messageInfo == null) {
     170           0 :       return Future.value(null);
     171             :     }
     172           0 :     EnvironmentConfig.debugLog("fetching $profileOnion $conversationIdentifier $id ${messageInfo.wrapper}");
     173           0 :     cache.addUnindexed(messageInfo);
     174           0 :     return Future.value(messageInfo);
     175             :   }
     176             : 
     177           0 :   Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
     178           0 :     var messageInfo = await lookup(cache);
     179             :     if (messageInfo != null) {
     180           0 :       return Future.value(messageInfo);
     181             :     }
     182           0 :     return fetch(cwtch, profileOnion, conversationIdentifier, cache);
     183             :   }
     184             : 
     185           0 :   @override
     186             :   Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
     187           0 :     return get(cwtch, profileOnion, conversationIdentifier, cache);
     188             :   }
     189             : }
     190             : 
     191             : class ByContentHash implements CacheHandler {
     192             :   String hash;
     193             : 
     194           0 :   ByContentHash(this.hash);
     195             : 
     196           0 :   Future<MessageInfo?> lookup(MessageCache cache) {
     197           0 :     return Future<MessageInfo?>.value(cache.getByContentHash(hash));
     198             :   }
     199             : 
     200           0 :   Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
     201           0 :     var rawMessageEnvelope = await cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
     202           0 :     var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
     203             :     if (messageInfo == null) {
     204           0 :       return Future.value(null);
     205             :     }
     206           0 :     cache.addUnindexed(messageInfo);
     207           0 :     return Future.value(messageInfo);
     208             :   }
     209             : 
     210           0 :   Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
     211           0 :     var messageInfo = await lookup(cache);
     212             :     if (messageInfo != null) {
     213           0 :       return Future.value(messageInfo);
     214             :     }
     215           0 :     return fetch(cwtch, profileOnion, conversationIdentifier, cache);
     216             :   }
     217             : 
     218           0 :   @override
     219             :   Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
     220           0 :     return get(cwtch, profileOnion, conversationIdentifier, cache);
     221             :   }
     222             : }
     223             : 
     224           0 : List<Message> getReplies(MessageCache cache, int messageIdentifier) {
     225           0 :   List<Message> replies = List.empty(growable: true);
     226             : 
     227             :   try {
     228           0 :     MessageInfo original = cache.cache[messageIdentifier]!;
     229           0 :     String hash = original.metadata.contenthash;
     230             : 
     231           0 :     cache.cache.forEach((key, messageInfo) {
     232             :       // only bother searching for identifiers that came *after*
     233           0 :       if (key > messageIdentifier) {
     234             :         try {
     235           0 :           dynamic message = jsonDecode(messageInfo.wrapper);
     236           0 :           var content = message['d'] as dynamic;
     237           0 :           dynamic qmessage = jsonDecode(content);
     238           0 :           if (qmessage["body"] == null || qmessage["quotedHash"] == null) {
     239             :             return;
     240             :           }
     241           0 :           if (qmessage["quotedHash"] == hash) {
     242           0 :             replies.add(compileOverlay(messageInfo));
     243             :           }
     244             :         } catch (e) {
     245             :           // ignore
     246             :         }
     247             :       }
     248             :     });
     249             :   } catch (e) {
     250           0 :     EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
     251             :   }
     252             : 
     253           0 :   replies.sort((a, b) {
     254           0 :     return a.getMetadata().messageID.compareTo(b.getMetadata().messageID);
     255             :   });
     256             : 
     257             :   return replies;
     258             : }
     259             : 
     260           0 : Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) async {
     261           0 :   var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false, "");
     262           0 :   var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;
     263             : 
     264             :   MessageCache? cache;
     265             :   try {
     266           0 :     cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
     267             :     if (cache == null) {
     268           0 :       EnvironmentConfig.debugLog("error: cannot get message cache for profile: $profileOnion conversation: $conversationIdentifier");
     269           0 :       return MalformedMessage(malformedMetadata);
     270             :     }
     271             :   } catch (e) {
     272           0 :     EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
     273             :     // provider check failed...make an expensive call...
     274           0 :     return MalformedMessage(malformedMetadata);
     275             :   }
     276             : 
     277           0 :   MessageInfo? messageInfo = await cacheHandler.get(cwtch, profileOnion, conversationIdentifier, cache);
     278             : 
     279             :   if (messageInfo != null) {
     280           0 :     if (messageInfo.metadata.ackd == false) {
     281           0 :       if (messageInfo.metadata.lastChecked == null || messageInfo.metadata.lastChecked!.difference(DateTime.now()).abs().inSeconds > 30) {
     282           0 :         messageInfo.metadata.lastChecked = DateTime.now();
     283             :         // NOTE: Only ByIndex lookups will trigger
     284           0 :         messageInfo = await cacheHandler.sync(cwtch, profileOnion, conversationIdentifier, cache);
     285             :       }
     286             :     }
     287             :   }
     288             : 
     289             :   if (messageInfo != null) {
     290           0 :     return compileOverlay(messageInfo);
     291             :   } else {
     292           0 :     return MalformedMessage(malformedMetadata);
     293             :   }
     294             : }
     295             : 
     296           0 : MessageInfo? messageJsonToInfo(String profileOnion, int conversationIdentifier, dynamic messageJson) {
     297             :   try {
     298           0 :     dynamic messageWrapper = jsonDecode(messageJson);
     299             : 
     300           0 :     if (messageWrapper == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
     301             :       return null;
     302             :     }
     303             : 
     304           0 :     return MessageWrapperToInfo(profileOnion, conversationIdentifier, messageWrapper);
     305             :   } catch (e, stacktrace) {
     306           0 :     EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
     307             :     return null;
     308             :   }
     309             : }
     310             : 
     311           0 : MessageInfo MessageWrapperToInfo(String profileOnion, int conversationIdentifier, dynamic messageWrapper) {
     312             :   // Construct the initial metadata
     313           0 :   var messageID = messageWrapper['ID'];
     314           0 :   var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
     315           0 :   var senderHandle = messageWrapper['PeerID'];
     316           0 :   var senderImage = messageWrapper['ContactImage'];
     317           0 :   var attributes = messageWrapper['Attributes'];
     318           0 :   var ackd = messageWrapper['Acknowledged'];
     319           0 :   var error = messageWrapper['Error'] != null;
     320           0 :   var signature = messageWrapper['Signature'];
     321           0 :   var contenthash = messageWrapper['ContentHash'];
     322           0 :   var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false, contenthash);
     323           0 :   var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
     324             : 
     325             :   return messageInfo;
     326             : }
     327             : 
     328             : class MessageMetadata extends ChangeNotifier {
     329             :   // meta-metadata
     330             :   final String profileOnion;
     331             :   final int conversationIdentifier;
     332             :   final int messageID;
     333             : 
     334             :   final DateTime timestamp;
     335             :   final String senderHandle;
     336             :   final String? senderImage;
     337             :   final dynamic _attributes;
     338             :   bool _ackd;
     339             :   bool _error;
     340             :   final bool isAuto;
     341             : 
     342             :   final String? signature;
     343             :   final String contenthash;
     344             :   DateTime? lastChecked;
     345             : 
     346           0 :   dynamic get attributes => this._attributes;
     347             : 
     348           0 :   bool get ackd => this._ackd;
     349             : 
     350             :   String translation = "";
     351           0 :   void updateTranslationEvent(String translation) {
     352           0 :     this.translation += translation;
     353           0 :     notifyListeners();
     354             :   }
     355             : 
     356           0 :   set ackd(bool newVal) {
     357           0 :     this._ackd = newVal;
     358           0 :     notifyListeners();
     359             :   }
     360             : 
     361           0 :   bool get error => this._error;
     362             : 
     363           0 :   set error(bool newVal) {
     364           0 :     this._error = newVal;
     365           0 :     notifyListeners();
     366             :   }
     367             : 
     368           0 :   MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error,
     369             :       this.isAuto, this.contenthash);
     370             : }

Generated by: LCOV version 1.14