Release 202506111007
This commit is contained in:
@@ -107,6 +107,7 @@ export default defineConfig({
|
||||
'/service/[...slug]/proof': '/service/[...slug]/#verification',
|
||||
'/attribute/[...slug]': '/attributes',
|
||||
'/attr/[...slug]': '/attributes',
|
||||
'/service/[...slug]/review': '/service/[...slug]#comments',
|
||||
// #endregion
|
||||
},
|
||||
env: {
|
||||
|
||||
@@ -1143,7 +1143,7 @@ async function main() {
|
||||
}
|
||||
|
||||
let users = await Promise.all(
|
||||
Array.from({ length: 10 }, async () => {
|
||||
Array.from({ length: 570 }, async () => {
|
||||
const { user } = await createAccount()
|
||||
return user
|
||||
})
|
||||
|
||||
@@ -150,7 +150,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
||||
checked={comment.suspicious}
|
||||
/>
|
||||
|
||||
<div class="comment-header scrollbar-w-none flex items-center gap-2 overflow-auto text-sm">
|
||||
<div class="comment-header scrollbar-w-none -mt-10 flex items-center gap-2 overflow-auto pt-10 text-sm">
|
||||
<label for={`collapse-${comment.id.toString()}`} class="cursor-pointer text-zinc-500 hover:text-zinc-300">
|
||||
<span class="collapse-symbol text-xs"></span>
|
||||
<span class="sr-only">Toggle comment visibility</span>
|
||||
@@ -240,15 +240,17 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
||||
}
|
||||
|
||||
{
|
||||
comment.author.serviceAffiliations.map((affiliation) => {
|
||||
const roleInfo = getServiceUserRoleInfo(affiliation.role)
|
||||
return (
|
||||
<BadgeSmall icon={roleInfo.icon} color={roleInfo.color} variant="faded" inlineIcon>
|
||||
{roleInfo.label} at
|
||||
<a href={`/service/${affiliation.service.slug}`}>{affiliation.service.name}</a>
|
||||
</BadgeSmall>
|
||||
)
|
||||
})
|
||||
comment.author.serviceAffiliations
|
||||
.filter((affiliation) => affiliation.service.slug === serviceSlug)
|
||||
.map((affiliation) => {
|
||||
const roleInfo = getServiceUserRoleInfo(affiliation.role)
|
||||
return (
|
||||
<BadgeSmall icon={roleInfo.icon} color={roleInfo.color} variant="faded" inlineIcon>
|
||||
{roleInfo.label} at
|
||||
<a href={`/service/${affiliation.service.slug}`}>{affiliation.service.name}</a>
|
||||
</BadgeSmall>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,11 @@ type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId' |
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}[]
|
||||
selectProps?: Omit<HTMLAttributes<'select'>, 'name'>
|
||||
selectProps?: Omit<HTMLAttributes<'select'>, 'name' | 'value'>
|
||||
selectedValue?: string[] | string
|
||||
}
|
||||
|
||||
const { options, selectProps, ...wrapperProps } = Astro.props
|
||||
const { options, selectProps, selectedValue, ...wrapperProps } = Astro.props
|
||||
|
||||
const inputId = selectProps?.id ?? Astro.locals.makeId(`input-${wrapperProps.name}`)
|
||||
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
||||
@@ -39,7 +40,15 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
||||
>
|
||||
{
|
||||
options.map((option) => (
|
||||
<option value={option.value} disabled={option.disabled}>
|
||||
<option
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
selected={
|
||||
Array.isArray(selectedValue)
|
||||
? selectedValue.includes(option.value)
|
||||
: selectedValue === option.value
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))
|
||||
|
||||
@@ -108,3 +108,19 @@ export const {
|
||||
},
|
||||
] as const satisfies AttributeTypeInfo<AttributeType>[]
|
||||
)
|
||||
|
||||
export const baseScoreType = {
|
||||
value: 'BASE_SCORE',
|
||||
slug: 'base-score',
|
||||
label: 'Base score',
|
||||
icon: 'ri:information-line',
|
||||
order: 5,
|
||||
classNames: {
|
||||
container: 'bg-night-500',
|
||||
subcontainer: '',
|
||||
text: 'text-day-200',
|
||||
textLight: '',
|
||||
icon: '',
|
||||
button: '',
|
||||
},
|
||||
} as const satisfies AttributeTypeInfo
|
||||
|
||||
@@ -106,7 +106,7 @@ export const {
|
||||
type: 'matrix',
|
||||
label: 'Matrix',
|
||||
matcher: /^https?:\/\/(?:www\.)?matrix\.to\/#\/(.+)/,
|
||||
formatter: ([, value]) => (value ? `#${value}` : 'Matrix'),
|
||||
formatter: ([, value]) => value ?? 'Matrix',
|
||||
icon: 'ri:hashtag',
|
||||
urlType: 'url',
|
||||
},
|
||||
@@ -121,7 +121,7 @@ export const {
|
||||
{
|
||||
type: 'simplex',
|
||||
label: 'SimpleX Chat',
|
||||
matcher: /^https?:\/\/(?:www\.)?(simplex\.chat)\//,
|
||||
matcher: /^https?:\/\/(?:www\.)?((?:simplex\.chat|smp\d+\.simplex\.im))\//,
|
||||
formatter: () => 'SimpleX Chat',
|
||||
icon: 'simplex',
|
||||
urlType: 'url',
|
||||
|
||||
@@ -2,12 +2,16 @@ import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
||||
import { parseIntWithFallback } from '../lib/numbers'
|
||||
import { transformCase } from '../lib/strings'
|
||||
|
||||
import type { AttributeType } from '@prisma/client'
|
||||
|
||||
type KycLevelInfo<T extends string | null | undefined = string> = {
|
||||
id: T
|
||||
value: number
|
||||
icon: string
|
||||
name: string
|
||||
description: string
|
||||
privacyPoints: number
|
||||
attributeType: AttributeType
|
||||
}
|
||||
|
||||
export const {
|
||||
@@ -22,6 +26,8 @@ export const {
|
||||
icon: 'diamond-question',
|
||||
name: `KYC ${id ? transformCase(id, 'title') : String(id)}`,
|
||||
description: '',
|
||||
privacyPoints: 0,
|
||||
attributeType: 'INFO',
|
||||
}),
|
||||
[
|
||||
{
|
||||
@@ -30,6 +36,8 @@ export const {
|
||||
icon: 'anonymous-mask',
|
||||
name: 'Guaranteed no KYC',
|
||||
description: 'Terms explicitly state KYC will never be requested.',
|
||||
privacyPoints: 25,
|
||||
attributeType: 'GOOD',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
@@ -37,6 +45,8 @@ export const {
|
||||
icon: 'diamond-question',
|
||||
name: 'No KYC mention',
|
||||
description: 'No mention of current or future KYC requirements.',
|
||||
privacyPoints: 15,
|
||||
attributeType: 'GOOD',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
@@ -45,6 +55,8 @@ export const {
|
||||
name: 'KYC on authorities request',
|
||||
description:
|
||||
'No routine KYC, but may cooperate with authorities, block funds or implement future KYC requirements.',
|
||||
privacyPoints: -5,
|
||||
attributeType: 'WARNING',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
@@ -52,6 +64,8 @@ export const {
|
||||
icon: 'gun',
|
||||
name: 'Shotgun KYC',
|
||||
description: 'May request KYC and block funds based on automated triggers.',
|
||||
privacyPoints: -15,
|
||||
attributeType: 'WARNING',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
@@ -59,6 +73,8 @@ export const {
|
||||
icon: 'fingerprint-detailed',
|
||||
name: 'Mandatory KYC',
|
||||
description: 'Required for key features and can be required arbitrarily at any time.',
|
||||
privacyPoints: -25,
|
||||
attributeType: 'BAD',
|
||||
},
|
||||
] as const satisfies KycLevelInfo<'0' | '1' | '2' | '3' | '4'>[]
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ import { orderBy } from 'lodash-es'
|
||||
|
||||
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
|
||||
import { getAttributeTypeInfo } from '../constants/attributeTypes'
|
||||
import { getKycLevelClarificationInfo } from '../constants/kycLevelClarifications'
|
||||
import { kycLevels } from '../constants/kycLevels'
|
||||
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
||||
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
||||
|
||||
@@ -27,7 +29,7 @@ type NonDbAttribute = Prisma.AttributeGetPayload<{
|
||||
}[]
|
||||
}
|
||||
|
||||
export const nonDbAttributes: (NonDbAttribute & {
|
||||
type NonDbAttributeFull = NonDbAttribute & {
|
||||
customize: (
|
||||
service: Prisma.ServiceGetPayload<{
|
||||
select: {
|
||||
@@ -41,12 +43,16 @@ export const nonDbAttributes: (NonDbAttribute & {
|
||||
onionUrls: true
|
||||
i2pUrls: true
|
||||
acceptedCurrencies: true
|
||||
kycLevel: true
|
||||
kycLevelClarification: true
|
||||
}
|
||||
}>
|
||||
) => Partial<Pick<NonDbAttribute, 'description' | 'title'>> & {
|
||||
show: boolean
|
||||
}
|
||||
})[] = [
|
||||
}
|
||||
|
||||
export const nonDbAttributes: NonDbAttributeFull[] = [
|
||||
{
|
||||
slug: 'verification-verified',
|
||||
title: 'Verified',
|
||||
@@ -135,6 +141,31 @@ export const nonDbAttributes: (NonDbAttribute & {
|
||||
description: `${verificationStatusesByValue.VERIFICATION_FAILED.description} ${READ_MORE_SENTENCE_LINK}\n\nCheck out the [proof](#verification).`,
|
||||
}),
|
||||
},
|
||||
...kycLevels.map<NonDbAttributeFull>((kycLevel) => ({
|
||||
slug: `kyc-level-${kycLevel.id}`,
|
||||
title: kycLevel.name,
|
||||
type: kycLevel.attributeType,
|
||||
category: 'PRIVACY',
|
||||
description: kycLevel.description,
|
||||
privacyPoints: kycLevel.privacyPoints,
|
||||
trustPoints: 0,
|
||||
links: [
|
||||
{
|
||||
url: `/?max-kyc=${kycLevel.id}`,
|
||||
label: 'With this or better',
|
||||
icon: 'ri:search-line',
|
||||
},
|
||||
],
|
||||
customize: (service) => {
|
||||
const clarification = getKycLevelClarificationInfo(service.kycLevelClarification)
|
||||
return {
|
||||
show: service.kycLevel === kycLevel.value,
|
||||
title: kycLevel.name + (clarification.value !== 'NONE' ? ` (${clarification.label})` : ''),
|
||||
description:
|
||||
kycLevel.description + (clarification.value !== 'NONE' ? ` ${clarification.description}` : ''),
|
||||
}
|
||||
},
|
||||
})),
|
||||
{
|
||||
slug: 'archived',
|
||||
title: serviceVisibilitiesById.ARCHIVED.label,
|
||||
@@ -261,20 +292,7 @@ export function sortAttributes<
|
||||
}
|
||||
|
||||
export function makeNonDbAttributes(
|
||||
service: Prisma.ServiceGetPayload<{
|
||||
select: {
|
||||
verificationStatus: true
|
||||
serviceVisibility: true
|
||||
isRecentlyListed: true
|
||||
listedAt: true
|
||||
createdAt: true
|
||||
tosReviewAt: true
|
||||
tosReview: true
|
||||
onionUrls: true
|
||||
i2pUrls: true
|
||||
acceptedCurrencies: true
|
||||
}
|
||||
}>,
|
||||
service: Parameters<NonDbAttributeFull['customize']>[0],
|
||||
{ filter = false }: { filter?: boolean } = {}
|
||||
) {
|
||||
const attributes = nonDbAttributes.map(({ customize, ...attribute }) => ({
|
||||
|
||||
@@ -153,15 +153,6 @@ Scores are calculated **automatically** using clear, fixed rules. We do not chan
|
||||
The privacy score measures how well a service protects user privacy, using a transparent, rules-based approach:
|
||||
|
||||
1. **Base Score:** Every service starts with a neutral score of 50 points.
|
||||
1. **KYC Level:** Adjusts the score based on the level of identity verification required:
|
||||
- KYC Level 0 (No KYC): **+25 points**
|
||||
- KYC Level 1 (Minimal KYC): **+10 points**
|
||||
- KYC Level 2 (Moderate KYC): **-5 points**
|
||||
- KYC Level 3 (More KYC): **-15 points**
|
||||
- KYC Level 4 (Full mandatory KYC): **-25 points**
|
||||
1. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL.
|
||||
1. **I2P URL:** **+5 points** if the service offers at least one I2P URL.
|
||||
1. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method.
|
||||
1. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes).
|
||||
1. **Final Score Range:** The final score is always kept between 0 and 100.
|
||||
|
||||
@@ -170,13 +161,6 @@ The privacy score measures how well a service protects user privacy, using a tra
|
||||
The trust score represents how reliable and trustworthy a service is, based on objective, transparent criteria.
|
||||
|
||||
1. **Base Score:** Every service begins with a neutral score of 50 points.
|
||||
1. **Verification Status:**
|
||||
- **Verification Success:** +10 points
|
||||
- **Approved:** +5 points
|
||||
- **Community Contributed:** 0 points
|
||||
- **Verification Failed (SCAM):** -50 points
|
||||
1. **Recently Listed:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed.
|
||||
1. **Can't Analyze ToS:** If a service's Terms of Service cannot be analyzed by our AI (usually due to captchas, client-side rendering, DDoS protections, or non-text format), a penalty of -3 points is applied to the trust score.
|
||||
1. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes).
|
||||
1. **Final Score Range:** The final score is always kept between 0 and 100.
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
|
||||
label: status.label,
|
||||
value: status.value,
|
||||
}))}
|
||||
selectProps={{ value: serviceSuggestion.status }}
|
||||
selectedValue={serviceSuggestion.status}
|
||||
class="flex-1"
|
||||
error={serviceSuggestionUpdateInputErrors.status}
|
||||
/>
|
||||
|
||||
@@ -801,7 +801,8 @@ const apiCalls = await Astro.locals.banners.try(
|
||||
label: type.label,
|
||||
value: type.id,
|
||||
}))}
|
||||
selectProps={{ required: true, value: event.type }}
|
||||
selectedValue={event.type}
|
||||
selectProps={{ required: true }}
|
||||
error={eventUpdateInputErrors.type}
|
||||
/>
|
||||
</div>
|
||||
@@ -982,7 +983,7 @@ const apiCalls = await Astro.locals.banners.try(
|
||||
label: status.label,
|
||||
value: status.value,
|
||||
}))}
|
||||
selectProps={{ value: step.status }}
|
||||
selectedValue={step.status}
|
||||
error={verificationStepUpdateInputErrors.status}
|
||||
/>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import Tooltip from '../../components/Tooltip.astro'
|
||||
import UserBadge from '../../components/UserBadge.astro'
|
||||
import VerificationWarningBanner from '../../components/VerificationWarningBanner.astro'
|
||||
import { getAttributeCategoryInfo } from '../../constants/attributeCategories'
|
||||
import { getAttributeTypeInfo } from '../../constants/attributeTypes'
|
||||
import { baseScoreType, getAttributeTypeInfo } from '../../constants/attributeTypes'
|
||||
import { formatContactMethod } from '../../constants/contactMethods'
|
||||
import { currencies, getCurrencyInfo } from '../../constants/currencies'
|
||||
import { getEventTypeInfo } from '../../constants/eventTypes'
|
||||
@@ -1052,6 +1052,20 @@ const activeEventToShow =
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li
|
||||
class={cn(
|
||||
'bg-night-400 flex items-center self-start rounded-md p-2 text-sm text-pretty select-none',
|
||||
baseScoreType.classNames.container,
|
||||
baseScoreType.classNames.text
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={baseScoreType.icon}
|
||||
class={cn('mr-2 size-4 flex-shrink-0', baseScoreType.classNames.icon)}
|
||||
/>
|
||||
<span class="font-title">{baseScoreType.label}</span>
|
||||
<span class={cn('mr-1 ml-auto', baseScoreType.classNames.icon)}>+50</span>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
@@ -1459,6 +1473,7 @@ const activeEventToShow =
|
||||
Comments
|
||||
</h2>
|
||||
<div
|
||||
id="discuss"
|
||||
class='grid grid-cols-1 gap-8 [grid-template-areas:"about""rating""ai"] sm:grid-cols-[1fr_1fr] sm:gap-4 sm:[grid-template-areas:"about_ai""rating_ai"]'
|
||||
>
|
||||
<div class="relative rounded-md bg-orange-400/10 p-2 px-2.5 text-xs text-orange-100/70 [grid-area:about]">
|
||||
|
||||
Reference in New Issue
Block a user