/** * Serves what the caller follows, and the index's "follow" tab built from it. * What another user follows is served by `routes/users.ts`. * * - `GET /api/me/follow` — signed in. Answers the ids of the users and * playlists they follow. * - `PUT /api/me/follow/users/:userId` — signed in. Follows a user. * - `DELETE /api/me/follow/users/:userId` — signed in. Stops following a user. * - `PUT /api/me/follow/playlists/:playlistId` — signed in. Follows a public * playlist. * - `DELETE /api/me/follow/playlists/:playlistId` — signed in. Stops following * a playlist. * - `GET /api/me/feed?from=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/me/feed?from=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. * * @module */ import { Router } from "@oak/oak"; import { authMiddleware } from "../middleware/auth.ts"; import { APIException } from "../lib/api-exception.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 { resolveCategoryFilter } from "../services/category-service.ts"; const EMPTY_FEED: Page = { items: [], hasMore: false }; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ const router: Router = new Router({ prefix: "/api/me" }); router.get("/follow", authMiddleware, (ctx) => { const status = getFollowStatus(ctx.state.user.userId); const body: APIResponse = { success: true, data: status }; ctx.response.body = body; }); router.put("/follow/users/:userId", authMiddleware, (ctx) => { followUser(ctx.state.user.userId, ctx.params.userId); ctx.response.status = 204; }); router.delete("/follow/users/:userId", authMiddleware, (ctx) => { unfollowUser(ctx.state.user.userId, ctx.params.userId); ctx.response.status = 204; }); router.put("/follow/playlists/:playlistId", authMiddleware, (ctx) => { followPlaylist(ctx.state.user.userId, ctx.params.playlistId); ctx.response.status = 204; }); router.delete("/follow/playlists/:playlistId", authMiddleware, (ctx) => { unfollowPlaylist(ctx.state.user.userId, ctx.params.playlistId); ctx.response.status = 204; }); router.get("/feed", authMiddleware, (ctx) => { const params = ctx.request.url.searchParams; const from = params.get("from"); if (from !== "users" && from !== "playlists") { throw new APIException(400, "from must be users or playlists"); } const { page, limit } = parsePagination(params); const categoryId = resolveCategoryFilter("dump", params); const userId = ctx.state.user.userId; const { items, hasMore } = categoryId === "missing" ? EMPTY_FEED : from === "users" ? getFollowedUsersDumpFeed(userId, page, limit, categoryId) : getFollowedPlaylistsDumpFeed(userId, page, limit, categoryId); const body: APIResponse> = { success: true, data: { items, hasMore }, }; ctx.response.body = body; }); export default router;