import { audit } from '../helpers/audit.ts' import { actorFrom, mayOnPage, unlockedFor } from './pages.ts' import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' import type { RatingMode } from '../models/pageRatings.ts' import type { Page } from '../models/pages.ts' const pageParams = { type: 'object', properties: { siteId: { type: 'string', format: 'uuid' }, pageId: { type: 'string', format: 'uuid' } }, required: ['siteId', 'pageId'] } const ratingResponse = { description: 'How the page is rated now, and what the caller gave it', type: 'object', properties: { ok: { type: 'boolean' }, rating: { $ref: 'PageRatingSummary#' }, value: { type: 'integer', description: 'The caller’s own rating, 0 once withdrawn.' } } } /** * The page being rated and the scale it is rated on, or a refusal. * * Rating belongs to an account — one opinion per person needs a person to hang it on — so an * anonymous request never gets as far as the page. Beyond that it is the same gate as reading: a page * somebody may not read is answered as though it were not there. Both switches have to be on, the * site's ratings mode and the page's own `allowRatings`. */ async function loadRateable( req: FastifyRequest<{ Params: { siteId: string; pageId: string } }>, reply: FastifyReply ): Promise<{ userId: string; mode: RatingMode; page: Page } | null> { const actor = actorFrom(req) if (!actor) { reply.unauthorized('Rating a page requires a logged in user.') return null } const mode = WIKI.models.pageRatings.modeFor(req.params.siteId) if (!mode) { reply.forbidden('Ratings are turned off for this site.') return null } const page = await WIKI.models.pages.getPage({ siteId: req.params.siteId, id: req.params.pageId, unlocked: (id: string) => unlockedFor(req, id) }) if (!page || !mayOnPage(req, 'read:pages', page)) { reply.notFound('This page does not exist.') return null } if (!page.allowRatings) { reply.forbidden('Ratings are turned off for this page.') return null } return { userId: actor.id, mode, page } } /** * Page Ratings API Routes * * A reader's thumbs or stars for a page. How a page is rated so far comes with the page itself * (`rating`, and the reader's own under `viewer.rating`), so there is no read route here. * * No route-level `permissions` on either: the test is `read:pages` on the page, which a group's page * rules decide and the hook in `index.ts` cannot see. */ async function routes(app: FastifyInstance) { /** * RATE A PAGE */ app.put<{ Params: { siteId: string; pageId: string }; Body: { value: number } }>( '/sites/:siteId/pages/:pageId/rating', { schema: { summary: 'Rate a page', description: 'Records the caller’s rating of the page, replacing any they gave it before. The scale is the site’s ratings mode: `1` or `-1` for thumbs, `1` to `5` for stars.\n\nNeeds a logged in user who may read the page, with ratings on for both the site and the page.', tags: ['Pages'], params: pageParams, body: { type: 'object', required: ['value'], properties: { value: { type: 'integer', minimum: -1, maximum: 5 } } }, response: { 200: ratingResponse } } }, async (req, reply) => { const target = await loadRateable(req, reply) if (!target) { return reply } const { userId, mode, page } = target if (!WIKI.models.pageRatings.isValid(mode, req.body.value)) { return reply.badRequest( mode === 'thumbs' ? 'A thumbs rating is 1 or -1.' : 'A star rating is a whole number from 1 to 5.' ) } const totals = await WIKI.models.pageRatings.rate({ pageId: page.id, userId, mode, value: req.body.value }) await audit(req, 'page', 'ratePage', { pageId: page.id, siteId: req.params.siteId, locale: page.locale, path: page.path, mode, value: req.body.value }) return { ok: true, rating: WIKI.models.pageRatings.summaryFromCache(totals, mode), value: req.body.value } } ) /** * WITHDRAW A RATING */ app.delete<{ Params: { siteId: string; pageId: string } }>( '/sites/:siteId/pages/:pageId/rating', { schema: { summary: 'Withdraw a rating', description: 'Forgets the caller’s rating of the page. A page they had not rated answers the same way, since the outcome asked for already holds.', tags: ['Pages'], params: pageParams, response: { 200: ratingResponse } } }, async (req, reply) => { const target = await loadRateable(req, reply) if (!target) { return reply } const { userId, mode, page } = target const totals = await WIKI.models.pageRatings.unrate({ pageId: page.id, userId }) await audit(req, 'page', 'unratePage', { pageId: page.id, siteId: req.params.siteId, locale: page.locale, path: page.path }) return { ok: true, rating: WIKI.models.pageRatings.summaryFromCache(totals, mode), value: 0 } } ) } export default routes