NBus 之 QQSDKHandler

QQ SDK 的接入问题不少,本 Handler 基于 QQ SDK 3.3.9 实现。


General

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class QQSDKHandler {

public let endpoints: [Endpoint] = [
Endpoints.QQ.friend,
Endpoints.QQ.timeline,
]

public let platform: Platform = Platforms.qq

public var isInstalled: Bool {
QQApiInterface.isQQInstalled()
}

private var shareCompletionHandler: Bus.ShareCompletionHandler?
private var oauthCompletionHandler: Bus.OauthCompletionHandler?

public let appID: String
public let universalLink: URL

public var logHandler: Bus.LogHandler = { message, _, _, _ in
#if DEBUG
print(message)
#endif
}

private var helper: Helper!
private var oauthHelper: TencentOAuth!

public init(appID: String, universalLink: URL) {
self.appID = appID
self.universalLink = universalLink

helper = Helper(master: self)

#if DEBUG
QQApiInterface.startLog { [weak self] message in
guard let message = message else { return }
self?.log("\(message)")
}
#endif

oauthHelper = TencentOAuth(
appId: appID.trimmingCharacters(in: .letters),
enableUniveralLink: true,
universalLink: universalLink.absoluteString,
delegate: helper
)
}
}

调用 TencentOAuth.init(appId:enableUniveralLink:universalLink:delegate:) 注册 QQ SDK,调用 QQApiInterface.startLog(_:) 记录日志。Helper 处理 QQ 的回调,相关内容下文会进行说明。

LogHandlerProxyType

1
extension QQSDKHandler: LogHandlerProxyType {}

声明 QQSDKHandler 遵循 LogHandlerProxyType 协议。

ShareHandlerType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
extension QQSDKHandler: ShareHandlerType {

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 request: SendMessageToQQReq

switch message {
case let message as TextMessage:
let textObject = QQApiTextObject(
text: message.text
)

request = SendMessageToQQReq(content: textObject)

case let message as ImageMessage:
let imageObject = QQApiImageObject(
data: message.data,
previewImageData: message.thumbnail,
title: message.title,
description: message.description
)

request = SendMessageToQQReq(content: imageObject)

case let message as AudioMessage:
let audioObject = QQApiAudioObject(
url: message.link,
title: message.title,
description: message.description,
previewImageData: message.thumbnail,
targetContentType: .audio
)

request = SendMessageToQQReq(content: audioObject)

case let message as VideoMessage:
let videoObject = QQApiVideoObject(
url: message.link,
title: message.title,
description: message.description,
previewImageData: message.thumbnail,
targetContentType: .video
)

request = SendMessageToQQReq(content: videoObject)

case let message as WebPageMessage:
let webPageObject = QQApiURLObject(
url: message.link,
title: message.title,
description: message.description,
previewImageData: message.thumbnail,
targetContentType: .news
)

request = SendMessageToQQReq(content: webPageObject)

case let message as FileMessage:
let fileObject = QQApiFileObject(
data: message.data,
previewImageData: message.thumbnail,
title: message.title,
description: message.description
)

request = SendMessageToQQReq(content: fileObject)

case let message as MiniProgramMessage:
let webPageObject = QQApiURLObject(
url: message.link,
title: message.link.absoluteString,
description: "",
previewImageData: message.thumbnail,
targetContentType: .news
)

let miniProgramObject = QQApiMiniProgramObject()
miniProgramObject.qqApiObject = webPageObject
miniProgramObject.miniAppID = message.miniProgramID
miniProgramObject.miniPath = message.path
miniProgramObject.webpageUrl = message.link.absoluteString
miniProgramObject.miniprogramType = miniProgramType(message.miniProgramType)

request = SendMessageToQQReq(miniContent: miniProgramObject)

default:
assertionFailure()
completionHandler(.failure(.unsupportedMessage))
return
}

let code: QQApiSendResultCode

switch endpoint {
case Endpoints.QQ.friend:
let cflag = self.cflag(endpoint, message.identifier)
.reduce(0) { result, flag in result | flag.rawValue }
request.message?.cflag |= UInt64(cflag)
code = QQApiInterface.send(request)
case Endpoints.QQ.timeline:
code = QQApiInterface.sendReq(toQZone: request)
default:
assertionFailure()
return
}

switch code {
case .EQQAPISENDSUCESS:
break
case .EQQAPIMESSAGECONTENTINVALID:
completionHandler(.failure(.invalidMessage))
default:
completionHandler(.failure(.unknown))
}
}

private func canShare(message: Message, to endpoint: Endpoint) -> Bool {
switch endpoint {
case Endpoints.QQ.friend:
return true
case Endpoints.QQ.timeline:
return ![Messages.file, Messages.miniProgram].contains(message)
default:
assertionFailure()
return false
}
}

private func cflag(_ endpoint: Endpoint, _ message: Message) -> [kQQAPICtrlFlag] {
var result: [kQQAPICtrlFlag] = []

switch endpoint {
case Endpoints.QQ.friend:
result.append(.qqapiCtrlFlagQZoneShareForbid)

if message == Messages.file {
result.append(.qqapiCtrlFlagQQShareDataline)
}
default:
assertionFailure()
}

return result
}

private func miniProgramType(_ miniProgramType: MiniProgramMessage.MiniProgramType) -> MiniProgramType {
switch miniProgramType {
case .release:
return .online
case .test:
return .test
case .preview:
return .preview
}
}
}

