From 6c27842bd2a944df03f711ded50cfd9d353d102b Mon Sep 17 00:00:00 2001 From: mineblock11 <93472213+mineblock11@users.noreply.github.com> Date: Fri, 19 May 2023 23:04:40 +0100 Subject: [PATCH 1/5] Project RSS Feed (atom1, json2, rss2) --- package.json | 1 + .../routes/feed/[feed_type]/project/[id].js | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 server/routes/feed/[feed_type]/project/[id].js diff --git a/package.json b/package.json index 8066711b28..f8a39a6b15 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/routes/feed/[feed_type]/project/[id].js b/server/routes/feed/[feed_type]/project/[id].js new file mode 100644 index 0000000000..5eb326a207 --- /dev/null +++ b/server/routes/feed/[feed_type]/project/[id].js @@ -0,0 +1,68 @@ +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 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 resultFeed = new Feed({ + title: projectInformation.title, + id: projectInformation.id, + description: renderString(projectInformation.description), + 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) => { + resultFeed.addItem({ + title: `New Version Released: ${version.name}`, + id: `release-${version.id}`, + link: + WEBSITE_URL + + `/${projectInformation.project_type}/${projectInformation.id}/version/${version.id}`, + content: + `This version is for ${version.game_versions.join(', ')}
` + + // Check for changelog length being greater than 1 to ensure no blank changelog section. + // Legacy changelog support. + `

Changelog

${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}`, + } + }), + ], + date: new Date(version.date_published), + }) + }) + + switch (event.context.params.feed_type) { + case 'rss': + return resultFeed.rss2() + case 'atom': + return resultFeed.atom1() + case 'json': + return resultFeed.json1() + default: + setResponseStatus(event, 404) + return 'Invalid Feed Type' + } +}) From 55f7bb1f142d2e2337b143d5c956fa22c3d3bf53 Mon Sep 17 00:00:00 2001 From: mineblock11 <93472213+mineblock11@users.noreply.github.com> Date: Fri, 19 May 2023 23:07:03 +0100 Subject: [PATCH 2/5] Make feed_type insensitive to capitals --- server/routes/feed/[feed_type]/project/[id].js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/feed/[feed_type]/project/[id].js b/server/routes/feed/[feed_type]/project/[id].js index 5eb326a207..b961952396 100644 --- a/server/routes/feed/[feed_type]/project/[id].js +++ b/server/routes/feed/[feed_type]/project/[id].js @@ -54,7 +54,7 @@ export default defineEventHandler(async (event) => { }) }) - switch (event.context.params.feed_type) { + switch (event.context.params.feed_type.toLowerCase()) { case 'rss': return resultFeed.rss2() case 'atom': From 881f01ffda38d7623cf5f5741ffefc54778605e0 Mon Sep 17 00:00:00 2001 From: Calum Date: Sun, 21 May 2023 14:23:47 +0100 Subject: [PATCH 3/5] Notification Feed --- .../routes/feed/[feed_type]/notifications.js | 67 +++++++++++++++++++ .../routes/feed/[feed_type]/project/[id].js | 15 +++-- 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 server/routes/feed/[feed_type]/notifications.js diff --git a/server/routes/feed/[feed_type]/notifications.js b/server/routes/feed/[feed_type]/notifications.js new file mode 100644 index 0000000000..feaec6b0cf --- /dev/null +++ b/server/routes/feed/[feed_type]/notifications.js @@ -0,0 +1,67 @@ +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', + description: `${userInfo.username} has ${userNotifications.length} notification${ + userNotifications.length === 1 ? '' : 's' + }`, + }) + + userNotifications.forEach((notification) => { + feed.addItem({ + title: renderString(notification.title), + description: renderString(notification.text), + id: notification.id, + link: WEBSITE_URL + notification.link, + date: new Date(notification.created), + }) + }) + + 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 ( + 'Please pass a valid authentication token to view your notifications as an RSS feed.\n\n' + e + ) + } +}) diff --git a/server/routes/feed/[feed_type]/project/[id].js b/server/routes/feed/[feed_type]/project/[id].js index b961952396..1de561d06b 100644 --- a/server/routes/feed/[feed_type]/project/[id].js +++ b/server/routes/feed/[feed_type]/project/[id].js @@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => { featuredImage = featuredImage.url } - const resultFeed = new Feed({ + const feed = new Feed({ title: projectInformation.title, id: projectInformation.id, description: renderString(projectInformation.description), @@ -29,7 +29,7 @@ export default defineEventHandler(async (event) => { }) projectVersions.forEach((version) => { - resultFeed.addItem({ + feed.addItem({ title: `New Version Released: ${version.name}`, id: `release-${version.id}`, link: @@ -56,13 +56,16 @@ export default defineEventHandler(async (event) => { switch (event.context.params.feed_type.toLowerCase()) { case 'rss': - return resultFeed.rss2() + setResponseHeader(event, 'Content-Type', 'application/rss+xml') + return feed.rss2() case 'atom': - return resultFeed.atom1() + setResponseHeader(event, 'Content-Type', 'application/atom+xml') + return feed.atom1() case 'json': - return resultFeed.json1() + setResponseHeader(event, 'Content-Type', 'application/feed+json') + return feed.json1() default: - setResponseStatus(event, 404) + setResponseStatus(event, 500) return 'Invalid Feed Type' } }) From 10548978eec22a8a9b2c4491a57086dc0a94220d Mon Sep 17 00:00:00 2001 From: Calum Date: Wed, 24 May 2023 21:03:50 +0100 Subject: [PATCH 4/5] Project RSS fixes. --- helpers/parse.js | 20 +++++++++ .../routes/feed/[feed_type]/notifications.js | 5 +++ .../routes/feed/[feed_type]/project/[id].js | 41 ++++++++++++++----- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/helpers/parse.js b/helpers/parse.js index 24487131e2..e5623a0f04 100644 --- a/helpers/parse.js +++ b/helpers/parse.js @@ -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 '<' + case '>': + return '>' + case '&': + return '&' + case "'": + return ''' + case '"': + return '"' + } + }) +} diff --git a/server/routes/feed/[feed_type]/notifications.js b/server/routes/feed/[feed_type]/notifications.js index feaec6b0cf..2eb2e27bea 100644 --- a/server/routes/feed/[feed_type]/notifications.js +++ b/server/routes/feed/[feed_type]/notifications.js @@ -32,6 +32,11 @@ export default defineEventHandler(async (event) => { 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) => { diff --git a/server/routes/feed/[feed_type]/project/[id].js b/server/routes/feed/[feed_type]/project/[id].js index 1de561d06b..0b78c6a81f 100644 --- a/server/routes/feed/[feed_type]/project/[id].js +++ b/server/routes/feed/[feed_type]/project/[id].js @@ -1,5 +1,5 @@ import { Feed } from 'feed' -import { renderString } from '~/helpers/parse' +import { renderString, escapeXmlAttr } from '~/helpers/parse' export default defineEventHandler(async (event) => { const config = useRuntimeConfig() @@ -18,8 +18,23 @@ export default defineEventHandler(async (event) => { const feed = new Feed({ title: projectInformation.title, - id: projectInformation.id, - description: renderString(projectInformation.description), + 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', @@ -31,17 +46,21 @@ export default defineEventHandler(async (event) => { projectVersions.forEach((version) => { feed.addItem({ title: `New Version Released: ${version.name}`, - id: `release-${version.id}`, + id: + WEBSITE_URL + + `/${projectInformation.project_type}/${projectInformation.id}/version/${version.id}`, link: WEBSITE_URL + `/${projectInformation.project_type}/${projectInformation.id}/version/${version.id}`, - content: - `This version is for ${version.game_versions.join(', ')}
` + - // Check for changelog length being greater than 1 to ensure no blank changelog section. - // Legacy changelog support. - `

Changelog

${renderString( - version.changelog.length > 1 ? version.changelog : 'No changelog was specified.' - )}`, + content: escapeXmlAttr( + `This version is for ${version.loaders.join( + ', ' + )} and works on the following Minecraft versions: ${version.game_versions.join(', ')}
` + + // Check for changelog length being greater than 1 to ensure no blank changelog section. + `

Changelog

${renderString( + version.changelog.length > 1 ? version.changelog : 'No changelog was specified.' + )}` + ), author: [ ...projectTeam.map((member) => { return { From 909d706d2a7cbb88226677bbb1dca3eb25586c96 Mon Sep 17 00:00:00 2001 From: Calum Date: Wed, 24 May 2023 21:13:36 +0100 Subject: [PATCH 5/5] Fix notification feed --- server/routes/feed/[feed_type]/notifications.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/server/routes/feed/[feed_type]/notifications.js b/server/routes/feed/[feed_type]/notifications.js index 2eb2e27bea..42909f8531 100644 --- a/server/routes/feed/[feed_type]/notifications.js +++ b/server/routes/feed/[feed_type]/notifications.js @@ -29,6 +29,7 @@ export default defineEventHandler(async (event) => { 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' }`, @@ -41,11 +42,17 @@ export default defineEventHandler(async (event) => { userNotifications.forEach((notification) => { feed.addItem({ - title: renderString(notification.title), + title: notification.title, description: renderString(notification.text), - id: notification.id, + 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}`, + }, + ], }) }) @@ -65,8 +72,6 @@ export default defineEventHandler(async (event) => { } } catch (e) { setResponseStatus(event, 401) - return ( - 'Please pass a valid authentication token to view your notifications as an RSS feed.\n\n' + e - ) + return 'There was an error generating the feed.\n\n' + e } })