mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
refactor(errors): derive FetchError and PixivError with thiserror
This commit is contained in:
@@ -52,7 +52,7 @@ Docker: `docker build -t tgxmb .` then `docker run --rm -d --name tgxmb --env-fi
|
|||||||
|
|
||||||
## Code Conventions & Common Patterns
|
## Code Conventions & Common Patterns
|
||||||
|
|
||||||
- **No anyhow/thiserror.** Errors are hand-rolled enums with manual `Display`/`source()`/`From` impls: `QueueError` (`Retryable { delay_seconds, payload }` / `Permanent`), `SendError` (Retryable/Permanent), `FetchError` (`Http`/`Json`/`Pixiv`/`Site`/`NotFound`/`Blocked`), `PixivError`, `Classification`. New errors should follow this pattern.
|
- **Errors via `thiserror` derive** (no anyhow): the public, stringified errors — `FetchError` (`Http`/`Json`/`Pixiv`/`Site`/`NotFound`/`Blocked`) and `PixivError` — derive `thiserror::Error` with `#[from]` conversions; `Display`/`source()` come from the derive. The internal control-flow enums — `QueueError` (`Retryable { delay_seconds, payload }` / `Permanent`), `SendError` (Retryable/Permanent), `Classification`, `FallbackError` — carry no `Display` and are handled by direct variant matching. New errors should follow the same split: stringified/public errors derive `thiserror`, internal flow enums stay plain.
|
||||||
- **Global state via `std::sync::LazyLock` statics**, not DI: `CONFIG`, `CHAT_STORE`, `TASK_QUEUE` in `handlers.rs`; shared reqwest `CLIENT` in `x-media/src/site/mod.rs`. `Bot` is passed/cloned into handlers; queue workers share the process-wide `send::BOT` (`LazyLock<Bot>`, force-initialized in `main` so a missing token fails at startup).
|
- **Global state via `std::sync::LazyLock` statics**, not DI: `CONFIG`, `CHAT_STORE`, `TASK_QUEUE` in `handlers.rs`; shared reqwest `CLIENT` in `x-media/src/site/mod.rs`. `Bot` is passed/cloned into handlers; queue workers share the process-wide `send::BOT` (`LazyLock<Bot>`, force-initialized in `main` so a missing token fails at startup).
|
||||||
- **Async**: tokio multi-thread runtime (`#[tokio::main]` default). All rusqlite I/O inside `tokio::task::spawn_blocking`. Long loops use `tokio::select!` with `tokio::sync::{watch, Notify}` stop/wake channels. No streams.
|
- **Async**: tokio multi-thread runtime (`#[tokio::main]` default). All rusqlite I/O inside `tokio::task::spawn_blocking`. Long loops use `tokio::select!` with `tokio::sync::{watch, Notify}` stop/wake channels. No streams.
|
||||||
- **Blocking sync primitives**: `parking_lot::Mutex` for hot caches, `tokio::sync::Mutex` for async-shared state (pixiv token cache), `AtomicBool` for feature gates.
|
- **Blocking sync primitives**: `parking_lot::Mutex` for hot caches, `tokio::sync::Mutex` for async-shared state (pixiv token cache), `AtomicBool` for feature gates.
|
||||||
|
|||||||
Generated
+1
@@ -2937,6 +2937,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"zip",
|
"zip",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ url = "2.5.2"
|
|||||||
bytes = "1"
|
bytes = "1"
|
||||||
zip = "2"
|
zip = "2"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
thiserror = "2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tokio = { version = "1.40", features = ["time"] }
|
tokio = { version = "1.40", features = ["time"] }
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
//! `PATTERN`, `enabled()` and `fetch_from_url()`; a future site plugs in by
|
//! `PATTERN`, `enabled()` and `fetch_from_url()`; a future site plugs in by
|
||||||
//! adding one guarded entry in [`fetch_once`].
|
//! adding one guarded entry in [`fetch_once`].
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
@@ -12,6 +11,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod bsky;
|
pub mod bsky;
|
||||||
pub mod pixiv;
|
pub mod pixiv;
|
||||||
@@ -183,84 +183,45 @@ pub fn site_id_from_key(key: &str) -> &'static str {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FetchError {
|
pub enum FetchError {
|
||||||
Http(reqwest::Error),
|
#[error("http error: {0}")]
|
||||||
Json(serde_json::Error),
|
Http(#[from] reqwest::Error),
|
||||||
Pixiv(PixivError),
|
#[error("json error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error("pixiv error: {0}")]
|
||||||
|
Pixiv(#[from] PixivError),
|
||||||
/// A site-specific error from a site that keeps its own error type.
|
/// A site-specific error from a site that keeps its own error type.
|
||||||
/// Permanent by default (sites that need retryable site errors convert
|
/// Permanent by default (sites that need retryable site errors convert
|
||||||
/// them to [`FetchError::Http`] / [`FetchError::Transient`] before
|
/// them to [`FetchError::Http`] / [`FetchError::Transient`] before
|
||||||
/// returning). Pixiv predates this and keeps the dedicated
|
/// returning). Pixiv predates this and keeps the dedicated
|
||||||
/// [`FetchError::Pixiv`] variant.
|
/// [`FetchError::Pixiv`] variant.
|
||||||
|
#[error("{site} error: {error}")]
|
||||||
Site {
|
Site {
|
||||||
site: &'static str,
|
site: &'static str,
|
||||||
|
#[source]
|
||||||
error: Box<dyn std::error::Error + Send + Sync>,
|
error: Box<dyn std::error::Error + Send + Sync>,
|
||||||
},
|
},
|
||||||
|
#[error("not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
#[error("blocked")]
|
||||||
Blocked,
|
Blocked,
|
||||||
/// The post exists but its content is withheld (twitter NSFW /
|
/// The post exists but its content is withheld (twitter NSFW /
|
||||||
/// age-restricted tweets come back as an empty `{}` from syndication).
|
/// age-restricted tweets come back as an empty `{}` from syndication).
|
||||||
|
#[error("content withheld (sensitive)")]
|
||||||
Sensitive,
|
Sensitive,
|
||||||
/// A download exceeded the caller's size cap (see [`download_media_limited`]).
|
/// A download exceeded the caller's size cap (see [`download_media_limited`]).
|
||||||
|
#[error("media too large")]
|
||||||
TooLarge,
|
TooLarge,
|
||||||
/// A transient server-side failure (429 / 5xx); [`fetch`] retries these.
|
/// A transient server-side failure (429 / 5xx); [`fetch`] retries these.
|
||||||
|
#[error("transient: {0}")]
|
||||||
Transient(String),
|
Transient(String),
|
||||||
/// A local I/O failure while streaming a download to disk
|
/// A local I/O failure while streaming a download to disk
|
||||||
/// (see [`download_media_to_file`]).
|
/// (see [`download_media_to_file`]).
|
||||||
|
#[error("io error: {0}")]
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for FetchError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
FetchError::Http(e) => write!(f, "http error: {e}"),
|
|
||||||
FetchError::Json(e) => write!(f, "json error: {e}"),
|
|
||||||
FetchError::Pixiv(e) => write!(f, "pixiv error: {e}"),
|
|
||||||
FetchError::Site { site, error } => write!(f, "{site} error: {error}"),
|
|
||||||
FetchError::NotFound => write!(f, "not found"),
|
|
||||||
FetchError::Blocked => write!(f, "blocked"),
|
|
||||||
FetchError::Sensitive => write!(f, "content withheld (sensitive)"),
|
|
||||||
FetchError::TooLarge => write!(f, "media too large"),
|
|
||||||
FetchError::Transient(message) => write!(f, "transient: {message}"),
|
|
||||||
FetchError::Io(e) => write!(f, "io error: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for FetchError {
|
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
||||||
match self {
|
|
||||||
FetchError::Http(e) => Some(e),
|
|
||||||
FetchError::Json(e) => Some(e),
|
|
||||||
FetchError::Pixiv(e) => Some(e),
|
|
||||||
FetchError::Site { error, .. } => Some(error.as_ref()),
|
|
||||||
FetchError::NotFound | FetchError::Blocked | FetchError::Sensitive => None,
|
|
||||||
FetchError::TooLarge => None,
|
|
||||||
FetchError::Transient(_) => None,
|
|
||||||
FetchError::Io(e) => Some(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for FetchError {
|
|
||||||
fn from(e: reqwest::Error) -> Self {
|
|
||||||
FetchError::Http(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for FetchError {
|
|
||||||
fn from(e: serde_json::Error) -> Self {
|
|
||||||
FetchError::Json(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PixivError> for FetchError {
|
|
||||||
fn from(e: PixivError) -> Self {
|
|
||||||
FetchError::Pixiv(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared HTTP client (browser User-Agent) for twitter/bsky fetches and
|
/// Shared HTTP client (browser User-Agent) for twitter/bsky fetches and
|
||||||
/// [`download_media`].
|
/// [`download_media`].
|
||||||
pub(crate) static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
pub(crate) static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use super::model::{IllustrationModel, TypeModel, UgoiraMetadataModel};
|
|||||||
use crate::media::Media;
|
use crate::media::Media;
|
||||||
use crate::site::FetchError;
|
use crate::site::FetchError;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fmt;
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
const AUTH_TOKEN_URL: &str = "https://oauth.secure.pixiv.net/auth/token";
|
const AUTH_TOKEN_URL: &str = "https://oauth.secure.pixiv.net/auth/token";
|
||||||
const APP_API_URL: &str = "https://app-api.pixiv.net";
|
const APP_API_URL: &str = "https://app-api.pixiv.net";
|
||||||
@@ -23,53 +23,24 @@ const APP_USER_AGENT: &str = "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)";
|
|||||||
/// Token refresh safe margin (seconds).
|
/// Token refresh safe margin (seconds).
|
||||||
const TOKEN_REFRESH_SAFE_MARGIN: u64 = 300;
|
const TOKEN_REFRESH_SAFE_MARGIN: u64 = 300;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PixivError {
|
pub enum PixivError {
|
||||||
/// No refresh token available (PIXIV_REFRESH_TOKEN unset).
|
/// No refresh token available (PIXIV_REFRESH_TOKEN unset).
|
||||||
|
#[error("pixiv: no authentication")]
|
||||||
NoAuth,
|
NoAuth,
|
||||||
Http(reqwest::Error),
|
#[error("pixiv http error: {0}")]
|
||||||
Json(serde_json::Error),
|
Http(#[from] reqwest::Error),
|
||||||
|
#[error("pixiv json error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
/// Non-2xx HTTP status from the app API. The code lets [`crate::site::fetch`]
|
/// Non-2xx HTTP status from the app API. The code lets [`crate::site::fetch`]
|
||||||
/// retry only transient classes (429 / 5xx) instead of burning attempts on
|
/// retry only transient classes (429 / 5xx) instead of burning attempts on
|
||||||
/// permanent 4xx (bad token, forbidden, not found).
|
/// permanent 4xx (bad token, forbidden, not found).
|
||||||
|
#[error("pixiv status {0}")]
|
||||||
Status(u16),
|
Status(u16),
|
||||||
|
#[error("pixiv api error: {0}")]
|
||||||
Api(String),
|
Api(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PixivError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
PixivError::NoAuth => write!(f, "pixiv: no authentication"),
|
|
||||||
PixivError::Http(e) => write!(f, "pixiv http error: {e}"),
|
|
||||||
PixivError::Json(e) => write!(f, "pixiv json error: {e}"),
|
|
||||||
PixivError::Status(code) => write!(f, "pixiv status {code}"),
|
|
||||||
PixivError::Api(message) => write!(f, "pixiv api error: {message}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for PixivError {
|
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
||||||
match self {
|
|
||||||
PixivError::Http(e) => Some(e),
|
|
||||||
PixivError::Json(e) => Some(e),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for PixivError {
|
|
||||||
fn from(e: reqwest::Error) -> Self {
|
|
||||||
PixivError::Http(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for PixivError {
|
|
||||||
fn from(e: serde_json::Error) -> Self {
|
|
||||||
PixivError::Json(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Native pixiv app-API client.
|
/// Native pixiv app-API client.
|
||||||
pub struct PixivAPI {
|
pub struct PixivAPI {
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user