/** * Timeline controller * @module controllers/timeline */ import { IndiekitError } from "@indiekit/error"; import { proxyItemImages } from "../media/proxy.js"; import { getChannel, getChannelById } from "../storage/channels.js"; import { getTimelineItems, markFeedItemsRead, markItemsRead, markItemsUnread, removeItems, } from "../storage/items.js"; import { getUserId } from "../utils/auth.js"; import { validateChannel, validateEntries, parseArrayParameter as parseArrayParametereter, } from "../utils/validation.js"; /** * Get timeline items for a channel * GET ?action=timeline&channel= * @param {object} request - Express request * @param {object} response - Express response */ export async function get(request, response) { const { application } = request.app.locals; const userId = getUserId(request); const { channel, before, after, limit } = request.query; validateChannel(channel); // Verify channel exists const channelDocument = await getChannel(application, channel, userId); if (!channelDocument) { throw new IndiekitError("Channel not found", { status: 404, }); } const timeline = await getTimelineItems(application, channelDocument._id, { before, after, limit, userId, }); // Proxy images if application URL is available const baseUrl = application.url; if (baseUrl && timeline.items) { timeline.items = timeline.items.map((item) => proxyItemImages(item, baseUrl), ); } response.json(timeline); } /** * Handle timeline actions (mark_read, mark_unread, remove) * POST ?action=timeline * @param {object} request - Express request * @param {object} response - Express response */ export async function action(request, response) { const { application } = request.app.locals; const userId = getUserId(request); const { method, channel } = request.body; validateChannel(channel); // Verify channel exists — try by UID first, fall back to ObjectId // (timeline view may send ObjectId string for items from orphan channels) let channelDocument = await getChannel(application, channel, userId); if (!channelDocument) { try { channelDocument = await getChannelById(application, channel); } catch { // Invalid ObjectId format — channel string is not a valid ObjectId } } if (!channelDocument) { throw new IndiekitError("Channel not found", { status: 404, }); } // Get entry IDs from request const entries = parseArrayParametereter(request.body, "entry"); switch (method) { case "mark_read": { validateEntries(entries); const count = await markItemsRead( application, channelDocument._id, entries, userId, ); return response.json({ result: "ok", updated: count }); } case "mark_read_source": { const feedId = request.body.feed; if (!feedId) { throw new IndiekitError("feed parameter required", { status: 400, }); } const count = await markFeedItemsRead( application, channelDocument._id, feedId, userId, ); return response.json({ result: "ok", updated: count }); } case "mark_unread": { validateEntries(entries); const count = await markItemsUnread( application, channelDocument._id, entries, userId, ); return response.json({ result: "ok", updated: count }); } case "remove": { validateEntries(entries); const count = await removeItems( application, channelDocument._id, entries, ); return response.json({ result: "ok", removed: count }); } default: { throw new IndiekitError(`Invalid timeline method: ${method}`, { status: 400, }); } } } export const timelineController = { get, action };