在分享流程中:

  1. 调用 isInstalled 判断是否安装 QQ,没有则提前退出。
  2. 调用 canShare(message:to:) 判断 Endpoint 是否支持此类型 Message。例如 Endpoints.QQ.timeline 空间不支持 Messages.file 文件和 Messages.miniProgram 小程序。
  3. 判断 message 的具体类型,创建 SendMessageToQQReq 请求。除了 QQApiMiniProgramObject 小程序类型,其他消息类型和 SendMessageToQQReq 都需要使用带参数标签的指定构造器,否则无效。
  4. EndpointEndpoints.QQ.friend 好友时,调用 cflag(_:_:) 设置 kQQAPICtrlFlag 参数,调用 QQApiInterface.send(_:) 拉起 QQ 好友分享。
  5. EndpointEndpoints.QQ.timeline 空间时,调用 QQApiInterface.sendReq(toQZone:) 拉起 QQ 空间分享。
  6. 调用 code 判断是否为 .EQQAPISENDSUCESS,不是则调用 completionHandler(.failure(Error)) 完成分享流程。

OauthHandlerType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension QQSDKHandler: OauthHandlerType {

public func oauth(
options: [Bus.OauthOptionKey: Any] = [:],
completionHandler: @escaping Bus.OauthCompletionHandler
) {
guard isInstalled else {
completionHandler(.failure(.missingApplication))
return
}

oauthCompletionHandler = completionHandler

let result = oauthHelper.authorize([kOPEN_PERMISSION_GET_USER_INFO])

if !result {
completionHandler(.failure(.unknown))
}
}
}

在登录流程中:

  1. 调用 isInstalled 判断是否安装 QQ,没有则提前退出。
  2. 调用 oauthHelper.authorize(_:) 拉起 QQ 登录。

OpenURLHandlerType

1
2
3
4
5
6
7
extension QQSDKHandler: OpenURLHandlerType {

public func openURL(_ url: URL) {
QQApiInterface.handleOpen(url, delegate: helper)
TencentOAuth.handleOpen(url)
}
}

调用 QQApiInterface.handleOpen(_:delegate:)TencentOAuth.handleOpen(:_) 处理 URL Scheme 回调。

OpenUserActivityHandlerType

1
2
3
4
5
6
7
extension QQSDKHandler: OpenUserActivityHandlerType {

public func openUserActivity(_ userActivity: NSUserActivity) {
QQApiInterface.handleOpenUniversallink(userActivity.webpageURL, delegate: helper)
TencentOAuth.handleUniversalLink(userActivity.webpageURL)
}
}

调用 QQApiInterface.handleOpenUniversallink(_:delegate:)TencentOAuth.handleUniversalLink(_:) 处理 NSUserActivity 回调。

Helper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
extension QQSDKHandler {

fileprivate class Helper: NSObject, QQApiInterfaceDelegate, TencentSessionDelegate {

weak var master: QQSDKHandler?

required init(master: QQSDKHandler) {
self.master = master
}

func onReq(_ req: QQBaseReq!) {
assertionFailure("\(String(describing: req))")
}

func onResp(_ resp: QQBaseResp!) {
switch resp {
case let response as SendMessageToQQResp:
switch response.result {
case "0":
master?.shareCompletionHandler?(.success(()))
case "-4":
master?.shareCompletionHandler?(.failure(.userCancelled))
default:
master?.shareCompletionHandler?(.failure(.unknown))
}
default:
assertionFailure("\(String(describing: resp))")
}
}

func isOnlineResponse(_ response: [AnyHashable: Any]!) {
assertionFailure("\(String(describing: response))")
}

func tencentDidLogin() {
let parameters = [
OauthInfoKeys.accessToken: master?.oauthHelper.accessToken,
OauthInfoKeys.openId: master?.oauthHelper.openId,
]
.compactMapContent()

if !parameters.isEmpty {
master?.oauthCompletionHandler?(.success(parameters))
} else {
master?.oauthCompletionHandler?(.failure(.unknown))
}
}

func tencentDidNotLogin(_ cancelled: Bool) {
if cancelled {
master?.oauthCompletionHandler?(.failure(.userCancelled))
} else {
master?.oauthCompletionHandler?(.failure(.unknown))
}
}

func tencentDidNotNetWork() {
assertionFailure()
}
}
}

extension QQSDKHandler {

public enum OauthInfoKeys {

public static let accessToken = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.qqSDKHandler.accessToken")

public static let openId = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.qqSDKHandler.openId")
}
}

创建 Helper 处理 QQ 的回调:

  1. QQSDKHandler 强持有 Helper,所以 Helpermaster 声明为 weak 避免循环引用。
  2. 处理分享回调时,onResp(_:) 返回 SendMessageToQQResp,调用 response.result 判断是否为 0,调用 master?.shareCompletionHandler?(.success(())) 完成分享回调。
  3. 处理登录回调时,成功则 tencentDidLogin() 被调用,从 oauthHelper 获取 accessTokenopenId,调用 master?.oauthCompletionHandler?(.success(parameters)) 完成登录回调。失败则 tencentDidNotLogin(_:) 被调用,调用 master?.oauthCompletionHandler?(.failure(Error)) 完成登录回调。

总结

本文通过封装 QQ SDK 做出 QQSDKHandler,实现 QQ 的登录和分享功能。相较于微信 SDK 的统一,QQ SDK 的分享和登录分散在两个地方,处理 URL SchemeNSUserActivity 时需要同时调用,否则无法完成登录流程。这对使用者来说可能造成一定的困扰。

参考