diff options
Diffstat (limited to 'packages/trpc/models/lists.ts')
| -rw-r--r-- | packages/trpc/models/lists.ts | 140 |
1 files changed, 116 insertions, 24 deletions
diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts index d278f8d9..2631ca7e 100644 --- a/packages/trpc/models/lists.ts +++ b/packages/trpc/models/lists.ts @@ -1,5 +1,6 @@ +import crypto from "node:crypto"; import { TRPCError } from "@trpc/server"; -import { and, count, eq } from "drizzle-orm"; +import { and, count, eq, or } from "drizzle-orm"; import invariant from "tiny-invariant"; import { z } from "zod"; @@ -7,14 +8,18 @@ import { SqliteError } from "@karakeep/db"; import { bookmarkLists, bookmarksInLists } from "@karakeep/db/schema"; import { triggerRuleEngineOnEvent } from "@karakeep/shared/queues"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; +import { ZSortOrder } from "@karakeep/shared/types/bookmarks"; import { ZBookmarkList, zEditBookmarkListSchemaWithValidation, zNewBookmarkListSchema, } from "@karakeep/shared/types/lists"; +import { ZCursor } from "@karakeep/shared/types/pagination"; -import { AuthedContext } from ".."; +import { AuthedContext, Context } from ".."; +import { buildImpersonatingAuthedContext } from "../lib/impersonate"; import { getBookmarkIdsFromMatcher } from "../lib/search"; +import { Bookmark } from "./bookmarks"; import { PrivacyAware } from "./privacy"; export abstract class List implements PrivacyAware { @@ -26,7 +31,7 @@ export abstract class List implements PrivacyAware { private static fromData( ctx: AuthedContext, data: ZBookmarkList & { userId: string }, - ): ManualList | SmartList { + ) { if (data.type === "smart") { return new SmartList(ctx, data); } else { @@ -34,21 +39,6 @@ export abstract class List implements PrivacyAware { } } - static async fromName( - ctx: AuthedContext, - name: string, - ): Promise<(ManualList | SmartList)[]> { - // Names are not unique, so we need to find all lists with the same name - const lists = await ctx.db.query.bookmarkLists.findMany({ - where: and( - eq(bookmarkLists.name, name), - eq(bookmarkLists.userId, ctx.user.id), - ), - }); - - return lists.map((l) => this.fromData(ctx, l)); - } - static async fromId( ctx: AuthedContext, id: string, @@ -66,7 +56,64 @@ export abstract class List implements PrivacyAware { message: "List not found", }); } - return this.fromData(ctx, list); + if (list.type === "smart") { + return new SmartList(ctx, list); + } else { + return new ManualList(ctx, list); + } + } + + static async getPublicListContents( + ctx: Context, + listId: string, + token: string | null, + pagination: { + limit: number; + order: Exclude<ZSortOrder, "relevance">; + cursor: ZCursor | null | undefined; + }, + ) { + const listdb = await ctx.db.query.bookmarkLists.findFirst({ + where: and( + eq(bookmarkLists.id, listId), + or( + eq(bookmarkLists.public, true), + token !== null ? eq(bookmarkLists.rssToken, token) : undefined, + ), + ), + }); + if (!listdb) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "List not found", + }); + } + + // The token here acts as an authed context, so we can create + // an impersonating context for the list owner as long as + // we don't leak the context. + const authedCtx = await buildImpersonatingAuthedContext(listdb.userId); + const list = List.fromData(authedCtx, listdb); + const bookmarkIds = await list.getBookmarkIds(); + + const bookmarks = await Bookmark.loadMulti(authedCtx, { + ids: bookmarkIds, + includeContent: false, + limit: pagination.limit, + sortOrder: pagination.order, + cursor: pagination.cursor, + }); + + return { + list: { + icon: list.list.icon, + name: list.list.name, + description: list.list.description, + numItems: bookmarkIds.length, + }, + bookmarks: bookmarks.bookmarks.map((b) => b.asPublicBookmark()), + nextCursor: bookmarks.nextCursor, + }; } static async create( @@ -90,6 +137,9 @@ export abstract class List implements PrivacyAware { static async getAll(ctx: AuthedContext): Promise<(ManualList | SmartList)[]> { const lists = await ctx.db.query.bookmarkLists.findMany({ + columns: { + rssToken: false, + }, where: and(eq(bookmarkLists.userId, ctx.user.id)), }); return lists.map((l) => this.fromData(ctx, l)); @@ -99,7 +149,11 @@ export abstract class List implements PrivacyAware { const lists = await ctx.db.query.bookmarksInLists.findMany({ where: and(eq(bookmarksInLists.bookmarkId, bookmarkId)), with: { - list: true, + list: { + columns: { + rssToken: false, + }, + }, }, }); invariant(lists.map((l) => l.list.userId).every((id) => id == ctx.user.id)); @@ -140,6 +194,7 @@ export abstract class List implements PrivacyAware { icon: input.icon, parentId: input.parentId, query: input.query, + public: input.public, }) .where( and( @@ -154,6 +209,45 @@ export abstract class List implements PrivacyAware { this.list = result[0]; } + private async setRssToken(token: string | null) { + const result = await this.ctx.db + .update(bookmarkLists) + .set({ rssToken: token }) + .where( + and( + eq(bookmarkLists.id, this.list.id), + eq(bookmarkLists.userId, this.ctx.user.id), + ), + ) + .returning(); + if (result.length == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + return result[0].rssToken; + } + + async getRssToken(): Promise<string | null> { + const [result] = await this.ctx.db + .select({ rssToken: bookmarkLists.rssToken }) + .from(bookmarkLists) + .where( + and( + eq(bookmarkLists.id, this.list.id), + eq(bookmarkLists.userId, this.ctx.user.id), + ), + ) + .limit(1); + return result.rssToken ?? null; + } + + async regenRssToken() { + return await this.setRssToken(crypto.randomBytes(32).toString("hex")); + } + + async clearRssToken() { + await this.setRssToken(null); + } + abstract get type(): "manual" | "smart"; abstract getBookmarkIds(ctx: AuthedContext): Promise<string[]>; abstract getSize(ctx: AuthedContext): Promise<number>; @@ -271,10 +365,8 @@ export class ManualList extends List { } catch (e) { if (e instanceof SqliteError) { if (e.code == "SQLITE_CONSTRAINT_PRIMARYKEY") { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Bookmark ${bookmarkId} is already in the list ${this.list.id}`, - }); + // this is fine, it just means the bookmark is already in the list + return; } } throw new TRPCError({ |
