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 && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
547 final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
548 final boolean duplicate = conversation.hasDuplicateMessage(message);
549 if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
550 Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
551 synchronized (replacedMessage) {
552 final String uuid = replacedMessage.getUuid();
553 replacedMessage.setUuid(UUID.randomUUID().toString());
554 replacedMessage.setBody(message.getBody());
555 replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
556 replacedMessage.setRemoteMsgId(remoteMsgId);
557 if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
558 replacedMessage.setServerMsgId(message.getServerMsgId());
559 }
560 replacedMessage.setEncryption(message.getEncryption());
561 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
562 replacedMessage.markUnread();
563 }
564 extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
565 mXmppConnectionService.updateMessage(replacedMessage, uuid);
566 if (mXmppConnectionService.confirmMessages()
567 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
568 && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
569 && remoteMsgId != null
570 && !selfAddressed
571 && !isTypeGroupChat) {
572 processMessageReceipts(account, packet, query);
573 }
574 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
575 conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
576 conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
577 }
578 }
579 mXmppConnectionService.getNotificationService().updateNotification();
580 return;
581 } else {
582 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
583 }
584 }
585 }
586
587 long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
588 if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
589 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
590 return;
591 }
592
593 boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
594 || message.isPrivateMessage()
595 || message.getServerMsgId() != null
596 || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
597 if (checkForDuplicates) {
598 final Message duplicate = conversation.findDuplicateMessage(message);
599 if (duplicate != null) {
600 final boolean serverMsgIdUpdated;
601 if (duplicate.getStatus() != Message.STATUS_RECEIVED
602 && duplicate.getUuid().equals(message.getRemoteMsgId())
603 && duplicate.getServerMsgId() == null
604 && message.getServerMsgId() != null) {
605 duplicate.setServerMsgId(message.getServerMsgId());
606 if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
607 serverMsgIdUpdated = true;
608 } else {
609 serverMsgIdUpdated = false;
610 Log.e(Config.LOGTAG, "failed to update message");
611 }
612 } else {
613 serverMsgIdUpdated = false;
614 }
615 Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
616 return;
617 }
618 }
619
620 if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
621 conversation.prepend(query.getActualInThisQuery(), message);
622 } else {
623 conversation.add(message);
624 }
625 if (query != null) {
626 query.incrementActualMessageCount();
627 }
628
629 if (query == null || query.isCatchup()) { //either no mam or catchup
630 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
631 mXmppConnectionService.markRead(conversation);
632 if (query == null) {
633 activateGracePeriod(account);
634 }
635 } else {
636 message.markUnread();
637 notify = true;
638 }
639 }
640
641 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
642 notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
643 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
644 notify = false;
645 }
646
647 if (query == null) {
648 extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
649 mXmppConnectionService.updateConversationUi();
650 }
651
652 if (mXmppConnectionService.confirmMessages()
653 && message.getStatus() == Message.STATUS_RECEIVED
654 && (message.trusted() || message.isPrivateMessage())
655 && remoteMsgId != null
656 && !selfAddressed
657 && !isTypeGroupChat) {
658 processMessageReceipts(account, packet, query);
659 }
660
661 mXmppConnectionService.databaseBackend.createMessage(message);
662 final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
663 if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
664 manager.createNewDownloadConnection(message);
665 } else if (notify) {
666 if (query != null && query.isCatchup()) {
667 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
668 } else {
669 mXmppConnectionService.getNotificationService().push(message);
670 }
671 }
672 } else if (!packet.hasChild("body")) { //no body
673
674 final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
675 if (axolotlEncrypted != null) {
676 Jid origin;
677 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
678 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
679 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
680 if (origin == null) {
681 Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
682 return;
683 }
684 } else if (isTypeGroupChat) {
685 return;
686 } else {
687 origin = from;
688 }
689 try {
690 final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
691 account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
692 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
693 } catch (Exception e) {
694 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
695 return;
696 }
697 }
698
699 if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
700 mXmppConnectionService.updateConversationUi();
701 }
702
703 if (isTypeGroupChat) {
704 if (packet.hasChild("subject")) {
705 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
706 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
707 String subject = packet.findInternationalizedChildContent("subject");
708 if (conversation.getMucOptions().setSubject(subject)) {
709 mXmppConnectionService.updateConversation(conversation);
710 }
711 mXmppConnectionService.updateConversationUi();
712 return;
713 }
714 }
715 }
716 if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
717 for (Element child : mucUserElement.getChildren()) {
718 if ("status".equals(child.getName())) {
719 try {
720 int code = Integer.parseInt(child.getAttribute("code"));
721 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
722 mXmppConnectionService.fetchConferenceConfiguration(conversation);
723 break;
724 }
725 } catch (Exception e) {
726 //ignored
727 }
728 } else if ("item".equals(child.getName())) {
729 MucOptions.User user = AbstractParser.parseItem(conversation, child);
730 Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
731 + user.getRealJid() + " to " + user.getAffiliation() + " in "
732 + conversation.getJid().asBareJid());
733 if (!user.realJidMatchesAccount()) {
734 boolean isNew = conversation.getMucOptions().updateUser(user);
735 mXmppConnectionService.getAvatarService().clear(conversation);
736 mXmppConnectionService.updateMucRosterUi();
737 mXmppConnectionService.updateConversationUi();
738 Contact contact = user.getContact();
739 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
740 Jid jid = user.getRealJid();
741 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
742 if (cryptoTargets.remove(user.getRealJid())) {
743 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
744 conversation.setAcceptedCryptoTargets(cryptoTargets);
745 mXmppConnectionService.updateConversation(conversation);
746 }
747 } else if (isNew
748 && user.getRealJid() != null
749 && conversation.getMucOptions().isPrivateAndNonAnonymous()
750 && (contact == null || !contact.mutualPresenceSubscription())
751 && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
752 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
753 }
754 }
755 }
756 }
757 }
758 }
759
760 Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
761 if (received == null) {
762 received = packet.findChild("received", "urn:xmpp:receipts");
763 }
764 if (received != null) {
765 String id = received.getAttribute("id");
766 if (packet.fromAccount(account)) {
767 if (query != null && id != null && packet.getTo() != null) {
768 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
769 }
770 } else {
771 mXmppConnectionService.markMessage(account, from.asBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
772 }
773 }
774 Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
775 if (displayed != null) {
776 final String id = displayed.getAttribute("id");
777 final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
778 if (packet.fromAccount(account) && !selfAddressed) {
779 dismissNotification(account, counterpart, query);
780 } else if (isTypeGroupChat) {
781 Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
782 if (conversation != null && id != null && sender != null) {
783 Message message = conversation.findMessageWithRemoteId(id, sender);
784 if (message != null) {
785 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
786 final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
787 final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
788 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
789 if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
790 mXmppConnectionService.markRead(conversation);
791 }
792 } else if (!counterpart.isBareJid() && trueJid != null) {
793 ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
794 if (message.addReadByMarker(readByMarker)) {
795 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
796 mXmppConnectionService.updateMessage(message, false);
797 }
798 }
799 }
800 }
801 } else {
802 final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
803 Message message = displayedMessage == null ? null : displayedMessage.prev();
804 while (message != null
805 && message.getStatus() == Message.STATUS_SEND_RECEIVED
806 && message.getTimeSent() < displayedMessage.getTimeSent()) {
807 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
808 message = message.prev();
809 }
810 if (displayedMessage != null && selfAddressed) {
811 dismissNotification(account, counterpart, query);
812 }
813 }
814 }
815
816 final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
817 if (event != null && InvalidJid.hasValidFrom(original)) {
818 if (event.hasChild("items")) {
819 parseEvent(event, original.getFrom(), account);
820 } else if (event.hasChild("delete")) {
821 parseDeleteEvent(event, original.getFrom(), account);
822 }
823 }
824
825 final String nick = packet.findChildContent("nick", Namespace.NICK);
826 if (nick != null && InvalidJid.hasValidFrom(original)) {
827 Contact contact = account.getRoster().getContact(from);
828 if (contact.setPresenceName(nick)) {
829 mXmppConnectionService.getAvatarService().clear(contact);
830 }
831 }
832 }
833
834 private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query) {
835 Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
836 if (conversation != null && (query == null || query.isCatchup())) {
837 mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
838 }
839 }
840
841 private void processMessageReceipts(Account account, MessagePacket packet, MessageArchiveService.Query query) {
842 final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
843 final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
844 if (query == null) {
845 final ArrayList<String> receiptsNamespaces = new ArrayList<>();
846 if (markable) {
847 receiptsNamespaces.add("urn:xmpp:chat-markers:0");
848 }
849 if (request) {
850 receiptsNamespaces.add("urn:xmpp:receipts");
851 }
852 if (receiptsNamespaces.size() > 0) {
853 MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
854 packet,
855 receiptsNamespaces,
856 packet.getType());
857 mXmppConnectionService.sendMessagePacket(account, receipt);
858 }
859 } else if (query.isCatchup()) {
860 if (request) {
861 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), packet.getId()));
862 }
863 }
864 }
865
866 private void activateGracePeriod(Account account) {
867 long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
868 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
869 account.activateGracePeriod(duration);
870 }
871
872 private class Invite {
873 final Jid jid;
874 final String password;
875 final Contact inviter;
876
877 Invite(Jid jid, String password, Contact inviter) {
878 this.jid = jid;
879 this.password = password;
880 this.inviter = inviter;
881 }
882
883 public boolean execute(Account account) {
884 if (jid != null) {
885 Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
886 if (conversation.getMucOptions().online()) {
887 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received invite to "+jid+" but muc is considered to be online");
888 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
889 } else {
890 conversation.getMucOptions().setPassword(password);
891 mXmppConnectionService.databaseBackend.updateConversation(conversation);
892 mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
893 mXmppConnectionService.updateConversationUi();
894 }
895 return true;
896 }
897 return false;
898 }
899 }
900}