another.im-ios/Monal/Classes/MLMessageProcessor.m
2024-11-18 15:53:52 +01:00

885 lines
50 KiB
Objective-C

//
// MLMessageProcessor.m
// Monal
//
// Created by Anurodh Pokharel on 9/1/19.
// Copyright © 2019 Monal.im. All rights reserved.
//
#import "MLMessageProcessor.h"
#import "DataLayer.h"
#import "SignalAddress.h"
#import "HelperTools.h"
#import "AESGcm.h"
#import "MLConstants.h"
#import "MLImageManager.h"
#import "XMPPIQ.h"
#import "MLPubSub.h"
#import "MLOMEMO.h"
#import "MLFiletransfer.h"
#import "MLMucProcessor.h"
#import "MLNotificationQueue.h"
#import "MonalAppDelegate.h"
@interface MLPubSub ()
-(void) handleHeadlineMessage:(XMPPMessage*) messageNode;
@end
static NSMutableDictionary* _typingNotifications;
@implementation MLMessageProcessor
+(void) initialize
{
_typingNotifications = [NSMutableDictionary new];
}
+(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessage:(XMPPMessage*) outerMessageNode forAccount:(xmpp*) account
{
return [self processMessage:messageNode andOuterMessage:outerMessageNode forAccount:account withHistoryId:nil];
}
+(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessage:(XMPPMessage*) outerMessageNode forAccount:(xmpp*) account withHistoryId:(NSNumber* _Nullable) historyIdToUse
{
MLAssert(messageNode != nil, @"messageNode should not be nil!");
MLAssert(outerMessageNode != nil, @"outerMessageNode should not be nil!");
MLAssert(account != nil, @"account should not be nil!");
//this will be the return value f tis method
//(a valid MLMessage, if this was a new message added to the db or nil, if it was another stanza not added
//directly to the message_history table (but possibly altering it, e.g. marking someentr as read)
MLMessage* message = nil;
//history messages have already been collected mam-page wise and reordered before they are inserted into db db
//(that's because mam always sorts the messages in a page by timestamp in ascending order)
BOOL isMLhistory = NO;
if([outerMessageNode check:@"{urn:xmpp:mam:2}result"] && [[outerMessageNode findFirst:@"{urn:xmpp:mam:2}result@queryid"] hasPrefix:@"MLhistory:"])
isMLhistory = YES;
MLAssert(!isMLhistory || historyIdToUse != nil, @"processing of MLhistory: mam messages is only possible if a history id was given", (@{
@"isMLhistory": @(isMLhistory),
@"historyIdToUse": historyIdToUse != nil ? historyIdToUse : @"(nil)",
}));
if([messageNode check:@"/<type=error>"])
{
DDLogError(@"Error type message received");
if(![messageNode check:@"/@id"])
{
DDLogError(@"Ignoring error messages having an empty ID");
return nil;
}
NSString* errorType = [messageNode findFirst:@"error@type"];
if(!errorType)
errorType= @"unknown error";
NSString* errorReason = [messageNode findFirst:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}!text$"];
NSString* errorText = [messageNode findFirst:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}text#"];
DDLogInfo(@"Got errorType='%@', errorReason='%@', errorText='%@' for message '%@'", errorType, errorReason, errorText, messageNode.id);
if(errorReason)
errorType = [NSString stringWithFormat:@"%@ - %@", errorType, errorReason];
if(!errorText)
errorText = NSLocalizedString(@"No further error description", @"");
//update db
[[DataLayer sharedInstance]
setMessageId:messageNode.id
andJid:messageNode.fromUser
errorType:errorType
errorReason:errorText
];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalMessageErrorNotice object:nil userInfo:@{
kMessageId: messageNode.id,
@"jid": messageNode.fromUser,
@"errorType": errorType,
@"errorReason": errorText,
}];
return nil;
}
NSString* buddyName = [messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid] ? messageNode.toUser : messageNode.fromUser;
MLContact* possiblyUnknownContact = [MLContact createContactFromJid:buddyName andAccountID:account.accountID];
//ignore unknown contacts if configured to do so
if(![[HelperTools defaultsDB] boolForKey: @"allowNonRosterContacts"] && !possiblyUnknownContact.isSubscribedFrom)
{
DDLogWarn(@"Ignoring incoming message stanza from unknown contact: %@", possiblyUnknownContact);
return nil;
}
//ignore prosody mod_muc_notifications muc push stanzas (they are only needed to trigger an apns push)
//but trigger a muc ping for these mucs nonetheless (if this muc is known, we don't want to arbitrarily join mucs just because of this stanza)
if([messageNode check:@"{http://quobis.com/xmpp/muc#push}notification"])
{
NSString* roomJid = [messageNode findFirst:@"{http://quobis.com/xmpp/muc#push}notification@jid"];
if([[[DataLayer sharedInstance] listMucsForAccount:account.accountID] containsObject:roomJid])
[account.mucProcessor ping:roomJid];
return nil;
}
if([messageNode check:@"/<type=headline>/{http://jabber.org/protocol/pubsub#event}event"])
{
[account.pubsub handleHeadlineMessage:messageNode];
return nil;
}
//ignore messages from our own device, see this github issue: https://github.com/monal-im/Monal/issues/941
if(!isMLhistory && [messageNode.from isEqualToString:account.connectionProperties.identity.fullJid] && [messageNode.toUser isEqualToString:account.connectionProperties.identity.jid])
return nil;
//handle incoming jmi calls (TODO: add entry to local history, once the UI for this is implemented)
//only handle incoming propose messages if not older than 60 seconds
if([messageNode check:@"{urn:xmpp:jingle-message:0}*"] && ![HelperTools shouldProvideVoip])
{
DDLogWarn(@"VoIP not supported, ignoring incoming JMI message!");
return nil;
}
else if([messageNode check:@"{urn:xmpp:jingle-message:0}*"])
{
MLContact* jmiContact = [MLContact createContactFromJid:messageNode.fromUser andAccountID:account.accountID];
if([messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
jmiContact = [MLContact createContactFromJid:messageNode.toUser andAccountID:account.accountID];
//only handle *incoming* call proposals
if([messageNode check:@"{urn:xmpp:jingle-message:0}propose"])
{
if(![messageNode.toUser isEqualToString:account.connectionProperties.identity.jid])
{
//TODO: record this call in history db even if it was outgoing from another device on our account
DDLogWarn(@"Ignoring incoming JMI propose coming from another device on our account");
return nil;
}
//only handle jmi stanzas exchanged with contacts allowed to see us and ignore all others
//--> no presence leak and no unwanted spam calls
//but: outgoing calls are still allowed even without presence subscriptions in either direction
if(![[HelperTools defaultsDB] boolForKey:@"allowCallsFromNonRosterContacts"] && !jmiContact.isSubscribedFrom)
{
DDLogWarn(@"Ignoring incoming JMI propose coming from a contact we are not subscribed from");
return nil;
}
NSDate* delayStamp = [messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"];
if(delayStamp == nil)
delayStamp = [NSDate date];
if([[NSDate date] timeIntervalSinceDate:delayStamp] > 60.0)
{
DDLogWarn(@"Ignoring incoming JMI propose: too old");
return nil;
}
//only allow audio calls for now
if([messageNode check:@"{urn:xmpp:jingle-message:0}propose/{urn:xmpp:jingle:apps:rtp:1}description<media=audio>"])
{
DDLogInfo(@"Got incoming JMI propose");
NSDictionary* callData = @{
@"messageNode": messageNode,
@"accountID": account.accountID,
};
//this is needed because this file resides in the monalxmpp compilation unit while the MLVoipProcessor resides
//in the monal compilation unit (the ui unit), the NSE resides in yet another compilation unit (the nse-appex unit)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalIncomingVoipCall object:account userInfo:callData];
}
else
DDLogWarn(@"Ignoring incoming non-audio JMI call, not implemented yet");
return nil;
}
//handle all other JMI events (TODO: add entry to local history, once the UI for this is implemented)
//if the corresponding call is unknown these will just be ignored by MLVoipProcessor --> no presence leak
else
{
DDLogInfo(@"Got %@ for JMI call %@", [messageNode findFirst:@"{urn:xmpp:jingle-message:0}*$"], [messageNode findFirst:@"{urn:xmpp:jingle-message:0}*@id"]);
if([HelperTools isAppExtension])
DDLogWarn(@"Ignoring incoming JMI message: we are in the appex which means any outgoing or ongoing call was already terminated");
else
{
NSDictionary* callData = @{
@"messageNode": messageNode,
@"accountID": account.accountID,
};
//this is needed because this file resides in the monalxmpp compilation unit while the MLVoipProcessor resides
//in the monal compilation unit (the ui unit), the NSE resides in yet another compilation unit (the nse-appex unit)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalIncomingJMIStanza object:account userInfo:callData];
}
return nil;
}
}
//ignore muc PMs (after discussion with holger we don't want to support that)
if(
![messageNode check:@"/<type=groupchat>"] &&
[messageNode check:@"{http://jabber.org/protocol/muc#user}x"] &&
![messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"] &&
[messageNode check:@"body#"]
)
{
DDLogWarn(@"Ignoring muc pm marked as such...");
//ignore muc pms without id attribute (we can't send out errors pointing to this message without an id)
if(messageNode.id == nil)
return nil;
XMPPMessage* errorReply = [XMPPMessage new];
[errorReply.attributes setObject:@"error" forKey:@"type"];
[errorReply.attributes setObject:messageNode.from forKey:@"to"]; //this has to be the full jid here
[errorReply.attributes setObject:messageNode.id forKey:@"id"]; //don't set origin id here
[errorReply addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"cancel"} andChildren:@[
[[MLXMLNode alloc] initWithElement:@"feature-not-implemented" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
[[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" withAttributes:@{} andChildren:@[] andData:@"The receiver does not seem to support MUC-PMs"]
] andData:nil]];
[errorReply setStoreHint];
[account send:errorReply];
return nil;
}
//ignore carbon copied muc pms not marked as such
NSString* carbonType = [outerMessageNode findFirst:@"{urn:xmpp:carbons:2}*$"];
if(carbonType != nil)
{
NSString* maybeMucJid = [carbonType isEqualToString:@"sent"] ? messageNode.toUser : messageNode.fromUser;
MLContact* carbonTestContact = [MLContact createContactFromJid:maybeMucJid andAccountID:account.accountID];
if(carbonTestContact.isMuc)
{
DDLogWarn(@"Ignoring carbon copied muc pm...");
return nil;
}
else
DDLogVerbose(@"Not a carbon copy of a muc pm for contact: %@", carbonTestContact);
}
if(([messageNode check:@"/<type=groupchat>"] || [messageNode check:@"{http://jabber.org/protocol/muc#user}x"]) && ![messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"])
{
// Ignore all group chat msgs from unkown groups
if(![[[DataLayer sharedInstance] listMucsForAccount:account.accountID] containsObject:messageNode.fromUser])
{
// ignore message
DDLogWarn(@"Ignoring groupchat message from %@", messageNode.toUser);
return nil;
}
}
else
{
// handle KeyTransportMessages directly without adding a 1:1 buddy
if([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header"] == YES && [messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/payload#"] == NO)
{
if(!isMLhistory)
{
DDLogInfo(@"Handling KeyTransportElement without trying to add a 1:1 buddy %@", possiblyUnknownContact);
[account.omemo decryptMessage:messageNode withMucParticipantJid:nil];
}
else
DDLogInfo(@"Ignoring MLhistory KeyTransportElement for buddy %@", possiblyUnknownContact);
return nil;
}
}
NSString* stanzaid = [outerMessageNode findFirst:@"{urn:xmpp:mam:2}result@id"];
//check stanza-id @by according to the rules outlined in XEP-0359
if(!stanzaid)
{
if(!possiblyUnknownContact.isMuc && [messageNode check:@"{urn:xmpp:sid:0}stanza-id<by=%@>", account.connectionProperties.identity.jid])
stanzaid = [messageNode findFirst:@"{urn:xmpp:sid:0}stanza-id<by=%@>@id", account.connectionProperties.identity.jid];
else if(possiblyUnknownContact.isMuc && [messageNode check:@"{urn:xmpp:sid:0}stanza-id<by=%@>", messageNode.fromUser] && [[account.mucProcessor getRoomFeaturesForMuc:messageNode.fromUser] containsObject:@"urn:xmpp:sid:0"])
stanzaid = [messageNode findFirst:@"{urn:xmpp:sid:0}stanza-id<by=%@>@id", messageNode.fromUser];
}
//all modern clients using origin-id should use the same id for origin-id AND message id
NSString* messageId = [messageNode findFirst:@"{urn:xmpp:sid:0}origin-id@id"];
if(messageId == nil || !messageId.length)
messageId = messageNode.id;
if(messageId == nil || !messageId.length)
{
if([messageNode check:@"body#"])
DDLogWarn(@"Message containing body has an empty stanza ID, using random UUID instead");
else
DDLogVerbose(@"Empty stanza ID, using random UUID instead");
messageId = [[NSUUID UUID] UUIDString];
}
//handle muc status changes or invites (this checks for the muc namespace itself)
if(isMLhistory)
{
if([messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"] || ([messageNode check:@"{jabber:x:conference}x@jid"] && [[messageNode findFirst:@"{jabber:x:conference}x@jid"] length] > 0))
return nil; //stop processing because this is a (mediated) muc invite received through backscrolling history
else
; //continue processing for backscrolling history but don't call mucProcessor.processMessage to not process ancient status/memberlist updates
}
else if([account.mucProcessor processMessage:messageNode])
{
DDLogVerbose(@"Muc processor said we have to stop message processing here...");
return nil; //the muc processor said we have stop processing
}
//add contact if possible (ignore groupchats or already existing contacts, or KeyTransportElements)
DDLogInfo(@"Adding possibly unknown contact for %@ to local contactlist (not updating remote roster!), doing nothing if contact is already known...", possiblyUnknownContact);
[[DataLayer sharedInstance] addContact:possiblyUnknownContact.contactJid forAccount:account.accountID nickname:nil];
NSString* ownNick = nil;
NSString* ownOccupantId = nil;
NSString* actualFrom = messageNode.fromUser;
NSString* participantJid = nil;
NSString* occupantId = nil;
if(possiblyUnknownContact.isMuc)
{
actualFrom = messageNode.fromResource ?: @"";
ownNick = [[DataLayer sharedInstance] ownNickNameforMuc:messageNode.fromUser forAccount:account.accountID];
ownOccupantId = [[DataLayer sharedInstance] getOwnOccupantIdForMuc:messageNode.fromUser onAccountID:account.accountID];
//occupant ids are widely supported now and allow us to have a stable identifier of every muc participant,
//even if it is a semi-anonymous channel
if([[account.mucProcessor getRoomFeaturesForMuc:messageNode.fromUser] containsObject:@"urn:xmpp:occupant-id:0"] && [messageNode check:@"{urn:xmpp:occupant-id:0}occupant-id@id"])
{
occupantId = [messageNode findFirst:@"{urn:xmpp:occupant-id:0}occupant-id@id"];
NSDictionary* mucParticipant = [[DataLayer sharedInstance] getParticipantForOccupant:occupantId inRoom:messageNode.fromUser forAccountID:account.accountID];
//we will be able to get to know the real jid, if this is a group or we are the channel admin
participantJid = mucParticipant ? mucParticipant[@"participant_jid"] : nil;
}
//mam catchups will contain a muc#user item listing the jid of the participant
//this can't be reconstructed from *current* participant lists because someone new could have taken the same nick
//we don't accept this in non-mam context to make sure this can't be spoofed somehow
//we also don't do that, if this was a message from the bare muc jid
//NOTE: this will override the participantJid extracted using the occupantId above,
//NOTE: but those should ALWAYS be the same (that's the exact purpose of occupant ids)
if([outerMessageNode check:@"{urn:xmpp:mam:2}result"] && ![@"" isEqualToString:actualFrom])
participantJid = [messageNode findFirst:@"{http://jabber.org/protocol/muc#user}x/item@jid"];
//try to get the jid of the current participant if the occupant-id based approach above did not work
//but don't do so, if this was a message from the bare muc jid
if(![outerMessageNode check:@"{urn:xmpp:mam:2}result"] && occupantId == nil && participantJid == nil && ![@"" isEqualToString:actualFrom])
{
NSDictionary* mucParticipant = [[DataLayer sharedInstance] getParticipantForNick:actualFrom inRoom:messageNode.fromUser forAccountID:account.accountID];
participantJid = mucParticipant ? mucParticipant[@"participant_jid"] : nil;
}
//make sure this is not the full jid
if(participantJid != nil)
participantJid = [HelperTools splitJid:participantJid][@"user"];
DDLogInfo(@"Extracted participantJid: %@", participantJid);
}
//inbound value for 1:1 chats
BOOL inbound = ![messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid];
//inbound value for groupchat messages
if(ownNick != nil)
{
//we got an occupant-id? --> use that for inbound calculation
//use the reported jid otherwise (note: biboumi will report made-up jids not matching our real jid, so this will fail)
//if both don't work, try the nickname (but only for inbound calculation, NOT for calculating LMC or retraction auth)
if(occupantId != nil)
inbound = ![occupantId isEqualToString:ownOccupantId];
else if(participantJid != nil)
inbound = ![participantJid isEqualToString:account.connectionProperties.identity.jid];
else
inbound = ![ownNick isEqualToString:actualFrom];
DDLogDebug(@"This is muc, inbound is now: %@ (ownNick: %@, ownOccupantId: %@, ownJid: %@, occupantId: %@, actualFrom: %@, participantJid: %@)", bool2str(inbound), ownNick, ownOccupantId, account.connectionProperties.identity.jid, occupantId, actualFrom, participantJid);
}
if([messageNode check:@"/<type=groupchat>/subject"])
{
if(!isMLhistory)
{
NSString* subject = nilDefault([messageNode findFirst:@"/<type=groupchat>/subject#"], @"");
subject = [subject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString* currentSubject = [[DataLayer sharedInstance] mucSubjectforAccount:account.accountID andRoom:messageNode.fromUser];
DDLogInfo(@"Got MUC subject for %@: '%@'", messageNode.fromUser, subject);
if([subject isEqualToString:currentSubject])
{
DDLogVerbose(@"Ignoring subject, nothing changed...");
return nil;
}
DDLogVerbose(@"Updating subject in database: %@", subject);
[[DataLayer sharedInstance] updateMucSubject:subject forAccount:account.accountID andRoom:messageNode.fromUser];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalMucSubjectChanged object:account userInfo:@{
@"room": messageNode.fromUser,
@"subject": subject,
}];
}
else
DDLogVerbose(@"Ignoring muc subject: isMLhistory=YES...");
return nil;
}
NSString* decrypted;
if([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header"])
{
if(isMLhistory)
{
//only show error for real messages having a fallback body, not for silent key exchange messages
if([messageNode check:@"body#"])
{
//use the fallback body on alpha builds (changes are good this fallback body really is the cleartext of the message because of "opportunistic" encryption)
#ifndef IS_ALPHA
decrypted = NSLocalizedString(@"Message was encrypted with OMEMO and can't be decrypted anymore", @"");
#endif
}
else
DDLogInfo(@"Ignoring encrypted mam history message without fallback body");
}
else
decrypted = [account.omemo decryptMessage:messageNode withMucParticipantJid:participantJid];
DDLogVerbose(@"Decrypted: %@", decrypted);
}
#ifdef IS_ALPHA
//thats the negation of our case from line 375
//--> opportunistic omemo in alpha builds should use the fallback body instead of the EME error because the fallback body could be the cleartext message
// (it could be a real omemo fallback, too, but there is no harm in using that instead of the EME message)
if(!([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header"] && isMLhistory && [messageNode check:@"body#"]))
#endif
//implement reading support for EME for messages having a fallback body (e.g. no silent key exchanges) that could not be decrypted
//this sets the var "decrypted" to the locally generated "fallback body"
if([messageNode check:@"body#"] && !decrypted && [messageNode check:@"{urn:xmpp:eme:0}encryption@namespace"])
{
if([[messageNode findFirst:@"{urn:xmpp:eme:0}encryption@namespace"] isEqualToString:@"eu.siacs.conversations.axolotl"])
decrypted = NSLocalizedString(@"Message was encrypted with OMEMO but could not be decrypted", @"");
else
{
NSString* encryptionName = [messageNode check:@"{urn:xmpp:eme:0}encryption@name"] ? [messageNode findFirst:@"{urn:xmpp:eme:0}encryption@name"] : [messageNode findFirst:@"{urn:xmpp:eme:0}encryption@namespace"];
//hardcoded names mandated by XEP 0380
if([[messageNode findFirst:@"{urn:xmpp:eme:0}encryption@namespace"] isEqualToString:@"urn:xmpp:otr:0"])
encryptionName = @"OTR";
else if([[messageNode findFirst:@"{urn:xmpp:eme:0}encryption@namespace"] isEqualToString:@"jabber:x:encrypted"])
encryptionName = @"Legacy OpenPGP";
else if([[messageNode findFirst:@"{urn:xmpp:eme:0}encryption@namespace"] isEqualToString:@"urn:xmpp:openpgp:0"])
encryptionName = @"OpenPGP for XMPP";
decrypted = [NSString stringWithFormat:NSLocalizedString(@"Message was encrypted with '%@' which isn't supported by Monal", @""), encryptionName];
}
}
//ignore encrypted messages coming from our own device id (most probably a muc reflection)
BOOL sentByOwnOmemoDevice = NO;
#ifndef DISABLE_OMEMO
if([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"])
sentByOwnOmemoDevice = ((NSNumber*)[messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"]).unsignedIntValue == [account.omemo getDeviceId].unsignedIntValue;
#endif
//handle message retraction (XEP-0424)
if([messageNode check:@"{urn:xmpp:message-retract:1}retract"])
{
NSString* idToRetract = [messageNode findFirst:@"{urn:xmpp:message-retract:1}retract@id"];
NSNumber* historyIdToRetract = nil;
if(possiblyUnknownContact.isMuc && [[account.mucProcessor getRoomFeaturesForMuc:possiblyUnknownContact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"] && [messageNode findFirst:@"{urn:xmpp:message-retract:1}retract/{urn:xmpp:message-moderate:1}moderated"])
{
historyIdToRetract = [[DataLayer sharedInstance] getRetractionHistoryIDForModeratedStanzaId:idToRetract from:messageNode.fromUser andAccount:account.accountID];
}
else
{
//this checks for everything spelled out in the business rules of XEP-0424
historyIdToRetract = [[DataLayer sharedInstance] getRetractionHistoryIDForMessageId:idToRetract from:messageNode.fromUser participantJid:participantJid occupantId:occupantId andAccount:account.accountID];
}
if(historyIdToRetract != nil)
{
[[DataLayer sharedInstance] retractMessageHistory:historyIdToRetract];
//update ui
DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract);
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{
@"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject],
@"contact": possiblyUnknownContact,
}];
//update unread count in active chats list
[possiblyUnknownContact updateUnreadCount];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
@"contact": possiblyUnknownContact,
}];
}
else
DDLogWarn(@"Could not find history ID for idToRetract '%@' from '%@' on account %@", idToRetract, messageNode.fromUser, account.accountID);
}
//handle retraction tombstone in MAM (XEP-0424)
else if([outerMessageNode check:@"{urn:xmpp:mam:2}result"] && [messageNode check:@"{urn:xmpp:message-retract:1}retracted@id"])
{
//ignore tombstones if not supported by server (someone probably faked them)
if(
(!possiblyUnknownContact.isMuc && [account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:message-retract:1#tombstone"]) ||
(possiblyUnknownContact.isMuc && [[account.mucProcessor getRoomFeaturesForMuc:possiblyUnknownContact.contactJid] containsObject:@"urn:xmpp:message-retract:1#tombstone"])
)
{
//first add an empty message into our history db...
NSNumber* historyIdToRetract = [[DataLayer sharedInstance]
addMessageToChatBuddy:buddyName
withInboundDir:inbound
forAccount:account.accountID
withBody:@""
actuallyfrom:actualFrom
occupantId:occupantId
participantJid:participantJid
sent:YES
unread:NO
messageId:messageId
serverMessageId:stanzaid
messageType:kMessageTypeText
andOverrideDate:[messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"]
encrypted:NO
displayMarkerWanted:NO
usingHistoryId:historyIdToUse
checkForDuplicates:[messageNode check:@"{urn:xmpp:sid:0}origin-id"] || (stanzaid != nil)
];
//...then retract this message (e.g. mark as retracted)
[[DataLayer sharedInstance] retractMessageHistory:historyIdToRetract];
//update ui
DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract);
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{
@"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject],
@"historyId": historyIdToRetract,
@"contact": possiblyUnknownContact,
}];
}
else
DDLogWarn(@"Got faked tombstone without server supporting them, ignoring it!");
}
//ignore encrypted body messages coming from our own device id (most probably a muc reflection)
else if(([messageNode check:@"body#"] || decrypted) && !sentByOwnOmemoDevice)
{
BOOL unread = YES;
BOOL showAlert = YES;
//if incoming or mam catchup we DO want an alert, otherwise we don't
//this will set unread=NO for MLhistory mssages, too (which is desired)
if(
!inbound ||
([outerMessageNode check:@"{urn:xmpp:mam:2}result"] && ![[outerMessageNode findFirst:@"{urn:xmpp:mam:2}result@queryid"] hasPrefix:@"MLcatchup:"])
)
{
DDLogVerbose(@"Setting showAlert to NO");
showAlert = NO;
unread = NO;
}
NSString* messageType = kMessageTypeText;
BOOL encrypted = NO;
NSString* body = [messageNode findFirst:@"body#"];
if(decrypted)
{
body = decrypted;
encrypted = YES;
}
body = [body stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
//messages with oob tag are filetransfers (but only if they are https urls)
NSString* lowercaseBody = [body lowercaseString];
if(body && [body isEqualToString:[messageNode findFirst:@"{jabber:x:oob}x/url#"]] && [lowercaseBody hasPrefix:@"https://"])
messageType = kMessageTypeFiletransfer;
//messages without spaces are potentially special ones
else if([body rangeOfString:@" "].location == NSNotFound)
{
if([lowercaseBody hasPrefix:@"geo:"])
messageType = kMessageTypeGeo;
//encrypted messages having one single string prefixed with "aesgcm:" are filetransfers, too (xep-0454)
else if(encrypted && [lowercaseBody hasPrefix:@"aesgcm://"])
messageType = kMessageTypeFiletransfer;
else if([lowercaseBody hasPrefix:@"https://"])
messageType = kMessageTypeUrl;
}
//messages from the bare muc jid are classified as status messages
if(possiblyUnknownContact.isMuc && [@"" isEqualToString:actualFrom])
messageType = kMessageTypeStatus;
DDLogInfo(@"Got message of type: %@", messageType);
if(body)
{
BOOL LMCReplaced = NO;
NSNumber* historyId = nil;
//handle LMC
if([messageNode check:@"{urn:xmpp:message-correct:0}replace"])
{
NSString* messageIdToReplace = [messageNode findFirst:@"{urn:xmpp:message-correct:0}replace@id"];
DDLogVerbose(@"Message id to LMC-replace: %@", messageIdToReplace);
//this checks if this message is from the same jid as the message it tries to do the LMC for (e.g. inbound can only correct inbound and outbound only outbound)
historyId = [[DataLayer sharedInstance] getLMCHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser occupantId:occupantId participantJid:participantJid andAccount:account.accountID];
DDLogVerbose(@"History id to LMC-replace: %@", historyId);
//now check if the LMC is allowed (we use historyIdToUse for MLhistory mam queries to only check LMC for the 3 messages coming before this ID in this converastion)
//historyIdToUse will be nil, for messages going forward in time which means (check for the newest 3 messages in this conversation)
if(historyId != nil && [[DataLayer sharedInstance] checkLMCEligible:historyId encrypted:encrypted historyBaseID:historyIdToUse])
{
[[DataLayer sharedInstance] updateMessageHistory:historyId withText:body];
LMCReplaced = YES;
}
else
historyId = nil;
}
//handle normal messages or LMC messages that can not be found
//(this will update stanzaid in database, too, if deduplication detects a duplicate/reflection)
if(historyId == nil)
{
historyId = [[DataLayer sharedInstance]
addMessageToChatBuddy:buddyName
withInboundDir:inbound
forAccount:account.accountID
withBody:[body copy]
actuallyfrom:actualFrom
occupantId:occupantId
participantJid:participantJid
sent:YES
unread:unread
messageId:messageId
serverMessageId:stanzaid
messageType:messageType
andOverrideDate:[messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"]
encrypted:encrypted
displayMarkerWanted:[messageNode check:@"{urn:xmpp:chat-markers:0}markable"]
usingHistoryId:historyIdToUse
checkForDuplicates:[messageNode check:@"{urn:xmpp:sid:0}origin-id"] || (stanzaid != nil)
];
}
message = [[DataLayer sharedInstance] messageForHistoryID:historyId];
if(message != nil && historyId != nil) //check historyId to make static analyzer happy
{
//send receive markers if requested, but DON'T do so for MLhistory messages (and don't do so for channel type mucs)
if(
[[HelperTools defaultsDB] boolForKey:@"SendReceivedMarkers"] &&
[messageNode check:@"{urn:xmpp:receipts}request"] &&
![messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid] &&
!isMLhistory
)
{
//ignore unknown groupchats or channel-type mucs or stanzas from the groupchat itself (e.g. not from a participant having a full jid)
if(
//1:1 with user in our contact list that subscribed us (e.g. is allowed to see us)
(!possiblyUnknownContact.isMuc && possiblyUnknownContact.isSubscribedFrom) ||
//muc group message from a user of this group
([possiblyUnknownContact.mucType isEqualToString:kMucTypeGroup] && messageNode.fromResource)
)
{
XMPPMessage* receiptNode = [XMPPMessage new];
//the message type is needed so that the store hint is accepted by the server --> mirror the incoming type
receiptNode.attributes[@"type"] = [messageNode findFirst:@"/@type"];
receiptNode.attributes[@"to"] = messageNode.fromUser;
if([messageNode check:@"{urn:xmpp:receipts}request"])
[receiptNode setReceipt:messageId];
[receiptNode setStoreHint];
[account send:receiptNode];
}
}
//check if we have an outgoing message sent from another client on our account
//if true we can mark all messages from this buddy as already read by us (using the other client)
//this only holds rue for non-MLhistory messages of course
//WARNING: kMonalMessageDisplayedNotice goes to chatViewController, kMonalDisplayedMessagesNotice goes to MLNotificationManager and activeChatsViewController/chatViewController
//e.g.: kMonalMessageDisplayedNotice means "remote party read our message" and kMonalDisplayedMessagesNotice means "we read a message"
if(body && stanzaid && !inbound && !isMLhistory)
{
DDLogInfo(@"Got outgoing message to contact '%@' sent by another client, removing all notifications for unread messages of this contact", buddyName);
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:buddyName andAccount:account.accountID tillStanzaId:stanzaid wasOutgoing:NO];
DDLogDebug(@"Marked as read: %@", unread);
//remove notifications of all remotely read messages (indicated by sending a response message)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}];
//update unread count in active chats list
if([unread count])
{
[possiblyUnknownContact updateUnreadCount];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
@"contact": possiblyUnknownContact,
}];
}
}
[[DataLayer sharedInstance] addActiveBuddies:buddyName forAccount:account.accountID];
DDLogInfo(@"Sending out kMonalNewMessageNotice notification for historyId %@", historyId);
[[MLNotificationQueue currentQueue] postNotificationName:kMonalNewMessageNotice object:account userInfo:@{
@"message": message,
@"showAlert": @(showAlert),
@"contact": possiblyUnknownContact,
@"LMCReplaced": @(LMCReplaced),
}];
//try to automatically determine content type of filetransfers
if(messageType == kMessageTypeFiletransfer && [[HelperTools defaultsDB] boolForKey:@"AutodownloadFiletransfers"])
[MLFiletransfer checkMimeTypeAndSizeForHistoryID:historyId];
}
}
}
else if(!inbound)
{
//just try to use the probably reflected message to update the stanzaid of our message in the db
//messageId is always a proper origin-id in this case, because inbound == NO and Monal uses origin-ids
NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound occupantId:occupantId andJid:buddyName onAccount:account.accountID];
if(historyId != nil)
{
message = [[DataLayer sharedInstance] messageForHistoryID:historyId];
DDLogDebug(@"Managed to update stanzaid of message (or stanzaid already known): %@", message);
DDLogInfo(@"Sending out kMonalNewMessageNotice notification for historyId %@", historyId);
[[MLNotificationQueue currentQueue] postNotificationName:kMonalNewMessageNotice object:account userInfo:@{
@"message": message,
@"showAlert": @(NO),
@"contact": possiblyUnknownContact,
}];
}
}
//handle message receipts
if(
[messageNode check:@"{urn:xmpp:receipts}received@id"] &&
[messageNode.toUser isEqualToString:account.connectionProperties.identity.jid]
)
{
NSString* msgId = [messageNode findFirst:@"{urn:xmpp:receipts}received@id"];
//save in DB
[[DataLayer sharedInstance] setMessageId:msgId andJid:messageNode.fromUser received:YES];
//Post notice
[[MLNotificationQueue currentQueue] postNotificationName:kMonalMessageReceivedNotice object:self userInfo:@{
kMessageId:msgId,
@"jid": messageNode.fromUser,
}];
}
//handle chat-markers in groupchats slightly different
if([messageNode check:@"{urn:xmpp:chat-markers:0}displayed@id"] && ownNick != nil)
{
//ignore unknown groupchats or channel-type mucs or stanzas from the groupchat itself (e.g. not from a participant having a full jid)
if(possiblyUnknownContact.isMuc && [possiblyUnknownContact.mucType isEqualToString:kMucTypeGroup] && messageNode.fromResource)
{
//incoming chat markers from own account (muc echo, muc "carbon")
//WARNING: kMonalMessageDisplayedNotice goes to chatViewController, kMonalDisplayedMessagesNotice goes to MLNotificationManager and activeChatsViewController/chatViewController
//e.g.: kMonalMessageDisplayedNotice means "remote party read our message" and kMonalDisplayedMessagesNotice means "we read a message"
if(!inbound)
{
DDLogInfo(@"Got OWN muc display marker in %@ for stanzaid: %@", buddyName, [messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"]);
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:buddyName andAccount:account.accountID tillStanzaId:[messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"] wasOutgoing:NO];
DDLogDebug(@"Marked as read: %@", unread);
//remove notifications of all remotely read messages (indicated by sending a display marker)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}];
//update unread count in active chats list
if([unread count])
{
[possiblyUnknownContact updateUnreadCount];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
@"contact": possiblyUnknownContact,
}];
}
}
//incoming chat markers from participant
//this will mark groupchat messages as read as soon as one of the participants sends a displayed chat-marker
//WARNING: kMonalMessageDisplayedNotice goes to chatViewController, kMonalDisplayedMessagesNotice goes to MLNotificationManager and activeChatsViewController/chatViewController
//e.g.: kMonalMessageDisplayedNotice means "remote party read our message" and kMonalDisplayedMessagesNotice means "we read a message"
else
{
DDLogInfo(@"Got remote muc display marker from %@ for stanzaid: %@", messageNode.from, [messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"]);
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:buddyName andAccount:account.accountID tillStanzaId:[messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"] wasOutgoing:YES];
DDLogDebug(@"Marked as displayed: %@", unread);
for(MLMessage* msg in unread)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalMessageDisplayedNotice object:account userInfo:@{@"message":msg, kMessageId:msg.messageId}];
}
}
}
else if([messageNode check:@"{urn:xmpp:chat-markers:0}displayed@id"])
{
//incoming chat markers from contact
//WARNING: kMonalMessageDisplayedNotice goes to chatViewController, kMonalDisplayedMessagesNotice goes to MLNotificationManager and activeChatsViewController/chatViewController
//e.g.: kMonalMessageDisplayedNotice means "remote party read our message" and kMonalDisplayedMessagesNotice means "we read a message"
if(inbound)
{
DDLogInfo(@"Got remote display marker from %@ for message id: %@", messageNode.fromUser, [messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"]);
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:messageNode.fromUser andAccount:account.accountID tillStanzaId:[messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"] wasOutgoing:YES];
DDLogDebug(@"Marked as displayed: %@", unread);
for(MLMessage* msg in unread)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalMessageDisplayedNotice object:account userInfo:@{@"message":msg, kMessageId:msg.messageId}];
}
//incoming chat markers from own account (carbon copy)
//WARNING: kMonalMessageDisplayedNotice goes to chatViewController, kMonalDisplayedMessagesNotice goes to MLNotificationManager and activeChatsViewController/chatViewController
//e.g.: kMonalMessageDisplayedNotice means "remote party read our message" and kMonalDisplayedMessagesNotice means "we read a message"
else
{
DDLogInfo(@"Got OWN display marker to %@ for message id: %@", messageNode.toUser, [messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"]);
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:messageNode.toUser andAccount:account.accountID tillStanzaId:[messageNode findFirst:@"{urn:xmpp:chat-markers:0}displayed@id"] wasOutgoing:NO];
DDLogDebug(@"Marked as read: %@", unread);
//remove notifications of all remotely read messages (indicated by sending a display marker)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}];
//update unread count in active chats list
if([unread count])
{
[possiblyUnknownContact updateUnreadCount];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
@"contact": possiblyUnknownContact,
}];
}
}
}
//handle typing notifications but ignore them in appex or for mam fetches (*any* mam fetches are ignored here, chatstates should *never* be in a mam archive!)
if(![HelperTools isAppExtension] && ![outerMessageNode check:@"{urn:xmpp:mam:2}result"])
{
//only use "is typing" messages when not older than 2 minutes (always allow "not typing" messages)
if(
[messageNode check:@"{http://jabber.org/protocol/chatstates}*"] &&
[[DataLayer sharedInstance] checkCap:@"http://jabber.org/protocol/chatstates" forUser:messageNode.fromUser onAccountID:account.accountID]
)
{
//deduce state
BOOL composing = NO;
if([@"active" isEqualToString:[messageNode findFirst:@"{http://jabber.org/protocol/chatstates}*$"]])
composing = NO;
else if([@"composing" isEqualToString:[messageNode findFirst:@"{http://jabber.org/protocol/chatstates}*$"]])
composing = YES;
else if([@"paused" isEqualToString:[messageNode findFirst:@"{http://jabber.org/protocol/chatstates}*$"]])
composing = NO;
else if([@"inactive" isEqualToString:[messageNode findFirst:@"{http://jabber.org/protocol/chatstates}*$"]])
composing = NO;
//handle state
if(
(
composing &&
(
![messageNode check:@"{urn:xmpp:delay}delay@stamp"] ||
[[NSDate date] timeIntervalSinceDate:[messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"]] < 120
)
) ||
!composing
)
{
[[MLNotificationQueue currentQueue] postNotificationName:kMonalLastInteractionUpdatedNotice object:self userInfo:@{
@"jid": messageNode.fromUser,
@"accountID": account.accountID,
@"isTyping": composing ? @YES : @NO
}];
//send "not typing" notifications (kMonalLastInteractionUpdatedNotice) 60 seconds after the last isTyping was received
@synchronized(_typingNotifications) {
//copy needed values into local variables to not retain self by our timer block
NSString* jid = messageNode.fromUser;
//abort old timer on new isTyping or isNotTyping message
if(_typingNotifications[messageNode.fromUser])
((monal_void_block_t) _typingNotifications[messageNode.fromUser])();
//start a new timer for every isTyping message
if(composing)
{
_typingNotifications[messageNode.fromUser] = createTimer(60, (^{
[[MLNotificationQueue currentQueue] postNotificationName:kMonalLastInteractionUpdatedNotice object:[[NSDate date] initWithTimeIntervalSince1970:0] userInfo:@{
@"jid": jid,
@"accountID": account.accountID,
@"isTyping": @NO
}];
}));
}
}
}
}
}
return message;
}
@end