Merge branch 'main' of https://github.com/civitai/civitai into main

This commit is contained in:
Briant Diehl
2025-10-29 15:15:46 -06:00
10 changed files with 94 additions and 19 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "model-share",
"version": "5.0.1187",
"version": "5.0.1188",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "model-share",
"version": "5.0.1187",
"version": "5.0.1188",
"hasInstallScript": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.490.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "model-share",
"version": "5.0.1187",
"version": "5.0.1188",
"private": true,
"scripts": {
"start": "next start",
+15 -5
View File
@@ -27,10 +27,12 @@ export default function TosModal({
onAccepted,
slug,
fieldKey,
showBackButton = true,
}: {
onAccepted: () => Promise<void>;
slug: string;
fieldKey: keyof SetUserSettingsInput;
showBackButton?: boolean;
}) {
const dialog = useDialogContext();
const handleClose = dialog.onClose;
@@ -60,7 +62,6 @@ export default function TosModal({
const elementGaps = 16 * 3;
const dividerHeight = 2;
const reservedSpace = headerHeight + footerHeight + elementGaps + dividerHeight + 32;
console.log('Reserved space:', reservedSpace);
return Math.max(210, reservedSpace);
};
@@ -78,7 +79,14 @@ export default function TosModal({
};
return (
<Modal {...dialog} size="lg" withCloseButton={false} radius="md">
<Modal
{...dialog}
size="lg"
withCloseButton={false}
closeOnClickOutside={false}
closeOnEscape={false}
radius="md"
>
{isLoading || !data?.content ? (
<Center>
<Loader />
@@ -119,9 +127,11 @@ export default function TosModal({
size="sm"
/>
<Group ml="auto">
<Button onClick={handleClose} color="gray" disabled={updateUserSettings.isLoading}>
Go back
</Button>
{showBackButton && (
<Button onClick={handleClose} color="gray" disabled={updateUserSettings.isLoading}>
Go back
</Button>
)}
<Button
onClick={handleConfirm}
disabled={!acceptedCoC}
+1
View File
@@ -32,6 +32,7 @@ export function useToSUpdateModal() {
props: {
slug: 'tos',
fieldKey: tosUpdate.tosFieldKey || ('tosLastSeenDate' as const),
showBackButton: false,
onAccepted: async () => {
await currentUser.refresh();
// Use queryUtils to update the query data from trpc.content.checkTosUpdate
+7 -1
View File
@@ -12,7 +12,10 @@ import { dbRead, dbWrite } from '~/server/db/client';
import { imageTagsCache } from '~/server/redis/caches';
import { reportAcceptedReward } from '~/server/rewards';
import type { GetByIdInput } from '~/server/schema/base.schema';
import { getUserCollectionPermissionsById } from '~/server/services/collection.service';
import {
getUserCollectionPermissionsById,
removeEntityFromAllCollections,
} from '~/server/services/collection.service';
import {
isImageInQueue,
updatePendingImageRatings,
@@ -217,6 +220,9 @@ export const setTosViolationHandler = async ({
},
});
// Remove image from all collections
await removeEntityFromAllCollections('image', id);
if (image.pHash) await addBlockedImage({ hash: image.pHash, reason: BlockImageReason.TOS });
await queueImageSearchIndexUpdate({ ids: [id], action: SearchIndexUpdateQueueAction.Delete });
+10 -4
View File
@@ -95,7 +95,7 @@ export const deliverPrepaidMembershipBuzz = createJob(
toAccountId: d.userId,
toAccountType: (d.buzzType as any) ?? 'yellow', // Default to yellow if not specified
type: TransactionType.Purchase,
externalTransactionId: `civitai-membership:${date}:${d.userId}:${d.productId}`,
externalTransactionId: `civitai-membership:${date}:${d.userId}:${d.productId}:v2`,
amount: amount,
description: `Membership Bonus`,
details: {
@@ -118,6 +118,7 @@ export const deliverPrepaidMembershipBuzz = createJob(
const userData = batch.map((b) => ({
id: b.toAccountId,
tier: (b.details as any).tier,
externalTransactionId: b.externalTransactionId,
}));
// Decrement prepaid counts for each user who received buzz
@@ -128,9 +129,13 @@ export const deliverPrepaidMembershipBuzz = createJob(
UPDATE "CustomerSubscription"
SET
"metadata" = jsonb_set(
"metadata",
ARRAY['prepaids', (updates.data ->> 'tier')],
(COALESCE(("metadata"->'prepaids'->>(updates.data ->> 'tier'))::int, 0) - 1)::text::jsonb
jsonb_set(
"metadata",
ARRAY['prepaids', (updates.data ->> 'tier')],
(COALESCE(("metadata"->'prepaids'->>(updates.data ->> 'tier'))::int, 0) - 1)::text::jsonb
),
ARRAY['buzzTransactionIds'],
COALESCE("metadata"->'buzzTransactionIds', '[]'::jsonb) || jsonb_build_array(updates.data ->> 'externalTransactionId')
),
"updatedAt" = NOW()
FROM (
@@ -141,6 +146,7 @@ export const deliverPrepaidMembershipBuzz = createJob(
userData.map((d) => ({
id: d.id,
tier: d.tier,
externalTransactionId: d.externalTransactionId,
}))
)}::json)
) AS updates
@@ -46,4 +46,5 @@ export const subscriptionMetadata = z.looseObject({
renewalBonus: z.number().optional(),
prepaids: z.partialRecord(productTierSchema, z.number()).optional(),
proratedDays: z.partialRecord(productTierSchema, z.number()).optional(),
buzzTransactionIds: z.array(z.string()).optional(),
});
+28
View File
@@ -2461,3 +2461,31 @@ export async function randomizeCollectionItems(collectionId: number) {
AND ci."status" = 'ACCEPTED'
`;
}
export type CollectionEntityType = 'image' | 'model' | 'post' | 'article';
/**
* Removes an entity (image, model, post, or article) from all collections it's part of.
* This is called when an entity is deleted or marked as ToS violation.
*
* @param entityType - The type of entity ('image', 'model', 'post', 'article')
* @param entityId - The ID of the entity to remove from collections
*/
export async function removeEntityFromAllCollections(
entityType: CollectionEntityType,
entityId: number
) {
// Build the where clause based on entity type
const whereClause = {
imageId: entityType === 'image' ? entityId : undefined,
modelId: entityType === 'model' ? entityId : undefined,
postId: entityType === 'post' ? entityId : undefined,
articleId: entityType === 'article' ? entityId : undefined,
};
// Delete all collection items for this entity
// If entity is not in any collections, this is a no-op (0 rows affected)
await dbWrite.collectionItem.deleteMany({
where: whereClause,
});
}
+11 -1
View File
@@ -96,7 +96,10 @@ import type { ImageResourceHelperModel } from '~/server/selectors/image.selector
import { imageSelect } from '~/server/selectors/image.selector';
import type { ImageV2Model } from '~/server/selectors/imagev2.selector';
import { imageTagCompositeSelect, simpleTagSelect } from '~/server/selectors/tag.selector';
import { getUserCollectionPermissionsById } from '~/server/services/collection.service';
import {
getUserCollectionPermissionsById,
removeEntityFromAllCollections,
} from '~/server/services/collection.service';
import { getCosmeticsForEntity } from '~/server/services/cosmetic.service';
import { addImageToQueue } from '~/server/services/games/new-order.service';
import { upsertImageFlag } from '~/server/services/image-flag.service';
@@ -237,6 +240,9 @@ export const deleteImageById = async ({
}: GetByIdInput & { updatePost?: boolean }) => {
updatePost ??= true;
try {
// Remove image from all collections before deleting
await removeEntityFromAllCollections('image', id);
const image = await dbWrite.image.delete({
where: { id },
select: { url: true, postId: true, nsfwLevel: true, userId: true },
@@ -269,6 +275,10 @@ export const deleteImageById = async ({
export async function deleteImages(ids: number[], updatePosts = true) {
const images = await Limiter({ batchSize: 100 }).process(ids, async (ids, batchIndex) => {
// Remove images from all collections before deleting
// Note: Since we're using raw SQL delete, Prisma cascades won't trigger automatically
await Promise.all(ids.map((id) => removeEntityFromAllCollections('image', id)));
const results = await dbWrite.$queryRaw<
{ id: number; url: string; postId: number | null; nsfwLevel: number; userId: number }[]
>`
+18 -5
View File
@@ -193,6 +193,10 @@ export async function consumeRedeemableCode({
.metadata as SubscriptionProductMetadata;
const consumedProductTier = consumedProductMetadata.tier ?? 'free';
// Calculate external transaction ID for buzz delivery (to be stored in metadata)
const date = dayjs().format('YYYY-MM');
const externalTransactionId = `civitai-membership:${date}:${userId}:${consumedCode.price.product.id}:${consumedCode.code}`;
if (userMembership) {
// Check states:
if (userMembership.status !== 'active') {
@@ -261,6 +265,10 @@ export async function consumeRedeemableCode({
(subscriptionMetadata.prepaids?.[consumedProductTier] ?? 0) +
consumedCode.unitValue,
},
buzzTransactionIds: [
...(subscriptionMetadata.buzzTransactionIds ?? []),
externalTransactionId,
],
},
status: 'active',
currentPeriodEnd: dayjs(activeUserMembership.currentPeriodEnd)
@@ -309,6 +317,10 @@ export async function consumeRedeemableCode({
membershipProductMetadata.tier ?? 'free'
] ?? 0) + Math.max(0, proratedDays),
},
buzzTransactionIds: [
...(subscriptionMetadata.buzzTransactionIds ?? []),
externalTransactionId,
],
},
},
});
@@ -326,6 +338,10 @@ export async function consumeRedeemableCode({
(subscriptionMetadata.prepaids?.[consumedProductTier] ?? 0) +
consumedCode.unitValue,
},
buzzTransactionIds: [
...(subscriptionMetadata.buzzTransactionIds ?? []),
externalTransactionId,
],
},
},
});
@@ -340,6 +356,7 @@ export async function consumeRedeemableCode({
prepaids: {
[consumedProductTier]: consumedCode.unitValue - 1, // -1 because we grant buzz right away
},
buzzTransactionIds: [externalTransactionId],
};
await tx.customerSubscription.create({
@@ -364,8 +381,6 @@ export async function consumeRedeemableCode({
});
}
const date = dayjs().format('YYYY-MM');
await withRetries(async () => {
// Grant buzz right away:
await createBuzzTransaction({
@@ -373,9 +388,7 @@ export async function consumeRedeemableCode({
toAccountId: userId,
toAccountType: (consumedProductMetadata.buzzType as any) ?? 'yellow', // Default to yellow if not specified
type: TransactionType.Purchase,
externalTransactionId: `civitai-membership:${date}:${userId}:${
consumedCode.price!.product.id
}:${consumedCode.code}`,
externalTransactionId: externalTransactionId,
amount: Number(consumedProductMetadata.monthlyBuzz ?? 5000), // Default to 5000 if not specified
description: `Membership bonus`,
details: {