SwiftDDP/Sources/DDPExtensions.swift

536 lines
20 KiB
Swift

// Copyright (c) 2016 Peter Siegesmund <peter.siegesmund@icloud.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
import CryptoSwift
private let DDP_ID = "DDP_ID"
private let DDP_EMAIL = "DDP_EMAIL"
private let DDP_USERNAME = "DDP_USERNAME"
private let DDP_TOKEN = "DDP_TOKEN"
private let DDP_TOKEN_EXPIRES = "DDP_TOKEN_EXPIRES"
private let DDP_LOGGED_IN = "DDP_LOGGED_IN"
public let DDP_USER_DID_LOGIN = "DDP_USER_DID_LOGIN"
public let DDP_USER_DID_LOGOUT = "DDP_USER_DID_LOGOUT"
let SWIFT_DDP_CALLBACK_DISPATCH_TIME = DispatchTime.distantFuture
private let syncWarning = {(name:String) -> Void in
if Thread.isMainThread {
print("\(name) is running synchronously on the main thread. This will block the main thread and should be run on a background thread")
}
}
extension String {
func dictionaryValue() -> NSDictionary? {
if let data = self.data(using: String.Encoding.utf8, allowLossyConversion: false) {
let dictionary = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as! NSDictionary
return dictionary
}
return nil
}
}
extension NSDictionary {
func stringValue() -> String? {
if let data = try? JSONSerialization.data(withJSONObject: self, options: JSONSerialization.WritingOptions(rawValue: 0)) {
return String(data: data, encoding: String.Encoding.utf8)
}
return nil
}
}
/**
Extensions that provide an api for interacting with basic Meteor server-side services
*/
extension DDPClient {
/**
Sends a subscription request to the server.
- parameter name: The name of the subscription
*/
public func subscribe(_ name:String) -> String { return sub(name, params:nil) }
/**
Sends a subscription request to the server.
- parameter name: The name of the subscription
- parameter params: An object containing method arguments, if any
*/
public func subscribe(_ name:String, params:[Any]) -> String { return sub(name, params:params) }
/**
Sends a subscription request to the server. If a callback is passed, the callback asynchronously
runs when the client receives a 'ready' message indicating that the initial subset of documents contained
in the subscription has been sent by the server.
- parameter name: The name of the subscription
- parameter params: An object containing method arguments, if any
- parameter callback: The closure to be executed when the server sends a 'ready' message
*/
public func subscribe(_ name:String, params:[Any]?, callback: DDPCallback?) -> String { return sub(name, params:params, callback:callback) }
/**
Sends a subscription request to the server. If a callback is passed, the callback asynchronously
runs when the client receives a 'ready' message indicating that the initial subset of documents contained
in the subscription has been sent by the server.
- parameter name: The name of the subscription
- parameter callback: The closure to be executed when the server sends a 'ready' message
*/
public func subscribe(_ name:String, callback: DDPCallback?) -> String { return sub(name, params:nil, callback:callback) }
/**
Asynchronously inserts a document into a collection on the server
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to insert
- parameter callback: A closure with result and error arguments describing the result of the operation
*/
@discardableResult public func insert(_ collection: String, document: NSArray, callback: DDPMethodCallback?) -> String {
let arg = "/\(collection)/insert"
return self.method(arg, params: document, callback: callback)
}
/**
Asynchronously inserts a document into a collection on the server
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to insert
*/
public func insert(_ collection: String, document: NSArray) -> String {
return insert(collection, document: document, callback:nil)
}
/**
Synchronously inserts a document into a collection on the server. Cannot be used on the main queue.
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to insert
*/
public func insert(sync collection: String, document: NSArray) -> Result {
syncWarning("Insert")
let semaphore = DispatchSemaphore(value: 0)
var serverResponse = Result()
insert(collection, document:document) { result, error in
serverResponse.result = result
serverResponse.error = error
semaphore.signal()
}
_ = semaphore.wait(timeout: SWIFT_DDP_CALLBACK_DISPATCH_TIME)
return serverResponse
}
/**
Asynchronously updates a document into a collection on the server
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to update
- parameter callback: A closure with result and error arguments describing the result of the operation
*/
@discardableResult public func update(_ collection: String, document: NSArray, callback: DDPMethodCallback?) -> String {
let arg = "/\(collection)/update"
return method(arg, params: document, callback: callback)
}
/**
Asynchronously updates a document on the server
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to update
*/
public func update(_ collection: String, document: NSArray) -> String {
return update(collection, document: document, callback:nil)
}
/**
Synchronously updates a document on the server. Cannot be used on the main queue
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to update
*/
public func update(sync collection: String, document: NSArray) -> Result {
syncWarning("Update")
let semaphore = DispatchSemaphore(value: 0)
var serverResponse = Result()
update(collection, document:document) { result, error in
serverResponse.result = result
serverResponse.error = error
semaphore.signal()
}
_ = semaphore.wait(timeout: SWIFT_DDP_CALLBACK_DISPATCH_TIME)
return serverResponse
}
/**
Asynchronously removes a document on the server
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to remove
- parameter callback: A closure with result and error arguments describing the result of the operation
*/
@discardableResult public func remove(_ collection: String, document: NSArray, callback: DDPMethodCallback?) -> String {
let arg = "/\(collection)/remove"
return method(arg, params: document, callback: callback)
}
/**
Asynchronously removes a document into a collection on the server
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to remove
*/
public func remove(_ collection: String, document: NSArray) -> String {
return remove(collection, document: document, callback:nil)
}
/**
Synchronously removes a document into a collection on the server. Cannot be used on the main queue.
- parameter collection: The name of the collection
- parameter document: An NSArray of documents to remove
*/
public func remove(sync collection: String, document: NSArray) -> Result {
syncWarning("Remove")
let semaphore = DispatchSemaphore(value: 0)
var serverResponse = Result()
remove(collection, document:document) { result, error in
serverResponse.result = result
serverResponse.error = error
semaphore.signal()
}
_ = semaphore.wait(timeout: SWIFT_DDP_CALLBACK_DISPATCH_TIME)
return serverResponse
}
// Callback runs on main thread
public func login(_ params: NSDictionary, callback: ((_ result: Any?, _ error: DDPError?) -> ())?) {
// method is run on the userBackground queue
method("login", params: NSArray(arrayLiteral: params)) { result, error in
guard let e = error, (e.isValid == true) else {
if let user = params["user"] as? NSDictionary {
if let email = user["email"] {
self.userData.set(email, forKey: DDP_EMAIL)
}
if let username = user["username"] {
self.userData.set(username, forKey: DDP_USERNAME)
}
}
if let data = result as? NSDictionary,
let id = data["id"] as? String,
let token = data["token"] as? String,
let tokenExpires = data["tokenExpires"] as? NSDictionary {
let expiration = dateFromTimestamp(tokenExpires)
self.userData.set(id, forKey: DDP_ID)
self.userData.set(token, forKey: DDP_TOKEN)
self.userData.set(expiration, forKey: DDP_TOKEN_EXPIRES)
}
self.userMainQueue.addOperation() {
if let c = callback { c(result, error) }
self.userData.set(true, forKey: DDP_LOGGED_IN)
NotificationCenter.default.post(name: Notification.Name(rawValue: DDP_USER_DID_LOGIN), object: nil)
if let _ = self.delegate {
self.delegate!.ddpUserDidLogin(self.user()!)
}
}
return
}
log.debug("Login error: \(e)")
if let c = callback { c(result, error) }
}
}
/**
Logs a user into the server using an email and password
- parameter email: An email string
- parameter password: A password string
- parameter callback: A closure with result and error parameters describing the outcome of the operation
*/
public func loginWithPassword(_ email: String, password: String, callback: DDPMethodCallback?) {
if !(loginWithToken(callback)) {
let params = ["user": ["email": email], "password":["digest": password.sha256(), "algorithm":"sha-256"]] as NSDictionary
login(params, callback: callback)
}
}
/**
Logs a user into the server using a username and password
- parameter username: A username string
- parameter password: A password string
- parameter callback: A closure with result and error parameters describing the outcome of the operation
*/
public func loginWithUsername(_ username: String, password: String, callback: DDPMethodCallback?) {
if !(loginWithToken(callback)) {
let params = ["user": ["username": username], "password":["digest": password.sha256(), "algorithm":"sha-256"]] as NSDictionary
login(params, callback: callback)
}
}
/**
Attempts to login a user with a token, if one exists
- parameter callback: A closure with result and error parameters describing the outcome of the operation
*/
@discardableResult public func loginWithToken(_ callback: DDPMethodCallback?) -> Bool {
if let token = userData.string(forKey: DDP_TOKEN),
let tokenDate = userData.object(forKey: DDP_TOKEN_EXPIRES) as? Date {
print("Found token & token expires \(token), \(tokenDate)")
if (tokenDate.compare(Date()) == ComparisonResult.orderedDescending) {
let params = ["resume":token] as NSDictionary
login(params, callback:callback)
return true
}
}
return false
}
public func signup(_ params:NSDictionary, callback:((_ result: Any?, _ error: DDPError?) -> ())?) {
method("createUser", params: NSArray(arrayLiteral: params)) { result, error in
guard let e = error, (e.isValid == true) else {
if let email = params["email"] {
self.userData.set(email, forKey: DDP_EMAIL)
}
if let username = params["username"] {
self.userData.set(username, forKey: DDP_USERNAME)
}
if let data = result as? NSDictionary,
let id = data["id"] as? String,
let token = data["token"] as? String,
let tokenExpires = data["tokenExpires"] as? NSDictionary {
let expiration = dateFromTimestamp(tokenExpires)
self.userData.set(id, forKey: DDP_ID)
self.userData.set(token, forKey: DDP_TOKEN)
self.userData.set(expiration, forKey: DDP_TOKEN_EXPIRES)
self.userData.synchronize()
}
if let c = callback { c(result, error) }
self.userData.set(true, forKey: DDP_LOGGED_IN)
return
}
log.debug("login error: \(e)")
if let c = callback { c(result, error) }
}
}
/**
Invokes a Meteor method to create a user account with a given email and password on the server
*/
public func signupWithEmail(_ email: String, password: String, callback: ((_ result:Any?, _ error:DDPError?) -> ())?) {
let params = ["email":email, "password":["digest":password.sha256(), "algorithm":"sha-256"]] as [String : Any]
signup(params as NSDictionary, callback: callback)
}
/**
Invokes a Meteor method to create a user account with a given email and password, and a NSDictionary containing a user profile
*/
public func signupWithEmail(_ email: String, password: String, profile: NSDictionary, callback: ((_ result:Any?, _ error:DDPError?) -> ())?) {
let params = ["email":email, "password":["digest":password.sha256(), "algorithm":"sha-256"], "profile":profile] as [String : Any]
signup(params as NSDictionary, callback: callback)
}
/**
Invokes a Meteor method to create a user account with a given username, email and password, and a NSDictionary containing a user profile
*/
public func signupWithUsername(_ username: String, password: String, email: String?, profile: NSDictionary?, callback: ((_ result:Any?, _ error:DDPError?) -> ())?) {
let params: NSMutableDictionary = ["username":username, "password":["digest":password.sha256(), "algorithm":"sha-256"]]
if let email = email {
params.setValue(email, forKey: "email")
}
if let profile = profile {
params.setValue(profile, forKey: "profile")
}
signup(params, callback: callback)
}
/**
Returns the client userId, if it exists
*/
public func userId() -> String? {
return self.userData.object(forKey: DDP_ID) as? String
}
/**
Returns the client's username or email, if it exists
*/
public func user() -> String? {
if let username = self.userData.object(forKey: DDP_USERNAME) as? String {
return username
} else if let email = self.userData.object(forKey: DDP_EMAIL) as? String {
return email
}
return nil
}
internal func resetUserData() {
self.userData.set(false, forKey: DDP_LOGGED_IN)
self.userData.removeObject(forKey: DDP_ID)
self.userData.removeObject(forKey: DDP_EMAIL)
self.userData.removeObject(forKey: DDP_USERNAME)
self.userData.removeObject(forKey: DDP_TOKEN)
self.userData.removeObject(forKey: DDP_TOKEN_EXPIRES)
self.userData.synchronize()
}
/**
Logs a user out and removes their account data from NSUserDefaults
*/
public func logout() {
logout(nil)
}
/**
Logs a user out and removes their account data from NSUserDefaults.
When it completes, it posts a notification: DDP_USER_DID_LOGOUT on the main queue
- parameter callback: A closure with result and error parameters describing the outcome of the operation
*/
public func logout(_ callback:DDPMethodCallback?) {
method("logout", params: nil) { result, error in
if let error = error {
log.error("\(error)")
} else {
self.userMainQueue.addOperation() {
if let user = self.user(),
let delegate = self.delegate {
delegate.ddpUserDidLogout(user)
}
self.resetUserData()
NotificationCenter.default.post(name: Notification.Name(rawValue: DDP_USER_DID_LOGOUT), object: nil)
}
}
callback?(result, error)
}
}
/**
Automatically attempts to resume a prior session, if one exists
- parameter url: The server url
*/
public func resume(_ url:String, callback:DDPCallback?) {
connect(url) { session in
if let _ = self.user() {
if !self.loginWithToken() { result, error in
if error == nil {
log.debug("Resumed previous session at launch")
if let completion = callback { completion() }
} else {
self.logout()
log.error("\(error)")
callback?()
}
}{
self.logout()
callback?()
}
} else {
if let completion = callback { completion() }
}
}
}
/**
Connects and logs in with an email address and password in one action
- parameter url: String url, ex. wss://todos.meteor.com/websocket
- parameter email: String email address
- parameter password: String password
- parameter callback: A closure with result and error parameters describing the outcome of the operation
*/
public convenience init(url: String, email: String, password: String, callback: DDPMethodCallback?) {
self.init()
connect(url) { session in
self.loginWithPassword(email, password: password, callback:callback)
}
}
/**
Returns true if the user is logged in, and false otherwise
*/
public func loggedIn() -> Bool {
if let userLoggedIn = self.userData.object(forKey: DDP_LOGGED_IN) as? Bool, (userLoggedIn == true) {
return true
}
return false
}
}