You've already forked AstralRinth
forked from didirus/AstralRinth
include moderation pkg in frontend locales (#4169)
* include moderation pkg in frontend locales * Fix moderation lib path * remove prints * feat: move moderation package into src folder * fix: lint --------- Co-authored-by: IMB11 <calum@modrinth.com> Co-authored-by: Cal H. <hendersoncal117@gmail.com>
This commit is contained in:
32
packages/moderation/src/data/checklist.ts
Normal file
32
packages/moderation/src/data/checklist.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Stage } from '../types/stage'
|
||||
import modpackPermissionsStage from './modpack-permissions-stage'
|
||||
import categories from './stages/categories'
|
||||
import reupload from './stages/reupload'
|
||||
import description from './stages/description'
|
||||
import gallery from './stages/gallery'
|
||||
import links from './stages/links'
|
||||
import ruleFollowing from './stages/rule-following'
|
||||
import sideTypes from './stages/side-types'
|
||||
import summary from './stages/summary'
|
||||
import titleSlug from './stages/title-slug'
|
||||
import versions from './stages/versions'
|
||||
import license from './stages/license'
|
||||
import undefinedProject from './stages/undefined-project'
|
||||
import statusAlerts from './stages/status-alerts'
|
||||
|
||||
export default [
|
||||
titleSlug,
|
||||
summary,
|
||||
description,
|
||||
links,
|
||||
license,
|
||||
categories,
|
||||
sideTypes,
|
||||
gallery,
|
||||
versions,
|
||||
reupload,
|
||||
ruleFollowing,
|
||||
modpackPermissionsStage,
|
||||
statusAlerts,
|
||||
undefinedProject,
|
||||
] as ReadonlyArray<Stage>
|
||||
45
packages/moderation/src/data/keybinds.ts
Normal file
45
packages/moderation/src/data/keybinds.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { KeybindListener } from '../types/keybinds'
|
||||
|
||||
const keybinds: KeybindListener[] = [
|
||||
{
|
||||
id: 'next-stage',
|
||||
keybind: 'ArrowRight',
|
||||
description: 'Go to next stage',
|
||||
enabled: (ctx) => !ctx.state.isDone && !ctx.state.hasGeneratedMessage,
|
||||
action: (ctx) => ctx.actions.tryGoNext(),
|
||||
},
|
||||
{
|
||||
id: 'previous-stage',
|
||||
keybind: 'ArrowLeft',
|
||||
description: 'Go to previous stage',
|
||||
enabled: (ctx) => !ctx.state.isDone && !ctx.state.hasGeneratedMessage,
|
||||
action: (ctx) => ctx.actions.tryGoBack(),
|
||||
},
|
||||
{
|
||||
id: 'generate-message',
|
||||
keybind: 'Ctrl+Shift+E',
|
||||
description: 'Generate moderation message',
|
||||
action: (ctx) => ctx.actions.tryGenerateMessage(),
|
||||
},
|
||||
{
|
||||
id: 'toggle-collapse',
|
||||
keybind: 'Shift+C',
|
||||
description: 'Toggle collapse/expand',
|
||||
action: (ctx) => ctx.actions.tryToggleCollapse(),
|
||||
},
|
||||
{
|
||||
id: 'reset-progress',
|
||||
keybind: 'Ctrl+Shift+R',
|
||||
description: 'Reset moderation progress',
|
||||
action: (ctx) => ctx.actions.tryResetProgress(),
|
||||
},
|
||||
{
|
||||
id: 'skip-project',
|
||||
keybind: 'Ctrl+Shift+S',
|
||||
description: 'Skip to next project',
|
||||
enabled: (ctx) => ctx.state.futureProjectCount > 0 && !ctx.state.isDone,
|
||||
action: (ctx) => ctx.actions.trySkipProject(),
|
||||
},
|
||||
]
|
||||
|
||||
export default keybinds
|
||||
@@ -0,0 +1,3 @@
|
||||
## Misuse of Tags
|
||||
|
||||
Per section 5.1 of %RULES%, it is important that the metadata of your projects is accurate. Including that selected tags honestly represent your project.
|
||||
@@ -0,0 +1 @@
|
||||
It looks like the Optimization tag is not accurate for this project.
|
||||
@@ -0,0 +1,5 @@
|
||||
Currently, your pack has multiple Resolutions selected, in most cases, this will misrepresent your project.
|
||||
For a brief rundown of how this works:
|
||||
Vanilla Minecraft textures like blocks and items have a width and height of 16 pixels.
|
||||
This means that packs with textures the same size as the default are considered 16x.
|
||||
Some packs make the resolution of textures smaller than the default, such as 8x, and some bigger, such as 32x.
|
||||
@@ -0,0 +1,2 @@
|
||||
**Featured Tags:** %PROJECT_CATEGORIES% \
|
||||
**Additional Tags:** %PROJECT_ADDITIONAL_CATEGORIES%
|
||||
@@ -0,0 +1,2 @@
|
||||
**License id:** %PROJECT_LICENSE_ID% \
|
||||
**License Link:** %PROJECT_LICENSE_URL%
|
||||
@@ -0,0 +1,4 @@
|
||||
**Issues:** %PROJECT_ISSUES_URL% \
|
||||
**Source:** %PROJECT_SOURCE_URL% \
|
||||
**Wiki:** %PROJECT_WIKI_URL% \
|
||||
**Discord:** %PROJECT_DISCORD_URL%
|
||||
@@ -0,0 +1 @@
|
||||
> **{PLATFORM}:** {URL}<br />
|
||||
@@ -0,0 +1,2 @@
|
||||
<br />
|
||||
<u>**Donation Links:**</u><br />
|
||||
@@ -0,0 +1,2 @@
|
||||
**Client:** `%PROJECT_CLIENT_SIDE%` \
|
||||
**Server:** `%PROJECT_SERVER_SIDE%`
|
||||
@@ -0,0 +1 @@
|
||||
**Applying for:** `%PROJECT_REQUESTED_STATUS%`
|
||||
@@ -0,0 +1,2 @@
|
||||
**Summary:**
|
||||
`%PROJECT_SUMMARY%`
|
||||
@@ -0,0 +1,4 @@
|
||||
**Title:** %PROJECT_TITLE% \
|
||||
**Slug:** `%PROJECT_SLUG%`
|
||||
|
||||
**Title issues?**
|
||||
@@ -0,0 +1,3 @@
|
||||
**Slug:** `%PROJECT_SLUG%` </br>
|
||||
|
||||
**Title issues?**
|
||||
@@ -0,0 +1 @@
|
||||
**Title:** %PROJECT_TITLE% </br>
|
||||
@@ -0,0 +1,7 @@
|
||||
## Description Clarity
|
||||
|
||||
Per section 2 of %RULES% It's important that your Description accurately and honestly represents the content of your project.
|
||||
Currently, some elements in your Description may be confusing or misleading.
|
||||
Please edit your description to ensure it accurately represents the current functionality of your project.
|
||||
Avoid making hyperbolic claims that could misrepresent the facts of your project.
|
||||
Ensure that your Description is accurate and not likely to confuse users.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Description Accessibility
|
||||
|
||||
In accordance with section 2.2 of %RULES%, we request that `# header`s not be used as body text.
|
||||
|
||||
Headers are interpreted differently by screen-readers and thus should generally only be used for things like separating sections of your Description.
|
||||
|
||||
If you would like to emphasize a particular sentence or paragraph, instead consider using `**bold**` text using the **B** button above the text editor.
|
||||
@@ -0,0 +1,9 @@
|
||||
## Image Descriptions
|
||||
|
||||
In accordance with section 2.2 of %RULES%, we ask that you provide a text alternative to your current Description.
|
||||
|
||||
It is important that your Description contains enough detail about your project that a user can have a full understanding of it from text alone.
|
||||
|
||||
A text-based transcription allows for those using screen readers, and users with slow internet connections unable to load images to be able to access the contents of your Description. This also acts as a backup in case the image in your Description ever goes offline for some reason.
|
||||
|
||||
We appreciate how much effort you put into your Description, but accessibility is important to us at Modrinth, if you would like you could put the transcription of your Description entirely in a `details` tag, so as to not spoil the visuals of your Description.
|
||||
@@ -0,0 +1,8 @@
|
||||
## Insufficient Description
|
||||
|
||||
Per section 2.1 of %RULES%, your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
|
||||
|
||||
Currently, it looks like there are some missing details.
|
||||
|
||||
What does your modpack add? What features does it have? Why would a user want to download it? Be specific!
|
||||
See descriptions like [Simply Optimized](https://modrinth.com/modpack/sop) or [Aged](https://modrinth.com/modpack/aged) for examples of what a good description looks like.
|
||||
@@ -0,0 +1,8 @@
|
||||
## Insufficient Description
|
||||
|
||||
Per section 2.1 of %RULES%, your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
|
||||
|
||||
Currently, it looks like there are some missing details.
|
||||
|
||||
What does your project add? What features does it have? Why would a user want to download it? Be specific!
|
||||
See descriptions like [Sodium](https://modrinth.com/mod/sodium) or [LambDynamicLights](https://modrinth.com/mod/lambdynamiclights) for examples of what a good description looks like.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Insufficient Description
|
||||
|
||||
Per section 2.1 of %RULES%, your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
|
||||
|
||||
Currently, it looks like there are some missing details.
|
||||
%EXPLAINER%
|
||||
@@ -0,0 +1,5 @@
|
||||
## No English Description
|
||||
|
||||
Per section 2.2 of %RULES%, a project's [Summary](%PROJECT_SETTINGS_LINK%) and %PROJECT_DESCRIPTION_FLINK% must be in English, unless meant exclusively for non-English use, such as translations.
|
||||
|
||||
You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your project page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).
|
||||
@@ -0,0 +1,7 @@
|
||||
## Description Accessibility
|
||||
|
||||
Per section 2 of %RULES%, your description must be plainly readable and accessible.
|
||||
|
||||
Using non-standard text characters like Zalgo or "fancy text" in place of text anywhere in your project, including the Description, Summary, or Title can make your project pages inaccessible.
|
||||
|
||||
This is important for users who rely on Screen Readers and for search engines in order to provide relevant results to users. Please remove any instances of this type of text.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Unfinished Description
|
||||
|
||||
It looks like your project Description is still a Work In Progress.
|
||||
Please remember to submit only when ready, as it is important your project meets the requirements of Section 2.1 of %RULES%.
|
||||
@@ -0,0 +1,8 @@
|
||||
## Insufficient Gallery Images
|
||||
|
||||
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of %RULES%.
|
||||
Keep in mind that you should:
|
||||
|
||||
- Set a featured image that best represents your project.
|
||||
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
|
||||
- Upload any relevant images in your Description to your Gallery tab for best results.
|
||||
@@ -0,0 +1,3 @@
|
||||
## Unrelated Gallery Images
|
||||
|
||||
Per section 5.5 of %RULES%, any images in your project's Gallery must be relevant to the project and also include a Title.
|
||||
@@ -0,0 +1 @@
|
||||
If this is a project-specific License you've written yourself, you must host the full License in a way that makes it publicly available, for instance, in a public source repository on a platform like GitHub.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Invalid License Link
|
||||
|
||||
It's important that your project's %PROJECT_LICENSE_FLINK% link is accurate and leads directly to a valid license for this content.
|
||||
Your current link: `%PROJECT_LICENSE_URL%` does not appear to lead to a valid license for this project, or it is not publicly accessible.
|
||||
@@ -0,0 +1,5 @@
|
||||
## No Source Code Provided
|
||||
|
||||
Your project's %PROJECT_LICENSE_FLINK% of `%PROJECT_LICENSE_NAME%`, requires source disclosure.
|
||||
Consider adding a Source link to your project's repository, or including a Sources file for each version as an Additional File.
|
||||
Keep in mind this may be a requirement of the source work's licensing, which must be abided per section 4 of %RULES%.
|
||||
@@ -0,0 +1,4 @@
|
||||
## No Source Code Provided
|
||||
|
||||
Your project's %PROJECT_LICENSE_FLINK% of `%PROJECT_LICENSE_NAME%`, requires source disclosure.
|
||||
Consider adding a Source link to your project's repository, or including a Sources file for each version as an Additional File. You may also want to refer to %LICENSING_GUIDE% if you wish to select a different License, remember to make sure your selected License is consistent with the license in your project's files as well.
|
||||
4
packages/moderation/src/data/messages/links/misused.md
Normal file
4
packages/moderation/src/data/messages/links/misused.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## Misuse of Links
|
||||
|
||||
Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project.
|
||||
Currently it looks like your %MISUSED_LINKS% link(s) are misused or incorrectly labeled.
|
||||
@@ -0,0 +1 @@
|
||||
Currently, your Discord link directs to an invalid invite, likely because your invite has expired. Make sure to set your invite link to permanent with unlimited uses before resubmitting your project.
|
||||
@@ -0,0 +1 @@
|
||||
Currently, your Source link directs to a Page Not Found error, likely because your repository is private. Make sure to set your repository to public before resubmitting your project.
|
||||
@@ -0,0 +1,3 @@
|
||||
## Unreachable Links
|
||||
|
||||
Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project.
|
||||
@@ -0,0 +1,3 @@
|
||||
Unfortunately, anti-virus software has consistently been found to be an unreliable tool for Minecraft mods.
|
||||
|
||||
If you have evidence of malicious activity concerning a specific mod, or of malicious code decompiled from a mod on Modrinth, please create a new Report and provide the required details, thank you.
|
||||
@@ -0,0 +1,3 @@
|
||||
Thank you for your report.
|
||||
|
||||
This project was confirmed to be malicious after a detailed investigation. Luckily, thanks to your report and quick action from our team, we have reason to believe this did not impact a significant amount of users and we have taken precautions to prevent this malicious code from appearing on Modrinth again.
|
||||
@@ -0,0 +1,6 @@
|
||||
Unfortunately, the Moderation team is unable to assist with your issue.
|
||||
|
||||
The reporting system is exclusively for reporting issues to Moderation staff; only violations of [Modrinth's Content Rules](https://modrinth.com/legal/rules) should be reported. The members of the project you're reporting do not see that you have submitted a report.
|
||||
|
||||
If you are having issues with crashes, please check out [our FAQ section](https://support.modrinth.com/aen/articles/8792916) to learn how to diagnose and fix crashes.
|
||||
For other project-specific issues consider asking the project's own community, check for a Discord or Issues link on the project page.
|
||||
@@ -0,0 +1,5 @@
|
||||
Unfortunately, the Moderation team is unable to assist with your issue.
|
||||
|
||||
The reporting system is exclusively for reporting issues to Moderation staff; only violations of [Modrinth's Content Rules](https://modrinth.com/legal/rules) should be reported.
|
||||
|
||||
Please reach out to the [Modrinth Help Center](https://support.modrinth.com/) so we can better assist you and bring up your concerns with our platform tean,
|
||||
3
packages/moderation/src/data/messages/reports/spam.md
Normal file
3
packages/moderation/src/data/messages/reports/spam.md
Normal file
@@ -0,0 +1,3 @@
|
||||
The reporting system is exclusively for reporting issues to Modrinth staff; only violations of [Modrinth's Content Rules](https://modrinth.com/legal/rules) should be reported. The members of the project you're reporting do not see that you have submitted a report.
|
||||
|
||||
Please ensure you are using the Reports system appropriately, repeated misuse may result in account suspension.
|
||||
3
packages/moderation/src/data/messages/reports/stale.md
Normal file
3
packages/moderation/src/data/messages/reports/stale.md
Normal file
@@ -0,0 +1,3 @@
|
||||
We haven't received a response in some time, so we're closing this report thread.
|
||||
|
||||
If you have additional information to share we ask that you create a new report.
|
||||
4
packages/moderation/src/data/messages/reupload/fork.md
Normal file
4
packages/moderation/src/data/messages/reupload/fork.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## Forks and Reuploads
|
||||
|
||||
Per section 4 of %RULES%, please provide proof that this project is both license-abiding and significantly divergent from the source work.
|
||||
Alternatively, please provide proof of your explicit permission from the author of the source work to distribute this content on Modrinth.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Identity Verification
|
||||
|
||||
**Welcome to Modrinth!** We're happy to see you here, we just want to make sure you're you.
|
||||
|
||||
Since this project already exists on the internet we ask that you provide evidence you are the rightful owner of this content.
|
||||
For instance, by submitting a screenshot accessing the settings of the project's existing pages such as %PLATFORM%.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Insufficient Fork
|
||||
|
||||
This project does not appear to significantly diverge from the source work, or does not abide by the license of the source work as required by section 4 of %RULES%.
|
||||
Please provide proof of your explicit permission to distribute this project from the creator(s) of the source work.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Proof of permissions
|
||||
|
||||
This project appears to contain content from other creators.
|
||||
Per section 4 of %RULES%, we ask that you provide proof of your permission to distribute this content, or derivatives of this content in your project on Modrinth.
|
||||
Either implicit permission abiding by the terms of the content's license(s) or explicit permission from the original creator of the content.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Reuploads are forbidden
|
||||
|
||||
This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%.
|
||||
Per section 4 of %RULES%, this is strictly forbidden.
|
||||
If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.
|
||||
1
packages/moderation/src/data/messages/rule-breaking.md
Normal file
1
packages/moderation/src/data/messages/rule-breaking.md
Normal file
@@ -0,0 +1 @@
|
||||
%MESSAGE%
|
||||
@@ -0,0 +1,9 @@
|
||||
## Environment Information
|
||||
|
||||
Per section 5.1 of %RULES%, it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
|
||||
|
||||
For a brief rundown of how this works:
|
||||
|
||||
- **Client side** refers to a mod that is only required by the client, like [Sodium](https://modrinth.com/mod/sodium).
|
||||
- **Server side** mods change the behavior of the server without the client needing the mod, like Datapacks, recipes, or server-side behaviors, like [Falling Tree](https://modrinth.com/mod/fallingtree).
|
||||
- A mod that adds features, entities, or new blocks and items, generally will be required on **both** the server and the client, for example [Cobblemon](https://modrinth.com/mod/cobblemon).
|
||||
@@ -0,0 +1,10 @@
|
||||
## Incorrect Environment Information
|
||||
|
||||
Per section 5.1 of %RULES%, it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
|
||||
|
||||
For a brief rundown of how this works:
|
||||
|
||||
- Some modpacks can be client-side, usually aimed at providing utility and optimization while allowing the player to join an unmodded server, for instance, [Fabulously Optimized](https://modrinth.com/project/1KVo5zza).
|
||||
- Most other modpacks that change how the game is played are going to be required on both the client and server, like the modpack [Aged](https://modrinth.com/project/i4XHCd7Q).
|
||||
|
||||
When in doubt, test for yourself or check the requirements of the mods in your pack.
|
||||
5
packages/moderation/src/data/messages/slug/misused.md
Normal file
5
packages/moderation/src/data/messages/slug/misused.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Misuse of custom URL
|
||||
|
||||
We ask that you ensure your project's %PROJECT_SLUG_FLINK% accurately represents your project.
|
||||
Your current slug of `%PROJECT_SLUG%` may not accurately match your project's Name or contain excess information.
|
||||
A mismatched URL may make it more difficult for users to find your content. Abbreviations or similar are fine to use if applicable. If your preferred URL is not available, and you cannot find a matching public project, let us know in this moderation thread when you resubmit your project, and our moderation team may be able to free it up for your project.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Account Issues Indicated
|
||||
|
||||
We're sorry to hear you're having trouble accessing your accounts, unfortunately, our moderation team is unable to assist with account-related issues.
|
||||
Before resubmitting your project, %SUPPORT%.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Warnings from AutoMod
|
||||
|
||||
Unfortunately, our AutoMod cannot read your project's Description or your messages to moderation.
|
||||
AutoMod will warn both you and our Moderation Staff about potential issues, but if you've already followed the necessary steps these warnings can safely be ignored.
|
||||
Note that if your project is being rejected by AutoMod this means your project has content that can not be included in your modpack and must be removed before resubmission, including deleting versions of your modpack that include the content.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Corrections Applied
|
||||
|
||||
Your submission contained some issues which may have prevented your project from being published.
|
||||
These have been corrected by our Moderation Team so your project can be Approved, be sure to read and understand each issue listed below to ensure a smooth review for your next submission.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Private Use
|
||||
|
||||
Under normal circumstances, your project would be rejected due to the issues listed below.
|
||||
However, since your project is not intended for public use, these requirements will be waived and your project will be unlisted. This means it will remain accessible through a direct link without appearing in public search results, allowing you to share it privately.
|
||||
If you're okay with this, or submitted your project to be unlisted already, than no further action is necessary.
|
||||
If you would like to publish your project publicly, please address all moderation concerns before resubmitting this project.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Source Code Requested
|
||||
|
||||
To ensure the safety of all Modrinth users, we ask that you provide the source code for this project before resubmission so that it can be reviewed by our Moderation Team.
|
||||
We also ask that you provide the source for any included binary files, as well as detailed build instructions allowing us to verify that the compiled code you are distributing matches the provided source.
|
||||
We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Source Code Requested
|
||||
|
||||
To ensure the safety of all Modrinth users, we ask that you provide the source code for this project and the process you used to obfuscate it before resubmission so that it can be reviewed by our Moderation Team.
|
||||
We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Invalid Summary Formatting
|
||||
|
||||
Per section 5.3 of %RULES%, your Summary can not include any extra formatting such as lists, or links.
|
||||
|
||||
Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Insufficient Summary
|
||||
|
||||
Per section 5.3 of %RULES%, your project summary should provide a brief overview of your project that informs and entices users.
|
||||
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.
|
||||
@@ -0,0 +1,5 @@
|
||||
## No English Summary
|
||||
|
||||
Per section 2.2 of %RULES%, a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations.
|
||||
|
||||
You may include your non-English Summary but we ask that you also add an English translation.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Insufficient Summary
|
||||
|
||||
Per section 5.3 of %RULES%, your Summary can not be the same as your project's Title.
|
||||
|
||||
Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Minecraft Project Names
|
||||
|
||||
Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the title.
|
||||
Your project's current Name of `%PROJECT_TITLE%` may be confusingly similar to, or imply association with, the game Minecraft. We encourage you to change your project's [Name](%PROJECT_SETTINGS_LINK%) to avoid a potential violation of Minecraft's Usage Guidelines.
|
||||
Abbreviations like "MC" or elaborate titles that do not make the name Minecraft a significant portion of the name are okay.
|
||||
When editing your project's Name, remember to update its [URL](%PROJECT_SETTINGS_LINK%) to match.
|
||||
@@ -0,0 +1,2 @@
|
||||
You may reference the source work in the Description of your fork, however, you must do so in a way that is unlikely to cause confusion or imply association with or endorsement from the author of the source work.
|
||||
Additionally, per section 4 of %RULES%, we ask that you ensure your project's Name and Branding abide by the license of the source work.
|
||||
@@ -0,0 +1 @@
|
||||
You may reference the projects that make up the focus point of your modpack, however, you must do so in a way that is unlikely to cause confusion or imply association with or endorsement from the author of that content.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Project Branding
|
||||
|
||||
Per section 1.8 of %RULES%, your project or its branding must not imply association or be easily confused with any other person or organization.
|
||||
We ask that you change your project's [Name](%PROJECT_SETTINGS_LINK%) and other relevant branding to avoid causing confusion or implying association with existing projects or individuals.
|
||||
When editing your project's Name, remember to update its [URL](%PROJECT_SETTINGS_LINK%) to match.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Misuse of Project Name
|
||||
|
||||
Per section 5.2 of %RULES%, your project's [Name](%PROJECT_SETTINGS_LINK%) should not include unnecessary information such as loaders, themes, tags, or versions.
|
||||
Your project's current Name of `%PROJECT_TITLE%` appears to contain extra information.
|
||||
We ask that you remove all additional information from the [Name](%PROJECT_SETTINGS_LINK%). Instead, consider including this in your project's Summary or Description, or as a part of its relevant metadata.
|
||||
When editing your project's Name, remember to update its [URL](%PROJECT_SETTINGS_LINK%) to match.
|
||||
@@ -0,0 +1,4 @@
|
||||
## No Versions
|
||||
|
||||
It looks like all versions of your project have been deleted, meaning that our Moderation Team cannot finish reviewing your project.
|
||||
Please resubmit your project once you've uploaded a new version.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Unsupported Project
|
||||
|
||||
Per section 5.7 of %RULES%, Modrinth does not support uploading multiple variations of your project as Additional files.
|
||||
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
|
||||
We ask that you upload each alternate version of your project as a new project, ensuring that all users will be able to access and easily find your content.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Unsupported Project
|
||||
|
||||
Modrinth does not support uploading projects that have unnecessary or extraneous installation steps.
|
||||
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
|
||||
We ask that you upload each alternate version of your project as a new project, ensuring that all users will be able to access and easily find your content.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Unsupported Project
|
||||
|
||||
Modrinth does not support uploading multiple variations of your project as separate versions.
|
||||
New Versions of your project should strictly be a linear upgrade of the same content.
|
||||
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
|
||||
We ask that you upload each alternate version of your project as a new project, ensuring that all users will be able to access and easily find your content.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Incorrect Additional Files
|
||||
|
||||
Per section 5.7 of %RULES%, the additional files section should only be used for specific designated purposes such as a `Sources.jar`.
|
||||
To ensure a smooth experience for you and your users, please upload each alternate version of your modpack as its own Modpack project, thank you.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Unsupported Project
|
||||
|
||||
Modrinth does not support uploading multiple variations of your project as separate versions.
|
||||
New Versions of your project should strictly be a linear upgrade of the same content.
|
||||
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
|
||||
To ensure a smooth experience for you and your users, please upload each alternate version of your modpack as its own Modpack project, thank you.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Incorrect Additional Files
|
||||
|
||||
Per section 5.7 of %RULES%, the additional files section should only be used for specific designated purposes such as a `Sources.jar`.
|
||||
Modrinth does not support the upload of modpacks in the `.zip` format, as this may cause issues for Modrinth users or distribute copyrighted content without the proper permissions.
|
||||
If you would like to upload a server-specific version of your modpack, consider creating a separate Modpack project.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Broken Version
|
||||
|
||||
It looks like you've uploaded multiple files with the same name to the same version. This triggers a bug that produces duplicate primary file entries.
|
||||
This may cause issues for users and creators as it would prevent Modpacks that include this content from functioning in the Modrinth App and other launchers.
|
||||
We ask that you delete and reupload all applicable versions. Be sure to only upload one file to each version before resubmission.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Incorrect Use of Additional Files
|
||||
|
||||
It looks like you've uploaded multiple primary files to one Version as Additional Files. Per section 5.7 of %RULES%, each Version of your project must include only one primary file that corresponds to its respective Minecraft and loader versions.
|
||||
This allows users to easily find and download the content they need for their game profile with ease. The Additional Files feature can be used for things like a `Sources.jar`.
|
||||
Please upload each version of your project separately, thank you.
|
||||
@@ -0,0 +1,3 @@
|
||||
## Data Packs on Modrinth
|
||||
|
||||
It looks like you've selected loaders for your Data Pack that are causing it to be marked as a different project type. Data Packs must only be uploaded with the "Data Pack" loader selected. Please re-upload all versions of your data pack and make sure to only select "Data Pack" as the loader.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Modpacks on Modrinth
|
||||
|
||||
It looks like you've uploaded your Modpack as a `.zip`, unfortunately, this is invalid and is why your project type is "Mod". Please refer to our support article to learn more about %MODPACKS_ON_MODRINTH%.
|
||||
Once you're ready feel free to resubmit your project as a `.mrpack`.
|
||||
Don't forget to delete the old files from your %PROJECT_VERSIONS_FLINK%!
|
||||
@@ -0,0 +1,3 @@
|
||||
## Resource Packs on Modrinth
|
||||
|
||||
It looks like you've selected loaders for your Resource Pack that are causing it to be marked as a different project type. Resource Packs must only be uploaded with the "Resource Pack" loader selected. Please re-upload all versions of your resource pack and make sure to only select "Resource Pack" as the loader.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Unnecessary redistribution of dependencies
|
||||
|
||||
This project appears to include libs or dependencies, unnecessarily redistributing their entire contents.
|
||||
This is often due to an error in project structure or compilation, and in some cases, may violate the copyrights or licensing agreements of these libraries.
|
||||
We ask that you remove all unnecessary files, assets, and code from your project before resubmission.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Unsupported Project
|
||||
|
||||
Unfortunately, Modrinth does not currently support the upload of %INVALID_TYPE%.
|
||||
|
||||
If you would like to publish this project in the future and help Modrinth grow, consider creating an [issue](https://github.com/modrinth/code/issues) suggesting support for this type of content.
|
||||
|
||||
We appreciate your understanding and look forward to hosting your other creations.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Vanilla Assets
|
||||
|
||||
Your resource pack currently includes an excessive amount of unmodified assets from vanilla Minecraft.
|
||||
Please remove these from your pack and ensure your project only contains original assets created by you, thank you.
|
||||
32
packages/moderation/src/data/modpack-permissions-stage.ts
Normal file
32
packages/moderation/src/data/modpack-permissions-stage.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ModerationModpackPermissionApprovalType, Project } from '@modrinth/utils'
|
||||
import type { Stage } from '../types/stage'
|
||||
import { PackageOpenIcon } from '@modrinth/assets'
|
||||
|
||||
export default {
|
||||
id: 'modpack-permissions',
|
||||
title: 'Modpack Permissions',
|
||||
icon: PackageOpenIcon,
|
||||
// Replace me please.
|
||||
guidance_url:
|
||||
'https://www.notion.so/Content-Moderation-Cheat-Sheets-22d5ee711bf081a4920ef08879fe6bf5?source=copy_link#22d5ee711bf08116bd8bc1186f357062',
|
||||
shouldShow: (project: Project) => project.project_type === 'modpack',
|
||||
actions: [
|
||||
{
|
||||
id: 'button',
|
||||
type: 'button',
|
||||
label: 'This dummy button must be present or the stage will not appear.',
|
||||
},
|
||||
],
|
||||
} as Stage
|
||||
|
||||
export const finalPermissionMessages: Record<
|
||||
ModerationModpackPermissionApprovalType['id'],
|
||||
string | undefined
|
||||
> = {
|
||||
yes: undefined,
|
||||
'with-attribution-and-source': undefined,
|
||||
'with-attribution': `The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your Modpack's description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):`,
|
||||
no: 'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:',
|
||||
'permanent-no': `The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:`,
|
||||
unidentified: `The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:`,
|
||||
}
|
||||
7
packages/moderation/src/data/nags.ts
Normal file
7
packages/moderation/src/data/nags.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Nag } from '../types/nags'
|
||||
import { coreNags } from './nags/core'
|
||||
import { descriptionNags } from './nags/description'
|
||||
import { linksNags } from './nags/links'
|
||||
import { tagsNags } from './nags/tags'
|
||||
|
||||
export default [...coreNags, ...linksNags, ...descriptionNags, ...tagsNags] as Nag[]
|
||||
292
packages/moderation/src/data/nags/core.ts
Normal file
292
packages/moderation/src/data/nags/core.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
|
||||
export const coreNags: Nag[] = [
|
||||
{
|
||||
id: 'moderator-feedback',
|
||||
title: defineMessage({
|
||||
id: 'nags.moderator-feedback.title',
|
||||
defaultMessage: 'Review moderator feedback',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.moderator-feedback.description',
|
||||
defaultMessage:
|
||||
'Review and address all concerns from the moderation team before resubmitting.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) =>
|
||||
context.tags.rejectedStatuses.includes(context.project.status),
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: defineMessage({
|
||||
id: 'nags.moderation.title',
|
||||
defaultMessage: 'Visit moderation thread',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upload-version',
|
||||
title: defineMessage({
|
||||
id: 'nags.upload-version.title',
|
||||
defaultMessage: 'Upload a version',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.upload-version.description',
|
||||
defaultMessage: 'At least one version is required for a project to be submitted for review.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.versions.length < 1,
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: defineMessage({
|
||||
id: 'nags.versions.title',
|
||||
defaultMessage: 'Visit versions page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-description',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-description.title',
|
||||
defaultMessage: 'Add a description',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-description.description',
|
||||
defaultMessage:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.project.body === '',
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.description.title',
|
||||
defaultMessage: 'Visit description settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-icon',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-icon.title',
|
||||
defaultMessage: 'Add an icon',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-icon.description',
|
||||
defaultMessage:
|
||||
'Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) => !context.project.icon_url,
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.title',
|
||||
defaultMessage: 'Visit general settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upload-gallery-image',
|
||||
title: defineMessage({
|
||||
id: 'nags.upload-gallery-image.title',
|
||||
defaultMessage: 'Upload a gallery image',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const projectType = formatProjectType(context.project.project_type).toLowerCase()
|
||||
let msg = ''
|
||||
if (context.project.project_type === 'resourcepack') {
|
||||
msg =
|
||||
', except for audio or localization packs. If this describes your pack, please select the appropriate tag'
|
||||
}
|
||||
const resourcepackMessage = msg
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.upload-gallery-image.description',
|
||||
defaultMessage:
|
||||
'At least one gallery image is required to showcase the content of your {type}{resourcepackMessage}.',
|
||||
}),
|
||||
{
|
||||
type: projectType,
|
||||
resourcepackMessage: resourcepackMessage,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
return (
|
||||
(context.project.project_type === 'resourcepack' ||
|
||||
context.project.project_type === 'shader') &&
|
||||
(!context.project.gallery || context.project.gallery?.length === 0) &&
|
||||
!(
|
||||
context.project.categories.includes('audio') ||
|
||||
context.project.additional_categories.includes('audio') ||
|
||||
context.project.categories.includes('locale') ||
|
||||
context.project.additional_categories.includes('locale')
|
||||
)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: defineMessage({
|
||||
id: 'nags.gallery.title',
|
||||
defaultMessage: 'Visit gallery page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'feature-gallery-image',
|
||||
title: defineMessage({
|
||||
id: 'nags.feature-gallery-image.title',
|
||||
defaultMessage: 'Feature a gallery image',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.feature-gallery-image.description',
|
||||
defaultMessage:
|
||||
'The featured gallery image is often how your project makes its first impression.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
|
||||
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
||||
},
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: defineMessage({
|
||||
id: 'nags.gallery.title',
|
||||
defaultMessage: 'Visit gallery page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-tags.title',
|
||||
defaultMessage: 'Select tags',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.select-tags.description',
|
||||
defaultMessage:
|
||||
'Select the tags that correctly apply to your project to help the right users find it.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) =>
|
||||
context.project.versions.length > 0 && context.project.categories.length < 1,
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.tags.title',
|
||||
defaultMessage: 'Visit tag settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
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) =>
|
||||
!(
|
||||
context.project.issues_url ||
|
||||
context.project.source_url ||
|
||||
context.project.wiki_url ||
|
||||
context.project.discord_url ||
|
||||
context.project.donation_urls.length > 0
|
||||
),
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.links.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-environments',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-environments.title',
|
||||
defaultMessage: 'Select supported environments',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.select-environments.description',
|
||||
defaultMessage: `Select if the {projectType} functions on the client-side and/or server-side.`,
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
|
||||
return (
|
||||
context.project.versions.length > 0 &&
|
||||
!excludedTypes.includes(context.project.project_type) &&
|
||||
(context.project.client_side === 'unknown' ||
|
||||
context.project.server_side === 'unknown' ||
|
||||
(context.project.client_side === 'unsupported' &&
|
||||
context.project.server_side === 'unsupported'))
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.environments.title',
|
||||
defaultMessage: 'Visit general settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-license',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-license.title',
|
||||
defaultMessage: 'Select a license',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.select-license.description',
|
||||
defaultMessage: 'Select the license your {projectType} is distributed under.',
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
|
||||
link: {
|
||||
path: 'settings/license',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.license.title',
|
||||
defaultMessage: 'Visit license settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
|
||||
},
|
||||
},
|
||||
]
|
||||
395
packages/moderation/src/data/nags/description.ts
Normal file
395
packages/moderation/src/data/nags/description.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import { renderHighlightedString } from '@modrinth/utils'
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
|
||||
export const MIN_DESCRIPTION_CHARS = 200
|
||||
export const MAX_HEADER_LENGTH = 80
|
||||
export const MIN_SUMMARY_CHARS = 30
|
||||
export const MIN_CHARS_PER_IMAGE = 60
|
||||
|
||||
export function analyzeHeaderLength(markdown: string): {
|
||||
hasLongHeaders: boolean
|
||||
longHeaders: string[]
|
||||
} {
|
||||
if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
|
||||
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
|
||||
const headerRegex = /^(#{1,3})\s+(.+)$/gm
|
||||
const headers = [...withoutCodeBlocks.matchAll(headerRegex)]
|
||||
|
||||
const longHeaders: string[] = []
|
||||
|
||||
headers.forEach((match) => {
|
||||
const headerText = match[2].trim()
|
||||
const sentenceEnders = /[.!?]+/g
|
||||
const sentences = headerText.split(sentenceEnders).filter((s) => s.trim().length > 0)
|
||||
|
||||
const isVeryLong = headerText.length > MAX_HEADER_LENGTH
|
||||
const hasMultipleSentences = sentences.length > 1
|
||||
|
||||
if (isVeryLong || hasMultipleSentences) {
|
||||
longHeaders.push(headerText)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
hasLongHeaders: longHeaders.length > 0,
|
||||
longHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
export function analyzeImageContent(markdown: string): {
|
||||
imageHeavy: boolean
|
||||
hasEmptyAltText: boolean
|
||||
} {
|
||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
|
||||
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
|
||||
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
|
||||
|
||||
const htmlImageRegex = /<img[^>]*>/gi
|
||||
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
|
||||
|
||||
const totalImages = images.length + htmlImages.length
|
||||
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
|
||||
const textLength = countText(withoutCodeBlocks)
|
||||
const recommendedTextLength = MIN_CHARS_PER_IMAGE * totalImages
|
||||
const imageHeavy =
|
||||
recommendedTextLength > MIN_DESCRIPTION_CHARS && textLength < recommendedTextLength
|
||||
|
||||
const hasEmptyAltText =
|
||||
images.some((match) => !match[1]?.trim()) ||
|
||||
htmlImages.some((match) => {
|
||||
const altMatch = match[0].match(/alt\s*=\s*["']([^"']*)["']/i)
|
||||
return !altMatch || !altMatch[1]?.trim()
|
||||
})
|
||||
|
||||
return { imageHeavy, hasEmptyAltText }
|
||||
}
|
||||
|
||||
export function countText(markdown: string): number {
|
||||
if (!markdown) return 0
|
||||
|
||||
const fallback = (md: string): number => {
|
||||
const withoutCode = md.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
const withoutImagesAndLinks = withoutCode
|
||||
.replace(/!\[[^\]]*]\([^)]+\)/g, ' ')
|
||||
.replace(/\[[^\]]*]\([^)]+\)/g, ' ')
|
||||
const withoutHtml = withoutImagesAndLinks.replace(/<[^>]+>/g, ' ')
|
||||
const withoutMdSyntax = withoutHtml
|
||||
.replace(/^>{1}\s?.*$/gm, ' ')
|
||||
.replace(/^#{1,6}\s+/gm, ' ')
|
||||
.replace(/[*_~`>-]/g, ' ')
|
||||
.replace(/\|/g, ' ')
|
||||
return withoutMdSyntax.replace(/\s+/g, ' ').trim().length
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || typeof globalThis.DOMParser === 'undefined') {
|
||||
console.warn(`[Moderation] SSR: no window/DOMParser, falling back for countText`)
|
||||
return fallback(markdown)
|
||||
}
|
||||
|
||||
try {
|
||||
const htmlString = renderHighlightedString(markdown)
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(htmlString, 'text/html')
|
||||
const walker = doc.createTreeWalker(doc.body || doc, NodeFilter.SHOW_TEXT)
|
||||
|
||||
const textList: string[] = []
|
||||
let node = walker.nextNode()
|
||||
while (node) {
|
||||
if (node.textContent) textList.push(node.textContent)
|
||||
node = walker.nextNode()
|
||||
}
|
||||
return textList.join(' ').replace(/\s+/g, ' ').trim().length
|
||||
} catch {
|
||||
return fallback(markdown)
|
||||
}
|
||||
}
|
||||
|
||||
export const descriptionNags: Nag[] = [
|
||||
{
|
||||
id: 'description-too-short',
|
||||
title: defineMessage({
|
||||
id: 'nags.description-too-short.title',
|
||||
defaultMessage: 'Expand the description',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const readableLength = countText(context.project.body || '')
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.description-too-short.description',
|
||||
defaultMessage:
|
||||
'Your description is {length} readable characters. At least {minChars} characters is recommended to create a clear and informative description.',
|
||||
}),
|
||||
{
|
||||
length: readableLength,
|
||||
minChars: MIN_DESCRIPTION_CHARS,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const readableLength = countText(context.project.body || '')
|
||||
return readableLength < MIN_DESCRIPTION_CHARS && readableLength > 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'long-headers',
|
||||
title: defineMessage({
|
||||
id: 'nags.long-headers.title',
|
||||
defaultMessage: 'Shorten headers',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||
const count = longHeaders.length
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.long-headers.description',
|
||||
defaultMessage:
|
||||
'{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences.',
|
||||
}),
|
||||
{
|
||||
count,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||
return hasLongHeaders
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-too-short',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-too-short.title',
|
||||
defaultMessage: 'Expand the summary',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.summary-too-short.description',
|
||||
defaultMessage:
|
||||
'Your summary is {length} characters. At least {minChars} characters is recommended to create an informative and enticing summary.',
|
||||
}),
|
||||
{
|
||||
length: context.project.description?.length || 0,
|
||||
minChars: MIN_SUMMARY_CHARS,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summaryLength = context.project.description?.trim()?.length || 0
|
||||
return summaryLength < MIN_SUMMARY_CHARS && summaryLength !== 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-special-formatting',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-special-formatting.title',
|
||||
defaultMessage: 'Clear up the summary',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.summary-special-formatting.description',
|
||||
defaultMessage: `Your summary should not contain formatting, line breaks, special characters, or links, since the summary will only display plain text.`,
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summary = context.project.description?.trim() || ''
|
||||
return Boolean(
|
||||
summary.match(/https:\/\//g) ||
|
||||
summary.match(/http:\/\//g) ||
|
||||
summary.match(/# .*/g) ||
|
||||
summary.match(/---/g) ||
|
||||
summary.match(/\n/g) ||
|
||||
summary.match(/\[.*\]\(.*\)/g) ||
|
||||
summary.match(/!\[.*\]/g) ||
|
||||
summary.match(/`.*`/g) ||
|
||||
summary.match(/\*.*\*/g) ||
|
||||
summary.match(/_.*_/g) ||
|
||||
summary.match(/~~.*~~/g) ||
|
||||
summary.match(/```/g) ||
|
||||
summary.match(/> /g),
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minecraft-title-clause',
|
||||
title: defineMessage({
|
||||
id: 'nags.minecraft-title-clause.title',
|
||||
defaultMessage: 'Avoid brand infringement',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.minecraft-title-clause.description',
|
||||
defaultMessage: `Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the name.`,
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.toLowerCase() || ''
|
||||
const wordsInTitle = title.split(' ').filter((word) => word.length > 0)
|
||||
return title.includes('minecraft') && title.length > 0 && wordsInTitle.length <= 3
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-title.title',
|
||||
defaultMessage: 'Edit title',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'title-contains-technical-info',
|
||||
title: defineMessage({
|
||||
id: 'nags.title-contains-technical-info.title',
|
||||
defaultMessage: 'Clean up the name',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.title-contains-technical-info.description',
|
||||
defaultMessage:
|
||||
"Keeping your project's Name clean and makes it memorable easier to find. Version and loader information is automatically displayed alongside your project.",
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.toLowerCase() || ''
|
||||
if (!title) return false
|
||||
|
||||
const loaderNames =
|
||||
context.tags.loaders?.map((loader: { name: string }) => loader.name?.toLowerCase()) || []
|
||||
const hasLoader = loaderNames.some((loader) => loader && title.includes(loader.toLowerCase()))
|
||||
const versionPatterns = [/\b1\.\d+(\.\d+)?\b/]
|
||||
const hasVersionPattern = versionPatterns.some((pattern) => pattern.test(title))
|
||||
|
||||
return hasLoader || hasVersionPattern
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-title.title',
|
||||
defaultMessage: 'Edit title',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-same-as-title',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-same-as-title.title',
|
||||
defaultMessage: 'Make the summary unique',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.summary-same-as-title.description',
|
||||
defaultMessage:
|
||||
"Your summary can not be the same as your project's Name. It's important to create an informative and enticing Summary.",
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.trim() || ''
|
||||
const summary = context.project.description?.trim() || ''
|
||||
return title === summary && title.length > 0 && summary.length > 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Don't like this one, is this needed?
|
||||
id: 'image-heavy-description',
|
||||
title: defineMessage({
|
||||
id: 'nags.image-heavy-description.title',
|
||||
defaultMessage: 'Ensure accessibility',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.image-heavy-description.description',
|
||||
defaultMessage:
|
||||
'Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { imageHeavy } = analyzeImageContent(context.project.body || '')
|
||||
return imageHeavy
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'missing-alt-text',
|
||||
title: defineMessage({
|
||||
id: 'nags.missing-alt-text.title',
|
||||
defaultMessage: 'Add image alt text',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.missing-alt-text.description',
|
||||
defaultMessage:
|
||||
'Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
|
||||
return hasEmptyAltText
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
]
|
||||
4
packages/moderation/src/data/nags/index.ts
Normal file
4
packages/moderation/src/data/nags/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './core'
|
||||
export * from './links'
|
||||
export * from './description'
|
||||
export * from './tags'
|
||||
281
packages/moderation/src/data/nags/links.ts
Normal file
281
packages/moderation/src/data/nags/links.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
|
||||
export const commonLinkDomains = {
|
||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'docs.google.com'],
|
||||
discord: ['discord.gg', 'discord.com', 'dsc.gg'],
|
||||
licenseBlocklist: [
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'modrinth.com',
|
||||
'curseforge.com',
|
||||
'twitter.com',
|
||||
'x.com',
|
||||
'discord.gg',
|
||||
'discord.com',
|
||||
'instagram.com',
|
||||
'facebook.com',
|
||||
'tiktok.com',
|
||||
'reddit.com',
|
||||
'twitch.tv',
|
||||
'patreon.com',
|
||||
'ko-fi.com',
|
||||
'paypal.com',
|
||||
'buymeacoffee.com',
|
||||
'google.com',
|
||||
'example.com',
|
||||
't.me',
|
||||
],
|
||||
linkShorteners: ['bit.ly', 'adf.ly', 'tinyurl.com', 'short.io', 'is.gd'],
|
||||
}
|
||||
|
||||
export function isCommonUrl(url: string | null, commonDomains: string[]): boolean {
|
||||
if (url === null || url === '') return true
|
||||
try {
|
||||
const domain = new URL(url).hostname.toLowerCase()
|
||||
return commonDomains.some((allowed) => domain.includes(allowed))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isCommonUrlOfType(url: string | null, commonDomains: string[]): boolean {
|
||||
if (url === null || url === '') return false
|
||||
return isCommonUrl(url, commonDomains)
|
||||
}
|
||||
|
||||
export function isDiscordUrl(url: string | null): boolean {
|
||||
return isCommonUrlOfType(url, commonLinkDomains.discord)
|
||||
}
|
||||
|
||||
export function isLinkShortener(url: string | null): boolean {
|
||||
return isCommonUrlOfType(url, commonLinkDomains.linkShorteners)
|
||||
}
|
||||
|
||||
export function isUncommonLicenseUrl(url: string | null): boolean {
|
||||
return isCommonUrlOfType(url, commonLinkDomains.licenseBlocklist)
|
||||
}
|
||||
|
||||
export const linksNags: Nag[] = [
|
||||
{
|
||||
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) => {
|
||||
return (
|
||||
!isCommonUrl(context.project.source_url ?? null, commonLinkDomains.source) ||
|
||||
!isCommonUrl(context.project.issues_url ?? null, commonLinkDomains.issues) ||
|
||||
!isCommonUrl(context.project.discord_url ?? null, commonLinkDomains.discord)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-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) =>
|
||||
isDiscordUrl(context.project.source_url ?? null) ||
|
||||
isDiscordUrl(context.project.issues_url ?? null) ||
|
||||
isDiscordUrl(context.project.wiki_url ?? null),
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'link-shortener-usage',
|
||||
title: defineMessage({
|
||||
id: 'nags.link-shortener-usage.title',
|
||||
defaultMessage: "Don't use link shorteners",
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.link-shortener-usage.description',
|
||||
defaultMessage:
|
||||
'Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
if (context.project.donation_urls) {
|
||||
for (const donation of context.project.donation_urls) {
|
||||
if (isLinkShortener(donation.url ?? null)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
isLinkShortener(context.project.source_url ?? null) ||
|
||||
isLinkShortener(context.project.issues_url ?? null) ||
|
||||
isLinkShortener(context.project.wiki_url ?? null) ||
|
||||
isLinkShortener(context.project.discord_url ?? null) ||
|
||||
Boolean(context.project.license.url && isLinkShortener(context.project.license.url ?? 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.',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const domain = new URL(licenseUrl).hostname.toLowerCase()
|
||||
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 },
|
||||
)
|
||||
} catch {
|
||||
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
|
||||
|
||||
const isBlocklisted = isUncommonLicenseUrl(licenseUrl)
|
||||
|
||||
try {
|
||||
new URL(licenseUrl)
|
||||
return isBlocklisted
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-license.title',
|
||||
defaultMessage: 'Edit license',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-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 {projectType} 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.',
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const gplLicenses = [
|
||||
'GPL-2.0',
|
||||
'GPL-2.0+',
|
||||
'GPL-2.0-only',
|
||||
'GPL-2.0-or-later',
|
||||
'GPL-3.0',
|
||||
'GPL-3.0+',
|
||||
'GPL-3.0-only',
|
||||
'GPL-3.0-or-later',
|
||||
'LGPL-2.1',
|
||||
'LGPL-2.1+',
|
||||
'LGPL-2.1-only',
|
||||
'LGPL-2.1-or-later',
|
||||
'LGPL-3.0',
|
||||
'LGPL-3.0+',
|
||||
'LGPL-3.0-only',
|
||||
'LGPL-3.0-or-later',
|
||||
'AGPL-3.0',
|
||||
'AGPL-3.0+',
|
||||
'AGPL-3.0-only',
|
||||
'AGPL-3.0-or-later',
|
||||
'MPL-2.0',
|
||||
]
|
||||
|
||||
const isGplLicense = gplLicenses.includes(context.project.license.id)
|
||||
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
|
||||
}
|
||||
const notSourceAsDistributed = (context: NagContext) =>
|
||||
context.project.project_type === 'mod' || context.project.project_type === 'plugin'
|
||||
|
||||
return (
|
||||
isGplLicense &&
|
||||
notSourceAsDistributed(context) &&
|
||||
!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-id-settings-links',
|
||||
},
|
||||
},
|
||||
]
|
||||
160
packages/moderation/src/data/nags/tags.ts
Normal file
160
packages/moderation/src/data/nags/tags.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { Project } from '@modrinth/utils'
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
|
||||
const allResolutionTags = ['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+']
|
||||
|
||||
const MAX_TAG_COUNT = 8
|
||||
|
||||
function getCategories(
|
||||
project: Project & { actualProjectType: string },
|
||||
tags: {
|
||||
categories?: {
|
||||
project_type: string
|
||||
}[]
|
||||
},
|
||||
) {
|
||||
return (
|
||||
tags.categories?.filter(
|
||||
(category: { project_type: string }) => category.project_type === project.actualProjectType,
|
||||
) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
export const tagsNags: Nag[] = [
|
||||
{
|
||||
id: 'too-many-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.too-many-tags.title',
|
||||
defaultMessage: 'Select accurate tags',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const tagCount =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
const maxTagCount = MAX_TAG_COUNT
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.too-many-tags.description',
|
||||
defaultMessage:
|
||||
"You've selected {tagCount} tags. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results.",
|
||||
}),
|
||||
{
|
||||
tagCount,
|
||||
maxTagCount,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const tagCount =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
return tagCount > MAX_TAG_COUNT
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'multiple-resolution-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.multiple-resolution-tags.title',
|
||||
defaultMessage: 'Select correct resolution',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const resolutionTags = context.project.categories
|
||||
.concat(context.project.additional_categories)
|
||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
||||
|
||||
const sortedTags = resolutionTags.toSorted((a, b) => {
|
||||
return allResolutionTags.indexOf(a) - allResolutionTags.indexOf(b)
|
||||
})
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.multiple-resolution-tags.description',
|
||||
defaultMessage:
|
||||
"You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution.",
|
||||
}),
|
||||
{
|
||||
count: resolutionTags.length,
|
||||
tags: sortedTags
|
||||
.join(', ')
|
||||
.replace('8x-', '8x or lower')
|
||||
.replace('512x+', '512x or higher'),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
if (context.project.project_type !== 'resourcepack') return false
|
||||
|
||||
const resolutionTags = context.project.categories
|
||||
.concat(context.project.additional_categories)
|
||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
||||
return resolutionTags.length > 1
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'all-tags-selected',
|
||||
title: defineMessage({
|
||||
id: 'nags.all-tags-selected.title',
|
||||
defaultMessage: 'Select accurate tags',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const categoriesForProjectType = getCategories(
|
||||
context.project as Project & { actualProjectType: string },
|
||||
context.tags,
|
||||
)
|
||||
const totalAvailableTags = categoriesForProjectType.length
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.all-tags-selected.description',
|
||||
defaultMessage:
|
||||
"You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project.",
|
||||
}),
|
||||
{
|
||||
totalAvailableTags,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const categoriesForProjectType = getCategories(
|
||||
context.project as Project & { actualProjectType: string },
|
||||
context.tags,
|
||||
)
|
||||
const totalSelectedTags =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
return (
|
||||
totalSelectedTags === categoriesForProjectType.length &&
|
||||
context.project.project_type !== 'project'
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
]
|
||||
34
packages/moderation/src/data/report-quick-replies.ts
Normal file
34
packages/moderation/src/data/report-quick-replies.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ReportQuickReply } from '../types/reports'
|
||||
|
||||
export default [
|
||||
{
|
||||
label: 'Antivirus',
|
||||
message: async () => (await import('./messages/reports/antivirus.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Spam',
|
||||
message: async () => (await import('./messages/reports/spam.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Gameplay Issue',
|
||||
message: async () => (await import('./messages/reports/gameplay-issue.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Platform Issue',
|
||||
message: async () => (await import('./messages/reports/platform-issue.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Stale',
|
||||
message: async () => (await import('./messages/reports/stale.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Confirmed Malware',
|
||||
message: async () => (await import('./messages/reports/confirmed-malware.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
] as ReadonlyArray<ReportQuickReply>
|
||||
58
packages/moderation/src/data/stages/categories.ts
Normal file
58
packages/moderation/src/data/stages/categories.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { TagsIcon } from '@modrinth/assets'
|
||||
|
||||
const categories: Stage = {
|
||||
title: "Are the project's tags accurate?",
|
||||
id: 'tags',
|
||||
icon: TagsIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings/tags',
|
||||
shouldShow: (project) =>
|
||||
project.categories.length > 0 || project.additional_categories.length > 0,
|
||||
text: async () => {
|
||||
return (await import('../messages/checklist-text/categories.md?raw')).default
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'categories_inaccurate',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 700,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/categories/inaccurate.md?raw')).default,
|
||||
disablesActions: ['categories_optimization_misused', 'categories_resolutions_misused'],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'categories_optimization_misused',
|
||||
type: 'button',
|
||||
label: 'Optimization',
|
||||
weight: 701,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) =>
|
||||
project.categories.includes('optimization') ||
|
||||
project.additional_categories.includes('optimization'),
|
||||
message: async () =>
|
||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||
(await import('../messages/categories/optimization_misused.md?raw')).default,
|
||||
disablesActions: ['categories_inaccurate', 'categories_resolutions_misused'],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'categories_resolutions_misused',
|
||||
type: 'button',
|
||||
label: 'Resolutions',
|
||||
weight: 702,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'resourcepack',
|
||||
message: async () =>
|
||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||
(await import('../messages/categories/resolutions_misused.md?raw')).default,
|
||||
disablesActions: ['categories_inaccurate', 'categories_optimization_misused'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default categories
|
||||
109
packages/moderation/src/data/stages/description.ts
Normal file
109
packages/moderation/src/data/stages/description.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { LibraryIcon } from '@modrinth/assets'
|
||||
|
||||
const description: Stage = {
|
||||
title: 'Is the description sufficient, accurate, and accessible?',
|
||||
id: 'description',
|
||||
icon: LibraryIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#general-expectations',
|
||||
navigate: '/',
|
||||
actions: [
|
||||
{
|
||||
id: 'description_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient (custom)',
|
||||
weight: 400,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/insufficient.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Please elaborate on how the author can improve their description.',
|
||||
variable: 'EXPLAINER',
|
||||
large: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_insufficient_packs',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 401,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/description/insufficient-packs.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_insufficient_projects',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 401,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => project.project_type !== 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/description/insufficient-projects.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_non_english',
|
||||
type: 'button',
|
||||
label: 'Non-english',
|
||||
weight: 402,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/non-english.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_unfinished',
|
||||
type: 'button',
|
||||
label: 'Unfinished',
|
||||
weight: 403,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/description/unfinished.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_headers_as_body',
|
||||
type: 'button',
|
||||
label: 'Headers as body text',
|
||||
weight: 404,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/description/headers-as-body.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_image_only',
|
||||
type: 'button',
|
||||
label: 'Image-only',
|
||||
weight: 405,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/image-only.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_non_standard_text',
|
||||
type: 'button',
|
||||
label: 'Non-standard text',
|
||||
weight: 406,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/description/non-standard-text.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_clarity',
|
||||
type: 'button',
|
||||
label: 'Unclear / Misleading',
|
||||
weight: 407,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/description/clarity.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default description
|
||||
34
packages/moderation/src/data/stages/gallery.ts
Normal file
34
packages/moderation/src/data/stages/gallery.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { ImageIcon } from '@modrinth/assets'
|
||||
|
||||
const gallery: Stage = {
|
||||
title: "Are this project's gallery images sufficient?",
|
||||
id: 'gallery',
|
||||
icon: ImageIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#general-expectations',
|
||||
navigate: '/gallery',
|
||||
actions: [
|
||||
{
|
||||
id: 'gallery_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 900,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/gallery/insufficient.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'gallery_not_relevant',
|
||||
type: 'button',
|
||||
label: 'Not relevant',
|
||||
weight: 901,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.gallery && project.gallery.length > 0,
|
||||
message: async () => (await import('../messages/gallery/not-relevant.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default gallery
|
||||
84
packages/moderation/src/data/stages/license.ts
Normal file
84
packages/moderation/src/data/stages/license.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { BookTextIcon } from '@modrinth/assets'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const licensesNotRequiringSource: string[] = [
|
||||
'LicenseRef-All-Rights-Reserved',
|
||||
'Apache-2.0',
|
||||
'BSD-2-Clause',
|
||||
'BSD-3-Clause',
|
||||
'CC0-1.0',
|
||||
'CC-BY-4.0',
|
||||
'CC-BY-SA-4.0',
|
||||
'CC-BY-NC-4.0',
|
||||
'CC-BY-NC-SA-4.0',
|
||||
'CC-BY-ND-4.0',
|
||||
'CC-BY-NC-ND-4.0',
|
||||
'ISC',
|
||||
'MIT',
|
||||
'Zlib',
|
||||
]
|
||||
|
||||
const licenseStage: Stage = {
|
||||
title: 'Is this license and link valid?',
|
||||
text: async () => (await import('../messages/checklist-text/licensing.md?raw')).default,
|
||||
id: 'license',
|
||||
icon: BookTextIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings/license',
|
||||
actions: [
|
||||
{
|
||||
id: 'license_invalid_link',
|
||||
type: 'button',
|
||||
label: 'Invalid Link',
|
||||
weight: 600,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => Boolean(project.license.url),
|
||||
message: async () => (await import('../messages/license/invalid_link.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_invalid_link-custom_license',
|
||||
type: 'toggle',
|
||||
label: 'Invalid Link: Custom License',
|
||||
weight: 601,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/license/invalid_link-custom_license.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'license_no_source',
|
||||
type: 'conditional-button',
|
||||
label: 'No Source',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => !licensesNotRequiringSource.includes(project.license.id),
|
||||
messageVariants: [
|
||||
{
|
||||
conditions: {
|
||||
excludedActions: ['license_no_source-fork'],
|
||||
},
|
||||
weight: 602,
|
||||
message: async () => (await import('../messages/license/no_source.md?raw')).default,
|
||||
},
|
||||
],
|
||||
fallbackWeight: 602,
|
||||
fallbackMessage: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_no_source-fork',
|
||||
type: 'toggle',
|
||||
label: 'No Source: Fork',
|
||||
weight: 602,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/license/no_source-fork.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default licenseStage
|
||||
88
packages/moderation/src/data/stages/links.ts
Normal file
88
packages/moderation/src/data/stages/links.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { LinkIcon } from '@modrinth/assets'
|
||||
|
||||
const links: Stage = {
|
||||
title: "Are the project's links accurate and accessible?",
|
||||
id: 'links',
|
||||
icon: LinkIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules',
|
||||
navigate: '/settings/links',
|
||||
shouldShow: (project) =>
|
||||
Boolean(
|
||||
project.issues_url ||
|
||||
project.source_url ||
|
||||
project.wiki_url ||
|
||||
project.discord_url ||
|
||||
project.donation_urls.length > 0,
|
||||
),
|
||||
text: async (project) => {
|
||||
let text = (await import('../messages/checklist-text/links/base.md?raw')).default
|
||||
|
||||
if (project.donation_urls.length > 0) {
|
||||
text += (await import('../messages/checklist-text/links/donation/donations.md?raw')).default
|
||||
|
||||
for (const donation of project.donation_urls) {
|
||||
text += (await import(`../messages/checklist-text/links/donation/donation.md?raw`)).default
|
||||
.replace('{URL}', donation.url)
|
||||
.replace('{PLATFORM}', donation.platform)
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'links_misused',
|
||||
type: 'button',
|
||||
label: 'Links are misused',
|
||||
weight: 500,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/links/misused.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'What links are misused?',
|
||||
variable: 'MISUSED_LINKS',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'links_unaccessible',
|
||||
type: 'button',
|
||||
label: 'Links are inaccessible',
|
||||
weight: 510,
|
||||
suggestedStatus: 'flagged',
|
||||
// Theoretically a conditional could go here to prevent overlap of misuse and unaccessible messages repeating while still allowing for a multi-select in each.
|
||||
// if links_misused was selected, send nothing.
|
||||
message: async () => (await import('../messages/links/not_accessible.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'links_unaccessible_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Warn of inaccessible link?',
|
||||
shouldShow: (project) => Boolean(project.source_url || project.discord_url),
|
||||
options: [
|
||||
{
|
||||
label: 'Source',
|
||||
weight: 511,
|
||||
shouldShow: (project) => Boolean(project.source_url),
|
||||
message: async () =>
|
||||
(await import('../messages/links/not_accessible-source.md?raw')).default,
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
weight: 512,
|
||||
shouldShow: (project) => Boolean(project.discord_url),
|
||||
message: async () =>
|
||||
(await import('../messages/links/not_accessible-discord.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default links
|
||||
111
packages/moderation/src/data/stages/reupload.ts
Normal file
111
packages/moderation/src/data/stages/reupload.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { CopyrightIcon } from '@modrinth/assets'
|
||||
|
||||
const reupload: Stage = {
|
||||
title: 'Does the author have proper permissions to post this project?',
|
||||
id: 'reupload',
|
||||
icon: CopyrightIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules',
|
||||
actions: [
|
||||
{
|
||||
id: 'reupload_reupload',
|
||||
type: 'button',
|
||||
label: 'Re-upload',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/reupload.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'What is the title of the original project?',
|
||||
variable: 'ORIGINAL_PROJECT',
|
||||
required: true,
|
||||
suggestions: ['Vanilla Tweaks'],
|
||||
},
|
||||
{
|
||||
label: 'What is the author of the original project?',
|
||||
variable: 'ORIGINAL_AUTHOR',
|
||||
required: true,
|
||||
suggestions: ['Vanilla Tweaks Team'],
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_unclear_fork',
|
||||
type: 'button',
|
||||
label: 'Unclear Fork',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/fork.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_insufficient_fork',
|
||||
type: 'button',
|
||||
label: 'Insufficient Fork',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/insufficient_fork.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_reupload',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_request_proof',
|
||||
type: 'button',
|
||||
label: 'Proof of permissions',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/proof_of_permissions.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'reupload_identity_verification',
|
||||
type: 'button',
|
||||
label: 'Verify Identity',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/identity_verification.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Where else can the project be found?',
|
||||
variable: 'PLATFORM',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default reupload
|
||||
33
packages/moderation/src/data/stages/rule-following.ts
Normal file
33
packages/moderation/src/data/stages/rule-following.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { ListBulletedIcon } from '@modrinth/assets'
|
||||
|
||||
const ruleFollowing: Stage = {
|
||||
title: 'Does this project violate the rules?',
|
||||
id: 'rule-following',
|
||||
icon: ListBulletedIcon,
|
||||
guidance_url:
|
||||
'https://www.notion.so/Creator-Communication-Guide-1b65ee711bf080ec9337e3ccdded146c',
|
||||
navigate: '/moderation',
|
||||
actions: [
|
||||
{
|
||||
id: 'rule_breaking_yes',
|
||||
type: 'button',
|
||||
label: 'Yes',
|
||||
weight: 0,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
message: async () => (await import('../messages/rule-breaking.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Please explain to the user how it infringes on our content rules.',
|
||||
variable: 'MESSAGE',
|
||||
required: true,
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default ruleFollowing
|
||||
37
packages/moderation/src/data/stages/side-types.ts
Normal file
37
packages/moderation/src/data/stages/side-types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { GlobeIcon } from '@modrinth/assets'
|
||||
|
||||
const sideTypes: Stage = {
|
||||
title: "Is the project's environment information accurate?",
|
||||
id: 'environment',
|
||||
icon: GlobeIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings',
|
||||
text: async () => (await import('../messages/checklist-text/side_types.md?raw')).default,
|
||||
actions: [
|
||||
{
|
||||
id: 'side_types_inaccurate_modpack',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 800,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/side-types/inaccurate-modpack.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'side_types_inaccurate_mod',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 800,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'mod',
|
||||
message: async () => (await import('../messages/side-types/inaccurate-mod.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default sideTypes
|
||||
88
packages/moderation/src/data/stages/status-alerts.ts
Normal file
88
packages/moderation/src/data/stages/status-alerts.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction, DropdownAction, DropdownActionOption } from '../../types/actions'
|
||||
import { TriangleAlertIcon } from '@modrinth/assets'
|
||||
|
||||
const statusAlerts: Stage = {
|
||||
title: `Is anything else affecting this project's status?`,
|
||||
id: 'status-alerts',
|
||||
icon: TriangleAlertIcon,
|
||||
text: async () => (await import('../messages/checklist-text/status-alerts/text.md?raw')).default,
|
||||
guidance_url:
|
||||
'https://www.notion.so/Project-Modification-Guidelines-22e5ee711bf080628416f0471ba6af02',
|
||||
navigate: '/moderation',
|
||||
actions: [
|
||||
{
|
||||
id: 'status_corrections_applied',
|
||||
type: 'button',
|
||||
label: 'Corrections applied',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'approved',
|
||||
disablesActions: ['status_private_use', 'status_account_issues'],
|
||||
message: async () => (await import('../messages/status-alerts/fixed.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_private_use',
|
||||
type: 'button',
|
||||
label: 'Private use',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'flagged',
|
||||
disablesActions: ['status_corrections_applied', 'status_account_issues'],
|
||||
message: async () => (await import('../messages/status-alerts/private.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_account_issues',
|
||||
type: 'button',
|
||||
label: 'Account issues',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'rejected',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/account_issues.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_tec_source_request',
|
||||
type: 'button',
|
||||
label: `Request Source`,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
shouldShow: (project) =>
|
||||
project.project_type === 'mod' ||
|
||||
project.project_type === 'shader' ||
|
||||
project.project_type.toString() === 'plugin',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'status_tec_source_request_options',
|
||||
type: 'dropdown',
|
||||
label: 'Why are you requesting source?',
|
||||
options: [
|
||||
{
|
||||
label: 'Obfuscated',
|
||||
weight: 999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-obfs.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Binaries',
|
||||
weight: 999000,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-bins.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_automod_confusion',
|
||||
type: 'button',
|
||||
label: `Automod confusion`,
|
||||
weight: -999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/automod_confusion.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default statusAlerts
|
||||
53
packages/moderation/src/data/stages/summary.ts
Normal file
53
packages/moderation/src/data/stages/summary.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { AlignLeftIcon } from '@modrinth/assets'
|
||||
|
||||
const summary: Stage = {
|
||||
title: "Is the project's summary sufficient?",
|
||||
text: async () => (await import('../messages/checklist-text/summary/summary.md?raw')).default,
|
||||
id: 'summary',
|
||||
icon: AlignLeftIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
actions: [
|
||||
{
|
||||
id: 'summary_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 300,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
disablesActions: ['summary_repeat_title'],
|
||||
message: async () => (await import('../messages/summary/insufficient.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_repeat_title',
|
||||
type: 'button',
|
||||
label: 'Repeat of title',
|
||||
weight: 300,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
disablesActions: ['summary_insufficient'],
|
||||
message: async () => (await import('../messages/summary/repeat-title.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_formatting',
|
||||
type: 'button',
|
||||
label: 'Formatting',
|
||||
weight: 301,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/summary/formatting.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_non_english',
|
||||
type: 'button',
|
||||
label: 'Non-english',
|
||||
weight: 302,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/summary/non-english.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default summary
|
||||
96
packages/moderation/src/data/stages/title-slug.ts
Normal file
96
packages/moderation/src/data/stages/title-slug.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { BookOpenIcon } from '@modrinth/assets'
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { Project } from '@modrinth/utils'
|
||||
|
||||
function hasCustomSlug(project: Project): boolean {
|
||||
return (
|
||||
project.slug !==
|
||||
project.title
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
)
|
||||
}
|
||||
|
||||
const titleSlug: Stage = {
|
||||
title: 'Are the Name and URL accurate and appropriate?',
|
||||
id: 'title-&-slug',
|
||||
text: async (project) => {
|
||||
let text = (await import('../messages/checklist-text/title-slug/title.md?raw')).default
|
||||
if (hasCustomSlug(project))
|
||||
text += (await import('../messages/checklist-text/title-slug/slug.md?raw')).default
|
||||
return text
|
||||
},
|
||||
icon: BookOpenIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
actions: [
|
||||
{
|
||||
id: 'title_useless_info',
|
||||
type: 'button',
|
||||
label: 'Contains useless info',
|
||||
weight: 100,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/title/useless-info.md?raw')).default,
|
||||
},
|
||||
{
|
||||
id: 'title_minecraft_branding',
|
||||
type: 'button',
|
||||
label: 'Minecraft title',
|
||||
weight: 100,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/title/minecraft-branding.md?raw')).default,
|
||||
},
|
||||
{
|
||||
id: 'title_similarities',
|
||||
type: 'button',
|
||||
label: 'Title similarities',
|
||||
weight: 110,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/title/similarities.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'title_similarities_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Similarities additional info',
|
||||
options: [
|
||||
{
|
||||
label: 'Modpack named after mod',
|
||||
weight: 111,
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/title/similarities-modpack.md?raw')).default,
|
||||
},
|
||||
{
|
||||
label: 'Forked project',
|
||||
weight: 112,
|
||||
message: async () =>
|
||||
(await import('../messages/title/similarities-fork.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slug_misused_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Slug issues?',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => hasCustomSlug(project),
|
||||
options: [
|
||||
{
|
||||
label: 'Misused',
|
||||
weight: 200,
|
||||
message: async () => (await import('../messages/slug/misused.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default titleSlug
|
||||
24
packages/moderation/src/data/stages/undefined-project.ts
Normal file
24
packages/moderation/src/data/stages/undefined-project.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const undefinedProjectStage: Stage = {
|
||||
title: 'This project is undefined!',
|
||||
id: 'undefined-project',
|
||||
icon: XIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/versions',
|
||||
shouldShow: (project) => project.versions.length === 0,
|
||||
actions: [
|
||||
{
|
||||
id: 'undefined_no_versions',
|
||||
type: 'button',
|
||||
label: 'No Versions',
|
||||
weight: -100,
|
||||
suggestedStatus: 'rejected',
|
||||
message: async () =>
|
||||
(await import('../messages/undefined-project/no_versions.md?raw')).default,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default undefinedProjectStage
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user