diff --git a/Example/NBus/AppDelegate.swift b/Example/NBus/AppDelegate.swift index 6f6323c..ba9d323 100644 --- a/Example/NBus/AppDelegate.swift +++ b/Example/NBus/AppDelegate.swift @@ -27,7 +27,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { clearStorage() -// observeQQ() +// observeSDK() AppState.shared.setup() @@ -67,48 +67,66 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension AppDelegate { + // swiftlint:disable function_body_length + private func pasteboardItems() -> Observable<[[String]]> { NotificationCenter.default.rx .notification(UIPasteboard.changedNotification) .map { _ -> [[String]] in - UIPasteboard.general.items.map { item -> [String] in + let items = UIPasteboard.general.items + + return items.enumerated().map { (index, item) -> [String] in item.map { key, value -> String in + let identity: String + let index = "(\(index + 1)/\(items.count))" + let content: String + switch value { case let data as Data: if + let string = String( + data: data, + encoding: .utf8 + ) { + identity = "[Data-String]" + content = string + } else if let object = NSKeyedUnarchiver.unarchiveObject( with: data ) { - return "[Data-Keyed] \(key), \(object)" + identity = "[Data-Keyed]" + content = "\(object)" } else if let plist = try? PropertyListSerialization.propertyList( from: data, options: [], format: nil ) { - return "[Data-Plist] \(key), \(plist)" - } else if - let string = String( - data: data, - encoding: .utf8 - ) { - return "[Data-String] \(key), \(string)" + identity = "[Data-Plist]" + content = "\(plist)" } else { assertionFailure() - return "\(key), \(value)" + identity = "[Data-Unknown]" + content = "\(value)" } case let string as String: - return "[String] \(key), \(string)" + identity = "[String]" + content = "\(string)" default: assertionFailure() - return "\(key), \(value)" + identity = "[Unknown]" + content = "\(value)" } + + return "\(identity)\(index), \(key), \(content)" } } } .distinctUntilChanged() } + // swiftlint:enable function_body_length + private func canOpenURL() -> Observable { UIApplication.shared.rx .methodInvoked(#selector(UIApplication.canOpenURL(_:))) @@ -118,17 +136,25 @@ extension AppDelegate { } private func openURL() -> Observable { - UIApplication.shared.rx + let oldURL = UIApplication.shared.rx + .methodInvoked(#selector(UIApplication.openURL(_:))) + .compactMap { args in + args[0] as? URL + } + + let newURL = UIApplication.shared.rx .methodInvoked(#selector(UIApplication.open(_:options:completionHandler:))) .compactMap { args in args[0] as? URL } + + return Observable.merge([oldURL, newURL]) } } extension AppDelegate { - private func observeSDK() { + private func observeSystem() { pasteboardItems() .bind(onNext: { items in logger.debug("\(items)") @@ -195,10 +221,13 @@ extension AppDelegate { extension AppDelegate { - private func observeQQ() { + private func observeSDK() { // SwiftTrace.traceClasses(matchingPattern: "^QQ") // SwiftTrace.traceClasses(matchingPattern: "^Tencent") - observeSDK() +// SwiftTrace.traceClasses(matchingPattern: "^WB") +// SwiftTrace.traceClasses(matchingPattern: "^Weibo") + + observeSystem() } } diff --git a/Example/NBus/Config.xcconfig b/Example/NBus/Config.xcconfig index 106ce08..0b4dbfc 100644 --- a/Example/NBus/Config.xcconfig +++ b/Example/NBus/Config.xcconfig @@ -28,5 +28,5 @@ WECHAT_APPID = wx$(WECHAT_ID) WECHAT_MINIPROGRAMID = gh_$(WECHAT_MID) WECHAT_UNIVERSALLINK = https:$(SIMPLE_SLASH)/$(DOMAIN)/wechat/$(WECHAT_ID)/ WEIBO_APPID = wb$(WEIBO_ID) -WEIBO_REDIRECTLINK = https:$(SIMPLE_SLASH)/$(DOMAIN)/weibo/$(WEIBO_ID)/ +WEIBO_REDIRECTLINK = https:$(SIMPLE_SLASH)/api.weibo.com/oauth2/default.html WEIBO_UNIVERSALLINK = https:$(SIMPLE_SLASH)/$(DOMAIN)/weibo/$(WEIBO_ID)/ diff --git a/Example/NBus/Model/AppState.swift b/Example/NBus/Model/AppState.swift index a7f0390..8512c76 100644 --- a/Example/NBus/Model/AppState.swift +++ b/Example/NBus/Model/AppState.swift @@ -135,10 +135,17 @@ extension AppState { logger.debug("\(message)", file: file, function: function, line: line) } + let weiboHandler = WeiboHandler( + appID: AppState.getAppID(for: Platforms.weibo)!, + universalLink: AppState.getUniversalLink(for: Platforms.weibo)!, + redirectLink: AppState.getRedirectLink(for: Platforms.weibo)! + ) + let weiboItem = AppState.PlatformItem( platform: Platforms.weibo, category: .sdk, handlers: [ + .bus: weiboHandler, .sdk: weiboSDKHandler, ], viewController: { PlatformViewController() } diff --git a/Example/NBus/Model/MediaSource.swift b/Example/NBus/Model/MediaSource.swift index 3899120..ba47866 100644 --- a/Example/NBus/Model/MediaSource.swift +++ b/Example/NBus/Model/MediaSource.swift @@ -78,10 +78,15 @@ enum MediaSource { let title = "iPhone" let description = "Apple" + let dataAsset = NSDataAsset(name: "giphy-J1ZajKJKzD0PK")! + let data = dataAsset.data + let thumbnail = UIImage(data: data)?.jpegData(compressionQuality: 0.2) + return Messages.webPage( link: url, title: title, - description: description + description: description, + thumbnail: thumbnail ) }() diff --git a/Example/Podfile.lock b/Example/Podfile.lock index abee88a..17d60a5 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,27 +1,30 @@ PODS: - - NBus/BusHandlers (0.7.0): + - NBus/BusHandlers (0.8.1): - NBus/QQHandler - NBus/SystemHandler - - NBus/Core (0.7.0) - - NBus/QQHandler (0.7.0): + - NBus/WeiboHandler + - NBus/Core (0.8.1) + - NBus/QQHandler (0.8.1): - NBus/Core - - NBus/QQSDK (0.7.0) - - NBus/QQSDKHandler (0.7.0): + - NBus/QQSDK (0.8.1) + - NBus/QQSDKHandler (0.8.1): - NBus/Core - NBus/QQSDK - - NBus/SDKHandlers (0.7.0): + - NBus/SDKHandlers (0.8.1): - NBus/QQSDKHandler - NBus/SystemHandler - NBus/WechatSDKHandler - NBus/WeiboSDKHandler - - NBus/SystemHandler (0.7.0): + - NBus/SystemHandler (0.8.1): - NBus/Core - - NBus/WechatSDK (0.7.0) - - NBus/WechatSDKHandler (0.7.0): + - NBus/WechatSDK (0.8.1) + - NBus/WechatSDKHandler (0.8.1): - NBus/Core - NBus/WechatSDK - - NBus/WeiboSDK (0.7.0) - - NBus/WeiboSDKHandler (0.7.0): + - NBus/WeiboHandler (0.8.1): + - NBus/Core + - NBus/WeiboSDK (0.8.1) + - NBus/WeiboSDKHandler (0.8.1): - NBus/Core - NBus/WeiboSDK - PinLayout (1.9.3) @@ -56,7 +59,7 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - NBus: 909effc8a33bd2e5e3549406dc8b718f028f1776 + NBus: 97c14cb69f0ec19bd0752f5e25a3518849b81f8c PinLayout: 4d8733121f8687edcc8f00c19bf1379d12808767 RxCocoa: 3f79328fafa3645b34600f37c31e64c73ae3a80e RxRelay: 8d593be109c06ea850df027351beba614b012ffb diff --git a/NBus.podspec b/NBus.podspec index 5e4a71b..7a83cef 100644 --- a/NBus.podspec +++ b/NBus.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "NBus" - s.version = "0.8.1" + s.version = "0.9.0" s.summary = "A short description of NBus." s.homepage = "https://github.com/nuomi1/NBus" @@ -16,6 +16,7 @@ Pod::Spec.new do |s| s.subspec "BusHandlers" do |ss| ss.dependency "NBus/QQHandler" + ss.dependency "NBus/WeiboHandler" ss.dependency "NBus/SystemHandler" end @@ -57,6 +58,12 @@ Pod::Spec.new do |s| ss.source_files = ["NBus/Classes/Handler/WeiboSDKHandler.swift"] end + s.subspec "WeiboHandler" do |ss| + ss.dependency "NBus/Core" + + ss.source_files = ["NBus/Classes/Handler/WeiboHandler.swift"] + end + s.subspec "SystemHandler" do |ss| ss.dependency "NBus/Core" diff --git a/NBus/Classes/Core/Bus.swift b/NBus/Classes/Core/Bus.swift index 9f6912e..5d192f4 100644 --- a/NBus/Classes/Core/Bus.swift +++ b/NBus/Classes/Core/Bus.swift @@ -150,5 +150,10 @@ extension Bus { static let openID = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.qq.openID") } + + enum Weibo { + + public static let accessToken = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.weibo.accessToken") + } } } diff --git a/NBus/Classes/Handler/WeiboHandler.swift b/NBus/Classes/Handler/WeiboHandler.swift new file mode 100644 index 0000000..13639a6 --- /dev/null +++ b/NBus/Classes/Handler/WeiboHandler.swift @@ -0,0 +1,419 @@ +// +// WeiboHandler.swift +// NBus +// +// Created by nuomi1 on 2021/1/18. +// Copyright © 2021 nuomi1. All rights reserved. +// + +import Foundation + +// swiftlint:disable file_length + +public class WeiboHandler { + + public let endpoints: [Endpoint] = [ + Endpoints.Weibo.timeline, + ] + + public let platform: Platform = Platforms.weibo + + public var isInstalled: Bool { + guard let url = URL(string: "sinaweibo://") else { + assertionFailure() + return false + } + + return UIApplication.shared.canOpenURL(url) + } + + private var shareCompletionHandler: Bus.ShareCompletionHandler? + private var oauthCompletionHandler: Bus.OauthCompletionHandler? + + public let appID: String + public let universalLink: URL + private let redirectLink: URL + + private lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss:SSS" + return dateFormatter + }() + + public init(appID: String, universalLink: URL, redirectLink: URL) { + self.appID = appID + self.universalLink = universalLink + self.redirectLink = redirectLink + } +} + +extension WeiboHandler: ShareHandlerType { + + // swiftlint:disable function_body_length + + public func share( + message: MessageType, + to endpoint: Endpoint, + options: [Bus.ShareOptionKey: Any], + completionHandler: @escaping Bus.ShareCompletionHandler + ) { + guard isInstalled else { + completionHandler(.failure(.missingApplication)) + return + } + + guard canShare(message: message.identifier, to: endpoint) else { + completionHandler(.failure(.unsupportedMessage)) + return + } + + shareCompletionHandler = completionHandler + + let uuidString = UUID().uuidString + + var transferObjectItems: [String: Any] = [:] + var messageItems: [String: Any] = [:] + + transferObjectItems["__class"] = "WBSendMessageToWeiboRequest" + transferObjectItems["requestID"] = uuidString + + messageItems["__class"] = "WBMessageObject" + + switch message { + case let message as TextMessage: + messageItems["text"] = message.text + + case let message as ImageMessage: + var imageItems: [String: Any] = [:] + + imageItems["imageData"] = message.data + + messageItems["imageObject"] = imageItems + + case let message as AudioMessage: + messageItems["mediaObject"] = webPageItems( + link: message.link, + title: message.title, + description: message.description, + thumbnail: message.thumbnail + ) + + case let message as VideoMessage: + messageItems["mediaObject"] = webPageItems( + link: message.link, + title: message.title, + description: message.description, + thumbnail: message.thumbnail + ) + + case let message as WebPageMessage: + messageItems["mediaObject"] = webPageItems( + link: message.link, + title: message.title, + description: message.description, + thumbnail: message.thumbnail + ) + + default: + assertionFailure() + completionHandler(.failure(.unsupportedMessage)) + return + } + + transferObjectItems["message"] = messageItems + + setPasteboard( + with: transferObjectItems, + in: .general + ) + + guard let url = getRequestUniversalLink(uuidString: uuidString) else { + assertionFailure() + completionHandler(.failure(.invalidParameter)) + return + } + + UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { result in + if !result { + completionHandler(.failure(.unknown)) + } + } + } + + // swiftlint:enable function_body_length + + private func canShare(message: Message, to endpoint: Endpoint) -> Bool { + switch endpoint { + case Endpoints.Weibo.timeline: + return ![ + Messages.file, + Messages.miniProgram, + ].contains(message) + default: + assertionFailure() + return false + } + } + + private func webPageItems( + link: URL, + title: String?, + description: String?, + thumbnail: Data? + ) -> [String: Any] { + var webPageItems: [String: Any] = [:] + + webPageItems["__class"] = "WBWebpageObject" + webPageItems["description"] = description + webPageItems["objectID"] = UUID().uuidString + webPageItems["thumbnailData"] = thumbnail + webPageItems["title"] = title + webPageItems["webpageUrl"] = link.absoluteString + + return webPageItems + } +} + +extension WeiboHandler: OauthHandlerType { + + public func oauth( + options: [Bus.OauthOptionKey: Any], + completionHandler: @escaping Bus.OauthCompletionHandler + ) { + guard isInstalled else { + completionHandler(.failure(.missingApplication)) + return + } + + oauthCompletionHandler = completionHandler + + let uuidString = UUID().uuidString + + var transferObjectItems: [String: Any] = [:] + + transferObjectItems["__class"] = "WBAuthorizeRequest" + transferObjectItems["redirectURI"] = redirectLink.absoluteString + transferObjectItems["requestID"] = uuidString + + setPasteboard( + with: transferObjectItems, + in: .general + ) + + guard let url = getRequestUniversalLink(uuidString: uuidString) else { + assertionFailure() + completionHandler(.failure(.invalidParameter)) + return + } + + UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { result in + if !result { + completionHandler(.failure(.unknown)) + } + } + } +} + +extension WeiboHandler { + + private var appNumber: String { + appID.trimmingCharacters(in: .letters) + } + + private var identifier: String? { + Bundle.main.bus.identifier + } + + private var sdkShortVersion: String { + "3.3" + } + + private var sdkVersion: String { + "003233000" + } +} + +extension WeiboHandler { + + private func setPasteboard( + with transferObjectItems: [String: Any], + in pasteboard: UIPasteboard + ) { + guard + let identifier = identifier + else { + assertionFailure() + return + } + + var userInfoItems: [String: Any] = [:] + var appItems: [String: Any] = [:] + + userInfoItems["startTime"] = dateFormatter.string(from: Date()) + + appItems["appKey"] = appNumber + appItems["bundleID"] = identifier + appItems["universalLink"] = universalLink.absoluteString + + setPasteboard( + transferObjectItems: transferObjectItems, + userInfoItems: userInfoItems, + appItems: appItems, + in: pasteboard + ) + } + + private func setPasteboard( + transferObjectItems: [String: Any], + userInfoItems: [String: Any], + appItems: [String: Any], + in pasteboard: UIPasteboard + ) { + let transferObjectData = NSKeyedArchiver.archivedData(withRootObject: transferObjectItems) + let userInfoData = NSKeyedArchiver.archivedData(withRootObject: userInfoItems) + let appData = NSKeyedArchiver.archivedData(withRootObject: appItems) + + var pbItems: [[String: Any]] = [] + + pbItems.append(["transferObject": transferObjectData]) + pbItems.append(["userInfo": userInfoData]) + pbItems.append(["app": appData]) + pbItems.append(["sdkVersion": sdkVersion]) + + pasteboard.items = pbItems + } + + private func getRequestUniversalLink(uuidString: String) -> URL? { + guard + let identifier = identifier + else { + return nil + } + + var components = URLComponents() + + components.scheme = "https" + components.host = "open.weibo.com" + components.path = "/weibosdk/request" + + var urlItems: [String: String] = [:] + + urlItems["lfid"] = identifier + urlItems["luicode"] = "10000360" + urlItems["newVersion"] = sdkShortVersion + urlItems["objId"] = uuidString + urlItems["sdkversion"] = sdkVersion + urlItems["urltype"] = "link" + + components.queryItems = urlItems.map { key, value in + URLQueryItem(name: key, value: value) + } + + return components.url + } +} + +extension WeiboHandler: OpenUserActivityHandlerType { + + public func openUserActivity(_ userActivity: NSUserActivity) { + guard + let url = userActivity.webpageURL, + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + else { + assertionFailure() + return + } + + switch components.path { + case universalLink.appendingPathComponent("weibosdk/response").path: + handleGeneral() + default: + assertionFailure() + } + } +} + +extension WeiboHandler { + + private func getPlist(from pasteboard: UIPasteboard) -> [String: Any]? { + guard + let itemData = pasteboard.data(forPasteboardType: "transferObject"), + let infos = NSKeyedUnarchiver.unarchiveObject(with: itemData) as? [String: Any] + else { + return nil + } + + return infos + } + + private func handleGeneral() { + guard + let infos = getPlist(from: .general) + else { + assertionFailure() + return + } + + let response = infos["__class"] as? String + + switch response { + case "WBSendMessageToWeiboResponse": + handleShare(with: infos) + case "WBAuthorizeResponse": + handleOauth(with: infos) + default: + assertionFailure() + } + } +} + +extension WeiboHandler { + + private func handleShare(with infos: [String: Any]) { + let statusCode = infos["statusCode"] as? Int + + switch statusCode { + case 0: + shareCompletionHandler?(.success(())) + case -1: + shareCompletionHandler?(.failure(.userCancelled)) + default: + assertionFailure() + shareCompletionHandler?(.failure(.unknown)) + } + } + + private func handleOauth(with infos: [String: Any]) { + let statusCode = infos["statusCode"] as? Int + + switch statusCode { + case 0: + let accessToken = infos["accessToken"] as? String + + let parameters = [ + OauthInfoKeys.accessToken: accessToken, + ] + .bus + .compactMapContent() + + if !parameters.isEmpty { + oauthCompletionHandler?(.success(parameters)) + } else { + oauthCompletionHandler?(.failure(.unknown)) + } + case -1: + oauthCompletionHandler?(.failure(.userCancelled)) + default: + assertionFailure() + oauthCompletionHandler?(.failure(.unknown)) + } + } +} + +extension WeiboHandler { + + public enum OauthInfoKeys { + + public static let accessToken = Bus.OauthInfoKeys.Weibo.accessToken + } +} diff --git a/NBus/Classes/Handler/WeiboSDKHandler.swift b/NBus/Classes/Handler/WeiboSDKHandler.swift index b07b990..afcbb0f 100644 --- a/NBus/Classes/Handler/WeiboSDKHandler.swift +++ b/NBus/Classes/Handler/WeiboSDKHandler.swift @@ -88,16 +88,29 @@ extension WeiboSDKHandler: ShareHandlerType { request.message.imageObject = imageObject - case let message as WebPageMessage: - let webPageObject = WBWebpageObject() - webPageObject.webpageUrl = message.link.absoluteString - webPageObject.title = message.title - webPageObject.description = message.description - webPageObject.thumbnailData = message.thumbnail - - webPageObject.objectID = UUID().uuidString + case let message as AudioMessage: + request.message.mediaObject = wbWebpageObject( + link: message.link, + title: message.title, + description: message.description, + thumbnail: message.thumbnail + ) + + case let message as VideoMessage: + request.message.mediaObject = wbWebpageObject( + link: message.link, + title: message.title, + description: message.description, + thumbnail: message.thumbnail + ) - request.message.mediaObject = webPageObject + case let message as WebPageMessage: + request.message.mediaObject = wbWebpageObject( + link: message.link, + title: message.title, + description: message.description, + thumbnail: message.thumbnail + ) default: assertionFailure() @@ -116,8 +129,6 @@ extension WeiboSDKHandler: ShareHandlerType { switch endpoint { case Endpoints.Weibo.timeline: return ![ - Messages.audio, - Messages.video, Messages.file, Messages.miniProgram, ].contains(message) @@ -126,6 +137,23 @@ extension WeiboSDKHandler: ShareHandlerType { return false } } + + private func wbWebpageObject( + link: URL, + title: String?, + description: String?, + thumbnail: Data? + ) -> WBWebpageObject { + let webPageObject = WBWebpageObject() + webPageObject.webpageUrl = link.absoluteString + webPageObject.title = title + webPageObject.description = description + webPageObject.thumbnailData = thumbnail + + webPageObject.objectID = UUID().uuidString + + return webPageObject + } } extension WeiboSDKHandler: OauthHandlerType { @@ -170,7 +198,7 @@ extension WeiboSDKHandler { public enum OauthInfoKeys { - public static let accessToken = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.weiboSDKHandler.accessToken") + public static let accessToken = Bus.OauthInfoKeys.Weibo.accessToken } } diff --git a/README.md b/README.md index bba73dd..e9fcefa 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ nuomi1, nuomi1@qq.com - [NBus 之 QQSDKHandler](https://blog.nuomi1.com/archives/2020/12/nbus-qqsdkhandler.html) - [NBus 之 SystemHandler](https://blog.nuomi1.com/archives/2020/12/nbus-systemhandler.html) - [NBus 之 WechatSDKHandler](https://blog.nuomi1.com/archives/2020/12/nbus-wechatsdkhandler.html) +- [NBus 之 WeiboHandler](https://blog.nuomi1.com/archives/2021/01/nbus-weibohandler.html) - [NBus 之 WeiboSDKHandler](https://blog.nuomi1.com/archives/2020/12/nbus-weibosdkhandler.html) ## Thanks