mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 13:16:38 +00:00
Merge branch 'main' into prospector/modpack-proj-fixes
This commit is contained in:
@@ -11,11 +11,15 @@ import {
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
EllipsisVerticalIcon,
|
||||
EyeOffIcon,
|
||||
LinkIcon,
|
||||
LoaderCircleIcon,
|
||||
ScaleIcon,
|
||||
ShieldCheckIcon,
|
||||
SpinnerIcon,
|
||||
TimerIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
|
||||
import {
|
||||
@@ -23,6 +27,7 @@ import {
|
||||
ButtonStyled,
|
||||
Collapsible,
|
||||
CollapsibleRegion,
|
||||
commonMessages,
|
||||
getProjectTypeIcon,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
@@ -30,6 +35,7 @@ import {
|
||||
type OverflowMenuOption,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { NavTabs } from '@modrinth/ui'
|
||||
import {
|
||||
@@ -47,6 +53,7 @@ import ThreadView from '~/components/ui/thread/ThreadView.vue'
|
||||
|
||||
const auth = await useAuth()
|
||||
const featureFlags = useFeatureFlags()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const formatDateTimeUtc = useFormatDateTime({
|
||||
year: 'numeric',
|
||||
@@ -96,6 +103,16 @@ const emit = defineEmits<{
|
||||
showMaliciousSummary: [unsafeFiles: UnsafeFile[]]
|
||||
}>()
|
||||
|
||||
const projectStatus = ref<Labrinth.Projects.v2.ProjectStatus>(props.item.project.status)
|
||||
const isProjectApproved = computed(() => {
|
||||
return (
|
||||
projectStatus.value === 'approved' ||
|
||||
projectStatus.value === 'archived' ||
|
||||
projectStatus.value === 'unlisted' ||
|
||||
projectStatus.value === 'private'
|
||||
)
|
||||
})
|
||||
|
||||
const quickActions = computed<OverflowMenuOption[]>(() => {
|
||||
const actions: OverflowMenuOption[] = []
|
||||
|
||||
@@ -141,6 +158,53 @@ const quickActions = computed<OverflowMenuOption[]>(() => {
|
||||
return actions
|
||||
})
|
||||
|
||||
const isLoadingStatusAction = ref(false)
|
||||
const projectStatusActions = computed<OverflowMenuOption[]>(() => [
|
||||
{
|
||||
id: 'approve',
|
||||
color: 'green',
|
||||
action: () => setStatus('approved'),
|
||||
hoverFilled: true,
|
||||
disabled: isProjectApproved.value || isLoadingStatusAction.value,
|
||||
},
|
||||
{
|
||||
id: 'withhold',
|
||||
color: 'orange',
|
||||
action: () => setStatus('withheld'),
|
||||
hoverFilled: true,
|
||||
disabled: projectStatus.value === 'withheld' || isLoadingStatusAction.value,
|
||||
},
|
||||
{
|
||||
id: 'send-to-review',
|
||||
action: () => setStatus('processing'),
|
||||
hoverFilled: true,
|
||||
disabled: projectStatus.value === 'processing' || isLoadingStatusAction.value,
|
||||
},
|
||||
{
|
||||
id: 'reject',
|
||||
color: 'red',
|
||||
action: () => setStatus('rejected'),
|
||||
hoverFilled: true,
|
||||
disabled: projectStatus.value === 'rejected' || isLoadingStatusAction.value,
|
||||
},
|
||||
])
|
||||
|
||||
async function setStatus(status: Labrinth.Projects.v2.ProjectStatus) {
|
||||
isLoadingStatusAction.value = true
|
||||
try {
|
||||
await client.labrinth.projects_v2.edit(props.item.project.id, { status })
|
||||
|
||||
projectStatus.value = status
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: (err as any)?.data?.description ? (err as any).data.description : String(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
isLoadingStatusAction.value = false
|
||||
}
|
||||
|
||||
type Tab = 'Thread' | 'Files' | 'File'
|
||||
const tabs: readonly ('Thread' | 'Files')[] = ['Thread', 'Files']
|
||||
const currentTab = ref<Tab>('Thread')
|
||||
@@ -347,13 +411,6 @@ const severityColor = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const isProjectApproved = computed(() => {
|
||||
const status = props.item.project.status
|
||||
return (
|
||||
status === 'approved' || status === 'archived' || status === 'unlisted' || status === 'private'
|
||||
)
|
||||
})
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
const dates = props.item.reports.map((r) => new Date(r.created))
|
||||
const earliest = new Date(Math.min(...dates.map((d) => d.getTime())))
|
||||
@@ -1117,6 +1174,37 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
<BugIcon /> Fail
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="standard">
|
||||
<OverflowMenu
|
||||
class="btn-dropdown-animation"
|
||||
:disabled="isLoadingStatusAction"
|
||||
:options="projectStatusActions"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="isLoadingStatusAction"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ScaleIcon v-else aria-hidden="true" />
|
||||
Set Status
|
||||
<template #approve>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
Approve
|
||||
</template>
|
||||
<template #withhold>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold
|
||||
</template>
|
||||
<template #send-to-review>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review
|
||||
</template>
|
||||
<template #reject>
|
||||
<XIcon aria-hidden="true" />
|
||||
Reject
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="featureFlags.developerMode" type="outlined">
|
||||
<button @click="emit('showMaliciousSummary', unsafeFiles)">Debug Summary</button>
|
||||
</ButtonStyled>
|
||||
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n update file_scans\n set attributions_scanned_at = $2\n where file_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4748e425c6bbd4d154bf658bf695661485a9700bffa413c5e94d80a525b53e27"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n update file_scans\n set attributions_scanned_at = now\n from unnest($1::bigint[], $2::timestamptz[]) as u(id, now)\n where file_scans.file_id = u.id\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TimestamptzArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "968904f577c2c696c6222e19cc145bce0e845f2ab9f8629b0789a4861bddb4dc"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
-- The original view used a single correlated `not exists` with an `or`, which
|
||||
-- prevents the partial unique indexes on `attributions_exemptions (version_id)`
|
||||
-- and `(project_id)` from being used, forcing a full sequential scan of the
|
||||
-- exemptions table for every candidate version.
|
||||
--
|
||||
-- `not exists (a or b)` is equivalent to `not exists (a) and not exists (b)`,
|
||||
-- so splitting the subquery lets each anti-join use its own index.
|
||||
create or replace view attribution_enforced_versions as
|
||||
select v.id
|
||||
from versions v
|
||||
where not exists (
|
||||
select 1
|
||||
from attributions_exemptions ae
|
||||
where ae.version_id = v.id
|
||||
)
|
||||
and not exists (
|
||||
select 1
|
||||
from attributions_exemptions ae
|
||||
where ae.project_id = v.mod_id
|
||||
);
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::database;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::project_item::ProjectQueryResult;
|
||||
use crate::database::models::version_item::VersionQueryResult;
|
||||
use crate::database::models::{DBCollection, DBOrganization, DBTeamMember};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{DBProject, DBVersion, models};
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::models::ids::FileId;
|
||||
use crate::models::projects::{
|
||||
MissingAttributionFile, OverrideSource, Version,
|
||||
@@ -19,10 +19,10 @@ use itertools::Itertools;
|
||||
|
||||
pub async fn enrich_dependency_attributions(
|
||||
versions: &mut [VersionQueryResult],
|
||||
pool: &PgPool,
|
||||
pool: &ReadOnlyPgPool,
|
||||
) {
|
||||
let version_ids = versions.iter().map(|v| v.inner.id).collect::<Vec<_>>();
|
||||
let dep_attr = get_dependency_attributions(pool, &version_ids)
|
||||
let dep_attr = get_dependency_attributions(&**pool, &version_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -222,6 +222,7 @@ pub async fn filter_visible_versions(
|
||||
mut versions: Vec<VersionQueryResult>,
|
||||
user_option: &Option<User>,
|
||||
pool: &PgPool,
|
||||
ro_pool: &ReadOnlyPgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<crate::models::projects::Version>, ApiError> {
|
||||
let filtered_version_ids = filter_visible_version_ids(
|
||||
@@ -238,7 +239,7 @@ pub async fn filter_visible_versions(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
enrich_dependency_attributions(&mut versions, pool).await;
|
||||
enrich_dependency_attributions(&mut versions, ro_pool).await;
|
||||
|
||||
Ok(versions
|
||||
.into_iter()
|
||||
|
||||
@@ -101,10 +101,11 @@ pub fn app_setup(
|
||||
{
|
||||
let automated_moderation_queue_ref = automated_moderation_queue.clone();
|
||||
let pool_ref = pool.clone();
|
||||
let ro_pool_ref = ro_pool.clone();
|
||||
let redis_pool_ref = redis_pool.clone();
|
||||
actix_rt::spawn(async move {
|
||||
automated_moderation_queue_ref
|
||||
.task(pool_ref, redis_pool_ref)
|
||||
.task(pool_ref, ro_pool_ref, redis_pool_ref)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ pub async fn scan_all_files(
|
||||
redis: &RedisPool,
|
||||
file_host: &dyn FileHost,
|
||||
) -> Result<()> {
|
||||
let mut txn = db.begin().await.wrap_err("beginning transaction")?;
|
||||
|
||||
let files_to_scan = sqlx::query!(
|
||||
r#"
|
||||
select
|
||||
@@ -59,13 +57,13 @@ pub async fn scan_all_files(
|
||||
where fa.attributions_scanned_at is null
|
||||
"#
|
||||
)
|
||||
.fetch_all(&mut txn)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.wrap_err("fetching files to scan")?;
|
||||
|
||||
info!("Found {} files to scan", files_to_scan.len());
|
||||
|
||||
let mut scanned_ids = Vec::new();
|
||||
let mut scanned_count = 0;
|
||||
|
||||
for row in files_to_scan {
|
||||
let human_file_id = FileId::from(row.file_id);
|
||||
@@ -74,6 +72,10 @@ pub async fn scan_all_files(
|
||||
info!("Scanning file");
|
||||
|
||||
let file_id = row.file_id;
|
||||
let mut txn = db
|
||||
.begin()
|
||||
.await
|
||||
.wrap_err("beginning file scan transaction")?;
|
||||
|
||||
let overrides = extract_override_files_from_storage(
|
||||
file_host, file_id, &row.url,
|
||||
@@ -108,33 +110,33 @@ pub async fn scan_all_files(
|
||||
})?;
|
||||
}
|
||||
|
||||
scanned_ids.push(file_id.0);
|
||||
let now = Utc::now();
|
||||
sqlx::query!(
|
||||
"
|
||||
update file_scans
|
||||
set attributions_scanned_at = $2
|
||||
where file_id = $1
|
||||
",
|
||||
file_id.0,
|
||||
now,
|
||||
)
|
||||
.execute(&mut txn)
|
||||
.await
|
||||
.wrap_err("marking file as scanned")?;
|
||||
|
||||
txn.commit()
|
||||
.await
|
||||
.wrap_err("committing file scan transaction")?;
|
||||
|
||||
eyre::Ok(())
|
||||
}
|
||||
.instrument(span)
|
||||
.await?;
|
||||
|
||||
scanned_count += 1;
|
||||
}
|
||||
|
||||
if !scanned_ids.is_empty() {
|
||||
let now = Utc::now();
|
||||
sqlx::query!(
|
||||
"
|
||||
update file_scans
|
||||
set attributions_scanned_at = now
|
||||
from unnest($1::bigint[], $2::timestamptz[]) as u(id, now)
|
||||
where file_scans.file_id = u.id
|
||||
",
|
||||
&scanned_ids,
|
||||
&vec![now; scanned_ids.len()],
|
||||
)
|
||||
.execute(&mut txn)
|
||||
.await
|
||||
.wrap_err("marking files as scanned")?;
|
||||
}
|
||||
|
||||
info!("Marked {} files as scanned", scanned_ids.len());
|
||||
|
||||
txn.commit().await.wrap_err("committing transaction")?;
|
||||
info!("Marked {} files as scanned", scanned_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::auth::checks::filter_visible_versions;
|
||||
use crate::database;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::DBUserId;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
@@ -229,7 +229,12 @@ impl Default for AutomatedModerationQueue {
|
||||
}
|
||||
|
||||
impl AutomatedModerationQueue {
|
||||
pub async fn task(&self, pool: PgPool, redis: RedisPool) {
|
||||
pub async fn task(
|
||||
&self,
|
||||
pool: PgPool,
|
||||
ro_pool: ReadOnlyPgPool,
|
||||
redis: RedisPool,
|
||||
) {
|
||||
loop {
|
||||
let projects = self.projects.clone();
|
||||
self.projects.clear();
|
||||
@@ -375,6 +380,7 @@ impl AutomatedModerationQueue {
|
||||
.await?,
|
||||
&None,
|
||||
&pool,
|
||||
&ro_pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -481,8 +481,7 @@ impl PayoutsQueue {
|
||||
Ok(options.options)
|
||||
}
|
||||
|
||||
pub async fn get_brex_balance() -> Result<Option<AccountBalance>, ApiError>
|
||||
{
|
||||
pub async fn get_brex_balance() -> eyre::Result<Option<AccountBalance>> {
|
||||
#[derive(Deserialize)]
|
||||
struct BrexBalance {
|
||||
pub amount: i64,
|
||||
@@ -505,9 +504,11 @@ impl PayoutsQueue {
|
||||
.get(format!("{}accounts/cash", ENV.BREX_API_URL))
|
||||
.bearer_auth(&ENV.BREX_API_KEY)
|
||||
.send()
|
||||
.await?
|
||||
.await
|
||||
.wrap_request_err("sending `accounts/cash` request")?
|
||||
.json::<BrexResponse>()
|
||||
.await?;
|
||||
.await
|
||||
.wrap_request_err("reading `accounts/cash` request")?;
|
||||
|
||||
Ok(Some(AccountBalance {
|
||||
available: Decimal::from(
|
||||
@@ -527,8 +528,7 @@ impl PayoutsQueue {
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_paypal_balance() -> Result<Option<AccountBalance>, ApiError>
|
||||
{
|
||||
pub async fn get_paypal_balance() -> eyre::Result<Option<AccountBalance>> {
|
||||
let api_username = &ENV.PAYPAL_NVP_USERNAME;
|
||||
let api_password = &ENV.PAYPAL_NVP_PASSWORD;
|
||||
let api_signature = &ENV.PAYPAL_NVP_SIGNATURE;
|
||||
@@ -544,9 +544,17 @@ impl PayoutsQueue {
|
||||
let endpoint = "https://api-3t.paypal.com/nvp";
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(endpoint).form(¶ms).send().await?;
|
||||
let response = client
|
||||
.post(endpoint)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.wrap_err("requesting `nvp` endpoint")?;
|
||||
|
||||
let text = response.text().await?;
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.wrap_err("reading response text from `nvp`")?;
|
||||
let body = urlencoding::decode(&text).unwrap_or_default();
|
||||
|
||||
let mut key_value_map = HashMap::new();
|
||||
@@ -573,7 +581,7 @@ impl PayoutsQueue {
|
||||
|
||||
pub async fn get_tremendous_balance(
|
||||
&self,
|
||||
) -> Result<Option<AccountBalance>, ApiError> {
|
||||
) -> eyre::Result<Option<AccountBalance>> {
|
||||
#[derive(Deserialize)]
|
||||
struct FundingSourceMeta {
|
||||
available_cents: Option<u64>,
|
||||
@@ -597,7 +605,8 @@ impl PayoutsQueue {
|
||||
"funding_sources",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_request_err("fetching funding sources")?;
|
||||
|
||||
Ok(val
|
||||
.funding_sources
|
||||
@@ -1257,7 +1266,7 @@ pub async fn index_payouts_notifications(
|
||||
pub async fn insert_bank_balances_and_webhook(
|
||||
payouts: &PayoutsQueue,
|
||||
pool: &PgPool,
|
||||
) -> Result<(), ApiError> {
|
||||
) -> eyre::Result<()> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let paypal_result = PayoutsQueue::get_paypal_balance().await;
|
||||
@@ -1320,25 +1329,29 @@ pub async fn insert_bank_balances_and_webhook(
|
||||
ENV.PAYPAL_BALANCE_ALERT_THRESHOLD,
|
||||
paypal_result,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_err("checking PayPal balance")?;
|
||||
check_balance_with_webhook(
|
||||
"brex",
|
||||
ENV.BREX_BALANCE_ALERT_THRESHOLD,
|
||||
brex_result,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_err("checking Brex balance")?;
|
||||
check_balance_with_webhook(
|
||||
"tremendous",
|
||||
ENV.TREMENDOUS_BALANCE_ALERT_THRESHOLD,
|
||||
tremendous_result,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_err("checking Tremendous balance")?;
|
||||
check_balance_with_webhook(
|
||||
"mural",
|
||||
ENV.MURAL_BALANCE_ALERT_THRESHOLD,
|
||||
mural_result,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_err("checking Mural balance")?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
@@ -1349,8 +1362,8 @@ pub async fn insert_bank_balances_and_webhook(
|
||||
async fn check_balance_with_webhook(
|
||||
source: &str,
|
||||
threshold: u64,
|
||||
result: Result<Option<AccountBalance>, ApiError>,
|
||||
) -> Result<Option<AccountBalance>, ApiError> {
|
||||
result: eyre::Result<Option<AccountBalance>>,
|
||||
) -> eyre::Result<Option<AccountBalance>> {
|
||||
let maybe_threshold = if threshold > 0 { Some(threshold) } else { None };
|
||||
let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK;
|
||||
|
||||
@@ -1374,13 +1387,18 @@ async fn check_balance_with_webhook(
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
error!(%error, "Failure getting balance for payout source '{source}'");
|
||||
// use compact single-line error repr here
|
||||
error!(
|
||||
error = format!("{error:#?}"),
|
||||
"Failure getting balance for payout source '{source}'"
|
||||
);
|
||||
|
||||
if maybe_threshold.is_some() {
|
||||
// use expanded multi-line error repr here
|
||||
send_slack_payout_source_alert_webhook(
|
||||
PayoutSourceAlertType::CheckFailure {
|
||||
source: source.to_owned(),
|
||||
display_error: error.to_string(),
|
||||
display_error: format!("{error:?}"),
|
||||
},
|
||||
payout_alert_webhook,
|
||||
)
|
||||
|
||||
@@ -106,20 +106,20 @@ impl PayoutsQueue {
|
||||
|
||||
pub async fn get_mural_balance(
|
||||
&self,
|
||||
) -> Result<Option<AccountBalance>, ApiError> {
|
||||
) -> eyre::Result<Option<AccountBalance>> {
|
||||
let muralpay = self.muralpay.load();
|
||||
let muralpay = muralpay
|
||||
.as_ref()
|
||||
.wrap_internal_err("Mural Pay client not available")?;
|
||||
.wrap_err("Mural Pay client not available")?;
|
||||
|
||||
let account = muralpay
|
||||
.client
|
||||
.get_account(muralpay.source_account_id)
|
||||
.await
|
||||
.wrap_internal_err("failed to get source account")?;
|
||||
.wrap_err("failed to get source account")?;
|
||||
let details = account
|
||||
.account_details
|
||||
.wrap_internal_err("source account does not have details")?;
|
||||
.wrap_err("source account does not have details")?;
|
||||
let available = details
|
||||
.balances
|
||||
.iter()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -36,6 +36,7 @@ pub async fn forge_updates(
|
||||
web::Query(neo): web::Query<NeoForge>,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -82,6 +83,7 @@ pub async fn forge_updates(
|
||||
.collect(),
|
||||
&user_option,
|
||||
&pool,
|
||||
&ro_pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::categories::LinkPlatform;
|
||||
use crate::database::models::{project_item, version_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::projects::{
|
||||
Link, MonetizationStatus, Project, ProjectStatus, Version,
|
||||
@@ -366,6 +366,7 @@ pub async fn dependency_list(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -374,6 +375,7 @@ pub async fn dependency_list(
|
||||
req,
|
||||
info,
|
||||
pool.clone(),
|
||||
ro_pool,
|
||||
redis.clone(),
|
||||
session_queue,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::models;
|
||||
use crate::models::ids::VersionId;
|
||||
use crate::models::projects::{
|
||||
@@ -89,6 +89,7 @@ pub async fn version_list(
|
||||
info: web::Path<(String,)>,
|
||||
web::Query(filters): web::Query<VersionListFilters>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -147,6 +148,7 @@ pub async fn version_list(
|
||||
info,
|
||||
web::Query(filters),
|
||||
pool,
|
||||
ro_pool,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
@@ -196,6 +198,7 @@ pub async fn version_project_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -204,6 +207,7 @@ pub async fn version_project_get(
|
||||
req,
|
||||
id,
|
||||
pool,
|
||||
ro_pool,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
@@ -238,6 +242,7 @@ pub async fn versions_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<VersionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -249,6 +254,7 @@ pub async fn versions_get(
|
||||
req,
|
||||
web::Query(ids),
|
||||
pool,
|
||||
ro_pool,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
@@ -286,15 +292,22 @@ pub async fn version_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let response =
|
||||
v3::versions::version_get_helper(req, id, pool, redis, session_queue)
|
||||
.await
|
||||
.map(|b| HttpResponse::Ok().json(b))
|
||||
.or_else(v2_reroute::flatten_404_error)?;
|
||||
let response = v3::versions::version_get_helper(
|
||||
req,
|
||||
id,
|
||||
pool,
|
||||
ro_pool,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
.map(|b| HttpResponse::Ok().json(b))
|
||||
.or_else(v2_reroute::flatten_404_error)?;
|
||||
// Convert response to V2 format
|
||||
match v2_reroute::extract_ok_json::<Version>(response).await {
|
||||
Ok(version) => {
|
||||
@@ -364,6 +377,7 @@ pub async fn version_edit(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
new_version: web::Json<EditVersion>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
@@ -384,6 +398,7 @@ pub async fn version_edit(
|
||||
req.clone(),
|
||||
(*info).0,
|
||||
pool.clone(),
|
||||
ro_pool.clone(),
|
||||
redis.clone(),
|
||||
session_queue.clone(),
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::database::models::{
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{self, models as db_models};
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::database::{PgPool, PgTransaction, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
@@ -1289,16 +1289,19 @@ pub async fn dependency_list(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
dependency_list_internal(req, info, pool, redis, session_queue).await
|
||||
dependency_list_internal(req, info, pool, ro_pool, redis, session_queue)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn dependency_list_internal(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -1372,6 +1375,7 @@ pub async fn dependency_list_internal(
|
||||
versions_result,
|
||||
&user_option,
|
||||
&pool,
|
||||
&ro_pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -251,6 +251,7 @@ pub async fn get_versions_from_hashes(
|
||||
.await?,
|
||||
&user_option,
|
||||
&pool,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::auth::checks::{
|
||||
};
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::loader_fields::{
|
||||
self, LoaderField, LoaderFieldEnumValue, VersionField,
|
||||
};
|
||||
@@ -16,6 +15,7 @@ use crate::database::models::version_item::{
|
||||
};
|
||||
use crate::database::models::{DBOrganization, image_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::models;
|
||||
use crate::models::ids::VersionId;
|
||||
use crate::models::images::ImageContext;
|
||||
@@ -64,16 +64,19 @@ pub async fn version_project_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let info = info.into_inner();
|
||||
version_project_get_helper(req, info, pool, redis, session_queue).await
|
||||
version_project_get_helper(req, info, pool, ro_pool, redis, session_queue)
|
||||
.await
|
||||
}
|
||||
pub async fn version_project_get_helper(
|
||||
req: HttpRequest,
|
||||
id: (String, String),
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -118,7 +121,7 @@ pub async fn version_project_get_helper(
|
||||
let version_id = version.inner.id;
|
||||
enrich_dependency_attributions(
|
||||
std::slice::from_mut(&mut version),
|
||||
&pool,
|
||||
&ro_pool,
|
||||
)
|
||||
.await;
|
||||
let mut v = models::projects::Version::from(version);
|
||||
@@ -168,6 +171,7 @@ pub async fn versions_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<VersionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -191,9 +195,14 @@ pub async fn versions_get(
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
let mut versions =
|
||||
filter_visible_versions(versions_data, &user_option, &pool, &redis)
|
||||
.await?;
|
||||
let mut versions = filter_visible_versions(
|
||||
versions_data,
|
||||
&user_option,
|
||||
&pool,
|
||||
&ro_pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !ids.include_changelog {
|
||||
for version in &mut versions {
|
||||
@@ -208,17 +217,19 @@ pub async fn version_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<models::projects::Version>, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
version_get_helper(req, id, pool, redis, session_queue).await
|
||||
version_get_helper(req, id, pool, ro_pool, redis, session_queue).await
|
||||
}
|
||||
|
||||
pub async fn version_get_helper(
|
||||
req: HttpRequest,
|
||||
id: models::ids::VersionId,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<models::projects::Version>, ApiError> {
|
||||
@@ -240,8 +251,11 @@ pub async fn version_get_helper(
|
||||
&& is_visible_version(&data.inner, &user_option, &pool, &redis).await?
|
||||
{
|
||||
let version_id = data.inner.id;
|
||||
enrich_dependency_attributions(std::slice::from_mut(&mut data), &pool)
|
||||
.await;
|
||||
enrich_dependency_attributions(
|
||||
std::slice::from_mut(&mut data),
|
||||
&ro_pool,
|
||||
)
|
||||
.await;
|
||||
let mut version = models::projects::Version::from(data);
|
||||
let missing = get_files_missing_attribution(&**pool, &[version_id])
|
||||
.await
|
||||
@@ -797,6 +811,7 @@ async fn version_list(
|
||||
info: web::Path<(String,)>,
|
||||
web::Query(filters): web::Query<VersionListFilters>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -805,6 +820,7 @@ async fn version_list(
|
||||
info,
|
||||
web::Query(filters),
|
||||
pool,
|
||||
ro_pool,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
@@ -816,6 +832,7 @@ pub async fn version_list_internal(
|
||||
info: web::Path<(String,)>,
|
||||
web::Query(filters): web::Query<VersionListFilters>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -955,9 +972,14 @@ pub async fn version_list_internal(
|
||||
});
|
||||
response.dedup_by(|a, b| a.inner.id == b.inner.id);
|
||||
|
||||
let mut response =
|
||||
filter_visible_versions(response, &user_option, &pool, &redis)
|
||||
.await?;
|
||||
let mut response = filter_visible_versions(
|
||||
response,
|
||||
&user_option,
|
||||
&pool,
|
||||
&ro_pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !filters.include_changelog {
|
||||
for version in &mut response {
|
||||
|
||||
@@ -70,7 +70,11 @@ impl TiltifyClient {
|
||||
}
|
||||
|
||||
if state.rate_limited_until > Instant::now() {
|
||||
return Err(eyre!("waiting for rate limit to reset"));
|
||||
return Err(eyre!(
|
||||
"waiting for rate limit to reset at {:.0?} (backoff {:.0?})",
|
||||
state.rate_limited_until,
|
||||
state.rate_limit_backoff
|
||||
));
|
||||
}
|
||||
|
||||
if ENV.TILTIFY_CLIENT_ID.is_empty()
|
||||
|
||||
@@ -198,13 +198,22 @@ impl PayoutSourceAlertType {
|
||||
threshold,
|
||||
current_balance,
|
||||
} => format!(
|
||||
"\u{1f6a8} *Payout Source Alert*\n\nPayout source '{source}' has an available balance under the ${threshold} threshold.\nBalance: ${current_balance}."
|
||||
"\u{1f6a8} *Payout Source Alert*
|
||||
|
||||
Payout source '{source}' has an available balance under the ${threshold} threshold.
|
||||
Balance: ${current_balance}."
|
||||
),
|
||||
PayoutSourceAlertType::CheckFailure {
|
||||
source,
|
||||
display_error,
|
||||
} => format!(
|
||||
"\u{1f6a8} *Payout Source Alert*\n\nFAILED TO CHECK payout source '{source}' balance.\nError: {display_error}"
|
||||
"\u{1f6a8} *Payout Source Alert*
|
||||
|
||||
Failed to check payout source '{source}' balance.
|
||||
Error:
|
||||
```
|
||||
{display_error}
|
||||
```"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ const authorLink = computed(() =>
|
||||
</section>
|
||||
<section id="changes">
|
||||
<h3 class="mt-0 mb-2 text-lg font-semibold">{{ formatMessage(messages.changes) }}</h3>
|
||||
<div class="p-4 bg-surface-3 rounded-2xl flex border-solid border border-surface-4">
|
||||
<div class="p-4 bg-surface-3 rounded-2xl border-solid border border-surface-4">
|
||||
<div
|
||||
v-if="version.changelog"
|
||||
class="markdown-body"
|
||||
|
||||
Reference in New Issue
Block a user