indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
use http::header::{CONTENT_TYPE, LINK};
use http::StatusCode;
use time::OffsetDateTime;
use url::Url;

use crate::standards::websub::{
    ContentDistribution, DeliveryResult, DeliveryStatus, Result, WebSubError,
};

#[tracing::instrument(skip(client, content))]
pub async fn distribute_to_callback(
    client: &impl crate::http::Client,
    hub: &Url,
    topic: &Url,
    callback: &Url,
    content: &ContentDistribution,
) -> Result<DeliveryResult<Url>> {
    let link_header_value = format!("<{}>; rel=\"hub\", <{}>; rel=\"self\"", hub, topic);
    let mut builder = http::Request::builder()
        .method(http::Method::POST)
        .uri(callback.as_str())
        .header(CONTENT_TYPE, content.content_type.as_str())
        .header(LINK, link_header_value);

    if let Some(signature) = &content.signature {
        builder = builder.header("X-Hub-Signature", signature.as_str());
    }

    let request = builder
        .body(crate::http::Body::Bytes(content.body.as_bytes().to_vec()))
        .map_err(|err| WebSubError::Http(err.to_string()))?;

    let response = client.send_request(request).await.map_err(|err| WebSubError::Network(Box::new(err)))?;
    let status = response.status();
    let response_code = Some(status.as_u16());

    let (delivery_status, error_message) = map_status(status);
    let delivered_at = if delivery_status == DeliveryStatus::Success {
        Some(OffsetDateTime::now_utc())
    } else {
        None
    };

    Ok(DeliveryResult {
        subscriber_id: callback.clone(),
        subscriber_callback: callback.clone(),
        status: delivery_status,
        response_code,
        error_message,
        retry_count: 0,
        delivered_at,
    })
}

pub(crate) fn map_status(status: StatusCode) -> (DeliveryStatus, Option<String>) {
    match status {
        StatusCode::OK | StatusCode::ACCEPTED | StatusCode::NO_CONTENT => {
            (DeliveryStatus::Success, None)
        }
        StatusCode::GONE => (DeliveryStatus::SubscriptionDeleted, None),
        StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => (
            DeliveryStatus::PermanentFailure,
            Some(format!("Subscriber rejected delivery: {}", status)),
        ),
        StatusCode::TOO_MANY_REQUESTS
        | StatusCode::BAD_GATEWAY
        | StatusCode::SERVICE_UNAVAILABLE
        | StatusCode::GATEWAY_TIMEOUT
        | StatusCode::REQUEST_TIMEOUT
        | StatusCode::INTERNAL_SERVER_ERROR => (
            DeliveryStatus::TemporaryFailure,
            Some(format!("Temporary failure delivering content: {}", status)),
        ),
        _ => (
            DeliveryStatus::TemporaryFailure,
            Some(format!("Unexpected response from subscriber: {}", status)),
        ),
    }
}