You've already forked AstralRinth
forked from didirus/AstralRinth
9706f1597b
* wip: gotenberg * Generate and provide supporting docs for Mural payouts * Correct docs * shear * update cargo lock because r-a complains otherwise * Remove local Gotenberg queue and use Redis instead * Store platform_id in database correctly * Address PR comments * Fix up CI * fix rebase * Add timeout to default env vars
230 lines
6.5 KiB
Rust
230 lines
6.5 KiB
Rust
use std::fmt;
|
|
|
|
use actix_web::{HttpMessage, error::ParseError, http::header, post, web};
|
|
use ariadne::ids::base62_impl::parse_base62;
|
|
use base64::Engine;
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
use tracing::trace;
|
|
|
|
use crate::database::redis::RedisPool;
|
|
use crate::models::ids::PayoutId;
|
|
use crate::routes::ApiError;
|
|
use crate::util::error::Context;
|
|
use crate::util::gotenberg::{
|
|
GeneratedPdfType, MODRINTH_GENERATED_PDF_TYPE, MODRINTH_PAYMENT_ID,
|
|
PAYMENT_STATEMENTS_NAMESPACE,
|
|
};
|
|
use crate::util::guards::internal_network_guard;
|
|
|
|
/// Document generated by Gotenberg and returned to us.
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct GotenbergDocument {
|
|
/// Body of the document as a base 64 string.
|
|
pub body: String,
|
|
}
|
|
|
|
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
|
cfg.service(success_callback).service(error_callback);
|
|
}
|
|
|
|
#[post("/gotenberg/success", guard = "internal_network_guard")]
|
|
pub async fn success_callback(
|
|
web::Header(header::ContentDisposition {
|
|
disposition,
|
|
parameters: disposition_parameters,
|
|
}): web::Header<header::ContentDisposition>,
|
|
web::Header(GotenbergTrace(trace)): web::Header<GotenbergTrace>,
|
|
web::Header(ModrinthGeneratedPdfType(r#type)): web::Header<
|
|
ModrinthGeneratedPdfType,
|
|
>,
|
|
maybe_payment_id: Option<web::Header<ModrinthPaymentId>>,
|
|
body: web::Bytes,
|
|
redis: web::Data<RedisPool>,
|
|
) -> Result<(), ApiError> {
|
|
trace!(
|
|
%trace,
|
|
%disposition,
|
|
?disposition_parameters,
|
|
r#type = r#type.as_str(),
|
|
?maybe_payment_id,
|
|
body.len = body.len(),
|
|
"Received Gotenberg generated PDF"
|
|
);
|
|
|
|
let payout_id = maybe_payment_id
|
|
.wrap_request_err("no payout ID for document")?
|
|
.0
|
|
.0;
|
|
|
|
let mut redis = redis
|
|
.connect()
|
|
.await
|
|
.wrap_internal_err("failed to get Redis connection")?;
|
|
|
|
let body = base64::engine::general_purpose::STANDARD.encode(&body);
|
|
|
|
let redis_msg = serde_json::to_string(&Ok::<
|
|
GotenbergDocument,
|
|
GotenbergError,
|
|
>(GotenbergDocument { body }))
|
|
.wrap_internal_err("failed to serialize document to JSON")?;
|
|
|
|
redis
|
|
.lpush(
|
|
PAYMENT_STATEMENTS_NAMESPACE,
|
|
&payout_id.to_string(),
|
|
&redis_msg,
|
|
)
|
|
.await
|
|
.wrap_internal_err("failed to send document over Redis")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
|
pub struct GotenbergError {
|
|
pub status: Option<String>,
|
|
pub message: Option<String>,
|
|
}
|
|
|
|
impl fmt::Display for GotenbergError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match (&self.status, &self.message) {
|
|
(None, None) => write!(f, "(unknown error)"),
|
|
(Some(status), None) => write!(f, "status {status}"),
|
|
(None, Some(message)) => write!(f, "(no status) {message}"),
|
|
(Some(status), Some(message)) => {
|
|
write!(f, "status {status}: {message}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[post("/gotenberg/error", guard = "internal_network_guard")]
|
|
pub async fn error_callback(
|
|
web::Header(GotenbergTrace(trace)): web::Header<GotenbergTrace>,
|
|
web::Header(ModrinthGeneratedPdfType(r#type)): web::Header<
|
|
ModrinthGeneratedPdfType,
|
|
>,
|
|
maybe_payment_id: Option<web::Header<ModrinthPaymentId>>,
|
|
web::Json(error_body): web::Json<GotenbergError>,
|
|
redis: web::Data<RedisPool>,
|
|
) -> Result<(), ApiError> {
|
|
trace!(
|
|
%trace,
|
|
r#type = r#type.as_str(),
|
|
?maybe_payment_id,
|
|
?error_body,
|
|
"Received Gotenberg error webhook"
|
|
);
|
|
|
|
let payout_id = maybe_payment_id
|
|
.wrap_request_err("no payout ID for document")?
|
|
.0
|
|
.0;
|
|
let mut redis = redis
|
|
.connect()
|
|
.await
|
|
.wrap_internal_err("failed to get Redis connection")?;
|
|
|
|
let redis_msg = serde_json::to_string(&Err::<
|
|
GotenbergDocument,
|
|
GotenbergError,
|
|
>(error_body))
|
|
.wrap_internal_err("failed to serialize error to JSON")?;
|
|
|
|
redis
|
|
.lpush(
|
|
PAYMENT_STATEMENTS_NAMESPACE,
|
|
&payout_id.to_string(),
|
|
&redis_msg,
|
|
)
|
|
.await
|
|
.wrap_internal_err("failed to send error over Redis")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct GotenbergTrace(String);
|
|
|
|
impl header::TryIntoHeaderValue for GotenbergTrace {
|
|
type Error = header::InvalidHeaderValue;
|
|
|
|
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
|
|
header::HeaderValue::from_str(&self.0)
|
|
}
|
|
}
|
|
|
|
impl header::Header for GotenbergTrace {
|
|
fn name() -> header::HeaderName {
|
|
header::HeaderName::from_static("gotenberg-trace")
|
|
}
|
|
|
|
fn parse<M: HttpMessage>(m: &M) -> Result<Self, ParseError> {
|
|
m.headers()
|
|
.get(Self::name())
|
|
.ok_or(ParseError::Header)?
|
|
.to_str()
|
|
.map_err(|_| ParseError::Header)
|
|
.map(ToOwned::to_owned)
|
|
.map(GotenbergTrace)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ModrinthGeneratedPdfType(GeneratedPdfType);
|
|
|
|
impl header::TryIntoHeaderValue for ModrinthGeneratedPdfType {
|
|
type Error = header::InvalidHeaderValue;
|
|
|
|
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
|
|
header::HeaderValue::from_str(self.0.as_str())
|
|
}
|
|
}
|
|
|
|
impl header::Header for ModrinthGeneratedPdfType {
|
|
fn name() -> header::HeaderName {
|
|
MODRINTH_GENERATED_PDF_TYPE
|
|
}
|
|
|
|
fn parse<M: HttpMessage>(m: &M) -> Result<Self, ParseError> {
|
|
m.headers()
|
|
.get(Self::name())
|
|
.ok_or(ParseError::Header)?
|
|
.to_str()
|
|
.map_err(|_| ParseError::Header)?
|
|
.parse()
|
|
.map_err(|_| ParseError::Header)
|
|
.map(ModrinthGeneratedPdfType)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct ModrinthPaymentId(pub PayoutId);
|
|
|
|
impl header::TryIntoHeaderValue for ModrinthPaymentId {
|
|
type Error = header::InvalidHeaderValue;
|
|
|
|
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
|
|
header::HeaderValue::from_str(&self.0.to_string())
|
|
}
|
|
}
|
|
|
|
impl header::Header for ModrinthPaymentId {
|
|
fn name() -> header::HeaderName {
|
|
MODRINTH_PAYMENT_ID
|
|
}
|
|
|
|
fn parse<M: HttpMessage>(m: &M) -> Result<Self, ParseError> {
|
|
m.headers()
|
|
.get(Self::name())
|
|
.ok_or(ParseError::Header)?
|
|
.to_str()
|
|
.map_err(|_| ParseError::Header)
|
|
.and_then(|s| parse_base62(s).map_err(|_| ParseError::Header))
|
|
.map(|id| Self(PayoutId(id)))
|
|
}
|
|
}
|