pub mod atproto;
pub mod config;
pub mod feed;
pub mod lexicon;
pub mod metrics;
pub mod net;
pub mod network;
pub mod oauth;
pub mod readstate;
pub mod repo;
pub mod runtime_health;
pub mod safe_link;
pub mod sanitized_html;
pub mod standard_site;
pub mod store;
pub mod vetted;
pub mod web;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use atproto::SidecarClient;
use config::Config;
use store::Pool;
#[derive(Clone, Debug)]
pub struct Session {
pub did: String,
pub handle: Option<String>,
}
#[derive(Clone, Default)]
pub struct SessionRegistry {
inner: Arc<RwLock<HashMap<String, Session>>>,
}
impl SessionRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn create(&self, session: Session) -> String {
let sid = new_session_id();
if let Ok(mut map) = self.inner.write() {
map.insert(sid.clone(), session);
}
sid
}
pub fn get(&self, sid: &str) -> Option<Session> {
self.inner.read().ok().and_then(|m| m.get(sid).cloned())
}
pub fn remove(&self, sid: &str) {
if let Ok(mut map) = self.inner.write() {
map.remove(sid);
}
}
}
fn new_session_id() -> String {
let mut bytes = [0u8; 32];
if getrandom::fill(&mut bytes).is_err() {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let seed = nanos as u64 ^ (&bytes as *const _ as u64);
let mut x = seed | 1;
for b in bytes.iter_mut() {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*b = (x & 0xff) as u8;
}
}
let mut s = String::with_capacity(64);
for b in bytes {
use std::fmt::Write;
let _ = write!(s, "{b:02x}");
}
s
}
#[derive(Clone)]
pub struct AppState {
pub config: Arc<Config>,
pub db: Pool,
pub http: reqwest::Client,
pub sidecar: SidecarClient,
pub sessions: SessionRegistry,
pub metrics: Arc<metrics::RepoMetrics>,
pub oauth: Option<Arc<oauth::runtime::OauthRuntime>>,
pub runtime_health: Arc<runtime_health::RuntimeHealth>,
pub sanitize_starvation: &'static feed::Starvation,
}
impl AppState {
pub fn new(config: Config, db: Pool) -> anyhow::Result<Self> {
let http = build_http_client()?;
let oauth = match oauth::runtime::OauthRuntime::new(&config) {
Ok(runtime) => Some(Arc::new(runtime)),
Err(err) if config.repo_backend == metrics::Backend::Sidecar => {
tracing::warn!(
%err,
"the Rust OAuth runtime could not be built; the sidecar backend is \
unaffected, but FEATHERREADER_REPO_BACKEND=rust would refuse to start"
);
None
}
Err(err) => return Err(err.context(
"FEATHERREADER_REPO_BACKEND=rust, but the Rust OAuth runtime could not be built",
)),
};
let sidecar = SidecarClient::new(
http.clone(),
config.sidecar.public_url.clone(),
config.sidecar.internal_url.clone(),
config.sidecar.internal_secret.clone(),
);
Ok(Self {
config: Arc::new(config),
db,
http,
sidecar,
sessions: SessionRegistry::new(),
metrics: Arc::new(metrics::RepoMetrics::new()),
oauth,
runtime_health: Arc::new(runtime_health::RuntimeHealth::new()),
sanitize_starvation: &feed::SANITIZE_STARVATION,
})
}
}
pub fn build_http_client() -> reqwest::Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent(USER_AGENT)
.no_proxy()
.build()
}
pub fn block_on_then_shutdown<F: std::future::Future>(
future: F,
shutdown: std::time::Duration,
) -> std::io::Result<F::Output> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let output = runtime.block_on(future);
runtime.shutdown_timeout(shutdown);
Ok(output)
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const USER_AGENT: &str = concat!(
"featherreader/",
env!("CARGO_PKG_VERSION"),
" (+https://feather-reader.com)"
);
#[cfg(test)]
mod runtime_shutdown_tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn shutdown_waits_for_blocking_work_at_most_the_bound() {
let started = Instant::now();
let out = block_on_then_shutdown(
async {
drop(tokio::task::spawn_blocking(|| {
std::thread::sleep(Duration::from_secs(10))
}));
7
},
Duration::from_millis(100),
)
.unwrap();
let took = started.elapsed();
assert_eq!(out, 7);
assert!(took < Duration::from_secs(2), "shutdown took {took:?}");
}
}