coolbot 13103b4950
various moderation fixes and improvements (#4061)
* Typo correction

* show optimization button when present in additional categories

* add more formatted link shortcuts

* Add info text to env info stage

* Only show gallery relevancy button when relevant.

* add unsupported project type message to versions stage

* Fix misuse of slug message.

* Update unsupported_project.md

* lint fix
2025-07-28 12:56:47 +00:00

345 lines
12 KiB
TypeScript

import type { Project } from '@modrinth/utils'
import type {
Action,
AdditionalTextInput,
ButtonAction,
ConditionalMessage,
ToggleAction,
} from './types/actions'
export interface ActionState {
selected: boolean
value?: Set<number> | number | string | unknown
}
export interface MessagePart {
weight: number
content: string
actionId: string
stageIndex: number
}
export type SerializedActionState = {
isSet?: boolean
} & ActionState
export function getActionIdForStage(
action: Action,
stageIndex: number,
actionIndex?: number,
enabledIndex?: number,
): string {
if (action.id) {
return `stage-${stageIndex}-${action.id}`
}
const suffix = enabledIndex !== undefined ? `-enabled-${enabledIndex}` : ''
return `stage-${stageIndex}-action-${actionIndex}${suffix}`
}
export function getActionId(action: Action, currentStage: number, index?: number): string {
return getActionIdForStage(action, currentStage, index)
}
export function getActionKey(
action: Action,
currentStage: number,
visibleActions: Action[],
): string {
const index = visibleActions.indexOf(action)
return `${currentStage}-${index}-${getActionId(action, currentStage)}`
}
export function serializeActionStates(states: Record<string, ActionState>): string {
const serializable: Record<string, SerializedActionState> = {}
for (const [key, state] of Object.entries(states)) {
serializable[key] = {
selected: state.selected,
value: state.value instanceof Set ? Array.from(state.value) : state.value,
isSet: state.value instanceof Set,
}
}
return JSON.stringify(serializable)
}
export function deserializeActionStates(data: string): Record<string, ActionState> {
try {
const parsed = JSON.parse(data)
const states: Record<string, ActionState> = {}
for (const [key, state] of Object.entries(parsed as Record<string, SerializedActionState>)) {
states[key] = {
selected: state.selected,
value: state.isSet ? new Set(state.value as unknown[]) : state.value,
}
}
return states
} catch {
return {}
}
}
export function initializeActionState(action: Action): ActionState {
if (action.type === 'toggle') {
return {
selected: action.defaultChecked || false,
}
} else if (action.type === 'dropdown') {
return {
selected: true,
value: action.defaultOption || 0,
}
} else if (action.type === 'multi-select-chips') {
return {
selected: false,
value: new Set<number>(),
}
} else {
return {
selected: false,
}
}
}
export function processMessage(
message: string,
action: Action,
stageIndex: number,
textInputValues: Record<string, string>,
): string {
let processedMessage = message
if (action.relevantExtraInput) {
action.relevantExtraInput.forEach((input, index) => {
if (input.variable) {
const inputKey = `stage-${stageIndex}-${action.id || `action-${index}`}-${index}`
const value = textInputValues[inputKey] || ''
const regex = new RegExp(`%${input.variable}%`, 'g')
processedMessage = processedMessage.replace(regex, value)
}
})
}
return processedMessage
}
export function findMatchingVariant(
variants: ConditionalMessage[],
selectedActionIds: string[],
allValidActionIds?: string[],
currentStageIndex?: number,
): ConditionalMessage | null {
for (const variant of variants) {
const conditions = variant.conditions
const meetsRequired =
!conditions.requiredActions ||
conditions.requiredActions.every((id) => {
let fullId = id
if (currentStageIndex !== undefined && !id.startsWith('stage-')) {
fullId = `stage-${currentStageIndex}-${id}`
}
if (allValidActionIds && !allValidActionIds.includes(fullId)) {
return false
}
return selectedActionIds.includes(fullId)
})
const meetsExcluded =
!conditions.excludedActions ||
!conditions.excludedActions.some((id) => {
let fullId = id
if (currentStageIndex !== undefined && !id.startsWith('stage-')) {
fullId = `stage-${currentStageIndex}-${id}`
}
return selectedActionIds.includes(fullId)
})
if (meetsRequired && meetsExcluded) {
return variant
}
}
return null
}
export async function getActionMessage(
action: ButtonAction | ToggleAction,
selectedActionIds: string[],
allValidActionIds?: string[],
): Promise<string> {
if (action.conditionalMessages && action.conditionalMessages.length > 0) {
const matchingConditional = findMatchingVariant(
action.conditionalMessages,
selectedActionIds,
allValidActionIds,
)
if (matchingConditional) {
return (await matchingConditional.message()) as string
}
}
return (await action.message()) as string
}
export function getVisibleInputs(
action: Action,
actionStates: Record<string, ActionState>,
): AdditionalTextInput[] {
if (!action.relevantExtraInput) return []
const selectedActionIds = Object.entries(actionStates)
.filter(([, state]) => state.selected)
.map(([id]) => id)
return action.relevantExtraInput.filter((input) => {
if (!input.showWhen) return true
const meetsRequired =
!input.showWhen.requiredActions ||
input.showWhen.requiredActions.every((id) => selectedActionIds.includes(id))
const meetsExcluded =
!input.showWhen.excludedActions ||
!input.showWhen.excludedActions.some((id) => selectedActionIds.includes(id))
return meetsRequired && meetsExcluded
})
}
export function expandVariables(
template: string,
project: Project,
variables?: Record<string, string>,
): string {
if (!variables) {
variables = flattenProjectVariables(project)
}
return Object.entries(variables).reduce((result, [key, value]) => {
const variable = `%${key}%`
return result.replace(new RegExp(variable, 'g'), value)
}, template)
}
export function kebabToTitleCase(input: string): string {
return input
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
export function arrayOrNone(arr: string[]): string {
return arr.length > 0 ? arr.join(', ') : 'None'
}
export function flattenProjectVariables(project: Project): Record<string, string> {
const vars: Record<string, string> = {}
vars['PROJECT_ID'] = project.id
vars['PROJECT_TYPE'] = project.project_type
vars['PROJECT_SLUG'] = project.slug
vars['PROJECT_TITLE'] = project.title
vars['PROJECT_SUMMARY'] = project.description
vars['PROJECT_STATUS'] = project.status
vars['PROJECT_REQUESTED_STATUS'] = project.requested_status
vars['PROJECT_MONETIZATION_STATUS'] = project.monetization_status
vars['PROJECT_BODY'] = project.body
vars['PROJECT_ICON_URL'] = project.icon_url || ''
vars['PROJECT_ISSUES_URL'] = project.issues_url || 'None'
vars['PROJECT_SOURCE_URL'] = project.source_url || 'None'
vars['PROJECT_WIKI_URL'] = project.wiki_url || 'None'
vars['PROJECT_DISCORD_URL'] = project.discord_url || 'None'
vars['PROJECT_DOWNLOADS'] = project.downloads.toString()
vars['PROJECT_FOLLOWERS'] = project.followers.toString()
vars['PROJECT_COLOR'] = project.color?.toString() || ''
vars['PROJECT_CLIENT_SIDE'] = project.client_side
vars['PROJECT_SERVER_SIDE'] = project.server_side
vars['PROJECT_TEAM'] = project.team
vars['PROJECT_THREAD_ID'] = project.thread_id
vars['PROJECT_ORGANIZATION'] = project.organization
vars['PROJECT_PUBLISHED'] = project.published
vars['PROJECT_UPDATED'] = project.updated
vars['PROJECT_APPROVED'] = project.approved
vars['PROJECT_QUEUED'] = project.queued
vars['PROJECT_LICENSE_ID'] = project.license.id
vars['PROJECT_LICENSE_NAME'] = project.license.name
vars['PROJECT_LICENSE_URL'] = project.license.url || 'None'
vars['PROJECT_CATEGORIES'] = arrayOrNone(project.categories)
vars['PROJECT_ADDITIONAL_CATEGORIES'] = arrayOrNone(project.additional_categories)
vars['PROJECT_GAME_VERSIONS'] = arrayOrNone(project.game_versions)
vars['PROJECT_LOADERS'] = arrayOrNone(project.loaders)
vars['PROJECT_VERSIONS'] = arrayOrNone(project.versions)
vars['PROJECT_CATEGORIES_COUNT'] = project.categories.length.toString()
vars['PROJECT_GAME_VERSIONS_COUNT'] = project.game_versions.length.toString()
vars['PROJECT_LOADERS_COUNT'] = project.loaders.length.toString()
vars['PROJECT_VERSIONS_COUNT'] = project.versions.length.toString()
vars['PROJECT_GALLERY_COUNT'] = (project.gallery?.length || 0).toString()
vars['PROJECT_DONATION_URLS_COUNT'] = project.donation_urls.length.toString()
project.donation_urls.forEach((donation, index) => {
vars[`PROJECT_DONATION_${index}_ID`] = donation.id
vars[`PROJECT_DONATION_${index}_PLATFORM`] = donation.platform
vars[`PROJECT_DONATION_${index}_URL`] = donation.url
})
project.gallery?.forEach((image, index) => {
vars[`PROJECT_GALLERY_${index}_URL`] = image.url
vars[`PROJECT_GALLERY_${index}_TITLE`] = image.title || ''
vars[`PROJECT_GALLERY_${index}_DESCRIPTION`] = image.description || ''
vars[`PROJECT_GALLERY_${index}_FEATURED`] = image.featured.toString()
})
// Static time saving stuff
vars[`RULES`] = `[Modrinth's Content Rules](https://modrinth.com/legal/rules)`
vars[`TOS`] = `[Terms of Use](https://modrinth.com/legal/terms)`
vars[`COPYRIGHT_POLICY`] = `[Copyright Policy](https://modrinth.com/legal/copyright)`
vars[`SUPPORT`] =
`please visit the [Modrinth Help Center](https://support.modrinth.com/) and click the green bubble to contact support.`
vars[`MODPACK_PERMISSIONS_GUIDE`] =
`our guide to [Obtaining Modpack Permissions](https://support.modrinth.com/en/articles/8797527-obtaining-modpack-permissions)`
vars[`MODPACKS_ON_MODRINTH`] =
`[Modpacks on Modrinth](https://support.modrinth.com/en/articles/8802250-modpacks-on-modrinth)`
vars[`ADVANCED_MARKDOWN`] =
`[Markdown Formatting Guide](https://support.modrinth.com/en/articles/8801962-advanced-markdown-formatting)`
vars[`LICENSING_GUIDE`] =
`our guide to [Guide to Licensing your Mods](https://modrinth.com/news/article/licensing-guide)`
// Navigation related variables
vars[`PROJECT_PERMANENT_LINK`] = `https://modrinth.com/project/${project.id}`
vars[`PROJECT_SETTINGS_LINK`] = `https://modrinth.com/project/${project.id}/settings`
vars[`PROJECT_SETTINGS_FLINK`] = `[Settings](https://modrinth.com/project/${project.id}/settings)`
vars[`PROJECT_TITLE_FLINK`] = `[Name](https://modrinth.com/project/${project.id}/settings)`
vars[`PROJECT_SLUG_FLINK`] = `[URL](https://modrinth.com/project/${project.id}/settings)`
vars[`PROJECT_SUMMARY_FLINK`] = `[Summary](https://modrinth.com/project/${project.id}/settings)`
vars[`PROJECT_ENVIRONMENT_FLINK`] =
`[Environment Information](https://modrinth.com/project/${project.id}/settings)`
vars[`PROJECT_TAGS_LINK`] = `https://modrinth.com/project/${project.id}/settings/tags`
vars[`PROJECT_TAGS_FLINK`] = `[Tags](https://modrinth.com/project/${project.id}/settings/tags)`
vars[`PROJECT_DESCRIPTION_LINK`] =
`https://modrinth.com/project/${project.id}/settings/description`
vars[`PROJECT_DESCRIPTION_FLINK`] =
`[Description](https://modrinth.com/project/${project.id}/settings/description)`
vars[`PROJECT_LICENSE_LINK`] = `https://modrinth.com/project/${project.id}/license`
vars[`PROJECT_LICENSE_FLINK`] = `[License](https://modrinth.com/project/${project.id}/license`
vars[`PROJECT_LINKS_LINK`] = `https://modrinth.com/project/${project.id}/settings/links`
vars[`PROJECT_LINKS_FLINK`] = `[Links](https://modrinth.com/project/${project.id}/settings/links)`
vars[`PROJECT_GALLERY_LINK`] = `https://modrinth.com/project/${project.id}/gallery`
vars[`PROJECT_GALLERY_FLINK`] = `[Gallery](https://modrinth.com/project/${project.id}/gallery)`
vars[`PROJECT_VERSIONS_LINK`] = `https://modrinth.com/project/${project.id}/versions`
vars[`PROJECT_VERSIONS_FLINK`] = `[Versions](https://modrinth.com/project/${project.id}/versions)`
vars[`PROJECT_MODERATION_LINK`] = `https://modrinth.com/project/${project.id}/moderation`
vars[`PROJECT_MODERATION_FLINK`] =
`[moderation tab](https://modrinth.com/project/${project.id}/moderation)`
return vars
}