diff options
Diffstat (limited to 'packages/api')
| -rw-r--r-- | packages/api/index.ts | 4 | ||||
| -rw-r--r-- | packages/api/middlewares/auth.ts | 17 | ||||
| -rw-r--r-- | packages/api/package.json | 2 | ||||
| -rw-r--r-- | packages/api/routes/rss.ts | 51 | ||||
| -rw-r--r-- | packages/api/utils/rss.ts | 54 |
5 files changed, 126 insertions, 2 deletions
diff --git a/packages/api/index.ts b/packages/api/index.ts index 00919f3e..a3ba8d42 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -9,6 +9,7 @@ import assets from "./routes/assets"; import bookmarks from "./routes/bookmarks"; import highlights from "./routes/highlights"; import lists from "./routes/lists"; +import rss from "./routes/rss"; import tags from "./routes/tags"; import users from "./routes/users"; @@ -22,7 +23,8 @@ const v1 = new Hono<{ .route("/lists", lists) .route("/tags", tags) .route("/users", users) - .route("/assets", assets); + .route("/assets", assets) + .route("/rss", rss); const app = new Hono<{ Variables: { diff --git a/packages/api/middlewares/auth.ts b/packages/api/middlewares/auth.ts index 7f39a6f9..42bca6c8 100644 --- a/packages/api/middlewares/auth.ts +++ b/packages/api/middlewares/auth.ts @@ -1,11 +1,26 @@ import { createMiddleware } from "hono/factory"; import { HTTPException } from "hono/http-exception"; -import { AuthedContext, createCallerFactory } from "@karakeep/trpc"; +import { AuthedContext, Context, createCallerFactory } from "@karakeep/trpc"; import { appRouter } from "@karakeep/trpc/routers/_app"; const createCaller = createCallerFactory(appRouter); +export const unauthedMiddleware = createMiddleware<{ + Variables: { + ctx: Context; + api: ReturnType<typeof createCaller>; + }; +}>(async (c, next) => { + if (!c.var.ctx) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + c.set("api", createCaller(c.get("ctx"))); + await next(); +}); + export const authMiddleware = createMiddleware<{ Variables: { ctx: AuthedContext; diff --git a/packages/api/package.json b/packages/api/package.json index f968ed94..82b2b9d0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -18,6 +18,7 @@ "@karakeep/shared": "workspace:*", "@karakeep/trpc": "workspace:*", "hono": "^4.7.10", + "rss": "^1.2.2", "zod": "^3.24.2" }, "devDependencies": { @@ -26,6 +27,7 @@ "@karakeep/tsconfig": "workspace:^0.1.0", "@types/bcryptjs": "^2.4.6", "@types/deep-equal": "^1.0.4", + "@types/rss": "^0.0.32", "vite-tsconfig-paths": "^4.3.1", "vitest": "^1.6.1" }, diff --git a/packages/api/routes/rss.ts b/packages/api/routes/rss.ts new file mode 100644 index 00000000..81c9756c --- /dev/null +++ b/packages/api/routes/rss.ts @@ -0,0 +1,51 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +import serverConfig from "@karakeep/shared/config"; +import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@karakeep/shared/types/bookmarks"; +import { List } from "@karakeep/trpc/models/lists"; + +import { unauthedMiddleware } from "../middlewares/auth"; +import { toRSS } from "../utils/rss"; + +const app = new Hono().get( + "/lists/:listId", + zValidator( + "query", + z.object({ + token: z.string().min(1), + limit: z.coerce + .number() + .min(1) + .max(MAX_NUM_BOOKMARKS_PER_PAGE) + .optional(), + }), + ), + unauthedMiddleware, + async (c) => { + const listId = c.req.param("listId"); + const searchParams = c.req.valid("query"); + const token = searchParams.token; + + const res = await List.getForRss(c.var.ctx, listId, token, { + limit: searchParams.limit ?? 20, + }); + const list = res.list; + + const rssFeed = toRSS( + { + title: `Bookmarks from ${list.icon} ${list.name}`, + feedUrl: `${serverConfig.publicApiUrl}/v1/rss/lists/${listId}`, + siteUrl: `${serverConfig.publicUrl}/dashboard/lists/${listId}`, + description: list.description ?? undefined, + }, + res.bookmarks, + ); + + c.header("Content-Type", "application/rss+xml"); + return c.body(rssFeed); + }, +); + +export default app; diff --git a/packages/api/utils/rss.ts b/packages/api/utils/rss.ts new file mode 100644 index 00000000..079b3f5a --- /dev/null +++ b/packages/api/utils/rss.ts @@ -0,0 +1,54 @@ +import RSS from "rss"; + +import serverConfig from "@karakeep/shared/config"; +import { + BookmarkTypes, + ZPublicBookmark, +} from "@karakeep/shared/types/bookmarks"; +import { getAssetUrl } from "@karakeep/shared/utils/assetUtils"; + +export function toRSS( + params: { + title: string; + description?: string; + feedUrl: string; + siteUrl: string; + }, + bookmarks: ZPublicBookmark[], +) { + const feed = new RSS({ + title: params.title, + feed_url: params.feedUrl, + site_url: params.siteUrl, + description: params.description, + generator: "Karakeep", + }); + + bookmarks + .filter( + (b) => + b.content.type === BookmarkTypes.LINK || + b.content.type === BookmarkTypes.ASSET, + ) + .forEach((bookmark) => { + feed.item({ + date: bookmark.createdAt, + title: bookmark.title ?? "", + url: + bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.url + : bookmark.content.type === BookmarkTypes.ASSET + ? `${serverConfig.publicUrl}${getAssetUrl(bookmark.content.assetId)}` + : "", + guid: bookmark.id, + author: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.author ?? undefined) + : undefined, + categories: bookmark.tags, + description: bookmark.description ?? "", + }); + }); + + return feed.xml({ indent: true }); +} |
