#![doc = include_str!("../README.md")]
#[cfg(all(feature = "cache", not(feature = "messenger")))]
compile_error!("feature `cache` requires feature `messenger`");
#[cfg(all(feature = "messenger", feature = "cache"))]
mod cache;
mod error;
#[cfg(feature = "messenger")]
mod events;
#[path = "client/extractor.rs"]
mod extractor_component;
#[cfg(feature = "messenger")]
#[path = "client/messenger.rs"]
mod messenger_component;
#[cfg(all(feature = "messenger", feature = "cache"))]
pub use cache::{
Cache,
CacheUpdate,
};
pub use error::*;
#[cfg(feature = "messenger")]
pub use events::{
Context,
EventHandler,
Message,
Presence,
Typing,
User,
};
use std::sync::Arc;
use rustybook_http::client::Client;
use serde::Deserialize;
#[cfg(feature = "messenger")]
use tokio::sync::Mutex;
#[cfg(feature = "messenger")]
use tokio::task::JoinHandle;
#[derive(Clone)]
pub struct Rustybook {
inner: Arc<Inner>,
}
#[cfg(feature = "messenger")]
#[derive(Debug, Clone)]
pub struct SendReceipt {
pub thread_id: String,
pub message_id: Option<String>,
pub offline_threading_id: String,
}
impl Rustybook {
pub fn new() -> Result<Rustybook, Error> {
RustybookBuilder::new().build_with_client()
}
pub fn builder() -> RustybookBuilder {
RustybookBuilder::new()
}
#[cfg(all(feature = "messenger", feature = "cache"))]
pub async fn user(&self) -> Option<User> {
self.inner.cache.user()
}
#[cfg(all(feature = "messenger", feature = "cache"))]
pub async fn messages(&self) -> Vec<Message> {
self.inner.cache.messages()
}
#[cfg(all(feature = "messenger", feature = "cache"))]
async fn set_user(&self, user: User) {
self.inner.cache.set_user(user);
}
}
#[derive(Clone)]
struct SessionConfig {
#[cfg(feature = "messenger")]
user_id: String,
cookie_header: String,
}
struct Inner {
http: Client,
#[cfg(feature = "messenger")]
session: Option<SessionConfig>,
#[cfg(feature = "messenger")]
user_agent: Option<String>,
#[cfg(feature = "messenger")]
proxy: Option<String>,
#[cfg(all(feature = "messenger", feature = "cache"))]
cache: Cache,
#[cfg(feature = "messenger")]
messenger: Mutex<Option<rustybook_messenger::MessengerClient>>,
#[cfg(feature = "messenger")]
handler_task: Mutex<Option<JoinHandle<()>>>,
#[cfg(feature = "messenger")]
messenger_online: bool,
}
pub struct RustybookBuilder {
redirect_limit: u32,
cookies_file_path: Option<String>,
user_agent: Option<String>,
proxy: Option<String>,
#[cfg(feature = "messenger")]
messenger_online: bool,
}
impl Default for RustybookBuilder {
fn default() -> Self {
Self::new()
}
}
impl RustybookBuilder {
pub fn new() -> Self {
Self {
redirect_limit: 10,
cookies_file_path: None,
user_agent: None,
proxy: None,
#[cfg(feature = "messenger")]
messenger_online: true,
}
}
pub fn cookies_file_path(mut self, path: impl Into<String>) -> Self {
self.cookies_file_path = Some(path.into());
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
self.proxy = Some(proxy.into());
self
}
#[cfg(feature = "messenger")]
pub fn online(mut self, online: bool) -> Self {
self.messenger_online = online;
self
}
pub fn build(self) -> Result<Rustybook, Error> {
self.build_with_client()
}
fn build_with_client(self) -> Result<Rustybook, Error> {
let session = match self.cookies_file_path {
Some(path) => Some(load_session_from_cookie_file(&path)?),
None => None,
};
let mut builder = rustybook_http::ClientBuilder::new().max_redirect(self.redirect_limit);
if let Some(user_agent) = self.user_agent.as_deref() {
builder = builder.user_agent(user_agent.to_string());
}
if let Some(proxy) = self.proxy.as_deref() {
builder = builder.proxy(proxy.to_string());
}
if let Some(session) = session.as_ref() {
builder = builder.cookie_header(session.cookie_header.clone());
}
let http = builder.build()?;
#[cfg(all(feature = "messenger", feature = "cache"))]
let cache = initial_cache_from_session(&session);
Ok(Rustybook {
inner: Arc::new(Inner {
http,
#[cfg(feature = "messenger")]
session,
#[cfg(feature = "messenger")]
user_agent: self.user_agent,
#[cfg(feature = "messenger")]
proxy: self.proxy,
#[cfg(all(feature = "messenger", feature = "cache"))]
cache,
#[cfg(feature = "messenger")]
messenger: Mutex::new(None),
#[cfg(feature = "messenger")]
handler_task: Mutex::new(None),
#[cfg(feature = "messenger")]
messenger_online: self.messenger_online,
}),
})
}
}
#[derive(Debug, Deserialize)]
struct Cookie {
name: String,
value: String,
}
fn load_session_from_cookie_file(path: &str) -> Result<SessionConfig, Error> {
let content = std::fs::read_to_string(path)
.map_err(|error| Error::Config(format!("failed to read cookie file: {error}")))?;
let cookies: Vec<Cookie> = serde_json::from_str(&content)
.map_err(|error| Error::Config(format!("failed to parse cookie file: {error}")))?;
let mut parts = Vec::with_capacity(cookies.len());
let mut user_id = None;
let mut has_xs = false;
for cookie in &cookies {
if cookie.name.is_empty() {
continue;
}
parts.push(format!("{}={}", cookie.name, cookie.value));
if cookie.name == "c_user" {
user_id = Some(cookie.value.clone());
}
if cookie.name == "xs" {
has_xs = true;
}
}
let Some(user_id) = user_id else {
return Err(Error::Config(
"missing c_user cookie in cookie file".to_string(),
));
};
#[cfg(not(feature = "messenger"))]
let _ = &user_id;
if !has_xs {
return Err(Error::Config(
"missing xs cookie in cookie file".to_string(),
));
}
Ok(SessionConfig {
#[cfg(feature = "messenger")]
user_id,
cookie_header: parts.join("; "),
})
}
#[cfg(all(feature = "messenger", feature = "cache"))]
fn initial_cache_from_session(session: &Option<SessionConfig>) -> Cache {
let user = session.as_ref().map(|session| User {
id: session.user_id.clone(),
name: None,
});
Cache::with_user(user)
}