From e083859b1ac8147dbd6540b9d87ae82df7ede281 Mon Sep 17 00:00:00 2001 From: czp3009 Date: Wed, 20 Feb 2019 14:55:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=94=A8=E6=88=B7=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 ++- build.gradle | 2 + .../kotlin/com/hiczp/bilibili/api/BaseUrl.kt | 5 + .../com/hiczp/bilibili/api/BilibiliClient.kt | 108 ++++++++++++------ .../com/hiczp/bilibili/api/app/AppAPI.kt | 12 ++ .../hiczp/bilibili/api/app/model/MyInfo.kt | 67 +++++++++++ .../bilibili/api/retrofit/CommonResponse.kt | 8 +- .../exception/BilibiliApiException.kt | 2 +- .../interceptor/FailureResponseInterceptor.kt | 12 +- .../com/hiczp/bilibili/api/test/Config.kt | 22 +++- .../hiczp/bilibili/api/test/UserInfoTest.kt | 13 +++ src/test/resources/_config.json | 61 +++++++++- 12 files changed, 285 insertions(+), 46 deletions(-) create mode 100644 src/main/kotlin/com/hiczp/bilibili/api/app/AppAPI.kt create mode 100644 src/main/kotlin/com/hiczp/bilibili/api/app/model/MyInfo.kt rename src/main/kotlin/com/hiczp/bilibili/api/{ => retrofit}/exception/BilibiliApiException.kt (85%) create mode 100644 src/test/kotlin/com/hiczp/bilibili/api/test/UserInfoTest.kt diff --git a/README.md b/README.md index da6fa4d..683b72e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析. # 登录和登出 +https://passport.bilibili.com + 登陆和登出均为异步方法, 需要在协程上下文中执行. runBlocking { @@ -27,7 +29,7 @@ {"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1>=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"} -自行访问 `data.url` 将打开一个极验弹窗, 通过验证码后再次调用登陆接口: +自行访问 `commonResponse.data.obj.url.string` 将打开一个极验弹窗, 通过验证码后再次调用登陆接口: login(username, password, challenge, secCode, validate) @@ -41,6 +43,21 @@ 登陆后, 可以访问全部 API. +# app +https://app.bilibili.com + + BilibiliClient().appAPI() + +为 app 提供通用接口, 例如获取个人信息. 完整示例如下 + + runBlocking { + val bilibiliClient = BilibiliClient().apply { + login(username, password) + } + val myInfo = bilibiliClient.appAPI().myInfo().await() + println(myInfo) + } + # 主站 //TODO diff --git a/build.gradle b/build.gradle index 9707a81..7649b86 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,8 @@ dependencies { compile group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.5.0' // https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson compile group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.5.0' + // https://mvnrepository.com/artifact/com.github.salomonbrys.kotson/kotson + compile group: 'com.github.salomonbrys.kotson', name: 'kotson', version: '2.5.0' // https://mvnrepository.com/artifact/com.jakewharton.retrofit/retrofit2-kotlin-coroutines-adapter compile group: 'com.jakewharton.retrofit', name: 'retrofit2-kotlin-coroutines-adapter', version: '0.9.2' // https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor diff --git a/src/main/kotlin/com/hiczp/bilibili/api/BaseUrl.kt b/src/main/kotlin/com/hiczp/bilibili/api/BaseUrl.kt index a30d42e..f109983 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/BaseUrl.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/BaseUrl.kt @@ -9,6 +9,11 @@ object BaseUrl { */ const val passport = "https://passport.bilibili.com" + /** + * 提供通用功能, 例如获取用户信息 + */ + const val app = "https://app.bilibili.com" + /** * 直播站 */ diff --git a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt index 19f0591..a1a000d 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt @@ -1,15 +1,17 @@ package com.hiczp.bilibili.api -import com.hiczp.bilibili.api.exception.BilibiliApiException +import com.hiczp.bilibili.api.app.AppAPI import com.hiczp.bilibili.api.passport.PassportAPI import com.hiczp.bilibili.api.passport.model.LoginResponse import com.hiczp.bilibili.api.retrofit.ParamType +import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException import com.hiczp.bilibili.api.retrofit.interceptor.CommonHeaderInterceptor import com.hiczp.bilibili.api.retrofit.interceptor.CommonParamInterceptor import com.hiczp.bilibili.api.retrofit.interceptor.FailureResponseInterceptor import com.hiczp.bilibili.api.retrofit.interceptor.SortAndSignInterceptor import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory import mu.KotlinLogging +import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -55,39 +57,57 @@ class BilibiliClient( val isLogin get() = loginResponse != null + //快捷方式 + val userId get() = loginResponse?.userId + val token get() = loginResponse?.token + + /** + * 用户鉴权相关的接口 + */ @Suppress("SpellCheckingInspection") - val passportAPI: PassportAPI by lazy { - val okHttpClient = OkHttpClient.Builder().apply { - addInterceptor(CommonHeaderInterceptor( - "Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" }, - "Buvid" to { billingClientProperties.buildVersionId }, - "User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" }, - "Device-ID" to { billingClientProperties.hardwareId } - )) - addInterceptor(CommonParamInterceptor(ParamType.FORM_URL_ENCODED, - "appkey" to { billingClientProperties.appKey }, - "build" to { billingClientProperties.build }, - "channel" to { billingClientProperties.channel }, - "mobi_app" to { billingClientProperties.platform }, - "platform" to { billingClientProperties.platform }, - "ts" to { Instant.now().epochSecond.toString() } - )) - addInterceptor(SortAndSignInterceptor(ParamType.FORM_URL_ENCODED, billingClientProperties.appSecret)) - addInterceptor(FailureResponseInterceptor) + val passportAPI by lazy { + createAPI(BaseUrl.passport, logLevel, + CommonHeaderInterceptor( + "Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" }, + "Buvid" to { billingClientProperties.buildVersionId }, + "User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" }, + "Device-ID" to { billingClientProperties.hardwareId } + ), + CommonParamInterceptor(ParamType.FORM_URL_ENCODED, + "appkey" to { billingClientProperties.appKey }, + "build" to { billingClientProperties.build }, + "channel" to { billingClientProperties.channel }, + "mobi_app" to { billingClientProperties.platform }, + "platform" to { billingClientProperties.platform }, + "ts" to { Instant.now().epochSecond.toString() } + ), + SortAndSignInterceptor(ParamType.FORM_URL_ENCODED, billingClientProperties.appSecret) + ) + } - //log - if (logLevel != HttpLoggingInterceptor.Level.NONE) { - addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel)) - } - }.build() - - Retrofit.Builder() - .baseUrl(BaseUrl.passport) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(CoroutineCallAdapterFactory()) - .client(okHttpClient) - .build() - .create(PassportAPI::class.java) + /** + * 提供一些通用信息 + */ + @Suppress("SpellCheckingInspection") + val appAPI by lazy { + createAPI(BaseUrl.app, logLevel, + CommonHeaderInterceptor( + "Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" }, + "Buvid" to { billingClientProperties.buildVersionId }, + "User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" }, + "Device-ID" to { billingClientProperties.hardwareId } + ), + CommonParamInterceptor(ParamType.QUERY, + "access_key" to { token }, + "appkey" to { billingClientProperties.appKey }, + "build" to { billingClientProperties.build }, + "channel" to { billingClientProperties.channel }, + "mobi_app" to { billingClientProperties.platform }, + "platform" to { billingClientProperties.platform }, + "ts" to { Instant.now().epochSecond.toString() } + ), + SortAndSignInterceptor(ParamType.QUERY, billingClientProperties.appSecret) + ) } /** @@ -145,3 +165,27 @@ class BilibiliClient( loginResponse = null } } + +@Suppress("SpellCheckingInspection") +private val gsonConverterFactory = GsonConverterFactory.create() +private val coroutineCallAdapterFactory = CoroutineCallAdapterFactory() +private inline fun createAPI( + baseUrl: String, + logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.NONE, + vararg interceptors: Interceptor +) = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(gsonConverterFactory) + .addCallAdapterFactory(coroutineCallAdapterFactory) + .client(OkHttpClient.Builder().apply { + interceptors.forEach { + addInterceptor(it) + } + addInterceptor(FailureResponseInterceptor) + //log + if (logLevel != HttpLoggingInterceptor.Level.NONE) { + addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel)) + } + }.build()) + .build() + .create(T::class.java) diff --git a/src/main/kotlin/com/hiczp/bilibili/api/app/AppAPI.kt b/src/main/kotlin/com/hiczp/bilibili/api/app/AppAPI.kt new file mode 100644 index 0000000..318c81a --- /dev/null +++ b/src/main/kotlin/com/hiczp/bilibili/api/app/AppAPI.kt @@ -0,0 +1,12 @@ +package com.hiczp.bilibili.api.app + +import com.hiczp.bilibili.api.app.model.MyInfo +import kotlinx.coroutines.Deferred +import retrofit2.http.GET + +@Suppress("DeferredIsResult") +interface AppAPI { + @Suppress("SpellCheckingInspection") + @GET("/x/v2/account/myinfo") + fun myInfo(): Deferred +} diff --git a/src/main/kotlin/com/hiczp/bilibili/api/app/model/MyInfo.kt b/src/main/kotlin/com/hiczp/bilibili/api/app/model/MyInfo.kt new file mode 100644 index 0000000..88e3ec9 --- /dev/null +++ b/src/main/kotlin/com/hiczp/bilibili/api/app/model/MyInfo.kt @@ -0,0 +1,67 @@ +package com.hiczp.bilibili.api.app.model + +import com.google.gson.annotations.SerializedName + +data class MyInfo( + @SerializedName("code") + var code: Int, // 0 + @SerializedName("data") + var `data`: Data, + @SerializedName("message") + var message: String, // 0 + @SerializedName("ttl") + var ttl: Int // 1 +) { + data class Data( + @SerializedName("birthday") + var birthday: String, // 1995-11-18 + @SerializedName("coins") + var coins: Int, // 1025 + @SerializedName("email_status") + var emailStatus: Int, // 0 + @SerializedName("face") + var face: String, // http://i1.hdslb.com/bfs/face/4f65e79399ad5a1bf3f877851b2f819d5870b494.jpg + @SerializedName("identification") + var identification: Int, // 1 + @SerializedName("level") + var level: Int, // 4 + @SerializedName("mid") + var mid: Int, // 20293030 + @SerializedName("name") + var name: String, // czp3009 + @SerializedName("official") + var official: Official, + @SerializedName("rank") + var rank: Int, // 10000 + @SerializedName("sex") + var sex: Int, // 0 + @SerializedName("sign") + var sign: String, + @SerializedName("silence") + var silence: Int, // 0 + @SerializedName("tel_status") + var telStatus: Int, // 1 + @SerializedName("vip") + var vip: Vip + ) { + data class Official( + @SerializedName("desc") + var desc: String, + @SerializedName("role") + var role: Int, // 0 + @SerializedName("title") + var title: String + ) + + data class Vip( + @SerializedName("due_date") + var dueDate: Int, // 0 + @SerializedName("status") + var status: Int, // 0 + @SerializedName("type") + var type: Int, // 0 + @SerializedName("vip_pay_type") + var vipPayType: Int // 0 + ) + } +} diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/CommonResponse.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/CommonResponse.kt index bcac225..417adeb 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/CommonResponse.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/CommonResponse.kt @@ -26,5 +26,11 @@ data class CommonResponse( * data 可能是各种类型, 例如 array, object, string */ @SerializedName("data") - var data: JsonElement? + var data: JsonElement?, + + /** + * ttl, 不明确含义, 如果存在则值总为 1 + */ + @SerializedName("ttl") + var ttl: Int? ) diff --git a/src/main/kotlin/com/hiczp/bilibili/api/exception/BilibiliApiException.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/exception/BilibiliApiException.kt similarity index 85% rename from src/main/kotlin/com/hiczp/bilibili/api/exception/BilibiliApiException.kt rename to src/main/kotlin/com/hiczp/bilibili/api/retrofit/exception/BilibiliApiException.kt index bc45040..fa8d2ed 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/exception/BilibiliApiException.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/exception/BilibiliApiException.kt @@ -1,4 +1,4 @@ -package com.hiczp.bilibili.api.exception +package com.hiczp.bilibili.api.retrofit.exception import com.hiczp.bilibili.api.retrofit.CommonResponse import java.io.IOException diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/FailureResponseInterceptor.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/FailureResponseInterceptor.kt index de9a211..a94d65f 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/FailureResponseInterceptor.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/FailureResponseInterceptor.kt @@ -1,9 +1,11 @@ package com.hiczp.bilibili.api.retrofit.interceptor +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.int +import com.github.salomonbrys.kotson.obj import com.google.gson.Gson import com.google.gson.JsonParser -import com.hiczp.bilibili.api.exception.BilibiliApiException -import com.hiczp.bilibili.api.retrofit.CommonResponse +import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException import okhttp3.Interceptor import okhttp3.Response @@ -32,12 +34,12 @@ object FailureResponseInterceptor : Interceptor { val jsonObject = body.source().also { it.request(Long.MAX_VALUE) }.buffer.clone().inputStream().reader(charset).let { - jsonParser.parse(it).asJsonObject + jsonParser.parse(it).obj } //判断 code 是否为 0 - if (jsonObject["code"].asInt != 0) { - throw BilibiliApiException(gson.fromJson(jsonObject, CommonResponse::class.java)) + if (jsonObject["code"].int != 0) { + throw BilibiliApiException(gson.fromJson(jsonObject)) } return response diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/Config.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/Config.kt index c51be7f..80be34f 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/Config.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/Config.kt @@ -1,15 +1,27 @@ package com.hiczp.bilibili.api.test +import com.github.salomonbrys.kotson.byString +import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson import com.google.gson.JsonObject +import com.hiczp.bilibili.api.BilibiliClient +import okhttp3.logging.HttpLoggingInterceptor object Config { - private val config = Gson().fromJson( - Config::class.java.getResourceAsStream("/config.json").reader(), - JsonObject::class.java + @Suppress("SpellCheckingInspection") + private val gson = Gson() + + private val config = gson.fromJson( + Config::class.java.getResourceAsStream("/config.json").reader() ) - val username = config["username"].asString!! + val username by config.byString - val password = config["password"].asString!! + val password by config.byString + + val presetBilibiliClient by lazy { + BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY).apply { + loginResponse = config["loginResponse"]?.let { gson.fromJson(it) } + } + } } diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/UserInfoTest.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/UserInfoTest.kt new file mode 100644 index 0000000..ca72be8 --- /dev/null +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/UserInfoTest.kt @@ -0,0 +1,13 @@ +package com.hiczp.bilibili.api.test + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test + +class UserInfoTest { + @Test + fun info() { + runBlocking { + Config.presetBilibiliClient.appAPI.myInfo().await() + } + } +} diff --git a/src/test/resources/_config.json b/src/test/resources/_config.json index c9e388e..cd4d587 100644 --- a/src/test/resources/_config.json +++ b/src/test/resources/_config.json @@ -1,5 +1,64 @@ { "username": "123456789", "password": "123456", - "roomId": "3" + "roomId": "3", + "loginResponse": { + "ts": 1550629285, + "code": 0, + "data": { + "status": 0, + "token_info": { + "mid": 20293030, + "access_token": "b0b214e97b7bf388769eb727da27f951", + "refresh_token": "54c56bda9bde64cacb44a14e92007d51", + "expires_in": 2592000 + }, + "cookie_info": { + "cookies": [ + { + "name": "bili_jct", + "value": "578b271b308c760cbd281c37afc420c4", + "http_only": 0, + "expires": 1553221285 + }, + { + "name": "DedeUserID", + "value": "20293035", + "http_only": 0, + "expires": 1553221285 + }, + { + "name": "DedeUserID__ckMd5", + "value": "cdff5c8e58b793cd", + "http_only": 0, + "expires": 1553221285 + }, + { + "name": "sid", + "value": "ja724hee", + "http_only": 0, + "expires": 1553221285 + }, + { + "name": "SESSDATA", + "value": "eaa5b420%2C1553221285%2C59590a31", + "http_only": 1, + "expires": 1553221285 + } + ], + "domains": [ + ".bilibili.com", + ".biligame.com", + ".im9.com", + ".bigfunapp.cn" + ] + }, + "sso": [ + "https://passport.bilibili.com/api/v2/sso", + "https://passport.biligame.com/api/v2/sso", + "https://passport.im9.com/api/v2/sso", + "https://passport.bigfunapp.cn/api/v2/sso" + ] + } + } }