From 459d7c91f76e885557afc933527270bd5e3b4ac2 Mon Sep 17 00:00:00 2001 From: pluja Date: Mon, 9 Jun 2025 19:01:08 +0000 Subject: [PATCH] Release 202506091901 --- web/prisma/seed.ts | 24 +- web/prisma/triggers/02_service_score.sql | 11 + web/src/lib/attributes.ts | 13 + web/src/pages/about.mdx | 24 +- web/src/pages/service/[slug].astro | 433 +++++++++-------------- 5 files changed, 232 insertions(+), 273 deletions(-) diff --git a/web/prisma/seed.ts b/web/prisma/seed.ts index d843805..74edd41 100755 --- a/web/prisma/seed.ts +++ b/web/prisma/seed.ts @@ -19,6 +19,7 @@ import { type ServiceVisibility, ServiceSuggestionType, KycLevelClarification, + VerificationStepStatus, } from '@prisma/client' import { omit, uniqBy } from 'lodash-es' import { generateUsername } from 'unique-username-generator' @@ -610,6 +611,10 @@ const generateFakeService = (users: User[]) => { const name = faker.helpers.arrayElement(serviceNames) const slug = `${faker.helpers.slugify(name).toLowerCase()}-${faker.string.alphanumeric({ length: 6, casing: 'lower' })}` + const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), { + probability: 0.8, + }) + return { name, slug, @@ -643,6 +648,19 @@ const generateFakeService = (users: User[]) => { }, verificationProofMd: status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraphs() : null, + verificationSteps: + (status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED') && faker.datatype.boolean(0.75) + ? { + create: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => ({ + title: faker.lorem.sentence(), + description: faker.lorem.paragraph(), + status: faker.helpers.arrayElement(Object.values(VerificationStepStatus)), + evidenceMd: faker.lorem.paragraph(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + })), + } + : undefined, referral: faker.helpers.arrayElement([ `?ref=${faker.string.alphanumeric(6)}`, `/ref/${faker.string.alphanumeric(6)}`, @@ -661,8 +679,10 @@ const generateFakeService = (users: User[]) => { imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`, listedAt: faker.date.past(), verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null, - tosReview: faker.helpers.arrayElement(tosReviewExamples), - tosReviewAt: faker.date.past(), + tosReview, + tosReviewAt: tosReview + ? faker.date.recent() + : faker.helpers.maybe(() => faker.date.recent(), { probability: 0.5 }), userSentiment: faker.helpers.maybe(() => generateFakeUserSentiment(), { probability: 0.8 }), userSentimentAt: faker.date.recent(), internalNotes: faker.helpers.maybe( diff --git a/web/prisma/triggers/02_service_score.sql b/web/prisma/triggers/02_service_score.sql index 8754201..6c21d3c 100644 --- a/web/prisma/triggers/02_service_score.sql +++ b/web/prisma/triggers/02_service_score.sql @@ -135,6 +135,17 @@ BEGIN SET "isRecentlyListed" = FALSE WHERE id = service_id; END IF; + + -- Apply penalty if ToS cannot be analyzed + IF EXISTS ( + SELECT 1 + FROM "Service" + WHERE id = service_id + AND "tosReviewAt" IS NOT NULL + AND "tosReview" IS NULL + ) THEN + trust_score := trust_score - 3; + END IF; -- Calculate final trust score (base 100) trust_score := trust_score + verification_factor + attributes_score; diff --git a/web/src/lib/attributes.ts b/web/src/lib/attributes.ts index 0192632..f26463a 100644 --- a/web/src/lib/attributes.ts +++ b/web/src/lib/attributes.ts @@ -41,6 +41,8 @@ export function makeNonDbAttributes( isRecentlyListed: true listedAt: true createdAt: true + tosReviewAt: true + tosReview: true } }>, { filter = false }: { filter?: boolean } = {} @@ -156,6 +158,17 @@ export function makeNonDbAttributes( trustPoints: -5, links: [], }, + { + title: "Can't analyse ToS", + show: service.tosReviewAt !== null && service.tosReview === null, + type: 'WARNING', + category: 'TRUST', + description: + 'The Terms of Service page is not analyable by our AI. Possible reasons may be: captchas, client side rendering, DDoS protections, or non-text format.', + privacyPoints: 0, + trustPoints: -3, + links: [], + }, ] if (filter) return nonDbAttributes.filter(({ show }) => show) diff --git a/web/src/pages/about.mdx b/web/src/pages/about.mdx index d5f089f..ee19be8 100644 --- a/web/src/pages/about.mdx +++ b/web/src/pages/about.mdx @@ -57,7 +57,6 @@ Some users are **verified**, this means that the moderators have confirmed that Users can also be **affiliated** with a service if they're related to it, such as being an owner or part of the team. If you own a service and want to get verified, just reach out to us. - ## Listings ### Suggesting a new listing @@ -141,7 +140,7 @@ You can view all available attributes on the [Attributes page](/attributes). Attributes are classified into two main types: 1. **Privacy Attributes** – Related to data protection and anonymity. -2. **Trust Attributes** – Related to reliability and security. +1. **Trust Attributes** – Related to reliability and security. These categories **directly influence** a service's Privacy and Trust scores, which contribute to its **overall rating**. @@ -154,31 +153,32 @@ 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. -2. **KYC Level:** Adjusts the score based on the level of identity verification required: +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** -3. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL. -4. **I2P URL:** **+5 points** if the service offers at least one I2P URL. -5. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method. -6. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes). -7. **Final Score Range:** The final score is always kept between 0 and 100. +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. ##### Trust Score 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. -2. **Verification Status Adjustment:** +1. **Verification Status:** - **Verification Success:** +10 points - **Approved:** +5 points - **Community Contributed:** 0 points - **Verification Failed (SCAM):** -50 points -3. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes). -4. **Recently Listed Penalty & Flag:** 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. -5. **Final Score Range:** The final score is always kept between 0 and 100. +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. ##### Overall Score diff --git a/web/src/pages/service/[slug].astro b/web/src/pages/service/[slug].astro index 3237077..ec04618 100644 --- a/web/src/pages/service/[slug].astro +++ b/web/src/pages/service/[slug].astro @@ -9,7 +9,6 @@ import { head, orderBy, pick, shuffle, sortBy, tail } from 'lodash-es' import AdminOnly from '../../components/AdminOnly.astro' import BadgeSmall from '../../components/BadgeSmall.astro' import BadgeStandard from '../../components/BadgeStandard.astro' -import Button from '../../components/Button.astro' import CommentSection from '../../components/CommentSection.astro' import CommentSummary from '../../components/CommentSummary.astro' import DropdownButton from '../../components/DropdownButton.astro' @@ -411,6 +410,8 @@ const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility const activeAlertOrWarningEvents = service.events.filter( (event) => getEventTypeInfo(event.type).showBanner && (event.endedAt === null || event.endedAt >= now) ) +const activeEventToShow = + activeAlertOrWarningEvents.find((event) => event.type === EventType.ALERT) ?? activeAlertOrWarningEvents[0] --- { - activeAlertOrWarningEvents.length > 0 && ( + !!activeEventToShow && ( e.type === EventType.ALERT) - ? 'bg-red-900/50 text-red-300 hover:bg-red-800/60' - : 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60' + 'group mb-4 block rounded-md px-3 py-2 text-sm transition-colors duration-200', + activeEventToShow.type === EventType.ALERT + ? 'bg-red-900/50 text-red-300 hover:bg-red-800/60 focus-visible:bg-red-800/60' + : 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60 focus-visible:bg-yellow-800/60' )} > e.type === EventType.ALERT) - ? 'ri:alert-fill' - : 'ri:alarm-warning-fill' - } + name={activeEventToShow.type === EventType.ALERT ? 'ri:alert-fill' : 'ri:alarm-warning-fill'} class="me-1.5 inline-block size-4 align-[-0.15em]" /> - {activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT) - ? 'There is an active alert for this service. Click to see details.' - : 'There is an active warning for this service. Click to see details.'} + {activeEventToShow.title} — {activeEventToShow.content} + {activeAlertOrWarningEvents.length >= 2 && <>+{activeAlertOrWarningEvents.length - 1} more events.} + Go to events ) } @@ -1081,105 +1078,119 @@ const activeAlertOrWarningEvents = service.events.filter( { service.verificationStatus === 'VERIFICATION_SUCCESS' || service.verificationStatus === 'APPROVED' ? ( - service.tosReview ? ( - <> - h.rating === 'positive') - .map( - (h, i) => - ({ - '@type': 'ListItem', - position: i + 1, - name: h.title, - description: h.content, - }) satisfies ListItem - ), - negativeNotes: service.tosReview.highlights - .filter((h) => h.rating === 'negative') - .map( - (h, i) => - ({ - '@type': 'ListItem', - position: i + 1, - name: h.title, - description: h.content, - }) satisfies ListItem - ), - } satisfies WithContext - } - /> + service.tosReviewAt !== null ? ( + service.tosReview ? ( + <> + h.rating === 'positive') + .map( + (h, i) => + ({ + '@type': 'ListItem', + position: i + 1, + name: h.title, + description: h.content, + }) satisfies ListItem + ), + negativeNotes: service.tosReview.highlights + .filter((h) => h.rating === 'negative') + .map( + (h, i) => + ({ + '@type': 'ListItem', + position: i + 1, + name: h.title, + description: h.content, + }) satisfies ListItem + ), + } satisfies WithContext + } + /> -
-
- {service.tosReview.summary ? ( - - ) : ( -

No summary available

- )} -
-
- -
- {sortBy( - service.tosReview.highlights.map((highlight) => ({ - ...highlight, - ratingInfo: getTosHighlightRatingInfo(highlight.rating), - })), - 'ratingInfo.order' - ).map((highlight) => ( -
-

- - {highlight.title} -

- {!!highlight.content && ( -
- -
+
+
+ {service.tosReview.summary ? ( + + ) : ( +

No summary available

)}
- ))} -
+
-

- {!!service.tosReviewAt && ( - <> - Reviewed - +

+ {sortBy( + service.tosReview.highlights.map((highlight) => ({ + ...highlight, + ratingInfo: getTosHighlightRatingInfo(highlight.rating), + })), + 'ratingInfo.order' + ).map((highlight) => ( +
+

+ + {highlight.title} +

+ {!!highlight.content && ( +
+ +
+ )} +
+ ))} +
+ +

+ Reviewed + {service.tosUrls.length > 0 && 'from'} + {service.tosUrls.map((url) => ( + + {urlDomain(url)} + + ))} +

+

+ ToS reviews are AI-generated and should be used as a reference only. +

+ + ) : ( +
+

Can't analyze ToS with AI

+

+ Maybe due to captchas, client side rendering, DDoS protections, or non-text format. +

+ {service.tosUrls.length > 0 && ( +

+ {service.tosUrls.map((url) => ( + + {urlDomain(url)} + + ))} +

)} - {!!service.tosReviewAt && !!service.tosUrls.length && 'from'} - {service.tosUrls.map((url) => ( - - {urlDomain(url)} - - ))} -

-

- ToS reviews are AI-generated and should be used as a reference only. -

- +
+ ) ) : (

Not reviewed yet

@@ -1332,77 +1343,6 @@ const activeAlertOrWarningEvents = service.events.filter(
) } - - { - service.verificationSteps.length > 0 && ( -
-

- - Verification Steps -

-
    - {service.verificationSteps.map((step) => { - const statusInfo = getVerificationStepStatusInfo(step.status) - return ( -
  • -
    - -
    -
    - {step.title} - - - {statusInfo.text} - - -
    -
    -
    -

    - Added - -

    - {step.description && ( -
    {step.description}
    - )} - {step.evidenceMd && ( -
    -
    Evidence:
    -
    - -
    -
    - )} -
    -
    -
  • - ) - })} -
-
- ) - }
@@ -1456,89 +1396,64 @@ const activeAlertOrWarningEvents = service.events.filter( ) } - - { - service.verificationProofMd && ( -
- - { - service.verificationProofMd && ( - <> - - - - ) - } + { + service.verificationProofMd && ( + <> +

Verification proof

+
+ +
+ + ) + } + { + service.verificationSteps.length > 0 && ( + <> +

Verification Steps

+
    + {service.verificationSteps.map((step) => { + const statusInfo = getVerificationStepStatusInfo(step.status) + + return ( +
  • +
    +
    +
    + + + {step.title} +
    +

    {step.description}

    +
    +

    + +

    +
    + + {step.evidenceMd && ( +
    + +
    + )} +
  • + ) + })} +
+ + ) + }

Comments