Skip to content
This repository has been archived by the owner on Aug 3, 2024. It is now read-only.

RSS Feeds #1159

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions helpers/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,23 @@ export const md = (options = {}) => {
}

export const renderString = (string) => configuredXss.process(md().render(string))

export const escapeXmlAttr = (unsafe) => {
if (!unsafe) {
return
}
return unsafe.replace(/[<>&'"]/g, function (c) {
switch (c) {
case '<':
return '&lt;'
case '>':
return '&gt;'
case '&':
return '&amp;'
case "'":
return '&apos;'
case '"':
return '&quot;'
}
})
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@ltd/j-toml": "^1.38.0",
"dayjs": "^1.11.7",
"feed": "^4.2.2",
"floating-vue": "^2.0.0-beta.20",
"highlight.js": "^11.7.0",
"js-yaml": "^4.1.0",
Expand Down
77 changes: 77 additions & 0 deletions server/routes/feed/[feed_type]/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Feed } from 'feed'
import { renderString } from '~/helpers/parse'

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const API_URL = config.apiBaseUrl
const WEBSITE_URL = config.public.siteUrl
const authorization = getHeader(event, 'Authorization')

if (authorization === undefined) {
setResponseStatus(event, 401)
return 'Please pass a valid authentication token to view your notifications as an RSS feed.'
}

try {
const userInfo = await $fetch(API_URL + 'user', {
headers: {
Authorization: authorization,
},
})

const userNotifications = await $fetch(API_URL + `user/${userInfo.id}/notifications`, {
headers: {
Authorization: authorization,
},
})

const feed = new Feed({
title: `Notifications for ${userInfo.username}`,
link: WEBSITE_URL + '/notifications',
generator: 'Modrinth',
id: WEBSITE_URL + '/notifications',
description: `${userInfo.username} has ${userNotifications.length} notification${
userNotifications.length === 1 ? '' : 's'
}`,
feedLinks: {
json: WEBSITE_URL + '/feed/json/notifications',
atom: WEBSITE_URL + '/feed/atom/notifications',
rss: WEBSITE_URL + '/feed/rss/notifications',
},
})

userNotifications.forEach((notification) => {
feed.addItem({
title: notification.title,
description: renderString(notification.text),
id: WEBSITE_URL + notification.link,
link: WEBSITE_URL + notification.link,
date: new Date(notification.created),
author: [
{
name: userInfo.username,
link: WEBSITE_URL + `/user/${userInfo.id}`,
},
],
})
})

switch (event.context.params.feed_type.toLowerCase()) {
case 'rss':
setResponseHeader(event, 'Content-Type', 'application/rss+xml')
return feed.rss2()
case 'atom':
setResponseHeader(event, 'Content-Type', 'application/atom+xml')
return feed.atom1()
case 'json':
setResponseHeader(event, 'Content-Type', 'application/feed+json')
return feed.json1()
default:
setResponseStatus(event, 500)
return 'Invalid Feed Type'
}
} catch (e) {
setResponseStatus(event, 401)
return 'There was an error generating the feed.\n\n' + e
}
})
90 changes: 90 additions & 0 deletions server/routes/feed/[feed_type]/project/[id].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Feed } from 'feed'
import { renderString, escapeXmlAttr } from '~/helpers/parse'

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const API_URL = config.apiBaseUrl
const WEBSITE_URL = config.public.siteUrl

const projectInformation = await $fetch(API_URL + 'project/' + event.context.params.id)
const projectVersions = await $fetch(API_URL + 'project/' + event.context.params.id + '/version')
const projectTeam = await $fetch(API_URL + 'project/' + event.context.params.id + '/members')

let featuredImage = projectInformation.gallery.filter((image) => image.featured)[0]

if (featuredImage) {
featuredImage = featuredImage.url
}

const feed = new Feed({
title: projectInformation.title,
id: WEBSITE_URL + `/${projectInformation.project_type}/${projectInformation.id}`,
description:
`${projectInformation.title} is a ${projectInformation.project_type} with ${
projectInformation.downloads
} download${projectInformation.downloads > 1 ? 's' : ''}` +
`${
projectInformation.followers > 0
? 'and ' + projectInformation.followers + ' follower' + projectInformation.followers > 1
? 's'
: ''
: ''
} that is available on Modrinth, an open-source platform to host mods, modpacks, shaders, resource packs, plugins and datapacks.`,
feedLinks: {
json: WEBSITE_URL + `/feed/json/project/${projectInformation.id}`,
atom: WEBSITE_URL + `/feed/atom/project/${projectInformation.id}`,
rss: WEBSITE_URL + `/feed/rss/project/${projectInformation.id}`,
},
generator: 'Modrinth',
link: WEBSITE_URL + `/${projectInformation.project_type}/${projectInformation.id}`,
language: 'en',
updated: new Date(projectInformation.updated),
favicon: projectInformation.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
image: featuredImage ?? undefined,
})

projectVersions.forEach((version) => {
feed.addItem({
title: `New Version Released: ${version.name}`,
id:
WEBSITE_URL +
`/${projectInformation.project_type}/${projectInformation.id}/version/${version.id}`,
link:
WEBSITE_URL +
`/${projectInformation.project_type}/${projectInformation.id}/version/${version.id}`,
content: escapeXmlAttr(
`This version is for ${version.loaders.join(
', '
)} and works on the following Minecraft versions: ${version.game_versions.join(', ')}<br>` +
// Check for changelog length being greater than 1 to ensure no blank changelog section.
`<h3>Changelog</h3>${renderString(
version.changelog.length > 1 ? version.changelog : 'No changelog was specified.'
)}`
),
author: [
...projectTeam.map((member) => {
return {
name: member.user.username,
link: WEBSITE_URL + `/user/${member.user.id}`,
}
}),
],
IMB11 marked this conversation as resolved.
Show resolved Hide resolved
date: new Date(version.date_published),
})
})

switch (event.context.params.feed_type.toLowerCase()) {
case 'rss':
setResponseHeader(event, 'Content-Type', 'application/rss+xml')
return feed.rss2()
case 'atom':
setResponseHeader(event, 'Content-Type', 'application/atom+xml')
return feed.atom1()
case 'json':
setResponseHeader(event, 'Content-Type', 'application/feed+json')
return feed.json1()
default:
setResponseStatus(event, 500)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 500? This is not a server error

return 'Invalid Feed Type'
}
})