feat(tips): serve /tips/social/received + /claimable as empty (public-only)

Public-only instances have no targeted-recipient inbox (public tips are claimed
via share URL, never delivered to a user), so the client's inbox poll of
/received + /claimable 404'd. Serve them 200-empty instead so the poll succeeds
quietly. Matches the client contract ({tips:[]}).
This commit is contained in:
jwinterm
2026-07-10 22:35:44 -04:00
parent 1d6d67faa5
commit 533f945290
3 changed files with 116 additions and 0 deletions
+64
View File
@@ -751,6 +751,70 @@
] ]
} }
}, },
"/tips/social/claimable": {
"get": {
"tags": [
"tips"
],
"summary": "Tips CLAIMABLE by the caller. As with `received`, a public-only instance has\nno targeted-claimable inbox, so this is always empty (served 200, not 404).",
"operationId": "get_claimable_social_tips",
"responses": {
"200": {
"description": "Claimable tips (empty on a public-only instance)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SocialTipsResponse"
}
}
}
},
"400": {
"description": "Tips off"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/received": {
"get": {
"tags": [
"tips"
],
"summary": "Tips RECEIVED by the caller. Public-only instances have no targeted-recipient\ninbox (public tips are claimed via share URL, never delivered to a user), so\nthis is always empty — served (200) rather than 404 so the client's inbox\npoll doesn't error on a targeted-only endpoint this instance doesn't serve.",
"operationId": "get_received_social_tips",
"responses": {
"200": {
"description": "Received tips (empty on a public-only instance)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SocialTipsResponse"
}
}
}
},
"400": {
"description": "Tips off"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/sent": { "/tips/social/sent": {
"get": { "get": {
"tags": [ "tags": [
+2
View File
@@ -70,6 +70,8 @@ use utoipa::OpenApi;
crate::api::premium::status, crate::api::premium::status,
crate::api::tips::create_social_tip, crate::api::tips::create_social_tip,
crate::api::tips::get_sent_social_tips, crate::api::tips::get_sent_social_tips,
crate::api::tips::get_received_social_tips,
crate::api::tips::get_claimable_social_tips,
crate::api::tips::get_public_social_tip, crate::api::tips::get_public_social_tip,
crate::api::tips::cancel_social_tip, crate::api::tips::cancel_social_tip,
crate::api::tips::attach_funding, crate::api::tips::attach_funding,
+50
View File
@@ -419,6 +419,54 @@ pub async fn get_sent_social_tips(
})) }))
} }
/// Tips RECEIVED by the caller. Public-only instances have no targeted-recipient
/// inbox (public tips are claimed via share URL, never delivered to a user), so
/// this is always empty — served (200) rather than 404 so the client's inbox
/// poll doesn't error on a targeted-only endpoint this instance doesn't serve.
#[utoipa::path(
security(("bearer_auth" = [])),
get,
path = "/tips/social/received",
responses(
(status = 200, description = "Received tips (empty on a public-only instance)", body = SocialTipsResponse),
(status = 400, description = "Tips off"),
(status = 401, description = "Missing or invalid token")
),
tag = "tips"
)]
#[instrument(skip(state, headers))]
pub async fn get_received_social_tips(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<SocialTipsResponse>, AppError> {
extract_user_id_from_token(&state, &headers).await?;
ensure_tips_enabled(&state)?;
Ok(Json(SocialTipsResponse { tips: Vec::new() }))
}
/// Tips CLAIMABLE by the caller. As with `received`, a public-only instance has
/// no targeted-claimable inbox, so this is always empty (served 200, not 404).
#[utoipa::path(
security(("bearer_auth" = [])),
get,
path = "/tips/social/claimable",
responses(
(status = 200, description = "Claimable tips (empty on a public-only instance)", body = SocialTipsResponse),
(status = 400, description = "Tips off"),
(status = 401, description = "Missing or invalid token")
),
tag = "tips"
)]
#[instrument(skip(state, headers))]
pub async fn get_claimable_social_tips(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<SocialTipsResponse>, AppError> {
extract_user_id_from_token(&state, &headers).await?;
ensure_tips_enabled(&state)?;
Ok(Json(SocialTipsResponse { tips: Vec::new() }))
}
/// Public tip metadata for a share-URL holder. UNAUTHENTICATED: the tip id (a /// Public tip metadata for a share-URL holder. UNAUTHENTICATED: the tip id (a
/// UUID) is the bearer token. 404s for an unknown or non-public tip. /// UUID) is the bearer token. 404s for an unknown or non-public tip.
#[utoipa::path( #[utoipa::path(
@@ -664,6 +712,8 @@ pub fn routes() -> Router<Arc<AppState>> {
Router::new() Router::new()
.route("/tips/social", post(create_social_tip)) .route("/tips/social", post(create_social_tip))
.route("/tips/social/sent", get(get_sent_social_tips)) .route("/tips/social/sent", get(get_sent_social_tips))
.route("/tips/social/received", get(get_received_social_tips))
.route("/tips/social/claimable", get(get_claimable_social_tips))
.route("/tips/social/:tip_id/public", get(get_public_social_tip)) .route("/tips/social/:tip_id/public", get(get_public_social_tip))
.route("/tips/social/:tip_id/cancel", post(cancel_social_tip)) .route("/tips/social/:tip_id/cancel", post(cancel_social_tip))
.route( .route(