snikket-ios/Snikket - Share/ShareViewController.swift
2020-12-09 13:44:57 +01:00

384 lines
17 KiB
Swift

//
// ShareViewController.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 Social
import Shared
import TigaseSwift
import MobileCoreServices
class ShareViewController: SLComposeServiceViewController {
var account: String? = nil;
var recipients: [JID] = [];
weak var handler: EventHandler?;
lazy var xmppClient: XMPPClient = {
let client = XMPPClient();
let sslHandler: ((SessionObject, SecTrust)->Bool) = {(sessionObject,secTrust) -> Bool in
return true;
};
client.sessionObject.setProperty(SocketConnector.SSL_CERTIFICATE_VALIDATOR, value: sslHandler);
_ = client.modulesManager.register(AuthModule());
_ = client.modulesManager.register(StreamFeaturesModule());
_ = client.modulesManager.register(SaslModule());
_ = client.modulesManager.register(ResourceBinderModule());
_ = client.modulesManager.register(SessionEstablishmentModule());
_ = client.modulesManager.register(DiscoveryModule());
client.modulesManager.register(PresenceModule()).initialPresence = false;
let messageModule = client.modulesManager.register(MessageModule());
let rosterModule = client.modulesManager.register(RosterModule());
_ = client.modulesManager.register(HttpFileUploadModule());
let handler = ShareEventHandler();
handler.controller = self;
self.handler = handler;
client.eventBus.register(handler: handler, for: RosterModule.ItemUpdatedEvent.TYPE)
return client;
}();
lazy var accountConfigurationItem: SLComposeSheetConfigurationItem = {
let item = SLComposeSheetConfigurationItem()!;
item.title = "Account";
item.tapHandler = self.showAccountSelection;
return item;
}();
lazy var buddiesConfigurationItem: SLComposeSheetConfigurationItem = {
let item = SLComposeSheetConfigurationItem()!;
item.title = "Recipients";
item.tapHandler = self.showRecipientsSelection;
return item;
}();
weak var rosterController: RecipientsSelectionViewController?;
var webUrl: URL?;
var sharedDefaults = UserDefaults(suiteName: "group.snikket.share");
override func isContentValid() -> Bool {
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;
if let provider = (self.extensionContext!.inputItems.first as? NSExtensionItem)?.attachments?.first {
error = !provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String);
}
if error {
self.showAlert(title: "Failure", message: "Sharing feature with HTTP upload is disabled within application. To use this feature you need to enable sharing with HTTP upload in application");
}
}
}
override func didSelectPost() {
if let provider = (self.extensionContext!.inputItems.first as? NSExtensionItem)?.attachments?.first {
if provider.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) {
provider.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil, completionHandler: { (item, error) in
if let localUrl = item as? URL {
let uti = try? localUrl.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier;
let mimeType = uti != nil ? (UTTypeCopyPreferredTagWithClass(uti! as CFString, kUTTagClassMIMEType)?.takeRetainedValue() as String?) : nil;
let size = try? FileManager.default.attributesOfItem(atPath: localUrl.path)[FileAttributeKey.size] as? UInt64;
self.upload(localUrl: localUrl, type: mimeType, handler: {(remoteUrl) in
guard remoteUrl != nil else {
self.showAlert(title: "Failure", message: "Please try again later.");
return;
}
if self.sharedDefaults!.integer(forKey: "fileDownloadSizeLimit") > 0 {
let hash = Digest.sha1.digest(toHex: remoteUrl!.absoluteString.data(using: .utf8)!)!;
var params: [String: Any] = [
"jids": self.recipients.map({ $0.bareJid.stringValue }),
"name": localUrl.lastPathComponent,
"timestamp": Date()
];
if mimeType != nil {
params["mimeType"] = mimeType;
}
if size != nil {
params["size"] = Int(size!);
}
let localUploadDirUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.snikket.shared")!.appendingPathComponent("upload", isDirectory: true);
if !FileManager.default.fileExists(atPath: localUploadDirUrl.path) {
try? FileManager.default.createDirectory(at: localUploadDirUrl, withIntermediateDirectories: true, attributes: nil);
}
do {
try FileManager.default.copyItem(at: localUrl, to: localUploadDirUrl.appendingPathComponent(hash, isDirectory: false));
self.sharedDefaults!.set(params as Any?, forKey: "upload-\(hash)");
} catch {
print("could not copy a file from:", localUrl, "to:", localUploadDirUrl)
}
}
self.share(url: nil, uploadedFileURL: remoteUrl);
});
} else {
self.showAlert(title: "Failure", message: "Please try again later.");
}
})
} else if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (value, error) in
self.share(url: (value as! URL), uploadedFileURL: nil);
})
// } else if provider.hasItemConformingToTypeIdentifier(kUTTypePlainText as String) {
// provider.loadItem(forTypeIdentifier: kUTTypePlainText as String, options: nil, completionHandler: { (item, error) in
// self.share(text: item as! String);
// });
// } else {
// self.showAlert(title: "Failure", message: "Please try again later.");
}
}
}
override func didSelectCancel() {
xmppClient.disconnect(true);
super.didSelectCancel();
}
override func configurationItems() -> [Any]! {
return [accountConfigurationItem, buddiesConfigurationItem];
}
func showAlert(title: String, message: String) {
DispatchQueue.main.async {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: {(action) in
self.extensionContext?.cancelRequest(withError: ShareError.failure);
}));
self.present(alert, animated: true, completion: nil);
}
}
func showAccountSelection() {
if xmppClient.state != .disconnected {
xmppClient.disconnect(true);
}
let controller = storyboard?.instantiateViewController(withIdentifier: "accountSelectionViewController") as! AccountsTableViewController;
// let controller = AccountsTableViewController(style: .plain);
controller.selected = account;
controller.delegate = self;
pushConfigurationViewController(controller);
}
func accountSelection(account: String) {
self.account = account;
self.recipients = [];
validateContent();
self.buddiesConfigurationItem.value = "";
accountConfigurationItem.value = account;
xmppClient.connectionConfiguration.setUserJID(BareJID(account)!);
if let password = getAccountPassword() {
xmppClient.connectionConfiguration.setUserPassword(password);
if let rosterStore: RosterStore = xmppClient.sessionObject.getProperty(RosterModule.ROSTER_STORE_KEY) {
rosterStore.cleared();
}
xmppClient.login();
}
}
func showRecipientsSelection() {
guard account != nil else {
return;
}
let controller = storyboard?.instantiateViewController(withIdentifier: "recipientsSelectionViewController") as! RecipientsSelectionViewController;
controller.selected = recipients;
controller.xmppClient = xmppClient;
controller.delegate = self;
self.rosterController = controller;
pushConfigurationViewController(controller);
}
func recipientsChanged(_ recipients: [JID]) {
self.recipients = recipients;
buddiesConfigurationItem.value = String(recipients.count);
validateContent();
}
func getAccountPassword() -> String? {
guard account != nil else {
return nil;
}
let query: [String: NSObject] = [ String(kSecClass) : kSecClassGenericPassword, String(kSecMatchLimit) : kSecMatchLimitOne, String(kSecReturnData) : kCFBooleanTrue, String(kSecAttrService) : "xmpp" as NSObject, String(kSecAttrAccount) : account! as NSObject ];
var result:AnyObject?;
let lastResultCode: OSStatus = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0));
}
if lastResultCode == noErr {
if let data = result as? NSData {
return String(data: data as Data, encoding: String.Encoding.utf8);
}
}
return nil;
}
func upload(localUrl: URL, type: String?, handler: @escaping (URL?)->Void) {
let size = try! FileManager.default.attributesOfItem(atPath: localUrl.path)[FileAttributeKey.size] as! UInt64;
print("trying to upload", localUrl, "size", size, "type", type as Any);
if let httpModule: HttpFileUploadModule = self.xmppClient.modulesManager.getModule(HttpFileUploadModule.ID) {
httpModule.findHttpUploadComponent(onSuccess: { (results) in
guard !results.isEmpty else {
self.showAlert(title: "Upload failed", message: "Feature not supported by XMPP server");
return;
}
let compJid = results.filter({ (k,v) -> Bool in
return v == nil || v! >= Int(size);
}).first?.key;
guard compJid != nil else {
self.showAlert(title: "Upload failed", message: "Selected object is too big!");
return;
}
httpModule.requestUploadSlot(componentJid: compJid!, filename: localUrl.pathComponents.last!, size: Int(size), contentType: type ?? "application/octet-stream", onSuccess: {(slot) in
print("allocated slot", slot.getUri, slot.putUri);
var request = URLRequest(url: slot.putUri);
slot.putHeaders.forEach({ (k,v) in
request.addValue(v, forHTTPHeaderField: k);
});
request.httpMethod = "PUT";
// let inputStream = InputStream(url: localUrl);
// request.httpBodyStream = inputStream;
request.addValue(type ?? "application/octet-stream", forHTTPHeaderField: "Content-Type");
URLSession.shared.uploadTask(with: request, fromFile: localUrl) { (data, response, error) in
guard error == nil && ((response as? HTTPURLResponse)?.statusCode ?? 500) == 201 else {
print(data as Any, error as Any, response as Any);
self.showAlert(title: "Upload failed", message: "Upload to HTTP server failed.");
return;
}
handler(slot.getUri);
}.resume();
}, onError: {(errorCondition, message) in
self.showAlert(title: "Upload failed", message: message ?? "Please try again later.");
});
}, onError: { (error) in
if error != nil && error! == ErrorCondition.item_not_found {
self.showAlert(title: "Upload failed", message: "Feature not supported by XMPP server");
} else {
self.showAlert(title: "Upload failed", message: "Please try again later.");
}
})
} else {
showAlert(title: "Upload failure", message: "Upload module not available!");
}
}
func share(text: String? = nil, url: URL? = nil, uploadedFileURL: URL? = nil) {
recipients.forEach { (recipient) in
if !contentText.isEmpty || url != nil {
let message = Message();
message.type = StanzaType.chat;
message.to = recipient;
if let text = text {
message.body = contentText.isEmpty ? text : "\(contentText!) - \(text)";
} else if let url = url {
message.body = contentText.isEmpty ? url.description : "\(contentText!) - \(url.description)";
} else {
message.body = contentText;
}
xmppClient.context.writer?.write(message);
}
if let url = uploadedFileURL {
let message = Message();
message.type = .chat;
message.to = recipient;
message.oob = url.description;
xmppClient.context.writer?.write(message);
}
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
self.xmppClient.disconnect();
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil);
});
}
// func shareText(url: URL?) {
// print("sharing", contentText as Any, url);
//
// recipients.forEach { (recipient) in
// let message = Message();
// message.type = StanzaType.chat;
// message.to = recipient;
// if let url = url {
// message.body = contentText.isEmpty ? url.description : "\(contentText!) - \(url.description)";
// message.oob = url.description;
// }
// xmppClient.context.writer?.write(message);
// }
//
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
// self.xmppClient.disconnect();
// self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil);
// });
// }
class ShareEventHandler: EventHandler {
weak var controller: ShareViewController?;
func handle(event: Event) {
switch event {
case let e as RosterModule.ItemUpdatedEvent:
DispatchQueue.main.async {
self.controller?.rosterController?.updateItem(item: e.rosterItem!);
}
default:
break;
}
}
}
enum ShareError: Error {
case firstRun
case featureNotAvailable
case tooBig
case failure
}
}