
Purely functional, highly asynchronous forum client handling network requests, session management and HTML parsing into strongly-typed DTOs, with async-first API and safe sealed-result handling.
ProfilePage, ThreadPage, and ForumPage data immediately.YamiboResult sealed class wrapping Success, Failure, Maintenance, and NotLoggedIn states.| Dependency | Supported Version |
|---|---|
| Kotlin | 2.4.10+ |
| Gradle | 8.14.3+ |
| Java JVM Target | 11+ |
Add the dependency to your build.gradle.kts:
dependencies {
implementation("io.github.littlesurvival:yamibo-api:$version")
}Include the following in your pom.xml:
<dependency>
<groupId>io.github.littlesurvival</groupId>
<artifactId>yamibo-api</artifactId>
<version>$version</version>
</dependency>The entry point of the library is the YamiboClient. Most Yamibo routes require authentication, so you must supply the user's cookie.
import io.github.littlesurvival.YamiboClient
// 1. Create a client instance
val client = YamiboClient(
timeoutMillis = 30_000L // 30 seconds timeout
)
// 2. Set the user authentication cookie
client.setCookie("your_user_cookie_here")All methods return a YamiboResult<T>, ensuring you handle all edge cases elegantly.
import io.github.littlesurvival.core.YamiboResult
suspend fun fetchYamiboHome() {
when (val result = client.fetchHomePage()) {
is YamiboResult.Success -> {
val homePage = result.value
println("Welcome! Found ${homePage.forums.size} forums.")
}
is YamiboResult.NotLoggedIn -> {
println("Error: Cookie expired or invalid.")
}
is YamiboResult.Maintenance -> {
println("Yamibo is currently under maintenance.")
}
is YamiboResult.NoPermission -> {
println("Permission denied: ${result.message()}")
}
is YamiboResult.WafChallenge -> {
println("Connection verification failed; refresh to retry: ${result.message()}")
}
is YamiboResult.Failure -> {
println("Network or parsing failed: ${result.message()}")
}
}
}Fetching a Forum Page:
import io.github.littlesurvival.dto.value.ForumId
suspend fun loadForum() {
// Fetch forum layout and threads on page 1
val forumResult = client.fetchForumById(ForumId(5), page = 1)
}Fetching a Thread Context:
import io.github.littlesurvival.dto.value.ThreadId
suspend fun loadThread() {
// Fetch posts and details within a specific thread
val threadResult = client.fetchThreadById(ThreadId(12345), page = 1)
}Fetching User Profile:
suspend fun loadProfile() {
// Requires a valid cookie to parse current user details
val profileResult = client.fetchProfileInfo()
}Searching the Forum:
import io.github.littlesurvival.dto.value.FormHash
suspend fun doSearch() {
val searchResult = client.fetchSearch(
query = "Yuri",
forumId = null, // Optional: Search within a specific forum
formHash = FormHash("your_form_hash") // Security token
)
}Interactions (Replying, Rating, Favorites):
import io.github.littlesurvival.dto.value.PostId
suspend fun interact() {
// Add thread to favorites
client.fetchAddFavorite(ThreadId(12345), FormHash("your_form_hash"))
// Reply to a thread/post
client.fetchReplyPost(ThreadId(12345), PostId(9876), "Great post!", FormHash("your_form_hash"))
// Rate a specific post
client.fetchRatePost(ThreadId(12345), PostId(9876), score = 1, reason = "Thanks", FormHash("your_form_hash"))
}Use one shared YamiboClient for API calls and mount one host behind the app content. The host creates a platform WebView only while recovering an observed Baidu NOX challenge; it never presents WAF-specific UI.
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.github.littlesurvival.YamiboClient
import io.github.littlesurvival.waf.YamiboWafChallengeHost
@Composable
fun YamiboApp(isForeground: Boolean) {
val client = remember { YamiboClient(timeoutMillis = 60_000L) }
DisposableEffect(client) { onDispose { client.close() } }
Box(Modifier.fillMaxSize()) {
// Keep the normal-size recovery WebView behind the visible application.
YamiboWafChallengeHost(client, isForeground, Modifier.fillMaxSize())
// Repositories and screens must use this exact client instance.
AppContent(client)
}
}Successful recovery only makes the current load take longer: the host polls the platform cookie store without waiting for the destination page to finish, verifies nox_jst_v1, and replays an eligible request at most once. The client keeps one composed Cookie header and replaces only its nox_jst_v1 entry; WAF-host guest cookies are never imported. Routine setCookie() calls preserve the current NOX entry. When an app-owned trusted login or sign-in WebView produces a complete cookie snapshot, call client.setCookie(cookie, importNox = true) so a valid newer NOX entry can replace it explicitly. If silent recovery fails, the API returns YamiboResult.WafChallenge; keep the existing failure UI and let the user refresh to fetch again. Headless/background clients never create a WebView. Call client.clearCookies() during logout to clear authentication state while preserving NOX. Use client.clearCookies(clearNox = true) only for a full network-cookie reset. WafRecoveryConfig(enabled = false) disables automatic recovery.
ProfilePage、ThreadPage 與 ForumPage 等豐富的結構化資料表。YamiboResult 封裝,妥善且優雅地處理 Success (成功)、Failure (失敗)、Maintenance (維護中) 以及 NotLoggedIn (未登入) 等各種極端狀態。| 依賴項目 | 支援版本 |
|---|---|
| Kotlin | 2.4.10+ |
| Gradle | 8.14.3+ |
| Java JVM 目標 | 11+ |
請在模組內的 build.gradle.kts 新增依賴:
dependencies {
implementation("io.github.littlesurvival:yamibo-api:$version")
}如果你的專案使用 Maven 建置環境,請在 pom.xml 加入配置:
<dependency>
<groupId>io.github.littlesurvival</groupId>
<artifactId>yamibo-api</artifactId>
<version>$version</version>
</dependency>操作本函式庫最核心的點位就是 YamiboClient。由於大多數的 Yamibo 網站路由都需要論壇使用者權限,你必須利用方法設定有效的使用者 Cookie。
import io.github.littlesurvival.YamiboClient
// 1. 建立 Client 實體物件
val client = YamiboClient(
timeoutMillis = 30_000L // 設定超時時間為 30 秒鐘
)
// 2. 寫入使用者的認證 Cookie
client.setCookie("請輸入你的_user_cookie")所有牽涉到網路的 API 方法都會回傳 YamiboResult<T>,這項類別會幫助你應對各種網路和未知的解析情況。
import io.github.littlesurvival.core.YamiboResult
suspend fun fetchYamiboHome() {
when (val result = client.fetchHomePage()) {
is YamiboResult.Success -> {
val homePage = result.value
println("歡迎回來!目前論壇共有 ${homePage.forums.size} 個看板。")
}
is YamiboResult.NotLoggedIn -> {
println("錯誤:Cookie 已過期或是你尚未登入。")
}
is YamiboResult.Maintenance -> {
println("當前 Yamibo 正在進行系統維護中。")
}
is YamiboResult.NoPermission -> {
println("權限不足:${result.message()}")
}
is YamiboResult.WafChallenge -> {
println("連線驗證失敗,請 refresh 後重試:${result.message()}")
}
is YamiboResult.Failure -> {
println("網路連線錯誤或是資料解析異常:${result.message()}")
}
}
}讀取特定看板 (Forum):
import io.github.littlesurvival.dto.value.ForumId
suspend fun loadForum() {
// 取得版塊編號為 5 的第一頁主題與貼文列表
val forumResult = client.fetchForumById(ForumId(5), page = 1)
}讀取討論串內容 (Thread):
import io.github.littlesurvival.dto.value.ThreadId
suspend fun loadThread() {
// 取得特定討論串的內容與它的各樓層回覆列表
val threadResult = client.fetchThreadById(ThreadId(12345), page = 1)
}取得當前使用者資訊 (Profile):
suspend fun loadProfile() {
// 發出請求,解析並獲得當前登入使用者的個人資訊、頭像與相關統計數據
val profileResult = client.fetchProfileInfo()
}全站或特定看板內搜尋 (Search):
import io.github.littlesurvival.dto.value.FormHash
suspend fun doSearch() {
val searchResult = client.fetchSearch(
query = "百合",
forumId = null, // 選填項目:可指定要搜尋的看板 ID
formHash = FormHash("請填入你的_form_hash") // 必要的安全驗證碼
)
}各類使用者網頁操作 (發出回覆、給予評分、收藏文章):
import io.github.littlesurvival.dto.value.PostId
suspend fun interact() {
// 將文章加入至我的使用者收藏內
client.fetchAddFavorite(ThreadId(12345), FormHash("你的_form_hash"))
// 對指定文章主題內進行樓層的發文回覆
client.fetchReplyPost(ThreadId(12345), PostId(9876), "感謝大大的熱情分享!", FormHash("你的_form_hash"))
// 對其他使用者的指定樓層文章進行論壇評分
client.fetchRatePost(ThreadId(12345), PostId(9876), score = 1, reason = "感謝分享", FormHash("你的_form_hash"))
}API 呼叫與 Host 必須共用同一個 YamiboClient,並把 Host 排在 App 內容後方。Host 只在處理已觀察到的百度 NOX challenge 時建立系統 WebView,全程不顯示 WAF 專用 UI。
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.github.littlesurvival.YamiboClient
import io.github.littlesurvival.waf.YamiboWafChallengeHost
@Composable
fun YamiboApp(isForeground: Boolean) {
val client = remember { YamiboClient(timeoutMillis = 60_000L) }
DisposableEffect(client) { onDispose { client.close() } }
Box(Modifier.fillMaxSize()) {
// 正常尺寸的 recovery WebView 位於可見 App 內容後方。
YamiboWafChallengeHost(client, isForeground, Modifier.fillMaxSize())
// Repository 與畫面必須使用這個完全相同的 client。
AppContent(client)
}
}恢復成功時,使用者只會感覺本次載入比較久:Host 不等待目的頁面載入完成,會持續讀取平台 Cookie store,驗證 nox_jst_v1 後最多重送一次符合政策的請求。Client 內只維護一份合成 Cookie header,恢復時只替換其中的 nox_jst_v1,不會匯入 WAF Host 產生的 guest cookies。一般 setCookie() 會保留 Client 現有 NOX;若 App 自己的可信登入/簽到 WebView 剛產生完整 cookie snapshot,則呼叫 client.setCookie(cookie, importNox = true),讓其中有效的新 NOX 明確覆蓋。若 silent recovery 失敗,API 回傳 YamiboResult.WafChallenge;前端維持原本的失敗畫面,讓使用者 refresh 後重新 fetch 即可。背景或無 UI client 不會建立 WebView。登出時呼叫 client.clearCookies(),只清除登入狀態並保留 NOX;只有完整重設網路 Cookie 時才使用 client.clearCookies(clearNox = true)。WafRecoveryConfig(enabled = false) 可停用自動恢復。
ProfilePage, ThreadPage, and ForumPage data immediately.YamiboResult sealed class wrapping Success, Failure, Maintenance, and NotLoggedIn states.| Dependency | Supported Version |
|---|---|
| Kotlin | 2.4.10+ |
| Gradle | 8.14.3+ |
| Java JVM Target | 11+ |
Add the dependency to your build.gradle.kts:
dependencies {
implementation("io.github.littlesurvival:yamibo-api:$version")
}Include the following in your pom.xml:
<dependency>
<groupId>io.github.littlesurvival</groupId>
<artifactId>yamibo-api</artifactId>
<version>$version</version>
</dependency>The entry point of the library is the YamiboClient. Most Yamibo routes require authentication, so you must supply the user's cookie.
import io.github.littlesurvival.YamiboClient
// 1. Create a client instance
val client = YamiboClient(
timeoutMillis = 30_000L // 30 seconds timeout
)
// 2. Set the user authentication cookie
client.setCookie("your_user_cookie_here")All methods return a YamiboResult<T>, ensuring you handle all edge cases elegantly.
import io.github.littlesurvival.core.YamiboResult
suspend fun fetchYamiboHome() {
when (val result = client.fetchHomePage()) {
is YamiboResult.Success -> {
val homePage = result.value
println("Welcome! Found ${homePage.forums.size} forums.")
}
is YamiboResult.NotLoggedIn -> {
println("Error: Cookie expired or invalid.")
}
is YamiboResult.Maintenance -> {
println("Yamibo is currently under maintenance.")
}
is YamiboResult.NoPermission -> {
println("Permission denied: ${result.message()}")
}
is YamiboResult.WafChallenge -> {
println("Connection verification failed; refresh to retry: ${result.message()}")
}
is YamiboResult.Failure -> {
println("Network or parsing failed: ${result.message()}")
}
}
}Fetching a Forum Page:
import io.github.littlesurvival.dto.value.ForumId
suspend fun loadForum() {
// Fetch forum layout and threads on page 1
val forumResult = client.fetchForumById(ForumId(5), page = 1)
}Fetching a Thread Context:
import io.github.littlesurvival.dto.value.ThreadId
suspend fun loadThread() {
// Fetch posts and details within a specific thread
val threadResult = client.fetchThreadById(ThreadId(12345), page = 1)
}Fetching User Profile:
suspend fun loadProfile() {
// Requires a valid cookie to parse current user details
val profileResult = client.fetchProfileInfo()
}Searching the Forum:
import io.github.littlesurvival.dto.value.FormHash
suspend fun doSearch() {
val searchResult = client.fetchSearch(
query = "Yuri",
forumId = null, // Optional: Search within a specific forum
formHash = FormHash("your_form_hash") // Security token
)
}Interactions (Replying, Rating, Favorites):
import io.github.littlesurvival.dto.value.PostId
suspend fun interact() {
// Add thread to favorites
client.fetchAddFavorite(ThreadId(12345), FormHash("your_form_hash"))
// Reply to a thread/post
client.fetchReplyPost(ThreadId(12345), PostId(9876), "Great post!", FormHash("your_form_hash"))
// Rate a specific post
client.fetchRatePost(ThreadId(12345), PostId(9876), score = 1, reason = "Thanks", FormHash("your_form_hash"))
}Use one shared YamiboClient for API calls and mount one host behind the app content. The host creates a platform WebView only while recovering an observed Baidu NOX challenge; it never presents WAF-specific UI.
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.github.littlesurvival.YamiboClient
import io.github.littlesurvival.waf.YamiboWafChallengeHost
@Composable
fun YamiboApp(isForeground: Boolean) {
val client = remember { YamiboClient(timeoutMillis = 60_000L) }
DisposableEffect(client) { onDispose { client.close() } }
Box(Modifier.fillMaxSize()) {
// Keep the normal-size recovery WebView behind the visible application.
YamiboWafChallengeHost(client, isForeground, Modifier.fillMaxSize())
// Repositories and screens must use this exact client instance.
AppContent(client)
}
}Successful recovery only makes the current load take longer: the host polls the platform cookie store without waiting for the destination page to finish, verifies nox_jst_v1, and replays an eligible request at most once. The client keeps one composed Cookie header and replaces only its nox_jst_v1 entry; WAF-host guest cookies are never imported. Routine setCookie() calls preserve the current NOX entry. When an app-owned trusted login or sign-in WebView produces a complete cookie snapshot, call client.setCookie(cookie, importNox = true) so a valid newer NOX entry can replace it explicitly. If silent recovery fails, the API returns YamiboResult.WafChallenge; keep the existing failure UI and let the user refresh to fetch again. Headless/background clients never create a WebView. Call client.clearCookies() during logout to clear authentication state while preserving NOX. Use client.clearCookies(clearNox = true) only for a full network-cookie reset. WafRecoveryConfig(enabled = false) disables automatic recovery.
ProfilePage、ThreadPage 與 ForumPage 等豐富的結構化資料表。YamiboResult 封裝,妥善且優雅地處理 Success (成功)、Failure (失敗)、Maintenance (維護中) 以及 NotLoggedIn (未登入) 等各種極端狀態。| 依賴項目 | 支援版本 |
|---|---|
| Kotlin | 2.4.10+ |
| Gradle | 8.14.3+ |
| Java JVM 目標 | 11+ |
請在模組內的 build.gradle.kts 新增依賴:
dependencies {
implementation("io.github.littlesurvival:yamibo-api:$version")
}如果你的專案使用 Maven 建置環境,請在 pom.xml 加入配置:
<dependency>
<groupId>io.github.littlesurvival</groupId>
<artifactId>yamibo-api</artifactId>
<version>$version</version>
</dependency>操作本函式庫最核心的點位就是 YamiboClient。由於大多數的 Yamibo 網站路由都需要論壇使用者權限,你必須利用方法設定有效的使用者 Cookie。
import io.github.littlesurvival.YamiboClient
// 1. 建立 Client 實體物件
val client = YamiboClient(
timeoutMillis = 30_000L // 設定超時時間為 30 秒鐘
)
// 2. 寫入使用者的認證 Cookie
client.setCookie("請輸入你的_user_cookie")所有牽涉到網路的 API 方法都會回傳 YamiboResult<T>,這項類別會幫助你應對各種網路和未知的解析情況。
import io.github.littlesurvival.core.YamiboResult
suspend fun fetchYamiboHome() {
when (val result = client.fetchHomePage()) {
is YamiboResult.Success -> {
val homePage = result.value
println("歡迎回來!目前論壇共有 ${homePage.forums.size} 個看板。")
}
is YamiboResult.NotLoggedIn -> {
println("錯誤:Cookie 已過期或是你尚未登入。")
}
is YamiboResult.Maintenance -> {
println("當前 Yamibo 正在進行系統維護中。")
}
is YamiboResult.NoPermission -> {
println("權限不足:${result.message()}")
}
is YamiboResult.WafChallenge -> {
println("連線驗證失敗,請 refresh 後重試:${result.message()}")
}
is YamiboResult.Failure -> {
println("網路連線錯誤或是資料解析異常:${result.message()}")
}
}
}讀取特定看板 (Forum):
import io.github.littlesurvival.dto.value.ForumId
suspend fun loadForum() {
// 取得版塊編號為 5 的第一頁主題與貼文列表
val forumResult = client.fetchForumById(ForumId(5), page = 1)
}讀取討論串內容 (Thread):
import io.github.littlesurvival.dto.value.ThreadId
suspend fun loadThread() {
// 取得特定討論串的內容與它的各樓層回覆列表
val threadResult = client.fetchThreadById(ThreadId(12345), page = 1)
}取得當前使用者資訊 (Profile):
suspend fun loadProfile() {
// 發出請求,解析並獲得當前登入使用者的個人資訊、頭像與相關統計數據
val profileResult = client.fetchProfileInfo()
}全站或特定看板內搜尋 (Search):
import io.github.littlesurvival.dto.value.FormHash
suspend fun doSearch() {
val searchResult = client.fetchSearch(
query = "百合",
forumId = null, // 選填項目:可指定要搜尋的看板 ID
formHash = FormHash("請填入你的_form_hash") // 必要的安全驗證碼
)
}各類使用者網頁操作 (發出回覆、給予評分、收藏文章):
import io.github.littlesurvival.dto.value.PostId
suspend fun interact() {
// 將文章加入至我的使用者收藏內
client.fetchAddFavorite(ThreadId(12345), FormHash("你的_form_hash"))
// 對指定文章主題內進行樓層的發文回覆
client.fetchReplyPost(ThreadId(12345), PostId(9876), "感謝大大的熱情分享!", FormHash("你的_form_hash"))
// 對其他使用者的指定樓層文章進行論壇評分
client.fetchRatePost(ThreadId(12345), PostId(9876), score = 1, reason = "感謝分享", FormHash("你的_form_hash"))
}API 呼叫與 Host 必須共用同一個 YamiboClient,並把 Host 排在 App 內容後方。Host 只在處理已觀察到的百度 NOX challenge 時建立系統 WebView,全程不顯示 WAF 專用 UI。
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.github.littlesurvival.YamiboClient
import io.github.littlesurvival.waf.YamiboWafChallengeHost
@Composable
fun YamiboApp(isForeground: Boolean) {
val client = remember { YamiboClient(timeoutMillis = 60_000L) }
DisposableEffect(client) { onDispose { client.close() } }
Box(Modifier.fillMaxSize()) {
// 正常尺寸的 recovery WebView 位於可見 App 內容後方。
YamiboWafChallengeHost(client, isForeground, Modifier.fillMaxSize())
// Repository 與畫面必須使用這個完全相同的 client。
AppContent(client)
}
}恢復成功時,使用者只會感覺本次載入比較久:Host 不等待目的頁面載入完成,會持續讀取平台 Cookie store,驗證 nox_jst_v1 後最多重送一次符合政策的請求。Client 內只維護一份合成 Cookie header,恢復時只替換其中的 nox_jst_v1,不會匯入 WAF Host 產生的 guest cookies。一般 setCookie() 會保留 Client 現有 NOX;若 App 自己的可信登入/簽到 WebView 剛產生完整 cookie snapshot,則呼叫 client.setCookie(cookie, importNox = true),讓其中有效的新 NOX 明確覆蓋。若 silent recovery 失敗,API 回傳 YamiboResult.WafChallenge;前端維持原本的失敗畫面,讓使用者 refresh 後重新 fetch 即可。背景或無 UI client 不會建立 WebView。登出時呼叫 client.clearCookies(),只清除登入狀態並保留 NOX;只有完整重設網路 Cookie 時才使用 client.clearCookies(clearNox = true)。WafRecoveryConfig(enabled = false) 可停用自動恢復。