Added initial support for encrypted push notifications #siskinim-164

This commit is contained in:
Andrzej Wójcik 2019-11-14 18:05:03 +01:00
parent 9b11c8a7b5
commit 95fe2c6a01
No known key found for this signature in database
GPG key ID: 2BE28BB9C1B5FF02
53 changed files with 2526 additions and 899 deletions

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>NotificationService</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.siskinim.shared</string>
<string>group.siskinim.notifications</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,155 @@
//
// NotificationService.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 UserNotifications
import Shared
import TigaseSwift
import os.log
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)? {
didSet {
debug("content handler set!");
}
}
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
debug("Received push!");
if let bestAttemptContent = bestAttemptContent {
bestAttemptContent.sound = UNNotificationSound.default;
bestAttemptContent.categoryIdentifier = "MESSAGE";
if let account = BareJID(bestAttemptContent.userInfo["account"] as? String) {
NotificationManager.instance.initialize(provider: ExtensionNotificationManagerProvider());
debug("push for account:", account);
if let encryped = bestAttemptContent.userInfo["encrypted"] as? String, let ivStr = bestAttemptContent.userInfo["iv"] as? String {
if let key = NotificationEncryptionKeys.key(for: account), let data = Data(base64Encoded: encryped), let iv = Data(base64Encoded: ivStr) {
debug("got encrypted push with known key");
let cipher = Cipher.AES_GCM();
var decoded = Data();
if cipher.decrypt(iv: iv, key: key, encoded: data, auth: nil, output: &decoded) {
debug("got decrypted data:", String(data: decoded, encoding: .utf8));
if let payload = try? JSONDecoder().decode(Payload.self, from: decoded) {
debug("decoded payload successfully!");
NotificationManager.instance.prepareNewMessageNotification(content: bestAttemptContent, account: account, sender: payload.sender.bareJid, type: payload.type, nickname: payload.nickname, body: payload.message, completionHandler: { content in
DispatchQueue.main.async {
contentHandler(content);
}
});
}
}
}
contentHandler(bestAttemptContent)
} else {
debug("got plain push with", bestAttemptContent.userInfo[AnyHashable("sender")] as? String, bestAttemptContent.userInfo[AnyHashable("body")] as? String, bestAttemptContent.userInfo[AnyHashable("unread-messages")] as? Int);
NotificationManager.instance.prepareNewMessageNotification(content: bestAttemptContent, account: account, sender: JID(bestAttemptContent.userInfo[AnyHashable("sender")] as? String)?.bareJid, type: .unknown, nickname: bestAttemptContent.userInfo[AnyHashable("nickname")] as? String, body: bestAttemptContent.userInfo[AnyHashable("body")] as? String, completionHandler: { content in
DispatchQueue.main.async {
contentHandler(content);
}
});
}
}
}
}
// func updateNotification(content: UNMutableNotificationContent, account: BareJID, unread: Int, sender: JID, type kind: Payload.Kind, nickname: String?, body: String) {
// let tmp = try! DBConnection.main.prepareStatement(NotificationService.GET_NAME_QUERY).findFirst(["account": account, "jid": sender.bareJid] as [String: Any?], map: { (cursor) -> (String?, Int)? in
// return (cursor["name"], cursor["type"]!);
// });
// let name = tmp?.0;
// let type: Payload.Kind = tmp?.1 == 1 ? .groupchat : .chat;
// switch type {
// case .chat:
// content.title = name ?? sender.stringValue;
// content.body = body;
// content.userInfo = ["account": account.stringValue, "sender": sender.bareJid.stringValue];
// case .groupchat:
// if let nickname = nickname {
// content.title = "\(nickname) mentioned you in \(name ?? sender.bareJid.stringValue)";
// } else {
// content.title = "\(name ?? sender.bareJid.stringValue)";
// }
// content.body = body;
// content.userInfo = ["account": account.stringValue, "sender": sender.bareJid.stringValue];
// default:
// break;
// }
// content.categoryIdentifier = NotificationCategory.MESSAGE.rawValue;
// //content.badge = 2;
//
// }
func debug(_ data: Any...) {
os_log("%{public}@", log: OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SiskinPush"), "\(Date()): \(data)");
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
class ExtensionNotificationManagerProvider: NotificationManagerProvider {
static let GET_NAME_QUERY = "select name, 0 as type from roster_items where account = :account and jid = :jid union select name, 1 as type from chats where account = :account and jid = :jid order by type";
static let GET_UNREAD_CHATS = "select c.account, c.jid from chats c inner join chat_history ch where ch.account = c.account and ch.jid = c.jid and ch.state in (2,6,7) group by c.account, c.jid";
func getChatNameAndType(for account: BareJID, with jid: BareJID, completionHandler: @escaping (String?, Payload.Kind) -> Void) {
let tmp = try! DBConnection.main.prepareStatement(ExtensionNotificationManagerProvider.GET_NAME_QUERY).findFirst(["account": account, "jid": jid] as [String: Any?], map: { (cursor) -> (String?, Int)? in
return (cursor["name"], cursor["type"]!);
});
completionHandler(tmp?.0, tmp?.1 == 1 ? .groupchat : .chat);
}
func countBadge(withThreadId: String?, completionHandler: @escaping (Int) -> Void) {
NotificationManager.unreadChatsThreadIds { (result) in
var unreadChats = result;
try! DBConnection.main.prepareStatement(ExtensionNotificationManagerProvider.GET_UNREAD_CHATS).query(forEach: { cursor in
if let account: BareJID = cursor["account"], let jid: BareJID = cursor["jid"] {
unreadChats.insert("account=\(account.stringValue)|sender=\(jid.stringValue)");
}
})
if let threadId = withThreadId {
unreadChats.insert(threadId);
}
completionHandler(unreadChats.count);
}
}
func shouldShowNotification(account: BareJID, sender: BareJID?, body: String?, completionHandler: @escaping (Bool)->Void) {
completionHandler(true);
}
}

22
Shared/Info.plist Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View file

@ -0,0 +1,39 @@
//
// NotificationCategory.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
public enum NotificationCategory: String {
case UNKNOWN
case ERROR
case MESSAGE
case SUBSCRIPTION_REQUEST
case MUC_ROOM_INVITATION
case CALL
case UNSENT_MESSAGES
public static func from(identifier: String?) -> NotificationCategory {
guard let str = identifier else {
return .UNKNOWN;
}
return NotificationCategory(rawValue: str) ?? .UNKNOWN;
}
}

32
Shared/Shared.h Normal file
View file

@ -0,0 +1,32 @@
//
// Shared.h
//
// 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/Foundation.h>
//! Project version number for Shared.
//FOUNDATION_EXPORT double SharedVersionNumber;
//! Project version string for Shared.
//FOUNDATION_EXPORT const unsigned char SharedVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Shared/PublicHeader.h>

View file

@ -0,0 +1,63 @@
//
// DBConnection_main.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
extension DBConnection {
public static func mainDbURL() -> URL {
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.siskinim.shared")!;
return containerURL.appendingPathComponent("siskinim_main.db");
}
private static var createIfNotExist: Bool = false;
public static let main: DBConnection = {
let dbURL = mainDbURL();
if (!FileManager.default.fileExists(atPath: dbURL.path)) && (!createIfNotExist) {
return try! DBConnection.initialize(dbPath: ":memory:");
} else {
return try! DBConnection.initialize(dbPath: dbURL.path);
}
}();
public static func migrateToGroupIfNeeded() throws {
let dbURL = mainDbURL();
if !FileManager.default.fileExists(atPath: dbURL.path) {
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true);
let documentDirectory = paths[0];
let path = documentDirectory.appending("/mobile_messenger1.db");
if FileManager.default.fileExists(atPath: path) {
try FileManager.default.moveItem(atPath: path, toPath: dbURL.path);
}
}
createIfNotExist = true;
}
private static func initialize(dbPath: String) throws -> DBConnection {
let conn = try DBConnection(dbPath: dbPath);
try DBSchemaManager(dbConnection: conn).upgradeSchema();
return conn;
}
}

View file

@ -21,7 +21,9 @@
import Foundation
import UIKit
import TigaseSwift
import SQLite3
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
@ -45,17 +47,16 @@ open class DBConnection {
return Int(sqlite3_changes(handle));
}
init(dbFilename:String) throws {
convenience public init(dbUrl: URL) throws {
try self.init(dbPath: dbUrl.path);
}
public init(dbPath: String) throws {
dispatcher = QueueDispatcher(label: "db_queue");
try dispatcher.sync {
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true);
let documentDirectory = paths[0];
let path = documentDirectory.appending("/" + dbFilename);
let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE;
_ = try self.check(sqlite3_open_v2(path, &self.handle_, flags | SQLITE_OPEN_FULLMUTEX, nil));
_ = try self.check(sqlite3_open_v2(dbPath, &self.handle_, flags | SQLITE_OPEN_FULLMUTEX, nil));
}
}
@ -424,24 +425,24 @@ open class DBCursor {
return String(cString: sqlite3_column_name(self.handle, idx)!);
}
init(statement:DBStatement) {
public init(statement:DBStatement) {
self.connection = statement.connection;
self.handle = statement.handle!;
}
subscript(index: Int) -> Double {
open subscript(index: Int) -> Double {
return sqlite3_column_double(handle, Int32(index));
}
subscript(index: Int) -> Int {
open subscript(index: Int) -> Int {
return Int(sqlite3_column_int64(handle, Int32(index)));
}
subscript(index: Int) -> Int32 {
open subscript(index: Int) -> Int32 {
return sqlite3_column_int(handle, Int32(index));
}
subscript(index: Int) -> String? {
open subscript(index: Int) -> String? {
let ptr = sqlite3_column_text(handle, Int32(index));
if ptr == nil {
return nil;
@ -449,11 +450,11 @@ open class DBCursor {
return String(cString: UnsafePointer(ptr!));
}
subscript(index: Int) -> Bool {
open subscript(index: Int) -> Bool {
return sqlite3_column_int64(handle, Int32(index)) != 0;
}
subscript(index: Int) -> [UInt8]? {
open subscript(index: Int) -> [UInt8]? {
let idx = Int32(index);
let origPtr = sqlite3_column_blob(handle, idx);
if origPtr == nil {
@ -464,7 +465,7 @@ open class DBCursor {
return DBCursor.convert(count, data: ptr!);
}
subscript(index: Int) -> Data? {
open subscript(index: Int) -> Data? {
let idx = Int32(index);
let origPtr = sqlite3_column_blob(handle, idx);
if origPtr == nil {
@ -474,32 +475,32 @@ open class DBCursor {
return Data(bytes: origPtr!, count: count);
}
subscript(index: Int) -> Date {
open subscript(index: Int) -> Date {
let timestamp = Double(sqlite3_column_int64(handle, Int32(index))) / 1000;
return Date(timeIntervalSince1970: timestamp);
}
subscript(index: Int) -> JID? {
open subscript(index: Int) -> JID? {
if let str:String = self[index] {
return JID(str);
}
return nil;
}
subscript(index: Int) -> BareJID? {
open subscript(index: Int) -> BareJID? {
if let str:String = self[index] {
return BareJID(str);
}
return nil;
}
subscript(column: String) -> Double? {
open subscript(column: String) -> Double? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> Int? {
open subscript(column: String) -> Int? {
// return forColumn(column) {
// let v:Int? = self[$0];
// print("for \(column), position \($0) got \(v)")
@ -511,7 +512,7 @@ open class DBCursor {
return nil;
}
subscript(column: String) -> Int32? {
open subscript(column: String) -> Int32? {
// return forColumn(column) {
// let v:Int? = self[$0];
// print("for \(column), position \($0) got \(v)")
@ -523,43 +524,43 @@ open class DBCursor {
return nil;
}
subscript(column: String) -> String? {
open subscript(column: String) -> String? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> Bool? {
open subscript(column: String) -> Bool? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> [UInt8]? {
open subscript(column: String) -> [UInt8]? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> Data? {
open subscript(column: String) -> Data? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> Date? {
open subscript(column: String) -> Date? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> JID? {
open subscript(column: String) -> JID? {
return forColumn(column) {
return self[$0];
}
}
subscript(column: String) -> BareJID? {
open subscript(column: String) -> BareJID? {
return forColumn(column) {
return self[$0];
}

View file

@ -20,15 +20,16 @@
//
import Foundation
import Shared
import TigaseSwift
public class DBSchemaManager {
static let CURRENT_VERSION = 7;
static let CURRENT_VERSION = 8;
fileprivate let dbConnection: DBConnection;
init(dbConnection: DBConnection) {
public init(dbConnection: DBConnection) {
self.dbConnection = dbConnection;
}
@ -54,6 +55,13 @@ public class DBSchemaManager {
version = try! getSchemaVersion();
}
let journalMode = try dbConnection.prepareStatement("pragma journal_mode").findFirst(map: { cursor -> String? in
return cursor["journal_mode"];
})!;
if journalMode != "wal" {
try dbConnection.execute("PRAGMA journal_mode=WAL");
}
// need to make sure that "error" column exists as there was an issue with db-schema-2.sql
// which did not create this column
do {
@ -62,30 +70,30 @@ public class DBSchemaManager {
try dbConnection.execute("ALTER TABLE chat_history ADD COLUMN error TEXT;");
}
let queryStmt = try dbConnection.prepareStatement("SELECT account, jid, encryption FROM chats WHERE encryption IS NOT NULL AND options IS NULL");
let toConvert = try queryStmt.query { (cursor) -> (BareJID, BareJID, ChatEncryption)? in
let account: BareJID = cursor["account"]!;
let jid: BareJID = cursor["jid"]!;
guard let encryptionStr: String = cursor["encryption"] else {
return nil;
}
guard let encryption = ChatEncryption(rawValue: encryptionStr) else {
return nil;
}
return (account, jid, encryption);
}
if !toConvert.isEmpty {
let updateStmt = try dbConnection.prepareStatement("UPDATE chats SET options = ?, encryption = null WHERE account = ? AND jid = ?");
try toConvert.forEach { (arg0) in
let (account, jid, encryption) = arg0
var options = ChatOptions();
options.encryption = encryption;
let data = try? JSONEncoder().encode(options);
let dataStr = data != nil ? String(data: data!, encoding: .utf8)! : nil;
_ = try updateStmt.update(dataStr, account, jid);
}
}
// let queryStmt = try dbConnection.prepareStatement("SELECT account, jid, encryption FROM chats WHERE encryption IS NOT NULL AND options IS NULL");
// let toConvert = try queryStmt.query { (cursor) -> (BareJID, BareJID, ChatEncryption)? in
// let account: BareJID = cursor["account"]!;
// let jid: BareJID = cursor["jid"]!;
// guard let encryptionStr: String = cursor["encryption"] else {
// return nil;
// }
// guard let encryption = ChatEncryption(rawValue: encryptionStr) else {
// return nil;
// }
//
// return (account, jid, encryption);
// }
// if !toConvert.isEmpty {
// let updateStmt = try dbConnection.prepareStatement("UPDATE chats SET options = ?, encryption = null WHERE account = ? AND jid = ?");
// try toConvert.forEach { (arg0) in
// let (account, jid, encryption) = arg0
// var options = ChatOptions();
// options.encryption = encryption;
// let data = try? JSONEncoder().encode(options);
// let dataStr = data != nil ? String(data: data!, encoding: .utf8)! : nil;
// _ = try updateStmt.update(dataStr, account, jid);
// }
// }
let toRemove: [(String,String,Int32)] = try dbConnection.prepareStatement("SELECT sess.account as account, sess.name as name, sess.device_id as deviceId FROM omemo_sessions sess WHERE NOT EXISTS (select 1 FROM omemo_identities i WHERE i.account = sess.account and i.name = sess.name and i.device_id = sess.device_id)").query([:] as [String: Any?], map: { (cursor:DBCursor) -> (String, String, Int32)? in
@ -103,7 +111,15 @@ public class DBSchemaManager {
}
fileprivate func loadSchemaFile(fileName: String) throws {
let resourcePath = Bundle.main.resourcePath! + fileName;
guard let bundle = Bundle.allFrameworks.first(where: { (bundle) -> Bool in
guard let resourcePath = bundle.resourcePath else {
return false;
}
return FileManager.default.fileExists(atPath: resourcePath.appending(fileName));
}) else {
return;
}
let resourcePath = bundle.resourcePath! + fileName;
print("loading SQL from file", resourcePath);
let dbSchema = try String(contentsOfFile: resourcePath, encoding: String.Encoding.utf8);
print("read schema:", dbSchema);

13
Shared/db-schema-8.sql Normal file
View file

@ -0,0 +1,13 @@
BEGIN;
CREATE TABLE IF NOT EXISTS chats_read (
account TEXT NOT NULL COLLATE NOCASE,
jid TEXT NOT NULL COLLATE NOCASE,
timestamp INTEGER,
UNIQUE (account, jid)
);
COMMIT;
PRAGMA user_version = 8;

View file

@ -0,0 +1,39 @@
//
// NotificationEncryptionKeys.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
public class NotificationEncryptionKeys {
private static let storage = UserDefaults(suiteName: "group.siskinim.notifications")!;
public static func key(for account: BareJID) -> Data? {
storage.data(forKey: account.stringValue)
}
public static func set(key: Data?, for account: BareJID) {
storage.setValue(key, forKey: account.stringValue);
}
}

View file

@ -0,0 +1,180 @@
//
// NotificationManager.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 UserNotifications
public class NotificationManager {
public static let instance: NotificationManager = NotificationManager();
public private(set) var provider: NotificationManagerProvider!;
public func initialize(provider: NotificationManagerProvider) {
self.provider = provider;
}
public static func unreadChatsThreadIds(completionHandler: @escaping (Set<String>)->Void) {
unreadThreadIds(for: [.MESSAGE], completionHandler: completionHandler);
}
public static func unreadThreadIds(for categories: [NotificationCategory], completionHandler: @escaping (Set<String>)->Void) {
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
let unreadChats = Set(notifications.filter({(notification) in
let category = NotificationCategory.from(identifier: notification.request.content.categoryIdentifier);
return categories.contains(category);
}).map({ (notification) in
return notification.request.content.threadIdentifier;
}));
completionHandler(unreadChats);
}
}
public func notifyNewMessage(account: BareJID, sender: BareJID?, type kind: Payload.Kind, nickname: String?, body: String) {
shouldShowNotification(account: account, sender: sender, body: body, completionHandler: { (result) in
guard result else {
return;
}
self.intNotifyNewMessage(account: account, sender: sender, type: kind, nickname: nickname, body: body);
});
}
public func shouldShowNotification(account: BareJID, sender: BareJID?, body: String?, completionHandler: @escaping (Bool)->Void) {
provider.shouldShowNotification(account: account, sender: sender, body: body) { (result) in
if result {
if let uid = self.generateMessageUID(account: account, sender: sender, body: body) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
let should = !notifications.contains(where: { (notification) -> Bool in
guard let nuid = notification.request.content.userInfo["uid"] as? String else {
return false;
}
return nuid == uid;
});
completionHandler(should);
});
return;
}
}
completionHandler(result);
}
}
private func intNotifyNewMessage(account: BareJID, sender: BareJID?, type kind: Payload.Kind, nickname: String?, body: String) {
let id = UUID().uuidString;
let content = UNMutableNotificationContent();
prepareNewMessageNotification(content: content, account: account, sender: sender, type: kind, nickname: nickname, body: body) { (content) in
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: id, content: content, trigger: nil)) { (error) in
print("message notification error", error as Any);
}
}
}
public func prepareNewMessageNotification(content: UNMutableNotificationContent, account: BareJID, sender jid: BareJID?, type kind: Payload.Kind, nickname: String?, body msg: String?, completionHandler: @escaping (UNMutableNotificationContent)->Void) {
content.sound = .default;
content.categoryIdentifier = NotificationCategory.MESSAGE.rawValue;
if let sender = jid, let body = msg {
let uid = generateMessageUID(account: account, sender: sender, body: body)!;
content.threadIdentifier = "account=\(account.stringValue)|sender=\(sender.stringValue)";
self.provider.getChatNameAndType(for: account, with: sender, completionHandler: { (name, type) in
switch type {
case .chat:
content.title = name ?? sender.stringValue;
content.body = body;
content.userInfo = ["account": account.stringValue, "sender": sender.stringValue, "uid": uid];
case .groupchat:
if let nickname = nickname {
content.title = "\(nickname) mentioned you in \(name ?? sender.stringValue)";
} else {
content.title = "\(name ?? sender.stringValue)";
}
content.body = body;
content.userInfo = ["account": account.stringValue, "sender": sender.stringValue, "uid": uid];
default:
break;
}
self.provider.countBadge(withThreadId: content.threadIdentifier, completionHandler: { count in
content.badge = count as NSNumber;
completionHandler(content);
});
});
} else {
content.threadIdentifier = "account=\(account.stringValue)";
content.body = "New message!";
self.provider.countBadge(withThreadId: content.threadIdentifier, completionHandler: { count in
content.badge = count as NSNumber;
completionHandler(content);
});
}
}
func generateMessageUID(account: BareJID, sender: BareJID?, body: String?) -> String? {
if let sender = sender, let body = body {
return Digest.sha256.digest(toHex: "\(account)|\(sender)|\(body)".data(using: .utf8));
}
return nil;
}
}
public protocol NotificationManagerProvider {
func getChatNameAndType(for account: BareJID, with jid: BareJID, completionHandler: @escaping (String?, Payload.Kind)->Void);
func countBadge(withThreadId: String?, completionHandler: @escaping (Int)->Void);
func shouldShowNotification(account: BareJID, sender: BareJID?, body: String?, completionHandler: @escaping (Bool)->Void);
}
public class Payload: Decodable {
public var unread: Int;
public var sender: JID;
public var type: Kind;
public var nickname: String?;
public var message: String?;
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self);
unread = try container.decode(Int.self, forKey: .unread);
sender = try container.decode(JID.self, forKey: .sender);
type = Kind(rawValue: (try container.decodeIfPresent(String.self, forKey: .type)) ?? Kind.unknown.rawValue)!;
nickname = try container.decodeIfPresent(String.self, forKey: .nickname);
message = try container.decodeIfPresent(String.self, forKey: .message);
// -- and so on...
}
public enum Kind: String {
case unknown
case groupchat
case chat
}
public enum CodingKeys: String, CodingKey {
case unread
case sender
case type
case nickname
case message
}
}

View file

@ -0,0 +1,129 @@
//
// Cipher+AES.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 openssl
open class Cipher {
}
extension Cipher {
open class AES_GCM {
public init() {
}
public static func generateKey(ofSize: Int) -> Data? {
var key = Data(count: ofSize/8);
let result = key.withUnsafeMutableBytes({ (ptr: UnsafeMutableRawBufferPointer) -> Int32 in
return SecRandomCopyBytes(kSecRandomDefault, ofSize/8, ptr.baseAddress!);
});
guard result == errSecSuccess else {
print("failed to generated AES encryption key:", result)
return nil;
}
return key;
}
open func encrypt(iv: Data, key: Data, message data: Data, output: UnsafeMutablePointer<Data>?, tag: UnsafeMutablePointer<Data>?) -> Bool {
let ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nil, nil, nil);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, Int32(iv.count), nil);
iv.withUnsafeBytes({ (ivBytes: UnsafeRawBufferPointer) -> Void in
key.withUnsafeBytes({ (keyBytes: UnsafeRawBufferPointer) -> Void in
EVP_EncryptInit_ex(ctx, nil, nil, keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), ivBytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
})
});
EVP_CIPHER_CTX_set_padding(ctx, 1);
var outbuf = Array(repeating: UInt8(0), count: data.count);
var outbufLen: Int32 = 0;
let encryptedBody = data.withUnsafeBytes { ( bytes) -> Data in
EVP_EncryptUpdate(ctx, &outbuf, &outbufLen, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(data.count));
return Data(bytes: &outbuf, count: Int(outbufLen));
}
EVP_EncryptFinal_ex(ctx, &outbuf, &outbufLen);
var tagData = Data(count: 16);
tagData.withUnsafeMutableBytes({ (bytes) -> Void in
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
});
EVP_CIPHER_CTX_free(ctx);
tag?.initialize(to: tagData);
output?.initialize(to: encryptedBody);
return true;
}
open func decrypt(iv: Data, key: Data, encoded payload: Data, auth tag: Data?, output: UnsafeMutablePointer<Data>?) -> Bool {
let ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nil, nil, nil);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, Int32(iv.count), nil);
key.withUnsafeBytes({ (keyBytes) -> Void in
iv.withUnsafeBytes({ (ivBytes) -> Void in
EVP_DecryptInit_ex(ctx, nil, nil, keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), ivBytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
})
})
EVP_CIPHER_CTX_set_padding(ctx, 1);
var auth = tag;
var encoded = payload;
if auth == nil {
auth = payload.subdata(in: (payload.count - 16)..<payload.count);
encoded = payload.subdata(in: 0..<(payload.count-16));
}
var outbuf = Array(repeating: UInt8(0), count: encoded.count);
var outbufLen: Int32 = 0;
let decoded = encoded.withUnsafeBytes({ (bytes) -> Data in
EVP_DecryptUpdate(ctx, &outbuf, &outbufLen, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(encoded.count));
return Data(bytes: &outbuf, count: Int(outbufLen));
});
if auth != nil {
auth!.withUnsafeMutableBytes({ [count = auth!.count] (bytes: UnsafeMutableRawBufferPointer) -> Void in
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, Int32(count), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
});
}
let ret = EVP_DecryptFinal_ex(ctx, &outbuf, &outbufLen);
EVP_CIPHER_CTX_free(ctx);
guard ret >= 0 else {
print("authentication of encrypted message failed:", ret);
return false;
}
output?.initialize(to: decoded);
return true;
}
}
}

View file

@ -20,6 +20,7 @@
//
import UIKit
import Social
import Shared
import TigaseSwift
import MobileCoreServices
@ -79,6 +80,18 @@ class ShareViewController: SLComposeServiceViewController {
return account != nil && xmppClient.state == .connected && recipients.count > 0;
}
override func viewDidLoad() {
super.viewDidLoad();
let dbURL = DBConnection.mainDbURL();
if !FileManager.default.fileExists(atPath: dbURL.path) {
let controller = UIAlertController(title: "Please launch application from the home screen before continuing.", message: nil, preferredStyle: .alert);
controller.addAction(UIAlertAction(title: "OK", style: .destructive, handler: { (action) in
self.extensionContext?.cancelRequest(withError: ShareError.firstRun);
}))
self.present(controller, animated: true, completion: nil);
}
}
override func presentationAnimationDidFinish() {
if !sharedDefaults!.bool(forKey: "SharingViaHttpUpload") {
var error = true;
@ -294,6 +307,7 @@ class ShareViewController: SLComposeServiceViewController {
}
enum ShareError: Error {
case firstRun
case featureNotAvailable
case tooBig
case failure

View file

@ -4,6 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.siskinim.shared</string>
<string>group.TigaseMessenger.Share</string>
</array>
<key>keychain-access-groups</key>

View file

@ -7,6 +7,8 @@
<key>com.apple.security.application-groups</key>
<array>
<string>group.TigaseMessenger.Share</string>
<string>group.siskinim.shared</string>
<string>group.siskinim.notifications</string>
</array>
<key>keychain-access-groups</key>
<array>

View file

@ -9,14 +9,13 @@
/* Begin PBXBuildFile section */
FE00157D2017617B00490340 /* StreamFeaturesCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE00157C2017617B00490340 /* StreamFeaturesCache.swift */; };
FE00157F2019090300490340 /* ExperimentalSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE00157E2019090300490340 /* ExperimentalSettingsViewController.swift */; };
FE01ADA91E224CF400FA7E65 /* TigasePushNotificationsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE01ADA81E224CF400FA7E65 /* TigasePushNotificationsModule.swift */; };
FE01ADA91E224CF400FA7E65 /* SiskinPushNotificationsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE01ADA81E224CF400FA7E65 /* SiskinPushNotificationsModule.swift */; };
FE137A4821F6464D006B7F7C /* UIColor_mix.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE137A4721F6464D006B7F7C /* UIColor_mix.swift */; };
FE137A4A21F72AEA006B7F7C /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE137A4921F72AEA006B7F7C /* Appearance.swift */; };
FE137A4C21F75660006B7F7C /* ChatBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE137A4B21F75660006B7F7C /* ChatBottomView.swift */; };
FE137A4E21F8851D006B7F7C /* CustomTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE137A4D21F8851D006B7F7C /* CustomTableViewController.swift */; };
FE1678F422FDDC1E0013B94A /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE1678F322FDDC1D0013B94A /* WebRTC.framework */; };
FE1678F522FDDC1E0013B94A /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE1678F322FDDC1D0013B94A /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FE168ACD1CCD197A003F8B26 /* db-schema-1.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE168ACC1CCD197A003F8B26 /* db-schema-1.sql */; };
FE1AC8F7216B8AB700D4CDAB /* NewFeaturesDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1AC8F6216B8AB700D4CDAB /* NewFeaturesDetector.swift */; };
FE1DCCA21EA52CE200850563 /* DataFormController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1DCCA11EA52CE200850563 /* DataFormController.swift */; };
FE233CD521E6846E00099281 /* CameraPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE233CD421E6846E00099281 /* CameraPreviewView.swift */; };
@ -25,8 +24,6 @@
FE258EAA1F3B8BC90042CED9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FE258EA91F3B8BC90042CED9 /* Assets.xcassets */; };
FE2809812167CE18002F5BD0 /* server_features_list.xml in Resources */ = {isa = PBXBuildFile; fileRef = FE2809802167CE18002F5BD0 /* server_features_list.xml */; };
FE2809832167CF1B002F5BD0 /* ServerFeaturesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2809822167CF1B002F5BD0 /* ServerFeaturesViewController.swift */; };
FE2A0E8E1F74012D006ADF08 /* db-schema-2.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE2A0E8D1F74012D006ADF08 /* db-schema-2.sql */; };
FE2A0E901F74051E006ADF08 /* DBSchemaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2A0E8F1F74051E006ADF08 /* DBSchemaManager.swift */; };
FE3024321CE2036A00466497 /* DBVCardsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3024311CE2036A00466497 /* DBVCardsCache.swift */; };
FE31291A22240BEB00A92863 /* PEPBookmarksModule_extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE31291922240BEB00A92863 /* PEPBookmarksModule_extension.swift */; };
FE31291C222C0D1500A92863 /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE31291B222C0D1500A92863 /* AvatarView.swift */; };
@ -40,7 +37,6 @@
FE4071E821E2653700F09B58 /* RoundButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4071E721E2653700F09B58 /* RoundButton.swift */; };
FE43EB551F3CC55900A4CAAD /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE43EB541F3CC55900A4CAAD /* ImageCache.swift */; };
FE43EB571F3DBAAE00A4CAAD /* BaseChatViewController_PreviewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE43EB561F3DBAAE00A4CAAD /* BaseChatViewController_PreviewExtension.swift */; };
FE4496C51F87934E009F649C /* db-schema-3.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE4496C31F87911C009F649C /* db-schema-3.sql */; };
FE4DDF561F39E0B500A4CE5A /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4DDF551F39E0B500A4CE5A /* ShareViewController.swift */; };
FE4DDF591F39E0B500A4CE5A /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FE4DDF571F39E0B500A4CE5A /* MainInterface.storyboard */; };
FE4DDF5D1F39E0B500A4CE5A /* Siskin IM - Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = FE4DDF531F39E0B500A4CE5A /* Siskin IM - Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -55,7 +51,6 @@
FE507A181CDB7B3B001A015C /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A011CDB7B3B001A015C /* ChatViewController.swift */; };
FE507A191CDB7B3B001A015C /* DBChatHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A031CDB7B3B001A015C /* DBChatHistoryStore.swift */; };
FE507A1A1CDB7B3B001A015C /* DBChatStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A041CDB7B3B001A015C /* DBChatStore.swift */; };
FE507A1B1CDB7B3B001A015C /* DBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A051CDB7B3B001A015C /* DBManager.swift */; };
FE507A1C1CDB7B3B001A015C /* DBRosterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A061CDB7B3B001A015C /* DBRosterStore.swift */; };
FE507A1D1CDB7B3B001A015C /* RosterItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A081CDB7B3B001A015C /* RosterItemTableViewCell.swift */; };
FE507A1E1CDB7B3B001A015C /* RosterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A091CDB7B3B001A015C /* RosterViewController.swift */; };
@ -72,13 +67,45 @@
FE6545621E9E7FDE006A14AC /* AccountDomainTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6545611E9E7FDE006A14AC /* AccountDomainTableViewCell.swift */; };
FE6545641E9E8B67006A14AC /* ServerSelectorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6545631E9E8B67006A14AC /* ServerSelectorTableViewCell.swift */; };
FE65D62822E9F8EB0065DEA5 /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE65D62722E9F8EB0065DEA5 /* Markdown.swift */; };
FE719E742271AF88007CEEC9 /* db-schema-5.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE719E732271AF88007CEEC9 /* db-schema-5.sql */; };
FE719E762271B2BA007CEEC9 /* OpenSSL_AES_GCM_Engine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE719E752271B2BA007CEEC9 /* OpenSSL_AES_GCM_Engine.swift */; };
FE719E782271B439007CEEC9 /* MessageEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE719E772271B439007CEEC9 /* MessageEncryption.swift */; };
FE719E7A227307D0007CEEC9 /* OMEMOEncryptionSwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE719E79227307D0007CEEC9 /* OMEMOEncryptionSwitchTableViewCell.swift */; };
FE719E7C22730DC3007CEEC9 /* OMEMOIdentityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE719E7B22730DC3007CEEC9 /* OMEMOIdentityTableViewCell.swift */; };
FE719E7E2274D20D007CEEC9 /* OMEMOFingerprintsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE719E7D2274D20D007CEEC9 /* OMEMOFingerprintsController.swift */; };
FE74D510234A4E1F001A925B /* ChatTableViewSystemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE74D50F234A4E1F001A925B /* ChatTableViewSystemCell.swift */; };
FE759FA42370ACA4001E78D9 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE759FA32370ACA4001E78D9 /* NotificationService.swift */; };
FE759FA82370ACA4001E78D9 /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = FE759FA12370ACA4001E78D9 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
FE759FC92370B2A4001E78D9 /* Shared.h in Headers */ = {isa = PBXBuildFile; fileRef = FE759FC72370B2A4001E78D9 /* Shared.h */; settings = {ATTRIBUTES = (Public, ); }; };
FE759FCC2370B2A4001E78D9 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FC52370B2A4001E78D9 /* Shared.framework */; };
FE759FCD2370B2A4001E78D9 /* Shared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FC52370B2A4001E78D9 /* Shared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FE759FD12370B2F2001E78D9 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FC52370B2A4001E78D9 /* Shared.framework */; };
FE759FD22370B2F2001E78D9 /* Shared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FC52370B2A4001E78D9 /* Shared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FE759FD72370B359001E78D9 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FED353822270BFD300B69C53 /* openssl.framework */; };
FE759FD82370B359001E78D9 /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FED353822270BFD300B69C53 /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FE759FDB2370B384001E78D9 /* Cipher+AES.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE759FDA2370B384001E78D9 /* Cipher+AES.swift */; };
FE759FDE2371989B001E78D9 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FDD2371988B001E78D9 /* libsqlite3.tbd */; };
FE759FDF23719A5C001E78D9 /* TigaseSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */; };
FE759FE023719A5C001E78D9 /* TigaseSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FE759FE22371C83D001E78D9 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FC52370B2A4001E78D9 /* Shared.framework */; };
FE759FE82371C972001E78D9 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FE759FE72371C966001E78D9 /* libxml2.tbd */; };
FE759FE92371DDD1001E78D9 /* DBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE507A051CDB7B3B001A015C /* DBManager.swift */; };
FE759FEB2371F11F001E78D9 /* DBConnection_main.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE759FEA2371F11F001E78D9 /* DBConnection_main.swift */; };
FE759FEC2371F1A5001E78D9 /* DBSchemaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2A0E8F1F74051E006ADF08 /* DBSchemaManager.swift */; };
FE759FED2371F213001E78D9 /* db-schema-2.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE2A0E8D1F74012D006ADF08 /* db-schema-2.sql */; };
FE759FEE2371F217001E78D9 /* db-schema-1.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE168ACC1CCD197A003F8B26 /* db-schema-1.sql */; };
FE759FEF2371F21C001E78D9 /* db-schema-3.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE4496C31F87911C009F649C /* db-schema-3.sql */; };
FE759FF02371F21C001E78D9 /* db-schema-4.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE8DD9CA221DBED80090F5AA /* db-schema-4.sql */; };
FE759FF12371F21C001E78D9 /* db-schema-5.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE719E732271AF88007CEEC9 /* db-schema-5.sql */; };
FE759FF22371F21C001E78D9 /* db-schema-6.sql in Resources */ = {isa = PBXBuildFile; fileRef = FEE9608B22F2F8950009B191 /* db-schema-6.sql */; };
FE759FF32371F21C001E78D9 /* db-schema-7.sql in Resources */ = {isa = PBXBuildFile; fileRef = FEF19F0523474943005CFE9A /* db-schema-7.sql */; };
FE759FF523741527001E78D9 /* db-schema-8.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE759FF423741527001E78D9 /* db-schema-8.sql */; };
FE759FF923742AC1001E78D9 /* NotificationCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE759FF823742AC1001E78D9 /* NotificationCategory.swift */; };
FE759FFC23742CE5001E78D9 /* NotificationCenterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE759FFA23742C48001E78D9 /* NotificationCenterDelegate.swift */; };
FE75A00323743A5C001E78D9 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A00223743A5C001E78D9 /* NotificationManager.swift */; };
FE75A006237475E2001E78D9 /* MainNotificationManagerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A004237475CD001E78D9 /* MainNotificationManagerProvider.swift */; };
FE75A008237585DC001E78D9 /* NotificationEncryptionKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A007237585DC001E78D9 /* NotificationEncryptionKeys.swift */; };
FE75A0102375F338001E78D9 /* PushEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A00E2375F324001E78D9 /* PushEventHandler.swift */; };
FE75A0122376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A0112376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift */; };
FE7F645B1D281B1C00B9DF56 /* DBCapabilitiesCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7F645A1D281B1C00B9DF56 /* DBCapabilitiesCache.swift */; };
FE7F9303200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7F9302200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift */; };
FE80BDA81D928AD2001914B0 /* TigaseSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -87,12 +114,10 @@
FE8DD9C5221B153A0090F5AA /* InviteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8DD9C4221B153A0090F5AA /* InviteViewController.swift */; };
FE8DD9C7221B15DC0090F5AA /* AbstractRosterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8DD9C6221B15DC0090F5AA /* AbstractRosterViewController.swift */; };
FE8DD9C9221B29520090F5AA /* MucNewGroupchatController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8DD9C8221B29520090F5AA /* MucNewGroupchatController.swift */; };
FE8DD9CB221DBED80090F5AA /* db-schema-4.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE8DD9CA221DBED80090F5AA /* db-schema-4.sql */; };
FE94E5251CCBA74F00FAE755 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE94E5241CCBA74F00FAE755 /* AppDelegate.swift */; };
FE94E52C1CCBA74F00FAE755 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FE94E52A1CCBA74F00FAE755 /* Main.storyboard */; };
FE94E52E1CCBA74F00FAE755 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FE94E52D1CCBA74F00FAE755 /* Assets.xcassets */; };
FE94E5311CCBA74F00FAE755 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FE94E52F1CCBA74F00FAE755 /* LaunchScreen.storyboard */; };
FE94E55E1CCCC14E00FAE755 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FE94E55D1CCCC14E00FAE755 /* libsqlite3.tbd */; };
FE9625A01D9AE7CB00D07118 /* RosterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE96259F1D9AE7CB00D07118 /* RosterProvider.swift */; };
FE9E136D1F25F5F7005C0EE5 /* ChatSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9E136C1F25F5F7005C0EE5 /* ChatSettingsViewController.swift */; };
FE9E136F1F26049A005C0EE5 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9E136E1F26049A005C0EE5 /* NotificationSettingsViewController.swift */; };
@ -113,7 +138,6 @@
FEC514241CEB2FF7003AF765 /* MucJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514231CEB2FF7003AF765 /* MucJoinViewController.swift */; };
FEC514261CEB74F8003AF765 /* BaseChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514251CEB74F8003AF765 /* BaseChatViewController.swift */; };
FEC514281CEB82E9003AF765 /* MucChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514271CEB82E9003AF765 /* MucChatViewController.swift */; };
FED353842270BFE600B69C53 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FED353822270BFD300B69C53 /* openssl.framework */; };
FED353852270BFE600B69C53 /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FED353822270BFD300B69C53 /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FED353862270BFE600B69C53 /* TigaseSwiftOMEMO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FED353732270BBA500B69C53 /* TigaseSwiftOMEMO.framework */; };
FED353872270BFE600B69C53 /* TigaseSwiftOMEMO.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FED353732270BBA500B69C53 /* TigaseSwiftOMEMO.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -133,10 +157,8 @@
FEDE939B1D0C38B000CA60A9 /* ContactFormTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDE939A1D0C38B000CA60A9 /* ContactFormTableViewCell.swift */; };
FEE097621F1FCE1800B1CEAB /* TablePicketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE097611F1FCE1800B1CEAB /* TablePicketViewController.swift */; };
FEE9608A22F191980009B191 /* MucChatSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE9608922F191980009B191 /* MucChatSettingsViewController.swift */; };
FEE9608C22F2F8950009B191 /* db-schema-6.sql in Resources */ = {isa = PBXBuildFile; fileRef = FEE9608B22F2F8950009B191 /* db-schema-6.sql */; };
FEF19F0223473B9E005CFE9A /* XmppServiceEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF19F0123473B9E005CFE9A /* XmppServiceEventHandler.swift */; };
FEF19F0423473C06005CFE9A /* MessageEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF19F0323473C06005CFE9A /* MessageEventHandler.swift */; };
FEF19F0623474943005CFE9A /* db-schema-7.sql in Resources */ = {isa = PBXBuildFile; fileRef = FEF19F0523474943005CFE9A /* db-schema-7.sql */; };
FEF19F08234751FF005CFE9A /* ChatViewItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF19F07234751FF005CFE9A /* ChatViewItemProtocol.swift */; };
FEF19F0A2347619D005CFE9A /* TasksQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF19F092347619D005CFE9A /* TasksQueue.swift */; };
FEF19F0C23476466005CFE9A /* MucEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF19F0B23476466005CFE9A /* MucEventHandler.swift */; };
@ -158,6 +180,34 @@
remoteGlobalIDString = FE4DDF521F39E0B500A4CE5A;
remoteInfo = "SiskinIM - Share";
};
FE759FA62370ACA4001E78D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = FE94E5191CCBA74F00FAE755 /* Project object */;
proxyType = 1;
remoteGlobalIDString = FE759FA02370ACA4001E78D9;
remoteInfo = NotificationService;
};
FE759FCA2370B2A4001E78D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = FE94E5191CCBA74F00FAE755 /* Project object */;
proxyType = 1;
remoteGlobalIDString = FE759FC42370B2A4001E78D9;
remoteInfo = Shared;
};
FE759FD32370B2F2001E78D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = FE94E5191CCBA74F00FAE755 /* Project object */;
proxyType = 1;
remoteGlobalIDString = FE759FC42370B2A4001E78D9;
remoteInfo = Shared;
};
FE759FE42371C83D001E78D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = FE94E5191CCBA74F00FAE755 /* Project object */;
proxyType = 1;
remoteGlobalIDString = FE759FC42370B2A4001E78D9;
remoteInfo = Shared;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -168,10 +218,34 @@
dstSubfolderSpec = 13;
files = (
FE4DDF5D1F39E0B500A4CE5A /* Siskin IM - Share.appex in Embed App Extensions */,
FE759FA82370ACA4001E78D9 /* NotificationService.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
FE759FD52370B2F3001E78D9 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
FE759FD22370B2F2001E78D9 /* Shared.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
FE759FD92370B359001E78D9 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
FE759FD82370B359001E78D9 /* openssl.framework in Embed Frameworks */,
FE759FE023719A5C001E78D9 /* TigaseSwift.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
FEF80DB71CDCC508005645A7 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 12;
@ -179,6 +253,7 @@
dstSubfolderSpec = 10;
files = (
FED353852270BFE600B69C53 /* openssl.framework in Embed Frameworks */,
FE759FCD2370B2A4001E78D9 /* Shared.framework in Embed Frameworks */,
FED353872270BFE600B69C53 /* TigaseSwiftOMEMO.framework in Embed Frameworks */,
FE80BDA81D928AD2001914B0 /* TigaseSwift.framework in Embed Frameworks */,
FE1678F522FDDC1E0013B94A /* WebRTC.framework in Embed Frameworks */,
@ -191,7 +266,7 @@
/* Begin PBXFileReference section */
FE00157C2017617B00490340 /* StreamFeaturesCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamFeaturesCache.swift; sourceTree = "<group>"; };
FE00157E2019090300490340 /* ExperimentalSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsViewController.swift; sourceTree = "<group>"; };
FE01ADA81E224CF400FA7E65 /* TigasePushNotificationsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TigasePushNotificationsModule.swift; sourceTree = "<group>"; };
FE01ADA81E224CF400FA7E65 /* SiskinPushNotificationsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiskinPushNotificationsModule.swift; sourceTree = "<group>"; };
FE137A4721F6464D006B7F7C /* UIColor_mix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor_mix.swift; sourceTree = "<group>"; };
FE137A4921F72AEA006B7F7C /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = "<group>"; };
FE137A4B21F75660006B7F7C /* ChatBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBottomView.swift; sourceTree = "<group>"; };
@ -263,6 +338,25 @@
FE719E7B22730DC3007CEEC9 /* OMEMOIdentityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OMEMOIdentityTableViewCell.swift; sourceTree = "<group>"; };
FE719E7D2274D20D007CEEC9 /* OMEMOFingerprintsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OMEMOFingerprintsController.swift; sourceTree = "<group>"; };
FE74D50F234A4E1F001A925B /* ChatTableViewSystemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewSystemCell.swift; sourceTree = "<group>"; };
FE759FA12370ACA4001E78D9 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
FE759FA32370ACA4001E78D9 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
FE759FA52370ACA4001E78D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FE759FC52370B2A4001E78D9 /* Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FE759FC72370B2A4001E78D9 /* Shared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Shared.h; sourceTree = "<group>"; };
FE759FC82370B2A4001E78D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FE759FDA2370B384001E78D9 /* Cipher+AES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cipher+AES.swift"; sourceTree = "<group>"; };
FE759FDD2371988B001E78D9 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; };
FE759FE12371C79E001E78D9 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
FE759FE72371C966001E78D9 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
FE759FEA2371F11F001E78D9 /* DBConnection_main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBConnection_main.swift; sourceTree = "<group>"; };
FE759FF423741527001E78D9 /* db-schema-8.sql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "db-schema-8.sql"; sourceTree = "<group>"; };
FE759FF823742AC1001E78D9 /* NotificationCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCategory.swift; sourceTree = "<group>"; };
FE759FFA23742C48001E78D9 /* NotificationCenterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterDelegate.swift; sourceTree = "<group>"; };
FE75A00223743A5C001E78D9 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
FE75A004237475CD001E78D9 /* MainNotificationManagerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNotificationManagerProvider.swift; sourceTree = "<group>"; };
FE75A007237585DC001E78D9 /* NotificationEncryptionKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationEncryptionKeys.swift; sourceTree = "<group>"; };
FE75A00E2375F324001E78D9 /* PushEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEventHandler.swift; sourceTree = "<group>"; };
FE75A0112376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiskinPushNotificationsModuleProvider.swift; sourceTree = "<group>"; };
FE7F645A1D281B1C00B9DF56 /* DBCapabilitiesCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DBCapabilitiesCache.swift; sourceTree = "<group>"; };
FE7F9302200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManagerScramSaltedPasswordCache.swift; sourceTree = "<group>"; };
FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TigaseSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -341,6 +435,26 @@
buildActionMask = 2147483647;
files = (
FEA370BE1F3F3F8B0050CBAC /* TigaseSwift.framework in Frameworks */,
FE759FE22371C83D001E78D9 /* Shared.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE759F9E2370ACA4001E78D9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FE759FD12370B2F2001E78D9 /* Shared.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE759FC22370B2A4001E78D9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FE759FDE2371989B001E78D9 /* libsqlite3.tbd in Frameworks */,
FE759FE82371C972001E78D9 /* libxml2.tbd in Frameworks */,
FE759FD72370B359001E78D9 /* openssl.framework in Frameworks */,
FE759FDF23719A5C001E78D9 /* TigaseSwift.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -348,11 +462,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FED353842270BFE600B69C53 /* openssl.framework in Frameworks */,
FED353862270BFE600B69C53 /* TigaseSwiftOMEMO.framework in Frameworks */,
FE5079F01CD3CA91001A015C /* Security.framework in Frameworks */,
FE1678F422FDDC1E0013B94A /* WebRTC.framework in Frameworks */,
FE94E55E1CCCC14E00FAE755 /* libsqlite3.tbd in Frameworks */,
FE759FCC2370B2A4001E78D9 /* Shared.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -362,7 +475,7 @@
FE01ADA11E214CEA00FA7E65 /* xmpp */ = {
isa = PBXGroup;
children = (
FE01ADA81E224CF400FA7E65 /* TigasePushNotificationsModule.swift */,
FE01ADA81E224CF400FA7E65 /* SiskinPushNotificationsModule.swift */,
);
path = xmpp;
sourceTree = "<group>";
@ -425,12 +538,10 @@
children = (
FE507A031CDB7B3B001A015C /* DBChatHistoryStore.swift */,
FE507A041CDB7B3B001A015C /* DBChatStore.swift */,
FE507A051CDB7B3B001A015C /* DBManager.swift */,
FE507A061CDB7B3B001A015C /* DBRosterStore.swift */,
FE3024311CE2036A00466497 /* DBVCardsCache.swift */,
FEC514211CEA3D2E003AF765 /* DBRoomsManager.swift */,
FE7F645A1D281B1C00B9DF56 /* DBCapabilitiesCache.swift */,
FE2A0E8F1F74051E006ADF08 /* DBSchemaManager.swift */,
FED353882270C1D000B69C53 /* DBOMEMOStore.swift */,
);
path = database;
@ -502,6 +613,7 @@
FE507A121CDB7B3B001A015C /* util */ = {
isa = PBXGroup;
children = (
FE719E752271B2BA007CEEC9 /* OpenSSL_AES_GCM_Engine.swift */,
FE507A131CDB7B3B001A015C /* AccountManager.swift */,
FE507A141CDB7B3B001A015C /* AvatarManager.swift */,
FEDE93881D081C3D00CA60A9 /* Settings.swift */,
@ -509,9 +621,10 @@
FE43EB541F3CC55900A4CAAD /* ImageCache.swift */,
FE7F9302200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift */,
FE137A4721F6464D006B7F7C /* UIColor_mix.swift */,
FE719E752271B2BA007CEEC9 /* OpenSSL_AES_GCM_Engine.swift */,
FE719E772271B439007CEEC9 /* MessageEncryption.swift */,
FEF19F092347619D005CFE9A /* TasksQueue.swift */,
FE75A004237475CD001E78D9 /* MainNotificationManagerProvider.swift */,
FE75A0112376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift */,
);
path = util;
sourceTree = "<group>";
@ -519,6 +632,8 @@
FE60F29A1ED48B470030D411 /* Frameworks */ = {
isa = PBXGroup;
children = (
FE759FE72371C966001E78D9 /* libxml2.tbd */,
FE759FDD2371988B001E78D9 /* libsqlite3.tbd */,
FED353822270BFD300B69C53 /* openssl.framework */,
FED353732270BBA500B69C53 /* TigaseSwiftOMEMO.framework */,
FE60F29B1ED48B470030D411 /* libxml2.tbd */,
@ -526,10 +641,70 @@
name = Frameworks;
sourceTree = "<group>";
};
FE759FA22370ACA4001E78D9 /* NotificationService */ = {
isa = PBXGroup;
children = (
FE759FE12371C79E001E78D9 /* NotificationService.entitlements */,
FE759FA32370ACA4001E78D9 /* NotificationService.swift */,
FE759FA52370ACA4001E78D9 /* Info.plist */,
);
path = NotificationService;
sourceTree = "<group>";
};
FE759FC62370B2A4001E78D9 /* Shared */ = {
isa = PBXGroup;
children = (
FE759FFF237435A0001E78D9 /* notifications */,
FE168ACC1CCD197A003F8B26 /* db-schema-1.sql */,
FE2A0E8D1F74012D006ADF08 /* db-schema-2.sql */,
FE4496C31F87911C009F649C /* db-schema-3.sql */,
FE8DD9CA221DBED80090F5AA /* db-schema-4.sql */,
FE719E732271AF88007CEEC9 /* db-schema-5.sql */,
FEE9608B22F2F8950009B191 /* db-schema-6.sql */,
FEF19F0523474943005CFE9A /* db-schema-7.sql */,
FE759FF423741527001E78D9 /* db-schema-8.sql */,
FE759FDC23719865001E78D9 /* database */,
FE759FD62370B316001E78D9 /* util */,
FE759FC72370B2A4001E78D9 /* Shared.h */,
FE759FC82370B2A4001E78D9 /* Info.plist */,
FE759FF823742AC1001E78D9 /* NotificationCategory.swift */,
);
path = Shared;
sourceTree = "<group>";
};
FE759FD62370B316001E78D9 /* util */ = {
isa = PBXGroup;
children = (
FE759FDA2370B384001E78D9 /* Cipher+AES.swift */,
);
path = util;
sourceTree = "<group>";
};
FE759FDC23719865001E78D9 /* database */ = {
isa = PBXGroup;
children = (
FE507A051CDB7B3B001A015C /* DBManager.swift */,
FE2A0E8F1F74051E006ADF08 /* DBSchemaManager.swift */,
FE759FEA2371F11F001E78D9 /* DBConnection_main.swift */,
);
path = database;
sourceTree = "<group>";
};
FE759FFF237435A0001E78D9 /* notifications */ = {
isa = PBXGroup;
children = (
FE75A00223743A5C001E78D9 /* NotificationManager.swift */,
FE75A007237585DC001E78D9 /* NotificationEncryptionKeys.swift */,
);
path = notifications;
sourceTree = "<group>";
};
FE94E5181CCBA74F00FAE755 = {
isa = PBXGroup;
children = (
FE1678F322FDDC1D0013B94A /* WebRTC.framework */,
FE759FA22370ACA4001E78D9 /* NotificationService */,
FE759FC62370B2A4001E78D9 /* Shared */,
FE60F29A1ED48B470030D411 /* Frameworks */,
FE94E55D1CCCC14E00FAE755 /* libsqlite3.tbd */,
FE94E5221CCBA74F00FAE755 /* Products */,
@ -546,6 +721,8 @@
children = (
FE94E5211CCBA74F00FAE755 /* Siskin.app */,
FE4DDF531F39E0B500A4CE5A /* Siskin IM - Share.appex */,
FE759FA12370ACA4001E78D9 /* NotificationService.appex */,
FE759FC52370B2A4001E78D9 /* Shared.framework */,
);
name = Products;
sourceTree = "<group>";
@ -557,13 +734,6 @@
FE86C4481F7BFF93009E3CB8 /* SiskinIM-Bridging-Header.h */,
FE94E5241CCBA74F00FAE755 /* AppDelegate.swift */,
FE94E52D1CCBA74F00FAE755 /* Assets.xcassets */,
FE168ACC1CCD197A003F8B26 /* db-schema-1.sql */,
FE2A0E8D1F74012D006ADF08 /* db-schema-2.sql */,
FE4496C31F87911C009F649C /* db-schema-3.sql */,
FE8DD9CA221DBED80090F5AA /* db-schema-4.sql */,
FE719E732271AF88007CEEC9 /* db-schema-5.sql */,
FEE9608B22F2F8950009B191 /* db-schema-6.sql */,
FEF19F0523474943005CFE9A /* db-schema-7.sql */,
FE94E5321CCBA74F00FAE755 /* Info.plist */,
FE94E52F1CCBA74F00FAE755 /* LaunchScreen.storyboard */,
FE94E52A1CCBA74F00FAE755 /* Main.storyboard */,
@ -580,6 +750,7 @@
FEDE938A1D08A4DD00CA60A9 /* vcard */,
FE01ADA11E214CEA00FA7E65 /* xmpp */,
FE233CDA21EA03DD00099281 /* Info.storyboard */,
FE759FFA23742C48001E78D9 /* NotificationCenterDelegate.swift */,
);
path = SiskinIM;
sourceTree = "<group>";
@ -600,6 +771,7 @@
FEF19F0F2348A046005CFE9A /* PresenceRosterEventHandler.swift */,
FEF19F112348A3B8005CFE9A /* AvatarEventHandler.swift */,
FEF19F132348B655005CFE9A /* DiscoEventHandler.swift */,
FE75A00E2375F324001E78D9 /* PushEventHandler.swift */,
);
path = service;
sourceTree = "<group>";
@ -631,6 +803,17 @@
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
FE759FC02370B2A4001E78D9 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
FE759FC92370B2A4001E78D9 /* Shared.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
FE4DDF521F39E0B500A4CE5A /* Siskin IM - Share */ = {
isa = PBXNativeTarget;
@ -643,12 +826,51 @@
buildRules = (
);
dependencies = (
FE759FE52371C83D001E78D9 /* PBXTargetDependency */,
);
name = "Siskin IM - Share";
productName = "SiskinIM - Share";
productReference = FE4DDF531F39E0B500A4CE5A /* Siskin IM - Share.appex */;
productType = "com.apple.product-type.app-extension";
};
FE759FA02370ACA4001E78D9 /* NotificationService */ = {
isa = PBXNativeTarget;
buildConfigurationList = FE759FAB2370ACA4001E78D9 /* Build configuration list for PBXNativeTarget "NotificationService" */;
buildPhases = (
FE759F9D2370ACA4001E78D9 /* Sources */,
FE759F9E2370ACA4001E78D9 /* Frameworks */,
FE759F9F2370ACA4001E78D9 /* Resources */,
FE759FD52370B2F3001E78D9 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
FE759FD42370B2F2001E78D9 /* PBXTargetDependency */,
);
name = NotificationService;
productName = NotificationService;
productReference = FE759FA12370ACA4001E78D9 /* NotificationService.appex */;
productType = "com.apple.product-type.app-extension";
};
FE759FC42370B2A4001E78D9 /* Shared */ = {
isa = PBXNativeTarget;
buildConfigurationList = FE759FCE2370B2A4001E78D9 /* Build configuration list for PBXNativeTarget "Shared" */;
buildPhases = (
FE759FC02370B2A4001E78D9 /* Headers */,
FE759FC12370B2A4001E78D9 /* Sources */,
FE759FC22370B2A4001E78D9 /* Frameworks */,
FE759FC32370B2A4001E78D9 /* Resources */,
FE759FD92370B359001E78D9 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Shared;
productName = Shared;
productReference = FE759FC52370B2A4001E78D9 /* Shared.framework */;
productType = "com.apple.product-type.framework";
};
FE94E5201CCBA74F00FAE755 /* Siskin IM */ = {
isa = PBXNativeTarget;
buildConfigurationList = FE94E54B1CCBA74F00FAE755 /* Build configuration list for PBXNativeTarget "Siskin IM" */;
@ -665,6 +887,8 @@
);
dependencies = (
FE4DDF5C1F39E0B500A4CE5A /* PBXTargetDependency */,
FE759FA72370ACA4001E78D9 /* PBXTargetDependency */,
FE759FCB2370B2A4001E78D9 /* PBXTargetDependency */,
);
name = "Siskin IM";
productName = SiskinIM;
@ -677,7 +901,7 @@
FE94E5191CCBA74F00FAE755 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1000;
ORGANIZATIONNAME = "Tigase, Inc.";
TargetAttributes = {
@ -695,6 +919,17 @@
};
};
};
FE759FA02370ACA4001E78D9 = {
CreatedOnToolsVersion = 11.2;
DevelopmentTeam = YBEYW6E35C;
ProvisioningStyle = Automatic;
};
FE759FC42370B2A4001E78D9 = {
CreatedOnToolsVersion = 11.2;
DevelopmentTeam = YBEYW6E35C;
LastSwiftMigration = 1120;
ProvisioningStyle = Automatic;
};
FE94E5201CCBA74F00FAE755 = {
CreatedOnToolsVersion = 7.3;
DevelopmentTeam = YBEYW6E35C;
@ -731,6 +966,8 @@
targets = (
FE94E5201CCBA74F00FAE755 /* Siskin IM */,
FE4DDF521F39E0B500A4CE5A /* Siskin IM - Share */,
FE759FA02370ACA4001E78D9 /* NotificationService */,
FE759FC42370B2A4001E78D9 /* Shared */,
);
};
/* End PBXProject section */
@ -745,23 +982,38 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
FE759F9F2370ACA4001E78D9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
FE759FC32370B2A4001E78D9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FE759FF22371F21C001E78D9 /* db-schema-6.sql in Resources */,
FE759FEF2371F21C001E78D9 /* db-schema-3.sql in Resources */,
FE759FED2371F213001E78D9 /* db-schema-2.sql in Resources */,
FE759FF02371F21C001E78D9 /* db-schema-4.sql in Resources */,
FE759FF32371F21C001E78D9 /* db-schema-7.sql in Resources */,
FE759FF12371F21C001E78D9 /* db-schema-5.sql in Resources */,
FE759FF523741527001E78D9 /* db-schema-8.sql in Resources */,
FE759FEE2371F217001E78D9 /* db-schema-1.sql in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE94E51F1CCBA74F00FAE755 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FE233CDB21EA03DD00099281 /* Info.storyboard in Resources */,
FE8DD9CB221DBED80090F5AA /* db-schema-4.sql in Resources */,
FE4496C51F87934E009F649C /* db-schema-3.sql in Resources */,
FE719E742271AF88007CEEC9 /* db-schema-5.sql in Resources */,
FE94E5311CCBA74F00FAE755 /* LaunchScreen.storyboard in Resources */,
FE2A0E8E1F74012D006ADF08 /* db-schema-2.sql in Resources */,
FEE9608C22F2F8950009B191 /* db-schema-6.sql in Resources */,
FE8DD9C3221B118E0090F5AA /* Groupchat.storyboard in Resources */,
FE94E52E1CCBA74F00FAE755 /* Assets.xcassets in Resources */,
FE4071E621E262D900F09B58 /* VoIP.storyboard in Resources */,
FE94E52C1CCBA74F00FAE755 /* Main.storyboard in Resources */,
FEF19F0623474943005CFE9A /* db-schema-7.sql in Resources */,
FE168ACD1CCD197A003F8B26 /* db-schema-1.sql in Resources */,
FE2809812167CE18002F5BD0 /* server_features_list.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -812,6 +1064,28 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
FE759F9D2370ACA4001E78D9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FE759FA42370ACA4001E78D9 /* NotificationService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE759FC12370B2A4001E78D9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FE75A008237585DC001E78D9 /* NotificationEncryptionKeys.swift in Sources */,
FE75A00323743A5C001E78D9 /* NotificationManager.swift in Sources */,
FE759FDB2370B384001E78D9 /* Cipher+AES.swift in Sources */,
FE759FE92371DDD1001E78D9 /* DBManager.swift in Sources */,
FE759FF923742AC1001E78D9 /* NotificationCategory.swift in Sources */,
FE759FEC2371F1A5001E78D9 /* DBSchemaManager.swift in Sources */,
FE759FEB2371F11F001E78D9 /* DBConnection_main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE94E51D1CCBA74F00FAE755 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -842,7 +1116,8 @@
FE36B3C821FA52E000D1F037 /* EmptyViewController.swift in Sources */,
FE507A161CDB7B3B001A015C /* ChatsListViewController.swift in Sources */,
FE8DD9C9221B29520090F5AA /* MucNewGroupchatController.swift in Sources */,
FE01ADA91E224CF400FA7E65 /* TigasePushNotificationsModule.swift in Sources */,
FE75A0122376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift in Sources */,
FE01ADA91E224CF400FA7E65 /* SiskinPushNotificationsModule.swift in Sources */,
FE507A1F1CDB7B3B001A015C /* AccountTableViewCell.swift in Sources */,
FEF19F122348A3B8005CFE9A /* AvatarEventHandler.swift in Sources */,
FE7F645B1D281B1C00B9DF56 /* DBCapabilitiesCache.swift in Sources */,
@ -856,12 +1131,13 @@
FE36B3C621F8E87700D1F037 /* CustomTableViewCell.swift in Sources */,
FE31291C222C0D1500A92863 /* AvatarView.swift in Sources */,
FE719E7E2274D20D007CEEC9 /* OMEMOFingerprintsController.swift in Sources */,
FE759FFC23742CE5001E78D9 /* NotificationCenterDelegate.swift in Sources */,
FEF19F0C23476466005CFE9A /* MucEventHandler.swift in Sources */,
FE1DCCA21EA52CE200850563 /* DataFormController.swift in Sources */,
FE2A0E901F74051E006ADF08 /* DBSchemaManager.swift in Sources */,
FE6545641E9E8B67006A14AC /* ServerSelectorTableViewCell.swift in Sources */,
FE31291A22240BEB00A92863 /* PEPBookmarksModule_extension.swift in Sources */,
FE00157F2019090300490340 /* ExperimentalSettingsViewController.swift in Sources */,
FE75A006237475E2001E78D9 /* MainNotificationManagerProvider.swift in Sources */,
FEDE93871D07564F00CA60A9 /* SwitchTableViewCell.swift in Sources */,
FE507A171CDB7B3B001A015C /* ChatTableViewCell.swift in Sources */,
FE31DDE4201261A200C2AB1D /* DNSSrvDiskCache.swift in Sources */,
@ -904,6 +1180,7 @@
FE507A221CDB7B3B001A015C /* AvatarStatusView.swift in Sources */,
FE719E782271B439007CEEC9 /* MessageEncryption.swift in Sources */,
FE3DCCEE1FE18334008B6C8B /* CertificateErrorAlert.swift in Sources */,
FE75A0102375F338001E78D9 /* PushEventHandler.swift in Sources */,
FEDCBF671D9C3EE700AE9129 /* RosterProviderFlat.swift in Sources */,
FE9E136D1F25F5F7005C0EE5 /* ChatSettingsViewController.swift in Sources */,
FEF19F08234751FF005CFE9A /* ChatViewItemProtocol.swift in Sources */,
@ -918,7 +1195,6 @@
FEFBEC5D233A7EAE00E5CCA5 /* AppearanceViewController.swift in Sources */,
FE3024321CE2036A00466497 /* DBVCardsCache.swift in Sources */,
FE74D510234A4E1F001A925B /* ChatTableViewSystemCell.swift in Sources */,
FE507A1B1CDB7B3B001A015C /* DBManager.swift in Sources */,
FE507A241CDB7B3B001A015C /* GlobalSplitViewController.swift in Sources */,
FE233CD521E6846E00099281 /* CameraPreviewView.swift in Sources */,
FEF19F0E23479F4C005CFE9A /* ChatViewDataSource.swift in Sources */,
@ -936,6 +1212,26 @@
target = FE4DDF521F39E0B500A4CE5A /* Siskin IM - Share */;
targetProxy = FE4DDF5B1F39E0B500A4CE5A /* PBXContainerItemProxy */;
};
FE759FA72370ACA4001E78D9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = FE759FA02370ACA4001E78D9 /* NotificationService */;
targetProxy = FE759FA62370ACA4001E78D9 /* PBXContainerItemProxy */;
};
FE759FCB2370B2A4001E78D9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = FE759FC42370B2A4001E78D9 /* Shared */;
targetProxy = FE759FCA2370B2A4001E78D9 /* PBXContainerItemProxy */;
};
FE759FD42370B2F2001E78D9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = FE759FC42370B2A4001E78D9 /* Shared */;
targetProxy = FE759FD32370B2F2001E78D9 /* PBXContainerItemProxy */;
};
FE759FE52371C83D001E78D9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = FE759FC42370B2A4001E78D9 /* Shared */;
targetProxy = FE759FE42371C83D001E78D9 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -984,7 +1280,7 @@
INFOPLIST_FILE = "SiskinIM - Share/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = "org.tigase.messenger.mobile.Tigase-Messenger---Share";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1011,7 +1307,7 @@
INFOPLIST_FILE = "SiskinIM - Share/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = "org.tigase.messenger.mobile.Tigase-Messenger---Share";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1019,6 +1315,151 @@
};
name = Release;
};
FE759FA92370ACA4001E78D9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = YBEYW6E35C;
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = (
"$(SDK_DIR)/usr/include",
"$(SDK_DIR)/usr/include/libxml2/**",
);
INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.tigase.messenger.mobile.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
FE759FAA2370ACA4001E78D9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = YBEYW6E35C;
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = (
"$(SDK_DIR)/usr/include",
"$(SDK_DIR)/usr/include/libxml2/**",
);
INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.tigase.messenger.mobile.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
FE759FCF2370B2A4001E78D9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = YBEYW6E35C;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = (
"$(SDK_DIR)/usr/include",
"$(SDK_DIR)/usr/include/libxml2/**",
);
INFOPLIST_FILE = Shared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.tigase.messenger.mobile.Shared;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
FE759FD02370B2A4001E78D9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = YBEYW6E35C;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = (
"$(SDK_DIR)/usr/include",
"$(SDK_DIR)/usr/include/libxml2/**",
);
INFOPLIST_FILE = Shared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.tigase.messenger.mobile.Shared;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
FE94E5491CCBA74F00FAE755 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1158,7 +1599,7 @@
INFOPLIST_FILE = SiskinIM/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = org.tigase.messenger.mobile;
PRODUCT_NAME = Siskin;
PROVISIONING_PROFILE = "";
@ -1191,7 +1632,7 @@
INFOPLIST_FILE = SiskinIM/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 5.4;
MARKETING_VERSION = 5.5;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.tigase.messenger.mobile;
PRODUCT_NAME = Siskin;
@ -1214,6 +1655,24 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
FE759FAB2370ACA4001E78D9 /* Build configuration list for PBXNativeTarget "NotificationService" */ = {
isa = XCConfigurationList;
buildConfigurations = (
FE759FA92370ACA4001E78D9 /* Debug */,
FE759FAA2370ACA4001E78D9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
FE759FCE2370B2A4001E78D9 /* Build configuration list for PBXNativeTarget "Shared" */ = {
isa = XCConfigurationList;
buildConfigurations = (
FE759FCF2370B2A4001E78D9 /* Debug */,
FE759FD02370B2A4001E78D9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
FE94E51C1CCBA74F00FAE755 /* Build configuration list for PBXProject "SiskinIM" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View file

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FE759FA02370ACA4001E78D9"
BuildableName = "NotificationService.appex"
BlueprintName = "NotificationService"
ReferencedContainer = "container:SiskinIM.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FE94E5201CCBA74F00FAE755"
BuildableName = "Siskin.app"
BlueprintName = "Siskin IM"
ReferencedContainer = "container:SiskinIM.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "1"
BundleIdentifier = "com.apple.stocks"
RemotePath = "/var/containers/Bundle/Application/ED135793-AFC6-47C6-BEA2-1FCB1255D81F/Stocks.app">
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FE94E5201CCBA74F00FAE755"
BuildableName = "Siskin.app"
BlueprintName = "Siskin IM"
ReferencedContainer = "container:SiskinIM.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FE94E5201CCBA74F00FAE755"
BuildableName = "Siskin.app"
BlueprintName = "Siskin IM"
ReferencedContainer = "container:SiskinIM.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -9,11 +9,21 @@
<key>orderHint</key>
<integer>5</integer>
</dict>
<key>Shared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>7</integer>
</dict>
<key>SiskinIM - Share.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
</dict>
<key>SiskinIM - Shared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>8</integer>
</dict>
<key>SiskinIM-Share.xcscheme</key>
<dict>
<key>orderHint</key>
@ -37,6 +47,11 @@
<key>primary</key>
<true/>
</dict>
<key>FE759FA02370ACA4001E78D9</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>FE94E5201CCBA74F00FAE755</key>
<dict>
<key>primary</key>

View file

@ -23,21 +23,12 @@ import UIKit
import UserNotifications
import TigaseSwift
//import CallKit
import Shared
import WebRTC
import BackgroundTasks
extension DBConnection {
static var main: DBConnection = {
let conn = try! DBConnection(dbFilename: "mobile_messenger1.db");
try! DBSchemaManager(dbConnection: conn).upgradeSchema();
return conn;
}();
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
class AppDelegate: UIResponder, UIApplicationDelegate {
fileprivate let backgroundRefreshTaskIdentifier = "org.tigase.messenger.mobile.refresh";
@ -49,12 +40,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return DBConnection.main;
}
let notificationCenterDelegate = NotificationCenterDelegate();
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13, *) {
BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundRefreshTaskIdentifier, using: nil) { (task) in
self.handleAppRefresh(task: task as! BGAppRefreshTask);
}
}
try! DBConnection.migrateToGroupIfNeeded();
RTCInitFieldTrialDictionary([:]);
RTCInitializeSSL();
RTCSetupInternalTracer();
@ -62,11 +56,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
Settings.initialize();
AccountSettings.initialize();
Appearance.sync();
NotificationManager.instance.initialize(provider: MainNotificationManagerProvider());
xmppService.initialize();
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
// sending notifications not granted!
}
UNUserNotificationCenter.current().delegate = self;
UNUserNotificationCenter.current().delegate = self.notificationCenterDelegate;
let categories = [
UNNotificationCategory(identifier: "MESSAGE", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: "New message", options: [.customDismissAction])
];
UNUserNotificationCenter.current().setNotificationCategories(Set(categories));
application.registerForRemoteNotifications();
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.newMessage), name: DBChatHistoryStore.MESSAGE_NEW, object: nil);
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.unreadMessagesCountChanged), name: DBChatStore.UNREAD_MESSAGES_COUNT_CHANGED, object: nil);
@ -142,6 +141,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if #available(iOS 13, *) {
scheduleAppRefresh();
}
print("keep online task ended", taskId, NSDate());
application.endBackgroundTask(taskId);
}
@ -151,7 +151,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
let toDiscard = notifications.filter({(notification) in notification.request.content.categoryIdentifier == "MESSAGE_NO_SENDER" || notification.request.content.categoryIdentifier == "UNSENT_MESSAGES"}).map({ (notiication) -> String in
let toDiscard = notifications.filter({(notification) in
switch NotificationCategory.from(identifier: notification.request.content.categoryIdentifier) {
case .UNSENT_MESSAGES:
return true;
case .MESSAGE:
return notification.request.content.userInfo["sender"] as? String == nil;
default:
return false;
}
}).map({ (notiication) -> String in
return notiication.request.identifier;
});
guard !toDiscard.isEmpty else {
@ -167,6 +176,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: backgroundRefreshTaskIdentifier);
}
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// TODO: XmppService::initialize() call in application:willFinishLaunchingWithOptions results in starting a connections while it may not always be desired if ie. app is relauched in the background due to crash
// Shouldn't it wait for reconnection till it becomes active? or background refresh task is called?
xmppService.applicationState = .active;
applicationKeepOnlineOnAwayFinished(application);
@ -315,216 +328,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let content = response.notification.request.content;
let userInfo = content.userInfo;
if content.categoryIdentifier == "ERROR" {
if userInfo["cert-name"] != nil {
let accountJid = BareJID(userInfo["account"] as! String);
let alert = CertificateErrorAlert.create(domain: accountJid.domain, certName: userInfo["cert-name"] as! String, certHash: userInfo["cert-hash-sha1"] as! String, issuerName: userInfo["issuer-name"] as? String, issuerHash: userInfo["issuer-hash-sha1"] as? String, onAccept: {
print("accepted certificate!");
guard let account = AccountManager.getAccount(for: accountJid) else {
return;
}
var certInfo = account.serverCertificate;
certInfo?["accepted"] = true as NSObject;
account.serverCertificate = certInfo;
account.active = true;
AccountSettings.LastError(accountJid).set(string: nil);
AccountManager.save(account: account);
}, onDeny: nil);
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
}
if let authError = userInfo["auth-error-type"] {
let accountJid = BareJID(userInfo["account"] as! String);
let alert = UIAlertController(title: "Authentication issue", message: "Authentication for account \(accountJid) failed: \(authError)\nVerify provided account password.", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
} else {
let alert = UIAlertController(title: content.title, message: content.body, preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
}
}
if content.categoryIdentifier == "SUBSCRIPTION_REQUEST" {
let userInfo = content.userInfo;
let senderJid = BareJID(userInfo["sender"] as! String);
let accountJid = BareJID(userInfo["account"] as! String);
var senderName = userInfo["senderName"] as! String;
if senderName != senderJid.stringValue {
senderName = "\(senderName) (\(senderJid.stringValue))";
}
let alert = UIAlertController(title: "Subscription request", message: "Received presence subscription request from\n\(senderName)\non account \(accountJid.stringValue)", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: {(action) in
guard let presenceModule: PresenceModule = self.xmppService.getClient(forJid: accountJid)?.context.modulesManager.getModule(PresenceModule.ID) else {
return;
}
presenceModule.subscribed(by: JID(senderJid));
if let sessionObject = self.xmppService.getClient(forJid: accountJid)?.context.sessionObject {
let subscription = RosterModule.getRosterStore(sessionObject).get(for: JID(senderJid))?.subscription ?? RosterItem.Subscription.none;
guard !subscription.isTo else {
return;
}
}
if (Settings.AutoSubscribeOnAcceptedSubscriptionRequest.getBool()) {
presenceModule.subscribe(to: JID(senderJid));
} else {
let alert2 = UIAlertController(title: "Subscribe to " + senderName, message: "Do you wish to subscribe to \n\(senderName)\non account \(accountJid.stringValue)", preferredStyle: .alert);
alert2.addAction(UIAlertAction(title: "Accept", style: .default, handler: {(action) in
presenceModule.subscribe(to: JID(senderJid));
}));
alert2.addAction(UIAlertAction(title: "Reject", style: .destructive, handler: nil));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert2, animated: true, completion: nil);
}
}));
alert.addAction(UIAlertAction(title: "Reject", style: .destructive, handler: {(action) in
guard let presenceModule: PresenceModule = self.xmppService.getClient(forJid: accountJid)?.context.modulesManager.getModule(PresenceModule.ID) else {
return;
}
presenceModule.unsubscribed(by: JID(senderJid));
}));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
}
if content.categoryIdentifier == "MUC_ROOM_INVITATION" {
guard let account = BareJID(content.userInfo["account"] as? String), let roomJid: BareJID = BareJID(content.userInfo["roomJid"] as? String) else {
return;
}
let password = content.userInfo["password"] as? String;
let navController = UIStoryboard(name: "Groupchat", bundle: nil).instantiateViewController(withIdentifier: "MucJoinNavigationController") as! UINavigationController;
let controller = navController.visibleViewController! as! MucJoinViewController;
_ = controller.view;
controller.accountTextField.text = account.stringValue;
controller.roomTextField.text = roomJid.localPart;
controller.serverTextField.text = roomJid.domain;
controller.passwordTextField.text = password;
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
// let navController = UINavigationController(rootViewController: controller);
navController.modalPresentationStyle = .formSheet;
topController?.present(navController, animated: true, completion: nil);
}
if content.categoryIdentifier == "MESSAGE" {
let senderJid = BareJID(userInfo["sender"] as! String);
let accountJid = BareJID(userInfo["account"] as! String);
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
if topController != nil {
let controller = (userInfo["type"] as? String == "muc") ? UIStoryboard(name: "Groupchat", bundle: nil).instantiateViewController(withIdentifier: "RoomViewNavigationController") : topController!.storyboard?.instantiateViewController(withIdentifier: "ChatViewNavigationController");
let navigationController = controller as? UINavigationController;
let destination = navigationController?.visibleViewController ?? controller;
if let baseChatViewController = destination as? BaseChatViewController {
baseChatViewController.account = accountJid;
baseChatViewController.jid = senderJid;
}
destination?.hidesBottomBarWhenPushed = true;
topController!.showDetailViewController(controller!, sender: self);
} else {
print("No top controller!");
}
}
#if targetEnvironment(simulator)
#else
if content.categoryIdentifier == "CALL" {
let senderName = userInfo["senderName"] as! String;
let senderJid = JID(userInfo["sender"] as! String);
let accountJid = BareJID(userInfo["account"] as! String);
let sdp = userInfo["sdpOffer"] as! String;
let sid = userInfo["sid"] as! String;
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
if let session = JingleManager.instance.session(for: accountJid, with: senderJid, sid: sid) {
// can still can be received!
let alert = UIAlertController(title: "Incoming call", message: "Incoming call from \(senderName)", preferredStyle: .alert);
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .denied, .restricted:
break;
default:
alert.addAction(UIAlertAction(title: "Video call", style: .default, handler: { action in
// accept video
VideoCallController.accept(session: session, sdpOffer: sdp, withAudio: true, withVideo: true, sender: topController!);
}))
}
alert.addAction(UIAlertAction(title: "Audio call", style: .default, handler: { action in
VideoCallController.accept(session: session, sdpOffer: sdp, withAudio: true, withVideo: false, sender: topController!);
}));
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: { action in
_ = session.decline();
}));
topController?.present(alert, animated: true, completion: nil);
} else {
// call missed...
let alert = UIAlertController(title: "Missed call", message: "Missed incoming call from \(senderName)", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil));
topController?.present(alert, animated: true, completion: nil);
}
}
#endif
completionHandler();
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if notification.request.content.categoryIdentifier == "MESSAGE" || notification.request.content.categoryIdentifier == "MESSAGE_NO_SENDER" {
let account = notification.request.content.userInfo["account"] as? String;
let sender = notification.request.content.userInfo["sender"] as? String;
if (isChatVisible(account: account, with: sender) && xmppService.applicationState == .active) {
completionHandler([]);
} else {
completionHandler([.alert, .sound]);
}
} else {
completionHandler([.alert, .sound]);
}
}
func isChatVisible(account acc: String?, with j: String?) -> Bool {
static func isChatVisible(account acc: String?, with j: String?) -> Bool {
guard let account = acc, let jid = j else {
return false;
}
@ -580,12 +384,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
print("Device Token:", tokenString)
print("Device Token:", deviceToken.map({ String(format: "%02x", $0 )}).joined());
Settings.DeviceToken.setValue(tokenString);
PushEventHandler.instance.deviceId = tokenString;
// Settings.DeviceToken.setValue(tokenString);
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register:", error);
Settings.DeviceToken.setValue(nil);
PushEventHandler.instance.deviceId = nil;
// Settings.DeviceToken.setValue(nil);
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
@ -598,125 +404,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if let account = JID(userInfo[AnyHashable("account")] as? String) {
let sender = JID(userInfo[AnyHashable("sender")] as? String);
let body = userInfo[AnyHashable("body")] as? String;
if let unreadMessages = userInfo[AnyHashable("unread-messages")] as? Int, unreadMessages == 0 && sender == nil && body == nil {
let state = self.xmppService.getClient(forJid: account.bareJid)?.state;
print("unread messages retrieved, client state =", state as Any);
if state != .connected {
dismissPushNotifications(for: account) {
dismissNewMessageNotifications(for: account) {
completionHandler(.newData);
}
return;
}
} else if body != nil {
if sender != nil {
let nickname = userInfo[AnyHashable("nickname")] as? String;
let isMuc = DBChatStore.instance.getChat(for: account.bareJid, with: sender!.bareJid) as? DBRoom != nil;
notifyNewMessage(account: account, sender: sender!, body: body!, type: (nickname == nil && !isMuc) ? "chat" : "muc", data: userInfo, isPush: true) {
completionHandler(.newData);
}
return;
} else {
notifyNewMessageWaiting(account: account) {
completionHandler(.newData);
}
return;
}
NotificationManager.instance.notifyNewMessage(account: account.bareJid, sender: sender?.bareJid, type: .unknown, nickname: userInfo[AnyHashable("nickname")] as? String, body: body!);
}
}
completionHandler(.newData);
}
func notifyNewMessageWaiting(account: JID, completionHandler: @escaping ()->Void) {
let threadId = "account=" + account.stringValue;
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
let content = UNMutableNotificationContent();
content.body = "New message received!";
content.sound = UNNotificationSound.default;
content.categoryIdentifier = "MESSAGE_NO_SENDER";
content.userInfo = ["account": account.stringValue, "push": true];
content.threadIdentifier = threadId;
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil), withCompletionHandler: {(error) in
print("message notification error", error as Any);
self.updateApplicationIconBadgeNumber(completionHandler: completionHandler);
});
}
}
func notifyNewMessage(account: BareJID, sender: BareJID, body: String, type: String?, isPush: Bool, completionHandler: (()->Void)?) {
notifyNewMessage(account: JID(account), sender: JID(sender), body: body, type: type, data: [:], isPush: isPush, completionHandler: completionHandler);
}
func notifyNewMessage(account: JID, sender: JID, body: String, type: String?, data userInfo: [AnyHashable:Any], isPush: Bool, completionHandler: (()->Void)?) {
guard userInfo["carbonAction"] == nil else {
return;
}
let isMuc = DBChatStore.instance.getChat(for: account.bareJid, with: sender.bareJid) as? DBRoom != nil;
var alertBody: String?;
switch (isMuc ? "muc" : (type ?? "chat")) {
case "muc":
guard let mucModule: MucModule = xmppService.getClient(forJid: account.bareJid)?.modulesManager.getModule(MucModule.ID) else {
return;
}
guard let room: DBRoom = mucModule.roomsManager.getRoom(for: sender.bareJid) as? DBRoom else {
return;
}
guard let nick = userInfo["senderName"] as? String ?? userInfo[AnyHashable("nickname")] as? String else {
return;
}
switch room.options.notifications {
case .none:
return;
case .always:
alertBody = "\(nick): \(body)";
case .mention:
let myNickname = room.nickname;
if body.contains(myNickname) {
alertBody = "\(nick) mentioned you: \(body)";
} else {
return;
}
}
default:
guard let sessionObject = xmppService.getClient(forJid: account.bareJid)?.sessionObject else {
return;
}
if let senderRosterItem = RosterModule.getRosterStore(sessionObject).get(for: sender.withoutResource) {
let senderName = senderRosterItem.name ?? sender.withoutResource.stringValue;
alertBody = "\(senderName): \(body)";
} else {
guard Settings.NotificationsFromUnknown.getBool() else {
return;
}
alertBody = "Message from unknown: " + sender.withoutResource.stringValue;
}
}
let threadId = "account=" + account.stringValue + "|sender=" + sender.bareJid.stringValue;
let id = threadId + ":body=" + body.prefix(400);
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
if notifications.filter({(notification) in notification.request.identifier == id}).isEmpty {
let content = UNMutableNotificationContent();
//content.title = "Received new message from \(senderName!)";
content.body = alertBody!;
content.sound = UNNotificationSound.default;
content.userInfo = ["account": account.stringValue, "sender": sender.bareJid.stringValue, "push": isPush, "type": (type ?? "chat")];
content.categoryIdentifier = "MESSAGE";
content.threadIdentifier = threadId;
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: id, content: content, trigger: nil), withCompletionHandler: {(error) in
print("message notification error", error as Any);
self.updateApplicationIconBadgeNumber(completionHandler: completionHandler);
});
}
}
}
@objc func newMessage(_ notification: NSNotification) {
guard let message = notification.object as? ChatMessage else {
return;
@ -725,12 +430,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return;
}
notifyNewMessage(account: message.account, sender: message.jid, body: message.message, type: message.authorNickname != nil ? "muc" : "chat", isPush: false, completionHandler: nil);
NotificationManager.instance.notifyNewMessage(account: message.account, sender: message.jid, type: message.authorNickname != nil ? .groupchat : .chat, nickname: message.authorNickname, body: message.message);
}
func dismissPushNotifications(for account: JID, completionHandler: (()-> Void)?) {
func dismissNewMessageNotifications(for account: JID, completionHandler: (()-> Void)?) {
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
let toRemove = notifications.filter({ (notification) in notification.request.content.categoryIdentifier == "MESSAGE" || notification.request.content.categoryIdentifier == "MESSAGE_NO_SENDER" }).filter({ (notification) in (notification.request.content.userInfo["account"] as? String) == account.stringValue && (notification.request.content.userInfo["push"] as? Bool ?? false) }).map({ (notification) in notification.request.identifier });
let toRemove = notifications.filter({ (notification) in
switch NotificationCategory.from(identifier: notification.request.content.categoryIdentifier) {
case .MESSAGE:
return (notification.request.content.userInfo["account"] as? String) == account.stringValue;
default:
return false;
}
}).map({ (notification) in notification.request.identifier });
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: toRemove);
self.updateApplicationIconBadgeNumber(completionHandler: completionHandler);
}
@ -796,23 +508,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func updateApplicationIconBadgeNumber(completionHandler: (()->Void)?) {
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
var unreadChats = Set(notifications.filter({(notification) in notification.request.content.categoryIdentifier == "MESSAGE" }).map({ (notification) in
return notification.request.content.threadIdentifier;
}));
DBChatStore.instance.getChats().filter({ chat -> Bool in
return chat.unread > 0;
}).forEach { (chat) in
unreadChats.insert("account=" + chat.account.stringValue + "|sender=" + chat.jid.bareJid.stringValue)
}
let badge = unreadChats.count;
NotificationManager.instance.provider.countBadge(withThreadId: nil, completionHandler: { count in
DispatchQueue.main.async {
print("setting badge to", badge);
UIApplication.shared.applicationIconBadgeNumber = badge;
print("setting badge to", count);
UIApplication.shared.applicationIconBadgeNumber = count;
completionHandler?();
}
}
});
}
@objc func serverCertificateError(_ notification: NSNotification) {

View file

@ -0,0 +1,301 @@
//
// NotificationCenterDelegate.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 UIKit
import Shared
import WebRTC
import TigaseSwift
import UserNotifications
class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
switch NotificationCategory.from(identifier: notification.request.content.categoryIdentifier) {
case .MESSAGE:
let account = notification.request.content.userInfo["account"] as? String;
let sender = notification.request.content.userInfo["sender"] as? String;
if (AppDelegate.isChatVisible(account: account, with: sender) && XmppService.instance.applicationState == .active) {
completionHandler([]);
} else {
completionHandler([.alert, .sound]);
}
default:
completionHandler([.alert, .sound]);
}
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let content = response.notification.request.content;
switch NotificationCategory.from(identifier: response.notification.request.content.categoryIdentifier) {
case .ERROR:
didReceive(error: content, withCompletionHandler: completionHandler);
case .SUBSCRIPTION_REQUEST:
didReceive(subscriptionRequest: content, withCompletionHandler: completionHandler);
case .MUC_ROOM_INVITATION:
didReceive(mucInvitation: content, withCompletionHandler: completionHandler);
case .MESSAGE:
didReceive(messageResponse: response, withCompletionHandler: completionHandler);
case .CALL:
didReceive(call: content, withCompletionHandler: completionHandler);
case .UNSENT_MESSAGES:
completionHandler();
case .UNKNOWN:
print("received unknown notification category:", response.notification.request.content.categoryIdentifier);
completionHandler();
}
}
func didReceive(error content: UNNotificationContent, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = content.userInfo;
if userInfo["cert-name"] != nil {
let accountJid = BareJID(userInfo["account"] as! String);
let alert = CertificateErrorAlert.create(domain: accountJid.domain, certName: userInfo["cert-name"] as! String, certHash: userInfo["cert-hash-sha1"] as! String, issuerName: userInfo["issuer-name"] as? String, issuerHash: userInfo["issuer-hash-sha1"] as? String, onAccept: {
print("accepted certificate!");
guard let account = AccountManager.getAccount(for: accountJid) else {
return;
}
var certInfo = account.serverCertificate;
certInfo?["accepted"] = true as NSObject;
account.serverCertificate = certInfo;
account.active = true;
AccountSettings.LastError(accountJid).set(string: nil);
AccountManager.save(account: account);
}, onDeny: nil);
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
}
if let authError = userInfo["auth-error-type"] {
let accountJid = BareJID(userInfo["account"] as! String);
let alert = UIAlertController(title: "Authentication issue", message: "Authentication for account \(accountJid) failed: \(authError)\nVerify provided account password.", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
} else {
let alert = UIAlertController(title: content.title, message: content.body, preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
}
completionHandler();
}
func didReceive(subscriptionRequest content: UNNotificationContent, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = content.userInfo;
let senderJid = BareJID(userInfo["sender"] as! String);
let accountJid = BareJID(userInfo["account"] as! String);
var senderName = userInfo["senderName"] as! String;
if senderName != senderJid.stringValue {
senderName = "\(senderName) (\(senderJid.stringValue))";
}
let alert = UIAlertController(title: "Subscription request", message: "Received presence subscription request from\n\(senderName)\non account \(accountJid.stringValue)", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: {(action) in
guard let client = XmppService.instance.getClient(forJid: accountJid), let presenceModule: PresenceModule = client.context.modulesManager.getModule(PresenceModule.ID) else {
return;
}
presenceModule.subscribed(by: JID(senderJid));
let subscription = RosterModule.getRosterStore(client.context.sessionObject).get(for: JID(senderJid))?.subscription ?? RosterItem.Subscription.none;
guard !subscription.isTo else {
return;
}
if (Settings.AutoSubscribeOnAcceptedSubscriptionRequest.getBool()) {
presenceModule.subscribe(to: JID(senderJid));
} else {
let alert2 = UIAlertController(title: "Subscribe to " + senderName, message: "Do you wish to subscribe to \n\(senderName)\non account \(accountJid.stringValue)", preferredStyle: .alert);
alert2.addAction(UIAlertAction(title: "Accept", style: .default, handler: {(action) in
presenceModule.subscribe(to: JID(senderJid));
}));
alert2.addAction(UIAlertAction(title: "Reject", style: .destructive, handler: nil));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert2, animated: true, completion: nil);
}
}));
alert.addAction(UIAlertAction(title: "Reject", style: .destructive, handler: {(action) in
guard let client = XmppService.instance.getClient(forJid: accountJid), let presenceModule: PresenceModule = client.context.modulesManager.getModule(PresenceModule.ID) else {
return;
}
presenceModule.unsubscribed(by: JID(senderJid));
}));
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
topController?.present(alert, animated: true, completion: nil);
completionHandler();
}
func didReceive(mucInvitation content: UNNotificationContent, withCompletionHandler completionHandler: @escaping () -> Void) {
guard let account = BareJID(content.userInfo["account"] as? String), let roomJid: BareJID = BareJID(content.userInfo["roomJid"] as? String) else {
return;
}
let password = content.userInfo["password"] as? String;
let navController = UIStoryboard(name: "Groupchat", bundle: nil).instantiateViewController(withIdentifier: "MucJoinNavigationController") as! UINavigationController;
let controller = navController.visibleViewController! as! MucJoinViewController;
_ = controller.view;
controller.accountTextField.text = account.stringValue;
controller.roomTextField.text = roomJid.localPart;
controller.serverTextField.text = roomJid.domain;
controller.passwordTextField.text = password;
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
// let navController = UINavigationController(rootViewController: controller);
navController.modalPresentationStyle = .formSheet;
topController?.present(navController, animated: true, completion: nil);
completionHandler();
}
func didReceive(messageResponse response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo;
guard let accountJid = BareJID(userInfo["account"] as? String) else {
completionHandler();
return;
}
guard let senderJid = BareJID(userInfo["sender"] as? String) else {
(UIApplication.shared.delegate as? AppDelegate)?.updateApplicationIconBadgeNumber(completionHandler: completionHandler);
return;
}
if response.actionIdentifier == UNNotificationDismissActionIdentifier {
if userInfo[AnyHashable("uid")] as? String != nil {
DBChatHistoryStore.instance.markAsRead(for: accountJid, with: senderJid, before: response.notification.date, completionHandler: {
let threadId = response.notification.request.content.threadIdentifier;
let date = response.notification.date;
UNUserNotificationCenter.current().getDeliveredNotifications { notifications in
let toRemove = notifications.filter({ (notification) -> Bool in
notification.request.content.threadIdentifier == threadId && notification.date < date;
}).map({ (notification) -> String in
return notification.request.identifier;
});
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: toRemove);
DispatchQueue.main.async {
(UIApplication.shared.delegate as? AppDelegate)?.updateApplicationIconBadgeNumber(completionHandler: completionHandler);
}
}
});
} else {
completionHandler();
}
} else {
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
if topController != nil {
guard let chat = DBChatStore.instance.getChat(for: accountJid, with: senderJid) else {
completionHandler();
return;
}
let controller = chat is DBRoom ? UIStoryboard(name: "Groupchat", bundle: nil).instantiateViewController(withIdentifier: "RoomViewNavigationController") : topController!.storyboard?.instantiateViewController(withIdentifier: "ChatViewNavigationController");
let navigationController = controller as? UINavigationController;
let destination = navigationController?.visibleViewController ?? controller;
if let baseChatViewController = destination as? BaseChatViewController {
baseChatViewController.account = accountJid;
baseChatViewController.jid = senderJid;
}
destination?.hidesBottomBarWhenPushed = true;
topController!.showDetailViewController(controller!, sender: self);
} else {
print("No top controller!");
}
completionHandler();
}
}
func didReceive(call content: UNNotificationContent, withCompletionHandler completionHandler: @escaping () -> Void) {
#if targetEnvironment(simulator)
#else
let userInfo = content.userInfo;
let senderName = userInfo["senderName"] as! String;
let senderJid = JID(userInfo["sender"] as! String);
let accountJid = BareJID(userInfo["account"] as! String);
let sdp = userInfo["sdpOffer"] as! String;
let sid = userInfo["sid"] as! String;
var topController = UIApplication.shared.keyWindow?.rootViewController;
while (topController?.presentedViewController != nil) {
topController = topController?.presentedViewController;
}
if let session = JingleManager.instance.session(for: accountJid, with: senderJid, sid: sid) {
// can still can be received!
let alert = UIAlertController(title: "Incoming call", message: "Incoming call from \(senderName)", preferredStyle: .alert);
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .denied, .restricted:
break;
default:
alert.addAction(UIAlertAction(title: "Video call", style: .default, handler: { action in
// accept video
VideoCallController.accept(session: session, sdpOffer: sdp, withAudio: true, withVideo: true, sender: topController!);
}))
}
alert.addAction(UIAlertAction(title: "Audio call", style: .default, handler: { action in
VideoCallController.accept(session: session, sdpOffer: sdp, withAudio: true, withVideo: false, sender: topController!);
}));
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: { action in
_ = session.decline();
}));
topController?.present(alert, animated: true, completion: nil);
} else {
// call missed...
let alert = UIAlertController(title: "Missed call", message: "Missed incoming call from \(senderName)", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil));
topController?.present(alert, animated: true, completion: nil);
}
#endif
completionHandler();
}
}

View file

@ -41,7 +41,6 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, UITableViewD
var chat: DBChatProtocol!;
var dbConnection:DBConnection!;
var xmppService:XmppService!;
var account:BareJID!;
@ -59,7 +58,6 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, UITableViewD
override func viewDidLoad() {
xmppService = (UIApplication.shared.delegate as! AppDelegate).xmppService;
dbConnection = (UIApplication.shared.delegate as! AppDelegate).dbConnection;
super.viewDidLoad()
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
@ -130,7 +128,7 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, UITableViewD
}
}
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: toRemove);
self.xmppService.dbChatHistoryStore.markAsRead(for: self.account, with: self.jid);
// self.xmppService.dbChatHistoryStore.markAsRead(for: self.account, with: self.jid);
}
}

View file

@ -21,6 +21,7 @@
import UIKit
import Shared
import TigaseSwift
import TigaseSwiftOMEMO

View file

@ -25,16 +25,14 @@ import UserNotifications
import TigaseSwift
class ChatsListViewController: CustomTableViewController {
var dbConnection:DBConnection!;
var xmppService:XmppService!;
@IBOutlet var addMucButton: UIBarButtonItem!
var dataSource: ChatsDataSource!;
var dataSource: ChatsDataSource?;
override func viewDidLoad() {
xmppService = (UIApplication.shared.delegate as! AppDelegate).xmppService;
dbConnection = (UIApplication.shared.delegate as! AppDelegate).dbConnection;
dataSource = ChatsDataSource(controller: self);
super.viewDidLoad();
@ -42,11 +40,15 @@ class ChatsListViewController: CustomTableViewController {
tableView.estimatedRowHeight = 66.0;
tableView.dataSource = self;
NotificationCenter.default.addObserver(self, selector: #selector(ChatsListViewController.unreadCountChanged), name: DBChatStore.UNREAD_MESSAGES_COUNT_CHANGED, object: nil);
// NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil);
self.updateBadge();
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.navigationBar.backgroundColor = Appearance.current.controlBackgroundColor;
// if dataSource == nil {
// dataSource = ChatsDataSource(controller: self);
// }
super.viewWillAppear(animated);
}
@ -58,6 +60,12 @@ class ChatsListViewController: CustomTableViewController {
NotificationCenter.default.removeObserver(self);
}
// @objc func appMovedToBackground(_ notification: Notification) {
// DispatchQueue.main.async {
// self.dataSource = nil;
// }
// }
//
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
@ -68,14 +76,14 @@ class ChatsListViewController: CustomTableViewController {
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count;
return dataSource?.count ?? 0;
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = Settings.EnableNewUI.getBool() ? "ChatsListTableViewCellNew" : "ChatsListTableViewCell";
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath as IndexPath) as! ChatsListTableViewCell;
if let item = dataSource.item(at: indexPath) {
if let item = dataSource!.item(at: indexPath) {
cell.nameLabel.textColor = Appearance.current.labelColor;
cell.nameLabel.font = item.unread > 0 ? UIFont.boldSystemFont(ofSize: cell.nameLabel.font.pointSize) : UIFont.systemFont(ofSize: cell.nameLabel.font.pointSize);
// if Settings.EnableNewUI.getBool() {
@ -139,7 +147,7 @@ class ChatsListViewController: CustomTableViewController {
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCell.EditingStyle.delete {
if indexPath.section == 0 {
guard let item = dataSource.item(at: indexPath) else {
guard let item = dataSource!.item(at: indexPath) else {
return;
}
@ -224,7 +232,7 @@ class ChatsListViewController: CustomTableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath as IndexPath, animated: true);
guard let item = dataSource.item(at: indexPath) else {
guard let item = dataSource!.item(at: indexPath) else {
return;
}
var identifier: String!;

View file

@ -68,8 +68,8 @@ class MucChatSettingsViewController: CustomTableViewController, UIImagePickerCon
dispatchGroup.enter();
discoveryModule.getInfo(for: room.jid, onInfoReceived: { (node, identities, features) in
DispatchQueue.main.async {
let pushModule: TigasePushNotificationsModule? = client.modulesManager.getModule(TigasePushNotificationsModule.ID);
self.pushNotificationsSwitch.isEnabled = (pushModule?.enabled ?? false) && features.contains("jabber:iq:register");
let pushModule: SiskinPushNotificationsModule? = client.modulesManager.getModule(SiskinPushNotificationsModule.ID);
self.pushNotificationsSwitch.isEnabled = (pushModule?.isEnabled ?? false) && features.contains("jabber:iq:register");
if self.pushNotificationsSwitch.isEnabled {
self.room.checkTigasePushNotificationRegistrationStatus(completionHandler: { (result) in
switch result {
@ -169,6 +169,17 @@ class MucChatSettingsViewController: CustomTableViewController, UIImagePickerCon
self.room.modifyOptions({ (options) in
options.notifications = (item as! NotificationItem).type;
})
let account = self.room.account;
if let pushModule: SiskinPushNotificationsModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(SiskinPushNotificationsModule.ID), let pushSettings = pushModule.pushSettings {
pushModule.reenable(pushSettings: pushSettings, completionHandler: { result in
switch result {
case .success(_):
break;
case .failure(let err):
AccountSettings.pushHash(account).set(int: 0);
}
});
}
}
self.navigationController?.pushViewController(controller, animated: true);
}

View file

@ -20,6 +20,7 @@
//
import Foundation
import Shared
import TigaseSwift
/**

View file

@ -21,6 +21,7 @@
import Foundation
import Shared
import TigaseSwift
import TigaseSwiftOMEMO
@ -79,16 +80,18 @@ open class DBChatHistoryStore: Logger {
NotificationCenter.default.addObserver(self, selector: #selector(DBChatHistoryStore.imageRemovedFromCache), name: ImageCache.DISK_CACHE_IMAGE_REMOVED, object: nil);
}
public func appendItem(for account: BareJID, with jid: BareJID, state: MessageState, authorNickname: String? = nil, authorJid: BareJID? = nil, type: ItemType = .message, timestamp: Date, stanzaId id: String?, data: String, chatState: ChatState? = nil, errorCondition: ErrorCondition? = nil, errorMessage: String? = nil, encryption: MessageEncryption, encryptionFingerprint: String?, completionHandler: ((Int)->Void)?) {
public func appendItem(for account: BareJID, with jid: BareJID, state inState: MessageState, authorNickname: String? = nil, authorJid: BareJID? = nil, type: ItemType = .message, timestamp: Date, stanzaId id: String?, data: String, chatState: ChatState? = nil, errorCondition: ErrorCondition? = nil, errorMessage: String? = nil, encryption: MessageEncryption, encryptionFingerprint: String?, completionHandler: ((Int)->Void)?) {
dispatcher.async {
guard !state.isError || id == nil || !self.processOutgoingError(for: account, with: jid, stanzaId: id!, errorCondition: errorCondition, errorMessage: errorMessage) else {
guard !inState.isError || id == nil || !self.processOutgoingError(for: account, with: jid, stanzaId: id!, errorCondition: errorCondition, errorMessage: errorMessage) else {
return;
}
guard !self.checkItemAlreadyAdded(for: account, with: jid, authorNickname: authorNickname, type: type, timestamp: timestamp, direction: state.direction, stanzaId: id, data: data) else {
guard !self.checkItemAlreadyAdded(for: account, with: jid, authorNickname: authorNickname, type: type, timestamp: timestamp, direction: inState.direction, stanzaId: id, data: data) else {
return;
}
let state = self.calculateState(for: account, with: jid, timestamp: timestamp, state: inState);
let params:[String:Any?] = ["account" : account, "jid" : jid, "timestamp": timestamp, "data": data, "item_type": type.rawValue, "state": state.rawValue, "stanza_id": id, "author_jid" : authorJid, "author_nickname": authorNickname, "encryption": encryption.rawValue, "fingerprint": encryptionFingerprint]
guard let msgId = try! self.appendMessageStmt.insert(params) else {
return;
@ -102,6 +105,17 @@ open class DBChatHistoryStore: Logger {
}
}
private func calculateState(for account: BareJID, with jid: BareJID, timestamp: Date, state: MessageState) -> MessageState {
guard state.isUnread else {
return state;
}
let readTill = DBChatStore.instance.getChat(for: account, with: jid)?.readTill ?? Date.distantPast;
guard timestamp <= readTill else {
return state;
}
return state.toRead();
}
fileprivate func processOutgoingError(for account: BareJID, with jid: BareJID, stanzaId: String, errorCondition: ErrorCondition?, errorMessage: String?) -> Bool {
guard let itemId = self.getMessageIdInt(account: account, jid: jid, stanzaId: stanzaId) else {
@ -181,21 +195,22 @@ open class DBChatHistoryStore: Logger {
}
}
open func markAsRead(for account: BareJID, with jid: BareJID, before: Date? = nil) {
open func markAsRead(for account: BareJID, with jid: BareJID, before: Date, completionHandler: (()->Void)? = nil) {
dispatcher.async {
if before == nil {
let params:[String:Any?] = ["account":account, "jid":jid];
let updatedRecords = try! self.msgsMarkAsReadStmt.update(params);
if updatedRecords > 0 {
DBChatStore.instance.markAsRead(for: account, with: jid);
}
} else {
let params:[String:Any?] = ["account":account, "jid":jid, "before": before];
let updatedRecords = try! self.msgsMarkAsReadBeforeStmt.update(params);
if updatedRecords > 0 {
DBChatStore.instance.markAsRead(for: account, with: jid, count: updatedRecords);
}
}
// if before == nil {
// let params:[String:Any?] = ["account":account, "jid":jid];
// let updatedRecords = try! self.msgsMarkAsReadStmt.update(params);
// if updatedRecords > 0 {
// DBChatStore.instance.markAsRead(for: account, with: jid, completionHandler: completionHandler);
// } else {
// completionHandler?();
// }
// } else {
let params:[String:Any?] = ["account":account, "jid":jid, "before": before];
let updatedRecords = try! self.msgsMarkAsReadBeforeStmt.update(params);
DBChatStore.instance.markAsRead(for: account, with: jid, before: before, count: updatedRecords, completionHandler: completionHandler);
}
}
@ -266,11 +281,6 @@ open class DBChatHistoryStore: Logger {
}
}
fileprivate func changeMessageState(account: BareJID, jid: BareJID, stanzaId: String, oldState: MessageState, newState: MessageState, completion: ((Int)->Void)?) {
dispatcher.async {
}
}
fileprivate func itemUpdated(withId id: Int, for account: BareJID, with jid: BareJID) {
dispatcher.async {
let params: [String: Any?] = ["id": id]
@ -361,6 +371,19 @@ public enum MessageState: Int {
return false;
}
}
func toRead() -> MessageState {
switch self {
case .incoming_unread:
return .incoming;
case .incoming_error_unread:
return .incoming_error;
case .outgoing_error_unread:
return .outgoing_error;
default:
return self;
}
}
}
public enum MessageDirection: Int {

View file

@ -20,6 +20,7 @@
//
import UIKit
import Shared
import TigaseSwift
open class DBChatStoreWrapper: ChatStore {
@ -82,7 +83,7 @@ open class DBChatStore {
public static let instance = DBChatStore.init();
fileprivate static let CHATS_GET = "SELECT id, type, thread_id, resource, nickname, password, timestamp, options FROM chats WHERE account = :account AND jid = :jid";
fileprivate static let CHATS_LIST = "SELECT c.id, c.type, c.jid, c.name, c.nickname, c.password, c.timestamp as creation_timestamp, last.timestamp as timestamp, last1.data, last1.encryption as lastEncryption, (SELECT count(id) FROM chat_history ch2 WHERE ch2.account = last.account AND ch2.jid = last.jid AND ch2.state IN (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))) as unread, c.options FROM chats c LEFT JOIN (SELECT ch.account, ch.jid, max(ch.timestamp) as timestamp FROM chat_history ch WHERE ch.account = :account GROUP BY ch.account, ch.jid) last ON c.jid = last.jid AND c.account = last.account LEFT JOIN chat_history last1 ON last1.account = c.account AND last1.jid = c.jid AND last1.timestamp = last.timestamp WHERE c.account = :account";
fileprivate static let CHATS_LIST = "SELECT c.id, c.type, c.jid, c.name, c.nickname, c.password, c.timestamp as creation_timestamp, cr.timestamp as read_till, last.timestamp as timestamp, last1.data, last1.encryption as lastEncryption, (SELECT count(id) FROM chat_history ch2 WHERE ch2.account = last.account AND ch2.jid = last.jid AND ch2.state IN (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))) as unread, c.options FROM chats c LEFT JOIN chats_read cr on c.account = cr.account AND c.jid = cr.jid LEFT JOIN (SELECT ch.account, ch.jid, max(ch.timestamp) as timestamp FROM chat_history ch WHERE ch.account = :account GROUP BY ch.account, ch.jid) last ON c.jid = last.jid AND c.account = last.account LEFT JOIN chat_history last1 ON last1.account = c.account AND last1.jid = c.jid AND last1.timestamp = last.timestamp WHERE c.account = :account";
fileprivate static let CHAT_IS = "SELECT count(id) as count FROM chats WHERE account = :account AND jid = :jid";
fileprivate static let CHAT_OPEN = "INSERT INTO chats (account, jid, timestamp, type, resource, thread_id) VALUES (:account, :jid, :timestamp, :type, :resource, :thread)";
fileprivate static let ROOM_OPEN = "INSERT INTO chats (account, jid, timestamp, type, nickname, password) VALUES (:account, :jid, :timestamp, :type, :nickname, :password)";
@ -113,6 +114,8 @@ open class DBChatStore {
fileprivate var accountChats = [BareJID: AccountChats]();
fileprivate var deleteReadTillStmt: DBStatement;
var unreadChats: Int {
if unreadMessagesCount > 0 {
return getChats().filter({ (chat) -> Bool in
@ -159,7 +162,9 @@ open class DBChatStore {
getLastMessageTimestampForAccountStmt = try! DBConnection.main.prepareStatement(DBChatStore.GET_LAST_MESSAGE_TIMESTAMP_FOR_ACCOUNT);
self.updateChatNameStmt = try! self.dbConnection.prepareStatement(DBChatStore.UPDATE_CHAT_NAME);
self.updateChatOptionsStmt = try! self.dbConnection.prepareStatement(DBChatStore.UPDATE_CHAT_OPTIONS);
self.getReadTillStmt = try! DBConnection.main.prepareStatement("SELECT timestamp FROM chats_read WHERE account = :account AND jid = :jid");
self.updateReadTillStmt = try! DBConnection.main.prepareStatement("INSERT INTO chats_read (account, jid, timestamp) VALUES (:account, :jid, :before) ON CONFLICT(account, jid) DO UPDATE SET timestamp = max(timestamp, excluded.timestamp)");
self.deleteReadTillStmt = try! DBConnection.main.prepareStatement("DELETE FROM chats_read WHERE account = :account AND jid = :jid");
NotificationCenter.default.addObserver(self, selector: #selector(DBChatStore.accountRemoved), name: NSNotification.Name(rawValue: "accountRemoved"), object: nil);
}
@ -233,7 +238,7 @@ open class DBChatStore {
destroyChat(account: account, chat: dbChat);
if dbChat.unread > 0 {
DBChatHistoryStore.instance.markAsRead(for: account, with: dbChat.jid.bareJid);
DBChatHistoryStore.instance.markAsRead(for: account, with: dbChat.jid.bareJid, before: dbChat.timestamp);
}
if Settings.DeleteChatHistoryOnChatClose.getBool() {
DBChatHistoryStore.instance.deleteMessages(for: account, with: chat.jid.bareJid);
@ -254,11 +259,11 @@ open class DBChatStore {
case let c as Chat:
let params:[String:Any?] = [ "account" : account, "jid" : c.jid.bareJid, "timestamp": NSDate(), "type" : 0, "resource" : c.jid.resource, "thread" : c.thread ];
let id = try! self.openChatStmt.insert(params);
return DBChat(id: id!, account: account, jid: c.jid.bareJid, timestamp: Date(), lastMessage: getLastMessage(for: account, jid: c.jid.bareJid), unread: 0);
return DBChat(id: id!, account: account, jid: c.jid.bareJid, timestamp: Date(), readTill: getReadTill(for: account, with: c.jid.bareJid), lastMessage: getLastMessage(for: account, jid: c.jid.bareJid), unread: 0);
case let r as Room:
let params:[String:Any?] = [ "account" : account, "jid" : r.jid.bareJid, "timestamp": NSDate(), "type" : 1, "nickname" : r.nickname, "password" : r.password ];
let id = try! self.openRoomStmt.insert(params);
return DBRoom(id: id!, context: r.context, account: account, roomJid: r.roomJid, roomName: nil, nickname: r.nickname, password: r.password, timestamp: Date(), lastMessage: getLastMessage(for: account, jid: r.jid.bareJid), unread: 0);
return DBRoom(id: id!, context: r.context, account: account, roomJid: r.roomJid, roomName: nil, nickname: r.nickname, password: r.password, timestamp: Date(), readTill: getReadTill(for: account, with: r.jid.bareJid), lastMessage: getLastMessage(for: account, jid: r.jid.bareJid), unread: 0);
default:
return nil;
}
@ -267,6 +272,18 @@ open class DBChatStore {
fileprivate func destroyChat(account: BareJID, chat: DBChatProtocol) {
let params: [String: Any?] = ["id": chat.id];
_ = try! self.closeChatStmt.update(params);
_ = try! self.deleteReadTillStmt.update(["account": account, "jid": chat.jid.bareJid] as [String: Any?]);
}
var getReadTillStmt: DBStatement;
fileprivate func getReadTill(for account: BareJID, with jid: BareJID) -> Date {
return dispatcher.sync {
let params: [String: Any?] = ["account": account, "jid": jid];
return try! self.getReadTillStmt.queryFirstMatching(params, forEachRowUntil: { (cursor) -> Date? in
return cursor["timestamp"];
}) ?? Date.distantPast;
}
}
fileprivate func getLastMessage(for account: BareJID, jid: BareJID) -> String? {
@ -312,6 +329,7 @@ open class DBChatStore {
let lastMessageTimestamp: Date = cursor["timestamp"]!;
let lastMessageEncryption = MessageEncryption(rawValue: cursor["lastEncryption"] ?? 0) ?? .none;
let lastMessage: String? = lastMessageEncryption.message() ?? cursor["data"];
let readTill: Date = cursor["read_till"] ?? Date.distantPast;
let unread: Int = cursor["unread"]!;
let timestamp = creationTimestamp.compare(lastMessageTimestamp) == .orderedAscending ? lastMessageTimestamp : creationTimestamp;
@ -322,7 +340,7 @@ open class DBChatStore {
let nickname: String = cursor["nickname"]!;
let password: String? = cursor["password"];
let name: String? = cursor["name"];
let room = DBRoom(id: id, context: context, account: account, roomJid: jid, roomName: name, nickname: nickname, password: password, timestamp: timestamp, lastMessage: lastMessage, unread: unread);
let room = DBRoom(id: id, context: context, account: account, roomJid: jid, roomName: name, nickname: nickname, password: password, timestamp: timestamp, readTill: readTill, lastMessage: lastMessage, unread: unread);
if lastMessage != nil {
room.lastMessageDate = timestamp;
}
@ -332,7 +350,7 @@ open class DBChatStore {
}
return room;
default:
let c = DBChat(id: id, account: account, jid: jid, timestamp: timestamp, lastMessage: lastMessage, unread: unread);
let c = DBChat(id: id, account: account, jid: jid, timestamp: timestamp, readTill: readTill, lastMessage: lastMessage, unread: unread);
if let dataStr: String = cursor["options"], let data = dataStr.data(using: .utf8), let options = try? JSONDecoder().decode(ChatOptions.self, from: data) {
c.options = options;
}
@ -393,16 +411,21 @@ open class DBChatStore {
}
}
func markAsRead(for account: BareJID, with jid: BareJID, count: Int? = nil) {
let updateReadTillStmt: DBStatement;
func markAsRead(for account: BareJID, with jid: BareJID, before: Date, count: Int? = nil, completionHandler: (()->Void)? = nil) {
dispatcher.async {
_ = try! self.updateReadTillStmt.insert(["account": account, "jid": jid, "before": before] as [String: Any?]);
if let chat = self.getChat(for: account, with: jid) {
let unread = chat.unread;
if chat.markAsRead(count: count ?? unread) {
if chat.markAsRead(before: before, count: count ?? unread) {
//if !self.isMuted(chat: chat) {
self.unreadMessagesCount = self.unreadMessagesCount - (count ?? unread);
//}
NotificationCenter.default.post(name: DBChatStore.CHAT_UPDATED, object: chat);
}
completionHandler?();
}
}
}
@ -527,11 +550,12 @@ public protocol DBChatProtocol: ChatProtocol {
var id: Int { get };
var account: BareJID { get }
var readTill: Date { get }
var unread: Int { get }
var timestamp: Date { get }
var lastMessage: String? { get }
func markAsRead(count: Int) -> Bool;
func markAsRead(before: Date, count: Int) -> Bool;
func updateLastMessage(_ message: String?, timestamp: Date, isUnread: Bool) -> Bool
@ -586,6 +610,7 @@ class DBChat: Chat, DBChatProtocol {
let id: Int;
let account: BareJID;
var timestamp: Date;
var readTill: Date;
var lastMessage: String? = nil;
var unread: Int;
fileprivate(set) var options: ChatOptions = ChatOptions();
@ -599,16 +624,18 @@ class DBChat: Chat, DBChatProtocol {
}
}
init(id: Int, account: BareJID, jid: BareJID, timestamp: Date, lastMessage: String?, unread: Int) {
init(id: Int, account: BareJID, jid: BareJID, timestamp: Date, readTill: Date, lastMessage: String?, unread: Int) {
self.id = id;
self.account = account;
self.timestamp = timestamp;
self.lastMessage = lastMessage;
self.unread = unread;
self.readTill = readTill;
super.init(jid: JID(jid), thread: nil);
}
func markAsRead(count: Int) -> Bool {
func markAsRead(before: Date, count: Int) -> Bool {
self.readTill = before;
guard unread > 0 else {
return false;
}
@ -647,22 +674,25 @@ class DBRoom: Room, DBChatProtocol {
var timestamp: Date;
var lastMessage: String? = nil;
var subject: String?;
var readTill: Date;
var unread: Int;
var name: String? = nil;
fileprivate(set) var options: RoomOptions = RoomOptions();
init(id: Int, context: Context, account: BareJID, roomJid: BareJID, roomName: String?, nickname: String, password: String?, timestamp: Date, lastMessage: String?, unread: Int) {
init(id: Int, context: Context, account: BareJID, roomJid: BareJID, roomName: String?, nickname: String, password: String?, timestamp: Date, readTill: Date, lastMessage: String?, unread: Int) {
self.id = id;
self.account = account;
self.timestamp = timestamp;
self.lastMessage = lastMessage;
self.name = roomName;
self.readTill = readTill;
self.unread = unread;
super.init(context: context, roomJid: roomJid, nickname: nickname);
self.password = password;
}
func markAsRead(count: Int) -> Bool {
func markAsRead(before: Date, count: Int) -> Bool {
self.readTill = before;
guard unread > 0 else {
return false;
}

View file

@ -20,6 +20,7 @@
//
import Foundation
import Shared
import TigaseSwift
import TigaseSwiftOMEMO

View file

@ -20,6 +20,7 @@
//
import Foundation
import Shared
import TigaseSwift
open class DBRosterStoreWrapper: RosterStore {

View file

@ -20,6 +20,7 @@
//
import Foundation
import Shared
import TigaseSwift
open class DBVCardsCache {

View file

@ -20,6 +20,7 @@
//
import UIKit
import Shared
import TigaseSwift
protocol RosterProvider {

View file

@ -20,6 +20,7 @@
//
import Foundation
import Shared
import TigaseSwift
public class RosterProviderFlat: RosterProviderAbstract<RosterProviderFlatItem>, RosterProvider {

View file

@ -20,6 +20,7 @@
//
import UIKit
import Shared
import TigaseSwift
public class RosterProviderGrouped: RosterProviderAbstract<RosterProviderGroupedItem>, RosterProvider {

View file

@ -210,7 +210,7 @@ Have it enabled will keep synchronized copy of your messages exchanged using \(a
return;
}
guard let pushModule: TigasePushNotificationsModule = xmppService.getClient(forJid: account)?.modulesManager.getModule(TigasePushNotificationsModule.ID), pushModule.deviceId != nil else {
guard let pushModule: SiskinPushNotificationsModule = xmppService.getClient(forJid: account)?.modulesManager.getModule(SiskinPushNotificationsModule.ID), PushEventHandler.instance.deviceId != nil else {
completionHandler([]);
return;
}
@ -246,35 +246,33 @@ With this feature enabled Tigase iOS Messenger can be automatically notified abo
}
func enablePush(xmppService: XmppService, account accountJid: BareJID, operationFinished: @escaping ()->Void, completionHandler: @escaping ()->Void) {
guard let pushModule: TigasePushNotificationsModule = xmppService.getClient(forJid: accountJid)?.modulesManager.getModule(TigasePushNotificationsModule.ID) else {
guard let pushModule: SiskinPushNotificationsModule = xmppService.getClient(forJid: accountJid)?.modulesManager.getModule(SiskinPushNotificationsModule.ID), let deviceId = PushEventHandler.instance.deviceId else {
completionHandler();
return;
}
pushModule.findPushComponent(completionHandler: {(jid) in
pushModule.pushServiceJid = jid ?? XmppService.pushServiceJid;
pushModule.pushServiceNode = nil;
pushModule.deviceId = Settings.DeviceToken.getString();
pushModule.enabled = true;
pushModule.registerDevice(onSuccess: {
pushModule.registerDeviceAndEnable(deviceId: deviceId) { (result) in
// FIXME: handle this somehow??
switch result {
case .success(_):
DispatchQueue.main.async {
operationFinished();
if let config = AccountManager.getAccount(for: accountJid) {
config.pushServiceNode = pushModule.pushServiceNode
config.pushServiceJid = jid;
config.pushNotifications = true;
AccountManager.save(account: config);
}
// if let config = AccountManager.getAccount(for: accountJid) {
// config.pushServiceNode = pushModule.pushServiceNode
// config.pushServiceJid = jid;
// config.pushNotifications = true;
// AccountManager.save(account: config);
// }
completionHandler();
}
}, onError: { (errorCondition) in
case .failure(let errorCondition):
DispatchQueue.main.async {
operationFinished();
self.showError(title: "Push Notifications Error", message: "Server \(accountJid.domain) returned an error on the request to enable push notifications. You can try to enable this feature later on from the account settings.");
}
})
});
}
}
}
}

View file

@ -0,0 +1,85 @@
//
// PushEventHandler.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
open class PushEventHandler: XmppServiceEventHandler {
public static let instance = PushEventHandler();
var deviceId: String?;
let events: [Event] = [DiscoveryModule.AccountFeaturesReceivedEvent.TYPE];
public func handle(event: Event) {
switch event {
case let e as DiscoveryModule.AccountFeaturesReceivedEvent:
updatePushRegistration(for: e.sessionObject.userBareJid!, features: e.features);
default:
break;
}
}
func updatePushRegistration(for account: BareJID, features: [String]) {
guard let client = XmppService.instance.getClient(for: account), let pushModule: SiskinPushNotificationsModule = client.modulesManager.getModule(SiskinPushNotificationsModule.ID), let deviceId = self.deviceId else {
return;
}
let hasPush = features.contains(SiskinPushNotificationsModule.PUSH_NOTIFICATIONS_XMLNS);
if hasPush && pushModule.shouldEnable {
if let pushSettings = pushModule.pushSettings {
if pushSettings.deviceId != deviceId {
pushModule.unregisterDeviceAndDisable(completionHandler: { result in
switch result {
case .success(_):
pushModule.registerDeviceAndEnable(deviceId: deviceId, completionHandler: { result2 in
print("reregistration:", result2);
});
case .failure(let err):
// we need to try again later
break;
}
});
return;
} else if AccountSettings.pushHash(account).int() == 0 {
pushModule.reenable(pushSettings: pushSettings, completionHandler: { result in
print("reenabling device:", result);
})
}
} else {
pushModule.registerDeviceAndEnable(deviceId: deviceId, completionHandler: { result in
print("automatic registration:", result);
})
}
} else {
if let pushSettings = pushModule.pushSettings, (!hasPush) || (!pushModule.shouldEnable) {
pushModule.unregisterDeviceAndDisable(completionHandler: { result in
print("automatic deregistration:", result);
})
}
}
}
}

View file

@ -21,6 +21,7 @@
import UIKit
import Shared
import TigaseSwift
import TigaseSwiftOMEMO
@ -47,9 +48,9 @@ open class XmppService: Logger, EventHandler {
fileprivate var fetchStart = NSDate();
#if targetEnvironment(simulator)
fileprivate let eventHandlers: [XmppServiceEventHandler] = [NewFeaturesDetector(), MessageEventHandler(), MucEventHandler(), PresenceRosterEventHandler(), AvatarEventHandler(), DiscoEventHandler()];
fileprivate let eventHandlers: [XmppServiceEventHandler] = [NewFeaturesDetector(), MessageEventHandler(), MucEventHandler(), PresenceRosterEventHandler(), AvatarEventHandler(), DiscoEventHandler(), PushEventHandler.instance];
#else
fileprivate let eventHandlers: [XmppServiceEventHandler] = [NewFeaturesDetector(), MessageEventHandler(), MucEventHandler(), PresenceRosterEventHandler(), AvatarEventHandler(), DiscoEventHandler(), JingleManager.instance];
fileprivate let eventHandlers: [XmppServiceEventHandler] = [NewFeaturesDetector(), MessageEventHandler(), MucEventHandler(), PresenceRosterEventHandler(), AvatarEventHandler(), DiscoEventHandler(), PushEventHandler.instance, JingleManager.instance];
#endif
public let dbCapsCache: DBCapabilitiesCache;
@ -58,7 +59,7 @@ open class XmppService: Logger, EventHandler {
fileprivate let dbRosterStore: DBRosterStore;
public let dbVCardsCache: DBVCardsCache;
fileprivate let avatarStore: AvatarStore;
open var applicationState: ApplicationState {
open var applicationState: ApplicationState = .inactive {
didSet {
if oldValue != applicationState {
applicationStateChanged();
@ -81,7 +82,9 @@ open class XmppService: Logger, EventHandler {
didSet {
if networkAvailable {
if !oldValue {
connectClients();
if applicationState == .active {
connectClients();
}
} else {
keepalive();
}
@ -287,10 +290,10 @@ open class XmppService: Logger, EventHandler {
mcModule.enable();
}
}
if client.state == .disconnected { // && client.pushNotificationsEnabled {
client.login();
//updateXmppClientInstance(forJid: client.sessionObject.userBareJid!);
}
// if client.state == .disconnected { // && client.pushNotificationsEnabled {
// client.login();
// //updateXmppClientInstance(forJid: client.sessionObject.userBareJid!);
// }
}
}
}
@ -357,11 +360,12 @@ open class XmppService: Logger, EventHandler {
sendAutoPresence();
case .DeviceToken:
let newDeviceId = notification.userInfo?["newValue"] as? String;
forEachClient { (client) in
if let pushModule: TigasePushNotificationsModule = client.modulesManager.getModule(PushNotificationsModule.ID) {
pushModule.deviceId = newDeviceId;
}
}
// FIXME: do something about it? is it needed?
// forEachClient { (client) in
// if let pushModule: TigasePushNotificationsModule = client.modulesManager.getModule(PushNotificationsModule.ID) {
// pushModule.deviceId = newDeviceId;
// }
// }
default:
break;
}
@ -446,17 +450,19 @@ open class XmppService: Logger, EventHandler {
fetchGroup?.enter();
fetchingFor.append(client.sessionObject.userBareJid!);
print("reconnecting client:", client.sessionObject.userBareJid!);
client.login();
if !self.connect(client: client) {
self.fetchEnded(for: client.sessionObject.userBareJid!);
}
}
} else {
client.keepalive();
}
}
}
fetchGroup?.notify(queue: DispatchQueue.main) {
self.isFetch = false;
self.fetchGroup = nil;
completionHandler(.newData);
fetchGroup?.notify(queue: DispatchQueue.main) {
self.isFetch = false;
self.fetchGroup = nil;
completionHandler(.newData);
}
}
}
@ -548,9 +554,10 @@ open class XmppService: Logger, EventHandler {
}
}
fileprivate func connect(client: XMPPClient) {
@discardableResult
fileprivate func connect(client: XMPPClient) -> Bool {
guard let account = AccountManager.getAccount(for: client.sessionObject.userBareJid!), account.active, self.networkAvailable, client.state == .disconnected else {
return;
return false;
}
if let seeOtherHostStr = AccountSettings.reconnectionLocation(account.name).getString(), let seeOtherHost = Data(base64Encoded: seeOtherHostStr), let val = try? JSONDecoder().decode(XMPPSrvRecord.self, from: seeOtherHost) {
@ -568,11 +575,9 @@ open class XmppService: Logger, EventHandler {
client.sessionObject.setUserProperty(SoftwareVersionModule.VERSION_KEY, value: Bundle.main.infoDictionary!["CFBundleVersion"] as! String);
client.sessionObject.setUserProperty(SoftwareVersionModule.OS_KEY, value: UIDevice.current.systemName);
if let pushModule: TigasePushNotificationsModule = client.modulesManager.getModule(TigasePushNotificationsModule.ID) {
pushModule.pushServiceJid = account.pushServiceJid ?? XmppService.pushServiceJid;
pushModule.pushServiceNode = account.pushServiceNode;
pushModule.deviceId = Settings.DeviceToken.getString();
pushModule.enabled = account.pushNotifications;
if let pushModule: SiskinPushNotificationsModule = client.modulesManager.getModule(SiskinPushNotificationsModule.ID) {
pushModule.pushSettings = account.pushSettings;
pushModule.shouldEnable = account.pushNotifications;
}
if let smModule: StreamManagementModule = client.modulesManager.getModule(StreamManagementModule.ID) {
// for push notifications this needs to be far lower value, ie. 60-90 seconds
@ -587,6 +592,7 @@ open class XmppService: Logger, EventHandler {
DispatchQueue.global(qos: .default).async {
NotificationCenter.default.post(name: XmppService.ACCOUNT_STATE_CHANGED, object: self, userInfo: ["account": account.name.stringValue]);
}
return true;
}
fileprivate func disconnect(client: XMPPClient, force: Bool = false, completionHandler: (()->Void)? = nil) {
@ -665,7 +671,7 @@ open class XmppService: Logger, EventHandler {
mucModule.roomsManager = DBRoomsManager(store: dbChatStore);
_ = client.modulesManager.register(mucModule);
_ = client.modulesManager.register(AdHocCommandsModule());
_ = client.modulesManager.register(TigasePushNotificationsModule(pushServiceJid: XmppService.pushServiceJid));
_ = client.modulesManager.register(SiskinPushNotificationsModule(defaultPushServiceJid: XmppService.pushServiceJid, provider: SiskinPushNotificationsModuleProvider()));
_ = client.modulesManager.register(HttpFileUploadModule());
_ = client.modulesManager.register(MessageDeliveryReceiptsModule());
#if targetEnvironment(simulator)
@ -708,12 +714,3 @@ open class XmppService: Logger, EventHandler {
case inactive
}
}
extension XMPPClient {
var pushNotificationsEnabled: Bool {
let pushNotificationModule: TigasePushNotificationsModule? = self.modulesManager.getModule(TigasePushNotificationsModule.ID);
return pushNotificationModule?.enabled ?? false;
}
}

View file

@ -143,9 +143,9 @@ class AccountSettingsViewController: CustomTableViewController {
func updateView() {
let client = XmppService.instance.getClient(for: account);
let pushModule: TigasePushNotificationsModule? = client?.modulesManager.getModule(TigasePushNotificationsModule.ID);
pushNotificationSwitch.isEnabled = (pushModule?.deviceId != nil) && (pushModule?.isAvailable ?? false);
pushNotificationsForAwaySwitch.isEnabled = pushNotificationSwitch.isEnabled && (pushModule?.isAvailablePushForAway ?? false);
let pushModule: SiskinPushNotificationsModule? = client?.modulesManager.getModule(SiskinPushNotificationsModule.ID);
pushNotificationSwitch.isEnabled = (PushEventHandler.instance.deviceId != nil) && (pushModule?.isAvailable ?? false);
pushNotificationsForAwaySwitch.isEnabled = pushNotificationSwitch.isEnabled && (pushModule?.isSupported(extension: TigasePushNotificationsModule.PushForAway.self) ?? false);
messageSyncAutomaticSwitch.isOn = AccountSettings.messageSyncAuto(account).getBool();
archivingEnabledSwitch.isEnabled = false;
@ -218,7 +218,7 @@ class AccountSettingsViewController: CustomTableViewController {
self.setPushNotificationsEnabled(forJid: account, value: value);
pushNotificationsForAwaySwitch.isOn = false;
} else {
let alert = UIAlertController(title: "Push Notifications", message: "Tigase iOS Messenger can be automatically notified by compatible XMPP servers about new messages when it is in background or stopped.\nIf enabled, notifications about new messages will be forwarded to our push component and delivered to the device. These notifications will contain message senders jid and part of a message.\nDo you want to enable push notifications?", preferredStyle: .alert);
let alert = UIAlertController(title: "Push Notifications", message: "Siskin IM can be automatically notified by compatible XMPP servers about new messages when it is in background or stopped.\nIf enabled, notifications about new messages will be forwarded to our push component and delivered to the device. These notifications may contain message senders jid and part of a message.\nDo you want to enable push notifications?", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: self.enablePushNotifications));
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: {(action) in
self.pushNotificationSwitch.isOn = false;
@ -239,26 +239,18 @@ class AccountSettingsViewController: CustomTableViewController {
}
}
// let's check if push notifications component is accessible
if let pushModule: TigasePushNotificationsModule = XmppService.instance.getClient(forJid: accountJid)?.modulesManager.getModule(TigasePushNotificationsModule.ID) {
pushModule.findPushComponent(completionHandler: {(jid) in
pushModule.pushServiceJid = jid ?? XmppService.pushServiceJid;
pushModule.pushServiceNode = nil;
pushModule.deviceId = Settings.DeviceToken.getString();
pushModule.enabled = true;
pushModule.registerDevice(onSuccess: {
if let config = AccountManager.getAccount(for: accountJid) {
config.pushServiceNode = pushModule.pushServiceNode
config.pushServiceJid = jid;
config.pushNotifications = true;
AccountManager.save(account: config);
}
}, onError: { (errorCondition) in
if let pushModule: SiskinPushNotificationsModule = XmppService.instance.getClient(forJid: accountJid)?.modulesManager.getModule(SiskinPushNotificationsModule.ID), let deviceId = PushEventHandler.instance.deviceId {
pushModule.registerDeviceAndEnable(deviceId: deviceId, completionHandler: { result in
switch result {
case .success(_):
break;
case .failure(let errorCondition):
DispatchQueue.main.async {
self.pushNotificationSwitch.isOn = false;
self.pushNotificationsForAwaySwitch.isOn = false;
}
onError(errorCondition);
})
}
});
} else {
pushNotificationSwitch.isOn = false;
@ -274,38 +266,41 @@ class AccountSettingsViewController: CustomTableViewController {
}
AccountSettings.PushNotificationsForAway(account).set(bool: self.pushNotificationsForAwaySwitch.isOn);
guard let pushModule: TigasePushNotificationsModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(TigasePushNotificationsModule.ID) else {
guard let pushModule: SiskinPushNotificationsModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(SiskinPushNotificationsModule.ID) else {
return;
}
guard let serviceJid = pushModule.pushServiceJid, let node = pushModule.pushServiceNode else {
guard let pushSettings = pushModule.pushSettings else {
return;
}
pushModule.enable(serviceJid: serviceJid, node: node, enableForAway: self.pushNotificationsForAwaySwitch.isOn, onSuccess: { (stanza) in
print("PUSH enabled!");
DispatchQueue.main.async {
guard self.pushNotificationsForAwaySwitch.isOn else {
return;
pushModule.reenable(pushSettings: pushSettings, completionHandler: { (result) in
switch result {
case .success(_):
print("PUSH enabled!");
DispatchQueue.main.async {
guard self.pushNotificationsForAwaySwitch.isOn else {
return;
}
let syncPeriod = AccountSettings.messageSyncPeriod(self.account).getDouble();
if !AccountSettings.messageSyncAuto(self.account).getBool() || syncPeriod < 12 {
let alert = UIAlertController(title: "Enable automatic message synchronization", message: "For best experience it is suggested to enable Message Archving with automatic message synchronization of at least last 12 hours.\nDo you wish to do this now?", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil));
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: {(action) in
AccountSettings.messageSyncAuto(self.account).set(bool: true);
if (syncPeriod < 12) {
AccountSettings.messageSyncPeriod(self.account).set(double: 12.0);
}
self.updateView();
}));
self.present(alert, animated: true, completion: nil);
}
}
let syncPeriod = AccountSettings.messageSyncPeriod(self.account).getDouble();
if !AccountSettings.messageSyncAuto(self.account).getBool() || syncPeriod < 12 {
let alert = UIAlertController(title: "Enable automatic message synchronization", message: "For best experience it is suggested to enable Message Archving with automatic message synchronization of at least last 12 hours.\nDo you wish to do this now?", preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil));
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: {(action) in
AccountSettings.messageSyncAuto(self.account).set(bool: true);
if (syncPeriod < 12) {
AccountSettings.messageSyncPeriod(self.account).set(double: 12.0);
}
self.updateView();
}));
self.present(alert, animated: true, completion: nil);
case .failure(let errorCondition):
DispatchQueue.main.async {
self.pushNotificationsForAwaySwitch.isOn = !self.pushNotificationsForAwaySwitch.isOn;
AccountSettings.PushNotificationsForAway(self.account).set(bool: self.pushNotificationsForAwaySwitch.isOn);
}
}
}, onError: {(errorCondition) in
DispatchQueue.main.async {
self.pushNotificationsForAwaySwitch.isOn = !self.pushNotificationsForAwaySwitch.isOn;
AccountSettings.PushNotificationsForAway(self.account).set(bool: self.pushNotificationsForAwaySwitch.isOn);
}
});
}

View file

@ -22,6 +22,7 @@
import Foundation
import Security
import Shared
import TigaseSwift
@ -147,6 +148,7 @@ open class AccountManager {
}
AccountSettings.removeSettings(for: account.name.stringValue);
NotificationEncryptionKeys.set(key: nil, for: account.name);
AccountManager.accountChanged(account: account);
return true;
@ -232,7 +234,24 @@ open class AccountManager {
}
}
open var pushServiceJid: JID? {
open var pushSettings: SiskinPushNotificationsModule.PushSettings? {
get {
guard let settings = SiskinPushNotificationsModule.PushSettings(dictionary: data["push"] as? [String: Any]) else {
guard let pushServiceNode = self.pushServiceNode, let deviceId = Settings.DeviceToken.string() else {
return nil;
}
return SiskinPushNotificationsModule.PushSettings(jid: self.pushServiceJid ?? XmppService.pushServiceJid, node: pushServiceNode, deviceId: deviceId, encryption: false);
}
return settings;
}
set {
data["push"] = newValue?.dictionary();
data.removeValue(forKey: "pushServiceJid");
data.removeValue(forKey: "pushServiceNode");
}
}
private var pushServiceJid: JID? {
get {
return JID(data["pushServiceJid"] as? String);
}
@ -245,7 +264,7 @@ open class AccountManager {
}
}
open var pushServiceNode: String? {
private var pushServiceNode: String? {
get {
return data["pushServiceNode"] as? String;
}

View file

@ -20,6 +20,7 @@
//
import UIKit
import Shared
import TigaseSwift
open class AvatarStore {

View file

@ -0,0 +1,92 @@
//
// MainNotificationManagerProvider.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 Shared
class MainNotificationManagerProvider: NotificationManagerProvider {
func getChatNameAndType(for account: BareJID, with jid: BareJID, completionHandler: @escaping (String?, Payload.Kind) -> Void) {
if let room = DBChatStore.instance.getChat(for: account, with: jid) as? DBRoom {
completionHandler(room.name, .groupchat);
} else {
let client = XmppService.instance.getClient(for: account);
let rosterModule: RosterModule? = client?.modulesManager.getModule(RosterModule.ID);
let item = rosterModule?.rosterStore.get(for: JID(jid))
completionHandler(item?.name, .chat);
}
}
func countBadge(withThreadId: String?, completionHandler: @escaping (Int) -> Void) {
NotificationManager.unreadChatsThreadIds() { result in
var unreadChats = result;
DBChatStore.instance.getChats().filter({ chat -> Bool in
return chat.unread > 0;
}).forEach { (chat) in
unreadChats.insert("account=" + chat.account.stringValue + "|sender=" + chat.jid.bareJid.stringValue)
}
if let threadId = withThreadId {
unreadChats.insert(threadId);
}
completionHandler(unreadChats.count);
}
}
func shouldShowNotification(account: BareJID, sender jid: BareJID?, body msg: String?, completionHandler: @escaping (Bool)->Void) {
guard let sender = jid, let body = msg else {
completionHandler(true);
return;
}
if let conv = DBChatStore.instance.getChat(for: account, with: sender) {
switch conv {
case let room as DBRoom:
switch room.options.notifications {
case .none:
completionHandler(false);
case .always:
completionHandler(true);
case .mention:
completionHandler(body.contains(room.nickname));
}
case let chat as DBChat:
if Settings.NotificationsFromUnknown.bool() {
completionHandler(true);
} else {
let rosterModule: RosterModule? = XmppService.instance.getClient(for: account)?.modulesManager.getModule(RosterModule.ID);
let known = rosterModule?.rosterStore.get(for: JID(sender)) != nil;
completionHandler(known)
}
default:
print("should not happen!");
completionHandler(true);
}
} else {
completionHandler(false);
}
}
}

View file

@ -19,87 +19,13 @@
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
import openssl
import TigaseSwiftOMEMO
import Shared
class OpenSSL_AES_GCM_Engine: AES_GCM_Engine {
func encrypt(iv: Data, key: Data, message data: Data, output: UnsafeMutablePointer<Data>?, tag: UnsafeMutablePointer<Data>?) -> Bool {
let ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nil, nil, nil);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, Int32(iv.count), nil);
iv.withUnsafeBytes({ (ivBytes: UnsafeRawBufferPointer) -> Void in
key.withUnsafeBytes({ (keyBytes: UnsafeRawBufferPointer) -> Void in
EVP_EncryptInit_ex(ctx, nil, nil, keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), ivBytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
})
});
EVP_CIPHER_CTX_set_padding(ctx, 1);
var outbuf = Array(repeating: UInt8(0), count: data.count);
var outbufLen: Int32 = 0;
let encryptedBody = data.withUnsafeBytes { ( bytes) -> Data in
EVP_EncryptUpdate(ctx, &outbuf, &outbufLen, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(data.count));
return Data(bytes: &outbuf, count: Int(outbufLen));
}
EVP_EncryptFinal_ex(ctx, &outbuf, &outbufLen);
var tagData = Data(count: 16);
tagData.withUnsafeMutableBytes({ (bytes) -> Void in
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
});
EVP_CIPHER_CTX_free(ctx);
tag?.initialize(to: tagData);
output?.initialize(to: encryptedBody);
return true;
}
func decrypt(iv: Data, key: Data, encoded payload: Data, auth tag: Data?, output: UnsafeMutablePointer<Data>?) -> Bool {
let ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nil, nil, nil);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, Int32(iv.count), nil);
key.withUnsafeBytes({ (keyBytes) -> Void in
iv.withUnsafeBytes({ (ivBytes) -> Void in
EVP_DecryptInit_ex(ctx, nil, nil, keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), ivBytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
})
})
EVP_CIPHER_CTX_set_padding(ctx, 1);
var auth = tag;
var encoded = payload;
if auth == nil {
auth = payload.subdata(in: (payload.count - 16)..<payload.count);
encoded = payload.subdata(in: 0..<(payload.count-16));
}
var outbuf = Array(repeating: UInt8(0), count: encoded.count);
var outbufLen: Int32 = 0;
let decoded = encoded.withUnsafeBytes({ (bytes) -> Data in
EVP_DecryptUpdate(ctx, &outbuf, &outbufLen, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(encoded.count));
return Data(bytes: &outbuf, count: Int(outbufLen));
});
if auth != nil {
auth!.withUnsafeMutableBytes({ [count = auth!.count] (bytes: UnsafeMutableRawBufferPointer) -> Void in
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, Int32(count), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self));
});
}
let ret = EVP_DecryptFinal_ex(ctx, &outbuf, &outbufLen);
EVP_CIPHER_CTX_free(ctx);
guard ret >= 0 else {
print("authentication of encrypted message failed:", ret);
return false;
}
output?.initialize(to: decoded);
return true;
class OpenSSL_AES_GCM_Engine: Cipher.AES_GCM, AES_GCM_Engine {
override init() {
super.init();
}
}

View file

@ -32,6 +32,7 @@ public enum Settings: String {
case RosterAvailableOnly
case RosterDisplayHiddenGroup
case AutoSubscribeOnAcceptedSubscriptionRequest
@available(swift, deprecated: 1.0)
case DeviceToken
case NotificationsFromUnknown
case RecentsMessageLinesNo
@ -160,6 +161,7 @@ public enum AccountSettings {
case KnownServerFeatures(BareJID)
case omemoRegistrationId(BareJID)
case reconnectionLocation(BareJID)
case pushHash(BareJID)
public var account: BareJID {
switch self {
@ -179,6 +181,8 @@ public enum AccountSettings {
return account;
case .reconnectionLocation(let account):
return account;
case .pushHash(let account):
return account;
}
}
@ -200,6 +204,8 @@ public enum AccountSettings {
return "omemoRegistrationId";
case .reconnectionLocation(_):
return "reconnectionLocation";
case .pushHash(_):
return "pushHash";
}
}
@ -248,6 +254,10 @@ public enum AccountSettings {
return date();
}
public func int() -> Int {
return Settings.store.integer(forKey: key);
}
func uint32() -> UInt32? {
return getUInt32();
}
@ -321,6 +331,10 @@ public enum AccountSettings {
}
}
func set(int value: Int) {
Settings.store.set(value, forKey: key);
}
public static func removeSettings(for account: String) {
let toRemove = Settings.store.dictionaryRepresentation().keys.filter { (key) -> Bool in
return key.hasPrefix("Account-" + account + "-");

View file

@ -0,0 +1,65 @@
//
// SiskinPushNotificationsModuleProvider.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
class SiskinPushNotificationsModuleProvider: SiskinPushNotificationsModuleProviderProtocol {
func mutedChats(for account: BareJID) -> [BareJID] {
return DBChatStore.instance.getChats(for: account).filter({ (chat) -> Bool in
let c = chat as? DBChat;
// TODO: add support for c.options.notifications!!
return false;
}).map({ (chat) -> BareJID in
return chat.jid.bareJid;
}).sorted { (j1, j2) -> Bool in
return j1.stringValue.compare(j2.stringValue) == .orderedAscending;
}
}
func groupchatFilterRules(for account: BareJID) -> [TigasePushNotificationsModule.GroupchatFilter.Rule] {
return DBChatStore.instance.getChats(for: account).filter({ (c) -> Bool in
if let room = c as? DBRoom {
switch room.options.notifications {
case .none:
return false;
case .always, .mention:
return true;
}
}
return false;
}).sorted(by: { (r1, r2) -> Bool in
return r1.jid.bareJid.stringValue.compare(r2.jid.bareJid.stringValue) == .orderedAscending;
}).map({ (c) -> TigasePushNotificationsModule.GroupchatFilter.Rule in
let room = c as! DBRoom;
switch room.options.notifications {
case .none:
return .never(room: room.roomJid);
case .always:
return .always(room: room.roomJid);
case .mention:
return .mentioned(room: room.roomJid, nickname: room.nickname);
}
});
}
}

View file

@ -0,0 +1,263 @@
//
// SiskinPushNotificationsModule.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 UIKit
import Foundation
import UserNotifications
import Shared
import TigaseSwift
open class SiskinPushNotificationsModule: TigasePushNotificationsModule {
public struct PushSettings {
public let jid: JID;
public let node: String;
public let deviceId: String;
public let encryption: Bool;
init?(dictionary: [String: Any]?) {
guard let dict = dictionary else {
return nil;
}
guard let jid = JID(dict["jid"] as? String), let node = dict["node"] as? String, let deviceId = dict["device"] as? String else {
return nil;
}
self.init(jid: jid, node: node, deviceId: deviceId, encryption: dict["encryption"] as? Bool ?? false);
}
init(jid: JID, node: String, deviceId: String, encryption: Bool) {
self.jid = jid;
self.node = node;
self.deviceId = deviceId;
self.encryption = encryption;
}
func dictionary() -> [String: Any] {
var dict: [String: Any] = ["jid": jid.stringValue, "node": node, "device": deviceId];
if encryption {
dict["encryption"] = true;
}
return dict;
}
}
open var pushSettings: PushSettings?;
open var shouldEnable: Bool = false;
open var isEnabled: Bool {
return pushSettings != nil && shouldEnable;
}
open func isEnabled(for deviceId: String) -> Bool {
guard let settings = self.pushSettings else {
return false;
}
return settings.deviceId == deviceId;
}
public let defaultPushServiceJid: JID;
fileprivate let providerId = "tigase:messenger:apns:1";
fileprivate let provider: SiskinPushNotificationsModuleProviderProtocol;
public init(defaultPushServiceJid: JID, provider: SiskinPushNotificationsModuleProviderProtocol) {
self.defaultPushServiceJid = defaultPushServiceJid;
self.provider = provider;
super.init();
}
open func registerDeviceAndEnable(deviceId: String, completionHandler: @escaping (Result<PushSettings,ErrorCondition>)->Void) {
self.findPushComponent { result in
switch result {
case .success(let jid):
self.registerDeviceAndEnable(deviceId: deviceId, pushServiceJid: jid, completionHandler: completionHandler);
case .failure(let error):
self.registerDeviceAndEnable(deviceId: deviceId, pushServiceJid: self.defaultPushServiceJid, completionHandler: completionHandler);
}
}
}
private func prepareExtensions(componentSupportsEncryption: Bool) -> [PushNotificationsModuleExtension] {
var extensions: [PushNotificationsModuleExtension] = [];
if !Settings.NotificationsFromUnknown.bool() {
if self.isSupported(extension: TigasePushNotificationsModule.IgnoreUnknown.self) {
extensions.append(TigasePushNotificationsModule.IgnoreUnknown());
}
}
let account = self.context.sessionObject.userBareJid!;
let groupchatFilter = self.isSupported(extension: TigasePushNotificationsModule.GroupchatFilter.self);
if groupchatFilter {
extensions.append(TigasePushNotificationsModule.GroupchatFilter(rules: provider.groupchatFilterRules(for: account)));
}
let muted = self.isSupported(extension: TigasePushNotificationsModule.Muted.self)
if muted {
extensions.append(TigasePushNotificationsModule.Muted(jids: provider.mutedChats(for: account)));
}
if muted && groupchatFilter {
let priority = self.isSupported(extension: TigasePushNotificationsModule.Priority.self);
if priority {
extensions.append(TigasePushNotificationsModule.Priority());
if componentSupportsEncryption && self.isSupported(extension: TigasePushNotificationsModule.Encryption.self) {
extensions.append(TigasePushNotificationsModule.Encryption(key: NotificationEncryptionKeys.key(for: account) ?? Cipher.AES_GCM.generateKey(ofSize: 128)!));
}
}
}
if AccountSettings.PushNotificationsForAway(self.context.sessionObject.userBareJid!).getBool() {
extensions.append(TigasePushNotificationsModule.PushForAway());
}
return extensions;
}
open func registerDeviceAndEnable(deviceId: String, pushServiceJid: JID, completionHandler: @escaping (Result<PushSettings,ErrorCondition>)->Void) {
self.registerDevice(serviceJid: pushServiceJid, provider: self.providerId, deviceId: deviceId, completionHandler: { (result) in
switch result {
case .success(let data):
self.enable(serviceJid: pushServiceJid, node: data.node, deviceId: deviceId, features: data.features ?? [], completionHandler: completionHandler);
case .failure(let err):
completionHandler(.failure(err));
}
});
}
open func reenable(pushSettings: PushSettings, completionHandler: @escaping (Result<PushSettings,ErrorCondition>)->Void) {
self.enable(serviceJid: pushSettings.jid, node: pushSettings.node, deviceId: pushSettings.deviceId, features: pushSettings.encryption ? [TigasePushNotificationsModule.Encryption.XMLNS] : [], completionHandler: completionHandler);
}
private func hash(extensions: [PushNotificationsModuleExtension]) -> Int {
var hasher = Hasher();
for ext in extensions {
ext.hash(into: &hasher);
}
let hash = hasher.finalize();
if hash == 0 {
return 1;
}
return hash;
}
private func enable(serviceJid: JID, node: String, deviceId: String, features: [String], publishOptions: JabberDataElement? = nil, completionHandler: @escaping (Result<PushSettings,ErrorCondition>)->Void) {
let extensions: [PushNotificationsModuleExtension] = self.prepareExtensions(componentSupportsEncryption: features.contains(TigasePushNotificationsModule.Encryption.XMLNS));
let newHash = hash(extensions: extensions);
if let oldSettings = self.pushSettings {
guard newHash != AccountSettings.pushHash(self.context.sessionObject.userBareJid!).int() else {
completionHandler(.success(oldSettings));
return;
}
}
let encryption = extensions.first(where: { ext in
return ext is TigasePushNotificationsModule.Encryption;
}) as? TigasePushNotificationsModule.Encryption;
let settings = PushSettings(jid: serviceJid, node: node, deviceId: deviceId, encryption: encryption != nil);
self.enable(serviceJid: serviceJid, node: node, extensions: extensions, completionHandler: { (result) in
switch result {
case .success(_):
let accountJid = self.context.sessionObject.userBareJid!;
NotificationEncryptionKeys.set(key: encryption?.key, for: accountJid);
AccountSettings.pushHash(accountJid).set(int: newHash);
self.pushSettings = settings;
if let config = AccountManager.getAccount(for: accountJid) {
config.pushSettings = settings;
config.pushNotifications = true;
AccountManager.save(account: config);
}
completionHandler(.success(settings));
case .failure(let err):
self.unregisterDevice(serviceJid: serviceJid, provider: self.providerId, deviceId: deviceId, completionHandler: { result in
print("unregistered device:", result);
completionHandler(.failure(err));
});
}
});
}
public func unregisterDeviceAndDisable(completionHandler: @escaping (Result<Void,ErrorCondition>) -> Void) {
if let settings = self.pushSettings {
var total: Result<Void, ErrorCondition> = .success(Void());
let group = DispatchGroup();
group.enter();
group.enter();
AccountSettings.pushHash(self.context.sessionObject.userBareJid!).set(int: 0);
let resultHandler: (Result<Void,ErrorCondition>)->Void = {
result in
DispatchQueue.main.async {
switch result {
case .failure(let error):
if error != .item_not_found {
total = .failure(error);
}
default:
break;
}
group.leave();
}
}
group.notify(queue: DispatchQueue.main) {
self.pushSettings = nil;
let accountJid = self.context.sessionObject.userBareJid!;
NotificationEncryptionKeys.set(key: nil, for: accountJid);
if let config = AccountManager.getAccount(for: accountJid) {
config.pushSettings = nil;
config.pushNotifications = false;
AccountManager.save(account: config);
}
completionHandler(total);
}
self.disable(serviceJid: settings.jid, node: settings.node, completionHandler: { result in
switch result {
case .success(_):
resultHandler(.success(Void()));
case .failure(let err):
resultHandler(.failure(err));
}
});
self.unregisterDevice(serviceJid: settings.jid, provider: self.providerId, deviceId: settings.deviceId, completionHandler: resultHandler);
}
}
func findPushComponent(completionHandler: @escaping (Result<JID,ErrorCondition>)->Void) {
self.findPushComponent(requiredFeatures: ["urn:xmpp:push:0", self.providerId], completionHandler: completionHandler);
}
}
public protocol SiskinPushNotificationsModuleProviderProtocol {
func mutedChats(for account: BareJID) -> [BareJID];
func groupchatFilterRules(for account: BareJID) -> [TigasePushNotificationsModule.GroupchatFilter.Rule];
}

View file

@ -1,231 +0,0 @@
//
// TigasePushNotificationsModule.swift
//
// Siskin IM
// Copyright (C) 2017 "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 UIKit
import UserNotifications
import TigaseSwift
open class TigasePushNotificationsModule: PushNotificationsModule, EventHandler {
fileprivate static let PUSH_FOR_AWAY_XMLNS = "tigase:push:away:0";
fileprivate var oldDeviceId: String? = nil;
open var deviceId: String? = "?" {
willSet {
guard deviceId != "?" && deviceId != newValue && oldDeviceId == nil else {
return;
}
oldDeviceId = deviceId;
}
didSet {
guard oldValue != deviceId else {
return;
}
updateDeviceId();
}
}
open var enabled: Bool = false;
open var pushServiceNode: String? {
didSet {
if let account = AccountManager.getAccount(for: context.sessionObject) {
if pushServiceNode != account.pushServiceNode {
account.pushServiceNode = pushServiceNode;
AccountManager.save(account: account);
}
}
}
}
open var isAvailablePushForAway: Bool {
if let features: [String] = context.sessionObject.getProperty(DiscoveryModule.SERVER_FEATURES_KEY) {
return features.contains(TigasePushNotificationsModule.PUSH_FOR_AWAY_XMLNS);
}
return false;
}
override open var context: Context! {
willSet {
if context != nil {
context.eventBus.unregister(handler: self, for: SessionEstablishmentModule.SessionEstablishmentSuccessEvent.TYPE);
}
}
didSet {
if context != nil {
context.eventBus.register(handler: self, for: SessionEstablishmentModule.SessionEstablishmentSuccessEvent.TYPE);
}
}
}
fileprivate let provider = "tigase:messenger:apns:1";
public init(pushServiceJid: JID) {
super.init();
self.pushServiceJid = pushServiceJid;
}
open func registerDevice(onSuccess: @escaping ()-> Void, onError: @escaping (ErrorCondition?)->Void) {
self.registerDevice(serviceJid: self.pushServiceJid!, provider: self.provider, deviceId: self.deviceId!, onSuccess: { (node) in
self.pushServiceNode = node;
self.enable(serviceJid: self.pushServiceJid!, node: node, enableForAway: AccountSettings.PushNotificationsForAway(self.context.sessionObject.userBareJid!).getBool(), onSuccess: { (stanza) in
onSuccess();
}, onError: onError);
}, onError: onError);
}
open func unregisterDevice(deviceId: String? = nil, onSuccess: @escaping () -> Void, onError: @escaping (ErrorCondition?) -> Void) {
if let node = self.pushServiceNode {
self.disable(serviceJid: self.pushServiceJid!, node: node, onSuccess: { (stanza) in
self.pushServiceNode = nil;
self.unregisterDevice(serviceJid: self.pushServiceJid!, provider: self.provider, deviceId: deviceId ?? self.deviceId!, onSuccess: onSuccess, onError: onError);
}, onError: onError);
} else {
self.unregisterDevice(serviceJid: self.pushServiceJid!, provider: self.provider, deviceId: deviceId ?? self.deviceId!, onSuccess: onSuccess, onError: onError);
}
}
open func handle(event: Event) {
switch event {
case _ as SessionEstablishmentModule.SessionEstablishmentSuccessEvent:
updateDeviceId();
default:
break;
}
}
func updateDeviceId() {
if (context != nil) && (ResourceBinderModule.getBindedJid(context.sessionObject) != nil) {
if (oldDeviceId != nil) {
let removed = {
self.oldDeviceId = nil;
if (self.enabled && self.deviceId != nil) {
self.registerDevice();
}
};
self.unregisterDevice(deviceId: oldDeviceId, onSuccess: removed, onError: { (error) in
if error != nil && error == ErrorCondition.item_not_found {
removed();
}
});
}
if deviceId != nil && pushServiceNode == nil && enabled {
self.registerDevice();
}
if deviceId != nil && pushServiceNode != nil && !enabled {
self.unregisterDevice(onSuccess: {
print("unregistered device", self.deviceId ?? "nil", "for push notifications");
}, onError: { (error) in
print("unregistration failed", self.deviceId ?? "nil", "with error", error ?? "nil");
})
}
}
}
func registerDevice() {
self.registerDevice(onSuccess: {
print("registered device", self.deviceId ?? "nil", "for push notifications at", self.pushServiceNode ?? "nil");
}, onError: { (error) in
let accountJid = self.context.sessionObject.userBareJid!;
if let account = AccountManager.getAccount(for: accountJid) {
account.pushNotifications = false;
self.enabled = false;
AccountManager.save(account: account);
let notification = UNMutableNotificationContent();
notification.title = "Error";
notification.userInfo = ["account": accountJid.stringValue];
notification.body = "Push Notifications for account \(accountJid.stringValue) disabled due to error during registration in Push Notification servce: \(error ?? ErrorCondition.undefined_condition)";
notification.sound = UNNotificationSound.default;
notification.categoryIdentifier = "ERROR";
DispatchQueue.main.async {
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: "push-notifications-" + accountJid.stringValue, content: notification, trigger: nil));
}
}
})
}
func findPushComponent(completionHandler: @escaping (JID?)->Void) {
guard let discoModule: DiscoveryModule = context.modulesManager.getModule(DiscoveryModule.ID) else {
completionHandler(nil);
return;
}
discoModule.getItems(for: JID(context.sessionObject.userBareJid!.domain)!, node: nil, onItemsReceived: {(node, items) in
let result = DiscoResults(items: items) { (jids) in
print("found proper push components at", jids);
completionHandler(jids.first);
};
items.forEach({ (item) in
discoModule.getInfo(for: item.jid, node: item.node, onInfoReceived: { (node, identities, features) in
if identities.filter({ (identity) -> Bool in
identity.category == "pubsub" && identity.type == "push"
}).isEmpty || features.firstIndex(of: "urn:xmpp:push:0") == nil || features.firstIndex(of: "tigase:messenger:apns:1") == nil {
result.failure();
} else {
result.found(item.jid);
}
}, onError: {(errorCondition) in
result.failure();
});
});
result.checkFinished();
}, onError: {(errorCondition) in
completionHandler(nil);
});
}
private class DiscoResults {
let items: [DiscoveryModule.Item];
let completionHandler: (([JID])->Void);
var responses = 0;
var found: [JID] = [];
init(items: [DiscoveryModule.Item], completionHandler: @escaping (([JID])->Void)) {
self.items = items;
self.completionHandler = completionHandler;
}
func found(_ jid: JID) {
DispatchQueue.main.async {
self.found.append(jid);
self.responses += 1;
self.checkFinished();
}
}
func failure() {
DispatchQueue.main.async {
self.responses += 1;
self.checkFinished();
}
}
func checkFinished() {
if (self.responses == items.count) {
self.completionHandler(found);
}
}
}
}