1package eu.siacs.conversations.parser;
2
3import android.util.Log;
4import android.util.Pair;
5
6import java.net.URL;
7import java.text.SimpleDateFormat;
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.Date;
11import java.util.List;
12import java.util.Locale;
13import java.util.Set;
14import java.util.UUID;
15
16import eu.siacs.conversations.Config;
17import eu.siacs.conversations.R;
18import eu.siacs.conversations.crypto.axolotl.AxolotlService;
19import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
20import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
21import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
22import eu.siacs.conversations.entities.Account;
23import eu.siacs.conversations.entities.Contact;
24import eu.siacs.conversations.entities.Conversation;
25import eu.siacs.conversations.entities.Conversational;
26import eu.siacs.conversations.entities.Message;
27import eu.siacs.conversations.entities.MucOptions;
28import eu.siacs.conversations.entities.ReadByMarker;
29import eu.siacs.conversations.entities.ReceiptRequest;
30import eu.siacs.conversations.http.HttpConnectionManager;
31import eu.siacs.conversations.http.P1S3UrlStreamHandler;
32import eu.siacs.conversations.services.MessageArchiveService;
33import eu.siacs.conversations.services.QuickConversationsService;
34import eu.siacs.conversations.services.XmppConnectionService;
35import eu.siacs.conversations.utils.CryptoHelper;
36import eu.siacs.conversations.xml.Namespace;
37import eu.siacs.conversations.xml.Element;
38import eu.siacs.conversations.xmpp.InvalidJid;
39import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
40import eu.siacs.conversations.xmpp.chatstate.ChatState;
41import eu.siacs.conversations.xmpp.pep.Avatar;
42import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
43import rocks.xmpp.addr.Jid;
44
45public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
46
47 private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
48
49 public MessageParser(XmppConnectionService service) {
50 super(service);
51 }
52
53 private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
54 final Jid by;
55 final boolean safeToExtract;
56 if (isTypeGroupChat) {
57 by = conversation.getJid().asBareJid();
58 safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
59 } else {
60 Account account = conversation.getAccount();
61 by = account.getJid().asBareJid();
62 safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
63 }
64 return safeToExtract ? extractStanzaId(packet, by) : null;
65 }
66
67 private static String extractStanzaId(Element packet, Jid by) {
68 for (Element child : packet.getChildren()) {
69 if (child.getName().equals("stanza-id")
70 && Namespace.STANZA_IDS.equals(child.getNamespace())
71 && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
72 return child.getAttribute("id");
73 }
74 }
75 return null;
76 }
77
78 private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
79 final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
80 Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
81 return result != null ? result : fallback;
82 }
83
84 private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
85 ChatState state = ChatState.parse(packet);
86 if (state != null && c != null) {
87 final Account account = c.getAccount();
88 Jid from = packet.getFrom();
89 if (from.asBareJid().equals(account.getJid().asBareJid())) {
90 c.setOutgoingChatState(state);
91 if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
92 mXmppConnectionService.markRead(c);
93 activateGracePeriod(account);
94 }
95 return false;
96 } else {
97 if (isTypeGroupChat) {
98 MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
99 if (user != null) {
100 return user.setChatState(state);
101 } else {
102 return false;
103 }
104 } else {
105 return c.setIncomingChatState(state);
106 }
107 }
108 }
109 return false;
110 }
111
112 private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean checkedForDuplicates, boolean postpone) {
113 final AxolotlService service = conversation.getAccount().getAxolotlService();
114 final XmppAxolotlMessage xmppAxolotlMessage;
115 try {
116 xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
117 } catch (Exception e) {
118 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage());
119 return null;
120 }
121 if (xmppAxolotlMessage.hasPayload()) {
122 final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
123 try {
124 plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
125 } catch (BrokenSessionException e) {
126 if (checkedForDuplicates) {
127 service.reportBrokenSessionException(e, postpone);
128 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
129 } else {
130 Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed");
131 //TODO should be still emit a failed message?
132 return null;
133 }
134 } catch (NotEncryptedForThisDeviceException e) {
135 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
136 }
137 if (plaintextMessage != null) {
138 Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
139 finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
140 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
141 return finishedMessage;
142 }
143 } else {
144 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message");
145 service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
146 }
147 return null;
148 }
149
150 private Invite extractInvite(Account account, Element message) {
151 Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
152 if (x != null) {
153 Element invite = x.findChild("invite");
154 if (invite != null) {
155 String password = x.findChildContent("password");
156 Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
157 Contact contact = from == null ? null : account.getRoster().getContact(from);
158 Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
159 if (room == null) {
160 return null;
161 }
162 return new Invite(room, password, contact);
163 }
164 } else {
165 x = message.findChild("x", "jabber:x:conference");
166 if (x != null) {
167 Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
168 Contact contact = from == null ? null : account.getRoster().getContact(from);
169 Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid"));
170 if (room == null) {
171 return null;
172 }
173 return new Invite(room, x.getAttribute("password"), contact);
174 }
175 }
176 return null;
177 }
178
179 private void parseEvent(final Element event, final Jid from, final Account account) {
180 Element items = event.findChild("items");
181 String node = items == null ? null : items.getAttribute("node");
182 if ("urn:xmpp:avatar:metadata".equals(node)) {
183 Avatar avatar = Avatar.parseMetadata(items);
184 if (avatar != null) {
185 avatar.owner = from.asBareJid();
186 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
187 if (account.getJid().asBareJid().equals(from)) {
188 if (account.setAvatar(avatar.getFilename())) {
189 mXmppConnectionService.databaseBackend.updateAccount(account);
190 mXmppConnectionService.notifyAccountAvatarHasChanged(account);
191 }
192 mXmppConnectionService.getAvatarService().clear(account);
193 mXmppConnectionService.updateConversationUi();
194 mXmppConnectionService.updateAccountUi();
195 } else {
196 Contact contact = account.getRoster().getContact(from);
197 if (contact.setAvatar(avatar)) {
198 mXmppConnectionService.syncRoster(account);
199 mXmppConnectionService.getAvatarService().clear(contact);
200 mXmppConnectionService.updateConversationUi();
201 mXmppConnectionService.updateRosterUi();
202 }
203 }
204 } else if (mXmppConnectionService.isDataSaverDisabled()) {
205 mXmppConnectionService.fetchAvatar(account, avatar);
206 }
207 }
208 } else if (Namespace.NICK.equals(node)) {
209 final Element i = items.findChild("item");
210 final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
211 if (nick != null) {
212 setNick(account, from, nick);
213 }
214 } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
215 Element item = items.findChild("item");
216 Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
217 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
218 AxolotlService axolotlService = account.getAxolotlService();
219 axolotlService.registerDevices(from, deviceIds);
220 } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
221 if (account.getXmppConnection().getFeatures().bookmarksConversion()) {
222 final Element i = items.findChild("item");
223 final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
224 mXmppConnectionService.processBookmarks(account, storage, true);
225 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": processing bookmark PEP event");
226 } else {
227 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring bookmark PEP event because bookmark conversion was not detected");
228 }
229 }
230 }
231
232 private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
233 final Element delete = event.findChild("delete");
234 if (delete == null) {
235 return;
236 }
237 String node = delete.getAttribute("node");
238 if (Namespace.NICK.equals(node)) {
239 Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
240 setNick(account, from, null);
241 }
242 }
243
244 private void setNick(Account account, Jid user, String nick) {
245 if (user.asBareJid().equals(account.getJid().asBareJid())) {
246 account.setDisplayName(nick);
247 if (QuickConversationsService.isQuicksy()) {
248 mXmppConnectionService.getAvatarService().clear(account);
249 }
250 } else {
251 Contact contact = account.getRoster().getContact(user);
252 if (contact.setPresenceName(nick)) {
253 mXmppConnectionService.getAvatarService().clear(contact);
254 }
255 }
256 mXmppConnectionService.updateConversationUi();
257 mXmppConnectionService.updateAccountUi();
258 }
259
260 private boolean handleErrorMessage(Account account, MessagePacket packet) {
261 if (packet.getType() == MessagePacket.TYPE_ERROR) {
262 Jid from = packet.getFrom();
263 if (from != null) {
264 mXmppConnectionService.markMessage(account,
265 from.asBareJid(),
266 packet.getId(),
267 Message.STATUS_SEND_FAILED,
268 extractErrorMessage(packet));
269 final Element error = packet.findChild("error");
270 final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found"));
271 if (pingWorthyError) {
272 Conversation conversation = mXmppConnectionService.find(account,from);
273 if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
274 if (conversation.getMucOptions().online()) {
275 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received ping worthy error for seemingly online muc at "+from);
276 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
277 }
278 }
279 }
280 }
281 return true;
282 }
283 return false;
284 }
285
286 @Override
287 public void onMessagePacketReceived(Account account, MessagePacket original) {
288 if (handleErrorMessage(account, original)) {
289 return;
290 }
291 final MessagePacket packet;
292 Long timestamp = null;
293 boolean isCarbon = false;
294 String serverMsgId = null;
295 final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
296 if (fin != null) {
297 mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
298 return;
299 }
300 final Element result = MessageArchiveService.Version.findResult(original);
301 final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid"));
302 if (query != null && query.validFrom(original.getFrom())) {
303 Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
304 if (f == null) {
305 return;
306 }
307 timestamp = f.second;
308 packet = f.first;
309 serverMsgId = result.getAttribute("id");
310 query.incrementMessageCount();
311 } else if (query != null) {
312 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result from invalid sender");
313 return;
314 } else if (original.fromServer(account)) {
315 Pair<MessagePacket, Long> f;
316 f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
317 f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
318 packet = f != null ? f.first : original;
319 if (handleErrorMessage(account, packet)) {
320 return;
321 }
322 timestamp = f != null ? f.second : null;
323 isCarbon = f != null;
324 } else {
325 packet = original;
326 }
327
328 if (timestamp == null) {
329 timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
330 }
331 final String body = packet.getBody();
332 final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
333 final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
334 final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
335 final Element oob = packet.findChild("x", Namespace.OOB);
336 final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
337 final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
338 final String oobUrl = oob != null ? oob.findChildContent("url") : null;
339 final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
340 final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
341 int status;
342 final Jid counterpart;
343 final Jid to = packet.getTo();
344 final Jid from = packet.getFrom();
345 final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
346 final String remoteMsgId;
347 if (originId != null && originId.getAttribute("id") != null) {
348 remoteMsgId = originId.getAttribute("id");
349 } else {
350 remoteMsgId = packet.getId();
351 }
352 boolean notify = false;
353
354 if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
355 Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
356 return;
357 }
358
359 boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
360 if (query != null && !query.muc() && isTypeGroupChat) {
361 Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
362 return;
363 }
364 boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
365 boolean selfAddressed;
366 if (packet.fromAccount(account)) {
367 status = Message.STATUS_SEND;
368 selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
369 if (selfAddressed) {
370 counterpart = from;
371 } else {
372 counterpart = to != null ? to : account.getJid();
373 }
374 } else {
375 status = Message.STATUS_RECEIVED;
376 counterpart = from;
377 selfAddressed = false;
378 }
379
380 Invite invite = extractInvite(account, packet);
381 if (invite != null && invite.execute(account)) {
382 return;
383 }
384
385 if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
386 final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain());
387 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
388 final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
389
390 if (serverMsgId == null) {
391 serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
392 }
393
394
395 if (selfAddressed) {
396 if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
397 return;
398 }
399 status = Message.STATUS_RECEIVED;
400 if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
401 return;
402 }
403 }
404
405 if (isTypeGroupChat) {
406 if (conversation.getMucOptions().isSelf(counterpart)) {
407 status = Message.STATUS_SEND_RECEIVED;
408 isCarbon = true; //not really carbon but received from another resource
409 if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
410 return;
411 } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
412 Message message = conversation.findSentMessageWithBody(packet.getBody());
413 if (message != null) {
414 mXmppConnectionService.markMessage(message, status);
415 return;
416 }
417 }
418 } else {
419 status = Message.STATUS_RECEIVED;
420 }
421 }
422 final Message message;
423 if (xP1S3url != null) {
424 message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
425 message.setOob(true);
426 if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
427 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
428 }
429 } else if (pgpEncrypted != null && Config.supportOpenPgp()) {
430 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
431 } else if (axolotlEncrypted != null && Config.supportOmemo()) {
432 Jid origin;
433 Set<Jid> fallbacksBySourceId = Collections.emptySet();
434 if (conversationMultiMode) {
435 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
436 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
437 if (origin == null) {
438 try {
439 fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
440 } catch (IllegalArgumentException e) {
441 //ignoring
442 }
443 }
444 if (origin == null && fallbacksBySourceId.size() == 0) {
445 Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
446 return;
447 }
448 } else {
449 fallbacksBySourceId = Collections.emptySet();
450 origin = from;
451 }
452
453 //TODO either or is probably fine?
454 final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId);
455
456 if (origin != null) {
457 message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates,query != null);
458 } else {
459 Message trial = null;
460 for (Jid fallback : fallbacksBySourceId) {
461 trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, checkedForDuplicates && fallbacksBySourceId.size() == 1, query != null);
462 if (trial != null) {
463 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
464 origin = fallback;
465 break;
466 }
467 }
468 message = trial;
469 }
470 if (message == null) {
471 if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
472 mXmppConnectionService.updateConversationUi();
473 }
474 if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
475 Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
476 if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
477 previouslySent.setServerMsgId(serverMsgId);
478 mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
479 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
480 }
481 }
482 return;
483 }
484 if (conversationMultiMode) {
485 message.setTrueCounterpart(origin);
486 }
487 } else if (body == null && oobUrl != null) {
488 message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
489 message.setOob(true);
490 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
491 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
492 }
493 } else {
494 message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
495 }
496
497 message.setCounterpart(counterpart);
498 message.setRemoteMsgId(remoteMsgId);
499 message.setServerMsgId(serverMsgId);
500 message.setCarbon(isCarbon);
501 message.setTime(timestamp);
502 if (body != null && body.equals(oobUrl)) {
503 message.setOob(true);
504 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
505 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
506 }
507 }
508 message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
509 if (conversationMultiMode) {
510 message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
511 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
512 Jid trueCounterpart;
513 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
514 trueCounterpart = message.getTrueCounterpart();
515 } else if (query != null && query.safeToExtractTrueCounterpart()) {
516 trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
517 } else {
518 trueCounterpart = fallback;
519 }
520 if (trueCounterpart != null && isTypeGroupChat) {
521 if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
522 status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
523 } else {
524 status = Message.STATUS_RECEIVED;
525 message.setCarbon(false);
526 }
527 }
528 message.setStatus(status);
529 message.setTrueCounterpart(trueCounterpart);
530 if (!isTypeGroupChat) {
531 message.setType(Message.TYPE_PRIVATE);
532 }
533 } else {
534 updateLastseen(account, from);
535 }
536
537 if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
538 final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
539 counterpart,
540 message.getStatus() == Message.STATUS_RECEIVED,
541 message.isCarbon());
542 if (replacedMessage != null) {
543 final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
544 || replacedMessage.getFingerprint().equals(message.getFingerprint());
545 final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
546 && message.getTrueCounterpart() != null
547 && replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
548 final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
549 final boolean duplicate = conversation.hasDuplicateMessage(message);
550 if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
551 Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
552 synchronized (replacedMessage) {
553 final String uuid = replacedMessage.getUuid();
554 replacedMessage.setUuid(UUID.randomUUID().toString());
555 replacedMessage.setBody(message.getBody());
556 replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
557 replacedMessage.setRemoteMsgId(remoteMsgId);
558 if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
559 replacedMessage.setServerMsgId(message.getServerMsgId());
560 }
561 replacedMessage.setEncryption(message.getEncryption());
562 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
563 replacedMessage.markUnread();
564 }
565 extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
566 mXmppConnectionService.updateMessage(replacedMessage, uuid);
567 if (mXmppConnectionService.confirmMessages()
568 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
569 && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
570 && remoteMsgId != null
571 && !selfAddressed
572 && !isTypeGroupChat) {
573 processMessageReceipts(account, packet, query);
574 }
575 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
576 conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
577 conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
578 }
579 }
580 mXmppConnectionService.getNotificationService().updateNotification();
581 return;
582 } else {
583 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
584 }
585 }
586 }
587
588 long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
589 if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
590 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
591 return;
592 }
593
594 boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
595 || message.isPrivateMessage()
596 || message.getServerMsgId() != null
597 || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
598 if (checkForDuplicates) {
599 final Message duplicate = conversation.findDuplicateMessage(message);
600 if (duplicate != null) {
601 final boolean serverMsgIdUpdated;
602 if (duplicate.getStatus() != Message.STATUS_RECEIVED
603 && duplicate.getUuid().equals(message.getRemoteMsgId())
604 && duplicate.getServerMsgId() == null
605 && message.getServerMsgId() != null) {
606 duplicate.setServerMsgId(message.getServerMsgId());
607 if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
608 serverMsgIdUpdated = true;
609 } else {
610 serverMsgIdUpdated = false;
611 Log.e(Config.LOGTAG, "failed to update message");
612 }
613 } else {
614 serverMsgIdUpdated = false;
615 }
616 Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
617 return;
618 }
619 }
620
621 if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
622 conversation.prepend(query.getActualInThisQuery(), message);
623 } else {
624 conversation.add(message);
625 }
626 if (query != null) {
627 query.incrementActualMessageCount();
628 }
629
630 if (query == null || query.isCatchup()) { //either no mam or catchup
631 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
632 mXmppConnectionService.markRead(conversation);
633 if (query == null) {
634 activateGracePeriod(account);
635 }
636 } else {
637 message.markUnread();
638 notify = true;
639 }
640 }
641
642 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
643 notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
644 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
645 notify = false;
646 }
647
648 if (query == null) {
649 extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
650 mXmppConnectionService.updateConversationUi();
651 }
652
653 if (mXmppConnectionService.confirmMessages()
654 && message.getStatus() == Message.STATUS_RECEIVED
655 && (message.trusted() || message.isPrivateMessage())
656 && remoteMsgId != null
657 && !selfAddressed
658 && !isTypeGroupChat) {
659 processMessageReceipts(account, packet, query);
660 }
661
662 mXmppConnectionService.databaseBackend.createMessage(message);
663 final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
664 if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
665 manager.createNewDownloadConnection(message);
666 } else if (notify) {
667 if (query != null && query.isCatchup()) {
668 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
669 } else {
670 mXmppConnectionService.getNotificationService().push(message);
671 }
672 }
673 } else if (!packet.hasChild("body")) { //no body
674
675 final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
676 if (axolotlEncrypted != null) {
677 Jid origin;
678 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
679 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
680 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
681 if (origin == null) {
682 Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
683 return;
684 }
685 } else if (isTypeGroupChat) {
686 return;
687 } else {
688 origin = from;
689 }
690 try {
691 final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
692 account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
693 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
694 } catch (Exception e) {
695 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
696 return;
697 }
698 }
699
700 if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
701 mXmppConnectionService.updateConversationUi();
702 }
703
704 if (isTypeGroupChat) {
705 if (packet.hasChild("subject")) {
706 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
707 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
708 String subject = packet.findInternationalizedChildContent("subject");
709 if (conversation.getMucOptions().setSubject(subject)) {
710 mXmppConnectionService.updateConversation(conversation);
711 }
712 mXmppConnectionService.updateConversationUi();
713 return;
714 }
715 }
716 }
717 if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
718 for (Element child : mucUserElement.getChildren()) {
719 if ("status".equals(child.getName())) {
720 try {
721 int code = Integer.parseInt(child.getAttribute("code"));
722 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
723 mXmppConnectionService.fetchConferenceConfiguration(conversation);
724 break;
725 }
726 } catch (Exception e) {
727 //ignored
728 }
729 } else if ("item".equals(child.getName())) {
730 MucOptions.User user = AbstractParser.parseItem(conversation, child);
731 Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
732 + user.getRealJid() + " to " + user.getAffiliation() + " in "
733 + conversation.getJid().asBareJid());
734 if (!user.realJidMatchesAccount()) {
735 boolean isNew = conversation.getMucOptions().updateUser(user);
736 mXmppConnectionService.getAvatarService().clear(conversation);
737 mXmppConnectionService.updateMucRosterUi();
738 mXmppConnectionService.updateConversationUi();
739 Contact contact = user.getContact();
740 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
741 Jid jid = user.getRealJid();
742 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
743 if (cryptoTargets.remove(user.getRealJid())) {
744 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
745 conversation.setAcceptedCryptoTargets(cryptoTargets);
746 mXmppConnectionService.updateConversation(conversation);
747 }
748 } else if (isNew
749 && user.getRealJid() != null
750 && conversation.getMucOptions().isPrivateAndNonAnonymous()
751 && (contact == null || !contact.mutualPresenceSubscription())
752 && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
753 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
754 }
755 }
756 }
757 }
758 }
759 }
760
761 Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
762 if (received == null) {
763 received = packet.findChild("received", "urn:xmpp:receipts");
764 }
765 if (received != null) {
766 String id = received.getAttribute("id");
767 if (packet.fromAccount(account)) {
768 if (query != null && id != null && packet.getTo() != null) {
769 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
770 }
771 } else {
772 mXmppConnectionService.markMessage(account, from.asBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
773 }
774 }
775 Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
776 if (displayed != null) {
777 final String id = displayed.getAttribute("id");
778 final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
779 if (packet.fromAccount(account) && !selfAddressed) {
780 dismissNotification(account, counterpart, query);
781 } else if (isTypeGroupChat) {
782 Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
783 if (conversation != null && id != null && sender != null) {
784 Message message = conversation.findMessageWithRemoteId(id, sender);
785 if (message != null) {
786 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
787 final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
788 final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
789 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
790 if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
791 mXmppConnectionService.markRead(conversation);
792 }
793 } else if (!counterpart.isBareJid() && trueJid != null) {
794 ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
795 if (message.addReadByMarker(readByMarker)) {
796 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
797 mXmppConnectionService.updateMessage(message, false);
798 }
799 }
800 }
801 }
802 } else {
803 final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
804 Message message = displayedMessage == null ? null : displayedMessage.prev();
805 while (message != null
806 && message.getStatus() == Message.STATUS_SEND_RECEIVED
807 && message.getTimeSent() < displayedMessage.getTimeSent()) {
808 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
809 message = message.prev();
810 }
811 if (displayedMessage != null && selfAddressed) {
812 dismissNotification(account, counterpart, query);
813 }
814 }
815 }
816
817 final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
818 if (event != null && InvalidJid.hasValidFrom(original)) {
819 if (event.hasChild("items")) {
820 parseEvent(event, original.getFrom(), account);
821 } else if (event.hasChild("delete")) {
822 parseDeleteEvent(event, original.getFrom(), account);
823 }
824 }
825
826 final String nick = packet.findChildContent("nick", Namespace.NICK);
827 if (nick != null && InvalidJid.hasValidFrom(original)) {
828 Contact contact = account.getRoster().getContact(from);
829 if (contact.setPresenceName(nick)) {
830 mXmppConnectionService.getAvatarService().clear(contact);
831 }
832 }
833 }
834
835 private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query) {
836 Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
837 if (conversation != null && (query == null || query.isCatchup())) {
838 mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
839 }
840 }
841
842 private void processMessageReceipts(Account account, MessagePacket packet, MessageArchiveService.Query query) {
843 final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
844 final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
845 if (query == null) {
846 final ArrayList<String> receiptsNamespaces = new ArrayList<>();
847 if (markable) {
848 receiptsNamespaces.add("urn:xmpp:chat-markers:0");
849 }
850 if (request) {
851 receiptsNamespaces.add("urn:xmpp:receipts");
852 }
853 if (receiptsNamespaces.size() > 0) {
854 MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
855 packet,
856 receiptsNamespaces,
857 packet.getType());
858 mXmppConnectionService.sendMessagePacket(account, receipt);
859 }
860 } else if (query.isCatchup()) {
861 if (request) {
862 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), packet.getId()));
863 }
864 }
865 }
866
867 private void activateGracePeriod(Account account) {
868 long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
869 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
870 account.activateGracePeriod(duration);
871 }
872
873 private class Invite {
874 final Jid jid;
875 final String password;
876 final Contact inviter;
877
878 Invite(Jid jid, String password, Contact inviter) {
879 this.jid = jid;
880 this.password = password;
881 this.inviter = inviter;
882 }
883
884 public boolean execute(Account account) {
885 if (jid != null) {
886 Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
887 if (conversation.getMucOptions().online()) {
888 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received invite to "+jid+" but muc is considered to be online");
889 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
890 } else {
891 conversation.getMucOptions().setPassword(password);
892 mXmppConnectionService.databaseBackend.updateConversation(conversation);
893 mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
894 mXmppConnectionService.updateConversationUi();
895 }
896 return true;
897 }
898 return false;
899 }
900 }
901}