121 lines
4.2 KiB
TypeScript
121 lines
4.2 KiB
TypeScript
/**
|
|
* Serves the endpoints for what the caller follows, and the index's "followed"
|
|
* tab built from it. What another user follows is served by `routes/users.ts`.
|
|
*
|
|
* - `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";
|
|
|
|
/**
|
|
* Returns the id of the category the `?category=` slug names, undefined without
|
|
* one, or "missing" when it names none, so that the feed comes back empty
|
|
* rather than unfiltered.
|
|
*/
|
|
function resolveCategoryFilter(
|
|
params: URLSearchParams,
|
|
): string | "missing" | undefined {
|
|
const slug = params.get("category");
|
|
if (!slug) return undefined;
|
|
return getCategoryIdBySlug(slug) ?? "missing";
|
|
}
|
|
|
|
const EMPTY_FEED: Page<Dump> = { 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/follows" });
|
|
|
|
router.get("/status", authMiddleware, (ctx) => {
|
|
const status = getFollowStatus(ctx.state.user.userId);
|
|
const body: APIResponse<FollowStatus> = { 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<Dump> = {
|
|
items,
|
|
hasMore,
|
|
};
|
|
const body: APIResponse<PaginatedData<Dump>> = { 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<Dump> = {
|
|
items,
|
|
hasMore,
|
|
};
|
|
const body: APIResponse<PaginatedData<Dump>> = { 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;
|