diff --git a/.github/workflows/status-check.yml b/.github/workflows/status-check.yml index 96bd8b3e4..482b147f2 100644 --- a/.github/workflows/status-check.yml +++ b/.github/workflows/status-check.yml @@ -10,7 +10,25 @@ jobs: build: name: Check Build runs-on: macos-13 + permissions: + checks: write + statuses: write + env: + FIN_STATUS: "error" + GH_TOKEN: ${{ github.token }} steps: + - name: Update Check Status + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/statuses/$GITHUB_SHA \ + -f state='pending' \ + -f target_url='https://github.com/Darock-Studio/Darock-Bili/actions/runs/${{ github.run_id }}' \ + -f description='Building...' \ + -f context='API Status Checker' + - name: Checkout uses: actions/checkout@v3 with: @@ -19,14 +37,36 @@ jobs: - name: Set Xcode Version run: sudo xcode-select -s /Applications/Xcode_15.0.app + - name: Get Current Time + id: current-time + run: echo "time=$(date +"%Y%m%d%H%M%S")" >> $GITHUB_OUTPUT + - name: Cache Build Caches uses: actions/cache@v3 with: - key: check-build-cache + key: ${{ runner.os }}-check-build-cache-${{ steps.current-time.outputs.time }} path: | ~/Library/Developer/Xcode/DerivedData + restore-keys: | + ${{ runner.os }}-check-build-cache- + + - name: Update Status Env + run: echo "FIN_STATUS=failure" >> $GITHUB_ENV - name: Build DarockBili App run: | xcodebuild -scheme 'DarockBili Watch App' -configuration Release build CODE_SIGN_IDENTITY="" - + echo "FIN_STATUS=success" >> $GITHUB_ENV + + - name: Update Check Status + if: always() + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/statuses/$GITHUB_SHA \ + -f state='${{ env.FIN_STATUS }}' \ + -f target_url='https://github.com/Darock-Studio/Darock-Bili/actions/runs/${{ github.run_id }}' \ + -f context='API Status Checker' + diff --git a/.github/workflows/statuscheck-runner.yml b/.github/workflows/statuscheck-runner.yml index f94dbea2c..9a06b1fbe 100644 --- a/.github/workflows/statuscheck-runner.yml +++ b/.github/workflows/statuscheck-runner.yml @@ -51,4 +51,3 @@ jobs: workflow: status-check.yml ref: ${{ needs.get-head-sha.outputs.psha }} inputs: '{ "psha": "${{ needs.get-head-sha.outputs.psha }}"}' - diff --git a/DarockBili Watch App/Extension/CodeExt.swift b/DarockBili Watch App/Extension/CodeExt.swift index c15207233..6a2e8ad3a 100644 --- a/DarockBili Watch App/Extension/CodeExt.swift +++ b/DarockBili Watch App/Extension/CodeExt.swift @@ -19,7 +19,9 @@ import OSLog import SwiftUI import Dynamic import CryptoKit +import DarockKit import Alamofire +import SwiftyJSON import Foundation import AVFoundation import CommonCrypto @@ -136,84 +138,75 @@ public func hideDigitalTime(_ b: Bool) { app._setStatusBarTimeHidden(b, animated: true, completion: nil) } -public class WbiSign: ObservableObject { - @State var img_key = "" - @State var sub_key = "" - func MD5Hash(_ string: String) -> String { - let data = Data(string.utf8) - var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) - data.withUnsafeBytes { bytes in - _ = CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash) - } - return hash.map { String(format: "%02x", $0) }.joined() - } - static let mixinKeyEncTab: [Int] = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52] - - func getMixinKey(orig: String) -> String { - let result = WbiSign.mixinKeyEncTab.reduce("") { result, index in - if index < orig.count { - let start = orig.index(orig.startIndex, offsetBy: index) - return result + String(orig[start]) - } else { - return result - } - } - print("Mixin Key: \(result)") - return result - } - - func filterInvalidCharacters(from value: Any) -> String { - let invalidCharacters = CharacterSet(charactersIn: "!'()*") - if let strValue = value as? String { - return strValue.components(separatedBy: invalidCharacters).joined() - } else { - return String(describing: value) - } - } - - func md5(data: String) -> String { - let digest = Insecure.MD5.hash(data: data.data(using: .utf8) ?? Data()) - return digest.map { String(format: "%02hhx", $0) }.joined() +func biliWbiSign(paramEncoded: String, completion: @escaping (String?) -> Void) { + func getMixinKey(orig: String) -> String { + return String(mixinKeyEncTab.map { orig[orig.index(orig.startIndex, offsetBy: $0)] }.prefix(32)) } - func encWbi(params: [String: Any]) -> String { - var newParams = params - let mixinKey = getMixinKey(orig: img_key + sub_key) - let currTime = Int(Date().timeIntervalSince1970) - newParams["wts"] = currTime - let sortedParams = newParams.sorted { $0.key < $1.key } - let query = sortedParams.map { "\($0.key)=\(filterInvalidCharacters(from: $0.value))" }.joined(separator: "&") - print("Query: \(query + mixinKey)") - let dataToHash = (query.urlEncoded() + mixinKey.prefix(32)) - let wbiSign = (dataToHash).DDMD5Encrypt() - print("WBI Sign: \(wbiSign)") - newParams["w_rid"] = wbiSign - var paramStr = "" - for param in newParams { - paramStr += "\(param.key)=\(param.value)&" - } - paramStr.removeLast() - return paramStr + func encWbi(params: [String: Any], imgKey: String, subKey: String) -> [String: Any] { + var params = params + let mixinKey = getMixinKey(orig: imgKey + subKey) + let currTime = round(Date().timeIntervalSince1970) + params["wts"] = currTime + params = params.sorted { $0.key < $1.key }.reduce(into: [:]) { $0[$1.key] = $1.value } + params = params.mapValues { String(describing: $0).filter { !"!'()*".contains($0) } } + let query = params.map { "\($0.key)=\($0.value)" }.joined(separator: "&") + let wbiSign = calculateMD5(string: query + mixinKey) + params["w_rid"] = wbiSign + return params } - - func getWbiKeys(completionHandler: @escaping (String, String) -> Void) { + + func getWbiKeys(completion: @escaping (Result<(imgKey: String, subKey: String), Error>) -> Void) { AF.request("https://api.bilibili.com/x/web-interface/nav").responseJSON { response in switch response.result { case .success(let value): - if let dict = value as? [String: Any], - let data = dict["data"] as? [String: Any], - let wbiImg = data["wbi_img"] as? [String: Any], - let imgUrl = wbiImg["img_url"] as? String, - let subUrl = wbiImg["sub_url"] as? String { - let imgKey = String(imgUrl.split(separator: "/").last!.split(separator: ".").first!) - let subKey = String(subUrl.split(separator: "/").last!.split(separator: ".").first!) - print("Image Key: \(imgKey)") - print("Sub Key: \(subKey)") - completionHandler(imgKey, subKey) - } + let json = JSON(value) + let imgURL = json["data"]["wbi_img"]["img_url"].string ?? "" + let subURL = json["data"]["wbi_img"]["sub_url"].string ?? "" + let imgKey = imgURL.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? "" + let subKey = subURL.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? "" + completion(.success((imgKey, subKey))) case .failure(let error): - print(error) + completion(.failure(error)) + } + } + } + + func calculateMD5(string: String) -> String { + let data = Data(string.utf8) + var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) + _ = data.withUnsafeBytes { + CC_MD5($0.baseAddress, CC_LONG(data.count), &digest) + } + return digest.map { String(format: "%02hhx", $0) }.joined() + } + + let mixinKeyEncTab = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52 + ] + + getWbiKeys { result in + switch result { + case .success(let keys): + let decParam = paramEncoded.base64Decoded() ?? "" + let spdParam = decParam.components(separatedBy: "&") + var spdDicParam = [String: String]() + spdParam.forEach { pair in + let components = pair.components(separatedBy: "=") + if components.count == 2 { + spdDicParam[components[0]] = components[1] + } } + + let signedParams = encWbi(params: spdDicParam, imgKey: keys.imgKey, subKey: keys.subKey) + let query = signedParams.map { "\($0.key)=\($0.value)" }.joined(separator: "&") + completion(query) + case .failure(let error): + print("Error getting keys: \(error)") + completion(nil) } } } @@ -248,24 +241,4 @@ extension Int { } } -postfix operator / -extension Optional { - static postfix func / (opt: Int?) -> Int { - return opt ?? 0 - } - static postfix func / (opt: String?) -> String { - return opt ?? "" - } - static postfix func / (opt: Double?) -> Double { - return opt ?? 0.0 - } - static postfix func / (opt: Float?) -> Float { - return opt ?? 0.0 - } - static postfix func / (opt: Bool?) -> Bool { - return opt ?? false - } -} - - diff --git a/DarockBili Watch App/InMain/MainView.swift b/DarockBili Watch App/InMain/MainView.swift index 0d40949e0..1c1884507 100644 --- a/DarockBili Watch App/InMain/MainView.swift +++ b/DarockBili Watch App/InMain/MainView.swift @@ -160,10 +160,10 @@ struct MainView: View { let headers: HTTPHeaders = [ "cookie": "SESSDATA=\(sessdata)" ] - DarockKit.Network.shared.requestString("https://api.darock.top/bili/wbi/sign/\("ps=\(isInLowBatteryMode ? 10 : 30)".base64Encoded())") { respStr, isSuccess in - if isSuccess { - debugPrint(respStr.apiFixed()) - DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd?\(respStr.apiFixed())", headers: headers) { respJson, isSuccess in + biliWbiSign(paramEncoded: "ps=\(isInLowBatteryMode ? 10 : 30)".base64Encoded()) { signed in + if let signed { + debugPrint(signed) + DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd?\(signed)", headers: headers) { respJson, isSuccess in if isSuccess { debugPrint(respJson) let datas = respJson["data"]["item"] diff --git a/DarockBili Watch App/PersonalCenter/PersonAccountView.swift b/DarockBili Watch App/PersonalCenter/PersonAccountView.swift index f8d773157..e0bb4517a 100644 --- a/DarockBili Watch App/PersonalCenter/PersonAccountView.swift +++ b/DarockBili Watch App/PersonalCenter/PersonAccountView.swift @@ -234,10 +234,10 @@ struct PersonAccountView: View { isNetworkFixPresented = true } } - DarockKit.Network.shared.requestString("https://api.darock.top/bili/wbi/sign/\("mid=\(dedeUserID)".base64Encoded())") { respStr, isSuccess in - if isSuccess { - debugPrint(respStr.apiFixed()) - DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/acc/info?\(respStr.apiFixed())", headers: headers) { respJson, isSuccess in + biliWbiSign(paramEncoded: "mid=\(dedeUserID)".base64Encoded()) { signed in + if let signed { + debugPrint(signed) + DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/acc/info?\(signed)", headers: headers) { respJson, isSuccess in if isSuccess { debugPrint(respJson) userFaceUrl = respJson["data"]["face"].string ?? "E" diff --git a/DarockBili Watch App/PersonalCenter/UserDetailView.swift b/DarockBili Watch App/PersonalCenter/UserDetailView.swift index eb07d00dc..400923b8f 100644 --- a/DarockBili Watch App/PersonalCenter/UserDetailView.swift +++ b/DarockBili Watch App/PersonalCenter/UserDetailView.swift @@ -146,10 +146,10 @@ struct UserDetailView: View { let headers: HTTPHeaders = [ "cookie": "SESSDATA=\(sessdata);" ] - DarockKit.Network.shared.requestString("https://api.darock.top/bili/wbi/sign/\("mid=\(uid)".base64Encoded())") { respStr, isSuccess in - if isSuccess { - debugPrint(respStr.apiFixed()) - DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/acc/info?\(respStr.apiFixed())", headers: headers) { respJson, isSuccess in + biliWbiSign(paramEncoded: "mid=\(uid)".base64Encoded()) { signed in + if let signed { + debugPrint(signed) + DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/acc/info?\(signed)", headers: headers) { respJson, isSuccess in if isSuccess { debugPrint(respJson) userFaceUrl = respJson["data"]["face"].string ?? "E" @@ -560,19 +560,24 @@ struct UserDetailView: View { func RefreshVideos() { videos = [[String: String]]() let headers: HTTPHeaders = [ - "cookie": "SESSDATA=\(sessdata);" + "cookie": "SESSDATA=\(sessdata);", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15" ] - DarockKit.Network.shared.requestString("https://api.darock.top/bili/wbi/sign/\("mid=\(uid)&ps=50&pn=\(videoNowPage)".base64Encoded())") { respStr, isSuccess in - if isSuccess { - debugPrint(respStr.apiFixed()) - DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/arc/search?\(respStr.apiFixed())", headers: headers) { respJson, isSuccess in + biliWbiSign(paramEncoded: "mid=\(uid)&ps=50&pn=\(videoNowPage)".base64Encoded()) { signed in + if let signed { + debugPrint(signed) + DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/arc/search?\(signed)", headers: headers) { respJson, isSuccess in if isSuccess { debugPrint(respJson) + if (respJson["code"].int ?? 0) != 0 { + tipWithText("加载失败:\(respJson["message"].string ?? "")", symbol: "xmark.circle.fill") + return + } let vlist = respJson["data"]["list"]["vlist"] for video in vlist { videos.append(["Title": video.1["title"].string ?? "[加载失败]", "Length": video.1["length"].string ?? "E", "PlayCount": String(video.1["play"].int ?? -1), "PicUrl": video.1["pic"].string ?? "E", "BV": video.1["bvid"].string ?? "E", "Timestamp": String(video.1["created"].int ?? 0), "DanmakuCount": String(video.1["video_review"].int ?? -1)]) } - debugPrint(respJson["data"]["page"]["count"].int!) + debugPrint(respJson["data"]["page"]["count"].int ?? 0) videoTotalPage = Int((respJson["data"]["page"]["count"].int ?? 0) / 50) + 1 videoCount = respJson["data"]["page"]["count"].int ?? 0 if !isVideosLoaded { @@ -589,14 +594,19 @@ struct UserDetailView: View { func RefreshArticles() { articles = [[String: String]]() let headers: HTTPHeaders = [ - "cookie": "SESSDATA=\(sessdata);" + "cookie": "SESSDATA=\(sessdata);", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15" ] - DarockKit.Network.shared.requestString("https://api.darock.top/bili/wbi/sign/\("mid=\(uid)&ps=30&pn=\(articleNowPage)&sort=publish_time&platform=web".base64Encoded())") { respStr, isSuccess in - if isSuccess { - debugPrint(respStr.apiFixed()) - DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/article?\(respStr.apiFixed())", headers: headers) { respJson, isSuccess in + biliWbiSign(paramEncoded: "mid=\(uid)&ps=30&pn=\(articleNowPage)&sort=publish_time&platform=web".base64Encoded()) { signed in + if let signed { + debugPrint(signed) + DarockKit.Network.shared.requestJSON("https://api.bilibili.com/x/space/wbi/article?\(signed)", headers: headers) { respJson, isSuccess in if isSuccess { debugPrint(respJson) + if (respJson["code"].int ?? 0) != 0 { + tipWithText("加载失败:\(respJson["message"].string ?? "")", symbol: "xmark.circle.fill") + return + } let articlesJson = respJson["data"]["articles"] for article in articlesJson { articles.append(["Title": article.1["title"].string ?? "[加载失败]", "Summary": article.1["summary"].string ?? "[加载失败]", "Type": article.1["categories"][0]["name"].string ?? "[加载失败]", "View": String(article.1["stats"]["view"].int ?? -1), "Like": String(article.1["stats"]["like"].int ?? -1), "Pic": article.1["image_urls"][0].string ?? "E", "CV": String(article.1["id"].int ?? 0)])