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 : EnvironmentConfig.debugLog("unknown overlay:$overlay");
59 0 : return MalformedMessage(messageInfo.metadata);
60 : }
61 : } catch (e) {
62 0 : return MalformedMessage(messageInfo.metadata);
63 : }
64 : }
65 :
66 : abstract class CacheHandler {
67 : Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
68 : Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
69 : }
70 :
71 : class ByIndex implements CacheHandler {
72 : int index;
73 :
74 0 : ByIndex(this.index);
75 :
76 0 : Future<MessageInfo?> lookup(MessageCache cache) async {
77 0 : var msg = cache.getByIndex(index);
78 : return msg;
79 : }
80 :
81 0 : Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
82 : // if in cache, get. But if the cache has unsynced or not in cache, we'll have to do a fetch
83 0 : if (index < cache.cacheByIndex.length) {
84 0 : return cache.getByIndex(index);
85 : }
86 :
87 : // otherwise we are going to fetch, so we'll fetch a chunk of messages
88 : // 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
89 : var amount = 40;
90 0 : var start = index;
91 : // we have to keep the indexed cache contiguous so reach back to the end of it and start the fetch from there
92 0 : if (index > cache.cacheByIndex.length) {
93 0 : start = cache.cacheByIndex.length;
94 0 : amount += index - start;
95 : }
96 :
97 : // check that we aren't asking for messages beyond stored messages
98 0 : if (start + amount >= cache.storageMessageCount) {
99 0 : amount = cache.storageMessageCount - start;
100 0 : if (amount <= 0) {
101 0 : return Future.value(null);
102 : }
103 : }
104 :
105 0 : cache.lockIndexes(start, start + amount);
106 0 : await fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
107 :
108 0 : return cache.getByIndex(index);
109 : }
110 :
111 0 : void loadUnsynced(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
112 : // return if inadvertently called when no unsynced messages
113 0 : if (cache.indexUnsynced == 0) {
114 : return;
115 : }
116 :
117 : // otherwise we are going to fetch, so we'll fetch a chunk of messages
118 : var start = 0;
119 0 : var amount = cache.indexUnsynced;
120 :
121 0 : cache.lockIndexes(start, start + amount);
122 0 : fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
123 : return;
124 : }
125 :
126 0 : Future<void> fetchAndProcess(int start, int amount, Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
127 0 : var msgs = await cwtch.GetMessages(profileOnion, conversationIdentifier, start, amount);
128 : 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
129 : try {
130 0 : List<dynamic> messagesWrapper = jsonDecode(msgs);
131 :
132 0 : for (; i < messagesWrapper.length; i++) {
133 0 : var messageInfo = MessageWrapperToInfo(profileOnion, conversationIdentifier, messagesWrapper[i]);
134 0 : messageInfo.metadata.lastChecked = DateTime.now();
135 0 : cache.addIndexed(messageInfo, start + i);
136 : }
137 : } catch (e, stacktrace) {
138 0 : EnvironmentConfig.debugLog("Error: Getting indexed messages $start to ${start + amount} failed parsing: " + e.toString() + " " + stacktrace.toString());
139 : } finally {
140 0 : if (i != amount) {
141 0 : cache.malformIndexes(start + i, start + amount);
142 : }
143 : }
144 : }
145 :
146 0 : void add(MessageCache cache, MessageInfo messageInfo) {
147 0 : cache.addIndexed(messageInfo, index);
148 : }
149 :
150 0 : @override
151 : Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
152 0 : EnvironmentConfig.debugLog("performing a resync on message ${index}");
153 0 : fetchAndProcess(index, 1, cwtch, profileOnion, conversationIdentifier, cache);
154 0 : return get(cwtch, profileOnion, conversationIdentifier, cache);
155 : }
156 : }
157 :
158 : class ById implements CacheHandler {
159 : int id;
160 :
161 0 : ById(this.id);
162 :
163 0 : Future<MessageInfo?> lookup(MessageCache cache) {
164 0 : return Future<MessageInfo?>.value(cache.getById(id));
165 : }
166 :
167 0 : Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
168 0 : var rawMessageEnvelope = await cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
169 0 : var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
170 : if (messageInfo == null) {
171 0 : return Future.value(null);
172 : }
173 0 : EnvironmentConfig.debugLog("fetching $profileOnion $conversationIdentifier $id ${messageInfo.wrapper}");
174 0 : cache.addUnindexed(messageInfo);
175 0 : return Future.value(messageInfo);
176 : }
177 :
178 0 : Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
179 0 : var messageInfo = await lookup(cache);
180 : if (messageInfo != null) {
181 0 : return Future.value(messageInfo);
182 : }
183 0 : return fetch(cwtch, profileOnion, conversationIdentifier, cache);
184 : }
185 :
186 0 : @override
187 : Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
188 0 : return get(cwtch, profileOnion, conversationIdentifier, cache);
189 : }
190 : }
191 :
192 : class ByContentHash implements CacheHandler {
193 : String hash;
194 :
195 0 : ByContentHash(this.hash);
196 :
197 0 : Future<MessageInfo?> lookup(MessageCache cache) {
198 0 : return Future<MessageInfo?>.value(cache.getByContentHash(hash));
199 : }
200 :
201 0 : Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
202 0 : var rawMessageEnvelope = await cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
203 0 : var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
204 : if (messageInfo == null) {
205 0 : return Future.value(null);
206 : }
207 0 : cache.addUnindexed(messageInfo);
208 0 : return Future.value(messageInfo);
209 : }
210 :
211 0 : Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
212 0 : var messageInfo = await lookup(cache);
213 : if (messageInfo != null) {
214 0 : return Future.value(messageInfo);
215 : }
216 0 : return fetch(cwtch, profileOnion, conversationIdentifier, cache);
217 : }
218 :
219 0 : @override
220 : Future<MessageInfo?> sync(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
221 0 : return get(cwtch, profileOnion, conversationIdentifier, cache);
222 : }
223 : }
224 :
225 0 : List<Message> getReplies(MessageCache cache, int messageIdentifier) {
226 0 : List<Message> replies = List.empty(growable: true);
227 :
228 : try {
229 0 : MessageInfo original = cache.cache[messageIdentifier]!;
230 0 : String hash = original.metadata.contenthash;
231 :
232 0 : cache.cache.forEach((key, messageInfo) {
233 : // only bother searching for identifiers that came *after*
234 0 : if (key > messageIdentifier) {
235 : try {
236 0 : dynamic message = jsonDecode(messageInfo.wrapper);
237 0 : var content = message['d'] as dynamic;
238 0 : dynamic qmessage = jsonDecode(content);
239 0 : if (qmessage["body"] == null || qmessage["quotedHash"] == null) {
240 : return;
241 : }
242 0 : if (qmessage["quotedHash"] == hash) {
243 0 : replies.add(compileOverlay(messageInfo));
244 : }
245 : } catch (e) {
246 : // ignore
247 : }
248 : }
249 : });
250 : } catch (e) {
251 0 : EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
252 : }
253 :
254 0 : replies.sort((a, b) {
255 0 : return a.getMetadata().messageID.compareTo(b.getMetadata().messageID);
256 : });
257 :
258 : return replies;
259 : }
260 :
261 0 : Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) async {
262 0 : var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false, "");
263 0 : var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;
264 :
265 : MessageCache? cache;
266 : try {
267 0 : cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
268 : if (cache == null) {
269 0 : EnvironmentConfig.debugLog("error: cannot get message cache for profile: $profileOnion conversation: $conversationIdentifier");
270 0 : return MalformedMessage(malformedMetadata);
271 : }
272 : } catch (e) {
273 0 : EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
274 : // provider check failed...make an expensive call...
275 0 : return MalformedMessage(malformedMetadata);
276 : }
277 :
278 0 : MessageInfo? messageInfo = await cacheHandler.get(cwtch, profileOnion, conversationIdentifier, cache);
279 :
280 : if (messageInfo != null) {
281 0 : if (messageInfo.metadata.ackd == false) {
282 0 : if (messageInfo.metadata.lastChecked == null || messageInfo.metadata.lastChecked!.difference(DateTime.now()).abs().inSeconds > 30) {
283 0 : messageInfo.metadata.lastChecked = DateTime.now();
284 : // NOTE: Only ByIndex lookups will trigger
285 0 : messageInfo = await cacheHandler.sync(cwtch, profileOnion, conversationIdentifier, cache);
286 : }
287 : }
288 : }
289 :
290 : if (messageInfo != null) {
291 0 : return compileOverlay(messageInfo);
292 : } else {
293 0 : return MalformedMessage(malformedMetadata);
294 : }
295 : }
296 :
297 0 : MessageInfo? messageJsonToInfo(String profileOnion, int conversationIdentifier, dynamic messageJson) {
298 : try {
299 0 : dynamic messageWrapper = jsonDecode(messageJson);
300 :
301 0 : if (messageWrapper == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
302 : return null;
303 : }
304 :
305 0 : return MessageWrapperToInfo(profileOnion, conversationIdentifier, messageWrapper);
306 : } catch (e, stacktrace) {
307 0 : EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
308 : return null;
309 : }
310 : }
311 :
312 0 : MessageInfo MessageWrapperToInfo(String profileOnion, int conversationIdentifier, dynamic messageWrapper) {
313 : // Construct the initial metadata
314 0 : var messageID = messageWrapper['ID'];
315 0 : var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
316 0 : var senderHandle = messageWrapper['PeerID'];
317 0 : var senderImage = messageWrapper['ContactImage'];
318 0 : var attributes = messageWrapper['Attributes'];
319 0 : var ackd = messageWrapper['Acknowledged'];
320 0 : var error = messageWrapper['Error'] != null;
321 0 : var signature = messageWrapper['Signature'];
322 0 : var contenthash = messageWrapper['ContentHash'];
323 0 : var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false, contenthash);
324 0 : var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
325 :
326 : return messageInfo;
327 : }
328 :
329 : class MessageMetadata extends ChangeNotifier {
330 : // meta-metadata
331 : final String profileOnion;
332 : final int conversationIdentifier;
333 : final int messageID;
334 :
335 : final DateTime timestamp;
336 : final String senderHandle;
337 : final String? senderImage;
338 : final dynamic _attributes;
339 : bool _ackd;
340 : bool _error;
341 : final bool isAuto;
342 :
343 : final String? signature;
344 : final String contenthash;
345 : DateTime? lastChecked;
346 :
347 0 : dynamic get attributes => this._attributes;
348 :
349 0 : bool get ackd => this._ackd;
350 :
351 : String translation = "";
352 0 : void updateTranslationEvent(String translation) {
353 0 : this.translation += translation;
354 0 : notifyListeners();
355 : }
356 :
357 0 : set ackd(bool newVal) {
358 0 : this._ackd = newVal;
359 0 : notifyListeners();
360 : }
361 :
362 0 : bool get error => this._error;
363 :
364 0 : set error(bool newVal) {
365 0 : this._error = newVal;
366 0 : notifyListeners();
367 : }
368 :
369 0 : MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error,
370 : this.isAuto, this.contenthash);
371 : }
|