use std::collections::BTreeSet;
use serde_json::Value;
use trust_tasks_rs::specs::vtc::admin::events::subscribe::v0_1 as subscribe;
use trust_tasks_rs::specs::vtc::admin::events::subscribe::v0_2 as subscribe_v0_2;
use trust_tasks_rs::{ErrorPayload, Payload, RejectReason, TrustTask};
use super::helpers::{
app_error_to_reject, error_response, extended_code, reject_with, reject_with_code,
success_response,
};
use super::{JoinAuthCtx, TrustTaskOutcome, parse_spec_payload};
use crate::admin_events::{self, Refusal, Topic, Version};
use crate::server::AppState;
pub(crate) const SUBSCRIBE_TYPE: &str = <subscribe::Payload as Payload>::TYPE_URI;
pub(crate) const SUBSCRIBE_V0_2_TYPE: &str = <subscribe_v0_2::Payload as Payload>::TYPE_URI;
pub(crate) const UNRECORDED_RESPONSES: &[&str] = &[SUBSCRIBE_TYPE, SUBSCRIBE_V0_2_TYPE];
const MAX_SUBSCRIBE_AGE: chrono::TimeDelta = chrono::TimeDelta::minutes(4);
fn refuse(doc: &TrustTask<Value>, code: &str, message: &str) -> TrustTaskOutcome {
reject_with_code(doc, extended_code(code), message, None)
}
pub(super) async fn handle_subscribe(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use subscribe::error_codes as codes;
let (version, requested, since): (Version, BTreeSet<Topic>, Option<String>) =
if doc.type_uri.to_string() == SUBSCRIBE_V0_2_TYPE {
let payload: subscribe_v0_2::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
(
Version::V0_2,
payload
.topics
.iter()
.filter_map(Topic::from_subscribe_v0_2)
.collect(),
payload.since.as_ref().map(|t| t.as_str().to_string()),
)
} else {
let payload: subscribe::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
(
Version::V0_1,
payload
.topics
.iter()
.filter_map(Topic::from_subscribe)
.collect(),
payload.since.as_ref().map(|t| t.as_str().to_string()),
)
};
let mut window = super::freshness_policy();
window.max_age = Some(MAX_SUBSCRIBE_AGE);
if let Err(reason) = doc.validate_freshness(chrono::Utc::now(), &window) {
return reject_with(&doc, reason);
}
let slot = match admin_events::current_slot() {
Some(slot) if slot.accepts_stream() => slot,
Some(_) => {
return refuse(
&doc,
codes::STREAM_UNAVAILABLE.code,
"This request did not ask for an event stream (Accept: text/event-stream).",
);
}
None => {
return refuse(
&doc,
codes::STREAM_UNAVAILABLE.code,
"This delivery cannot carry an event stream; only HTTPS binding 0.3 defines one.",
);
}
};
let Some(signer) = ctx.verified_signer.clone() else {
return reject_with(&doc, RejectReason::ProofRequired);
};
let standing = match admin_events::standing(state, &signer).await {
Ok(Some(s)) => s,
Ok(None) => {
return refuse(
&doc,
codes::NOT_ADMINISTRATOR.code,
"only an administrator of this community has a live console channel",
);
}
Err(e) => return app_error_to_reject(&doc, &e),
};
let since = since.as_deref();
if let (Some(header), Some(since)) = (slot.last_event_id(), since)
&& header != since
{
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: "Last-Event-ID disagrees with the payload's `since`; resumption is \
in-band only (HTTPS binding 0.3 §2.1.3)"
.to_string(),
},
);
}
let parent_thread = doc.thread_id.clone().unwrap_or_else(|| doc.id.clone());
let opened = match admin_events::open(
state,
&signer,
&standing,
version,
&requested,
since,
parent_thread,
doc.expires_at,
)
.await
{
Ok(opened) => opened,
Err(Refusal::NoTopics) => {
return reject_with(
&doc,
RejectReason::PermissionDenied {
reason: "none of the requested topics is one this caller may read".to_string(),
},
);
}
Err(Refusal::TooMany) => {
let err = ErrorPayload::new(extended_code(codes::TOO_MANY_STREAMS.code))
.with_message(format!(
"no room for another stream: this community allows {} per administrator and \
{} in all; close one (another tab or device), or poll",
admin_events::MAX_STREAMS_PER_SUBJECT,
admin_events::MAX_STREAMS_TOTAL,
))
.with_retryable(codes::TOO_MANY_STREAMS.retryable);
return error_response(
doc.reject_with(format!("urn:uuid:{}", uuid::Uuid::new_v4()), err),
);
}
Err(Refusal::Internal(e)) => return app_error_to_reject(&doc, &e),
};
let outcome = success_response(&doc, opened.response);
if outcome.status.is_success() {
slot.grant(opened.grant);
}
outcome
}