mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
add AI metadata checking to block image uploads with notice
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
"wrangler": "^4.115.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@contentauth/c2pa-web": "^0.13.1",
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"@ltd/j-toml": "^1.38.0",
|
||||
"@modrinth/api-client": "workspace:*",
|
||||
|
||||
@@ -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>
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user