/** * The follow routes, all for what the caller follows. * * - `GET /api/follows/status` — signed in. Answers the ids of the users and * playlists they follow. * - `GET /api/follows/feed/users?page=&limit=&category=` — signed in. Answers a * page of the public dumps of the users they follow, newest first, optionally * in one category. * - `GET /api/follows/feed/playlists?page=&limit=&category=` — signed in. * Answers a page of the public dumps in the public playlists they follow, * most recently added first, optionally in one category. * - `POST /api/follows/users/:userId` — signed in. Follows a user. * - `DELETE /api/follows/users/:userId` — signed in. Stops following a user. * - `POST /api/follows/playlists/:playlistId` — signed in. Follows a public * playlist. * - `DELETE /api/follows/playlists/:playlistId` — signed in. Stops following a * playlist. * * @module */ import { Router } from "@oak/oak"; import { authMiddleware } from "../middleware/auth.ts"; import type { APIResponse, PaginatedData } from "../../shared/api.ts"; import type { Dump } from "../../shared/dump.ts"; import type { FollowStatus } from "../../shared/follow.ts"; import { type Page, parsePagination } from "../lib/pagination.ts"; import { followPlaylist, followUser, getFollowedPlaylistsDumpFeed, getFollowedUsersDumpFeed, getFollowStatus, unfollowPlaylist, unfollowUser, } from "../services/follow-service.ts"; import { getCategoryIdBySlug } from "../services/category-service.ts"; /** * Resolves the optional ?category= filter. Returns the matching category * id, or "missing" when a slug was given but matches nothing (so the feed comes * back empty rather than unfiltered), or undefined when no filter was * requested. */ function resolveCategoryFilter( params: URLSearchParams, ): string | "missing" | undefined { const slug = params.get("category"); if (!slug) return undefined; return getCategoryIdBySlug(slug) ?? "missing"; } /** The feed a filter naming no category yields. */ const EMPTY_FEED: Page = { items: [], hasMore: false }; /** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/follows" }); // Static routes first to prevent Oak matching "status"/"feed" as a :param router.get("/status", authMiddleware, (ctx) => { const status = getFollowStatus(ctx.state.user.userId); const body: APIResponse = { success: true, data: status }; ctx.response.body = body; }); router.get("/feed/users", authMiddleware, (ctx) => { const { page, limit } = parsePagination(ctx.request.url.searchParams); const categoryId = resolveCategoryFilter(ctx.request.url.searchParams); const { items, hasMore } = categoryId === "missing" ? EMPTY_FEED : getFollowedUsersDumpFeed(ctx.state.user.userId, page, limit, categoryId); const data: PaginatedData = { items, hasMore, }; const body: APIResponse> = { success: true, data }; ctx.response.body = body; }); router.get("/feed/playlists", authMiddleware, (ctx) => { const { page, limit } = parsePagination(ctx.request.url.searchParams); const categoryId = resolveCategoryFilter(ctx.request.url.searchParams); const { items, hasMore } = categoryId === "missing" ? EMPTY_FEED : getFollowedPlaylistsDumpFeed( ctx.state.user.userId, page, limit, categoryId, ); const data: PaginatedData = { items, hasMore, }; const body: APIResponse> = { success: true, data }; ctx.response.body = body; }); router.post("/users/:userId", authMiddleware, (ctx) => { followUser(ctx.state.user.userId, ctx.params.userId); ctx.response.status = 204; }); router.delete("/users/:userId", authMiddleware, (ctx) => { unfollowUser(ctx.state.user.userId, ctx.params.userId); ctx.response.status = 204; }); router.post("/playlists/:playlistId", authMiddleware, (ctx) => { followPlaylist(ctx.state.user.userId, ctx.params.playlistId); ctx.response.status = 204; }); router.delete("/playlists/:playlistId", authMiddleware, (ctx) => { unfollowPlaylist(ctx.state.user.userId, ctx.params.playlistId); ctx.response.status = 204; }); export default router;