Files
gerbeur/api/routes/follows.ts

97 lines
3.5 KiB
TypeScript

/**
* 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<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/me" });
router.get("/follow", authMiddleware, (ctx) => {
const status = getFollowStatus(ctx.state.user.userId);
const body: APIResponse<FollowStatus> = { 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(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<PaginatedData<Dump>> = {
success: true,
data: { items, hasMore },
};
ctx.response.body = body;
});
export default router;