mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-16 06:16:06 +00:00
feat: add bsky and bsky-support cargo features to manage bsky specific code
This commit is contained in:
@@ -47,3 +47,7 @@ tracing = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
|
||||
[features]
|
||||
bsky = ["bsky-support"]
|
||||
bsky-support = []
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod preferences;
|
||||
|
||||
pub use preferences::{get_preferences, put_preferences};
|
||||
@@ -205,20 +205,24 @@ pub async fn sequence_new_account(
|
||||
{
|
||||
tracing::warn!("Failed to sequence sync event for {}: {}", did, e);
|
||||
}
|
||||
let profile_record = serde_json::json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": display_name
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
state,
|
||||
did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
// TODO: make this configurable and also deduplicate with tranquil-oauth-server/src/sso_endpoints.rs:1210
|
||||
#[cfg(feature = "bsky")]
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
let profile_record = serde_json::json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": display_name
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
state,
|
||||
did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
pub mod actor;
|
||||
// BSKY: Bluesky requires PDSs to implement its private preferences API
|
||||
#[cfg(feature = "bsky-support")]
|
||||
pub mod actor {
|
||||
mod preferences;
|
||||
|
||||
pub use preferences::{get_preferences, put_preferences};
|
||||
}
|
||||
pub mod admin;
|
||||
#[cfg(feature = "bsky")]
|
||||
pub mod age_assurance;
|
||||
pub mod common;
|
||||
pub mod delegation;
|
||||
@@ -21,7 +28,7 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
let blob_body_limit =
|
||||
DefaultBodyLimit::max(tranquil_config::get().server.max_blob_size as usize);
|
||||
|
||||
axum::Router::new()
|
||||
let router = axum::Router::new()
|
||||
.route("/_health", get(server::health))
|
||||
.route(
|
||||
"/com.atproto.server.describeServer",
|
||||
@@ -373,14 +380,6 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
post(admin::update_subject_status),
|
||||
)
|
||||
.route("/com.atproto.admin.sendEmail", post(admin::send_email))
|
||||
.route(
|
||||
"/app.bsky.actor.getPreferences",
|
||||
get(actor::get_preferences),
|
||||
)
|
||||
.route(
|
||||
"/app.bsky.actor.putPreferences",
|
||||
post(actor::put_preferences),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.temp.checkSignupQueue",
|
||||
get(temp::check_signup_queue),
|
||||
@@ -438,7 +437,21 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
.route(
|
||||
"/_delegation.resolveController",
|
||||
get(delegation::resolve_controller),
|
||||
);
|
||||
|
||||
#[cfg(feature = "bsky-support")]
|
||||
let router = router
|
||||
.route(
|
||||
"/app.bsky.actor.getPreferences",
|
||||
get(actor::get_preferences),
|
||||
)
|
||||
.route(
|
||||
"/app.bsky.actor.putPreferences",
|
||||
post(actor::put_preferences),
|
||||
);
|
||||
|
||||
#[cfg(feature = "bsky")]
|
||||
let router = router
|
||||
.route(
|
||||
"/app.bsky.ageassurance.getState",
|
||||
get(age_assurance::get_state),
|
||||
@@ -446,7 +459,9 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
.route(
|
||||
"/app.bsky.unspecced.getAgeAssuranceState",
|
||||
get(age_assurance::get_age_assurance_state),
|
||||
)
|
||||
);
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
pub fn well_known_api_routes() -> axum::Router<AppState> {
|
||||
|
||||
@@ -37,3 +37,6 @@ webauthn-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[features]
|
||||
bsky = []
|
||||
|
||||
@@ -1209,20 +1209,23 @@ pub async fn complete_registration(
|
||||
tracing::warn!("Failed to sequence account event for {}: {}", did, e);
|
||||
}
|
||||
|
||||
let profile_record = json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle.as_str()
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
#[cfg(feature = "bsky")]
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
let profile_record = json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle.as_str()
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
};
|
||||
}
|
||||
|
||||
let app_password = generate_app_password();
|
||||
|
||||
@@ -76,6 +76,8 @@ webauthn-rs = { workspace = true }
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
|
||||
[features]
|
||||
bsky = ["bsky-support"]
|
||||
bsky-support = []
|
||||
external-infra = []
|
||||
postgres = ["tranquil-db/postgres"]
|
||||
s3 = ["tranquil-storage/s3"]
|
||||
|
||||
@@ -19,9 +19,7 @@ use tower::{Service, util::BoxCloneSyncService};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
static PROTECTED_METHODS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
[
|
||||
"app.bsky.actor.getPreferences",
|
||||
"app.bsky.actor.putPreferences",
|
||||
let mut methods: HashSet<&str> = [
|
||||
"com.atproto.admin.deleteAccount",
|
||||
"com.atproto.admin.disableAccountInvites",
|
||||
"com.atproto.admin.disableInviteCodes",
|
||||
@@ -103,7 +101,13 @@ static PROTECTED_METHODS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"com.atproto.temp.dereferenceScope",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
.collect();
|
||||
// BSKY: the Bluesky preferences API must be implemented by PDSs
|
||||
if cfg!(feature = "bsky-support") {
|
||||
methods.insert("app.bsky.actor.getPreferences");
|
||||
methods.insert("app.bsky.actor.putPreferences");
|
||||
};
|
||||
methods
|
||||
});
|
||||
|
||||
fn is_protected_method(method: &str) -> bool {
|
||||
@@ -111,6 +115,7 @@ fn is_protected_method(method: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Fetch the `feed` generator record from the AppView and return its `did`.
|
||||
#[cfg(feature = "bsky-support")]
|
||||
async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> Option<Did> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetFeedQuery {
|
||||
@@ -201,6 +206,7 @@ impl<S: Service<Request, Response = Response, Error = Infallible>> Service<Reque
|
||||
}
|
||||
|
||||
// If the age assurance override is set and this is an age assurance call then we dont want to proxy even if the client requests it
|
||||
#[cfg(feature = "bsky")]
|
||||
if tranquil_config::get().server.age_assurance_override
|
||||
&& (path.ends_with("app.bsky.ageassurance.getState")
|
||||
|| path.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
|
||||
@@ -328,27 +334,28 @@ async fn proxy_handler(
|
||||
},
|
||||
};
|
||||
|
||||
// getFeed must be audienced to the feed generator, not the AppView.
|
||||
let (token_aud, token_lxm) = if method == "app.bsky.feed.getFeed" {
|
||||
match resolve_feed_generator_did(&resolved.url, query.as_deref()).await {
|
||||
Some(feed_did) => (
|
||||
feed_did,
|
||||
"app.bsky.feed.getFeedSkeleton"
|
||||
.parse::<Nsid>()
|
||||
.expect("getFeedSkeleton is a valid NSID"),
|
||||
),
|
||||
None => {
|
||||
warn!(
|
||||
"getFeed proxy: could not resolve feed generator DID; refusing \
|
||||
// BSKY: getFeed must be audienced to the feed generator, not the AppView.
|
||||
let (token_aud, token_lxm) =
|
||||
if cfg!(feature = "bsky-support") && method == "app.bsky.feed.getFeed" {
|
||||
match resolve_feed_generator_did(&resolved.url, query.as_deref()).await {
|
||||
Some(feed_did) => (
|
||||
feed_did,
|
||||
"app.bsky.feed.getFeedSkeleton"
|
||||
.parse::<Nsid>()
|
||||
.expect("getFeedSkeleton is a valid NSID"),
|
||||
),
|
||||
None => {
|
||||
warn!(
|
||||
"getFeed proxy: could not resolve feed generator DID; refusing \
|
||||
to mint an AppView-audienced token"
|
||||
);
|
||||
return ApiError::InvalidRequest("Could not resolve feed".into())
|
||||
.into_response();
|
||||
);
|
||||
return ApiError::InvalidRequest("Could not resolve feed".into())
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(resolved.did.clone(), method_nsid.clone())
|
||||
};
|
||||
} else {
|
||||
(resolved.did.clone(), method_nsid.clone())
|
||||
};
|
||||
|
||||
match crate::auth::create_service_token(
|
||||
&auth_user.did,
|
||||
|
||||
@@ -147,7 +147,9 @@ impl std::fmt::Display for SsrfError {
|
||||
}
|
||||
|
||||
impl std::error::Error for SsrfError {}
|
||||
|
||||
// TODO: update to match https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/pipethrough.ts
|
||||
// Currently spec says nothing at all!! about forwarding headers during proxying and https://github.com/bluesky-social/atproto/discussions/2350
|
||||
#[cfg(feature = "bsky-support")]
|
||||
pub static HEADERS_TO_FORWARD: LazyLock<[HeaderName; 4]> = LazyLock::new(|| {
|
||||
[
|
||||
HeaderName::from_static("accept-language"),
|
||||
@@ -156,6 +158,14 @@ pub static HEADERS_TO_FORWARD: LazyLock<[HeaderName; 4]> = LazyLock::new(|| {
|
||||
http::header::CONTENT_TYPE,
|
||||
]
|
||||
});
|
||||
#[cfg(not(feature = "bsky-support"))]
|
||||
pub static HEADERS_TO_FORWARD: LazyLock<[HeaderName; 3]> = LazyLock::new(|| {
|
||||
[
|
||||
HeaderName::from_static("accept-language"),
|
||||
crate::util::HEADER_ATPROTO_ACCEPT_LABELERS,
|
||||
http::header::CONTENT_TYPE,
|
||||
]
|
||||
});
|
||||
pub static RESPONSE_HEADERS_TO_FORWARD: LazyLock<[HeaderName; 6]> = LazyLock::new(|| {
|
||||
[
|
||||
crate::util::HEADER_ATPROTO_REPO_REV,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// TODO: make all of this configurable.
|
||||
// PDS implementation should not impose any reserved domains imo.
|
||||
// some of these are even bad to have as defaults let alone non-configurables
|
||||
const ATP_SPECIFIC: &[&str] = &[
|
||||
"at", "atp", "plc", "pds", "did", "repo", "tid", "nsid", "xrpc", "lex", "lexicon", "bsky",
|
||||
"bluesky", "handle",
|
||||
|
||||
@@ -115,6 +115,10 @@ pub fn extract_blob_cids(record: &Value) -> Vec<crate::types::CidLink> {
|
||||
use crate::types::AtUri;
|
||||
use tranquil_db_traits::{Backlink, BacklinkPath};
|
||||
|
||||
// TODO: it really really should not be necessary to extract backlinks and store those.
|
||||
// especially not in a way that isnt generic.
|
||||
// figure out what the fuck is going on here
|
||||
// (lewis do you remember why you did this???)
|
||||
pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec<Backlink> {
|
||||
let record_type = record
|
||||
.get("$type")
|
||||
|
||||
@@ -2,6 +2,8 @@ pub use tranquil_types::*;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[cfg(feature = "bsky")]
|
||||
pub static PROFILE_COLLECTION: LazyLock<Nsid> =
|
||||
LazyLock::new(|| "app.bsky.actor.profile".parse().unwrap());
|
||||
#[cfg(feature = "bsky")]
|
||||
pub static PROFILE_RKEY: LazyLock<Rkey> = LazyLock::new(|| "self".parse().unwrap());
|
||||
|
||||
@@ -87,6 +87,7 @@ pub const HEADER_ATPROTO_ACCEPT_LABELERS: HeaderName =
|
||||
pub const HEADER_ATPROTO_REPO_REV: HeaderName = HeaderName::from_static("atproto-repo-rev");
|
||||
pub const HEADER_ATPROTO_CONTENT_LABELERS: HeaderName =
|
||||
HeaderName::from_static("atproto-content-labelers");
|
||||
#[cfg(feature = "bsky-support")]
|
||||
pub const HEADER_X_BSKY_TOPICS: HeaderName = HeaderName::from_static("x-bsky-topics");
|
||||
|
||||
pub fn get_header_str(
|
||||
@@ -247,7 +248,13 @@ pub fn build_full_url(path: &str) -> String {
|
||||
let cfg = tranquil_config::get();
|
||||
let normalized_path = if !path.starts_with("/xrpc/")
|
||||
&& (path.starts_with("/com.atproto.")
|
||||
|| path.starts_with("/app.bsky.")
|
||||
// BSKY: Bluesky requires that the PDS implement some app.bsky.* endpoints so we need to deal with those here too.
|
||||
// TODO: surely we can figure out a way to do this more generically?
|
||||
|| (if cfg!(feature = "bsky-support") {
|
||||
path.starts_with("/app.bsky.")
|
||||
} else {
|
||||
true
|
||||
})
|
||||
|| path.starts_with("/_"))
|
||||
{
|
||||
format!("/xrpc{path}")
|
||||
|
||||
@@ -138,20 +138,25 @@ fn check_banned_content(
|
||||
rkey: Option<&Rkey>,
|
||||
) -> Result<(), ValidationError> {
|
||||
match record_type {
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.feed.post" => {
|
||||
check_post_banned_content(obj)?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.actor.profile" => {
|
||||
check_string_field(obj, "displayName")?;
|
||||
check_string_field(obj, "description")?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.graph.list" => {
|
||||
check_string_field(obj, "name")?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.graph.starterpack" => {
|
||||
check_string_field(obj, "name")?;
|
||||
check_string_field(obj, "description")?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.feed.generator" => {
|
||||
if let Some(rkey) = rkey
|
||||
&& crate::moderation::has_explicit_slur(rkey)
|
||||
@@ -167,6 +172,7 @@ fn check_banned_content(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "bsky")]
|
||||
fn check_post_banned_content(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if let Some(tags) = obj.get("tags").and_then(|v| v.as_array()) {
|
||||
tags.iter().enumerate().try_for_each(|(i, tag)| {
|
||||
|
||||
@@ -41,7 +41,9 @@ tracing-subscriber = { workspace = true }
|
||||
rcgen = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["frontend", "postgres", "s3", "valkey"]
|
||||
default = ["bsky", "frontend", "postgres", "s3", "valkey"]
|
||||
bsky = ["bsky-support", "tranquil-pds/bsky", "tranquil-api/bsky"]
|
||||
bsky-support = ["tranquil-pds/bsky-support", "tranquil-api/bsky-support"]
|
||||
frontend = ["tranquil-pds/frontend"]
|
||||
postgres = ["tranquil-pds/postgres"]
|
||||
s3 = ["tranquil-pds/s3"]
|
||||
|
||||
Reference in New Issue
Block a user