snikket-ios/Snikket/service/MessageEventHandler.swift
2023-04-19 15:52:00 +01:00

480 lines
25 KiB
Swift

//
// MessageEventHandler.swift
//
// Siskin IM
// Copyright (C) 2019 "Tigase, Inc." <office@tigase.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. Look for COPYING file in the top folder.
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
import TigaseSwift
import TigaseSwiftOMEMO
import os
class MessageEventHandler: XmppServiceEventHandler {
public static let OMEMO_AVAILABILITY_CHANGED = Notification.Name(rawValue: "OMEMOAvailabilityChanged");
public static let MESSAGE_SYNCHRONIZATION_FINISHED = Notification.Name(rawValue: "MessageSynchronizationFinished");
static func prepareBody(message: Message, forAccount account: BareJID) -> (String?, MessageEncryption, String?) {
var encryption: MessageEncryption = .none;
var fingerprint: String? = nil;
guard (message.type ?? .chat) != .error else {
guard let body = message.body else {
return (message.to?.resource == nil ? nil : "", encryption, nil);
}
return (body, encryption, nil);
}
var encryptionErrorBody: String?;
if var from = message.from?.bareJid, let omemoModule: OMEMOModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(OMEMOModule.ID) {
if message.type == .groupchat, let nickname = message.from?.resource, let room = DBChatStore.instance.getChat(for: account, with: from) as? DBRoom, let occupant = room.presences[nickname], occupant.jid != nil {
from = occupant.jid!.bareJid;
}
switch omemoModule.decode(message: message, from: from) {
case .successMessage(_, let keyFingerprint):
encryption = .decrypted;
fingerprint = keyFingerprint
break;
case .successTransportKey(_, _):
print("got transport key with key and iv!");
case .failure(let error):
switch error {
case .invalidMessage:
encryptionErrorBody = "Message was not encrypted for this device.";
encryption = .notForThisDevice;
case .duplicateMessage:
return (nil, .none, nil);
case .notEncrypted:
encryption = .none;
default:
encryptionErrorBody = "Message decryption failed!";
encryption = .decryptionFailed;
}
break;
}
}
guard let body = message.body ?? message.oob ?? encryptionErrorBody else {
return (nil, encryption, nil);
}
return (body, encryption, fingerprint);
}
let events: [Event] = [MessageModule.MessageReceivedEvent.TYPE, MessageDeliveryReceiptsModule.ReceiptEvent.TYPE, MessageCarbonsModule.CarbonReceivedEvent.TYPE, DiscoveryModule.AccountFeaturesReceivedEvent.TYPE, DiscoveryModule.ServerFeaturesReceivedEvent.TYPE, MessageArchiveManagementModule.ArchivedMessageReceivedEvent.TYPE, SessionEstablishmentModule.SessionEstablishmentSuccessEvent.TYPE, StreamManagementModule.ResumedEvent.TYPE, OMEMOModule.AvailabilityChangedEvent.TYPE];
init() {
}
func handle(event: Event) {
switch event {
case let e as MessageModule.MessageReceivedEvent:
guard e.message.from != nil, let account = e.sessionObject.userBareJid else {
return;
}
DBChatHistoryStore.instance.append(for: account, message: e.message, source: .stream);
case let e as MessageDeliveryReceiptsModule.ReceiptEvent:
guard let from = e.message.from?.bareJid, let account = e.sessionObject.userBareJid else {
return;
}
DBChatHistoryStore.instance.updateItemState(for: account, with: from, stanzaId: e.messageId, from: .outgoing, to: .outgoing_delivered);
case let e as SessionEstablishmentModule.SessionEstablishmentSuccessEvent:
let account = e.sessionObject.userBareJid!;
MessageEventHandler.scheduleMessageSync(for: account);
sendUnsentMessages(for: account);
case let e as StreamManagementModule.ResumedEvent:
let account = e.sessionObject.userBareJid!;
sendUnsentMessages(for: account);
case let e as DiscoveryModule.AccountFeaturesReceivedEvent:
if let account = e.sessionObject.userBareJid, let mamModule: MessageArchiveManagementModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(MessageArchiveManagementModule.ID), mamModule.isAvailable {
MessageEventHandler.syncMessagesScheduled(for: account);
}
case let e as DiscoveryModule.ServerFeaturesReceivedEvent:
guard Settings.enableMessageCarbons.bool() else {
return;
}
guard e.features.contains(MessageCarbonsModule.MC_XMLNS) else {
return;
}
guard let mcModule: MessageCarbonsModule = XmppService.instance.getClient(for: e.sessionObject.userBareJid!)?.modulesManager.getModule(MessageCarbonsModule.ID) else {
return;
}
guard !XmppService.instance.isFetch else {
return;
}
mcModule.enable();
case let e as MessageCarbonsModule.CarbonReceivedEvent:
guard let account = e.sessionObject.userBareJid, e.message.from != nil, e.message.to != nil else {
return;
}
DBChatHistoryStore.instance.append(for: account, message: e.message, source: .carbons(action: e.action));
case let e as MessageArchiveManagementModule.ArchivedMessageReceivedEvent:
guard let account = e.sessionObject.userBareJid, e.message.from != nil, e.message.to != nil else {
return;
}
DBChatHistoryStore.instance.append(for: account, message: e.message, source: .archive(source: e.source, version: e.version, messageId: e.messageId, timestamp: e.timestamp));
case let e as OMEMOModule.AvailabilityChangedEvent:
NotificationCenter.default.post(name: MessageEventHandler.OMEMO_AVAILABILITY_CHANGED, object: e);
default:
break;
}
}
static func sendAttachment(chat: DBChat, originalUrl: URL?, uploadedUrl: String, appendix: ChatAttachmentAppendix, completionHandler: (()->Void)?) {
self.sendMessage(chat: chat, body: nil, url: uploadedUrl, chatAttachmentAppendix: appendix, messageStored: { (msgId) in
DispatchQueue.main.async {
if originalUrl != nil {
_ = DownloadStore.instance.store(originalUrl!, filename: originalUrl!.lastPathComponent, with: "\(msgId)");
}
completionHandler?();
}
})
}
static func sendMessage(chat: DBChat, body: String?, url: String?, encrypted: ChatEncryption? = nil, stanzaId: String? = nil, chatAttachmentAppendix: ChatAttachmentAppendix? = nil, correctedMessageOriginId: String? = nil, messageStored: ((Int)->Void)? = nil) {
guard let msg = body ?? url else {
return;
}
var encryption = encrypted ?? chat.options.encryption ?? ChatEncryption(rawValue: Settings.messageEncryption.string()!)!;
let message = chat.createMessage(msg);
message.id = stanzaId ?? UUID().uuidString;
message.messageDelivery = .request;
message.lastMessageCorrectionId = correctedMessageOriginId;
let account = chat.account;
let jid = chat.jid.bareJid;
// chat.options.encryption == nil means it is set to defualt
if let telephonyProvider = AccountSettings.telephonyProvider(account).getString(), jid.domain.lowercased() == telephonyProvider.lowercased(), (chat.options.encryption == nil || chat.options.encryption == ChatEncryption.none) {
encryption = ChatEncryption.none
} else if jid.domain.lowercased() == "cheogram.com" {
encryption = ChatEncryption.none
}
switch encryption {
case .omemo:
if stanzaId == nil {
if let correctedMessageId = correctedMessageOriginId {
DBChatHistoryStore.instance.correctMessage(for: account, with: jid, stanzaId: correctedMessageId, authorNickname: nil, participantId: nil, data: msg, correctionStanzaId: message.id!, correctionTimestamp: Date(), newState: .outgoing_unsent);
} else {
let fingerprint = DBOMEMOStore.instance.identityFingerprint(forAccount: account, andAddress: SignalAddress(name: account.stringValue, deviceId: Int32(bitPattern: DBOMEMOStore.instance.localRegistrationId(forAccount: account)!)));
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, authorNickname: nil, authorJid: nil, recipientNickname: nil, participantId: nil, type: url == nil ? .message : .attachment, timestamp: Date(), stanzaId: message.id, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .decrypted, encryptionFingerprint: fingerprint, appendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
}
}
XmppService.instance.tasksQueue.schedule(for: jid, task: { (completionHandler) in
sendEncryptedMessage(message, from: account, completionHandler: { result in
switch result {
case .success(_):
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: correctedMessageOriginId ?? message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: correctedMessageOriginId != nil ? nil : Date());
case .failure(let err):
let condition = (err is ErrorCondition) ? (err as? ErrorCondition) : nil;
guard condition == nil || condition! != .gone else {
completionHandler();
return;
}
var errorMessage: String? = nil;
if let encryptionError = err as? SignalError {
switch encryptionError {
case .noSession:
errorMessage = "There is no trusted device to send message to";
default:
errorMessage = "It was not possible to send encrypted message due to encryption error";
}
}
DBChatHistoryStore.instance.markOutgoingAsError(for: account, with: jid, stanzaId: message.id!, errorCondition: .undefined_condition, errorMessage: errorMessage);
}
completionHandler();
});
});
case .none:
message.oob = url;
let type: ItemType = url == nil ? .message : .attachment;
if stanzaId == nil {
if let correctedMessageId = correctedMessageOriginId {
DBChatHistoryStore.instance.correctMessage(for: account, with: jid, stanzaId: correctedMessageId, authorNickname: nil, participantId: nil, data: msg, correctionStanzaId: message.id!, correctionTimestamp: Date(), newState: .outgoing_unsent);
} else {
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, authorNickname: nil, authorJid: nil, recipientNickname: nil, participantId: nil, type: type, timestamp: Date(), stanzaId: message.id, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .none, encryptionFingerprint: nil, appendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
}
}
XmppService.instance.tasksQueue.schedule(for: jid, task: { (completionHandler) in
sendUnencryptedMessage(message, from: account, completionHandler: { result in
switch result {
case .success(_):
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: correctedMessageOriginId ?? message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: correctedMessageOriginId != nil ? nil : Date());
case .failure(let err):
guard let condition = err as? ErrorCondition, condition != .gone else {
completionHandler();
return;
}
DBChatHistoryStore.instance.markOutgoingAsError(for: account, with: jid, stanzaId: message.id!, errorCondition: err as? ErrorCondition ?? .undefined_condition, errorMessage: "Could not send message");
}
completionHandler();
});
});
}
}
fileprivate static func sendUnencryptedMessage(_ message: Message, from account: BareJID, completionHandler: @escaping (Result<Void,Error>)->Void) {
guard let client = XmppService.instance.getClient(for: account), client.state == .connected else {
completionHandler(.failure(ErrorCondition.gone));
return;
}
client.context.writer?.write(message);
completionHandler(.success(Void()));
}
fileprivate static func sendEncryptedMessage(_ message: Message, from account: BareJID, completionHandler resultHandler: @escaping (Result<Void,Error>)->Void) {
guard let omemoModule: OMEMOModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(OMEMOModule.ID) else {
DBChatHistoryStore.instance.updateItemState(for: account, with: message.to!.bareJid, stanzaId: message.id!, from: .outgoing_unsent, to: .outgoing_error);
resultHandler(.failure(ErrorCondition.unexpected_request));
return;
}
guard let client = XmppService.instance.getClient(for: account), client.state == .connected else {
resultHandler(.failure(ErrorCondition.gone));
return;
}
let completionHandler: ((EncryptionResult<Message, SignalError>)->Void)? = { (result) in
switch result {
case .failure(let error):
resultHandler(.failure(error));
case .successMessage(let encryptedMessage, _):
guard let client = XmppService.instance.getClient(for: account) else {
resultHandler(.failure(ErrorCondition.gone));
return;
}
client.context.writer?.write(encryptedMessage);
resultHandler(.success(Void()));
}
};
omemoModule.encode(message: message, completionHandler: completionHandler!);
}
fileprivate func sendUnsentMessages(for account: BareJID) {
DBChatHistoryStore.instance.loadUnsentMessage(for: account, completionHandler: { (account, jid, data, stanzaId, encryption, correctionStanzaId, type) in
var chat = DBChatStore.instance.getChat(for: account, with: jid);
if chat == nil {
switch DBChatStore.instance.createChat(for: account, jid: JID(jid), thread: nil) {
case .success(let dbChat):
chat = dbChat;
case .failure(_):
chat = nil;
}
}
if let dbChat = chat as? DBChat {
if type == .message {
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: nil, stanzaId: correctionStanzaId == nil ? stanzaId : correctionStanzaId, correctedMessageOriginId: correctionStanzaId == nil ? nil : stanzaId);
} else if type == .attachment {
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: data, stanzaId: stanzaId);
}
}
});
}
static func calculateDirection(direction: MessageDirection, for account: BareJID, with jid: BareJID, authorNickname: String?, authorJid: BareJID?) -> MessageDirection {
if let authorJid = authorJid {
return account == authorJid ? .outgoing : .incoming;
}
guard let senderNickname = authorNickname else {
return direction;
}
if let conversation = DBChatStore.instance.getChat(for: account, with: jid) {
switch conversation {
case let channel as DBChannel:
return channel.participantId == senderNickname ? .outgoing : .incoming;
case let room as DBRoom:
return room.nickname == senderNickname ? .outgoing : .incoming;
default:
break;
}
}
return direction;
}
static func calculateState(direction: MessageDirection, isError error: Bool, isFromArchive archived: Bool, isMuc: Bool) -> MessageState {
let unread = (!archived) || isMuc;
if direction == .incoming {
if error {
return unread ? .incoming_error_unread : .incoming_error;
}
return unread ? .incoming_unread : .incoming;
} else {
if error {
return unread ? .outgoing_error_unread : .outgoing_error;
}
return .outgoing;
}
}
private static var syncSinceQueue = DispatchQueue(label: "syncSinceQueue");
private static var syncSince: [BareJID: Date] = [:];
static func scheduleMessageSync(for account: BareJID) {
if AccountSettings.messageSyncAuto(account).bool() {
var syncPeriod = AccountSettings.messageSyncPeriod(account).double();
if syncPeriod == 0 {
syncPeriod = 72;
}
let syncMessagesSince = max(DBChatStore.instance.lastMessageTimestamp(for: account), Date(timeIntervalSinceNow: -1 * syncPeriod * 3600));
// use last "received" stable stanza id for account MAM archive in case of MAM:2?
syncSinceQueue.async {
self.syncSince[account] = syncMessagesSince;
}
} else {
syncSinceQueue.async {
syncSince.removeValue(forKey: account);
DBChatHistorySyncStore.instance.removeSyncPeriods(forAccount: account);
}
}
}
static func syncMessagesScheduled(for account: BareJID) {
syncSinceQueue.async {
guard AccountSettings.messageSyncAuto(account).bool(), let syncMessagesSince = syncSince[account] else {
return;
}
syncMessages(for: account, since: syncMessagesSince);
}
}
static func syncMessagesAfterID(for account: BareJID, version: MessageArchiveManagementModule.Version?, componentJID: JID, afterId: String) {
guard let client = XmppService.instance.getClient(for: account), let mamModule: MessageArchiveManagementModule = client.modulesManager.getModule(MessageArchiveManagementModule.ID) else { return }
let queryId = UUID().uuidString
let query = JabberDataElement(type: .submit)
let formTypeField = HiddenField(name: "FORM_TYPE")
formTypeField.value = version?.rawValue
query.addField(formTypeField)
let withField = JidSingleField(name: "with")
withField.value = componentJID
query.addField(withField)
let startField = TextSingleField(name: "after-id")
startField.value = afterId
query.addField(startField)
mamModule.queryItems(version: version, componentJid: componentJID, node: nil, query: query, queryId: queryId, rsm: RSM.Query(after:afterId ,max: 150)) { responseStanza in
return
}
}
static func syncMessages(for account: BareJID, version: MessageArchiveManagementModule.Version? = nil, componentJID: JID? = nil, since: Date, rsmQuery: RSM.Query? = nil) {
let period = DBChatHistorySyncStore.Period(account: account, component: componentJID?.bareJid, from: since, after: nil);
DBChatHistorySyncStore.instance.addSyncPeriod(period);
syncMessagePeriods(for: account, version: version, componentJID: componentJID?.bareJid)
}
static func syncMessagePeriods(for account: BareJID, version: MessageArchiveManagementModule.Version? = nil, componentJID jid: BareJID? = nil) {
guard let first = DBChatHistorySyncStore.instance.loadSyncPeriods(forAccount: account, component: jid).first else {
return;
}
syncSinceQueue.async {
syncMessages(forPeriod: first, version: version);
}
}
static func syncMessages(forPeriod period: DBChatHistorySyncStore.Period, version: MessageArchiveManagementModule.Version? = nil, rsmQuery: RSM.Query? = nil, retry: Int = 3) {
guard let client = XmppService.instance.getClient(for: period.account), let mamModule: MessageArchiveManagementModule = client.modulesManager.getModule(MessageArchiveManagementModule.ID) else {
return;
}
let start = Date();
let queryId = UUID().uuidString;
mamModule.queryItems(version: version, componentJid: period.component == nil ? nil : JID(period.component!), start: period.from, end: period.to, queryId: queryId, rsm: rsmQuery ?? RSM.Query(after: period.after, max: 150), completionHandler: { (result) in
switch result {
case .success(_, let complete, let rsmResponse):
if complete || rsmResponse == nil {
DBChatHistorySyncStore.instance.removeSyncPerod(period);
syncMessagePeriods(for: period.account, version: version, componentJID: period.component);
} else {
if let last = rsmResponse?.last, UUID(uuidString: last) != nil {
DBChatHistorySyncStore.instance.updatePeriod(period, after: last);
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
self.syncMessages(forPeriod: period, version: version, rsmQuery: rsmResponse?.next(150));
}
}
os_log("for account %s fetch for component %s with id %s executed in %f s", log: .chatHistorySync, type: .debug, period.account.stringValue, period.component?.stringValue ?? "nil", queryId, Date().timeIntervalSince(start));
case .failure(let errorCondition, let response):
guard client.state == .connected, retry > 0 && errorCondition != .feature_not_implemented else {
os_log("for account %s fetch for component %s with id %s could not synchronize message archive for: %{public}s got: %s", log: .chatHistorySync, type: .debug, period.account.stringValue, period.component?.stringValue ?? "nil", queryId, errorCondition.rawValue, response?.description ?? "nil");
return;
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
self.syncMessages(forPeriod: period, version: version, rsmQuery: rsmQuery, retry: retry - 1);
}
}
});
}
static func extractRealAuthor(from message: Message, for account: BareJID, with jid: JID) -> (String?, BareJID?, String?, String?) {
if message.type == .groupchat {
if let mix = message.mix {
let authorNickname = mix.nickname;
let authorJid = mix.jid;
return (authorNickname, authorJid, nil, jid.resource);
} else {
// in this case it is most likely MUC groupchat message..
return (message.from?.resource, nil, nil, nil);
}
} else {
// this can be 1-1 message from MUC..
if let room = DBChatStore.instance.getChat(for: account, with: jid.bareJid) as? DBRoom {
if room.nickname == message.from?.resource {
return (message.from?.resource, nil, message.to?.resource, nil);
} else {
return (message.from?.resource, nil, message.to?.resource, nil);
}
}
}
return (nil, nil, nil, nil);
}
static func itemType(fromMessage message: Message) -> ItemType {
if let oob = message.oob {
if (message.body == nil || oob == message.body), URL(string: oob) != nil {
return .attachment;
}
}
return .message;
}
}