You've already forked AstralRinth
forked from didirus/AstralRinth
Supporting documents for Mural payouts (#4721)
* 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
This commit is contained in:
@@ -1,22 +1,29 @@
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::PayoutId;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError};
|
||||
use crate::util::env::env_var;
|
||||
use crate::util::error::Context;
|
||||
use actix_web::http::header::HeaderName;
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const MODRINTH_GENERATED_PDF_TYPE: HeaderName =
|
||||
HeaderName::from_static("modrinth-generated-pdf-type");
|
||||
pub const MODRINTH_PAYMENT_ID: HeaderName =
|
||||
HeaderName::from_static("modrinth-payment-id");
|
||||
pub const PAYMENT_STATEMENTS_NAMESPACE: &str = "payment_statements";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PaymentStatement {
|
||||
pub payment_id: String,
|
||||
pub payment_id: PayoutId,
|
||||
pub recipient_address_line_1: Option<String>,
|
||||
pub recipient_address_line_2: Option<String>,
|
||||
pub recipient_address_line_3: Option<String>,
|
||||
pub recipient_email: String,
|
||||
pub payment_date: String,
|
||||
pub payment_date: DateTime<Utc>,
|
||||
pub gross_amount_cents: i64,
|
||||
pub net_amount_cents: i64,
|
||||
pub fees_cents: i64,
|
||||
@@ -53,28 +60,27 @@ pub struct GotenbergClient {
|
||||
gotenberg_url: String,
|
||||
site_url: String,
|
||||
callback_base: String,
|
||||
redis: RedisPool,
|
||||
}
|
||||
|
||||
impl GotenbergClient {
|
||||
/// Initialize the client from environment variables.
|
||||
pub fn from_env() -> Result<Self, ApiError> {
|
||||
pub fn from_env(redis: RedisPool) -> eyre::Result<Self> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Modrinth")
|
||||
.build()
|
||||
.wrap_internal_err("failed to build reqwest client")?;
|
||||
.wrap_err("failed to build reqwest client")?;
|
||||
|
||||
let gotenberg_url = dotenvy::var("GOTENBERG_URL")
|
||||
.wrap_internal_err("GOTENBERG_URL is not set")?;
|
||||
let site_url = dotenvy::var("SITE_URL")
|
||||
.wrap_internal_err("SITE_URL is not set")?;
|
||||
let callback_base = dotenvy::var("GOTENBERG_CALLBACK_BASE")
|
||||
.wrap_internal_err("GOTENBERG_CALLBACK_BASE is not set")?;
|
||||
let gotenberg_url = env_var("GOTENBERG_URL")?;
|
||||
let site_url = env_var("SITE_URL")?;
|
||||
let callback_base = env_var("GOTENBERG_CALLBACK_BASE")?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
gotenberg_url: gotenberg_url.trim_end_matches('/').to_owned(),
|
||||
site_url: site_url.trim_end_matches('/').to_owned(),
|
||||
callback_base: callback_base.trim_end_matches('/').to_owned(),
|
||||
redis,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,7 +146,7 @@ impl GotenbergClient {
|
||||
)
|
||||
.header(
|
||||
"Modrinth-Payment-Id",
|
||||
&statement.payment_id,
|
||||
statement.payment_id.to_string(),
|
||||
)
|
||||
.header(
|
||||
"Gotenberg-Output-Filename",
|
||||
@@ -155,11 +161,67 @@ impl GotenbergClient {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tells Gotenberg to generate a payment statement PDF, and waits until we
|
||||
/// get a response for that PDF.
|
||||
///
|
||||
/// This submits the PDF via [`GotenbergClient::generate_payment_statement`]
|
||||
/// then waits until some Labrinth instance receives a response on the
|
||||
/// Gotenberg webhook, sends the response over Redis to our instance, and
|
||||
/// returns that from this function.
|
||||
///
|
||||
/// In a local environment, the Labrinth instance that receives the webhook
|
||||
/// response will be the same one that is waiting on the response, so the
|
||||
/// Redis step is unnecessary, but in a real deployment, we have multiple
|
||||
/// Labrinth instances so we need Redis in between.
|
||||
///
|
||||
/// If Gotenberg does not return a response to us within `GOTENBERG_TIMEOUT`
|
||||
/// number of milliseconds, this will fail.
|
||||
pub async fn wait_for_payment_statement(
|
||||
&self,
|
||||
statement: &PaymentStatement,
|
||||
) -> Result<GotenbergDocument, ApiError> {
|
||||
let mut redis = self
|
||||
.redis
|
||||
.connect()
|
||||
.await
|
||||
.wrap_internal_err("failed to get Redis connection")?;
|
||||
|
||||
self.generate_payment_statement(statement).await?;
|
||||
|
||||
let timeout_ms = env_var("GOTENBERG_TIMEOUT")
|
||||
.map_err(ApiError::Internal)?
|
||||
.parse::<u64>()
|
||||
.wrap_internal_err(
|
||||
"`GOTENBERG_TIMEOUT` is not a valid number of milliseconds",
|
||||
)?;
|
||||
|
||||
let [_key, document] = tokio::time::timeout(
|
||||
Duration::from_millis(timeout_ms),
|
||||
redis.brpop(
|
||||
PAYMENT_STATEMENTS_NAMESPACE,
|
||||
&statement.payment_id.to_string(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("Gotenberg document generation timed out")?
|
||||
.wrap_internal_err("failed to get document over Redis")?
|
||||
.wrap_internal_err("no document was returned from Redis")?;
|
||||
|
||||
let document = serde_json::from_str::<
|
||||
Result<GotenbergDocument, GotenbergError>,
|
||||
>(&document)
|
||||
.wrap_internal_err("failed to deserialize Redis document response")?
|
||||
.wrap_internal_err("Gotenberg document generation failed")?;
|
||||
|
||||
Ok(document)
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_statement_template(html: &str, s: &PaymentStatement) -> String {
|
||||
let variables: Vec<(&str, String)> = vec![
|
||||
("statement.payment_id", s.payment_id.clone()),
|
||||
("statement.payment_id", s.payment_id.to_string()),
|
||||
(
|
||||
"statement.recipient_address_line_1",
|
||||
s.recipient_address_line_1.clone().unwrap_or_default(),
|
||||
@@ -173,7 +235,15 @@ fn fill_statement_template(html: &str, s: &PaymentStatement) -> String {
|
||||
s.recipient_address_line_3.clone().unwrap_or_default(),
|
||||
),
|
||||
("statement.recipient_email", s.recipient_email.clone()),
|
||||
("statement.payment_date", s.payment_date.clone()),
|
||||
(
|
||||
"statement.payment_date",
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}",
|
||||
s.payment_date.year(),
|
||||
s.payment_date.month(),
|
||||
s.payment_date.day()
|
||||
),
|
||||
),
|
||||
(
|
||||
"statement.gross_amount",
|
||||
format_money(s.gross_amount_cents, &s.currency_code),
|
||||
|
||||
Reference in New Issue
Block a user