mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
Merge branch 'main' of https://github.com/civitai/civitai into main
This commit is contained in:
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "model-share",
|
||||
"version": "5.0.1187",
|
||||
"version": "5.0.1188",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "next start",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 }[]
|
||||
>`
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user