Release 202506101742
This commit is contained in:
41
web/src/pages/feeds/events.xml.ts
Normal file
41
web/src/pages/feeds/events.xml.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import rss from '@astrojs/rss'
|
||||
import { SITE_URL } from 'astro:env/client'
|
||||
|
||||
import { getEventTypeInfo } from '../../constants/eventTypes'
|
||||
import { getEvents } from '../../lib/feeds'
|
||||
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
export const GET: APIRoute = async (context) => {
|
||||
try {
|
||||
const origin = context.site?.origin ?? new URL(SITE_URL).origin
|
||||
|
||||
const result = await getEvents()
|
||||
if (!result.success) return new Response(result.error.message, result.error.responseInit)
|
||||
const { events } = result.data
|
||||
|
||||
return await rss({
|
||||
title: 'KYCnot.me - Service Events',
|
||||
description: 'Latest events and updates from privacy-focused services tracked on KYCnot.me',
|
||||
site: origin,
|
||||
xmlns: { atom: 'http://www.w3.org/2005/Atom' },
|
||||
items: events.map((event) => {
|
||||
const eventTypeInfo = getEventTypeInfo(event.type)
|
||||
const isOngoing = !event.endedAt || event.endedAt > new Date()
|
||||
const statusText = isOngoing ? 'Ongoing' : 'Resolved'
|
||||
|
||||
return {
|
||||
title: `${event.service.name}: ${event.title}`,
|
||||
pubDate: event.createdAt,
|
||||
description: `${event.content}${event.source ? `\n\nSource: ${event.source}` : ''}`,
|
||||
link: `/service/${event.service.slug}/#event-${String(event.id)}`,
|
||||
categories: [eventTypeInfo.label, event.service.name, statusText],
|
||||
}
|
||||
}),
|
||||
customData: `<language>en-us</language><atom:link href="${context.url.href}" rel="self" type="application/rss+xml"/>`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating events RSS feed:', error)
|
||||
return new Response('Error generating RSS feed', { status: 500 })
|
||||
}
|
||||
}
|
||||
87
web/src/pages/feeds/index.astro
Normal file
87
web/src/pages/feeds/index.astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
import MiniLayout from '../../layouts/MiniLayout.astro'
|
||||
import { takeCounts } from '../../lib/feeds'
|
||||
|
||||
const user = Astro.locals.user
|
||||
|
||||
const feeds = [
|
||||
{
|
||||
title: user ? 'Your notifications' : 'User notifications',
|
||||
description: `Last ${takeCounts.userNotifications.toLocaleString()} of your notifications`,
|
||||
rss: user ? `/feeds/user/${user.feedId}/notifications.xml` : '/feeds/user/[feedId]/notifications.xml',
|
||||
icon: 'ri:notification-line',
|
||||
replacementDescription: user
|
||||
? null
|
||||
: "Replace [feedId] with the user feed ID, found in the user's notifications page",
|
||||
},
|
||||
{
|
||||
title: 'Service comments',
|
||||
description: `Last ${takeCounts.serviceComments.toLocaleString()} comments (and reviews) from a specific service`,
|
||||
rss: '/feeds/service/[slug]/comments.xml',
|
||||
icon: 'ri:chat-3-line',
|
||||
replacementDescription: 'Replace [slug] with the actual service slug',
|
||||
},
|
||||
{
|
||||
title: 'Service events',
|
||||
description: `Last ${takeCounts.serviceEvents.toLocaleString()} events from a specific service`,
|
||||
rss: '/feeds/service/[slug]/events.xml',
|
||||
icon: 'ri:calendar-event-line',
|
||||
replacementDescription: 'Replace [slug] with the actual service slug',
|
||||
},
|
||||
{
|
||||
title: 'All events',
|
||||
description: `Last ${takeCounts.allEvents.toLocaleString()} events from all listed services`,
|
||||
rss: '/feeds/events.xml',
|
||||
icon: 'ri:calendar-2-line',
|
||||
replacementDescription: null,
|
||||
},
|
||||
] as const satisfies {
|
||||
title: string
|
||||
description: string
|
||||
rss: `/feeds/${string}`
|
||||
icon: string
|
||||
replacementDescription: string | null
|
||||
}[]
|
||||
---
|
||||
|
||||
<MiniLayout
|
||||
pageTitle="RSS Feeds"
|
||||
description="Subscribe to RSS feeds to stay updated with the latest comments and events on KYCnot.me"
|
||||
ogImage={{
|
||||
template: 'generic',
|
||||
title: 'RSS Feeds',
|
||||
description: 'Subscribe to stay updated',
|
||||
icon: 'ri:rss-line',
|
||||
}}
|
||||
layoutHeader={{
|
||||
icon: 'ri:rss-line',
|
||||
title: 'RSS Feeds',
|
||||
subtitle: 'Copy the feed URL to your RSS reader',
|
||||
}}
|
||||
size="md"
|
||||
>
|
||||
<div class="space-y-8">
|
||||
{
|
||||
feeds.map((feed) => (
|
||||
<div>
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<Icon name={feed.icon} class="inline-block size-6 shrink-0 text-white" />
|
||||
<h2 class="text-left text-lg leading-tight font-bold text-white">{feed.title}</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-day-300 mt-1 text-center text-sm text-balance">{feed.description}</p>
|
||||
|
||||
<div
|
||||
class="border-night-500 bg-night-600 relative mt-2 rounded-lg border px-4 py-2 font-mono break-all text-white"
|
||||
set:text={`${Astro.url.origin}${feed.rss}`}
|
||||
/>
|
||||
{!!feed.replacementDescription && (
|
||||
<p class="text-day-500 mt-1 text-center text-xs italic">{feed.replacementDescription}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</MiniLayout>
|
||||
61
web/src/pages/feeds/service/[slug]/comments.xml.ts
Normal file
61
web/src/pages/feeds/service/[slug]/comments.xml.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import rss from '@astrojs/rss'
|
||||
import { SITE_URL } from 'astro:env/client'
|
||||
|
||||
import { getServiceUserRoleInfo } from '../../../../constants/serviceUserRoles'
|
||||
import { makeCommentUrl } from '../../../../lib/commentsWithReplies'
|
||||
import { getCommentsForService } from '../../../../lib/feeds'
|
||||
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
export const GET: APIRoute = async (context) => {
|
||||
try {
|
||||
const origin = context.site?.origin ?? new URL(SITE_URL).origin
|
||||
|
||||
const result = await getCommentsForService(context.params.slug)
|
||||
if (!result.success) return new Response(result.error.message, result.error.responseInit)
|
||||
const { service, comments } = result.data
|
||||
|
||||
return await rss({
|
||||
title: `${service.name} - Comments & Reviews | KYCnot.me`,
|
||||
description: `Latest comments and reviews about ${service.name} from KYCnot.me users`,
|
||||
site: origin,
|
||||
xmlns: { dc: 'http://purl.org/dc/elements/1.1/', atom: 'http://www.w3.org/2005/Atom' },
|
||||
items: comments.map((comment) => {
|
||||
const authorName = comment.author.displayName ?? comment.author.name
|
||||
const isRating = comment.ratingActive && comment.rating
|
||||
const title = isRating
|
||||
? `${authorName} rated ${service.name} (${String(comment.rating)}/5 stars)`
|
||||
: `${authorName} commented on ${service.name}`
|
||||
|
||||
const badges = [
|
||||
comment.author.verified ? '✅' : null,
|
||||
comment.author.spammer ? '(Spammer)' : null,
|
||||
comment.author.admin ? '(Admin)' : null,
|
||||
comment.author.moderator && !comment.author.admin ? '(Moderator)' : null,
|
||||
...comment.author.serviceAffiliations.map(
|
||||
(affiliation) =>
|
||||
` (${getServiceUserRoleInfo(affiliation.role).label} at ${affiliation.service.name})`
|
||||
),
|
||||
].filter((badge) => badge !== null)
|
||||
|
||||
return {
|
||||
title,
|
||||
pubDate: comment.createdAt,
|
||||
description: comment.content,
|
||||
link: makeCommentUrl({
|
||||
origin,
|
||||
serviceSlug: service.slug,
|
||||
commentId: comment.id,
|
||||
}),
|
||||
categories: isRating ? ['Rating'] : ['Comment'],
|
||||
guid: `${service.slug}-comment-${String(comment.id)}`,
|
||||
customData: `<dc:creator>${authorName}${badges.length > 0 ? ` ${badges.join(' ')}` : ''}</dc:creator>`,
|
||||
}
|
||||
}),
|
||||
customData: `<language>en-us</language><atom:link href="${context.url.href}" rel="self" type="application/rss+xml"/>`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating service comments RSS feed:', error)
|
||||
return new Response('Error generating RSS feed', { status: 500 })
|
||||
}
|
||||
}
|
||||
41
web/src/pages/feeds/service/[slug]/events.xml.ts
Normal file
41
web/src/pages/feeds/service/[slug]/events.xml.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import rss from '@astrojs/rss'
|
||||
import { SITE_URL } from 'astro:env/client'
|
||||
|
||||
import { getEventTypeInfo } from '../../../../constants/eventTypes'
|
||||
import { getEventsForService } from '../../../../lib/feeds'
|
||||
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
export const GET: APIRoute = async (context) => {
|
||||
try {
|
||||
const origin = context.site?.origin ?? new URL(SITE_URL).origin
|
||||
|
||||
const result = await getEventsForService(context.params.slug)
|
||||
if (!result.success) return new Response(result.error.message, result.error.responseInit)
|
||||
const { service, events } = result.data
|
||||
|
||||
return await rss({
|
||||
title: `${service.name} - Events & Updates | KYCnot.me`,
|
||||
description: `Latest events and updates for ${service.name} tracked on KYCnot.me`,
|
||||
site: origin,
|
||||
xmlns: { atom: 'http://www.w3.org/2005/Atom' },
|
||||
items: events.map((event) => {
|
||||
const eventTypeInfo = getEventTypeInfo(event.type)
|
||||
const isOngoing = !event.endedAt || event.endedAt > new Date()
|
||||
const statusText = isOngoing ? 'Ongoing' : 'Resolved'
|
||||
|
||||
return {
|
||||
title: `${service.name}: ${event.title}`,
|
||||
pubDate: event.createdAt,
|
||||
description: `${event.content}${event.source ? `\n\nSource: ${event.source}` : ''}`,
|
||||
link: `/service/${service.slug}/#event-${String(event.id)}`,
|
||||
categories: [eventTypeInfo.label, statusText],
|
||||
}
|
||||
}),
|
||||
customData: `<language>en-us</language><atom:link href="${context.url.href}" rel="self" type="application/rss+xml"/>`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating service events RSS feed:', error)
|
||||
return new Response('Error generating RSS feed', { status: 500 })
|
||||
}
|
||||
}
|
||||
52
web/src/pages/feeds/user/[feedId]/notifications.xml.ts
Normal file
52
web/src/pages/feeds/user/[feedId]/notifications.xml.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import rss from '@astrojs/rss'
|
||||
import { SITE_URL } from 'astro:env/client'
|
||||
|
||||
import { getNotificationTypeInfo } from '../../../../constants/notificationTypes'
|
||||
import { getUserNotifications } from '../../../../lib/feeds'
|
||||
import {
|
||||
makeNotificationActions,
|
||||
makeNotificationContent,
|
||||
makeNotificationTitle,
|
||||
} from '../../../../lib/notifications'
|
||||
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
export const GET: APIRoute = async (context) => {
|
||||
try {
|
||||
const origin = context.site?.origin ?? new URL(SITE_URL).origin
|
||||
const feedId = context.params.feedId
|
||||
|
||||
const result = await getUserNotifications(feedId)
|
||||
if (!result.success) return new Response(result.error.message, result.error.responseInit)
|
||||
const { user, notifications } = result.data
|
||||
|
||||
const displayName = user.displayName ?? user.name
|
||||
|
||||
return await rss({
|
||||
title: `${displayName}'s Notifications - KYCnot.me`,
|
||||
description: `Notifications for ${displayName} on KYCnot.me - Privacy-focused service reviews and updates`,
|
||||
site: origin,
|
||||
xmlns: { atom: 'http://www.w3.org/2005/Atom' },
|
||||
items: notifications.map((notification) => {
|
||||
const typeInfo = getNotificationTypeInfo(notification.type)
|
||||
|
||||
const title = makeNotificationTitle(notification, user)
|
||||
const description = makeNotificationContent(notification) ?? typeInfo.label
|
||||
const actions = makeNotificationActions(notification, origin)
|
||||
const link = actions[0]?.url ?? `${origin}/notifications`
|
||||
|
||||
return {
|
||||
title,
|
||||
pubDate: notification.createdAt,
|
||||
description,
|
||||
link,
|
||||
categories: [typeInfo.label],
|
||||
}
|
||||
}),
|
||||
customData: `<language>en-us</language><atom:link href="${context.url.href}" rel="self" type="application/rss+xml"/>`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating user notifications RSS feed:', error)
|
||||
return new Response('Error generating RSS feed', { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user