Batch of tech review backend fixes (#5398)
* Don't enter project into tech review if no new traces
* Send tech review exited message if files are deleted
* change PATCH /issue-detail/{id} to batch update details
* Fix sorting
* store delphi jar in backend
* show jar in tech review card
* improve jar display in frontend
* Fix live/in review label for tech review cards
* sqlx prepare
* polish: decode segments + code qual fix
* fix: skip first seg
* fix: only slice if needed
* Fix tech rev card styling
---------
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
@@ -159,6 +160,15 @@ watch(selectedFile, (newFile) => {
|
|||||||
|
|
||||||
const client = injectModrinthClient()
|
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 severityOrder = { severe: 3, high: 2, medium: 1, low: 0 } as Record<string, number>
|
||||||
|
|
||||||
const detailDecisions = reactive<Map<string, 'safe' | 'malware'>>(new Map())
|
const detailDecisions = reactive<Map<string, 'safe' | 'malware'>>(new Map())
|
||||||
@@ -393,11 +403,7 @@ async function batchMarkRemaining(verdict: 'safe' | 'unsafe') {
|
|||||||
|
|
||||||
isBatchUpdating.value = true
|
isBatchUpdating.value = true
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await updateIssueDetails(detailIds.map((detailId) => ({ detail_id: detailId, verdict })))
|
||||||
detailIds.map((detailId) =>
|
|
||||||
client.labrinth.tech_review_internal.updateIssueDetail(detailId, { verdict }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const decision = verdict === 'safe' ? 'safe' : 'malware'
|
const decision = verdict === 'safe' ? 'safe' : 'malware'
|
||||||
for (const detailId of detailIds) {
|
for (const detailId of detailIds) {
|
||||||
@@ -445,7 +451,7 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
|
|||||||
updatingDetails.add(detailId)
|
updatingDetails.add(detailId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.labrinth.tech_review_internal.updateIssueDetail(detailId, { verdict })
|
await updateIssueDetails([{ detail_id: detailId, verdict }])
|
||||||
|
|
||||||
const decision = verdict === 'safe' ? 'safe' : 'malware'
|
const decision = verdict === 'safe' ? 'safe' : 'malware'
|
||||||
|
|
||||||
@@ -484,7 +490,7 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
|
|||||||
for (const classGroup of groupedByClass.value) {
|
for (const classGroup of groupedByClass.value) {
|
||||||
const hasThisDetail = classGroup.flags.some((f) => f.detail.id === detailId)
|
const hasThisDetail = classGroup.flags.some((f) => f.detail.id === detailId)
|
||||||
if (hasThisDetail && getMarkedFlagsCount(classGroup.flags) === classGroup.flags.length) {
|
if (hasThisDetail && getMarkedFlagsCount(classGroup.flags) === classGroup.flags.length) {
|
||||||
expandedClasses.delete(classGroup.filePath)
|
expandedClasses.delete(classGroup.key)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,6 +539,8 @@ const expandedClasses = reactive<Set<string>>(new Set())
|
|||||||
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
|
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
|
||||||
|
|
||||||
interface ClassGroup {
|
interface ClassGroup {
|
||||||
|
key: string
|
||||||
|
jar: string | null
|
||||||
filePath: string
|
filePath: string
|
||||||
flags: Array<{
|
flags: Array<{
|
||||||
issueId: string
|
issueId: string
|
||||||
@@ -543,6 +551,26 @@ 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[]>(() => {
|
const groupedByClass = computed<ClassGroup[]>(() => {
|
||||||
if (!selectedFile.value) return []
|
if (!selectedFile.value) return []
|
||||||
|
|
||||||
@@ -550,14 +578,20 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
|||||||
|
|
||||||
for (const issue of selectedFile.value.issues) {
|
for (const issue of selectedFile.value.issues) {
|
||||||
for (const detail of issue.details) {
|
for (const detail of issue.details) {
|
||||||
if (!classMap.has(detail.file_path)) {
|
const classKey = `${detail.jar ?? ''}::${detail.file_path}`
|
||||||
classMap.set(detail.file_path, { filePath: detail.file_path, flags: [] })
|
if (!classMap.has(classKey)) {
|
||||||
|
classMap.set(classKey, {
|
||||||
|
key: classKey,
|
||||||
|
jar: detail.jar ?? null,
|
||||||
|
filePath: detail.file_path,
|
||||||
|
flags: [],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// Cast detail to include status (backend will provide this field)
|
// Cast detail to include status (backend will provide this field)
|
||||||
const detailWithStatus = detail as Labrinth.TechReview.Internal.ReportIssueDetail & {
|
const detailWithStatus = detail as Labrinth.TechReview.Internal.ReportIssueDetail & {
|
||||||
status: Labrinth.TechReview.Internal.DelphiReportIssueStatus
|
status: Labrinth.TechReview.Internal.DelphiReportIssueStatus
|
||||||
}
|
}
|
||||||
classMap.get(detail.file_path)!.flags.push({
|
classMap.get(classKey)!.flags.push({
|
||||||
issueId: issue.id,
|
issueId: issue.id,
|
||||||
issueType: issue.issue_type,
|
issueType: issue.issue_type,
|
||||||
detail: detailWithStatus,
|
detail: detailWithStatus,
|
||||||
@@ -585,12 +619,35 @@ 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
|
// Auto-expand if there's only one class in the file
|
||||||
watch(
|
watch(
|
||||||
groupedByClass,
|
groupedByClass,
|
||||||
(classes) => {
|
(classes) => {
|
||||||
if (classes.length === 1) {
|
if (classes.length === 1) {
|
||||||
expandedClasses.add(classes[0].filePath)
|
expandedClasses.add(classes[0].key)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -608,11 +665,11 @@ function getHighestSeverityInClass(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleClass(filePath: string) {
|
function toggleClass(classKey: string) {
|
||||||
if (expandedClasses.has(filePath)) {
|
if (expandedClasses.has(classKey)) {
|
||||||
expandedClasses.delete(filePath)
|
expandedClasses.delete(classKey)
|
||||||
} else {
|
} else {
|
||||||
expandedClasses.add(filePath)
|
expandedClasses.add(classKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,204 +1154,234 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(classItem, idx) in groupedByClass"
|
v-for="jarGroup in groupedByJar"
|
||||||
:key="classItem.filePath"
|
:key="jarGroup.key"
|
||||||
class="border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2"
|
class="border-x border-b-0 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
|
<div
|
||||||
class="flex cursor-pointer items-center justify-between p-4 transition-colors duration-200 hover:bg-surface-4"
|
v-if="jarGroup.segments.length > 0"
|
||||||
@click="toggleClass(classItem.filePath)"
|
class="border-b border-solid border-surface-1 px-4 py-3"
|
||||||
>
|
>
|
||||||
<div class="my-auto flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
<ButtonStyled type="transparent" circular>
|
<template
|
||||||
<button
|
v-for="(segment, index) in jarGroup.segments"
|
||||||
class="transition-transform"
|
:key="`${jarGroup.key}-${index}`"
|
||||||
:class="{ 'rotate-180': expandedClasses.has(classItem.filePath) }"
|
>
|
||||||
|
<span
|
||||||
|
class="font-mono text-sm"
|
||||||
|
:class="
|
||||||
|
index === jarGroup.segments.length - 1
|
||||||
|
? 'font-semibold text-contrast'
|
||||||
|
: 'text-secondary'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon class="h-5 w-5 text-contrast" />
|
{{ segment }}
|
||||||
</button>
|
</span>
|
||||||
</ButtonStyled>
|
<ChevronRightIcon
|
||||||
|
v-if="index < jarGroup.segments.length - 1"
|
||||||
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
|
class="size-4 text-secondary"
|
||||||
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
|
</template>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<Collapsible :collapsed="!expandedClasses.has(classItem.filePath)">
|
<div
|
||||||
<div class="mt-2 flex flex-col gap-2 px-4 pb-4">
|
v-for="classItem in jarGroup.classes"
|
||||||
<div
|
:key="classItem.key"
|
||||||
v-for="flag in classItem.flags"
|
class="border-b border-solid border-surface-1 last:border-b-0"
|
||||||
: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="flex cursor-pointer items-center justify-between p-4 transition-colors duration-200 hover:bg-surface-4"
|
||||||
<div class="grid grid-cols-[1fr_auto] items-center">
|
@click="toggleClass(classItem.key)"
|
||||||
<div
|
>
|
||||||
class="flex items-center gap-2"
|
<div class="my-auto flex items-center gap-2">
|
||||||
:class="{
|
<ButtonStyled type="transparent" circular>
|
||||||
'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
|
<button
|
||||||
v-tooltip="`Copy code`"
|
class="transition-transform"
|
||||||
class="absolute right-2 top-2 border-[1px]"
|
:class="{ 'rotate-180': expandedClasses.has(classItem.key) }"
|
||||||
@click="
|
|
||||||
copyToClipboard(getClassDecompiledSource(classItem)!, classItem.filePath)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<CopyIcon v-if="!showCopyFeedback.get(classItem.filePath)" />
|
<ChevronDownIcon class="h-5 w-5 text-contrast" />
|
||||||
<CheckIcon v-else />
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<div class="overflow-x-auto bg-surface-3 py-3">
|
<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
|
<div
|
||||||
v-for="(line, n) in highlightCodeLines(
|
v-if="classItem.flags.some((f) => loadingIssues.has(f.issueId))"
|
||||||
getClassDecompiledSource(classItem)!,
|
class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1"
|
||||||
'java',
|
>
|
||||||
)"
|
<span class="flex items-center gap-1.5 text-sm font-medium text-secondary">
|
||||||
:key="n"
|
<LoaderCircleIcon class="size-4 animate-spin" />
|
||||||
class="flex font-mono text-[13px] leading-[1.6]"
|
Loading source...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</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
|
<div
|
||||||
class="select-none border-0 border-r border-solid border-surface-5 px-4 py-0 text-right text-primary"
|
v-for="[key, value] in Object.entries(flag.detail.data).sort(([a], [b]) =>
|
||||||
style="min-width: 3.5rem"
|
a.localeCompare(b),
|
||||||
|
)"
|
||||||
|
:key="key"
|
||||||
|
class="flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{{ n + 1 }}
|
<span class="text-secondary">{{ key }}:</span>
|
||||||
</div>
|
<a
|
||||||
<div class="flex-1 px-4 py-0 text-primary">
|
v-if="typeof value === 'string' && value.startsWith('http')"
|
||||||
<pre v-html="line || ' '"></pre>
|
: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>
|
||||||
</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>
|
</div>
|
||||||
<div v-else class="rounded-lg border border-solid border-surface-5 bg-surface-3 p-4">
|
</Collapsible>
|
||||||
<p class="text-sm text-secondary">
|
</div>
|
||||||
Source code not available or failed to decompile for this file.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"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.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.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 ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -20,16 +20,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "file_path!: String",
|
"name": "jar?: String",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
|
"name": "file_path!: String",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
"name": "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
"name": "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
||||||
"type_info": "Jsonb"
|
"type_info": "Jsonb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 6,
|
||||||
"name": "severity!: DelphiSeverity",
|
"name": "severity!: DelphiSeverity",
|
||||||
"type_info": {
|
"type_info": {
|
||||||
"Custom": {
|
"Custom": {
|
||||||
@@ -46,7 +51,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 6,
|
"ordinal": 7,
|
||||||
"name": "status!: DelphiStatus",
|
"name": "status!: DelphiStatus",
|
||||||
"type_info": {
|
"type_info": {
|
||||||
"Custom": {
|
"Custom": {
|
||||||
@@ -71,11 +76,12 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
null
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "80b52a09ca9a056251d1040936f768c266e5814c15638d455f569deed13ee7d0"
|
"hash": "263ad3654f544ffb6061c839d49dada47fb382a76fdcabad2077fb1ef6d1010a"
|
||||||
}
|
}
|
||||||
34
apps/labrinth/.sqlx/query-30a5fa3f44e56c412d07625ea9110238c533a1994e95c805a3babc39cde23004.json
generated
Normal file
34
apps/labrinth/.sqlx/query-30a5fa3f44e56c412d07625ea9110238c533a1994e95c805a3babc39cde23004.json
generated
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
22
apps/labrinth/.sqlx/query-555342b0ec9fb808f05a18aaeaf06fb61e968fb3379c9d0c7ad82c8747bd4256.json
generated
Normal file
22
apps/labrinth/.sqlx/query-555342b0ec9fb808f05a18aaeaf06fb61e968fb3379c9d0c7ad82c8747bd4256.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
22
apps/labrinth/.sqlx/query-8c80f3158fb5772adc8542cdf5419437bb8cd65723a32e587022d0c8decba68d.json
generated
Normal file
22
apps/labrinth/.sqlx/query-8c80f3158fb5772adc8542cdf5419437bb8cd65723a32e587022d0c8decba68d.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"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 ",
|
"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 ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
|
"Text",
|
||||||
"Jsonb",
|
"Jsonb",
|
||||||
{
|
{
|
||||||
"Custom": {
|
"Custom": {
|
||||||
@@ -35,5 +36,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4"
|
"hash": "9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2"
|
||||||
}
|
}
|
||||||
23
apps/labrinth/.sqlx/query-997944b328b628792d84b21747f9e9c670ad40d0f89a175aedece93df1169195.json
generated
Normal file
23
apps/labrinth/.sqlx/query-997944b328b628792d84b21747f9e9c670ad40d0f89a175aedece93df1169195.json
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
29
apps/labrinth/.sqlx/query-ccedb120b05ff47ddc15bb4570025a8e8249050c12f7036d936f9a01f939db1f.json
generated
Normal file
29
apps/labrinth/.sqlx/query-ccedb120b05ff47ddc15bb4570025a8e8249050c12f7036d936f9a01f939db1f.json
generated
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE delphi_report_issue_details
|
||||||
|
ADD COLUMN jar TEXT;
|
||||||
@@ -221,6 +221,9 @@ pub struct ReportIssueDetail {
|
|||||||
/// This acts as a stable identifier for an issue detail, even across
|
/// This acts as a stable identifier for an issue detail, even across
|
||||||
/// different versions of the same file.
|
/// different versions of the same file.
|
||||||
pub key: String,
|
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.
|
/// Name of the Java class path in which this issue was found.
|
||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
/// Decompiled, pretty-printed source of the Java class.
|
/// Decompiled, pretty-printed source of the Java class.
|
||||||
@@ -241,12 +244,13 @@ impl ReportIssueDetail {
|
|||||||
) -> Result<DelphiReportIssueDetailsId, DatabaseError> {
|
) -> Result<DelphiReportIssueDetailsId, DatabaseError> {
|
||||||
Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!(
|
Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!(
|
||||||
"
|
"
|
||||||
INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity)
|
INSERT INTO delphi_report_issue_details (issue_id, key, jar, file_path, decompiled_source, data, severity)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
",
|
",
|
||||||
self.issue_id as DelphiReportIssueId,
|
self.issue_id as DelphiReportIssueId,
|
||||||
self.key,
|
self.key,
|
||||||
|
self.jar,
|
||||||
self.file_path,
|
self.file_path,
|
||||||
self.decompiled_source,
|
self.decompiled_source,
|
||||||
sqlx::types::Json(&self.data) as Json<&HashMap<String, serde_json::Value>>,
|
sqlx::types::Json(&self.data) as Json<&HashMap<String, serde_json::Value>>,
|
||||||
|
|||||||
@@ -59,15 +59,42 @@ static DELPHI_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
/// 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)]
|
||||||
struct DelphiReportIssueDetails {
|
struct DelphiReportIssueDetails {
|
||||||
pub file: String,
|
pub file: String,
|
||||||
pub key: String,
|
pub key: IssueDetailKey,
|
||||||
|
pub jar: Option<String>,
|
||||||
pub data: HashMap<String, serde_json::Value>,
|
pub data: HashMap<String, serde_json::Value>,
|
||||||
pub severity: DelphiSeverity,
|
pub severity: DelphiSeverity,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct DelphiReport {
|
struct DelphiReport {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub project_id: crate::models::ids::ProjectId,
|
pub project_id: crate::models::ids::ProjectId,
|
||||||
@@ -205,11 +232,34 @@ async fn ingest_report_deserialized(
|
|||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to check if pending issue details exist")?;
|
.wrap_internal_err("failed to check if pending issue details exist")?;
|
||||||
|
|
||||||
if record.pending_issue_details_exist {
|
let issue_detail_keys = report
|
||||||
info!(
|
.issues
|
||||||
"File's project already has pending issue details, is not entering tech review queue"
|
.values()
|
||||||
);
|
.flatten()
|
||||||
} else {
|
.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 {
|
||||||
info!("File's project is entering tech review queue");
|
info!("File's project is entering tech review queue");
|
||||||
|
|
||||||
ThreadMessageBuilder {
|
ThreadMessageBuilder {
|
||||||
@@ -221,6 +271,10 @@ async fn ingest_report_deserialized(
|
|||||||
.insert(&mut transaction)
|
.insert(&mut transaction)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to add entering tech review message")?;
|
.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
|
// TODO: Currently, the way we determine if an issue is in tech review or not
|
||||||
@@ -232,7 +286,7 @@ async fn ingest_report_deserialized(
|
|||||||
// This is undesirable, but we can't rework the database schema to fix it
|
// 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
|
// right now. As a hack, we add a dummy report issue which blocks the
|
||||||
// project from exiting the tech review queue.
|
// project from exiting the tech review queue.
|
||||||
{
|
if should_enter_tech_review {
|
||||||
let dummy_issue_id = DBDelphiReportIssue {
|
let dummy_issue_id = DBDelphiReportIssue {
|
||||||
id: DelphiReportIssueId(0), // This will be set by the database
|
id: DelphiReportIssueId(0), // This will be set by the database
|
||||||
report_id,
|
report_id,
|
||||||
@@ -245,6 +299,7 @@ async fn ingest_report_deserialized(
|
|||||||
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
||||||
issue_id: dummy_issue_id,
|
issue_id: dummy_issue_id,
|
||||||
key: "".into(),
|
key: "".into(),
|
||||||
|
jar: None,
|
||||||
file_path: "".into(),
|
file_path: "".into(),
|
||||||
decompiled_source: None,
|
decompiled_source: None,
|
||||||
data: HashMap::new(),
|
data: HashMap::new(),
|
||||||
@@ -275,7 +330,8 @@ async fn ingest_report_deserialized(
|
|||||||
ReportIssueDetail {
|
ReportIssueDetail {
|
||||||
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
||||||
issue_id,
|
issue_id,
|
||||||
key: issue_detail.key,
|
key: issue_detail.key.0,
|
||||||
|
jar: issue_detail.jar,
|
||||||
file_path: issue_detail.file,
|
file_path: issue_detail.file,
|
||||||
decompiled_source: decompiled_source.cloned().flatten(),
|
decompiled_source: decompiled_source.cloned().flatten(),
|
||||||
data: issue_detail.data,
|
data: issue_detail.data,
|
||||||
@@ -333,6 +389,83 @@ pub async fn run(
|
|||||||
Ok(HttpResponse::NoContent().finish())
|
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")]
|
#[post("run")]
|
||||||
async fn _run(
|
async fn _run(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
|
|||||||
.service(get_report)
|
.service(get_report)
|
||||||
.service(get_issue)
|
.service(get_issue)
|
||||||
.service(submit_report)
|
.service(submit_report)
|
||||||
.service(update_issue_detail)
|
.service(update_issue_details)
|
||||||
.service(add_report);
|
.service(add_report);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +388,8 @@ pub struct ProjectModerationInfo {
|
|||||||
pub thread_id: ThreadId,
|
pub thread_id: ThreadId,
|
||||||
/// Project name.
|
/// Project name.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Current project status.
|
||||||
|
pub status: ProjectStatus,
|
||||||
/// The aggregated project typos of the versions of this project
|
/// The aggregated project typos of the versions of this project
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub project_types: Vec<String>,
|
pub project_types: Vec<String>,
|
||||||
@@ -498,6 +500,7 @@ async fn fetch_project_reports(
|
|||||||
drid.id AS "id!: DelphiReportIssueDetailsId",
|
drid.id AS "id!: DelphiReportIssueDetailsId",
|
||||||
drid.issue_id AS "issue_id!: DelphiReportIssueId",
|
drid.issue_id AS "issue_id!: DelphiReportIssueId",
|
||||||
drid.key AS "key!: String",
|
drid.key AS "key!: String",
|
||||||
|
drid.jar AS "jar?: String",
|
||||||
drid.file_path AS "file_path!: String",
|
drid.file_path AS "file_path!: String",
|
||||||
drid.data AS "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
drid.data AS "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
||||||
drid.severity AS "severity!: DelphiSeverity",
|
drid.severity AS "severity!: DelphiSeverity",
|
||||||
@@ -561,6 +564,7 @@ async fn fetch_project_reports(
|
|||||||
id: d.id,
|
id: d.id,
|
||||||
issue_id: d.issue_id,
|
issue_id: d.issue_id,
|
||||||
key: d.key,
|
key: d.key,
|
||||||
|
jar: d.jar,
|
||||||
file_path: d.file_path,
|
file_path: d.file_path,
|
||||||
decompiled_source: None,
|
decompiled_source: None,
|
||||||
data: d.data.0,
|
data: d.data.0,
|
||||||
@@ -698,9 +702,9 @@ async fn search_projects(
|
|||||||
|
|
||||||
let rows = sqlx::query!(
|
let rows = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT DISTINCT ON (m.id)
|
SELECT
|
||||||
m.id AS "project_id: DBProjectId",
|
m.id AS "project_id: DBProjectId",
|
||||||
t.id AS "thread_id: DBThreadId"
|
MIN(t.id) AS "thread_id!: DBThreadId"
|
||||||
FROM mods m
|
FROM mods m
|
||||||
INNER JOIN threads t ON t.mod_id = m.id
|
INNER JOIN threads t ON t.mod_id = m.id
|
||||||
INNER JOIN versions v ON v.mod_id = m.id
|
INNER JOIN versions v ON v.mod_id = m.id
|
||||||
@@ -734,12 +738,14 @@ 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 = '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'))
|
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, t.id
|
GROUP BY m.id
|
||||||
ORDER BY m.id,
|
ORDER BY
|
||||||
CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,
|
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 = '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_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
|
CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,
|
||||||
|
-- tie-breaker: oldest reports
|
||||||
|
MIN(dr.created) ASC
|
||||||
LIMIT $1 OFFSET $2
|
LIMIT $1 OFFSET $2
|
||||||
"#,
|
"#,
|
||||||
limit,
|
limit,
|
||||||
@@ -831,6 +837,7 @@ async fn search_projects(
|
|||||||
id,
|
id,
|
||||||
thread_id: project.thread_id,
|
thread_id: project.thread_id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
|
status: project.status,
|
||||||
project_types: project.project_types,
|
project_types: project.project_types,
|
||||||
icon_url: project.icon_url,
|
icon_url: project.icon_url,
|
||||||
},
|
},
|
||||||
@@ -1119,6 +1126,8 @@ async fn submit_report(
|
|||||||
/// See [`update_issue`].
|
/// See [`update_issue`].
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct UpdateIssue {
|
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.
|
/// What the moderator has decided the outcome of this issue is.
|
||||||
pub verdict: DelphiVerdict,
|
pub verdict: DelphiVerdict,
|
||||||
}
|
}
|
||||||
@@ -1131,14 +1140,13 @@ pub struct UpdateIssue {
|
|||||||
security(("bearer_auth" = [])),
|
security(("bearer_auth" = [])),
|
||||||
responses((status = NO_CONTENT))
|
responses((status = NO_CONTENT))
|
||||||
)]
|
)]
|
||||||
#[patch("/issue-detail/{id}")]
|
#[patch("/issue-detail")]
|
||||||
async fn update_issue_detail(
|
async fn update_issue_details(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
update_req: web::Json<UpdateIssue>,
|
update_reqs: web::Json<Vec<UpdateIssue>>,
|
||||||
path: web::Path<(DelphiReportIssueDetailsId,)>,
|
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
check_is_moderator_from_headers(
|
check_is_moderator_from_headers(
|
||||||
&req,
|
&req,
|
||||||
@@ -1148,44 +1156,76 @@ async fn update_issue_detail(
|
|||||||
Scopes::PROJECT_WRITE,
|
Scopes::PROJECT_WRITE,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let (issue_detail_id,) = path.into_inner();
|
|
||||||
|
|
||||||
let mut txn = pool
|
let mut txn = pool
|
||||||
.begin()
|
.begin()
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to start transaction")?;
|
.wrap_internal_err("failed to start transaction")?;
|
||||||
|
|
||||||
let status = match update_req.verdict {
|
let updates = update_reqs.into_inner();
|
||||||
DelphiVerdict::Safe => DelphiStatus::Safe,
|
let detail_ids = updates.iter().map(|u| u.detail_id.0).collect::<Vec<_>>();
|
||||||
DelphiVerdict::Unsafe => DelphiStatus::Unsafe,
|
let verdicts = updates
|
||||||
};
|
.iter()
|
||||||
let results = sqlx::query!(
|
.map(|u| match u.verdict {
|
||||||
|
DelphiVerdict::Safe => "safe".to_string(),
|
||||||
|
DelphiVerdict::Unsafe => "unsafe".to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let record = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO delphi_issue_detail_verdicts (
|
WITH incoming AS (
|
||||||
project_id,
|
SELECT *
|
||||||
detail_key,
|
FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY
|
||||||
verdict
|
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
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
didws.project_id,
|
(v.incoming_count = v.resolved_count) AS "all_found!",
|
||||||
didws.key,
|
(SELECT COUNT(*) FROM upserted) AS "upserted_count!"
|
||||||
$1
|
FROM validated v
|
||||||
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
|
|
||||||
"#,
|
"#,
|
||||||
status as _,
|
&detail_ids,
|
||||||
issue_detail_id as _,
|
&verdicts,
|
||||||
)
|
)
|
||||||
.execute(&mut txn)
|
.fetch_one(&mut txn)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to update issue detail")?;
|
.wrap_internal_err("failed to update issue details")?;
|
||||||
if results.rows_affected() == 0 {
|
|
||||||
|
if !record.all_found {
|
||||||
return Err(ApiError::Request(eyre!("issue detail does not exist")));
|
return Err(ApiError::Request(eyre!("issue detail does not exist")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use crate::models::threads::MessageBody;
|
|||||||
use crate::queue::moderation::AutomatedModerationQueue;
|
use crate::queue::moderation::AutomatedModerationQueue;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
|
use crate::routes::internal::delphi;
|
||||||
use crate::search::indexing::remove_documents;
|
use crate::search::indexing::remove_documents;
|
||||||
use crate::search::{SearchConfig, SearchError, search_for_project};
|
use crate::search::{SearchConfig, SearchError, search_for_project};
|
||||||
use crate::util::error::Context;
|
use crate::util::error::Context;
|
||||||
@@ -2218,6 +2219,18 @@ pub async fn project_delete(
|
|||||||
.begin()
|
.begin()
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to start transaction")?;
|
.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 {
|
let context = ImageContext::Project {
|
||||||
project_id: Some(project.inner.id.into()),
|
project_id: Some(project.inner.id.into()),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::models::pats::Scopes;
|
|||||||
use crate::models::projects::VersionType;
|
use crate::models::projects::VersionType;
|
||||||
use crate::models::teams::ProjectPermissions;
|
use crate::models::teams::ProjectPermissions;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
|
use crate::routes::internal::delphi;
|
||||||
use crate::{database, models};
|
use crate::{database, models};
|
||||||
use actix_web::{HttpRequest, HttpResponse, web};
|
use actix_web::{HttpRequest, HttpResponse, web};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
@@ -688,6 +689,9 @@ pub async fn delete_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut transaction = pool.begin().await?;
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -709,6 +713,13 @@ pub async fn delete_file(
|
|||||||
.execute(&mut transaction)
|
.execute(&mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
delphi::send_tech_review_exit_file_deleted_message_if_exited(
|
||||||
|
row.project_id,
|
||||||
|
was_in_tech_review,
|
||||||
|
&mut transaction,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use crate::models::projects::{
|
|||||||
use crate::models::projects::{Loader, skip_nulls};
|
use crate::models::projects::{Loader, skip_nulls};
|
||||||
use crate::models::teams::ProjectPermissions;
|
use crate::models::teams::ProjectPermissions;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
|
use crate::routes::internal::delphi;
|
||||||
use crate::search::SearchConfig;
|
use crate::search::SearchConfig;
|
||||||
use crate::search::indexing::remove_documents;
|
use crate::search::indexing::remove_documents;
|
||||||
use crate::util::error::Context;
|
use crate::util::error::Context;
|
||||||
@@ -959,6 +960,12 @@ pub async fn version_delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut transaction = pool.begin().await?;
|
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 {
|
let context = ImageContext::Version {
|
||||||
version_id: Some(version.inner.id.into()),
|
version_id: Some(version.inner.id.into()),
|
||||||
};
|
};
|
||||||
@@ -977,6 +984,14 @@ pub async fn version_delete(
|
|||||||
&mut transaction,
|
&mut transaction,
|
||||||
)
|
)
|
||||||
.await?;
|
.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?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
database::models::DBProject::clear_cache(
|
database::models::DBProject::clear_cache(
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ services:
|
|||||||
delphi:
|
delphi:
|
||||||
profiles:
|
profiles:
|
||||||
- with-delphi
|
- with-delphi
|
||||||
image: ghcr.io/modrinth/delphi:feature-schema-rework
|
image: ghcr.io/modrinth/delphi:main
|
||||||
container_name: labrinth-delphi
|
container_name: labrinth-delphi
|
||||||
ports:
|
ports:
|
||||||
- '127.0.0.1:59999:59999'
|
- '127.0.0.1:59999:59999'
|
||||||
|
|||||||
Reference in New Issue
Block a user