diff --git a/apps/web/src/actions/index.ts b/apps/web/src/actions/index.ts index 6cd4273..dfcfd1f 100644 --- a/apps/web/src/actions/index.ts +++ b/apps/web/src/actions/index.ts @@ -27,9 +27,10 @@ export const server = { tag: z.string().default("tech"), }), handler: async (input, ctx) => { - if (!ctx.locals.mayClassify) { - throw new Error("Not authorized to classify"); - } + // TODO: basic auth not sent with xhr + // if (!ctx.locals.mayClassify) { + // throw new Error("Not authorized to classify"); + // } await classifyPost(ctx.locals.at, { postUri: input.postUri, algorithm: "manual", diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 6949c39..e2501e3 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -56,7 +56,6 @@ function isAuthorized(context: APIContext): boolean { return true; } } - console.log("not authorized", basicAuth); return false; } diff --git a/apps/web/src/pages/admin/feed/[rkey]/index.astro b/apps/web/src/pages/admin/feed/[rkey]/index.astro index 556b87c..71f3a6f 100644 --- a/apps/web/src/pages/admin/feed/[rkey]/index.astro +++ b/apps/web/src/pages/admin/feed/[rkey]/index.astro @@ -10,10 +10,11 @@ if (!feed) { console.log("404"); return Astro.redirect("/404"); } -const postUris = await feed.handler({ +const res = await feed.handler({ ctx: Astro.locals.at, actorDid: DEFAULT_FEED_ACTOR, }); +const postUris = res.feed.map((p) => p.post); const posts = await getPublicPosts(postUris); const mayClassify = Astro.locals.mayClassify; --- diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro index 608bef4..94e9917 100644 --- a/apps/web/src/pages/index.astro +++ b/apps/web/src/pages/index.astro @@ -2,7 +2,7 @@ import Layout from "~astro/layout.astro"; // Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build // Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. -return Astro.redirect("/feed"); +//return Astro.redirect("/feed"); // TODO: When session is ready --- - Nothing here + Nothing here yet diff --git a/apps/web/src/pages/xrpc/app.bsky.feed.getFeedSkeleton.ts b/apps/web/src/pages/xrpc/app.bsky.feed.getFeedSkeleton.ts index 81784b3..cd5b161 100644 --- a/apps/web/src/pages/xrpc/app.bsky.feed.getFeedSkeleton.ts +++ b/apps/web/src/pages/xrpc/app.bsky.feed.getFeedSkeleton.ts @@ -9,12 +9,13 @@ export const GET: APIRoute = async ({ request: req, locals }) => { // return new Response( // "No authorization header found. This endpoint requires it." // ); - - const FeedParam = new URL(req.url).searchParams.get("feed"); - if (!FeedParam) { + const url = new URL(req.url); + const feedParam = url.searchParams.get("feed"); + if (!feedParam) { return HTTPError("no feed requested"); } - const feedUri = new AtUri(FeedParam); + const cursor = url.searchParams.get("cursor"); + const feedUri = new AtUri(feedParam); // pathname looks like this: /app.bsky.feed.generator/tech-following const rkeyMatch = feedUri.pathname.match(/\/([^/]+)$/); if (!rkeyMatch || rkeyMatch.length < 2) { @@ -26,13 +27,15 @@ export const GET: APIRoute = async ({ request: req, locals }) => { if (!feed) return HTTPError("feed not found"); const actor = jwt?.iss ?? "did:plc:rrrwbar3wv576qpsymwey5p5"; // defaults to me for testing - const posts = (await feed.handler({ ctx, actorDid: actor })).map((p) => ({ - post: p, - })); + const res = await feed.handler({ + ctx, + actorDid: actor, + cursor: cursor ?? undefined, + }); return Response.json({ // Possible to inject posts here later (ads, announcements, etc) - feed: [...posts], + ...res, } satisfies AppBskyFeedGetFeedSkeleton.Response["data"]); }; diff --git a/packages/atproto/domain/get-tech-all-feed.ts b/packages/atproto/domain/get-tech-all-feed.ts index f1b570d..f8e1b8b 100644 --- a/packages/atproto/domain/get-tech-all-feed.ts +++ b/packages/atproto/domain/get-tech-all-feed.ts @@ -1,23 +1,32 @@ -import { and, desc, eq, gte } from "drizzle-orm/expressions"; +import { and, desc, eq, gt, gte } from "drizzle-orm/expressions"; import { postScores, postTable } from "../db/schema"; -import type { FeedHandlerArgs } from "../feeds"; +import type { FeedHandlerArgs, FeedHandlerOutput } from "../feeds"; +import { fromCursor, toCursor } from "../helpers/cursor"; export async function getTechAllFeed( args: FeedHandlerArgs -): Promise> { +): Promise { const { ctx, cursor } = args; const posts = await ctx.db - .select({ id: postTable.id }) + .select({ id: postTable.id, created: postTable.created }) .from(postTable) .innerJoin( postScores, - and(eq(postScores.postId, postTable.id), eq(postScores.tagId, "tech")) + and( + eq(postScores.postId, postTable.id), + eq(postScores.tagId, "tech"), + cursor ? gt(postTable.created, fromCursor(cursor)) : undefined + ) ) .where(gte(postScores.avgScore, 80)) .orderBy(desc(postTable.created)) .limit(30); - return posts.map((post) => post.id); + return { + feed: posts.map((post) => ({ post: post.id })), + cursor: + posts.length > 0 ? toCursor(posts[posts.length - 1].created) : undefined, + }; } diff --git a/packages/atproto/domain/get-tech-following-feed.ts b/packages/atproto/domain/get-tech-following-feed.ts index 984f93b..4f577f1 100644 --- a/packages/atproto/domain/get-tech-following-feed.ts +++ b/packages/atproto/domain/get-tech-following-feed.ts @@ -1,12 +1,13 @@ -import { and, desc, eq, gte } from "drizzle-orm/expressions"; +import { and, desc, eq, gt, gte } from "drizzle-orm/expressions"; import { followTable, postScores, postTable } from "../db/schema"; -import type { FeedHandlerArgs } from "../feeds"; +import type { FeedHandlerArgs, FeedHandlerOutput } from "../feeds"; +import { fromCursor, toCursor } from "../helpers/cursor"; import { getOrUpdateFollows } from "./get-or-update-follows"; export async function getTechFollowingFeed( args: FeedHandlerArgs -): Promise> { - const { ctx, actorDid } = args; +): Promise { + const { ctx, actorDid, cursor } = args; await getOrUpdateFollows(ctx, actorDid); @@ -17,16 +18,26 @@ export async function getTechFollowingFeed( .as("fls"); const posts = await ctx.db - .select({ id: postTable.id }) + .select({ id: postTable.id, created: postTable.created }) .from(postTable) .innerJoin(fls, eq(postTable.authorId, fls.follows)) .innerJoin( postScores, - and(eq(postScores.postId, postTable.id), eq(postScores.tagId, "tech")) + and( + eq(postScores.postId, postTable.id), + eq(postScores.tagId, "tech"), + cursor ? gt(postTable.created, fromCursor(cursor)) : undefined + ) ) .where(gte(postScores.avgScore, 80)) // TODO: Raise this to 80 .orderBy(desc(postTable.created)) .limit(50); - return posts.map((post) => post.id); + return { + feed: posts.map((p) => ({ + post: p.id, + })), + cursor: + posts.length > 0 ? toCursor(posts[posts.length - 1].created) : undefined, + }; } diff --git a/packages/atproto/feeds/index.ts b/packages/atproto/feeds/index.ts index 1781a12..c1f0b4f 100644 --- a/packages/atproto/feeds/index.ts +++ b/packages/atproto/feeds/index.ts @@ -1,4 +1,7 @@ -import type { AppBskyFeedGenerator } from "@atproto/api"; +import type { + AppBskyFeedGenerator, + AppBskyFeedGetFeedSkeleton, +} from "@atproto/api"; import { config } from "../config"; import type { AtContext } from "../context"; import { getTechAllFeed } from "../domain/get-tech-all-feed"; @@ -12,10 +15,12 @@ export type FeedHandlerArgs = { limit?: number; }; +export type FeedHandlerOutput = AppBskyFeedGetFeedSkeleton.Response["data"]; + export type FeedDefinition = { rkey: string; // feed-id (affects url) record: AppBskyFeedGenerator.Record; - handler: (args: FeedHandlerArgs) => Promise>; + handler: (args: FeedHandlerArgs) => Promise; }; export const feeds: Array = [ diff --git a/packages/atproto/helpers/cursor.ts b/packages/atproto/helpers/cursor.ts new file mode 100644 index 0000000..b1fcbc1 --- /dev/null +++ b/packages/atproto/helpers/cursor.ts @@ -0,0 +1,9 @@ +export function toCursor(d: Date): string { + // Convert date to unixtime string, in seconds + return Math.floor(d.getTime() / 1000).toString(); +} + +export function fromCursor(c: string): Date { + // Convert cursor string (unixtime) to Date + return new Date(parseInt(c) * 1000); +}