add AI metadata checking to block image uploads with notice

This commit is contained in:
Prospector
2026-08-07 17:30:58 -07:00
parent 8ab484a298
commit d8e53cf7dc
12 changed files with 226 additions and 31 deletions
@@ -0,0 +1,52 @@
<template>
<NewModal
ref="modal"
:header="formatMessage(messages.title)"
:closable="false"
fade="warning"
max-width="544px"
>
<div class="flex flex-col">
<p class="mb-4 mt-0 leading-normal">
{{ formatMessage(messages.body) }}
</p>
<div class="flex justify-end">
<Button type="colored" color="brand" native-type="button" @click="hide">
<CheckCircleIcon class="size-4" />
{{ formatMessage(commonMessages.iUnderstandButton) }}
</Button>
</div>
</div>
</NewModal>
</template>
<script setup lang="ts">
import { CheckCircleIcon } from '@modrinth/assets'
import { Button, commonMessages, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
import { useTemplateRef } from 'vue'
const { formatMessage } = useVIntl()
const modal = useTemplateRef('modal')
const messages = defineMessages({
title: {
id: 'project.ai-image-warning-modal.title',
defaultMessage: 'AI-generated images not allowed',
},
body: {
id: 'project.ai-image-warning-modal.body',
defaultMessage:
'Using AI-generated images to represent your project is not allowed. Attempting to work around detection may lead to your Modrinth account being suspended.',
},
})
function show() {
modal.value?.show()
}
function hide() {
modal.value?.hide()
}
defineExpose({ show, hide })
</script>
+41
View File
@@ -0,0 +1,41 @@
import type { C2paSdk } from '@contentauth/c2pa-web'
import { createC2pa, isSupportedReaderFormat } from '@contentauth/c2pa-web/inline'
// https://cv.iptc.org/newscodes/digitalsourcetype/
// only detecting generative AI types
const AI_DIGITAL_SOURCE_TYPES = [
'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia',
'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicData',
'http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia',
'http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic',
]
let c2paInstance: Promise<C2paSdk> | null = null
function getC2pa() {
if (!c2paInstance) {
c2paInstance = createC2pa()
}
return c2paInstance
}
// reference: https://github.com/contentauth/c2pa-js/blob/main/packages/c2pa-web/README.md
export async function fileDeclaresAi(file: File): Promise<boolean> {
try {
if (!isSupportedReaderFormat(file.type)) {
return false
}
const c2pa = await getC2pa()
const reader = await c2pa.reader.fromBlob(file.type, file)
const manifestStore = await reader.manifestStore()
await reader.free()
const manifestJson = JSON.stringify(manifestStore)
return AI_DIGITAL_SOURCE_TYPES.some((type) => manifestJson.includes(type))
} catch {
return false
}
}
@@ -3485,6 +3485,12 @@
"project.actions.servers-promo.title": {
"message": "Create a server"
},
"project.ai-image-warning-modal.body": {
"message": "Using AI-generated images to represent your project is not allowed. Attempting to work around detection may lead to your Modrinth account being suspended."
},
"project.ai-image-warning-modal.title": {
"message": "AI-generated images not allowed"
},
"project.collections.create-new": {
"message": "Create new collection"
},
@@ -1,5 +1,6 @@
<template>
<div>
<AiImageWarningModal ref="aiImageWarningModal" />
<Modal
v-if="currentMember"
ref="modal_edit_item"
@@ -18,12 +19,7 @@
:accept="acceptFileTypes"
:max-size="5242880"
aria-label="Replace image"
@change="
(x) => {
editFile = x[0]
showPreviewImage()
}
"
@change="replaceEditFile"
>
<TransferIcon aria-hidden="true" />
</FileButton>
@@ -315,6 +311,8 @@ import {
} from '@modrinth/ui'
import { useEventListener } from '@vueuse/core'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
import { fileDeclaresAi } from '~/helpers/c2pa'
import { isPermission } from '~/utils/permissions.ts'
const formatDate = useFormatDateTime({
@@ -333,6 +331,7 @@ const {
} = injectProjectPageContext()
// Template refs
const aiImageWarningModal = useTemplateRef('aiImageWarningModal')
const modalEditItem = useTemplateRef('modal_edit_item')
const modalConfirm = useTemplateRef('modal_confirm')
@@ -439,14 +438,35 @@ function resetEdit() {
previewImage.value = null
}
function handleFiles(files: File[]) {
async function handleFiles(files: File[]) {
const file = files[0]
if (!file) {
return
}
if (await fileDeclaresAi(file)) {
aiImageWarningModal.value?.show()
return
}
resetEdit()
editFile.value = files[0]
editFile.value = file
showPreviewImage()
modalEditItem.value?.show()
}
async function replaceEditFile(files: File[]) {
const file = files[0]
if (!file) {
return
}
if (await fileDeclaresAi(file)) {
aiImageWarningModal.value?.show()
return
}
editFile.value = file
showPreviewImage()
}
function showPreviewImage() {
const reader = new FileReader()
if (editFile.value instanceof Blob) {
@@ -1,5 +1,6 @@
<template>
<div>
<AiImageWarningModal ref="aiImageWarningModal" />
<ConfirmLeaveModal ref="confirmLeaveModal" />
<div class="universal-card">
<div class="markdown-disclaimer">
@@ -51,11 +52,14 @@ import {
useSavable,
} from '@modrinth/ui'
import { TeamMemberPermission } from '@modrinth/utils'
import { computed } from 'vue'
import { computed, useTemplateRef } from 'vue'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
import { useImageUpload } from '~/composables/image-upload.ts'
import { fileDeclaresAi } from '~/helpers/c2pa'
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
const aiImageWarningModal = useTemplateRef('aiImageWarningModal')
useProjectSettingsHeadTitle(commonProjectSettingsMessages.description)
@@ -80,6 +84,10 @@ const descriptionWarning = computed(() => {
})
async function onUploadHandler(file: File) {
if (await fileDeclaresAi(file)) {
aiImageWarningModal.value?.show()
return
}
const response = await useImageUpload(file, {
context: 'project',
projectID: project.value.id,
@@ -1,5 +1,6 @@
<template>
<div>
<AiImageWarningModal ref="aiImageWarningModal" />
<Modal
v-if="currentMember"
ref="modal_edit_item"
@@ -18,12 +19,7 @@
:accept="acceptFileTypes"
:max-size="5242880"
aria-label="Replace image"
@change="
(x) => {
editFile = x[0]
showPreviewImage()
}
"
@change="replaceEditFile"
>
<TransferIcon aria-hidden="true" />
</FileButton>
@@ -305,6 +301,8 @@ import {
useFormatDateTime,
} from '@modrinth/ui'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
import { fileDeclaresAi } from '~/helpers/c2pa'
import { isPermission } from '~/utils/permissions.ts'
const formatDate = useFormatDateTime({
@@ -323,6 +321,7 @@ const {
useProjectSettingsHeadTitle(commonProjectSettingsMessages.gallery)
const aiImageWarningModal = ref(null)
const modal_edit_item = ref(null)
const modal_confirm = ref(null)
@@ -380,14 +379,35 @@ const resetEdit = () => {
previewImage.value = null
}
const handleFiles = (files) => {
const handleFiles = async (files) => {
const file = files[0]
if (!file) {
return
}
if (await fileDeclaresAi(file)) {
aiImageWarningModal.value?.show()
return
}
resetEdit()
editFile.value = files[0]
editFile.value = file
showPreviewImage()
modal_edit_item.value.show()
}
const replaceEditFile = async (files) => {
const file = files[0]
if (!file) {
return
}
if (await fileDeclaresAi(file)) {
aiImageWarningModal.value?.show()
return
}
editFile.value = file
showPreviewImage()
}
const showPreviewImage = () => {
const reader = new FileReader()
if (editFile.value instanceof Blob) {
@@ -1,5 +1,6 @@
<template>
<div>
<AiImageWarningModal ref="aiImageWarningModal" />
<ConfirmModal
ref="modal_confirm"
:title="formatMessage(messages.deleteConfirmationTitle)"
@@ -322,7 +323,9 @@ import {
} from '@modrinth/ui'
import { fileIsValid, formatProjectStatus } from '@modrinth/utils'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
import { useAuth } from '~/composables/auth.js'
import { fileDeclaresAi } from '~/helpers/c2pa'
import { getProjectTypeForUrl } from '~/helpers/projects.js'
const auth = await useAuth()
@@ -337,6 +340,7 @@ const {
invalidate,
} = injectProjectPageContext()
const { labrinth } = injectModrinthClient()
const aiImageWarningModal = useTemplateRef('aiImageWarningModal')
useProjectSettingsHeadTitle(commonProjectSettingsMessages.general)
@@ -553,9 +557,17 @@ async function handleSave() {
}
}
const showPreviewImage = (files) => {
const showPreviewImage = async (files) => {
const file = files[0]
if (!file) {
return
}
if (await fileDeclaresAi(file)) {
aiImageWarningModal.value?.show()
return
}
const reader = new FileReader()
icon.value = files[0]
icon.value = file
deletedIcon.value = false
reader.readAsDataURL(icon.value)
reader.onload = (event) => {