mirror of
https://github.com/czp3009/bilibili-api.git
synced 2026-08-13 22:43:29 +08:00
throw Exception when login fail
This commit is contained in:
45
README.md
45
README.md
@@ -9,11 +9,45 @@ RestFul API
|
||||
compile group: 'com.hiczp', name: 'bilibili-api-rest', version: '0.2.0'
|
||||
```
|
||||
|
||||
Websocket(用于连接直播间弹幕推送服务器以获取实时弹幕)
|
||||
Websocket(用于获取直播间实时弹幕)
|
||||
```groovy
|
||||
compile group: 'com.hiczp', name: 'bilibili-api-websocket', version: '0.2.0'
|
||||
```
|
||||
|
||||
# RestFul API
|
||||
大部分 API 都是 RestFul API.
|
||||
|
||||
`BilibiliClient` 是一个模拟的客户端, 内含登陆状态. 应持有其引用, 并在合适时执行 `close()`.
|
||||
|
||||
## 登陆
|
||||
```kotlin
|
||||
val bilibiliClient = BilibiliClient()
|
||||
runBlocking {
|
||||
bilibiliClient.login(username, password)
|
||||
}
|
||||
```
|
||||
|
||||
`BilibiliClient.login` 会返回一个 `Credential` 实例. 将其序列化后保存. 下次可以直接使用这一凭证来恢复登陆状态
|
||||
|
||||
```kotlin
|
||||
val bilibiliClient = BilibiliClient(credential)
|
||||
```
|
||||
|
||||
多次错误的登陆将导致下一次登陆需要验证码(极验).
|
||||
|
||||
登陆失败将抛出 `LoginException` 异常, 其中包含服务器原始返回内容 `LoginResponse`.
|
||||
|
||||
## 登出
|
||||
```kotlin
|
||||
runBlocking {
|
||||
bilibiliClient.revoke()
|
||||
}
|
||||
```
|
||||
|
||||
`BilibiliClient.revoke` 返回已被注销的凭证, 或返回 `null` 当此 `BilibiliClient` 实例没有包含凭证时.
|
||||
|
||||
如果凭证是错误的, 将抛出 `RevokeException`.
|
||||
|
||||
# 获取直播间实时弹幕
|
||||
举个例子
|
||||
```kotlin
|
||||
@@ -49,9 +83,10 @@ runBlocking {
|
||||
|
||||
```kotlin
|
||||
val command = commandPacket.content
|
||||
val danmakuMessage = command.asDanmakuMessage()
|
||||
with(danmakuMessage) {
|
||||
println("$nickname: $message")
|
||||
if (command.cmd == "DANMU_MSG") {
|
||||
with(command.asDanmakuMessage()) {
|
||||
println("$nickname: $message")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -59,7 +94,7 @@ with(danmakuMessage) {
|
||||
|
||||
如果要为读取操作设定超时, 可以设定为 40秒.
|
||||
|
||||
注意: 如果使用短房间号来连接弹幕推送服务器, 可能会得不到正确的人气值信息(一直为 0 或者一直为 1). 因此在连接弹幕推送服务器前应当首先获取直播间基本信息. 同时, 弹幕服务器不是唯一的, 在构造 `LiveClient` 时可以传入从房间基本信息中获取到的其他服务器地址.
|
||||
注意: 如果使用短房间号来连接弹幕推送服务器, 可能会得不到正确的人气值信息(一直为 0 或者一直为 1). 因此在连接弹幕推送服务器前应当首先获取直播间基本信息. 同时, 弹幕服务器不是唯一的, 在构造 `LiveClient` 时可以传入其他服务器地址.
|
||||
|
||||
# License
|
||||
GPL V3
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package com.hiczp.bilibili.rest
|
||||
|
||||
import com.hiczp.bilibili.rest.ktor.appendMissing
|
||||
import com.hiczp.bilibili.rest.ktor.commonParams
|
||||
import com.hiczp.bilibili.rest.ktor.logging
|
||||
import com.hiczp.bilibili.rest.ktor.rsaEncrypt
|
||||
import com.hiczp.bilibili.rest.ktor.*
|
||||
import com.hiczp.bilibili.rest.service.live.LiveService
|
||||
import com.hiczp.bilibili.rest.service.passport.Credential
|
||||
import com.hiczp.bilibili.rest.service.passport.PassportService
|
||||
import com.hiczp.bilibili.rest.service.passport.cookieMap
|
||||
import com.hiczp.bilibili.rest.service.passport.toCredential
|
||||
import com.hiczp.bilibili.rest.service.orThrow
|
||||
import com.hiczp.bilibili.rest.service.passport.*
|
||||
import com.hiczp.caeruleum.create
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
@@ -41,6 +36,7 @@ class BilibiliClient(credential: Credential? = null) : Closeable {
|
||||
*
|
||||
* @return new credential
|
||||
*/
|
||||
@Throws(LoginException::class)
|
||||
suspend fun login(
|
||||
username: String, password: String,
|
||||
//极验
|
||||
@@ -49,10 +45,12 @@ class BilibiliClient(credential: Credential? = null) : Closeable {
|
||||
validate: String? = null
|
||||
): Credential {
|
||||
val (hash, key) = PassportService.getKey().let { response ->
|
||||
response.hash to response.key.split('\n').filterNot { it.startsWith('-') }.joinToString(separator = "")
|
||||
response.hash to response.key.toDER()
|
||||
}
|
||||
val cipheredPassword = (hash + password).rsaEncrypt(key)
|
||||
return PassportService.login(username, cipheredPassword, challenge, secCode, validate).let {
|
||||
return PassportService.login(username, cipheredPassword, challenge, secCode, validate).orThrow {
|
||||
LoginException(it)
|
||||
}.let {
|
||||
credential.updateAndGet { oldCredential ->
|
||||
if (oldCredential != null) logger.warn { "Override credential: ${oldCredential.userId}" }
|
||||
it.toCredential().also {
|
||||
@@ -67,12 +65,15 @@ class BilibiliClient(credential: Credential? = null) : Closeable {
|
||||
*
|
||||
* @return return old credential, null if not logged in
|
||||
*/
|
||||
@Throws(RevokeException::class)
|
||||
suspend fun revoke() = credential.getAndUpdate { oldCredential ->
|
||||
if (oldCredential != null) {
|
||||
PassportService.revoke(
|
||||
oldCredential.accessToken,
|
||||
oldCredential.cookieMap()
|
||||
)
|
||||
).orThrow {
|
||||
RevokeException(it)
|
||||
}
|
||||
logger.debug { "Token revoked: ${oldCredential.userId}" }
|
||||
}
|
||||
null
|
||||
|
||||
@@ -22,11 +22,17 @@ internal fun String.base64() = Base64.getDecoder().decode(this)
|
||||
|
||||
internal fun ByteArray.base64() = String(Base64.getEncoder().encode(this))
|
||||
|
||||
internal fun String.rsaEncrypt(publicKey: String) =
|
||||
X509EncodedKeySpec(publicKey.base64()).let {
|
||||
internal fun String.rsaEncrypt(publicKey: DER) =
|
||||
X509EncodedKeySpec(publicKey).let {
|
||||
KeyFactory.getInstance("RSA").generatePublic(it)
|
||||
}.let {
|
||||
Cipher.getInstance("RSA/ECB/PKCS1Padding").apply {
|
||||
init(Cipher.ENCRYPT_MODE, it)
|
||||
}
|
||||
}.doFinal(toByteArray()).base64()
|
||||
|
||||
internal typealias PEM = String
|
||||
|
||||
internal typealias DER = ByteArray
|
||||
|
||||
internal fun PEM.toDER(): DER = split('\n').filterNot { it.startsWith('-') }.joinToString(separator = "").base64()
|
||||
|
||||
@@ -3,3 +3,5 @@ package com.hiczp.bilibili.rest.service
|
||||
interface Ok {
|
||||
fun ok(): Boolean
|
||||
}
|
||||
|
||||
inline fun <T : Ok> T.orThrow(block: (T) -> Throwable) = if (ok()) this else throw block(this)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package com.hiczp.bilibili.rest.service.passport
|
||||
|
||||
import com.hiczp.bilibili.rest.service.passport.model.LoginResponse
|
||||
import com.hiczp.bilibili.rest.service.passport.model.RevokeResponse
|
||||
|
||||
class LoginException internal constructor(val loginResponse: LoginResponse) : IllegalStateException(loginResponse.message)
|
||||
|
||||
class RevokeException internal constructor(val revokeResponse: RevokeResponse) : IllegalStateException(revokeResponse.message)
|
||||
@@ -4,10 +4,10 @@ import com.hiczp.bilibili.rest.BilibiliClientInherent
|
||||
import com.hiczp.bilibili.rest.ktor.appendMissing
|
||||
import com.hiczp.bilibili.rest.ktor.commonParams
|
||||
import com.hiczp.bilibili.rest.ktor.logging
|
||||
import com.hiczp.bilibili.rest.service.Response
|
||||
import com.hiczp.bilibili.rest.service.passport.model.GetKeyResponse
|
||||
import com.hiczp.bilibili.rest.service.passport.model.LoginResponse
|
||||
import com.hiczp.bilibili.rest.service.passport.model.OAuth2Info
|
||||
import com.hiczp.bilibili.rest.service.passport.model.RevokeResponse
|
||||
import com.hiczp.caeruleum.annotation.*
|
||||
import com.hiczp.caeruleum.create
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -63,7 +63,7 @@ interface PassportService {
|
||||
@Field("SESSDATA") sessData: String? = null,
|
||||
@Field("bili_jct") biliJct: String? = null,
|
||||
@Field sid: String? = null
|
||||
): Response
|
||||
): RevokeResponse
|
||||
|
||||
/**
|
||||
* 将所有 cookie 以 Map 形式传入
|
||||
@@ -73,7 +73,7 @@ interface PassportService {
|
||||
suspend fun revoke(
|
||||
@Field("access_token") accessToken: String,
|
||||
@FieldMap cookieMap: Map<String, String> = emptyMap()
|
||||
): Response
|
||||
): RevokeResponse
|
||||
|
||||
/**
|
||||
* 获取 OAuth2 信息
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.hiczp.bilibili.rest.service.passport.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.hiczp.bilibili.rest.ktor.PEM
|
||||
import com.hiczp.bilibili.rest.service.Ok
|
||||
|
||||
data class GetKeyResponse(
|
||||
@@ -17,7 +18,7 @@ data class GetKeyResponse(
|
||||
@SerializedName("hash")
|
||||
val hash: String = "", // a953480d976dd1ba
|
||||
@SerializedName("key")
|
||||
val key: String = "" // -----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjb4V7EidX/ym28t2ybo0U6t0n6p4ej8VjqKHg100va6jkNbNTrLQqMCQCAYtXMXXp2Fwkk6WR+12N9zknLjf+C9sx/+l48mjUU8RqahiFD1XT/u2e0m2EN029OhCgkHx3Fc/KlFSIbak93EH/XlYis0w+Xl69GV6klzgxW6d2xQIDAQAB-----END PUBLIC KEY-----
|
||||
val key: PEM = "" // -----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjb4V7EidX/ym28t2ybo0U6t0n6p4ej8VjqKHg100va6jkNbNTrLQqMCQCAYtXMXXp2Fwkk6WR+12N9zknLjf+C9sx/+l48mjUU8RqahiFD1XT/u2e0m2EN029OhCgkHx3Fc/KlFSIbak93EH/XlYis0w+Xl69GV6klzgxW6d2xQIDAQAB-----END PUBLIC KEY-----
|
||||
)
|
||||
|
||||
override fun ok() = code == 0
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.hiczp.bilibili.rest.service.passport.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.hiczp.bilibili.rest.service.Ok
|
||||
|
||||
data class RevokeResponse(
|
||||
@SerializedName("message")
|
||||
val message: String? = "", // user not login
|
||||
@SerializedName("ts")
|
||||
val ts: Long = 0, // 1562653921
|
||||
@SerializedName("code")
|
||||
val code: Int = 0 // -101
|
||||
) : Ok {
|
||||
override fun ok() = code == 0
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class BilibiliClientTest {
|
||||
runBlocking {
|
||||
bilibiliClient.login(username, password)
|
||||
bilibiliClient.credential.value!!.println()
|
||||
bilibiliClient.revoke()
|
||||
bilibiliClient.revoke()!!.println()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user