mirror of
https://github.com/modrinth/code.git
synced 2026-08-01 21:55:54 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
199b2b1fcf | ||
|
|
9849ffaa1f | ||
|
|
43a7688b53 | ||
|
|
399529fdd8 |
@@ -65,7 +65,6 @@ import IncompatibilityWarningModal from '@/components/ui/install_flow/Incompatib
|
||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
|
||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
@@ -78,7 +77,7 @@ import UpdateToast from '@/components/ui/UpdateToast.vue'
|
||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||
@@ -272,11 +271,12 @@ async function setupApp() {
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
})
|
||||
|
||||
if (telemetry) {
|
||||
initAnalytics()
|
||||
if (dev) debugAnalytics()
|
||||
trackEvent('Launched', { version, dev, onboarded })
|
||||
initAnalytics()
|
||||
if (!telemetry) {
|
||||
optOutAnalytics()
|
||||
}
|
||||
if (dev) debugAnalytics()
|
||||
trackEvent('Launched', { version, dev, onboarded })
|
||||
|
||||
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
|
||||
|
||||
@@ -389,7 +389,6 @@ loading.setEnabled(false)
|
||||
|
||||
const error = useError()
|
||||
const errorModal = ref()
|
||||
const minecraftAuthErrorModal = ref()
|
||||
|
||||
const install = useInstall()
|
||||
const modInstallModal = ref()
|
||||
@@ -468,7 +467,6 @@ onMounted(() => {
|
||||
invoke('show_window')
|
||||
|
||||
error.setErrorModal(errorModal.value)
|
||||
error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value)
|
||||
|
||||
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
||||
install.setInstallConfirmModal(installConfirmModal)
|
||||
@@ -1134,7 +1132,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<I18nDebugPanel />
|
||||
<NotificationPanel has-sidebar />
|
||||
<ErrorModal ref="errorModal" />
|
||||
<MinecraftAuthErrorModal ref="minecraftAuthErrorModal" />
|
||||
<ModInstallModal ref="modInstallModal" />
|
||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||
<InstallConfirmModal ref="installConfirmModal" />
|
||||
|
||||
@@ -33,7 +33,6 @@ const metadata = ref({})
|
||||
|
||||
defineExpose({
|
||||
async show(errorVal, context, canClose = true, source = null) {
|
||||
console.log(errorVal, context, canClose, source)
|
||||
closable.value = canClose
|
||||
|
||||
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||
|
||||
@@ -49,14 +49,19 @@ const creatingInstance = ref(false)
|
||||
const profiles = ref([])
|
||||
|
||||
const shownProfiles = computed(() =>
|
||||
profiles.value.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
}),
|
||||
profiles.value
|
||||
.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
})
|
||||
.filter((profile) => {
|
||||
const version = {
|
||||
game_versions: versions.value.flatMap((v) => v.game_versions),
|
||||
loaders: versions.value.flatMap((v) => v.loaders),
|
||||
}
|
||||
return isVersionCompatible(version, project.value, profile)
|
||||
}),
|
||||
)
|
||||
|
||||
const isProfileCompatible = (profile) =>
|
||||
versions.value?.some((version) => isVersionCompatible(version, project.value, profile))
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
@@ -159,13 +164,13 @@ const createInstance = async () => {
|
||||
const gameVersion = gameVersions[0]
|
||||
|
||||
const loaders = versions.value[0].loaders
|
||||
const loader = loaders.includes('fabric')
|
||||
const loader = loaders.contains('fabric')
|
||||
? 'fabric'
|
||||
: loaders.includes('neoforge')
|
||||
: loaders.contains('neoforge')
|
||||
? 'neoforge'
|
||||
: loaders.includes('forge')
|
||||
: loaders.contains('forge')
|
||||
? 'forge'
|
||||
: loaders.includes('quilt')
|
||||
: loaders.contains('quilt')
|
||||
? 'quilt'
|
||||
: 'vanilla'
|
||||
|
||||
@@ -235,23 +240,17 @@ const createInstance = async () => {
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:disabled="
|
||||
!isProfileCompatible(profile) || profile.installedMod || profile.installing
|
||||
"
|
||||
:disabled="profile.installedMod || profile.installing"
|
||||
@click="install(profile)"
|
||||
>
|
||||
<DownloadIcon
|
||||
v-if="isProfileCompatible(profile) && !profile.installedMod && !profile.installing"
|
||||
/>
|
||||
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
||||
<CheckIcon v-else-if="profile.installedMod" />
|
||||
{{
|
||||
profile.installing
|
||||
? 'Installing...'
|
||||
: profile.installedMod
|
||||
? 'Installed'
|
||||
: !isProfileCompatible(profile)
|
||||
? 'Incompatible'
|
||||
: 'Install'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
DropdownIcon,
|
||||
LogInIcon,
|
||||
MessagesSquareIcon,
|
||||
WrenchIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, Collapsible, NewModal } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
import { type MinecraftAuthError, minecraftAuthErrors } from './minecraft-auth-errors'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const rawError = ref<string>('')
|
||||
const matchedError = ref<MinecraftAuthError | null>(null)
|
||||
const debugCollapsed = ref(true)
|
||||
const copied = ref(false)
|
||||
const loadingSignIn = ref(false)
|
||||
|
||||
function show(errorVal: { message?: string }) {
|
||||
rawError.value = errorVal?.message ?? String(errorVal)
|
||||
|
||||
matchedError.value = minecraftAuthErrors.find((e) => rawError.value.includes(e.errorCode)) ?? null
|
||||
|
||||
debugCollapsed.value = true
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
|
||||
async function signInAgain() {
|
||||
try {
|
||||
loadingSignIn.value = true
|
||||
const loggedIn = await login_flow()
|
||||
if (loggedIn) {
|
||||
await set_default_user(loggedIn.profile.id)
|
||||
}
|
||||
loadingSignIn.value = false
|
||||
modal.value?.hide()
|
||||
} catch (err) {
|
||||
loadingSignIn.value = false
|
||||
handleSevereError(err)
|
||||
}
|
||||
}
|
||||
|
||||
const debugInfo = computed(() => rawError.value || 'No error message.')
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'">
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition
|
||||
type="warning"
|
||||
body=" We couldn't sign you into your Microsoft account. This may be due to account restrictions or
|
||||
regional limitations."
|
||||
>
|
||||
</Admonition>
|
||||
|
||||
<!-- Matched error details -->
|
||||
<div class="bg-surface-2 rounded-2xl p-4 px-5 flex flex-col gap-3">
|
||||
<template v-if="matchedError">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h3 class="text-base font-bold m-0">What we think happened</h3>
|
||||
<p class="text-sm text-secondary m-0">
|
||||
{{ matchedError.whatHappened }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h3 class="text-base font-bold m-0">How to fix it</h3>
|
||||
<ol class="list-none flex flex-col gap-2 m-0 pl-0">
|
||||
<li
|
||||
v-for="(step, index) in matchedError.stepsToFix"
|
||||
:key="index"
|
||||
class="flex items-baseline gap-2"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center shrink-0 w-5 h-5 rounded-full bg-surface-4 border border-solid border-surface-5 text-xs font-medium"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span
|
||||
class="text-sm [&_a]:text-info [&_a]:font-medium [&_a]:underline"
|
||||
v-html="step"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h3 class="text-base font-bold m-0">Unknown error</h3>
|
||||
<p class="text-sm text-secondary m-0">
|
||||
We don’t recognize this error and can’t recommend specific steps to resolve it.
|
||||
</p>
|
||||
<p class="text-sm text-secondary m-0">
|
||||
Try visiting
|
||||
<a
|
||||
class="text-info font-medium underline hover:underline"
|
||||
href="https://www.minecraft.net/en-us/login"
|
||||
>Minecraft Login</a
|
||||
>
|
||||
and signing in, as it may prompt you with the necessary steps. You can also contact
|
||||
support and we can look into it further.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<a href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||
<MessagesSquareIcon /> Contact support
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="loadingSignIn" class="!w-full" @click="signInAgain">
|
||||
<LogInIcon /> Sign in again
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-full h-[1px] bg-surface-5"></div>
|
||||
|
||||
<!-- Debug info -->
|
||||
<div class="overflow-clip">
|
||||
<button
|
||||
class="flex items-center justify-between w-full bg-transparent border-0 py-4 cursor-pointer"
|
||||
@click="debugCollapsed = !debugCollapsed"
|
||||
>
|
||||
<span class="flex items-center gap-2 text-contrast font-extrabold m-0">
|
||||
<WrenchIcon class="h-4 w-4" />
|
||||
Debug information
|
||||
</span>
|
||||
<DropdownIcon
|
||||
class="h-5 w-5 text-secondary transition-transform"
|
||||
:class="{ 'rotate-180': !debugCollapsed }"
|
||||
/>
|
||||
</button>
|
||||
<Collapsible :collapsed="debugCollapsed">
|
||||
<div class="p-3 bg-surface-2 rounded-2xl text-xs flex items-start">
|
||||
<div class="m-0 p-0 rounded-none bg-transparent text-sm font-mono">
|
||||
{{ debugInfo }}
|
||||
</div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
export interface MinecraftAuthError {
|
||||
errorCode: string
|
||||
whatHappened: string
|
||||
stepsToFix: string[]
|
||||
}
|
||||
|
||||
export const minecraftAuthErrors: MinecraftAuthError[] = [
|
||||
{
|
||||
errorCode: '2148916222',
|
||||
whatHappened:
|
||||
'Your Minecraft/Xbox Live account requires age verification to comply with UK regulations. You must complete this before signing in.',
|
||||
stepsToFix: [
|
||||
'Go to the <a href="https://www.minecraft.net/en-us/login">Minecraft Login</a> page and sign in',
|
||||
'Follow the instructions to verify your age',
|
||||
'Once verified, try signing in again',
|
||||
'For additional help, visit <a href="https://support.xbox.com/en-GB/help/family-online-safety/online-safety/UK-age-verification">UK age verification on Xbox</a>',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916233',
|
||||
whatHappened: "This account doesn't have an Xbox profile set up or doesn't own Minecraft.",
|
||||
stepsToFix: [
|
||||
'Make sure Minecraft is purchased on this account',
|
||||
'Visit <a href="https://www.minecraft.net/en-us/login">Minecraft Login</a> and sign in',
|
||||
'Complete Xbox profile setup if prompted',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916235',
|
||||
whatHappened: "Xbox Live isn't available in your region, so sign-in is blocked.",
|
||||
stepsToFix: [
|
||||
'Xbox services must be supported in your country before you can sign in',
|
||||
'Check <a href="https://www.xbox.com/en-US/regions">Xbox Availability</a> for supported regions',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916236',
|
||||
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||
'Complete the identity verification process',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916237',
|
||||
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||
'Complete the identity verification process',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916238',
|
||||
whatHappened: 'This account is underage and not linked to a Microsoft family group.',
|
||||
stepsToFix: [
|
||||
'Review the <a href="https://help.minecraft.net/hc/en-us/articles/4408968616077">Family Setup Guide</a>',
|
||||
'Join or create a family group as instructed',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916227',
|
||||
whatHappened: 'This account was suspended for violating Xbox Community Standards.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://support.xbox.com">Xbox Support</a> and review the enforcement details',
|
||||
'Submit an appeal if one is available',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916229',
|
||||
whatHappened: "This account is restricted and doesn't have permission to play online.",
|
||||
stepsToFix: [
|
||||
'Have a guardian sign in to <a href="https://account.microsoft.com/family/">Microsoft Family</a>',
|
||||
'Update online play permissions',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916234',
|
||||
whatHappened: "This account hasn't accepted Xbox's Terms of Service.",
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||
'Accept the Terms if prompted',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
import { posthog } from 'posthog-js'
|
||||
|
||||
export const initAnalytics = () => {
|
||||
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
||||
persistence: 'localStorage',
|
||||
api_host: 'https://posthog.modrinth.com',
|
||||
})
|
||||
}
|
||||
|
||||
export const debugAnalytics = () => {
|
||||
posthog.debug()
|
||||
}
|
||||
|
||||
export const optOutAnalytics = () => {
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
|
||||
export const optInAnalytics = () => {
|
||||
posthog.opt_in_capturing()
|
||||
}
|
||||
|
||||
export const trackEvent = (eventName, properties) => {
|
||||
posthog.capture(eventName, properties)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { posthog } from 'posthog-js'
|
||||
|
||||
interface InstanceProperties {
|
||||
loader: string
|
||||
game_version: string
|
||||
}
|
||||
|
||||
interface ProjectProperties extends InstanceProperties {
|
||||
id: string
|
||||
project_type: string
|
||||
}
|
||||
|
||||
type AnalyticsEventMap = {
|
||||
Launched: { version: string; dev: boolean; onboarded: boolean }
|
||||
PageView: { path: string; fromPath: string; failed: unknown }
|
||||
InstanceCreate: { source: string }
|
||||
InstanceCreateStart: { source: string }
|
||||
InstancePlay: InstanceProperties & { source: string }
|
||||
InstanceStop: Partial<InstanceProperties> & { source?: string }
|
||||
InstanceDuplicate: InstanceProperties
|
||||
InstanceRepair: InstanceProperties
|
||||
InstanceSetIcon: Record<string, never>
|
||||
InstanceRemoveIcon: Record<string, never>
|
||||
InstanceUpdateAll: InstanceProperties & { count: number; selected: boolean }
|
||||
InstanceProjectUpdate: InstanceProperties & { id: string; name: string; project_type: string }
|
||||
InstanceProjectDisable: InstanceProperties & {
|
||||
id: string
|
||||
name: string
|
||||
project_type: string
|
||||
disabled: boolean
|
||||
}
|
||||
InstanceProjectRemove: InstanceProperties & { id: string; name: string; project_type: string }
|
||||
ProjectInstall: ProjectProperties & { version_id: string; title: string; source: string }
|
||||
ProjectInstallStart: { source: string }
|
||||
PackInstall: { id: string; version_id: string; title: string; source: string }
|
||||
PackInstallStart: Record<string, never>
|
||||
AccountLogIn: { source?: string }
|
||||
AccountLogOut: Record<string, never>
|
||||
JavaTest: { path: string; success: boolean }
|
||||
JavaManualSelect: { version: string }
|
||||
JavaAutoDetect: { path: string; version: string }
|
||||
}
|
||||
|
||||
export type AnalyticsEvent = keyof AnalyticsEventMap
|
||||
|
||||
let initialized = false
|
||||
|
||||
export const initAnalytics = () => {
|
||||
if (initialized) return
|
||||
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
||||
persistence: 'localStorage',
|
||||
api_host: 'https://posthog.modrinth.com',
|
||||
})
|
||||
initialized = true
|
||||
}
|
||||
|
||||
export const debugAnalytics = () => {
|
||||
if (!initialized) return
|
||||
posthog.debug()
|
||||
}
|
||||
|
||||
export const optOutAnalytics = () => {
|
||||
if (!initialized) return
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
|
||||
export const optInAnalytics = () => {
|
||||
initAnalytics()
|
||||
posthog.opt_in_capturing()
|
||||
}
|
||||
|
||||
type OptionalArgs<T> = Record<string, never> extends T ? [properties?: T] : [properties: T]
|
||||
|
||||
export const trackEvent = <E extends AnalyticsEvent>(
|
||||
eventName: E,
|
||||
...args: OptionalArgs<AnalyticsEventMap[E]>
|
||||
) => {
|
||||
if (!initialized) return
|
||||
posthog.capture(eventName, args[0])
|
||||
}
|
||||
@@ -48,7 +48,7 @@ onUnmounted(() => {
|
||||
]"
|
||||
/>
|
||||
<template v-if="instances.length > 0">
|
||||
<RouterView v-if="route.path.startsWith('/library')" :instances="instances" />
|
||||
<RouterView :instances="instances" />
|
||||
</template>
|
||||
<div v-else class="no-instance">
|
||||
<div class="icon">
|
||||
|
||||
@@ -3,24 +3,12 @@ import { defineStore } from 'pinia'
|
||||
export const useError = defineStore('errorsStore', {
|
||||
state: () => ({
|
||||
errorModal: null,
|
||||
minecraftAuthErrorModal: null,
|
||||
}),
|
||||
actions: {
|
||||
setErrorModal(ref) {
|
||||
this.errorModal = ref
|
||||
},
|
||||
setMinecraftAuthErrorModal(ref) {
|
||||
this.minecraftAuthErrorModal = ref
|
||||
},
|
||||
showError(error, context, closable = true, source = null) {
|
||||
if (
|
||||
error.message &&
|
||||
error.message.includes('Minecraft authentication error:') &&
|
||||
this.minecraftAuthErrorModal
|
||||
) {
|
||||
this.minecraftAuthErrorModal.show(error)
|
||||
return
|
||||
}
|
||||
this.errorModal.show(error, context, closable, source)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { trackEvent } from '@/helpers/analytics.js'
|
||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||
import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
|
||||
import {
|
||||
|
||||
@@ -191,20 +191,12 @@ function computeActiveIndex(): { index: number; isSubpage: boolean } {
|
||||
}
|
||||
|
||||
function getTabElement(index: number): HTMLElement | null {
|
||||
if (index === -1) return null
|
||||
if (!tabLinkElements.value?.[index]) return null
|
||||
|
||||
const container = scrollContainer.value as HTMLElement | undefined
|
||||
if (!container) return null
|
||||
|
||||
const tabs = container.querySelectorAll('.button-animation')
|
||||
const element = tabs[index] as HTMLElement | undefined
|
||||
|
||||
if (!element) return null
|
||||
|
||||
// In navigation mode, elements are NuxtLinks, but since we used querySelectorAll,
|
||||
// we already have the raw HTMLElement ($el), so no further conversion is needed.
|
||||
// In local mode, elements are already plain divs.
|
||||
return element
|
||||
// In navigation mode, elements are NuxtLinks with $el property
|
||||
// In local mode, elements are plain divs
|
||||
const element = tabLinkElements.value[index]
|
||||
return props.mode === 'navigation' ? (element as any).$el : element
|
||||
}
|
||||
|
||||
function positionSlider() {
|
||||
@@ -261,8 +253,7 @@ function animateSliderTo(newPosition: {
|
||||
sliderBottom.value = newPosition.bottom
|
||||
}
|
||||
|
||||
async function updateActiveTab() {
|
||||
await nextTick()
|
||||
function updateActiveTab() {
|
||||
const { index, isSubpage } = computeActiveIndex()
|
||||
currentActiveIndex.value = index
|
||||
subpageSelected.value = isSubpage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="font-semibold text-contrast">Loaders</span>
|
||||
<span class="font-semibold text-contrast">Loaders <span class="text-red">*</span></span>
|
||||
|
||||
<Chips
|
||||
v-model="loaderGroup"
|
||||
|
||||
@@ -77,13 +77,7 @@
|
||||
</TagItem>
|
||||
</template>
|
||||
|
||||
<TagItem
|
||||
v-if="!draftVersionLoaders.length && projectType === 'modpack'"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
>
|
||||
No mod loader
|
||||
</TagItem>
|
||||
<span v-else-if="!draftVersionLoaders.length">No loaders selected.</span>
|
||||
<span v-if="!draftVersion.loaders.length">No loaders selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-for="field in visibleFields" :key="field.name" class="flex flex-col gap-2.5">
|
||||
<div v-for="field in selectedRail?.fields" :key="field.name" class="flex flex-col gap-2.5">
|
||||
<label>
|
||||
<span class="text-md font-semibold text-contrast">
|
||||
{{ formatMessage(field.label) }}
|
||||
@@ -398,26 +398,6 @@ const isBusinessEntity = computed(() => {
|
||||
return providerDataValue.kycData?.type === 'business'
|
||||
})
|
||||
|
||||
const visibleFields = computed(() => {
|
||||
const rail = selectedRail.value
|
||||
if (!rail) return []
|
||||
|
||||
return rail.fields.filter((field) => {
|
||||
if (!field.dependsOn) return true
|
||||
|
||||
const { field: dependsOnField, value: dependsOnValue } = field.dependsOn
|
||||
const currentValue = formData.value[dependsOnField]
|
||||
|
||||
if (!currentValue) return false
|
||||
|
||||
if (Array.isArray(dependsOnValue)) {
|
||||
return dependsOnValue.includes(currentValue)
|
||||
} else {
|
||||
return currentValue === dependsOnValue
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const allRequiredFieldsFilled = computed(() => {
|
||||
const rail = selectedRail.value
|
||||
if (!rail) return false
|
||||
@@ -427,7 +407,7 @@ const allRequiredFieldsFilled = computed(() => {
|
||||
|
||||
if (rail.requiresBankName && !formData.value.bankName) return false
|
||||
|
||||
const requiredFields = visibleFields.value.filter((f) => f.required)
|
||||
const requiredFields = rail.fields.filter((f) => f.required)
|
||||
const allRequiredPresent = requiredFields.every((f) => {
|
||||
const value = formData.value[f.name]
|
||||
return value !== undefined && value !== null && value !== ''
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClipboardCopyIcon,
|
||||
CodeIcon,
|
||||
CopyIcon,
|
||||
@@ -160,15 +159,6 @@ watch(selectedFile, (newFile) => {
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
async function updateIssueDetails(data: { detail_id: string; verdict: 'safe' | 'unsafe' }[]) {
|
||||
await client.request('/moderation/tech-review/issue-detail', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
const severityOrder = { severe: 3, high: 2, medium: 1, low: 0 } as Record<string, number>
|
||||
|
||||
const detailDecisions = reactive<Map<string, 'safe' | 'malware'>>(new Map())
|
||||
@@ -403,7 +393,11 @@ async function batchMarkRemaining(verdict: 'safe' | 'unsafe') {
|
||||
|
||||
isBatchUpdating.value = true
|
||||
try {
|
||||
await updateIssueDetails(detailIds.map((detailId) => ({ detail_id: detailId, verdict })))
|
||||
await Promise.all(
|
||||
detailIds.map((detailId) =>
|
||||
client.labrinth.tech_review_internal.updateIssueDetail(detailId, { verdict }),
|
||||
),
|
||||
)
|
||||
|
||||
const decision = verdict === 'safe' ? 'safe' : 'malware'
|
||||
for (const detailId of detailIds) {
|
||||
@@ -451,7 +445,7 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
|
||||
updatingDetails.add(detailId)
|
||||
|
||||
try {
|
||||
await updateIssueDetails([{ detail_id: detailId, verdict }])
|
||||
await client.labrinth.tech_review_internal.updateIssueDetail(detailId, { verdict })
|
||||
|
||||
const decision = verdict === 'safe' ? 'safe' : 'malware'
|
||||
|
||||
@@ -490,7 +484,7 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
|
||||
for (const classGroup of groupedByClass.value) {
|
||||
const hasThisDetail = classGroup.flags.some((f) => f.detail.id === detailId)
|
||||
if (hasThisDetail && getMarkedFlagsCount(classGroup.flags) === classGroup.flags.length) {
|
||||
expandedClasses.delete(classGroup.key)
|
||||
expandedClasses.delete(classGroup.filePath)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -539,8 +533,6 @@ const expandedClasses = reactive<Set<string>>(new Set())
|
||||
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
|
||||
|
||||
interface ClassGroup {
|
||||
key: string
|
||||
jar: string | null
|
||||
filePath: string
|
||||
flags: Array<{
|
||||
issueId: string
|
||||
@@ -551,26 +543,6 @@ interface ClassGroup {
|
||||
}>
|
||||
}
|
||||
|
||||
interface JarGroup {
|
||||
key: string
|
||||
jar: string | null
|
||||
segments: string[]
|
||||
classes: ClassGroup[]
|
||||
}
|
||||
|
||||
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
|
||||
if (!jar) return []
|
||||
const segments = jar
|
||||
.split('#')
|
||||
.map((s) => decodeURIComponent(s.trim()))
|
||||
.filter((s) => s.length > 0)
|
||||
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
|
||||
if (segments.length > 0 && currentFileName && segments[0] === currentFileName) {
|
||||
return segments.slice(1)
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
const groupedByClass = computed<ClassGroup[]>(() => {
|
||||
if (!selectedFile.value) return []
|
||||
|
||||
@@ -578,20 +550,14 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
||||
|
||||
for (const issue of selectedFile.value.issues) {
|
||||
for (const detail of issue.details) {
|
||||
const classKey = `${detail.jar ?? ''}::${detail.file_path}`
|
||||
if (!classMap.has(classKey)) {
|
||||
classMap.set(classKey, {
|
||||
key: classKey,
|
||||
jar: detail.jar ?? null,
|
||||
filePath: detail.file_path,
|
||||
flags: [],
|
||||
})
|
||||
if (!classMap.has(detail.file_path)) {
|
||||
classMap.set(detail.file_path, { filePath: detail.file_path, flags: [] })
|
||||
}
|
||||
// Cast detail to include status (backend will provide this field)
|
||||
const detailWithStatus = detail as Labrinth.TechReview.Internal.ReportIssueDetail & {
|
||||
status: Labrinth.TechReview.Internal.DelphiReportIssueStatus
|
||||
}
|
||||
classMap.get(classKey)!.flags.push({
|
||||
classMap.get(detail.file_path)!.flags.push({
|
||||
issueId: issue.id,
|
||||
issueType: issue.issue_type,
|
||||
detail: detailWithStatus,
|
||||
@@ -619,35 +585,12 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const groupedByJar = computed<JarGroup[]>(() => {
|
||||
const jarMap = new Map<string, JarGroup>()
|
||||
|
||||
for (const classItem of groupedByClass.value) {
|
||||
const jarKey = classItem.jar ?? ''
|
||||
if (!jarMap.has(jarKey)) {
|
||||
jarMap.set(jarKey, {
|
||||
key: jarKey,
|
||||
jar: classItem.jar,
|
||||
segments: splitJarSegments(classItem.jar, selectedFile.value?.file_name ?? null),
|
||||
classes: [],
|
||||
})
|
||||
}
|
||||
jarMap.get(jarKey)!.classes.push(classItem)
|
||||
}
|
||||
|
||||
return Array.from(jarMap.values()).sort((a, b) => {
|
||||
const aSeverity = getHighestSeverityInClass(a.classes.flatMap((classItem) => classItem.flags))
|
||||
const bSeverity = getHighestSeverityInClass(b.classes.flatMap((classItem) => classItem.flags))
|
||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
// Auto-expand if there's only one class in the file
|
||||
watch(
|
||||
groupedByClass,
|
||||
(classes) => {
|
||||
if (classes.length === 1) {
|
||||
expandedClasses.add(classes[0].key)
|
||||
expandedClasses.add(classes[0].filePath)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -665,11 +608,11 @@ function getHighestSeverityInClass(
|
||||
)
|
||||
}
|
||||
|
||||
function toggleClass(classKey: string) {
|
||||
if (expandedClasses.has(classKey)) {
|
||||
expandedClasses.delete(classKey)
|
||||
function toggleClass(filePath: string) {
|
||||
if (expandedClasses.has(filePath)) {
|
||||
expandedClasses.delete(filePath)
|
||||
} else {
|
||||
expandedClasses.add(classKey)
|
||||
expandedClasses.add(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,234 +1097,204 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
v-for="jarGroup in groupedByJar"
|
||||
:key="jarGroup.key"
|
||||
class="border-x border-b-0 border-t-0 border-solid border-surface-3 bg-surface-2"
|
||||
v-for="(classItem, idx) in groupedByClass"
|
||||
:key="classItem.filePath"
|
||||
class="border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2"
|
||||
:class="{
|
||||
'rounded-bl-2xl rounded-br-2xl':
|
||||
idx === groupedByClass.length - 1 && remainingUnmarkedCount === 0,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="jarGroup.segments.length > 0"
|
||||
class="border-b border-solid border-surface-1 px-4 py-3"
|
||||
class="flex cursor-pointer items-center justify-between p-4 transition-colors duration-200 hover:bg-surface-4"
|
||||
@click="toggleClass(classItem.filePath)"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<template
|
||||
v-for="(segment, index) in jarGroup.segments"
|
||||
:key="`${jarGroup.key}-${index}`"
|
||||
>
|
||||
<span
|
||||
class="font-mono text-sm"
|
||||
:class="
|
||||
index === jarGroup.segments.length - 1
|
||||
? 'font-semibold text-contrast'
|
||||
: 'text-secondary'
|
||||
"
|
||||
<div class="my-auto flex items-center gap-2">
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button
|
||||
class="transition-transform"
|
||||
:class="{ 'rotate-180': expandedClasses.has(classItem.filePath) }"
|
||||
>
|
||||
{{ segment }}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="index < jarGroup.segments.length - 1"
|
||||
class="size-4 text-secondary"
|
||||
<ChevronDownIcon class="h-5 w-5 text-contrast" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
|
||||
truncateMiddle(classItem.filePath)
|
||||
}}</span>
|
||||
|
||||
<div
|
||||
class="rounded-full border-solid px-2.5 py-1"
|
||||
:class="getSeverityBadgeColor(getHighestSeverityInClass(classItem.flags))"
|
||||
>
|
||||
<span class="text-sm font-medium">{{
|
||||
capitalizeString(getHighestSeverityInClass(classItem.flags))
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-full border border-solid px-2.5 py-1 text-sm"
|
||||
:class="
|
||||
getMarkedFlagsCount(classItem.flags) === classItem.flags.length
|
||||
? 'border-green/60 bg-highlight-green text-green'
|
||||
: 'border-red/60 bg-highlight-red text-red'
|
||||
"
|
||||
>
|
||||
<CheckIcon
|
||||
v-if="getMarkedFlagsCount(classItem.flags) === classItem.flags.length"
|
||||
class="size-4"
|
||||
/>
|
||||
</template>
|
||||
{{ getMarkedFlagsCount(classItem.flags) }}/{{ classItem.flags.length }} flags
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="classItem.flags.some((f) => loadingIssues.has(f.issueId))"
|
||||
class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1"
|
||||
>
|
||||
<span class="flex items-center gap-1.5 text-sm font-medium text-secondary">
|
||||
<LoaderCircleIcon class="size-4 animate-spin" />
|
||||
Loading source...
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="classItem in jarGroup.classes"
|
||||
:key="classItem.key"
|
||||
class="border-b border-solid border-surface-1 last:border-b-0"
|
||||
>
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between p-4 transition-colors duration-200 hover:bg-surface-4"
|
||||
@click="toggleClass(classItem.key)"
|
||||
>
|
||||
<div class="my-auto flex items-center gap-2">
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button
|
||||
class="transition-transform"
|
||||
:class="{ 'rotate-180': expandedClasses.has(classItem.key) }"
|
||||
<Collapsible :collapsed="!expandedClasses.has(classItem.filePath)">
|
||||
<div class="mt-2 flex flex-col gap-2 px-4 pb-4">
|
||||
<div
|
||||
v-for="flag in classItem.flags"
|
||||
:key="`${flag.issueId}-${flag.detail.id}`"
|
||||
class="flex flex-col gap-2 rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
|
||||
>
|
||||
<div class="grid grid-cols-[1fr_auto] items-center">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
:class="{
|
||||
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
|
||||
}"
|
||||
>
|
||||
<ChevronDownIcon class="h-5 w-5 text-contrast" />
|
||||
<span class="text-base font-semibold text-contrast">{{
|
||||
flag.issueType.replace(/_/g, ' ')
|
||||
}}</span>
|
||||
<div
|
||||
class="rounded-full border-solid px-2.5 py-1"
|
||||
:class="getSeverityBadgeColor(flag.detail.severity)"
|
||||
>
|
||||
<span class="text-sm font-medium">{{
|
||||
capitalizeString(flag.detail.severity)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-40 items-center justify-center gap-2">
|
||||
<ButtonStyled
|
||||
color="brand"
|
||||
:type="
|
||||
getDetailDecision(flag.detail.id, flag.detail.status) === 'safe'
|
||||
? undefined
|
||||
: 'outlined'
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="updatingDetails.has(flag.detail.id)"
|
||||
@click="updateDetailStatus(flag.detail.id, 'safe')"
|
||||
>
|
||||
Pass
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled
|
||||
color="red"
|
||||
:type="
|
||||
getDetailDecision(flag.detail.id, flag.detail.status) === 'malware'
|
||||
? undefined
|
||||
: 'outlined'
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="updatingDetails.has(flag.detail.id)"
|
||||
@click="updateDetailStatus(flag.detail.id, 'unsafe')"
|
||||
>
|
||||
Fail
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="flag.detail.data && Object.keys(flag.detail.data).length > 0"
|
||||
class="flex flex-wrap gap-x-4 gap-y-1 pr-4 text-sm"
|
||||
>
|
||||
<div
|
||||
v-for="[key, value] in Object.entries(flag.detail.data).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
)"
|
||||
:key="key"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<span class="text-secondary">{{ key }}:</span>
|
||||
<a
|
||||
v-if="typeof value === 'string' && value.startsWith('http')"
|
||||
:href="value"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-brand-blue hover:underline"
|
||||
>
|
||||
{{ value }}
|
||||
</a>
|
||||
<span v-else class="font-mono text-contrast">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="getClassDecompiledSource(classItem)"
|
||||
class="relative inset-0 overflow-hidden rounded-lg border border-solid border-surface-5 bg-surface-4"
|
||||
>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="`Copy code`"
|
||||
class="absolute right-2 top-2 border-[1px]"
|
||||
@click="
|
||||
copyToClipboard(getClassDecompiledSource(classItem)!, classItem.filePath)
|
||||
"
|
||||
>
|
||||
<CopyIcon v-if="!showCopyFeedback.get(classItem.filePath)" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
|
||||
truncateMiddle(classItem.filePath)
|
||||
}}</span>
|
||||
|
||||
<div
|
||||
class="rounded-full border-solid px-2.5 py-1"
|
||||
:class="getSeverityBadgeColor(getHighestSeverityInClass(classItem.flags))"
|
||||
>
|
||||
<span class="text-sm font-medium">{{
|
||||
capitalizeString(getHighestSeverityInClass(classItem.flags))
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-full border border-solid px-2.5 py-1 text-sm"
|
||||
:class="
|
||||
getMarkedFlagsCount(classItem.flags) === classItem.flags.length
|
||||
? 'border-green/60 bg-highlight-green text-green'
|
||||
: 'border-red/60 bg-highlight-red text-red'
|
||||
"
|
||||
>
|
||||
<CheckIcon
|
||||
v-if="getMarkedFlagsCount(classItem.flags) === classItem.flags.length"
|
||||
class="size-4"
|
||||
/>
|
||||
{{ getMarkedFlagsCount(classItem.flags) }}/{{ classItem.flags.length }} flags
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div class="overflow-x-auto bg-surface-3 py-3">
|
||||
<div
|
||||
v-if="classItem.flags.some((f) => loadingIssues.has(f.issueId))"
|
||||
class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1"
|
||||
v-for="(line, n) in highlightCodeLines(
|
||||
getClassDecompiledSource(classItem)!,
|
||||
'java',
|
||||
)"
|
||||
:key="n"
|
||||
class="flex font-mono text-[13px] leading-[1.6]"
|
||||
>
|
||||
<span class="flex items-center gap-1.5 text-sm font-medium text-secondary">
|
||||
<LoaderCircleIcon class="size-4 animate-spin" />
|
||||
Loading source...
|
||||
</span>
|
||||
<div
|
||||
class="select-none border-0 border-r border-solid border-surface-5 px-4 py-0 text-right text-primary"
|
||||
style="min-width: 3.5rem"
|
||||
>
|
||||
{{ n + 1 }}
|
||||
</div>
|
||||
<div class="flex-1 px-4 py-0 text-primary">
|
||||
<pre v-html="line || ' '"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rounded-lg border border-solid border-surface-5 bg-surface-3 p-4">
|
||||
<p class="text-sm text-secondary">
|
||||
Source code not available or failed to decompile for this file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapsible :collapsed="!expandedClasses.has(classItem.key)">
|
||||
<div class="mt-2 flex flex-col gap-2 px-4 pb-4">
|
||||
<div
|
||||
v-for="flag in classItem.flags"
|
||||
:key="`${flag.issueId}-${flag.detail.id}`"
|
||||
class="flex flex-col gap-2 rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
|
||||
>
|
||||
<div class="grid grid-cols-[1fr_auto] items-center">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
:class="{
|
||||
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
|
||||
}"
|
||||
>
|
||||
<span class="text-base font-semibold text-contrast">{{
|
||||
flag.issueType.replace(/_/g, ' ')
|
||||
}}</span>
|
||||
<div
|
||||
class="rounded-full border-solid px-2.5 py-1"
|
||||
:class="getSeverityBadgeColor(flag.detail.severity)"
|
||||
>
|
||||
<span class="text-sm font-medium">{{
|
||||
capitalizeString(flag.detail.severity)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-40 items-center justify-center gap-2">
|
||||
<ButtonStyled
|
||||
color="brand"
|
||||
:type="
|
||||
getDetailDecision(flag.detail.id, flag.detail.status) === 'safe'
|
||||
? undefined
|
||||
: 'outlined'
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="updatingDetails.has(flag.detail.id)"
|
||||
@click="updateDetailStatus(flag.detail.id, 'safe')"
|
||||
>
|
||||
Pass
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled
|
||||
color="red"
|
||||
:type="
|
||||
getDetailDecision(flag.detail.id, flag.detail.status) === 'malware'
|
||||
? undefined
|
||||
: 'outlined'
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="updatingDetails.has(flag.detail.id)"
|
||||
@click="updateDetailStatus(flag.detail.id, 'unsafe')"
|
||||
>
|
||||
Fail
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="flag.detail.data && Object.keys(flag.detail.data).length > 0"
|
||||
class="flex flex-wrap gap-x-4 gap-y-1 pr-4 text-sm"
|
||||
>
|
||||
<div
|
||||
v-for="[key, value] in Object.entries(flag.detail.data).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
)"
|
||||
:key="key"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<span class="text-secondary">{{ key }}:</span>
|
||||
<a
|
||||
v-if="typeof value === 'string' && value.startsWith('http')"
|
||||
:href="value"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-brand-blue hover:underline"
|
||||
>
|
||||
{{ value }}
|
||||
</a>
|
||||
<span v-else class="font-mono text-contrast">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="getClassDecompiledSource(classItem)"
|
||||
class="relative inset-0 overflow-hidden rounded-lg border border-solid border-surface-5 bg-surface-4"
|
||||
>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="`Copy code`"
|
||||
class="absolute right-2 top-2 border-[1px]"
|
||||
@click="copyToClipboard(getClassDecompiledSource(classItem)!, classItem.key)"
|
||||
>
|
||||
<CopyIcon v-if="!showCopyFeedback.get(classItem.key)" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="overflow-x-auto bg-surface-3 py-3">
|
||||
<div
|
||||
v-for="(line, n) in highlightCodeLines(
|
||||
getClassDecompiledSource(classItem)!,
|
||||
'java',
|
||||
)"
|
||||
:key="n"
|
||||
class="flex font-mono text-[13px] leading-[1.6]"
|
||||
>
|
||||
<div
|
||||
class="select-none border-0 border-r border-solid border-surface-5 px-4 py-0 text-right text-primary"
|
||||
style="min-width: 3.5rem"
|
||||
>
|
||||
{{ n + 1 }}
|
||||
</div>
|
||||
<div class="flex-1 px-4 py-0 text-primary">
|
||||
<pre v-html="line || ' '"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-lg border border-solid border-surface-5 bg-surface-3 p-4"
|
||||
>
|
||||
<p class="text-sm text-secondary">
|
||||
Source code not available or failed to decompile for this file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
projectBackground: false,
|
||||
searchBackground: false,
|
||||
advancedDebugInfo: false,
|
||||
FilesRefreshButton: false,
|
||||
showProjectPageDownloadModalServersPromo: false,
|
||||
showProjectPageCreateServersTooltip: true,
|
||||
showProjectPageQuickServerButton: false,
|
||||
|
||||
@@ -97,8 +97,6 @@
|
||||
ref="downloadModal"
|
||||
:on-show="
|
||||
() => {
|
||||
debug('on-show fired')
|
||||
loadVersions()
|
||||
navigateTo({ query: route.query, hash: '#download' })
|
||||
}
|
||||
"
|
||||
@@ -390,9 +388,7 @@
|
||||
currentGameVersion &&
|
||||
!filteredRelease &&
|
||||
!filteredBeta &&
|
||||
!filteredAlpha &&
|
||||
!versionsLoading &&
|
||||
versions.length > 0
|
||||
!filteredAlpha
|
||||
"
|
||||
>
|
||||
{{
|
||||
@@ -990,7 +986,6 @@ import {
|
||||
ServersPromo,
|
||||
StyledInput,
|
||||
TagItem,
|
||||
useDebugLogger,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
@@ -1037,8 +1032,6 @@ const cosmetics = useCosmetics()
|
||||
|
||||
const { locale, formatMessage } = useVIntl()
|
||||
|
||||
const debug = useDebugLogger('DownloadModal')
|
||||
|
||||
const settingsModal = ref()
|
||||
const downloadModal = ref()
|
||||
const overTheTopDownloadAnimation = ref()
|
||||
@@ -1082,10 +1075,9 @@ const currentPlatform = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const currentPlatformText = computed(() => {
|
||||
if (!currentPlatform.value) return null
|
||||
return formatMessage(getTagMessage(currentPlatform.value, 'loader'))
|
||||
})
|
||||
const currentPlatformText = computed(() =>
|
||||
formatMessage(getTagMessage(currentPlatform.value, 'loader')),
|
||||
)
|
||||
|
||||
const releaseVersions = computed(() => {
|
||||
const set = new Set()
|
||||
@@ -1418,21 +1410,11 @@ async function getLicenseData(event) {
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const result = versions.value.filter(
|
||||
return versions.value.filter(
|
||||
(x) =>
|
||||
x.game_versions?.includes(currentGameVersion.value) &&
|
||||
(x.loaders?.includes(currentPlatform.value) || project.value.project_type === 'resourcepack'),
|
||||
x.game_versions.includes(currentGameVersion.value) &&
|
||||
(x.loaders.includes(currentPlatform.value) || project.value.project_type === 'resourcepack'),
|
||||
)
|
||||
debug('filteredVersions', {
|
||||
total: versions.value.length,
|
||||
filtered: result.length,
|
||||
currentGameVersion: currentGameVersion.value,
|
||||
currentPlatform: currentPlatform.value,
|
||||
versionsEnabled: versionsEnabled.value,
|
||||
versionsLoading: versionsV3Loading.value,
|
||||
sampleLoaders: versions.value.slice(0, 3).map((v) => v.loaders),
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
const filteredRelease = computed(() => {
|
||||
@@ -1625,10 +1607,6 @@ const versionsLoading = computed(() => versionsV3Loading.value)
|
||||
|
||||
// Load versions on demand (client-side only)
|
||||
function loadVersions() {
|
||||
debug('loadVersions called', {
|
||||
projectId: projectId.value,
|
||||
alreadyEnabled: versionsEnabled.value,
|
||||
})
|
||||
versionsEnabled.value = true
|
||||
}
|
||||
|
||||
@@ -2092,35 +2070,15 @@ if (project.value && loader !== undefined && project.value.loaders.includes(load
|
||||
userSelectedPlatform.value = loader
|
||||
}
|
||||
|
||||
if (route.hash === '#download' || version !== undefined || loader !== undefined) {
|
||||
debug('eager loadVersions from setup', { hash: route.hash, version, loader })
|
||||
loadVersions()
|
||||
}
|
||||
|
||||
watch(downloadModal, (modal) => {
|
||||
if (!modal) return
|
||||
|
||||
// route.hash returns everything in the hash string, including the # itself
|
||||
if (route.hash === '#download') {
|
||||
debug('hash #download watch fired, opening modal')
|
||||
loadVersions()
|
||||
modal.show()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
[versionsV3, _versionsV3Error],
|
||||
([data, error]) => {
|
||||
debug('versionsV3 query changed', {
|
||||
hasData: !!data,
|
||||
count: data?.length ?? 0,
|
||||
error: error?.message ?? null,
|
||||
projectId: projectId.value,
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function setProcessing() {
|
||||
// Guard against multiple submissions while mutation is pending
|
||||
if (patchStatusMutation.isPending.value) return
|
||||
|
||||
@@ -10,8 +10,5 @@ useHead({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageFilesPage
|
||||
:show-debug-info="flags.advancedDebugInfo"
|
||||
:show-refresh-button="flags.FilesRefreshButton"
|
||||
/>
|
||||
<ServersManageFilesPage :show-debug-info="flags.advancedDebugInfo" />
|
||||
</template>
|
||||
|
||||
@@ -421,6 +421,10 @@ export function createManageVersionContext(
|
||||
inferred.loaders = ['datapack']
|
||||
}
|
||||
|
||||
if (noLoaders && projectType.value === 'modpack') {
|
||||
inferred.loaders = ['minecraft']
|
||||
}
|
||||
|
||||
inferredVersionData.value = inferred
|
||||
|
||||
return inferred
|
||||
|
||||
@@ -10,14 +10,8 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-loaders',
|
||||
stageContent: markRaw(LoadersStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit loaders' : 'Loaders'),
|
||||
skip: (ctx) => {
|
||||
const inferredLoadersLength = ctx.inferredVersionData.value?.loaders?.length ?? 0
|
||||
return (
|
||||
inferredLoadersLength > 0 ||
|
||||
ctx.editingVersion.value ||
|
||||
(inferredLoadersLength === 0 && ctx.projectType.value === 'modpack')
|
||||
)
|
||||
},
|
||||
skip: (ctx) =>
|
||||
(ctx.inferredVersionData.value?.loaders?.length ?? 0) > 0 || ctx.editingVersion.value,
|
||||
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
|
||||
cannotNavigateForward: (ctx) => ctx.draftVersion.value.loaders.length === 0,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
@@ -42,22 +36,20 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0 && ctx.projectType.value !== 'modpack',
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||
onClick: () => ctx.modal.value?.setStage('metadata'),
|
||||
}),
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? {
|
||||
...ctx.saveButtonConfig(),
|
||||
disabled:
|
||||
ctx.draftVersion.value.loaders.length === 0 && ctx.projectType.value !== 'modpack',
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||
}
|
||||
: {
|
||||
label: 'Add details',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled:
|
||||
ctx.draftVersion.value.loaders.length === 0 && ctx.projectType.value !== 'modpack',
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,10 +13,6 @@ export interface FieldConfig {
|
||||
pattern?: string
|
||||
validate?: (value: string) => string | null
|
||||
autocomplete?: string
|
||||
dependsOn?: {
|
||||
field: string
|
||||
value?: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface RailConfig {
|
||||
@@ -334,10 +330,6 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
|
||||
defaultMessage: 'Enter PIX email',
|
||||
}),
|
||||
autocomplete: 'email',
|
||||
dependsOn: {
|
||||
field: 'pixAccountType',
|
||||
value: 'EMAIL',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pixPhone',
|
||||
@@ -349,10 +341,6 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
|
||||
defaultMessage: '+55...',
|
||||
}),
|
||||
autocomplete: 'tel',
|
||||
dependsOn: {
|
||||
field: 'pixAccountType',
|
||||
value: 'PHONE',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'branchCode',
|
||||
@@ -364,10 +352,6 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
|
||||
defaultMessage: 'Enter branch code',
|
||||
}),
|
||||
autocomplete: 'off',
|
||||
dependsOn: {
|
||||
field: 'pixAccountType',
|
||||
value: 'BANK_ACCOUNT',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'documentNumber',
|
||||
@@ -383,10 +367,6 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
|
||||
defaultMessage: 'Brazilian tax identification number',
|
||||
}),
|
||||
autocomplete: 'off',
|
||||
dependsOn: {
|
||||
field: 'pixAccountType',
|
||||
value: 'DOCUMENT',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,8 +8,6 @@ SITE_URL=http://localhost:3000
|
||||
# This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH
|
||||
CDN_URL=file:///tmp/modrinth
|
||||
LABRINTH_ADMIN_KEY=feedbeef
|
||||
LABRINTH_MEDAL_KEY=
|
||||
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
|
||||
RATE_LIMIT_IGNORE_KEY=feedbeef
|
||||
|
||||
DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth
|
||||
@@ -154,6 +152,6 @@ ARCHON_URL=none
|
||||
MURALPAY_API_URL=https://api.muralpay.com
|
||||
MURALPAY_API_KEY=none
|
||||
MURALPAY_TRANSFER_API_KEY=none
|
||||
MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
|
||||
MURALPAY_SOURCE_ACCOUNT_ID=none
|
||||
|
||||
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
|
||||
|
||||
@@ -8,7 +8,6 @@ SITE_URL=http://localhost:3000
|
||||
# This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH
|
||||
CDN_URL=file:///tmp/modrinth
|
||||
LABRINTH_ADMIN_KEY=feedbeef
|
||||
LABRINTH_MEDAL_KEY=
|
||||
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
|
||||
RATE_LIMIT_IGNORE_KEY=feedbeef
|
||||
|
||||
@@ -164,6 +163,6 @@ ARCHON_URL=none
|
||||
MURALPAY_API_URL=https://api-staging.muralpay.com
|
||||
MURALPAY_API_KEY=none
|
||||
MURALPAY_TRANSFER_API_KEY=none
|
||||
MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
|
||||
MURALPAY_SOURCE_ACCOUNT_ID=none
|
||||
|
||||
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
|
||||
|
||||
Generated
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON m.id = didv.project_id AND drid.key = didv.detail_key\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND (didv.verdict IS NULL OR didv.verdict = 'pending'::delphi_report_issue_status)\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Int4Array",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "30a5fa3f44e56c412d07625ea9110238c533a1994e95c805a3babc39cde23004"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id AS \"thread_id: DBThreadId\"\n FROM threads\n WHERE mod_id = $1\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "thread_id: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "555342b0ec9fb808f05a18aaeaf06fb61e968fb3379c9d0c7ad82c8747bd4256"
|
||||
}
|
||||
+5
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n drid.id AS \"id!: DelphiReportIssueDetailsId\",\n drid.issue_id AS \"issue_id!: DelphiReportIssueId\",\n drid.key AS \"key!: String\",\n drid.jar AS \"jar?: String\",\n drid.file_path AS \"file_path!: String\",\n drid.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n drid.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status) AS \"status!: DelphiStatus\"\n FROM delphi_report_issue_details drid\n INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON m.id = didv.project_id AND drid.key = didv.detail_key\n WHERE drid.issue_id = ANY($1::bigint[])\n ",
|
||||
"query": "\n SELECT\n drid.id AS \"id!: DelphiReportIssueDetailsId\",\n drid.issue_id AS \"issue_id!: DelphiReportIssueId\",\n drid.key AS \"key!: String\",\n drid.file_path AS \"file_path!: String\",\n drid.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n drid.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status) AS \"status!: DelphiStatus\"\n FROM delphi_report_issue_details drid\n INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON m.id = didv.project_id AND drid.key = didv.detail_key\n WHERE drid.issue_id = ANY($1::bigint[])\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,21 +20,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "jar?: String",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "file_path!: String",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 5,
|
||||
"name": "severity!: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -51,7 +46,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 6,
|
||||
"name": "status!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -76,12 +71,11 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "263ad3654f544ffb6061c839d49dada47fb382a76fdcabad2077fb1ef6d1010a"
|
||||
"hash": "80b52a09ca9a056251d1040936f768c266e5814c15638d455f569deed13ee7d0"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.project_id = $1\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n ) AS \"is_in_tech_review!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "is_in_tech_review!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8c80f3158fb5772adc8542cdf5419437bb8cd65723a32e587022d0c8decba68d"
|
||||
}
|
||||
Generated
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM unnest($2::text[]) AS incoming(detail_key)\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = $1 AND didv.detail_key = incoming.detail_key\n WHERE didv.project_id IS NULL\n ) AS \"has_unflagged_issue_details!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "has_unflagged_issue_details!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "997944b328b628792d84b21747f9e9c670ad40d0f89a175aedece93df1169195"
|
||||
}
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, jar, file_path, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ",
|
||||
"query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,7 +15,6 @@
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
{
|
||||
"Custom": {
|
||||
@@ -36,5 +35,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2"
|
||||
"hash": "b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4"
|
||||
}
|
||||
Generated
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n SELECT\n didws.project_id,\n didws.key,\n $1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.id = $2\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
{
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b767ca57e4d8abf164a951ce77f1e721b955fc4f2a4d4ac196611bc8d6b04706"
|
||||
}
|
||||
Generated
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict::delphi_report_issue_status AS verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n -- see delphi.rs todo comment\n dri.issue_type != '__dummy'\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "all_found!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "upserted_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ccedb120b05ff47ddc15bb4570025a8e8249050c12f7036d936f9a01f939db1f"
|
||||
}
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT ON (m.id)\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON m.id = didv.project_id AND drid.key = didv.detail_key\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND (didv.verdict IS NULL OR didv.verdict = 'pending'::delphi_report_issue_status)\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id, t.id\n ORDER BY m.id,\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MAX(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC\n LIMIT $1 OFFSET $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Int4Array",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f10a09a0fb0774dad4933e78db94bfb231020b356edbc58bdb6c5a11ad0fb4ac"
|
||||
}
|
||||
@@ -1,83 +1,83 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
COPY public.categories (id, category, project_type, icon, header, ordering) FROM stdin;
|
||||
10060 cursed 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg> categories 0
|
||||
10061 locale 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg> features 0
|
||||
10062 48x 3 resolutions 0
|
||||
1001 technology 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/></svg> categories 0
|
||||
10016 challenging 2 <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> categories 0
|
||||
1005 decoration 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> categories 0
|
||||
1006 library 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> categories 0
|
||||
1007 cursed 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg> categories 0
|
||||
1002 adventure 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg> categories 0
|
||||
10056 64x 3 resolutions 0
|
||||
10055 32x 3 resolutions 0
|
||||
10058 256x 3 resolutions 0
|
||||
10059 512x+ 3 resolutions 0
|
||||
10054 16x 3 resolutions 0
|
||||
10063 path-tracing 4 <svg viewBox="0 0 24 24" style="" fill="none" stroke="currentColor" stroke-width="2"><path d="M2.977 19.17h16.222" style="" transform="translate(-.189 -.328) scale(1.09932)"/><path d="M3.889 3.259 12 19.17l5.749-11.277" style="" transform="translate(-1.192 -.328) scale(1.09932)"/><path d="M9.865 6.192h4.623v4.623" style="" transform="scale(1.09931) rotate(-18 20.008 .02)"/></svg> features 0
|
||||
10064 realistic 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg> categories 0
|
||||
10065 medium 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path><path d="M12 20v-8"></path></svg> performance impact 0
|
||||
10066 low 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path></svg> performance impact 0
|
||||
10067 high 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path><path d="M12 20v-8"></path><path d="M17 20V8"></path></svg> performance impact 0
|
||||
10068 atmosphere 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"/><path d="M11 20v2"/><path d="M7 19v2"/></svg> features 0
|
||||
10069 fantasy 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg> categories 0
|
||||
10070 foliage 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="M12 22v-7l-2-2"/><path d="M17 8v.8A6 6 0 0 1 13.8 20v0H10v0A6.5 6.5 0 0 1 7 8h0a5 5 0 0 1 10 0Z"/><path d="m14 14-2 2"/></svg> features 0
|
||||
10071 bloom 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2h8l4 10H4L8 2Z"/><path d="M12 12v6"/><path d="M8 22v-2c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v2H8Z"/></svg> features 0
|
||||
10072 vanilla-like 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"/><path d="M17 7A5 5 0 0 0 7 7"/><path d="M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"/></svg> categories 0
|
||||
10073 cartoon 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg> categories 0
|
||||
10074 potato 4 <svg viewBox="0 0 512 512" fill="currentColor" stroke="currentColor"><g><g><path d="M218.913,116.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-6.4,4.8-11.2S222.113,120,218.913,116.8z"/></g></g><g><g><path d="M170.913,372.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C175.713,379.2,174.113,376,170.913,372.8z"/></g></g><g><g><path d="M250.913,228.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C255.713,235.2,254.113,232,250.913,228.8z"/></g></g><g><g><path d="M410.913,212.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C415.713,219.2,414.113,216,410.913,212.8z"/></g></g><g><g><path d="M346.913,308.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C351.713,315.2,350.113,312,346.913,308.8z"/></g></g><g><g><path d="M346.913,100.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8s4.8-6.4,4.8-11.2S350.113,104,346.913,100.8z"/></g></g><g><g><path d="M503.713,142.4c-28.8-136-179.2-142.4-208-142.4c-4.8,0-9.6,0-16,0c-67.2,1.6-132.8,36.8-187.2,97.6 c-60.8,67.2-96,155.2-91.2,227.2c8,126.4,70.4,187.2,192,187.2c115.2,0,201.6-33.6,256-100.8 C513.313,331.2,519.713,219.2,503.713,142.4z M423.713,392c-48,59.2-126.4,89.6-230.4,89.6s-152-48-160-158.4 c-4.8-64,28.8-144,83.2-203.2c48-54.4,107.2-84.8,164.8-88c4.8,0,9.6,0,14.4,0c140.8,0,171.2,89.6,176,116.8 C486.113,219.2,481.313,320,423.713,392z"/></g></g></svg> performance impact 0
|
||||
10075 shadows 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg> features 0
|
||||
10076 pbr 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg> features 0
|
||||
10077 semi-realistic 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg> categories 0
|
||||
10078 cursed 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg> categories 0
|
||||
10079 reflections 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style=""><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg> features 0
|
||||
10080 screenshot 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg> performance impact 0
|
||||
10081 colored-lighting 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(3.143 .726) scale(1.16268)"/><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(-.862 7.796) scale(1.16268)"/><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(7.148 7.796) scale(1.16268)"/></svg> features 0
|
||||
10057 128x 3 resolutions 0
|
||||
10030 economy 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg> categories 0
|
||||
10031 management 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg> categories 0
|
||||
10026 optimization 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> categories 0
|
||||
10027 mobs 1 <svg xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 24 24">\n <path fill="none" d="M0 0h24v24H0z"/>\n <path fill="none" stroke="currentColor" stroke-width="2" d="M3 3h18v18H3z"/>\n <path stroke="currentColor" fill="currentColor" d="M6 6h4v4H6zm8 0h4v4h-4zm-4 4h4v2h2v6h-2v-2h-4v2H8v-6h2v-2Z"/>\n</svg> categories 0
|
||||
10028 transportation 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg> categories 0
|
||||
10024 kitchen-sink 2 <svg viewBox="0 0 24 24" xml:space="preserve"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19.9 14-1.4 4.9c-.3 1-1.1 1.7-2.1 1.7H7.6c-.9 0-1.8-.7-2.1-1.7L4.1 14h15.8zM12 10V4.5M12 4.5c0-1.2.9-2.1 2.1-2.1M14.1 2.4c1.2 0 2.1.9 2.1 2.1M22.2 12c0 .6-.2 1.1-.6 1.4-.4.4-.9.6-1.4.6H3.8c-1.1 0-2-.9-2-2 0-.6.2-1.1.6-1.4.4-.4.9-.6 1.4-.6h16.4c1.1 0 2 .9 2 2z"/></g><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M16.2 7.2h0"/></svg> categories 0
|
||||
10044 blocks 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg> features 0
|
||||
10043 audio 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg> features 0
|
||||
10021 combat 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg> categories 0
|
||||
10022 adventure 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg> categories 0
|
||||
10023 technology 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/></svg> categories 0
|
||||
10033 minigame 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg> categories 0
|
||||
10034 combat 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg> categories 0
|
||||
10035 decoration 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> categories 0
|
||||
10036 modded 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"\\><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> categories 0
|
||||
10047 environment 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"]><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg> features 0
|
||||
10046 entities 3 <svg xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 24 24">\n <path fill="none" d="M0 0h24v24H0z"/>\n <path fill="none" stroke="currentColor" stroke-width="2" d="M3 3h18v18H3z"/>\n <path stroke="currentColor" fill="currentColor" d="M6 6h4v4H6zm8 0h4v4h-4zm-4 4h4v2h2v6h-2v-2h-4v2H8v-6h2v-2Z"/>\n</svg> features 0
|
||||
10032 game-mechanics 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg> categories 0
|
||||
10041 utility 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> categories 0
|
||||
10045 core-shaders 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg> features 0
|
||||
10040 tweaks 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> categories 0
|
||||
10051 items 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> features 0
|
||||
10052 models 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg> features 0
|
||||
10048 equipment 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> features 0
|
||||
10049 fonts 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg> features 0
|
||||
10037 simplistic 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> categories 0
|
||||
10038 realistic 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> categories 0
|
||||
10039 themed 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg> categories 0
|
||||
1003 magic 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg> categories 0
|
||||
1004 utility 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> categories 0
|
||||
10015 optimization 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> categories 0
|
||||
1009 storage 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg> categories 0
|
||||
10053 8x- 3 resolutions 0
|
||||
10042 vanilla-like 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"/><path d="M17 7A5 5 0 0 0 7 7"/><path d="M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"/></svg> categories 0
|
||||
10010 food 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46"></path><path d="M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z"></path><path d="M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z"></path></svg> categories 0
|
||||
10011 equipment 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg> categories 0
|
||||
10050 gui 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg> features 0
|
||||
1008 worldgen 1 <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> categories 0
|
||||
10017 multiplayer 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> categories 0
|
||||
10018 quests 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="6"></rect><rect x="16" y="16" width="6" height="6"></rect><rect x="2" y="16" width="6" height="6"></rect><path d="M12 8v4m0 0H5v4m7-4h7v4"></path></svg> categories 0
|
||||
10019 magic 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg> categories 0
|
||||
10020 lightweight 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line></svg>\n categories 0
|
||||
10029 social 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> categories 0
|
||||
60 cursed 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg> categories 0
|
||||
61 locale 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg> features 0
|
||||
62 48x 3 resolutions 0
|
||||
1 technology 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/></svg> categories 0
|
||||
16 challenging 2 <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> categories 0
|
||||
5 decoration 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> categories 0
|
||||
6 library 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> categories 0
|
||||
7 cursed 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg> categories 0
|
||||
2 adventure 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg> categories 0
|
||||
56 64x 3 resolutions 0
|
||||
55 32x 3 resolutions 0
|
||||
58 256x 3 resolutions 0
|
||||
59 512x+ 3 resolutions 0
|
||||
54 16x 3 resolutions 0
|
||||
63 path-tracing 4 <svg viewBox="0 0 24 24" style="" fill="none" stroke="currentColor" stroke-width="2"><path d="M2.977 19.17h16.222" style="" transform="translate(-.189 -.328) scale(1.09932)"/><path d="M3.889 3.259 12 19.17l5.749-11.277" style="" transform="translate(-1.192 -.328) scale(1.09932)"/><path d="M9.865 6.192h4.623v4.623" style="" transform="scale(1.09931) rotate(-18 20.008 .02)"/></svg> features 0
|
||||
64 realistic 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg> categories 0
|
||||
65 medium 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path><path d="M12 20v-8"></path></svg> performance impact 0
|
||||
66 low 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path></svg> performance impact 0
|
||||
67 high 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path><path d="M12 20v-8"></path><path d="M17 20V8"></path></svg> performance impact 0
|
||||
68 atmosphere 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"/><path d="M11 20v2"/><path d="M7 19v2"/></svg> features 0
|
||||
69 fantasy 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg> categories 0
|
||||
70 foliage 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="M12 22v-7l-2-2"/><path d="M17 8v.8A6 6 0 0 1 13.8 20v0H10v0A6.5 6.5 0 0 1 7 8h0a5 5 0 0 1 10 0Z"/><path d="m14 14-2 2"/></svg> features 0
|
||||
71 bloom 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2h8l4 10H4L8 2Z"/><path d="M12 12v6"/><path d="M8 22v-2c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v2H8Z"/></svg> features 0
|
||||
72 vanilla-like 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"/><path d="M17 7A5 5 0 0 0 7 7"/><path d="M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"/></svg> categories 0
|
||||
73 cartoon 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg> categories 0
|
||||
74 potato 4 <svg viewBox="0 0 512 512" fill="currentColor" stroke="currentColor"><g><g><path d="M218.913,116.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-6.4,4.8-11.2S222.113,120,218.913,116.8z"/></g></g><g><g><path d="M170.913,372.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C175.713,379.2,174.113,376,170.913,372.8z"/></g></g><g><g><path d="M250.913,228.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C255.713,235.2,254.113,232,250.913,228.8z"/></g></g><g><g><path d="M410.913,212.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C415.713,219.2,414.113,216,410.913,212.8z"/></g></g><g><g><path d="M346.913,308.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C351.713,315.2,350.113,312,346.913,308.8z"/></g></g><g><g><path d="M346.913,100.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8s4.8-6.4,4.8-11.2S350.113,104,346.913,100.8z"/></g></g><g><g><path d="M503.713,142.4c-28.8-136-179.2-142.4-208-142.4c-4.8,0-9.6,0-16,0c-67.2,1.6-132.8,36.8-187.2,97.6 c-60.8,67.2-96,155.2-91.2,227.2c8,126.4,70.4,187.2,192,187.2c115.2,0,201.6-33.6,256-100.8 C513.313,331.2,519.713,219.2,503.713,142.4z M423.713,392c-48,59.2-126.4,89.6-230.4,89.6s-152-48-160-158.4 c-4.8-64,28.8-144,83.2-203.2c48-54.4,107.2-84.8,164.8-88c4.8,0,9.6,0,14.4,0c140.8,0,171.2,89.6,176,116.8 C486.113,219.2,481.313,320,423.713,392z"/></g></g></svg> performance impact 0
|
||||
75 shadows 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg> features 0
|
||||
76 pbr 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg> features 0
|
||||
77 semi-realistic 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg> categories 0
|
||||
78 cursed 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg> categories 0
|
||||
79 reflections 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style=""><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg> features 0
|
||||
80 screenshot 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg> performance impact 0
|
||||
81 colored-lighting 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(3.143 .726) scale(1.16268)"/><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(-.862 7.796) scale(1.16268)"/><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(7.148 7.796) scale(1.16268)"/></svg> features 0
|
||||
57 128x 3 resolutions 0
|
||||
30 economy 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg> categories 0
|
||||
31 management 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg> categories 0
|
||||
26 optimization 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> categories 0
|
||||
27 mobs 1 <svg xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 24 24">\n <path fill="none" d="M0 0h24v24H0z"/>\n <path fill="none" stroke="currentColor" stroke-width="2" d="M3 3h18v18H3z"/>\n <path stroke="currentColor" fill="currentColor" d="M6 6h4v4H6zm8 0h4v4h-4zm-4 4h4v2h2v6h-2v-2h-4v2H8v-6h2v-2Z"/>\n</svg> categories 0
|
||||
28 transportation 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg> categories 0
|
||||
24 kitchen-sink 2 <svg viewBox="0 0 24 24" xml:space="preserve"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19.9 14-1.4 4.9c-.3 1-1.1 1.7-2.1 1.7H7.6c-.9 0-1.8-.7-2.1-1.7L4.1 14h15.8zM12 10V4.5M12 4.5c0-1.2.9-2.1 2.1-2.1M14.1 2.4c1.2 0 2.1.9 2.1 2.1M22.2 12c0 .6-.2 1.1-.6 1.4-.4.4-.9.6-1.4.6H3.8c-1.1 0-2-.9-2-2 0-.6.2-1.1.6-1.4.4-.4.9-.6 1.4-.6h16.4c1.1 0 2 .9 2 2z"/></g><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M16.2 7.2h0"/></svg> categories 0
|
||||
44 blocks 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg> features 0
|
||||
43 audio 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg> features 0
|
||||
21 combat 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg> categories 0
|
||||
22 adventure 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg> categories 0
|
||||
23 technology 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/></svg> categories 0
|
||||
33 minigame 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg> categories 0
|
||||
34 combat 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg> categories 0
|
||||
35 decoration 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> categories 0
|
||||
36 modded 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"\\><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> categories 0
|
||||
47 environment 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"]><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg> features 0
|
||||
46 entities 3 <svg xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 24 24">\n <path fill="none" d="M0 0h24v24H0z"/>\n <path fill="none" stroke="currentColor" stroke-width="2" d="M3 3h18v18H3z"/>\n <path stroke="currentColor" fill="currentColor" d="M6 6h4v4H6zm8 0h4v4h-4zm-4 4h4v2h2v6h-2v-2h-4v2H8v-6h2v-2Z"/>\n</svg> features 0
|
||||
32 game-mechanics 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg> categories 0
|
||||
41 utility 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> categories 0
|
||||
45 core-shaders 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg> features 0
|
||||
40 tweaks 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> categories 0
|
||||
51 items 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> features 0
|
||||
52 models 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg> features 0
|
||||
48 equipment 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> features 0
|
||||
49 fonts 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg> features 0
|
||||
37 simplistic 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> categories 0
|
||||
38 realistic 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> categories 0
|
||||
39 themed 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg> categories 0
|
||||
3 magic 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg> categories 0
|
||||
4 utility 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> categories 0
|
||||
15 optimization 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> categories 0
|
||||
9 storage 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg> categories 0
|
||||
53 8x- 3 resolutions 0
|
||||
42 vanilla-like 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"/><path d="M17 7A5 5 0 0 0 7 7"/><path d="M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"/></svg> categories 0
|
||||
10 food 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46"></path><path d="M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z"></path><path d="M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z"></path></svg> categories 0
|
||||
11 equipment 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg> categories 0
|
||||
50 gui 3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg> features 0
|
||||
8 worldgen 1 <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> categories 0
|
||||
17 multiplayer 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> categories 0
|
||||
18 quests 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="6"></rect><rect x="16" y="16" width="6" height="6"></rect><rect x="2" y="16" width="6" height="6"></rect><path d="M12 8v4m0 0H5v4m7-4h7v4"></path></svg> categories 0
|
||||
19 magic 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg> categories 0
|
||||
20 lightweight 2 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line></svg>\n categories 0
|
||||
29 social 1 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> categories 0
|
||||
\.
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN components JSONB NOT NULL DEFAULT '{}';
|
||||
@@ -1,8 +0,0 @@
|
||||
ALTER TABLE versions
|
||||
ADD COLUMN components JSONB NOT NULL DEFAULT '{}';
|
||||
|
||||
-- extra metadata for the `minecraft_java_server` version component
|
||||
CREATE TABLE minecraft_java_server_versions (
|
||||
id bigint PRIMARY KEY REFERENCES versions(id),
|
||||
modpack_id bigint REFERENCES versions(id)
|
||||
);
|
||||
@@ -1,60 +0,0 @@
|
||||
INSERT INTO link_platforms (name, donation) VALUES ('store', false);
|
||||
|
||||
INSERT INTO project_types (id, name)
|
||||
VALUES (7, 'minecraft_java_server');
|
||||
|
||||
INSERT INTO categories (header, category, project_type)
|
||||
VALUES
|
||||
('minecraft_server_gameplay', 'skyblock', 7),
|
||||
('minecraft_server_gameplay', 'oneblock', 7),
|
||||
('minecraft_server_gameplay', 'gens', 7),
|
||||
('minecraft_server_gameplay', 'prison', 7),
|
||||
('minecraft_server_gameplay', 'factions', 7),
|
||||
('minecraft_server_gameplay', 'lifesteal', 7),
|
||||
('minecraft_server_gameplay', 'anarchy', 7),
|
||||
('minecraft_server_gameplay', 'towns', 7),
|
||||
('minecraft_server_gameplay', 'vanilla-like', 7),
|
||||
('minecraft_server_gameplay', 'rpg', 7),
|
||||
('minecraft_server_gameplay', 'minigames', 7),
|
||||
('minecraft_server_gameplay', 'racing', 7),
|
||||
('minecraft_server_gameplay', 'battle-royale', 7),
|
||||
('minecraft_server_gameplay', 'parkour', 7),
|
||||
('minecraft_server_gameplay', 'bedwars', 7),
|
||||
('minecraft_server_gameplay', 'kitpvp', 7),
|
||||
('minecraft_server_gameplay', 'microgames', 7),
|
||||
|
||||
('minecraft_server_features', 'pokemon', 7),
|
||||
('minecraft_server_features', 'teams', 7),
|
||||
('minecraft_server_features', 'pvp', 7),
|
||||
('minecraft_server_features', 'pve', 7),
|
||||
('minecraft_server_features', 'op', 7),
|
||||
('minecraft_server_features', 'economy', 7),
|
||||
('minecraft_server_features', 'plots', 7),
|
||||
('minecraft_server_features', 'questing', 7),
|
||||
('minecraft_server_features', 'personal-worlds', 7),
|
||||
('minecraft_server_features', 'media', 7),
|
||||
('minecraft_server_features', 'bosses', 7),
|
||||
('minecraft_server_features', 'classes', 7),
|
||||
('minecraft_server_features', 'custom-content', 7),
|
||||
('minecraft_server_features', 'dungeons', 7),
|
||||
|
||||
('minecraft_server_meta', 'crossplay', 7),
|
||||
('minecraft_server_meta', 'offline-mode', 7),
|
||||
('minecraft_server_meta', 'whitelisted', 7),
|
||||
('minecraft_server_meta', 'keep-inventory', 7),
|
||||
('minecraft_server_meta', 'network', 7),
|
||||
('minecraft_server_meta', 'world-resets', 7),
|
||||
('minecraft_server_meta', 'creative-mode', 7),
|
||||
('minecraft_server_meta', 'hardcore-mode', 7),
|
||||
('minecraft_server_meta', 'survival-mode', 7),
|
||||
('minecraft_server_meta', 'adventure-mode', 7),
|
||||
|
||||
('minecraft_server_community', 'adventure-mode', 7),
|
||||
('minecraft_server_community', 'smp', 7),
|
||||
('minecraft_server_community', 'mmo', 7),
|
||||
('minecraft_server_community', 'roleplay', 7),
|
||||
('minecraft_server_community', 'social', 7),
|
||||
('minecraft_server_community', 'creator-community', 7),
|
||||
('minecraft_server_community', 'recording-smp', 7),
|
||||
('minecraft_server_community', 'competitive', 7),
|
||||
('minecraft_server_community', 'technical', 7);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE delphi_report_issue_details
|
||||
ADD COLUMN jar TEXT;
|
||||
@@ -2,7 +2,6 @@ use super::AuthProvider;
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::database::models::{DBUser, user_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::User;
|
||||
use crate::queue::session::AuthQueue;
|
||||
@@ -147,7 +146,7 @@ where
|
||||
user_item::DBUser::get_id(session.user_id, executor, redis)
|
||||
.await?;
|
||||
|
||||
let rate_limit_ignore = &ENV.RATE_LIMIT_IGNORE_KEY;
|
||||
let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?;
|
||||
if req
|
||||
.headers()
|
||||
.get("x-ratelimit-key")
|
||||
|
||||
@@ -5,10 +5,9 @@ mod fetch;
|
||||
|
||||
pub use fetch::*;
|
||||
|
||||
use crate::env::ENV;
|
||||
|
||||
pub async fn init_client() -> clickhouse::error::Result<clickhouse::Client> {
|
||||
init_client_with_database(&ENV.CLICKHOUSE_DATABASE).await
|
||||
init_client_with_database(&dotenvy::var("CLICKHOUSE_DATABASE").unwrap())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn init_client_with_database(
|
||||
@@ -25,9 +24,9 @@ pub async fn init_client_with_database(
|
||||
.build(https_connector);
|
||||
|
||||
clickhouse::Client::with_http_client(hyper_client)
|
||||
.with_url(&ENV.CLICKHOUSE_URL)
|
||||
.with_user(&ENV.CLICKHOUSE_USER)
|
||||
.with_password(&ENV.CLICKHOUSE_PASSWORD)
|
||||
.with_url(dotenvy::var("CLICKHOUSE_URL").unwrap())
|
||||
.with_user(dotenvy::var("CLICKHOUSE_USER").unwrap())
|
||||
.with_password(dotenvy::var("CLICKHOUSE_PASSWORD").unwrap())
|
||||
.with_validation(false)
|
||||
};
|
||||
|
||||
@@ -36,7 +35,8 @@ pub async fn init_client_with_database(
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
let clickhouse_replicated = ENV.CLICKHOUSE_REPLICATED;
|
||||
let clickhouse_replicated =
|
||||
dotenvy::var("CLICKHOUSE_REPLICATED").unwrap() == "true";
|
||||
let cluster_line = if clickhouse_replicated {
|
||||
"ON cluster '{cluster}'"
|
||||
} else {
|
||||
|
||||
@@ -221,9 +221,6 @@ pub struct ReportIssueDetail {
|
||||
/// This acts as a stable identifier for an issue detail, even across
|
||||
/// different versions of the same file.
|
||||
pub key: String,
|
||||
/// If this detail was found inside a JAR embedded inside the scanned JAR,
|
||||
/// this will point to the path of that JAR inside the outer JAR.
|
||||
pub jar: Option<String>,
|
||||
/// Name of the Java class path in which this issue was found.
|
||||
pub file_path: String,
|
||||
/// Decompiled, pretty-printed source of the Java class.
|
||||
@@ -244,13 +241,12 @@ impl ReportIssueDetail {
|
||||
) -> Result<DelphiReportIssueDetailsId, DatabaseError> {
|
||||
Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!(
|
||||
"
|
||||
INSERT INTO delphi_report_issue_details (issue_id, key, jar, file_path, decompiled_source, data, severity)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
",
|
||||
self.issue_id as DelphiReportIssueId,
|
||||
self.key,
|
||||
self.jar,
|
||||
self.file_path,
|
||||
self.decompiled_source,
|
||||
sqlx::types::Json(&self.data) as Json<&HashMap<String, serde_json::Value>>,
|
||||
|
||||
@@ -13,8 +13,6 @@ pub type PgTransaction<'c> = sqlx_tracing::Transaction<'c, Postgres>;
|
||||
pub use sqlx_tracing::Acquire;
|
||||
pub use sqlx_tracing::Executor;
|
||||
|
||||
use crate::env::ENV;
|
||||
|
||||
// pub type PgPool = sqlx::PgPool;
|
||||
// pub type PgTransaction<'c> = sqlx::Transaction<'c, Postgres>;
|
||||
// pub use sqlx::Acquire;
|
||||
@@ -52,27 +50,57 @@ impl DerefMut for ReadOnlyPgPool {
|
||||
|
||||
pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> {
|
||||
info!("Initializing database connection");
|
||||
let database_url = &ENV.DATABASE_URL;
|
||||
let database_url =
|
||||
dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
|
||||
|
||||
let acquire_timeout =
|
||||
Duration::from_millis(ENV.DATABASE_ACQUIRE_TIMEOUT_MS);
|
||||
dotenvy::var("DATABASE_ACQUIRE_TIMEOUT_MS")
|
||||
.ok()
|
||||
.map_or_else(
|
||||
|| Duration::from_millis(30000),
|
||||
|x| {
|
||||
Duration::from_millis(x.parse::<u64>().expect(
|
||||
"DATABASE_ACQUIRE_TIMEOUT_MS must be a valid u64",
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
let pool = PgPoolOptions::new()
|
||||
.acquire_timeout(acquire_timeout)
|
||||
.min_connections(ENV.DATABASE_MIN_CONNECTIONS)
|
||||
.max_connections(ENV.DATABASE_MAX_CONNECTIONS)
|
||||
.min_connections(
|
||||
dotenvy::var("DATABASE_MIN_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(0),
|
||||
)
|
||||
.max_connections(
|
||||
dotenvy::var("DATABASE_MAX_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(16),
|
||||
)
|
||||
.max_lifetime(Some(Duration::from_secs(60 * 60)))
|
||||
.connect(database_url)
|
||||
.connect(&database_url)
|
||||
.await?;
|
||||
let pool = PgPool::from(pool);
|
||||
|
||||
if !ENV.READONLY_DATABASE_URL.is_empty() {
|
||||
if let Ok(url) = dotenvy::var("READONLY_DATABASE_URL") {
|
||||
let ro_pool = PgPoolOptions::new()
|
||||
.acquire_timeout(acquire_timeout)
|
||||
.min_connections(ENV.READONLY_DATABASE_MIN_CONNECTIONS)
|
||||
.max_connections(ENV.READONLY_DATABASE_MAX_CONNECTIONS)
|
||||
.min_connections(
|
||||
dotenvy::var("READONLY_DATABASE_MIN_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(0),
|
||||
)
|
||||
.max_connections(
|
||||
dotenvy::var("READONLY_DATABASE_MAX_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(1),
|
||||
)
|
||||
.max_lifetime(Some(Duration::from_secs(60 * 60)))
|
||||
.connect(&ENV.READONLY_DATABASE_URL)
|
||||
.connect(&url)
|
||||
.await?;
|
||||
let ro_pool = PgPool::from(ro_pool);
|
||||
|
||||
@@ -84,7 +112,8 @@ pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> {
|
||||
}
|
||||
|
||||
pub async fn check_for_migrations() -> eyre::Result<()> {
|
||||
let uri = &ENV.DATABASE_URL;
|
||||
let uri =
|
||||
dotenvy::var("DATABASE_URL").wrap_err("`DATABASE_URL` not in .env")?;
|
||||
let uri = uri.as_str();
|
||||
if !Postgres::database_exists(uri)
|
||||
.await
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::env::ENV;
|
||||
|
||||
use super::models::DatabaseError;
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use chrono::{TimeZone, Utc};
|
||||
@@ -44,26 +42,44 @@ impl RedisPool {
|
||||
// testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests)
|
||||
// PANICS: production pool will panic if redis url is not set
|
||||
pub fn new(meta_namespace: impl Into<Arc<str>>) -> Self {
|
||||
let wait_timeout = Duration::from_millis(ENV.REDIS_WAIT_TIMEOUT_MS);
|
||||
let wait_timeout =
|
||||
dotenvy::var("REDIS_WAIT_TIMEOUT_MS").ok().map_or_else(
|
||||
|| Duration::from_millis(15000),
|
||||
|x| {
|
||||
Duration::from_millis(
|
||||
x.parse::<u64>().expect(
|
||||
"REDIS_WAIT_TIMEOUT_MS must be a valid u64",
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let url = &ENV.REDIS_URL;
|
||||
let url = dotenvy::var("REDIS_URL").expect("Redis URL not set");
|
||||
let pool = Config::from_url(url.clone())
|
||||
.builder()
|
||||
.expect("Error building Redis pool")
|
||||
.max_size(ENV.REDIS_MAX_CONNECTIONS as usize)
|
||||
.max_size(
|
||||
dotenvy::var("REDIS_MAX_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(10000),
|
||||
)
|
||||
.wait_timeout(Some(wait_timeout))
|
||||
.runtime(Runtime::Tokio1)
|
||||
.build()
|
||||
.expect("Redis connection failed");
|
||||
|
||||
let pool = RedisPool {
|
||||
url: url.clone(),
|
||||
url,
|
||||
pool,
|
||||
cache_list: Arc::new(DashMap::with_capacity(2048)),
|
||||
meta_namespace: meta_namespace.into(),
|
||||
};
|
||||
|
||||
let redis_min_connections = ENV.REDIS_MIN_CONNECTIONS;
|
||||
let redis_min_connections = dotenvy::var("REDIS_MIN_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let spawn_min_connections = (0..redis_min_connections)
|
||||
.map(|_| {
|
||||
let pool = pool.clone();
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
use std::{any::type_name, convert::Infallible, str::FromStr, sync::LazyLock};
|
||||
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use eyre::{Context, eyre};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
macro_rules! vars {
|
||||
(
|
||||
$(
|
||||
$field:ident: $ty:ty $(= $default:expr)?;
|
||||
)*
|
||||
) => {
|
||||
#[derive(Debug)]
|
||||
#[allow(
|
||||
non_snake_case,
|
||||
reason = "environment variables are UPPER_SNAKE_CASE",
|
||||
)]
|
||||
pub struct EnvVars {
|
||||
$(
|
||||
pub $field: $ty,
|
||||
)*
|
||||
}
|
||||
|
||||
impl EnvVars {
|
||||
pub fn from_env() -> eyre::Result<Self> {
|
||||
let mut err = eyre!("failed to read environment variables");
|
||||
|
||||
$(
|
||||
#[expect(
|
||||
non_snake_case,
|
||||
reason = "environment variables are UPPER_SNAKE_CASE",
|
||||
)]
|
||||
#[allow(
|
||||
unused_assignments,
|
||||
unused_mut,
|
||||
reason = "`default` is not used if there is no default",
|
||||
)]
|
||||
let $field: Option<$ty> = {
|
||||
let mut default = None::<$ty>;
|
||||
$( default = Some({ $default }.into()); )?
|
||||
|
||||
match parse_value::<$ty>(stringify!($field), default) {
|
||||
Ok(value) => Some(value),
|
||||
Err(source) => {
|
||||
err = err.wrap_err(eyre!("{source:#}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
)*
|
||||
|
||||
Ok(EnvVars {
|
||||
$(
|
||||
$field: match $field {
|
||||
Some(value) => value,
|
||||
None => return Err(err),
|
||||
},
|
||||
)*
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub static ENV: LazyLock<EnvVars> = LazyLock::new(|| {
|
||||
EnvVars::from_env().unwrap_or_else(|err| panic!("{err:?}"))
|
||||
});
|
||||
|
||||
fn parse_value<T>(key: &str, default: Option<T>) -> eyre::Result<T>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
match (dotenvy::var(key), default) {
|
||||
(Ok(value), _) => value.parse::<T>().wrap_err_with(|| {
|
||||
eyre!("`{key}` is not a valid `{}`", type_name::<T>())
|
||||
}),
|
||||
(Err(_), Some(default)) => Ok(default),
|
||||
(Err(_), None) => Err(eyre!("`{key}` missing")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> eyre::Result<()> {
|
||||
EnvVars::from_env()?;
|
||||
LazyLock::force(&ENV);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut,
|
||||
)]
|
||||
pub struct Json<T: DeserializeOwned>(pub T);
|
||||
|
||||
impl<T: DeserializeOwned> FromStr for Json<T> {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut,
|
||||
)]
|
||||
pub struct StringCsv(pub Vec<String>);
|
||||
|
||||
impl FromStr for StringCsv {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let v = s
|
||||
.split(',')
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Self(v))
|
||||
}
|
||||
}
|
||||
|
||||
vars! {
|
||||
SENTRY_ENVIRONMENT: String;
|
||||
SENTRY_TRACES_SAMPLE_RATE: f32;
|
||||
SITE_URL: String;
|
||||
CDN_URL: String;
|
||||
LABRINTH_ADMIN_KEY: String;
|
||||
LABRINTH_MEDAL_KEY: String;
|
||||
LABRINTH_EXTERNAL_NOTIFICATION_KEY: String;
|
||||
RATE_LIMIT_IGNORE_KEY: String;
|
||||
DATABASE_URL: String;
|
||||
MEILISEARCH_READ_ADDR: String;
|
||||
MEILISEARCH_WRITE_ADDRS: StringCsv;
|
||||
MEILISEARCH_KEY: String;
|
||||
REDIS_URL: String;
|
||||
BIND_ADDR: String;
|
||||
SELF_ADDR: String;
|
||||
|
||||
LOCAL_INDEX_INTERVAL: u64;
|
||||
VERSION_INDEX_INTERVAL: u64;
|
||||
|
||||
WHITELISTED_MODPACK_DOMAINS: Json<Vec<String>>;
|
||||
ALLOWED_CALLBACK_URLS: Json<Vec<String>>;
|
||||
ANALYTICS_ALLOWED_ORIGINS: Json<Vec<String>>;
|
||||
|
||||
// storage
|
||||
STORAGE_BACKEND: crate::file_hosting::FileHostKind;
|
||||
|
||||
// s3
|
||||
S3_PUBLIC_BUCKET_NAME: String = "";
|
||||
S3_PUBLIC_USES_PATH_STYLE_BUCKET: bool = false;
|
||||
S3_PUBLIC_REGION: String = "";
|
||||
S3_PUBLIC_URL: String = "";
|
||||
S3_PUBLIC_ACCESS_TOKEN: String = "";
|
||||
S3_PUBLIC_SECRET: String = "";
|
||||
|
||||
S3_PRIVATE_BUCKET_NAME: String = "";
|
||||
S3_PRIVATE_USES_PATH_STYLE_BUCKET: bool = false;
|
||||
S3_PRIVATE_REGION: String = "";
|
||||
S3_PRIVATE_URL: String = "";
|
||||
S3_PRIVATE_ACCESS_TOKEN: String = "";
|
||||
S3_PRIVATE_SECRET: String = "";
|
||||
|
||||
// local
|
||||
MOCK_FILE_PATH: String = "";
|
||||
|
||||
GITHUB_CLIENT_ID: String;
|
||||
GITHUB_CLIENT_SECRET: String;
|
||||
GITLAB_CLIENT_ID: String;
|
||||
GITLAB_CLIENT_SECRET: String;
|
||||
DISCORD_CLIENT_ID: String;
|
||||
DISCORD_CLIENT_SECRET: String;
|
||||
MICROSOFT_CLIENT_ID: String;
|
||||
MICROSOFT_CLIENT_SECRET: String;
|
||||
GOOGLE_CLIENT_ID: String;
|
||||
GOOGLE_CLIENT_SECRET: String;
|
||||
STEAM_API_KEY: String;
|
||||
|
||||
TREMENDOUS_API_URL: String;
|
||||
TREMENDOUS_API_KEY: String;
|
||||
TREMENDOUS_PRIVATE_KEY: String;
|
||||
|
||||
PAYPAL_API_URL: String;
|
||||
PAYPAL_WEBHOOK_ID: String;
|
||||
PAYPAL_CLIENT_ID: String;
|
||||
PAYPAL_CLIENT_SECRET: String;
|
||||
PAYPAL_NVP_USERNAME: String;
|
||||
PAYPAL_NVP_PASSWORD: String;
|
||||
PAYPAL_NVP_SIGNATURE: String;
|
||||
|
||||
PAYPAL_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
|
||||
BREX_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
|
||||
TREMENDOUS_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
|
||||
MURAL_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
|
||||
|
||||
HCAPTCHA_SECRET: String;
|
||||
|
||||
SMTP_USERNAME: String;
|
||||
SMTP_PASSWORD: String;
|
||||
SMTP_HOST: String;
|
||||
SMTP_PORT: u16;
|
||||
SMTP_TLS: String;
|
||||
SMTP_FROM_NAME: String;
|
||||
SMTP_FROM_ADDRESS: String;
|
||||
|
||||
SITE_VERIFY_EMAIL_PATH: String;
|
||||
SITE_RESET_PASSWORD_PATH: String;
|
||||
SITE_BILLING_PATH: String;
|
||||
|
||||
SENDY_URL: String;
|
||||
SENDY_LIST_ID: String;
|
||||
SENDY_API_KEY: String;
|
||||
|
||||
CLICKHOUSE_REPLICATED: bool;
|
||||
CLICKHOUSE_URL: String;
|
||||
CLICKHOUSE_USER: String;
|
||||
CLICKHOUSE_PASSWORD: String;
|
||||
CLICKHOUSE_DATABASE: String;
|
||||
|
||||
FLAME_ANVIL_URL: String;
|
||||
|
||||
GOTENBERG_URL: String;
|
||||
GOTENBERG_CALLBACK_BASE: String;
|
||||
GOTENBERG_TIMEOUT: u64;
|
||||
|
||||
STRIPE_API_KEY: String;
|
||||
STRIPE_WEBHOOK_SECRET: String;
|
||||
|
||||
ADITUDE_API_KEY: String;
|
||||
|
||||
PYRO_API_KEY: String;
|
||||
|
||||
BREX_API_URL: String;
|
||||
BREX_API_KEY: String;
|
||||
|
||||
DELPHI_URL: String;
|
||||
|
||||
AVALARA_1099_API_URL: String;
|
||||
AVALARA_1099_API_KEY: String;
|
||||
AVALARA_1099_API_TEAM_ID: String;
|
||||
AVALARA_1099_COMPANY_ID: String;
|
||||
|
||||
ANROK_API_URL: String;
|
||||
ANROK_API_KEY: String;
|
||||
|
||||
COMPLIANCE_PAYOUT_THRESHOLD: String;
|
||||
|
||||
PAYOUT_ALERT_SLACK_WEBHOOK: String;
|
||||
CLOUDFLARE_INTEGRATION: bool = false;
|
||||
|
||||
ARCHON_URL: String;
|
||||
|
||||
MURALPAY_API_URL: String;
|
||||
MURALPAY_API_KEY: String;
|
||||
MURALPAY_TRANSFER_API_KEY: String;
|
||||
MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId = muralpay::AccountId(uuid::Uuid::nil());
|
||||
|
||||
DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal;
|
||||
|
||||
DATABASE_ACQUIRE_TIMEOUT_MS: u64 = 30000u64;
|
||||
DATABASE_MIN_CONNECTIONS: u32 = 0u32;
|
||||
DATABASE_MAX_CONNECTIONS: u32 = 16u32;
|
||||
READONLY_DATABASE_URL: String = "";
|
||||
READONLY_DATABASE_MIN_CONNECTIONS: u32 = 0u32;
|
||||
READONLY_DATABASE_MAX_CONNECTIONS: u32 = 1u32;
|
||||
|
||||
REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64;
|
||||
REDIS_MAX_CONNECTIONS: u32 = 10000u32;
|
||||
REDIS_MIN_CONNECTIONS: usize = 0usize;
|
||||
|
||||
SEARCH_OPERATION_TIMEOUT: u64 = 300000u64;
|
||||
|
||||
SMTP_REPLY_TO_NAME: String = "";
|
||||
SMTP_REPLY_TO_ADDRESS: String = "";
|
||||
|
||||
PUBLIC_DISCORD_WEBHOOK: String = "";
|
||||
MODERATION_SLACK_WEBHOOK: String = "";
|
||||
DELPHI_SLACK_WEBHOOK: String = "";
|
||||
|
||||
TREMENDOUS_CAMPAIGN_ID: String = "";
|
||||
}
|
||||
@@ -9,8 +9,6 @@ use hex::ToHex;
|
||||
use sha2::Digest;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::env::ENV;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MockHost(());
|
||||
|
||||
@@ -56,7 +54,8 @@ impl FileHost for MockHost {
|
||||
file_name: &str,
|
||||
_expiry_secs: u32,
|
||||
) -> Result<String, FileHostingError> {
|
||||
Ok(format!("{}/private/{file_name}", ENV.CDN_URL))
|
||||
let cdn_url = dotenvy::var("CDN_URL").unwrap();
|
||||
Ok(format!("{cdn_url}/private/{file_name}"))
|
||||
}
|
||||
|
||||
async fn delete_file(
|
||||
@@ -78,7 +77,7 @@ fn get_file_path(
|
||||
file_name: &str,
|
||||
file_publicity: FileHostPublicity,
|
||||
) -> PathBuf {
|
||||
let mut path = PathBuf::from(ENV.MOCK_FILE_PATH.clone());
|
||||
let mut path = PathBuf::from(dotenvy::var("MOCK_FILE_PATH").unwrap());
|
||||
|
||||
if matches!(file_publicity, FileHostPublicity::Private) {
|
||||
path.push("private");
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -65,25 +63,3 @@ pub trait FileHost {
|
||||
file_publicity: FileHostPublicity,
|
||||
) -> Result<DeleteFileData, FileHostingError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum FileHostKind {
|
||||
S3,
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid file host kind")]
|
||||
pub struct InvalidFileHostKind;
|
||||
|
||||
impl FromStr for FileHostKind {
|
||||
type Err = InvalidFileHostKind;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"s3" => Self::S3,
|
||||
"local" => Self::Local,
|
||||
_ => return Err(InvalidFileHostKind),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+196
-7
@@ -16,11 +16,11 @@ use util::gotenberg::GotenbergClient;
|
||||
|
||||
use crate::background_task::update_versions;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::queue::billing::{index_billing, index_subscriptions};
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::util::anrok;
|
||||
use crate::util::archon::ArchonClient;
|
||||
use crate::util::env::{parse_strings_from_var, parse_var};
|
||||
use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters};
|
||||
use sync::friends::handle_pubsub;
|
||||
|
||||
@@ -28,7 +28,6 @@ pub mod auth;
|
||||
pub mod background_task;
|
||||
pub mod clickhouse;
|
||||
pub mod database;
|
||||
pub mod env;
|
||||
pub mod file_hosting;
|
||||
pub mod models;
|
||||
pub mod queue;
|
||||
@@ -84,7 +83,10 @@ pub fn app_setup(
|
||||
gotenberg_client: GotenbergClient,
|
||||
enable_background_tasks: bool,
|
||||
) -> LabrinthConfig {
|
||||
info!("Starting labrinth on {}", &ENV.BIND_ADDR);
|
||||
info!(
|
||||
"Starting labrinth on {}",
|
||||
dotenvy::var("BIND_ADDR").unwrap()
|
||||
);
|
||||
|
||||
let automated_moderation_queue =
|
||||
web::Data::new(AutomatedModerationQueue::default());
|
||||
@@ -110,8 +112,9 @@ pub fn app_setup(
|
||||
if enable_background_tasks {
|
||||
// The interval in seconds at which the local database is indexed
|
||||
// for searching. Defaults to 1 hour if unset.
|
||||
let local_index_interval =
|
||||
Duration::from_secs(ENV.LOCAL_INDEX_INTERVAL);
|
||||
let local_index_interval = Duration::from_secs(
|
||||
parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600),
|
||||
);
|
||||
let pool_ref = pool.clone();
|
||||
let search_config_ref = search_config.clone();
|
||||
let redis_pool_ref = redis_pool.clone();
|
||||
@@ -139,8 +142,9 @@ pub fn app_setup(
|
||||
}
|
||||
});
|
||||
|
||||
let version_index_interval =
|
||||
Duration::from_secs(ENV.VERSION_INDEX_INTERVAL);
|
||||
let version_index_interval = Duration::from_secs(
|
||||
parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800),
|
||||
);
|
||||
let pool_ref = pool.clone();
|
||||
let redis_pool_ref = redis_pool.clone();
|
||||
scheduler.run(version_index_interval, move || {
|
||||
@@ -345,3 +349,188 @@ pub fn utoipa_app_config(
|
||||
.configure(routes::v3::utoipa_config)
|
||||
.configure(routes::internal::utoipa_config);
|
||||
}
|
||||
|
||||
// This is so that env vars not used immediately don't panic at runtime
|
||||
pub fn check_env_vars() -> bool {
|
||||
let mut failed = false;
|
||||
|
||||
fn check_var<T: std::str::FromStr>(var: &str) -> bool {
|
||||
let check = parse_var::<T>(var).is_none();
|
||||
if check {
|
||||
warn!(
|
||||
"Variable `{}` missing in dotenv or not of type `{}`",
|
||||
var,
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
check
|
||||
}
|
||||
|
||||
failed |= check_var::<String>("SENTRY_ENVIRONMENT");
|
||||
failed |= check_var::<String>("SENTRY_TRACES_SAMPLE_RATE");
|
||||
failed |= check_var::<String>("SITE_URL");
|
||||
failed |= check_var::<String>("CDN_URL");
|
||||
failed |= check_var::<String>("LABRINTH_ADMIN_KEY");
|
||||
failed |= check_var::<String>("LABRINTH_EXTERNAL_NOTIFICATION_KEY");
|
||||
failed |= check_var::<String>("RATE_LIMIT_IGNORE_KEY");
|
||||
failed |= check_var::<String>("DATABASE_URL");
|
||||
failed |= check_var::<String>("MEILISEARCH_READ_ADDR");
|
||||
failed |= check_var::<String>("MEILISEARCH_WRITE_ADDRS");
|
||||
failed |= check_var::<String>("MEILISEARCH_KEY");
|
||||
failed |= check_var::<String>("REDIS_URL");
|
||||
failed |= check_var::<String>("BIND_ADDR");
|
||||
failed |= check_var::<String>("SELF_ADDR");
|
||||
|
||||
failed |= check_var::<String>("STORAGE_BACKEND");
|
||||
|
||||
let storage_backend = dotenvy::var("STORAGE_BACKEND").ok();
|
||||
match storage_backend.as_deref() {
|
||||
Some("s3") => {
|
||||
let mut check_var_set = |var_prefix| {
|
||||
failed |= check_var::<String>(&format!(
|
||||
"S3_{var_prefix}_BUCKET_NAME"
|
||||
));
|
||||
failed |= check_var::<bool>(&format!(
|
||||
"S3_{var_prefix}_USES_PATH_STYLE_BUCKET"
|
||||
));
|
||||
failed |=
|
||||
check_var::<String>(&format!("S3_{var_prefix}_REGION"));
|
||||
failed |= check_var::<String>(&format!("S3_{var_prefix}_URL"));
|
||||
failed |= check_var::<String>(&format!(
|
||||
"S3_{var_prefix}_ACCESS_TOKEN"
|
||||
));
|
||||
failed |=
|
||||
check_var::<String>(&format!("S3_{var_prefix}_SECRET"));
|
||||
};
|
||||
|
||||
check_var_set("PUBLIC");
|
||||
check_var_set("PRIVATE");
|
||||
}
|
||||
Some("local") => {
|
||||
failed |= check_var::<String>("MOCK_FILE_PATH");
|
||||
}
|
||||
Some(backend) => {
|
||||
warn!(
|
||||
"Variable `STORAGE_BACKEND` contains an invalid value: {backend}. Expected \"s3\" or \"local\"."
|
||||
);
|
||||
failed |= true;
|
||||
}
|
||||
_ => {
|
||||
warn!("Variable `STORAGE_BACKEND` is not set!");
|
||||
failed |= true;
|
||||
}
|
||||
}
|
||||
|
||||
failed |= check_var::<usize>("LOCAL_INDEX_INTERVAL");
|
||||
failed |= check_var::<usize>("VERSION_INDEX_INTERVAL");
|
||||
|
||||
if parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").is_none() {
|
||||
warn!(
|
||||
"Variable `WHITELISTED_MODPACK_DOMAINS` missing in dotenv or not a json array of strings"
|
||||
);
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
if parse_strings_from_var("ALLOWED_CALLBACK_URLS").is_none() {
|
||||
warn!(
|
||||
"Variable `ALLOWED_CALLBACK_URLS` missing in dotenv or not a json array of strings"
|
||||
);
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
failed |= check_var::<String>("GITHUB_CLIENT_ID");
|
||||
failed |= check_var::<String>("GITHUB_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("GITLAB_CLIENT_ID");
|
||||
failed |= check_var::<String>("GITLAB_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("DISCORD_CLIENT_ID");
|
||||
failed |= check_var::<String>("DISCORD_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("MICROSOFT_CLIENT_ID");
|
||||
failed |= check_var::<String>("MICROSOFT_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("GOOGLE_CLIENT_ID");
|
||||
failed |= check_var::<String>("GOOGLE_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("STEAM_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("TREMENDOUS_API_URL");
|
||||
failed |= check_var::<String>("TREMENDOUS_API_KEY");
|
||||
failed |= check_var::<String>("TREMENDOUS_PRIVATE_KEY");
|
||||
|
||||
failed |= check_var::<String>("PAYPAL_API_URL");
|
||||
failed |= check_var::<String>("PAYPAL_WEBHOOK_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("PAYPAL_NVP_USERNAME");
|
||||
failed |= check_var::<String>("PAYPAL_NVP_PASSWORD");
|
||||
failed |= check_var::<String>("PAYPAL_NVP_SIGNATURE");
|
||||
|
||||
failed |= check_var::<String>("HCAPTCHA_SECRET");
|
||||
|
||||
failed |= check_var::<String>("SMTP_USERNAME");
|
||||
failed |= check_var::<String>("SMTP_PASSWORD");
|
||||
failed |= check_var::<String>("SMTP_HOST");
|
||||
failed |= check_var::<u16>("SMTP_PORT");
|
||||
failed |= check_var::<String>("SMTP_TLS");
|
||||
failed |= check_var::<String>("SMTP_FROM_NAME");
|
||||
failed |= check_var::<String>("SMTP_FROM_ADDRESS");
|
||||
|
||||
failed |= check_var::<String>("SITE_VERIFY_EMAIL_PATH");
|
||||
failed |= check_var::<String>("SITE_RESET_PASSWORD_PATH");
|
||||
failed |= check_var::<String>("SITE_BILLING_PATH");
|
||||
|
||||
failed |= check_var::<String>("SENDY_URL");
|
||||
failed |= check_var::<String>("SENDY_LIST_ID");
|
||||
failed |= check_var::<String>("SENDY_API_KEY");
|
||||
|
||||
if parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS").is_none() {
|
||||
warn!(
|
||||
"Variable `ANALYTICS_ALLOWED_ORIGINS` missing in dotenv or not a json array of strings"
|
||||
);
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
failed |= check_var::<bool>("CLICKHOUSE_REPLICATED");
|
||||
failed |= check_var::<String>("CLICKHOUSE_URL");
|
||||
failed |= check_var::<String>("CLICKHOUSE_USER");
|
||||
failed |= check_var::<String>("CLICKHOUSE_PASSWORD");
|
||||
failed |= check_var::<String>("CLICKHOUSE_DATABASE");
|
||||
|
||||
failed |= check_var::<String>("FLAME_ANVIL_URL");
|
||||
|
||||
failed |= check_var::<String>("GOTENBERG_URL");
|
||||
failed |= check_var::<String>("GOTENBERG_CALLBACK_BASE");
|
||||
failed |= check_var::<String>("GOTENBERG_TIMEOUT");
|
||||
|
||||
failed |= check_var::<String>("STRIPE_API_KEY");
|
||||
failed |= check_var::<String>("STRIPE_WEBHOOK_SECRET");
|
||||
|
||||
failed |= check_var::<String>("ADITUDE_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("PYRO_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("BREX_API_URL");
|
||||
failed |= check_var::<String>("BREX_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("DELPHI_URL");
|
||||
|
||||
failed |= check_var::<String>("AVALARA_1099_API_URL");
|
||||
failed |= check_var::<String>("AVALARA_1099_API_KEY");
|
||||
failed |= check_var::<String>("AVALARA_1099_API_TEAM_ID");
|
||||
failed |= check_var::<String>("AVALARA_1099_COMPANY_ID");
|
||||
|
||||
failed |= check_var::<String>("ANROK_API_URL");
|
||||
failed |= check_var::<String>("ANROK_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("COMPLIANCE_PAYOUT_THRESHOLD");
|
||||
|
||||
failed |= check_var::<String>("PAYOUT_ALERT_SLACK_WEBHOOK");
|
||||
|
||||
failed |= check_var::<String>("ARCHON_URL");
|
||||
|
||||
failed |= check_var::<String>("MURALPAY_API_URL");
|
||||
failed |= check_var::<String>("MURALPAY_API_KEY");
|
||||
failed |= check_var::<String>("MURALPAY_TRANSFER_API_KEY");
|
||||
failed |= check_var::<String>("MURALPAY_SOURCE_ACCOUNT_ID");
|
||||
|
||||
failed |= check_var::<String>("DEFAULT_AFFILIATE_REVENUE_SPLIT");
|
||||
|
||||
failed
|
||||
}
|
||||
|
||||
+47
-40
@@ -4,21 +4,21 @@ use actix_web::{App, HttpServer};
|
||||
use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use clap::Parser;
|
||||
|
||||
use labrinth::app_config;
|
||||
use labrinth::background_task::BackgroundTask;
|
||||
use labrinth::database::redis::RedisPool;
|
||||
use labrinth::env::ENV;
|
||||
use labrinth::file_hosting::{FileHostKind, S3BucketConfig, S3Host};
|
||||
use labrinth::file_hosting::{S3BucketConfig, S3Host};
|
||||
use labrinth::queue::email::EmailQueue;
|
||||
use labrinth::search;
|
||||
use labrinth::util::anrok;
|
||||
use labrinth::util::env::parse_var;
|
||||
use labrinth::util::gotenberg::GotenbergClient;
|
||||
use labrinth::util::ratelimit::rate_limit_middleware;
|
||||
use labrinth::utoipa_app_config;
|
||||
use labrinth::{app_config, env};
|
||||
use labrinth::{clickhouse, database, file_hosting};
|
||||
use labrinth::{check_env_vars, clickhouse, database, file_hosting};
|
||||
use std::ffi::CStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
use tracing::{Instrument, error, info, info_span};
|
||||
use tracing_actix_web::TracingLogger;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||
@@ -58,7 +58,11 @@ fn main() -> std::io::Result<()> {
|
||||
color_eyre::install().expect("failed to install `color-eyre`");
|
||||
dotenvy::dotenv().ok();
|
||||
modrinth_util::log::init().expect("failed to initialize logging");
|
||||
env::init().expect("failed to initialize environment variables");
|
||||
|
||||
if check_env_vars() {
|
||||
error!("Some environment variables are missing!");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Sentry must be set up before the async runtime is started
|
||||
// <https://docs.sentry.io/platforms/rust/guides/actix-web/>
|
||||
@@ -66,8 +70,11 @@ fn main() -> std::io::Result<()> {
|
||||
// Has no effect if not set.
|
||||
let sentry = sentry::init(sentry::ClientOptions {
|
||||
release: sentry::release_name!(),
|
||||
traces_sample_rate: ENV.SENTRY_TRACES_SAMPLE_RATE,
|
||||
environment: Some((&ENV.SENTRY_ENVIRONMENT).into()),
|
||||
traces_sample_rate: dotenvy::var("SENTRY_TRACES_SAMPLE_RATE")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("failed to parse `SENTRY_TRACES_SAMPLE_RATE` as number"),
|
||||
environment: Some(dotenvy::var("SENTRY_ENVIRONMENT").unwrap().into()),
|
||||
..Default::default()
|
||||
});
|
||||
if sentry.is_enabled() {
|
||||
@@ -92,7 +99,10 @@ async fn app() -> std::io::Result<()> {
|
||||
.unwrap();
|
||||
|
||||
if args.run_background_task.is_none() {
|
||||
info!("Starting labrinth on {}", &ENV.BIND_ADDR);
|
||||
info!(
|
||||
"Starting labrinth on {}",
|
||||
dotenvy::var("BIND_ADDR").unwrap()
|
||||
);
|
||||
|
||||
if !args.no_migrations {
|
||||
database::check_for_migrations()
|
||||
@@ -109,44 +119,40 @@ async fn app() -> std::io::Result<()> {
|
||||
// Redis connector
|
||||
let redis_pool = RedisPool::new("");
|
||||
|
||||
let storage_backend = ENV.STORAGE_BACKEND;
|
||||
let storage_backend =
|
||||
dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
|
||||
|
||||
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
|
||||
match storage_backend {
|
||||
FileHostKind::S3 => {
|
||||
let not_empty = |v: &str| -> String {
|
||||
assert!(!v.is_empty(), "S3 env var is empty");
|
||||
v.to_string()
|
||||
match storage_backend.as_str() {
|
||||
"s3" => {
|
||||
let config_from_env = |bucket_type| S3BucketConfig {
|
||||
name: parse_var(&format!("S3_{bucket_type}_BUCKET_NAME"))
|
||||
.unwrap(),
|
||||
uses_path_style: parse_var(&format!(
|
||||
"S3_{bucket_type}_USES_PATH_STYLE_BUCKET"
|
||||
))
|
||||
.unwrap(),
|
||||
region: parse_var(&format!("S3_{bucket_type}_REGION"))
|
||||
.unwrap(),
|
||||
url: parse_var(&format!("S3_{bucket_type}_URL")).unwrap(),
|
||||
access_token: parse_var(&format!(
|
||||
"S3_{bucket_type}_ACCESS_TOKEN"
|
||||
))
|
||||
.unwrap(),
|
||||
secret: parse_var(&format!("S3_{bucket_type}_SECRET"))
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
Arc::new(
|
||||
S3Host::new(
|
||||
S3BucketConfig {
|
||||
name: not_empty(&ENV.S3_PUBLIC_BUCKET_NAME),
|
||||
uses_path_style: ENV
|
||||
.S3_PUBLIC_USES_PATH_STYLE_BUCKET,
|
||||
region: not_empty(&ENV.S3_PUBLIC_REGION),
|
||||
url: not_empty(&ENV.S3_PUBLIC_URL),
|
||||
access_token: not_empty(
|
||||
&ENV.S3_PUBLIC_ACCESS_TOKEN,
|
||||
),
|
||||
secret: not_empty(&ENV.S3_PUBLIC_SECRET),
|
||||
},
|
||||
S3BucketConfig {
|
||||
name: not_empty(&ENV.S3_PRIVATE_BUCKET_NAME),
|
||||
uses_path_style: ENV
|
||||
.S3_PRIVATE_USES_PATH_STYLE_BUCKET,
|
||||
region: not_empty(&ENV.S3_PRIVATE_REGION),
|
||||
url: not_empty(&ENV.S3_PRIVATE_URL),
|
||||
access_token: not_empty(
|
||||
&ENV.S3_PRIVATE_ACCESS_TOKEN,
|
||||
),
|
||||
secret: not_empty(&ENV.S3_PRIVATE_SECRET),
|
||||
},
|
||||
config_from_env("PUBLIC"),
|
||||
config_from_env("PRIVATE"),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
FileHostKind::Local => Arc::new(file_hosting::MockHost::new()),
|
||||
"local" => Arc::new(file_hosting::MockHost::new()),
|
||||
_ => panic!("Invalid storage backend specified. Aborting startup!"),
|
||||
};
|
||||
|
||||
info!("Initializing clickhouse connection");
|
||||
@@ -154,7 +160,8 @@ async fn app() -> std::io::Result<()> {
|
||||
|
||||
let search_config = search::SearchConfig::new(None);
|
||||
|
||||
let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone());
|
||||
let stripe_client =
|
||||
stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
|
||||
|
||||
let anrok_client = anrok::Client::from_env().unwrap();
|
||||
let email_queue =
|
||||
@@ -265,7 +272,7 @@ async fn app() -> std::io::Result<()> {
|
||||
.into_app()
|
||||
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
|
||||
})
|
||||
.bind(&ENV.BIND_ADDR)?
|
||||
.bind(dotenvy::var("BIND_ADDR").unwrap())?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{env::ENV, models::v2::projects::LegacySideType};
|
||||
use crate::{
|
||||
models::v2::projects::LegacySideType, util::env::parse_strings_from_var,
|
||||
};
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
@@ -42,7 +44,9 @@ fn validate_download_url(
|
||||
return Err(validator::ValidationError::new("invalid URL"));
|
||||
}
|
||||
|
||||
if !ENV.WHITELISTED_MODPACK_DOMAINS.contains(
|
||||
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS")
|
||||
.unwrap_or_default();
|
||||
if !domains.contains(
|
||||
&url.domain()
|
||||
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?
|
||||
.to_string(),
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::database::models::{
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::billing::{
|
||||
ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration,
|
||||
ProductMetadata, SubscriptionMetadata, SubscriptionStatus,
|
||||
@@ -914,10 +913,10 @@ async fn unprovision_subscriptions(
|
||||
let res = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/suspend",
|
||||
ENV.ARCHON_URL,
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
server_id
|
||||
))
|
||||
.header("X-Master-Key", &ENV.PYRO_API_KEY)
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"reason": if charge.status == ChargeStatus::Cancelled || charge.status == ChargeStatus::Expiring {
|
||||
"cancelled"
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::database::models::notifications_template_item::NotificationTemplate;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::notifications::{NotificationBody, NotificationType};
|
||||
use crate::models::v3::notifications::{
|
||||
NotificationChannel, NotificationDeliveryStatus,
|
||||
@@ -37,16 +36,16 @@ impl Mailer {
|
||||
) -> Result<Arc<AsyncSmtpTransport<Tokio1Executor>>, MailError> {
|
||||
let maybe_transport = match self {
|
||||
Mailer::Uninitialized => {
|
||||
let username = &ENV.SMTP_USERNAME;
|
||||
let password = &ENV.SMTP_PASSWORD;
|
||||
let host = &ENV.SMTP_HOST;
|
||||
let port = ENV.SMTP_PORT;
|
||||
let username = dotenvy::var("SMTP_USERNAME")?;
|
||||
let password = dotenvy::var("SMTP_PASSWORD")?;
|
||||
let host = dotenvy::var("SMTP_HOST")?;
|
||||
let port =
|
||||
dotenvy::var("SMTP_PORT")?.parse::<u16>().unwrap_or(465);
|
||||
|
||||
let creds = (!username.is_empty()).then(|| {
|
||||
Credentials::new(username.clone(), password.clone())
|
||||
});
|
||||
let creds = (!username.is_empty())
|
||||
.then(|| Credentials::new(username, password));
|
||||
|
||||
let tls_setting = match ENV.SMTP_TLS.as_str() {
|
||||
let tls_setting = match dotenvy::var("SMTP_TLS")?.as_str() {
|
||||
"none" => Tls::None,
|
||||
"opportunistic_start_tls" => Tls::Opportunistic(
|
||||
TlsParameters::new(host.to_string())?,
|
||||
@@ -66,7 +65,7 @@ impl Mailer {
|
||||
};
|
||||
|
||||
let mut mailer =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(host)?
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?
|
||||
.port(port)
|
||||
.tls(tls_setting);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::database::models::{
|
||||
DBOrganization, DBProject, DBUser, DatabaseError,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::v3::notifications::NotificationBody;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::Context;
|
||||
@@ -97,18 +96,10 @@ pub struct MailingIdentity {
|
||||
impl MailingIdentity {
|
||||
pub fn from_env() -> dotenvy::Result<Self> {
|
||||
Ok(Self {
|
||||
from_name: ENV.SMTP_FROM_NAME.clone(),
|
||||
from_address: ENV.SMTP_FROM_ADDRESS.clone(),
|
||||
reply_name: if ENV.SMTP_REPLY_TO_NAME.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ENV.SMTP_REPLY_TO_NAME.clone())
|
||||
},
|
||||
reply_address: if ENV.SMTP_REPLY_TO_ADDRESS.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ENV.SMTP_REPLY_TO_ADDRESS.clone())
|
||||
},
|
||||
from_name: dotenvy::var("SMTP_FROM_NAME")?,
|
||||
from_address: dotenvy::var("SMTP_FROM_ADDRESS")?,
|
||||
reply_name: dotenvy::var("SMTP_REPLY_TO_NAME").ok(),
|
||||
reply_address: dotenvy::var("SMTP_REPLY_TO_ADDRESS").ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -567,7 +558,9 @@ async fn collect_template_variables(
|
||||
NotificationBody::ResetPassword { flow } => {
|
||||
let url = format!(
|
||||
"{}/{}?flow={}",
|
||||
ENV.SITE_URL, ENV.SITE_RESET_PASSWORD_PATH, flow
|
||||
dotenvy::var("SITE_URL")?,
|
||||
dotenvy::var("SITE_RESET_PASSWORD_PATH")?,
|
||||
flow
|
||||
);
|
||||
|
||||
map.insert(RESETPASSWORD_URL, url);
|
||||
@@ -578,7 +571,9 @@ async fn collect_template_variables(
|
||||
NotificationBody::VerifyEmail { flow } => {
|
||||
let url = format!(
|
||||
"{}/{}?flow={}",
|
||||
ENV.SITE_URL, ENV.SITE_VERIFY_EMAIL_PATH, flow
|
||||
dotenvy::var("SITE_URL")?,
|
||||
dotenvy::var("SITE_VERIFY_EMAIL_PATH")?,
|
||||
flow
|
||||
);
|
||||
|
||||
map.insert(VERIFYEMAIL_URL, url);
|
||||
@@ -608,7 +603,11 @@ async fn collect_template_variables(
|
||||
}
|
||||
|
||||
NotificationBody::PaymentFailed { amount, service } => {
|
||||
let url = format!("{}/{}", ENV.SITE_URL, ENV.SITE_BILLING_PATH,);
|
||||
let url = format!(
|
||||
"{}/{}",
|
||||
dotenvy::var("SITE_URL")?,
|
||||
dotenvy::var("SITE_BILLING_PATH")?,
|
||||
);
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(PAYMENTFAILED_AMOUNT, amount.clone());
|
||||
@@ -749,7 +748,8 @@ async fn dynamic_email_body(
|
||||
key: &str,
|
||||
) -> Result<String, ApiError> {
|
||||
get_or_set_cached_dynamic_html(redis, key, || async {
|
||||
let site_url = &ENV.SITE_URL;
|
||||
let site_url = dotenvy::var("SITE_URL")
|
||||
.wrap_internal_err("SITE_URL is not set")?;
|
||||
let site_url = site_url.trim_end_matches('/');
|
||||
|
||||
let url = format!("{site_url}/_internal/templates/email/dynamic");
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::database::PgPool;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pack::{PackFile, PackFileHash, PackFormat};
|
||||
@@ -455,7 +454,7 @@ impl AutomatedModerationQueue {
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(format!("{}/v1/fingerprints", ENV.FLAME_ANVIL_URL))
|
||||
.post(format!("{}/v1/fingerprints", dotenvy::var("FLAME_ANVIL_URL")?))
|
||||
.json(&serde_json::json!({
|
||||
"fingerprints": hashes.iter().filter_map(|x| x.3).collect::<Vec<u32>>()
|
||||
}))
|
||||
@@ -554,11 +553,11 @@ impl AutomatedModerationQueue {
|
||||
continue;
|
||||
}
|
||||
|
||||
let flame_projects = if flame_files.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let res = client
|
||||
.post(format!("{}v1/mods", ENV.FLAME_ANVIL_URL))
|
||||
let flame_projects = if flame_files.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let res = client
|
||||
.post(format!("{}v1/mods", dotenvy::var("FLAME_ANVIL_URL")?))
|
||||
.json(&serde_json::json!({
|
||||
"modIds": flame_files.iter().map(|x| x.1).collect::<Vec<_>>()
|
||||
}))
|
||||
@@ -665,16 +664,16 @@ impl AutomatedModerationQueue {
|
||||
.insert_many(members.into_iter().map(|x| x.user_id).collect(), &mut transaction, &redis)
|
||||
.await?;
|
||||
|
||||
if !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
|
||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") {
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
project.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
&ENV.MODERATION_SLACK_WEBHOOK,
|
||||
webhook_url,
|
||||
Some(
|
||||
format!(
|
||||
"*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*",
|
||||
ENV.SITE_URL,
|
||||
dotenvy::var("SITE_URL")?,
|
||||
&project.inner.status.as_friendly_str(),
|
||||
)
|
||||
.to_string(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::database::PgPool;
|
||||
use crate::env::ENV;
|
||||
use chrono::{Datelike, Duration, TimeZone, Utc};
|
||||
use eyre::{Context, Result, eyre};
|
||||
use rust_decimal::{Decimal, dec};
|
||||
@@ -63,7 +62,11 @@ pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> {
|
||||
.await
|
||||
.wrap_err("failed to fetch charges awaiting affiliate payout")?;
|
||||
|
||||
let default_affiliate_revenue_split = ENV.DEFAULT_AFFILIATE_REVENUE_SPLIT;
|
||||
let default_affiliate_revenue_split =
|
||||
dotenvy::var("DEFAULT_AFFILIATE_REVENUE_SPLIT")
|
||||
.wrap_err("no env var `DEFAULT_AFFILIATE_REVENUE_SPLIT`")?
|
||||
.parse::<Decimal>()
|
||||
.wrap_err("`DEFAULT_AFFILIATE_REVENUE_SPLIT` is not a decimal")?;
|
||||
|
||||
let (
|
||||
mut insert_usap_charges,
|
||||
|
||||
@@ -8,7 +8,6 @@ use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
database::models::payout_item::DBPayout,
|
||||
env::ENV,
|
||||
models::payouts::{
|
||||
PayoutMethod, PayoutMethodFee, PayoutMethodType, PayoutStatus,
|
||||
TremendousCurrency, TremendousDetails, TremendousForexResponse,
|
||||
@@ -211,7 +210,7 @@ pub(super) async fn execute(
|
||||
"products": [
|
||||
method_id,
|
||||
],
|
||||
"campaign_id": ENV.TREMENDOUS_CAMPAIGN_ID.as_str(),
|
||||
"campaign_id": dotenvy::var("TREMENDOUS_CAMPAIGN_ID")?,
|
||||
}]
|
||||
});
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::payouts_values_notifications;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::payouts::{
|
||||
PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodType,
|
||||
TremendousForexResponse,
|
||||
};
|
||||
use crate::models::projects::MonetizationStatus;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::env::env_var;
|
||||
use crate::util::error::Context;
|
||||
use crate::util::webhook::{
|
||||
PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
|
||||
@@ -76,18 +76,21 @@ impl Default for PayoutsQueue {
|
||||
}
|
||||
|
||||
pub fn create_muralpay_client() -> Result<muralpay::Client> {
|
||||
Ok(muralpay::Client::new(
|
||||
&ENV.MURALPAY_API_URL,
|
||||
ENV.MURALPAY_API_KEY.as_str(),
|
||||
ENV.MURALPAY_TRANSFER_API_KEY.as_str(),
|
||||
))
|
||||
let api_url = env_var("MURALPAY_API_URL")?;
|
||||
let api_key = env_var("MURALPAY_API_KEY")?;
|
||||
let transfer_api_key = env_var("MURALPAY_TRANSFER_API_KEY")?;
|
||||
Ok(muralpay::Client::new(api_url, api_key, transfer_api_key))
|
||||
}
|
||||
|
||||
pub fn create_muralpay() -> Result<MuralPayConfig> {
|
||||
let client = create_muralpay_client()?;
|
||||
let source_account_id = env_var("MURALPAY_SOURCE_ACCOUNT_ID")?
|
||||
.parse::<muralpay::AccountId>()
|
||||
.wrap_err("failed to parse source account ID")?;
|
||||
|
||||
Ok(MuralPayConfig {
|
||||
client,
|
||||
source_account_id: ENV.MURALPAY_SOURCE_ACCOUNT_ID,
|
||||
source_account_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,8 +185,11 @@ impl PayoutsQueue {
|
||||
let mut creds = self.credential.write().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let combined_key =
|
||||
format!("{}:{}", ENV.PAYPAL_CLIENT_ID, ENV.PAYPAL_CLIENT_SECRET);
|
||||
let combined_key = format!(
|
||||
"{}:{}",
|
||||
dotenvy::var("PAYPAL_CLIENT_ID")?,
|
||||
dotenvy::var("PAYPAL_CLIENT_SECRET")?
|
||||
);
|
||||
let formatted_key = format!(
|
||||
"Basic {}",
|
||||
base64::engine::general_purpose::STANDARD.encode(combined_key)
|
||||
@@ -200,7 +206,7 @@ impl PayoutsQueue {
|
||||
}
|
||||
|
||||
let credential: PaypalCredential = client
|
||||
.post(format!("{}oauth2/token", ENV.PAYPAL_API_URL))
|
||||
.post(format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Language", "en_US")
|
||||
.header("Authorization", formatted_key)
|
||||
@@ -268,7 +274,7 @@ impl PayoutsQueue {
|
||||
if no_api_prefix.unwrap_or(false) {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("{}{path}", ENV.PAYPAL_API_URL)
|
||||
format!("{}{path}", dotenvy::var("PAYPAL_API_URL")?)
|
||||
},
|
||||
)
|
||||
.header(
|
||||
@@ -349,10 +355,13 @@ impl PayoutsQueue {
|
||||
) -> Result<X, ApiError> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client
|
||||
.request(method, format!("{}{path}", ENV.TREMENDOUS_API_URL))
|
||||
.request(
|
||||
method,
|
||||
format!("{}{path}", dotenvy::var("TREMENDOUS_API_URL")?),
|
||||
)
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", ENV.TREMENDOUS_API_KEY),
|
||||
format!("Bearer {}", dotenvy::var("TREMENDOUS_API_KEY")?),
|
||||
);
|
||||
|
||||
if let Some(body) = body {
|
||||
@@ -502,8 +511,8 @@ impl PayoutsQueue {
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(format!("{}accounts/cash", ENV.BREX_API_URL))
|
||||
.bearer_auth(&ENV.BREX_API_KEY)
|
||||
.get(format!("{}accounts/cash", dotenvy::var("BREX_API_URL")?))
|
||||
.bearer_auth(&dotenvy::var("BREX_API_KEY")?)
|
||||
.send()
|
||||
.await?
|
||||
.json::<BrexResponse>()
|
||||
@@ -529,16 +538,16 @@ impl PayoutsQueue {
|
||||
|
||||
pub async fn get_paypal_balance() -> Result<Option<AccountBalance>, ApiError>
|
||||
{
|
||||
let api_username = &ENV.PAYPAL_NVP_USERNAME;
|
||||
let api_password = &ENV.PAYPAL_NVP_PASSWORD;
|
||||
let api_signature = &ENV.PAYPAL_NVP_SIGNATURE;
|
||||
let api_username = dotenvy::var("PAYPAL_NVP_USERNAME")?;
|
||||
let api_password = dotenvy::var("PAYPAL_NVP_PASSWORD")?;
|
||||
let api_signature = dotenvy::var("PAYPAL_NVP_SIGNATURE")?;
|
||||
|
||||
let mut params = HashMap::new();
|
||||
params.insert("METHOD", "GetBalance");
|
||||
params.insert("VERSION", "204");
|
||||
params.insert("USER", api_username);
|
||||
params.insert("PWD", api_password);
|
||||
params.insert("SIGNATURE", api_signature);
|
||||
params.insert("USER", &api_username);
|
||||
params.insert("PWD", &api_password);
|
||||
params.insert("SIGNATURE", &api_signature);
|
||||
params.insert("RETURNALLCURRENCIES", "1");
|
||||
|
||||
let endpoint = "https://api-3t.paypal.com/nvp";
|
||||
@@ -861,7 +870,7 @@ pub async fn make_aditude_request(
|
||||
) -> Result<Vec<AditudePoints>, ApiError> {
|
||||
let request = reqwest::Client::new()
|
||||
.post("https://cloud.aditude.io/api/public/insights/metrics")
|
||||
.bearer_auth(&ENV.ADITUDE_API_KEY)
|
||||
.bearer_auth(&dotenvy::var("ADITUDE_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"metrics": metrics,
|
||||
"range": range,
|
||||
@@ -1317,25 +1326,25 @@ pub async fn insert_bank_balances_and_webhook(
|
||||
if inserted {
|
||||
check_balance_with_webhook(
|
||||
"paypal",
|
||||
ENV.PAYPAL_BALANCE_ALERT_THRESHOLD,
|
||||
"PAYPAL_BALANCE_ALERT_THRESHOLD",
|
||||
paypal_result,
|
||||
)
|
||||
.await?;
|
||||
check_balance_with_webhook(
|
||||
"brex",
|
||||
ENV.BREX_BALANCE_ALERT_THRESHOLD,
|
||||
"BREX_BALANCE_ALERT_THRESHOLD",
|
||||
brex_result,
|
||||
)
|
||||
.await?;
|
||||
check_balance_with_webhook(
|
||||
"tremendous",
|
||||
ENV.TREMENDOUS_BALANCE_ALERT_THRESHOLD,
|
||||
"TREMENDOUS_BALANCE_ALERT_THRESHOLD",
|
||||
tremendous_result,
|
||||
)
|
||||
.await?;
|
||||
check_balance_with_webhook(
|
||||
"mural",
|
||||
ENV.MURAL_BALANCE_ALERT_THRESHOLD,
|
||||
"MURAL_BALANCE_ALERT_THRESHOLD",
|
||||
mural_result,
|
||||
)
|
||||
.await?;
|
||||
@@ -1348,11 +1357,14 @@ pub async fn insert_bank_balances_and_webhook(
|
||||
|
||||
async fn check_balance_with_webhook(
|
||||
source: &str,
|
||||
threshold: u64,
|
||||
threshold_env_var_name: &str,
|
||||
result: Result<Option<AccountBalance>, ApiError>,
|
||||
) -> Result<Option<AccountBalance>, ApiError> {
|
||||
let maybe_threshold = if threshold > 0 { Some(threshold) } else { None };
|
||||
let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK;
|
||||
let maybe_threshold = dotenvy::var(threshold_env_var_name)
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
.filter(|x| *x != 0);
|
||||
let payout_alert_webhook = dotenvy::var("PAYOUT_ALERT_SLACK_WEBHOOK")?;
|
||||
|
||||
match &result {
|
||||
Ok(Some(account_balance)) => {
|
||||
@@ -1367,7 +1379,7 @@ async fn check_balance_with_webhook(
|
||||
threshold,
|
||||
current_balance: available,
|
||||
},
|
||||
payout_alert_webhook,
|
||||
&payout_alert_webhook,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1382,7 +1394,7 @@ async fn check_balance_with_webhook(
|
||||
source: source.to_owned(),
|
||||
display_error: error.to_string(),
|
||||
},
|
||||
payout_alert_webhook,
|
||||
&payout_alert_webhook,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::analytics::{PageView, Playtime};
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::analytics::AnalyticsQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::date::get_current_tenths_of_ms;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use actix_web::{post, web};
|
||||
use serde::Deserialize;
|
||||
@@ -73,10 +73,11 @@ pub async fn page_view_ingest(
|
||||
})?;
|
||||
let url_origin = url.origin().ascii_serialization();
|
||||
|
||||
let is_valid_url_origin = ENV
|
||||
.ANALYTICS_ALLOWED_ORIGINS
|
||||
.iter()
|
||||
.any(|origin| origin == "*" || url_origin == *origin);
|
||||
let is_valid_url_origin =
|
||||
parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS")
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.any(|origin| origin == "*" || url_origin == *origin);
|
||||
|
||||
if !is_valid_url_origin {
|
||||
return Err(ApiError::InvalidInput(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{collections::HashMap, net::Ipv4Addr, sync::Arc};
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::env::ENV;
|
||||
use crate::{
|
||||
auth::get_user_from_headers,
|
||||
database::{
|
||||
@@ -14,7 +13,10 @@ use crate::{
|
||||
},
|
||||
queue::{analytics::AnalyticsQueue, session::AuthQueue},
|
||||
routes::analytics::FILTERED_HEADERS,
|
||||
util::{date::get_current_tenths_of_ms, error::Context},
|
||||
util::{
|
||||
date::get_current_tenths_of_ms, env::parse_strings_from_var,
|
||||
error::Context,
|
||||
},
|
||||
};
|
||||
use actix_web::{HttpRequest, delete, get, patch, post, put, web};
|
||||
use ariadne::ids::UserId;
|
||||
@@ -68,10 +70,11 @@ async fn ingest_click(
|
||||
})?;
|
||||
let url_origin = url.origin().ascii_serialization();
|
||||
|
||||
let is_valid_url_origin = ENV
|
||||
.ANALYTICS_ALLOWED_ORIGINS
|
||||
.iter()
|
||||
.any(|origin| origin == "*" || url_origin == *origin);
|
||||
let is_valid_url_origin =
|
||||
parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS")
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.any(|origin| origin == "*" || url_origin == *origin);
|
||||
|
||||
if !is_valid_url_origin {
|
||||
return Err(ApiError::InvalidInput(
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::database::models::{
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::billing::{
|
||||
Charge, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration,
|
||||
Product, ProductMetadata, ProductPrice, SubscriptionMetadata,
|
||||
@@ -1438,7 +1437,7 @@ pub async fn active_servers(
|
||||
pool: web::Data<PgPool>,
|
||||
query: web::Query<ActiveServersQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let master_key = &ENV.PYRO_API_KEY;
|
||||
let master_key = dotenvy::var("PYRO_API_KEY")?;
|
||||
|
||||
if req
|
||||
.head()
|
||||
@@ -1627,7 +1626,7 @@ pub async fn stripe_webhook(
|
||||
if let Ok(event) = Webhook::construct_event(
|
||||
&payload,
|
||||
stripe_signature,
|
||||
&ENV.STRIPE_WEBHOOK_SECRET,
|
||||
&dotenvy::var("STRIPE_WEBHOOK_SECRET")?,
|
||||
) {
|
||||
struct PaymentIntentMetadata {
|
||||
pub user_item: crate::database::models::user_item::DBUser,
|
||||
@@ -2037,23 +2036,23 @@ pub async fn stripe_webhook(
|
||||
client
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/unsuspend",
|
||||
ENV.ARCHON_URL,
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
id
|
||||
))
|
||||
.header("X-Master-Key", &ENV.PYRO_API_KEY)
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
client
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/reallocate",
|
||||
ENV.ARCHON_URL,
|
||||
id
|
||||
))
|
||||
"{}/modrinth/v0/servers/{}/reallocate",
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
id
|
||||
))
|
||||
.header(
|
||||
"X-Master-Key",
|
||||
&ENV.PYRO_API_KEY,
|
||||
dotenvy::var("PYRO_API_KEY")?,
|
||||
)
|
||||
.json(&body)
|
||||
.send()
|
||||
@@ -2115,9 +2114,9 @@ pub async fn stripe_webhook(
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/create",
|
||||
ENV.ARCHON_URL,
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
))
|
||||
.header("X-Master-Key", &ENV.PYRO_API_KEY)
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"user_id": to_base62(metadata.user_item.id.0 as u64),
|
||||
"name": server_name,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant};
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::env::ENV;
|
||||
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
||||
use chrono::{DateTime, Utc};
|
||||
use eyre::eyre;
|
||||
@@ -59,42 +58,15 @@ static DELPHI_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// Type of [`DelphiReportIssueDetails::key`].
|
||||
///
|
||||
/// Delphi may provide `null` for the key, but we require a key for storing
|
||||
/// issue details in the database, since detail verdicts are keyed by
|
||||
/// (project id, issue detail key). Keys are opaque strings generated by Delphi
|
||||
/// which refer to some "unique location" in a JAR file, such that subsequent
|
||||
/// Delphi scans of different JARs with the same issue detail will result in
|
||||
/// having the same key.
|
||||
///
|
||||
/// If Delphi doesn't provide us with a key, we generate a random one.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IssueDetailKey(pub String);
|
||||
|
||||
impl<'de> Deserialize<'de> for IssueDetailKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
let value = value.unwrap_or_else(|| {
|
||||
format!("<no-key-{:016x}>", rand::random::<u64>())
|
||||
});
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
struct DelphiReportIssueDetails {
|
||||
pub file: String,
|
||||
pub key: IssueDetailKey,
|
||||
pub jar: Option<String>,
|
||||
pub key: String,
|
||||
pub data: HashMap<String, serde_json::Value>,
|
||||
pub severity: DelphiSeverity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
struct DelphiReport {
|
||||
pub url: String,
|
||||
pub project_id: crate::models::ids::ProjectId,
|
||||
@@ -117,7 +89,7 @@ impl DelphiReport {
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), ApiError> {
|
||||
let webhook_url = ENV.DELPHI_SLACK_WEBHOOK.clone();
|
||||
let webhook_url = dotenvy::var("DELPHI_SLACK_WEBHOOK")?;
|
||||
|
||||
let mut message_header =
|
||||
format!("⚠️ Suspicious traces found at {}", self.url);
|
||||
@@ -143,7 +115,7 @@ impl DelphiReport {
|
||||
self.project_id,
|
||||
pool,
|
||||
redis,
|
||||
&webhook_url,
|
||||
webhook_url,
|
||||
Some(message_header),
|
||||
)
|
||||
.await
|
||||
@@ -232,34 +204,11 @@ async fn ingest_report_deserialized(
|
||||
.await
|
||||
.wrap_internal_err("failed to check if pending issue details exist")?;
|
||||
|
||||
let issue_detail_keys = report
|
||||
.issues
|
||||
.values()
|
||||
.flatten()
|
||||
.map(|issue_detail| issue_detail.key.0.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let has_unflagged_issue_details = sqlx::query!(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM unnest($2::text[]) AS incoming(detail_key)
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON didv.project_id = $1 AND didv.detail_key = incoming.detail_key
|
||||
WHERE didv.project_id IS NULL
|
||||
) AS "has_unflagged_issue_details!"
|
||||
"#,
|
||||
DBProjectId::from(report.project_id) as _,
|
||||
&issue_detail_keys
|
||||
)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to check if report has unflagged issue details")?;
|
||||
|
||||
let should_enter_tech_review = !record.pending_issue_details_exist
|
||||
&& has_unflagged_issue_details.has_unflagged_issue_details;
|
||||
|
||||
if should_enter_tech_review {
|
||||
if record.pending_issue_details_exist {
|
||||
info!(
|
||||
"File's project already has pending issue details, is not entering tech review queue"
|
||||
);
|
||||
} else {
|
||||
info!("File's project is entering tech review queue");
|
||||
|
||||
ThreadMessageBuilder {
|
||||
@@ -271,10 +220,6 @@ async fn ingest_report_deserialized(
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to add entering tech review message")?;
|
||||
} else {
|
||||
info!(
|
||||
"File's project is not entering tech review queue (already pending or no new unflagged issue details)"
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Currently, the way we determine if an issue is in tech review or not
|
||||
@@ -286,7 +231,7 @@ async fn ingest_report_deserialized(
|
||||
// This is undesirable, but we can't rework the database schema to fix it
|
||||
// right now. As a hack, we add a dummy report issue which blocks the
|
||||
// project from exiting the tech review queue.
|
||||
if should_enter_tech_review {
|
||||
{
|
||||
let dummy_issue_id = DBDelphiReportIssue {
|
||||
id: DelphiReportIssueId(0), // This will be set by the database
|
||||
report_id,
|
||||
@@ -299,7 +244,6 @@ async fn ingest_report_deserialized(
|
||||
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
||||
issue_id: dummy_issue_id,
|
||||
key: "".into(),
|
||||
jar: None,
|
||||
file_path: "".into(),
|
||||
decompiled_source: None,
|
||||
data: HashMap::new(),
|
||||
@@ -330,8 +274,7 @@ async fn ingest_report_deserialized(
|
||||
ReportIssueDetail {
|
||||
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
||||
issue_id,
|
||||
key: issue_detail.key.0,
|
||||
jar: issue_detail.jar,
|
||||
key: issue_detail.key,
|
||||
file_path: issue_detail.file,
|
||||
decompiled_source: decompiled_source.cloned().flatten(),
|
||||
data: issue_detail.data,
|
||||
@@ -374,7 +317,7 @@ pub async fn run(
|
||||
);
|
||||
|
||||
DELPHI_CLIENT
|
||||
.post(&ENV.DELPHI_URL)
|
||||
.post(dotenvy::var("DELPHI_URL")?)
|
||||
.json(&serde_json::json!({
|
||||
"url": file_data.url,
|
||||
"project_id": ProjectId(file_data.project_id.0 as u64),
|
||||
@@ -389,83 +332,6 @@ pub async fn run(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
pub async fn is_project_in_tech_review(
|
||||
project_id: DBProjectId,
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<bool, ApiError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
didws.project_id = $1
|
||||
AND didws.status = 'pending'
|
||||
-- see delphi.rs todo comment
|
||||
AND dri.issue_type != '__dummy'
|
||||
) AS "is_in_tech_review!"
|
||||
"#,
|
||||
project_id as _,
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project tech review state")?;
|
||||
|
||||
Ok(row.is_in_tech_review)
|
||||
}
|
||||
|
||||
pub async fn send_tech_review_exit_file_deleted_message(
|
||||
project_id: DBProjectId,
|
||||
txn: &mut crate::database::PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let thread = sqlx::query!(
|
||||
r#"
|
||||
SELECT id AS "thread_id: DBThreadId"
|
||||
FROM threads
|
||||
WHERE mod_id = $1
|
||||
LIMIT 1
|
||||
"#,
|
||||
project_id as _,
|
||||
)
|
||||
.fetch_optional(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch thread for tech review exit message")?;
|
||||
|
||||
if let Some(thread) = thread {
|
||||
ThreadMessageBuilder {
|
||||
author_id: None,
|
||||
body: MessageBody::TechReviewExitFileDeleted,
|
||||
thread_id: thread.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to add tech review exit message")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_tech_review_exit_file_deleted_message_if_exited(
|
||||
project_id: DBProjectId,
|
||||
was_in_tech_review: bool,
|
||||
txn: &mut crate::database::PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
if !was_in_tech_review {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let is_still_in_tech_review =
|
||||
is_project_in_tech_review(project_id, &mut *txn).await?;
|
||||
|
||||
if !is_still_in_tech_review {
|
||||
send_tech_review_exit_file_deleted_message(project_id, txn).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("run")]
|
||||
async fn _run(
|
||||
req: HttpRequest,
|
||||
@@ -541,7 +407,7 @@ async fn issue_type_schema(
|
||||
&cache_entry
|
||||
.insert((
|
||||
DELPHI_CLIENT
|
||||
.get(format!("{}/schema", ENV.DELPHI_URL))
|
||||
.get(format!("{}/schema", dotenvy::var("DELPHI_URL")?))
|
||||
.send()
|
||||
.await
|
||||
.and_then(|res| res.error_for_status())
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::database::models::flow_item::DBFlow;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::{DBUser, DBUserId};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -18,6 +17,7 @@ use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::session::issue_session;
|
||||
use crate::util::captcha::check_hcaptcha;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use crate::util::error::Context;
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
@@ -257,41 +257,41 @@ impl AuthProvider {
|
||||
&self,
|
||||
state: String,
|
||||
) -> Result<String, AuthenticationError> {
|
||||
let self_addr = &ENV.SELF_ADDR;
|
||||
let self_addr = dotenvy::var("SELF_ADDR")?;
|
||||
let raw_redirect_uri = format!("{self_addr}/v2/auth/callback");
|
||||
let redirect_uri = urlencoding::encode(&raw_redirect_uri);
|
||||
|
||||
Ok(match self {
|
||||
AuthProvider::GitHub => {
|
||||
let client_id = &ENV.GITHUB_CLIENT_ID;
|
||||
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://github.com/login/oauth/authorize?client_id={client_id}&prompt=select_account&state={state}&scope=read%3Auser%20user%3Aemail&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Discord => {
|
||||
let client_id = &ENV.DISCORD_CLIENT_ID;
|
||||
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}"
|
||||
)
|
||||
}
|
||||
AuthProvider::Microsoft => {
|
||||
let client_id = &ENV.MICROSOFT_CLIENT_ID;
|
||||
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&scope=user.read&state={state}&prompt=select_account&redirect_uri={redirect_uri}"
|
||||
)
|
||||
}
|
||||
AuthProvider::GitLab => {
|
||||
let client_id = &ENV.GITLAB_CLIENT_ID;
|
||||
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://gitlab.com/oauth/authorize?client_id={client_id}&state={state}&scope=read_user+profile+email&response_type=code&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Google => {
|
||||
let client_id = &ENV.GOOGLE_CLIENT_ID;
|
||||
let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&scope={}&response_type=code&redirect_uri={}",
|
||||
@@ -317,8 +317,8 @@ impl AuthProvider {
|
||||
)
|
||||
}
|
||||
AuthProvider::PayPal => {
|
||||
let api_url = &ENV.PAYPAL_API_URL;
|
||||
let client_id = &ENV.PAYPAL_CLIENT_ID;
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?;
|
||||
|
||||
let auth_url = if api_url.contains("sandbox") {
|
||||
"sandbox.paypal.com"
|
||||
@@ -340,7 +340,8 @@ impl AuthProvider {
|
||||
&self,
|
||||
query: HashMap<String, String>,
|
||||
) -> Result<String, AuthenticationError> {
|
||||
let redirect_uri = format!("{}/v2/auth/callback", &ENV.SELF_ADDR);
|
||||
let redirect_uri =
|
||||
format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AccessToken {
|
||||
@@ -352,8 +353,8 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = ENV.GITHUB_CLIENT_ID.as_str();
|
||||
let client_secret = ENV.GITHUB_CLIENT_SECRET.as_str();
|
||||
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?;
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}"
|
||||
@@ -373,12 +374,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = ENV.DISCORD_CLIENT_ID.as_str();
|
||||
let client_secret = ENV.DISCORD_CLIENT_SECRET.as_str();
|
||||
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("DISCORD_CLIENT_SECRET")?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", client_id);
|
||||
map.insert("client_secret", client_secret);
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
map.insert("redirect_uri", &redirect_uri);
|
||||
@@ -398,12 +399,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = ENV.MICROSOFT_CLIENT_ID.as_str();
|
||||
let client_secret = ENV.MICROSOFT_CLIENT_SECRET.as_str();
|
||||
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("MICROSOFT_CLIENT_SECRET")?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", client_id);
|
||||
map.insert("client_secret", client_secret);
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
map.insert("redirect_uri", &redirect_uri);
|
||||
@@ -423,12 +424,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = ENV.GITLAB_CLIENT_ID.as_str();
|
||||
let client_secret = ENV.GITLAB_CLIENT_SECRET.as_str();
|
||||
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("GITLAB_CLIENT_SECRET")?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", client_id);
|
||||
map.insert("client_secret", client_secret);
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
map.insert("redirect_uri", &redirect_uri);
|
||||
@@ -448,12 +449,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = ENV.GOOGLE_CLIENT_ID.as_str();
|
||||
let client_secret = ENV.GOOGLE_CLIENT_SECRET.as_str();
|
||||
let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("GOOGLE_CLIENT_SECRET")?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", client_id);
|
||||
map.insert("client_secret", client_secret);
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
map.insert("redirect_uri", &redirect_uri);
|
||||
@@ -528,9 +529,9 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let api_url = ENV.PAYPAL_API_URL.as_str();
|
||||
let client_id = ENV.PAYPAL_CLIENT_ID.as_str();
|
||||
let client_secret = ENV.PAYPAL_CLIENT_SECRET.as_str();
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("PAYPAL_CLIENT_SECRET")?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("code", code.as_str());
|
||||
@@ -579,7 +580,9 @@ impl AuthProvider {
|
||||
.get("x-oauth-client-id")
|
||||
.and_then(|x| x.to_str().ok());
|
||||
|
||||
if client_id != Some(ENV.GITHUB_CLIENT_ID.as_str()) {
|
||||
if client_id
|
||||
!= Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap())
|
||||
{
|
||||
return Err(AuthenticationError::InvalidClientId);
|
||||
}
|
||||
}
|
||||
@@ -729,7 +732,7 @@ impl AuthProvider {
|
||||
}
|
||||
}
|
||||
AuthProvider::Steam => {
|
||||
let api_key = &ENV.STEAM_API_KEY;
|
||||
let api_key = dotenvy::var("STEAM_API_KEY")?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SteamResponse {
|
||||
@@ -794,7 +797,7 @@ impl AuthProvider {
|
||||
pub country: String,
|
||||
}
|
||||
|
||||
let api_url = &ENV.PAYPAL_API_URL;
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
|
||||
let paypal_user: PayPalUser = reqwest::Client::new()
|
||||
.get(format!(
|
||||
@@ -1097,11 +1100,10 @@ pub async fn init(
|
||||
let url =
|
||||
url::Url::parse(&info.url).map_err(|_| AuthenticationError::Url)?;
|
||||
|
||||
let allowed_callback_urls =
|
||||
parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
|
||||
let domain = url.host_str().ok_or(AuthenticationError::Url)?;
|
||||
if !ENV
|
||||
.ALLOWED_CALLBACK_URLS
|
||||
.iter()
|
||||
.any(|x| domain.ends_with(x))
|
||||
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x))
|
||||
&& domain != "modrinth.com"
|
||||
{
|
||||
return Err(AuthenticationError::Url);
|
||||
@@ -1394,9 +1396,9 @@ pub async fn delete_auth_provider(
|
||||
pub async fn check_sendy_subscription(
|
||||
email: &str,
|
||||
) -> Result<bool, AuthenticationError> {
|
||||
let url = &ENV.SENDY_URL;
|
||||
let id = &ENV.SENDY_LIST_ID;
|
||||
let api_key = &ENV.SENDY_API_KEY;
|
||||
let url = dotenvy::var("SENDY_URL")?;
|
||||
let id = dotenvy::var("SENDY_LIST_ID")?;
|
||||
let api_key = dotenvy::var("SENDY_API_KEY")?;
|
||||
|
||||
if url.is_empty() || url == "none" {
|
||||
tracing::info!(
|
||||
@@ -1406,9 +1408,9 @@ pub async fn check_sendy_subscription(
|
||||
}
|
||||
|
||||
let mut form = HashMap::new();
|
||||
form.insert("api_key", api_key.as_str());
|
||||
form.insert("api_key", &*api_key);
|
||||
form.insert("email", email);
|
||||
form.insert("list_id", id.as_str());
|
||||
form.insert("list_id", &*id);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{collections::HashMap, sync::LazyLock};
|
||||
|
||||
use crate::env::ENV;
|
||||
use actix_web::{get, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -29,8 +28,7 @@ static GLOBALS: LazyLock<Globals> = LazyLock::new(|| Globals {
|
||||
tax_compliance_thresholds: [(2025, 600), (2026, 2000)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
captcha_enabled: !ENV.HCAPTCHA_SECRET.is_empty()
|
||||
&& ENV.HCAPTCHA_SECRET != "none",
|
||||
captcha_enabled: dotenvy::var("HCAPTCHA_SECRET").is_ok_and(|x| x != "none"),
|
||||
});
|
||||
|
||||
/// Gets configured global non-secret variables for this backend instance.
|
||||
|
||||
@@ -42,7 +42,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
|
||||
.service(get_report)
|
||||
.service(get_issue)
|
||||
.service(submit_report)
|
||||
.service(update_issue_details)
|
||||
.service(update_issue_detail)
|
||||
.service(add_report);
|
||||
}
|
||||
|
||||
@@ -388,8 +388,6 @@ pub struct ProjectModerationInfo {
|
||||
pub thread_id: ThreadId,
|
||||
/// Project name.
|
||||
pub name: String,
|
||||
/// Current project status.
|
||||
pub status: ProjectStatus,
|
||||
/// The aggregated project typos of the versions of this project
|
||||
#[serde(default)]
|
||||
pub project_types: Vec<String>,
|
||||
@@ -500,7 +498,6 @@ async fn fetch_project_reports(
|
||||
drid.id AS "id!: DelphiReportIssueDetailsId",
|
||||
drid.issue_id AS "issue_id!: DelphiReportIssueId",
|
||||
drid.key AS "key!: String",
|
||||
drid.jar AS "jar?: String",
|
||||
drid.file_path AS "file_path!: String",
|
||||
drid.data AS "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
||||
drid.severity AS "severity!: DelphiSeverity",
|
||||
@@ -564,7 +561,6 @@ async fn fetch_project_reports(
|
||||
id: d.id,
|
||||
issue_id: d.issue_id,
|
||||
key: d.key,
|
||||
jar: d.jar,
|
||||
file_path: d.file_path,
|
||||
decompiled_source: None,
|
||||
data: d.data.0,
|
||||
@@ -702,9 +698,9 @@ async fn search_projects(
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
SELECT DISTINCT ON (m.id)
|
||||
m.id AS "project_id: DBProjectId",
|
||||
MIN(t.id) AS "thread_id!: DBThreadId"
|
||||
t.id AS "thread_id: DBThreadId"
|
||||
FROM mods m
|
||||
INNER JOIN threads t ON t.mod_id = m.id
|
||||
INNER JOIN versions v ON v.mod_id = m.id
|
||||
@@ -738,14 +734,12 @@ async fn search_projects(
|
||||
OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))
|
||||
OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))
|
||||
)
|
||||
GROUP BY m.id
|
||||
ORDER BY
|
||||
CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,
|
||||
CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,
|
||||
CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,
|
||||
CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,
|
||||
-- tie-breaker: oldest reports
|
||||
MIN(dr.created) ASC
|
||||
GROUP BY m.id, t.id
|
||||
ORDER BY m.id,
|
||||
CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,
|
||||
CASE WHEN $3 = 'created_desc' THEN MAX(dr.created) ELSE TO_TIMESTAMP(0) END DESC,
|
||||
CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,
|
||||
CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#,
|
||||
limit,
|
||||
@@ -837,7 +831,6 @@ async fn search_projects(
|
||||
id,
|
||||
thread_id: project.thread_id,
|
||||
name: project.name,
|
||||
status: project.status,
|
||||
project_types: project.project_types,
|
||||
icon_url: project.icon_url,
|
||||
},
|
||||
@@ -1126,8 +1119,6 @@ async fn submit_report(
|
||||
/// See [`update_issue`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateIssue {
|
||||
/// ID of the issue detail to update.
|
||||
pub detail_id: DelphiReportIssueDetailsId,
|
||||
/// What the moderator has decided the outcome of this issue is.
|
||||
pub verdict: DelphiVerdict,
|
||||
}
|
||||
@@ -1140,13 +1131,14 @@ pub struct UpdateIssue {
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[patch("/issue-detail")]
|
||||
async fn update_issue_details(
|
||||
#[patch("/issue-detail/{id}")]
|
||||
async fn update_issue_detail(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
update_reqs: web::Json<Vec<UpdateIssue>>,
|
||||
update_req: web::Json<UpdateIssue>,
|
||||
path: web::Path<(DelphiReportIssueDetailsId,)>,
|
||||
) -> Result<(), ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
@@ -1156,76 +1148,44 @@ async fn update_issue_details(
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
let (issue_detail_id,) = path.into_inner();
|
||||
|
||||
let mut txn = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to start transaction")?;
|
||||
|
||||
let updates = update_reqs.into_inner();
|
||||
let detail_ids = updates.iter().map(|u| u.detail_id.0).collect::<Vec<_>>();
|
||||
let verdicts = updates
|
||||
.iter()
|
||||
.map(|u| match u.verdict {
|
||||
DelphiVerdict::Safe => "safe".to_string(),
|
||||
DelphiVerdict::Unsafe => "unsafe".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let record = sqlx::query!(
|
||||
let status = match update_req.verdict {
|
||||
DelphiVerdict::Safe => DelphiStatus::Safe,
|
||||
DelphiVerdict::Unsafe => DelphiStatus::Unsafe,
|
||||
};
|
||||
let results = sqlx::query!(
|
||||
r#"
|
||||
WITH incoming AS (
|
||||
SELECT *
|
||||
FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY
|
||||
AS u(detail_id, verdict, ord)
|
||||
),
|
||||
resolved AS (
|
||||
SELECT
|
||||
i.ord,
|
||||
didws.project_id,
|
||||
didws.key AS detail_key,
|
||||
i.verdict::delphi_report_issue_status AS verdict
|
||||
FROM incoming i
|
||||
INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
-- see delphi.rs todo comment
|
||||
dri.issue_type != '__dummy'
|
||||
),
|
||||
validated AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM incoming) AS incoming_count,
|
||||
(SELECT COUNT(*) FROM resolved) AS resolved_count
|
||||
),
|
||||
upserted AS (
|
||||
INSERT INTO delphi_issue_detail_verdicts (
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict
|
||||
)
|
||||
SELECT DISTINCT ON (project_id, detail_key)
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict
|
||||
FROM resolved
|
||||
ORDER BY project_id, detail_key, ord DESC
|
||||
ON CONFLICT (project_id, detail_key)
|
||||
DO UPDATE SET verdict = EXCLUDED.verdict
|
||||
RETURNING 1
|
||||
INSERT INTO delphi_issue_detail_verdicts (
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict
|
||||
)
|
||||
SELECT
|
||||
(v.incoming_count = v.resolved_count) AS "all_found!",
|
||||
(SELECT COUNT(*) FROM upserted) AS "upserted_count!"
|
||||
FROM validated v
|
||||
didws.project_id,
|
||||
didws.key,
|
||||
$1
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
didws.id = $2
|
||||
-- see delphi.rs todo comment
|
||||
AND dri.issue_type != '__dummy'
|
||||
ON CONFLICT (project_id, detail_key)
|
||||
DO UPDATE SET verdict = EXCLUDED.verdict
|
||||
"#,
|
||||
&detail_ids,
|
||||
&verdicts,
|
||||
status as _,
|
||||
issue_detail_id as _,
|
||||
)
|
||||
.fetch_one(&mut txn)
|
||||
.execute(&mut txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to update issue details")?;
|
||||
|
||||
if !record.all_found {
|
||||
.wrap_internal_err("failed to update issue detail")?;
|
||||
if results.rows_affected() == 0 {
|
||||
return Err(ApiError::Request(eyre!("issue detail does not exist")));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::database::models::session_item::DBSession;
|
||||
use crate::database::models::session_item::SessionBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::sessions::Session;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::env::parse_var;
|
||||
use actix_web::http::header::AUTHORIZATION;
|
||||
use actix_web::web::{Data, ServiceConfig, scope};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
||||
@@ -41,7 +41,7 @@ pub async fn get_session_metadata(
|
||||
req: &HttpRequest,
|
||||
) -> Result<SessionMetadata, AuthenticationError> {
|
||||
let conn_info = req.connection_info().clone();
|
||||
let ip_addr = if ENV.CLOUDFLARE_INTEGRATION {
|
||||
let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
||||
if let Some(header) = req.headers().get("CF-Connecting-IP") {
|
||||
header.to_str().ok()
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::database::models::DelphiReportIssueDetailsId;
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::routes::analytics::{page_view_ingest, playtime_ingest};
|
||||
use crate::util::cors::default_cors;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use actix_cors::Cors;
|
||||
use actix_files::Files;
|
||||
use actix_web::http::StatusCode;
|
||||
@@ -40,7 +40,10 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
|
||||
.wrap(
|
||||
Cors::default()
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
let allowed_origins = &ENV.ANALYTICS_ALLOWED_ORIGINS;
|
||||
let allowed_origins =
|
||||
parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS")
|
||||
.unwrap_or_default();
|
||||
|
||||
allowed_origins.contains(&"*".to_string())
|
||||
|| allowed_origins.contains(
|
||||
&origin
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::env::ENV;
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -95,7 +94,11 @@ pub async fn forge_updates(
|
||||
}
|
||||
|
||||
let mut response = ForgeUpdates {
|
||||
homepage: format!("{}/mod/{}", ENV.SITE_URL, id),
|
||||
homepage: format!(
|
||||
"{}/mod/{}",
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
id
|
||||
),
|
||||
promos: HashMap::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::database::PgPool;
|
||||
use crate::database::models::DBUserId;
|
||||
use crate::database::models::{generate_payout_id, users_compliance};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::ids::PayoutId;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::payouts::{PayoutMethodType, PayoutStatus, Withdrawal};
|
||||
@@ -213,7 +212,7 @@ pub async fn paypal_webhook(
|
||||
\"webhook_id\": \"{}\",
|
||||
\"webhook_event\": {body}
|
||||
}}",
|
||||
ENV.PAYPAL_WEBHOOK_ID,
|
||||
dotenvy::var("PAYPAL_WEBHOOK_ID")?
|
||||
)),
|
||||
None,
|
||||
)
|
||||
@@ -323,7 +322,7 @@ pub async fn tremendous_webhook(
|
||||
})?;
|
||||
|
||||
let mut mac: Hmac<Sha256> = Hmac::new_from_slice(
|
||||
ENV.TREMENDOUS_PRIVATE_KEY.as_bytes(),
|
||||
dotenvy::var("TREMENDOUS_PRIVATE_KEY")?.as_bytes(),
|
||||
)
|
||||
.map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?;
|
||||
mac.update(body.as_bytes());
|
||||
@@ -1115,7 +1114,9 @@ async fn update_compliance_status(
|
||||
}
|
||||
|
||||
fn tax_compliance_payout_threshold() -> Option<Decimal> {
|
||||
ENV.COMPLIANCE_PAYOUT_THRESHOLD.parse().ok()
|
||||
dotenvy::var("COMPLIANCE_PAYOUT_THRESHOLD")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::database::models::{
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{self, models as db_models};
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models;
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
@@ -28,7 +27,6 @@ use crate::models::threads::MessageBody;
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::delphi;
|
||||
use crate::search::indexing::remove_documents;
|
||||
use crate::search::{SearchConfig, SearchError, search_for_project};
|
||||
use crate::util::error::Context;
|
||||
@@ -429,13 +427,13 @@ pub async fn project_edit(
|
||||
|
||||
if status.is_searchable()
|
||||
&& !project_item.inner.webhook_sent
|
||||
&& !ENV.PUBLIC_DISCORD_WEBHOOK.is_empty()
|
||||
&& let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK")
|
||||
{
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
&ENV.PUBLIC_DISCORD_WEBHOOK,
|
||||
webhook_url,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -453,16 +451,18 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if user.role.is_mod() && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
|
||||
if user.role.is_mod()
|
||||
&& let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK")
|
||||
{
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
&ENV.MODERATION_SLACK_WEBHOOK,
|
||||
webhook_url,
|
||||
Some(
|
||||
format!(
|
||||
"*<{}/user/{}|{}>* changed project status from *{}* to *{}*",
|
||||
ENV.SITE_URL,
|
||||
dotenvy::var("SITE_URL")?,
|
||||
user.username,
|
||||
user.username,
|
||||
&project_item.inner.status.as_friendly_str(),
|
||||
@@ -2219,18 +2219,6 @@ pub async fn project_delete(
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to start transaction")?;
|
||||
let was_in_tech_review =
|
||||
delphi::is_project_in_tech_review(project.inner.id, &mut transaction)
|
||||
.await?;
|
||||
|
||||
if was_in_tech_review {
|
||||
delphi::send_tech_review_exit_file_deleted_message(
|
||||
project.inner.id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let context = ImageContext::Project {
|
||||
project_id: Some(project.inner.id.into()),
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::database::models::image_item;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::ids::{ThreadId, ThreadMessageId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
@@ -632,8 +631,9 @@ pub async fn message_delete(
|
||||
let images =
|
||||
database::DBImage::get_many_contexted(context, &mut transaction)
|
||||
.await?;
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
for image in images {
|
||||
let name = image.url.split(&format!("{}/", ENV.CDN_URL)).nth(1);
|
||||
let name = image.url.split(&format!("{cdn_url}/")).nth(1);
|
||||
if let Some(icon_path) = name {
|
||||
file_host
|
||||
.delete_file(
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::database::models::version_item::{
|
||||
};
|
||||
use crate::database::models::{self, DBOrganization, image_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::ids::{ImageId, ProjectId, VersionId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
@@ -975,7 +974,7 @@ pub async fn upload_file(
|
||||
|
||||
version_files.push(VersionFileBuilder {
|
||||
filename: file_name.to_string(),
|
||||
url: format!("{}/{file_path_encode}", ENV.CDN_URL),
|
||||
url: format!("{}/{file_path_encode}", dotenvy::var("CDN_URL")?),
|
||||
hashes: vec![
|
||||
models::version_item::HashBuilder {
|
||||
algorithm: "sha1".to_string(),
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::projects::VersionType;
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::internal::delphi;
|
||||
use crate::{database, models};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use dashmap::DashMap;
|
||||
@@ -689,9 +688,6 @@ pub async fn delete_file(
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let was_in_tech_review =
|
||||
delphi::is_project_in_tech_review(row.project_id, &mut transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -713,13 +709,6 @@ pub async fn delete_file(
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
|
||||
delphi::send_tech_review_exit_file_deleted_message_if_exited(
|
||||
row.project_id,
|
||||
was_in_tech_review,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
|
||||
@@ -25,7 +25,6 @@ use crate::models::projects::{
|
||||
use crate::models::projects::{Loader, skip_nulls};
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::internal::delphi;
|
||||
use crate::search::SearchConfig;
|
||||
use crate::search::indexing::remove_documents;
|
||||
use crate::util::error::Context;
|
||||
@@ -960,12 +959,6 @@ pub async fn version_delete(
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let was_in_tech_review = delphi::is_project_in_tech_review(
|
||||
version.inner.project_id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let context = ImageContext::Version {
|
||||
version_id: Some(version.inner.id.into()),
|
||||
};
|
||||
@@ -984,14 +977,6 @@ pub async fn version_delete(
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
delphi::send_tech_review_exit_file_deleted_message_if_exited(
|
||||
version.inner.project_id,
|
||||
was_in_tech_review,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
database::models::DBProject::clear_cache(
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::time::Duration;
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::search::{SearchConfig, UploadSearchProject};
|
||||
use crate::util::error::Context;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
@@ -44,7 +43,12 @@ pub enum IndexingError {
|
||||
const MEILISEARCH_CHUNK_SIZE: usize = 50000; // 10_000_000
|
||||
|
||||
fn search_operation_timeout() -> std::time::Duration {
|
||||
std::time::Duration::from_millis(ENV.SEARCH_OPERATION_TIMEOUT)
|
||||
let default_ms = 5 * 60 * 1000; // 5 minutes
|
||||
let ms = dotenvy::var("SEARCH_OPERATION_TIMEOUT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(default_ms);
|
||||
std::time::Duration::from_millis(ms)
|
||||
}
|
||||
|
||||
pub async fn remove_documents(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::env::ENV;
|
||||
use crate::models::projects::SearchRequest;
|
||||
use crate::{models::error::ApiError, search::indexing::IndexingError};
|
||||
use actix_web::HttpResponse;
|
||||
@@ -135,11 +134,26 @@ impl SearchConfig {
|
||||
// Panics if the environment variables are not set,
|
||||
// but these are already checked for on startup.
|
||||
pub fn new(meta_namespace: Option<String>) -> Self {
|
||||
let address_many = dotenvy::var("MEILISEARCH_WRITE_ADDRS")
|
||||
.expect("MEILISEARCH_WRITE_ADDRS not set");
|
||||
|
||||
let read_lb_address = dotenvy::var("MEILISEARCH_READ_ADDR")
|
||||
.expect("MEILISEARCH_READ_ADDR not set");
|
||||
|
||||
let addresses = address_many
|
||||
.split(',')
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let key =
|
||||
dotenvy::var("MEILISEARCH_KEY").expect("MEILISEARCH_KEY not set");
|
||||
|
||||
Self {
|
||||
addresses: ENV.MEILISEARCH_WRITE_ADDRS.0.clone(),
|
||||
key: ENV.MEILISEARCH_KEY.clone(),
|
||||
addresses,
|
||||
key,
|
||||
meta_namespace: meta_namespace.unwrap_or_default(),
|
||||
read_lb_address: ENV.MEILISEARCH_READ_ADDR.clone(),
|
||||
read_lb_address,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ use super::{
|
||||
environment::LocalService,
|
||||
};
|
||||
use crate::LabrinthConfig;
|
||||
use crate::env::ENV;
|
||||
use actix_web::{App, dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
use std::rc::Rc;
|
||||
@@ -47,7 +46,10 @@ impl Api for ApiV2 {
|
||||
async fn reset_search_index(&self) -> ServiceResponse {
|
||||
let req = actix_web::test::TestRequest::post()
|
||||
.uri("/v2/admin/_force_reindex")
|
||||
.append_header(("Modrinth-Admin", ENV.LABRINTH_ADMIN_KEY.clone()))
|
||||
.append_header((
|
||||
"Modrinth-Admin",
|
||||
dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(),
|
||||
))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use super::{
|
||||
environment::LocalService,
|
||||
};
|
||||
use crate::LabrinthConfig;
|
||||
use crate::env::ENV;
|
||||
use actix_web::{App, dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
use std::rc::Rc;
|
||||
@@ -52,7 +51,10 @@ impl Api for ApiV3 {
|
||||
async fn reset_search_index(&self) -> ServiceResponse {
|
||||
let req = actix_web::test::TestRequest::post()
|
||||
.uri("/_internal/admin/_force_reindex")
|
||||
.append_header(("Modrinth-Admin", ENV.LABRINTH_ADMIN_KEY.clone()))
|
||||
.append_header((
|
||||
"Modrinth-Admin",
|
||||
dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(),
|
||||
))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::database::PgPool;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{MIGRATOR, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::search;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use std::time::Duration;
|
||||
@@ -58,14 +57,15 @@ impl TemporaryDatabase {
|
||||
let temp_database_name = generate_random_name("labrinth_tests_db_");
|
||||
println!("Creating temporary database: {}", &temp_database_name);
|
||||
|
||||
let database_url = &ENV.DATABASE_URL;
|
||||
let database_url =
|
||||
dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
|
||||
// Create the temporary (and template database, if needed)
|
||||
Self::create_temporary(database_url, &temp_database_name).await;
|
||||
Self::create_temporary(&database_url, &temp_database_name).await;
|
||||
|
||||
// Pool to the temporary database
|
||||
let mut temporary_url =
|
||||
Url::parse(database_url).expect("Invalid database URL");
|
||||
Url::parse(&database_url).expect("Invalid database URL");
|
||||
|
||||
temporary_url.set_path(&format!("/{}", &temp_database_name));
|
||||
let temp_db_url = temporary_url.to_string();
|
||||
@@ -139,8 +139,10 @@ impl TemporaryDatabase {
|
||||
}
|
||||
|
||||
// Switch to template
|
||||
let mut template_url = Url::parse(&ENV.DATABASE_URL)
|
||||
.expect("Invalid database URL");
|
||||
let url =
|
||||
dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
let mut template_url =
|
||||
Url::parse(&url).expect("Invalid database URL");
|
||||
template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}"));
|
||||
|
||||
let pool = sqlx::PgPool::connect(template_url.as_str())
|
||||
@@ -232,10 +234,11 @@ impl TemporaryDatabase {
|
||||
// If a temporary db is created, it must be cleaned up with cleanup.
|
||||
// This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise.
|
||||
pub async fn cleanup(mut self) {
|
||||
let database_url = &ENV.DATABASE_URL;
|
||||
let database_url =
|
||||
dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
self.pool.close().await;
|
||||
|
||||
self.pool = sqlx::PgPool::connect(database_url)
|
||||
self.pool = sqlx::PgPool::connect(&database_url)
|
||||
.await
|
||||
.map(PgPool::from)
|
||||
.expect("Connection to main database failed");
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::env::ENV;
|
||||
use crate::queue::email::EmailQueue;
|
||||
use crate::util::anrok;
|
||||
use crate::util::gotenberg::GotenbergClient;
|
||||
use crate::{LabrinthConfig, file_hosting};
|
||||
use crate::{clickhouse, env};
|
||||
use crate::{check_env_vars, clickhouse};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod api_common;
|
||||
@@ -23,8 +22,12 @@ pub mod search;
|
||||
// If making a test, you should probably use environment::TestEnvironment::build() (which calls this)
|
||||
pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
|
||||
println!("Setting up labrinth config");
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
env::init().expect("failed to initialize environment variables");
|
||||
|
||||
if check_env_vars() {
|
||||
println!("Some environment variables are missing!");
|
||||
}
|
||||
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
@@ -36,7 +39,8 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
|
||||
Arc::new(file_hosting::MockHost::new());
|
||||
let mut clickhouse = clickhouse::init_client().await.unwrap();
|
||||
|
||||
let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone());
|
||||
let stripe_client =
|
||||
stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
|
||||
|
||||
let anrok_client = anrok::Client::from_env().unwrap();
|
||||
let email_queue =
|
||||
|
||||
@@ -7,9 +7,6 @@ use serde_with::{DisplayFromStr, serde_as};
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
|
||||
use crate::env::ENV;
|
||||
use crate::routes::ApiError;
|
||||
|
||||
pub fn transaction_id_stripe_pi(pi: &stripe::PaymentIntentId) -> String {
|
||||
format!("stripe:charge:{pi}")
|
||||
}
|
||||
@@ -157,14 +154,19 @@ pub struct Client {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn from_env() -> Result<Self, ApiError> {
|
||||
pub fn from_env() -> Result<Self, dotenvy::Error> {
|
||||
let api_key = dotenvy::var("ANROK_API_KEY")?;
|
||||
let api_url = dotenvy::var("ANROK_API_URL")?
|
||||
.trim_start_matches('/')
|
||||
.to_owned();
|
||||
|
||||
Ok(Self {
|
||||
client: reqwest::Client::builder()
|
||||
.user_agent("Modrinth")
|
||||
.build()
|
||||
.expect("AnrokClient to build"),
|
||||
api_key: ENV.ANROK_API_KEY.clone(),
|
||||
api_url: ENV.ANROK_API_URL.trim_start_matches('/').to_owned(),
|
||||
api_key,
|
||||
api_url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use reqwest::header::HeaderName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::env::ENV;
|
||||
use crate::routes::ApiError;
|
||||
|
||||
const X_MASTER_KEY: HeaderName = HeaderName::from_static("x-master-key");
|
||||
@@ -43,12 +42,13 @@ impl ArchonClient {
|
||||
pub fn from_env() -> Result<Self, ApiError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let base_url = ENV.ARCHON_URL.trim_end_matches('/').to_owned();
|
||||
let base_url =
|
||||
dotenvy::var("ARCHON_URL")?.trim_end_matches('/').to_owned();
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url,
|
||||
pyro_api_key: ENV.PYRO_API_KEY.clone(),
|
||||
pyro_api_key: dotenvy::var("PYRO_API_KEY")?,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::database::models::{DBUserId, users_compliance::FormType};
|
||||
use crate::env::ENV;
|
||||
use crate::routes::ApiError;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use chrono::Datelike;
|
||||
@@ -132,10 +131,10 @@ fn team_request(
|
||||
method: reqwest::Method,
|
||||
route: &str,
|
||||
) -> Result<(reqwest::RequestBuilder, String), ApiError> {
|
||||
let key = &ENV.AVALARA_1099_API_KEY;
|
||||
let url = &ENV.AVALARA_1099_API_URL;
|
||||
let team = &ENV.AVALARA_1099_API_TEAM_ID;
|
||||
let company = &ENV.AVALARA_1099_COMPANY_ID;
|
||||
let key = dotenvy::var("AVALARA_1099_API_KEY")?;
|
||||
let url = dotenvy::var("AVALARA_1099_API_URL")?;
|
||||
let team = dotenvy::var("AVALARA_1099_API_TEAM_ID")?;
|
||||
let company = dotenvy::var("AVALARA_1099_COMPANY_ID")?;
|
||||
|
||||
let url = url.trim_end_matches('/');
|
||||
|
||||
@@ -145,8 +144,8 @@ fn team_request(
|
||||
client
|
||||
.request(method, format!("{url}/v1/{team}{route}"))
|
||||
.header(reqwest::header::USER_AGENT, "Modrinth")
|
||||
.bearer_auth(key),
|
||||
company.to_string(),
|
||||
.bearer_auth(&key),
|
||||
company,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::env::ENV;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::env::parse_var;
|
||||
use actix_web::HttpRequest;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -8,7 +8,7 @@ pub async fn check_hcaptcha(
|
||||
req: &HttpRequest,
|
||||
challenge: &str,
|
||||
) -> Result<bool, ApiError> {
|
||||
let secret = &ENV.HCAPTCHA_SECRET;
|
||||
let secret = dotenvy::var("HCAPTCHA_SECRET")?;
|
||||
|
||||
if secret.is_empty() || secret == "none" {
|
||||
tracing::info!("hCaptcha secret not set, skipping check");
|
||||
@@ -16,7 +16,7 @@ pub async fn check_hcaptcha(
|
||||
}
|
||||
|
||||
let conn_info = req.connection_info().clone();
|
||||
let ip_addr = if ENV.CLOUDFLARE_INTEGRATION {
|
||||
let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
||||
if let Some(header) = req.headers().get("CF-Connecting-IP") {
|
||||
header.to_str().ok()
|
||||
} else {
|
||||
@@ -38,7 +38,7 @@ pub async fn check_hcaptcha(
|
||||
let mut form = HashMap::new();
|
||||
|
||||
form.insert("response", challenge);
|
||||
form.insert("secret", secret);
|
||||
form.insert("secret", &*secret);
|
||||
form.insert("remoteip", ip_addr);
|
||||
|
||||
let val: Response = client
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use eyre::{Context, eyre};
|
||||
|
||||
pub fn env_var(key: &str) -> eyre::Result<String> {
|
||||
dotenvy::var(key)
|
||||
.wrap_err_with(|| eyre!("missing environment variable `{key}`"))
|
||||
}
|
||||
|
||||
pub fn parse_var<T: FromStr>(var: &str) -> Option<T> {
|
||||
dotenvy::var(var).ok().and_then(|i| i.parse().ok())
|
||||
}
|
||||
pub fn parse_strings_from_var(var: &'static str) -> Option<Vec<String>> {
|
||||
dotenvy::var(var)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::ids::PayoutId;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError};
|
||||
use crate::util::env::env_var;
|
||||
use crate::util::error::Context;
|
||||
use actix_web::http::header::HeaderName;
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
@@ -71,14 +71,15 @@ impl GotenbergClient {
|
||||
.build()
|
||||
.wrap_err("failed to build reqwest client")?;
|
||||
|
||||
let gotenberg_url = env_var("GOTENBERG_URL")?;
|
||||
let site_url = env_var("SITE_URL")?;
|
||||
let callback_base = env_var("GOTENBERG_CALLBACK_BASE")?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
gotenberg_url: ENV.GOTENBERG_URL.trim_end_matches('/').to_owned(),
|
||||
site_url: ENV.SITE_URL.trim_end_matches('/').to_owned(),
|
||||
callback_base: ENV
|
||||
.GOTENBERG_CALLBACK_BASE
|
||||
.trim_end_matches('/')
|
||||
.to_owned(),
|
||||
gotenberg_url: gotenberg_url.trim_end_matches('/').to_owned(),
|
||||
site_url: site_url.trim_end_matches('/').to_owned(),
|
||||
callback_base: callback_base.trim_end_matches('/').to_owned(),
|
||||
redis,
|
||||
})
|
||||
}
|
||||
@@ -188,7 +189,12 @@ impl GotenbergClient {
|
||||
|
||||
self.generate_payment_statement(statement).await?;
|
||||
|
||||
let timeout_ms = ENV.GOTENBERG_TIMEOUT;
|
||||
let timeout_ms = env_var("GOTENBERG_TIMEOUT")
|
||||
.map_err(ApiError::Internal)?
|
||||
.parse::<u64>()
|
||||
.wrap_internal_err(
|
||||
"`GOTENBERG_TIMEOUT` is not a valid number of milliseconds",
|
||||
)?;
|
||||
|
||||
let [_key, document] = tokio::time::timeout(
|
||||
Duration::from_millis(timeout_ms),
|
||||
|
||||
@@ -1,33 +1,47 @@
|
||||
use actix_web::guard::GuardContext;
|
||||
use actix_web::http::header::X_FORWARDED_FOR;
|
||||
|
||||
use crate::env::ENV;
|
||||
|
||||
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
|
||||
pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key";
|
||||
pub const EXTERNAL_NOTIFICATION_KEY_HEADER: &str = "External-Notification-Key";
|
||||
|
||||
pub fn admin_key_guard(ctx: &GuardContext) -> bool {
|
||||
let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect(
|
||||
"No admin key provided, this should have been caught by check_env_vars",
|
||||
);
|
||||
ctx.head()
|
||||
.headers()
|
||||
.get(ADMIN_KEY_HEADER)
|
||||
.is_some_and(|it| it.as_bytes() == ENV.LABRINTH_ADMIN_KEY.as_bytes())
|
||||
.is_some_and(|it| it.as_bytes() == admin_key.as_bytes())
|
||||
}
|
||||
|
||||
pub fn medal_key_guard(ctx: &GuardContext) -> bool {
|
||||
ctx.head()
|
||||
.headers()
|
||||
.get(MEDAL_KEY_HEADER)
|
||||
.is_some_and(|it| it.as_bytes() == ENV.LABRINTH_MEDAL_KEY.as_bytes())
|
||||
let maybe_medal_key = dotenvy::var("LABRINTH_MEDAL_KEY").ok();
|
||||
|
||||
match maybe_medal_key {
|
||||
None => false,
|
||||
Some(medal_key) => ctx
|
||||
.head()
|
||||
.headers()
|
||||
.get(MEDAL_KEY_HEADER)
|
||||
.is_some_and(|it| it.as_bytes() == medal_key.as_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn external_notification_key_guard(ctx: &GuardContext) -> bool {
|
||||
ctx.head()
|
||||
.headers()
|
||||
.get(EXTERNAL_NOTIFICATION_KEY_HEADER)
|
||||
.is_some_and(|it| {
|
||||
it.as_bytes() == ENV.LABRINTH_EXTERNAL_NOTIFICATION_KEY.as_bytes()
|
||||
})
|
||||
let maybe_external_notification_key =
|
||||
dotenvy::var("LABRINTH_EXTERNAL_NOTIFICATION_KEY").ok();
|
||||
|
||||
match maybe_external_notification_key {
|
||||
None => false,
|
||||
Some(external_notification_key) => ctx
|
||||
.head()
|
||||
.headers()
|
||||
.get(EXTERNAL_NOTIFICATION_KEY_HEADER)
|
||||
.is_some_and(|it| {
|
||||
it.as_bytes() == external_notification_key.as_bytes()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_network_guard(ctx: &GuardContext) -> bool {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::database::models::image_item;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{self, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::images::ImageContext;
|
||||
use crate::routes::ApiError;
|
||||
@@ -60,7 +59,7 @@ pub async fn upload_image_optimized(
|
||||
))
|
||||
})?;
|
||||
|
||||
let cdn_url = &ENV.CDN_URL;
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
|
||||
let hash = sha1::Sha1::digest(&bytes).encode_hex::<String>();
|
||||
let (processed_image, processed_image_ext) = process_image(
|
||||
@@ -176,7 +175,7 @@ pub async fn delete_old_images(
|
||||
publicity: FileHostPublicity,
|
||||
file_host: &dyn FileHost,
|
||||
) -> Result<(), ApiError> {
|
||||
let cdn_url = &ENV.CDN_URL;
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let cdn_url_start = format!("{cdn_url}/");
|
||||
if let Some(image_url) = image_url {
|
||||
let name = image_url.split(&cdn_url_start).nth(1);
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod bitflag;
|
||||
pub mod captcha;
|
||||
pub mod cors;
|
||||
pub mod date;
|
||||
pub mod env;
|
||||
pub mod error;
|
||||
pub mod ext;
|
||||
pub mod gotenberg;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::routes::ApiError;
|
||||
use crate::{database::redis::RedisPool, env::ENV};
|
||||
use crate::util::env::parse_var;
|
||||
use actix_web::{
|
||||
Error, ResponseError,
|
||||
body::{EitherBody, MessageBody},
|
||||
@@ -133,13 +134,14 @@ pub async fn rate_limit_middleware(
|
||||
.clone();
|
||||
|
||||
if let Some(key) = req.headers().get("x-ratelimit-key")
|
||||
&& key.to_str().ok() == Some(&ENV.RATE_LIMIT_IGNORE_KEY)
|
||||
&& key.to_str().ok()
|
||||
== dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref()
|
||||
{
|
||||
return Ok(next.call(req).await?.map_into_left_body());
|
||||
}
|
||||
|
||||
let conn_info = req.connection_info().clone();
|
||||
let ip = if ENV.CLOUDFLARE_INTEGRATION {
|
||||
let ip = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
||||
if let Some(header) = req.headers().get("CF-Connecting-IP") {
|
||||
header.to_str().ok()
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::legacy_loader_fields::MinecraftGameVersion;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::routes::ApiError;
|
||||
use crate::{database::PgPool, env::ENV};
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
@@ -69,7 +69,7 @@ async fn get_webhook_metadata(
|
||||
name: organization.name,
|
||||
url: format!(
|
||||
"{}/organization/{}",
|
||||
ENV.SITE_URL,
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
to_base62(organization.id.0 as u64)
|
||||
),
|
||||
icon_url: organization.icon_url,
|
||||
@@ -95,7 +95,7 @@ async fn get_webhook_metadata(
|
||||
owner = Some(WebhookAuthor {
|
||||
url: format!(
|
||||
"{}/user/{}",
|
||||
ENV.SITE_URL,
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
to_base62(user.id.0 as u64)
|
||||
),
|
||||
name: user.username,
|
||||
@@ -142,7 +142,7 @@ async fn get_webhook_metadata(
|
||||
Ok(Some(WebhookMetadata {
|
||||
project_url: format!(
|
||||
"{}/{}/{}",
|
||||
ENV.SITE_URL,
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
project_type,
|
||||
to_base62(project.inner.id.0 as u64)
|
||||
),
|
||||
@@ -251,7 +251,7 @@ pub async fn send_slack_project_webhook(
|
||||
project_id: ProjectId,
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
webhook_url: &str,
|
||||
webhook_url: String,
|
||||
message: Option<String>,
|
||||
) -> Result<(), ApiError> {
|
||||
let metadata = get_webhook_metadata(project_id, pool, redis).await?;
|
||||
@@ -350,7 +350,7 @@ pub async fn send_slack_project_webhook(
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post(webhook_url)
|
||||
.post(&webhook_url)
|
||||
.json(&serde_json::json!({
|
||||
"blocks": blocks,
|
||||
}))
|
||||
@@ -422,7 +422,7 @@ pub async fn send_discord_webhook(
|
||||
project_id: ProjectId,
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
webhook_url: &str,
|
||||
webhook_url: String,
|
||||
message: Option<String>,
|
||||
) -> Result<(), ApiError> {
|
||||
let metadata = get_webhook_metadata(project_id, pool, redis).await?;
|
||||
@@ -482,7 +482,7 @@ pub async fn send_discord_webhook(
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post(webhook_url)
|
||||
.post(&webhook_url)
|
||||
.json(&DiscordWebhook {
|
||||
avatar_url: Some(
|
||||
"https://cdn.modrinth.com/Modrinth_Dark_Logo.png"
|
||||
|
||||
+15
-12
@@ -21,18 +21,21 @@ async fn get_tags() {
|
||||
.into_iter()
|
||||
.map(|x| x.name)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for name in [
|
||||
"combat",
|
||||
"economy",
|
||||
"food",
|
||||
"optimization",
|
||||
"decoration",
|
||||
"mobs",
|
||||
"magic",
|
||||
] {
|
||||
assert!(category_names.contains(name));
|
||||
}
|
||||
assert_eq!(
|
||||
category_names,
|
||||
[
|
||||
"combat",
|
||||
"economy",
|
||||
"food",
|
||||
"optimization",
|
||||
"decoration",
|
||||
"mobs",
|
||||
"magic"
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ services:
|
||||
delphi:
|
||||
profiles:
|
||||
- with-delphi
|
||||
image: ghcr.io/modrinth/delphi:main
|
||||
image: ghcr.io/modrinth/delphi:feature-schema-rework
|
||||
container_name: labrinth-delphi
|
||||
ports:
|
||||
- '127.0.0.1:59999:59999'
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
|
||||
export type IconComponent = FunctionalComponent<SVGAttributes>
|
||||
|
||||
import _AffiliateIcon from './icons/affiliate.svg?component'
|
||||
import _AlignLeftIcon from './icons/align-left.svg?component'
|
||||
import _ArchiveIcon from './icons/archive.svg?component'
|
||||
@@ -144,7 +142,6 @@ import _ManageIcon from './icons/manage.svg?component'
|
||||
import _MaximizeIcon from './icons/maximize.svg?component'
|
||||
import _MemoryStickIcon from './icons/memory-stick.svg?component'
|
||||
import _MessageIcon from './icons/message.svg?component'
|
||||
import _MessagesSquareIcon from './icons/messages-square.svg?component'
|
||||
import _MicrophoneIcon from './icons/microphone.svg?component'
|
||||
import _MinimizeIcon from './icons/minimize.svg?component'
|
||||
import _MinusIcon from './icons/minus.svg?component'
|
||||
@@ -328,6 +325,8 @@ import _XCircleIcon from './icons/x-circle.svg?component'
|
||||
import _ZoomInIcon from './icons/zoom-in.svg?component'
|
||||
import _ZoomOutIcon from './icons/zoom-out.svg?component'
|
||||
|
||||
export type IconComponent = FunctionalComponent<SVGAttributes>
|
||||
|
||||
export const AffiliateIcon = _AffiliateIcon
|
||||
export const AlignLeftIcon = _AlignLeftIcon
|
||||
export const ArchiveIcon = _ArchiveIcon
|
||||
@@ -467,7 +466,6 @@ export const ManageIcon = _ManageIcon
|
||||
export const MaximizeIcon = _MaximizeIcon
|
||||
export const MemoryStickIcon = _MemoryStickIcon
|
||||
export const MessageIcon = _MessageIcon
|
||||
export const MessagesSquareIcon = _MessagesSquareIcon
|
||||
export const MicrophoneIcon = _MicrophoneIcon
|
||||
export const MinimizeIcon = _MinimizeIcon
|
||||
export const MinusIcon = _MinusIcon
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<!-- @license lucide-static v0.562.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-messages-square"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
<path d="M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 544 B |
@@ -8,13 +8,13 @@ const props = defineProps<{
|
||||
watch(
|
||||
() => props.shown,
|
||||
(shown) => {
|
||||
document?.body.classList.toggle('floating-action-bar-shown', shown)
|
||||
document.body.classList.toggle('floating-action-bar-shown', shown)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
document?.body.classList.remove('floating-action-bar-shown')
|
||||
document.body.classList.remove('floating-action-bar-shown')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -80,17 +80,6 @@
|
||||
@update:model-value="$emit('update:searchQuery', $event)"
|
||||
/>
|
||||
|
||||
<ButtonStyled v-if="showRefreshButton" type="outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 items-center gap-2 !border-[1px] !border-surface-5"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<RefreshCwIcon aria-hidden="true" class="h-5 w-5" />
|
||||
Refresh
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled type="outlined">
|
||||
<OverflowMenu
|
||||
:dropdown-id="`create-new-${baseId}`"
|
||||
@@ -189,7 +178,6 @@ const props = defineProps<{
|
||||
editingFilePath?: string
|
||||
isEditingImage?: boolean
|
||||
searchQuery: string
|
||||
showRefreshButton?: boolean
|
||||
baseId: string
|
||||
}>()
|
||||
|
||||
@@ -202,7 +190,6 @@ defineEmits<{
|
||||
upload: []
|
||||
uploadZip: []
|
||||
unzipFromUrl: [cf: boolean]
|
||||
refresh: []
|
||||
save: []
|
||||
saveAs: []
|
||||
saveRestart: []
|
||||
|
||||
@@ -22,7 +22,7 @@ export function useDebugLogger(namespace: string) {
|
||||
// eslint-disable-next-line
|
||||
return (...args: any[]) => {
|
||||
const location = getCallerLocation()
|
||||
const prefix = location ? `[${namespace}] ${location}` : `[${namespace}]`
|
||||
const prefix = location ? `[${namespace}] [${location}]` : `[${namespace}]`
|
||||
console.debug(prefix, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
:editing-file-path="editingFile?.path"
|
||||
:is-editing-image="fileEditorRef?.isEditingImage"
|
||||
:search-query="searchQuery"
|
||||
:show-refresh-button="showRefreshButton"
|
||||
:base-id="baseId"
|
||||
@navigate="navigateToSegment"
|
||||
@navigate-home="() => navigateToSegment(-1)"
|
||||
@@ -40,7 +39,6 @@
|
||||
@upload="initiateFileUpload"
|
||||
@upload-zip="() => {}"
|
||||
@unzip-from-url="showUnzipFromUrlModal"
|
||||
@refresh="refreshList"
|
||||
@save="() => fileEditorRef?.saveFileContent(true)"
|
||||
@save-as="() => fileEditorRef?.saveFileContent(false)"
|
||||
@save-restart="() => fileEditorRef?.saveAndRestart()"
|
||||
@@ -305,7 +303,6 @@ import {
|
||||
|
||||
defineProps<{
|
||||
showDebugInfo?: boolean
|
||||
showRefreshButton?: boolean
|
||||
}>()
|
||||
|
||||
const notifications = injectNotificationManager()
|
||||
|
||||
Reference in New Issue
Block a user