add AI report types + fix report page fetching (#7175)

* add AI report types + fix report page fetching

* use auth middleware on report
This commit is contained in:
Prospector
2026-08-16 23:55:14 -07:00
committed by GitHub
parent 65df8b90b8
commit 248c7bb217
4 changed files with 150 additions and 107 deletions
+9 -3
View File
@@ -4322,12 +4322,21 @@
"report.not-for.dmca.description": { "report.not-for.dmca.description": {
"message": "See our <policy-link>Copyright Policy</policy-link>." "message": "See our <policy-link>Copyright Policy</policy-link>."
}, },
"report.note.ai-images.1": {
"message": "We've just updated our rules to prohibit AI-generated images in icons, galleries, and descriptions. We're giving creators a grace period to remove AI images from their projects before we begin accepting reports for them."
},
"report.note.check-back-later": {
"message": "Check back later once the grace period has ended. Reports of this type are not being accepted at this time."
},
"report.note.copyright.1": { "report.note.copyright.1": {
"message": "Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content." "message": "Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content."
}, },
"report.note.copyright.2": { "report.note.copyright.2": {
"message": "If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>." "message": "If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>."
}, },
"report.note.fully-ai-generated.1": {
"message": "We've just updated our rules to prohibit fully AI-generated projects. We're giving creators a grace period to update or take down fully AI-generated projects before we begin accepting reports for them."
},
"report.note.malicious.1": { "report.note.malicious.1": {
"message": "Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples." "message": "Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples."
}, },
@@ -4337,9 +4346,6 @@
"report.note.missing-disclosure.1": { "report.note.missing-disclosure.1": {
"message": "Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures." "message": "Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures."
}, },
"report.note.missing-disclosure.2": {
"message": "Check back later once the grace period has ended. Reports of this type are not being accepted at this time."
},
"report.please-report": { "report.please-report": {
"message": "Please report:" "message": "Please report:"
}, },
+127 -104
View File
@@ -109,17 +109,7 @@
<span class="text-lg font-semibold text-contrast"> <span class="text-lg font-semibold text-contrast">
{{ formatMessage(messages.whatContentType) }} {{ formatMessage(messages.whatContentType) }}
</span> </span>
<RadioButtons <RadioButtons v-slot="{ item }" v-model="reportItem" :items="reportItems">
v-slot="{ item }"
v-model="reportItem"
:items="reportItems"
@update:model-value="
() => {
prefilled = false
fetchItem()
}
"
>
{{ capitalizeString(item) }} {{ capitalizeString(item) }}
</RadioButtons> </RadioButtons>
</div> </div>
@@ -139,13 +129,6 @@
autocomplete="off" autocomplete="off"
:disabled="reportItem === ''" :disabled="reportItem === ''"
wrapper-class="w-40" wrapper-class="w-40"
@blur="
() => {
prefilled = false
reportItemID = reportItemID.trim()
fetchItem()
}
"
/> />
<div v-if="checkingId || checkedId" class="flex items-center gap-1"> <div v-if="checkingId || checkedId" class="flex items-center gap-1">
<template v-if="checkingId"> <template v-if="checkingId">
@@ -163,8 +146,8 @@
class="flex items-center gap-1 font-semibold text-contrast hover:underline" class="flex items-center gap-1 font-semibold text-contrast hover:underline"
> >
<Avatar <Avatar
v-if="typeof itemIcon === 'string'" v-if="typeof itemIcon === 'string' || !itemIcon"
:src="itemIcon" :src="itemIcon ?? null"
:alt="itemName" :alt="itemName"
size="24px" size="24px"
:circle="reportItem === 'user'" :circle="reportItem === 'user'"
@@ -303,22 +286,22 @@ import {
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { Project, Report, User, Version } from '@modrinth/utils' import type { Project, Report, User, Version } from '@modrinth/utils'
import { useDebounceFn } from '@vueuse/core'
import { useImageUpload } from '~/composables/image-upload.ts' import { useImageUpload } from '~/composables/image-upload.ts'
definePageMeta({
middleware: 'auth',
})
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const tags = useGeneratedState() const tags = useGeneratedState()
const route = useNativeRoute() const route = useNativeRoute()
const router = useRouter()
const auth = await useAuth() const auth = await useAuth()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
if (!auth.value.user) {
router.push('/auth/sign-in?redirect=' + encodeURIComponent(route.fullPath))
}
const accessQuery = (id: string): string => { const accessQuery = (id: string): string => {
return route.query?.[id]?.toString() || '' return route.query?.[id]?.toString() || ''
} }
@@ -368,58 +351,83 @@ async function fetchExistingReports() {
) )
} }
async function fetchItem() { const fetchItemDebounced = useDebounceFn(fetchItem, 500)
if (reportItem.value && reportItemID.value) {
checkingId.value = true
itemIcon.value = undefined
itemName.value = undefined
itemLink.value = undefined
itemId.value = undefined
itemIssueTracker.value = undefined
try {
if (reportItem.value === 'project') {
const project = (await useBaseFetch(`project/${reportItemID.value}`)) as Project
currentProject.value = project
itemIcon.value = project.icon_url watch([reportItem, reportItemID], ([type, id], [prevType]) => {
itemName.value = project.title prefilled.value = false
itemLink.value = `/project/${project.id}` if (!id.trim() || type !== prevType) {
itemId.value = project.id fetchItem()
itemIssueTracker.value = project.issues_url } else {
} else if (reportItem.value === 'version') { fetchItemDebounced()
const version = (await useBaseFetch(`version/${reportItemID.value}`)) as Version
currentVersion.value = version
itemIcon.value = VersionIcon
itemName.value = version.version_number
itemLink.value = `project/${version.project_id}/version/${version.id}`
itemId.value = version.id
} else if (reportItem.value === 'user') {
const user = (await useBaseFetch(`user/${reportItemID.value}`)) as User
currentUser.value = user
itemIcon.value = user.avatar_url
itemName.value = user.username
itemLink.value = `/user/${user.username}`
itemId.value = user.id
}
} catch {
// Ignored
}
checkedId.value = true
checkingId.value = false
} }
})
async function fetchItem() {
const type = reportItem.value
const id = reportItemID.value.trim()
currentProject.value = null
currentVersion.value = null
currentUser.value = null
itemIcon.value = undefined
itemName.value = undefined
itemLink.value = undefined
itemId.value = undefined
itemIssueTracker.value = undefined
if (!type || !id) {
checkedId.value = false
checkingId.value = false
return
}
checkingId.value = true
try {
if (type === 'project') {
const project = (await useBaseFetch(`project/${id}`)) as Project
currentProject.value = project
itemIcon.value = project.icon_url
itemName.value = project.title
itemLink.value = `/project/${project.id}`
itemId.value = project.id
itemIssueTracker.value = project.issues_url
} else if (type === 'version') {
const version = (await useBaseFetch(`version/${id}`)) as Version
currentVersion.value = version
itemIcon.value = VersionIcon
itemName.value = version.version_number
itemLink.value = `project/${version.project_id}/version/${version.id}`
itemId.value = version.id
} else if (type === 'user') {
const user = (await useBaseFetch(`user/${id}`)) as User
currentUser.value = user
itemIcon.value = user.avatar_url
itemName.value = user.username
itemLink.value = `/user/${user.username}`
itemId.value = user.id
}
} catch {
// do nothing
}
checkedId.value = true
checkingId.value = false
} }
const reportItems = ['project', 'version', 'user'] const reportItems = ['project', 'version', 'user']
const dummyProjectReportTypes = ['missing-disclosure', 'ai-images', 'fully-ai-generated'] as const
const reportTypes = computed(() => { const reportTypes = computed(() => {
const types = [...tags.value.reportTypes] const types = [...tags.value.reportTypes]
if (reportItem.value === 'project') { if (reportItem.value === 'project') {
types.push('missing-disclosure') const otherIndex = types.indexOf('other')
if (otherIndex === -1) {
types.push(...dummyProjectReportTypes)
} else {
types.splice(otherIndex, 0, ...dummyProjectReportTypes)
}
} }
return types return types
}) })
const disableReporting = computed(() => reportType.value === 'missing-disclosure') const disableReporting = computed(() => dummyProjectReportTypes.includes(reportType.value))
const canSubmit = computed(() => { const canSubmit = computed(() => {
return ( return (
@@ -531,44 +539,6 @@ const onImageUpload = async (file: File) => {
return item.url return item.url
} }
const warnings: Record<string, MessageDescriptor[]> = {
copyright: [
defineMessage({
id: 'report.note.copyright.1',
defaultMessage:
'Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content.',
}),
defineMessage({
id: 'report.note.copyright.2',
defaultMessage:
'If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>.',
}),
],
malicious: [
defineMessage({
id: 'report.note.malicious.1',
defaultMessage:
'Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples.',
}),
defineMessage({
id: 'report.note.malicious.2',
defaultMessage:
'Summaries from Microsoft Defender, VirusTotal, or AI malware detection are not sufficient forms of evidence and will not be accepted.',
}),
],
'missing-disclosure': [
defineMessage({
id: 'report.note.missing-disclosure.1',
defaultMessage: `Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures.`,
}),
defineMessage({
id: 'report.note.missing-disclosure.2',
defaultMessage:
'Check back later once the grace period has ended. Reports of this type are not being accepted at this time.',
}),
],
}
const messages = defineMessages({ const messages = defineMessages({
reportContent: { reportContent: {
id: 'report.report-content', id: 'report.report-content',
@@ -662,7 +632,60 @@ const messages = defineMessages({
id: 'report.submit', id: 'report.submit',
defaultMessage: 'Submit report', defaultMessage: 'Submit report',
}, },
checkBackLater: {
id: 'report.note.check-back-later',
defaultMessage:
'Check back later once the grace period has ended. Reports of this type are not being accepted at this time.',
},
}) })
const warnings: Record<string, MessageDescriptor[]> = {
copyright: [
defineMessage({
id: 'report.note.copyright.1',
defaultMessage:
'Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content.',
}),
defineMessage({
id: 'report.note.copyright.2',
defaultMessage:
'If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>.',
}),
],
malicious: [
defineMessage({
id: 'report.note.malicious.1',
defaultMessage:
'Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples.',
}),
defineMessage({
id: 'report.note.malicious.2',
defaultMessage:
'Summaries from Microsoft Defender, VirusTotal, or AI malware detection are not sufficient forms of evidence and will not be accepted.',
}),
],
'missing-disclosure': [
defineMessage({
id: 'report.note.missing-disclosure.1',
defaultMessage: `Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures.`,
}),
messages.checkBackLater,
],
'ai-images': [
defineMessage({
id: 'report.note.ai-images.1',
defaultMessage: `We've just updated our rules to prohibit AI-generated images in icons, galleries, and descriptions. We're giving creators a grace period to remove AI images from their projects before we begin accepting reports for them.`,
}),
messages.checkBackLater,
],
'fully-ai-generated': [
defineMessage({
id: 'report.note.fully-ai-generated.1',
defaultMessage: `We've just updated our rules to prohibit fully AI-generated projects. We're giving creators a grace period to update or take down fully AI-generated projects before we begin accepting reports for them.`,
}),
messages.checkBackLater,
],
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+6
View File
@@ -3995,9 +3995,15 @@
"report.item-type.version": { "report.item-type.version": {
"defaultMessage": "version" "defaultMessage": "version"
}, },
"report.type.ai-images": {
"defaultMessage": "AI-generated images in icon, gallery, or description"
},
"report.type.copyright": { "report.type.copyright": {
"defaultMessage": "Reuploaded work" "defaultMessage": "Reuploaded work"
}, },
"report.type.fully-ai-generated": {
"defaultMessage": "Fully AI-generated project"
},
"report.type.inappropriate": { "report.type.inappropriate": {
"defaultMessage": "Inappropriate" "defaultMessage": "Inappropriate"
}, },
+8
View File
@@ -1049,6 +1049,14 @@ export const reportTypeMessages = defineMessages({
id: 'report.type.missing-disclosure', id: 'report.type.missing-disclosure',
defaultMessage: 'Missing or incorrect disclosure', defaultMessage: 'Missing or incorrect disclosure',
}, },
'ai-images': {
id: 'report.type.ai-images',
defaultMessage: 'AI-generated images in icon, gallery, or description',
},
'fully-ai-generated': {
id: 'report.type.fully-ai-generated',
defaultMessage: 'Fully AI-generated project',
},
}) })
export function formatReportType(formatMessage: FormatMessage, type: string | undefined): string { export function formatReportType(formatMessage: FormatMessage, type: string | undefined): string {