Files
modrinth/packages/moderation/src/data/nags/links.ts
T
2026-08-27 13:39:50 -06:00

296 lines
9.1 KiB
TypeScript

import { defineMessage, formatProjectTypeSentence, useVIntl } from '@modrinth/ui'
import type { Nag, NagContext } from '../../types/nags'
import { licenseRequiresSource, notSourceAsDistributed } from '../../utils'
import {
getBlockedProjectExternalLink,
getLinkHostname,
isCommonProjectLink,
isDiscordLink,
isInappropriateLicenseLink,
} from '../../validators/links'
export function findBlockedProjectExternalLink(context: Pick<NagContext, 'project' | 'projectV3'>) {
const urls = [
context.project.source_url,
context.project.issues_url,
context.project.wiki_url,
context.project.discord_url,
context.project.license.url,
...(context.project.donation_urls ?? []).map(({ url }) => url),
...Object.values(context.projectV3?.link_urls ?? {}).map(({ url }) => url),
]
for (const url of urls) {
if (!url) continue
const blockedLink = getBlockedProjectExternalLink(url)
if (blockedLink) return blockedLink
}
return null
}
export const linksNags: Nag[] = [
{
id: 'add-links',
title: defineMessage({
id: 'nags.add-links.title',
defaultMessage: 'Add external links',
}),
description: defineMessage({
id: 'nags.add-links.description',
defaultMessage:
'Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) => {
return (
!context.projectV3?.minecraft_server &&
Object.keys(context.projectV3?.link_urls ?? {}).length === 0
)
},
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings-links',
},
},
{
id: 'add-links-server',
title: defineMessage({
id: 'nags.add-links-server.title',
defaultMessage: 'Add external links',
}),
description: defineMessage({
id: 'nags.add-links-server.description',
defaultMessage:
'Add any relevant links targeted outside of Modrinth, such as a website, store, or a Discord invite.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) => {
return (
!!context.projectV3?.minecraft_server &&
Object.keys(context.projectV3?.link_urls ?? {}).length === 0
)
},
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings-links',
},
},
{
id: 'identical-links',
title: defineMessage({
id: 'nags.identical-links.title',
defaultMessage: 'Clean up identical links',
}),
description: defineMessage({
id: 'nags.identical-links.description',
defaultMessage:
'Some of your external links appear to be identical. Each link should be entered only once and with the appropriate link type.',
}),
status: 'required',
shouldShow: (context: NagContext) =>
new Set(Object.values(context.projectV3?.link_urls ?? {}).map((link) => link.url)).size !==
Object.values(context.projectV3?.link_urls ?? {}).map((link) => link.url).length,
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings-links',
},
},
{
id: 'verify-external-links',
title: defineMessage({
id: 'nags.verify-external-links.title',
defaultMessage: 'Verify external links',
}),
description: defineMessage({
id: 'nags.verify-external-links.description',
defaultMessage:
'Some of your external links may be using domains that are inappropriate for that type of link.',
}),
status: 'warning',
shouldShow: (context: NagContext) => {
const sourceUrl = context.project.source_url
const issuesUrl = context.project.issues_url
const discordUrl = context.project.discord_url
return (
(!!sourceUrl && !isCommonProjectLink(sourceUrl, 'source')) ||
(!!issuesUrl && !isCommonProjectLink(issuesUrl, 'issues')) ||
(!!discordUrl && !isCommonProjectLink(discordUrl, 'discord'))
)
},
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.visit-links-settings.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings-links',
},
},
{
id: 'misused-discord-link',
title: defineMessage({
id: 'nags.misused-discord-link.title',
defaultMessage: 'Move Discord invite',
}),
description: defineMessage({
id: 'nags.misused-discord-link-description',
defaultMessage:
'Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only.',
}),
status: 'required',
shouldShow: (context: NagContext) =>
isDiscordLink(context.project.source_url) ||
isDiscordLink(context.project.issues_url) ||
isDiscordLink(context.project.wiki_url) ||
isDiscordLink(context.projectV3?.link_urls?.site?.url) ||
isDiscordLink(context.projectV3?.link_urls?.store?.url),
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.visit-links-settings.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings-links',
},
},
{
id: 'banned-link-usage',
title: defineMessage({
id: 'nags.banned-link-usage.title',
defaultMessage: 'Remove prohibited links',
}),
description: (context: NagContext) => {
const blockedLink = findBlockedProjectExternalLink(context)
if (!blockedLink) return ''
const { formatMessage } = useVIntl()
return formatMessage(
defineMessage({
id: 'nags.banned-link-usage.description',
defaultMessage: '“{url}” is not allowed in project links.',
}),
blockedLink,
)
},
status: 'required',
shouldShow: (context: NagContext) => findBlockedProjectExternalLink(context) !== null,
},
{
id: 'invalid-license-url',
title: defineMessage({
id: 'nags.invalid-license-url.title',
defaultMessage: 'Add a valid license link',
}),
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const licenseUrl = context.project.license.url
if (!licenseUrl) {
return formatMessage(
defineMessage({
id: 'nags.invalid-license-url.description.default',
defaultMessage: 'License URL is invalid.',
}),
)
}
const domain = getLinkHostname(licenseUrl)
if (domain) {
return formatMessage(
defineMessage({
id: 'nags.invalid-license-url.description.domain',
defaultMessage:
'Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc.',
}),
{ domain },
)
}
return formatMessage(
defineMessage({
id: 'nags.invalid-license-url.description.malformed',
defaultMessage:
'Your license URL appears to be malformed. Please provide a valid URL to your license text.',
}),
)
},
status: 'required',
shouldShow: (context: NagContext) => {
const licenseUrl = context.project.license.url
if (!licenseUrl) return false
return getLinkHostname(licenseUrl) === null || isInappropriateLicenseLink(licenseUrl)
},
link: {
path: 'settings',
title: defineMessage({
id: 'nags.edit-license.title',
defaultMessage: 'Edit license',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings',
},
},
{
id: 'gpl-license-source-required',
title: defineMessage({
id: 'nags.gpl-license-source-required.title',
defaultMessage: 'Provide source code',
}),
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
return formatMessage(
defineMessage({
id: 'nags.gpl-license-source-required.description',
defaultMessage:
'Your {type} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license.',
}),
{
type: formatProjectTypeSentence(formatMessage, context.project.project_type),
},
)
},
status: 'required',
shouldShow: (context: NagContext) => {
if (context.projectV3.project_types.includes('datapack')) return false
const hasSourceUrl = !!context.project.source_url
const hasAdditionalFiles = (context: NagContext) => {
let hasAdditional = true
context.versions.forEach((version) => {
if (version.files.length < 2) hasAdditional = false
})
return hasAdditional
}
return (
licenseRequiresSource(context.projectV3.license.id) &&
notSourceAsDistributed(context.projectV3.project_types) &&
!hasSourceUrl &&
!hasAdditionalFiles(context)
)
},
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.visit-links-settings.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-project-settings-links',
},
},
]