Files
indiekit-endpoint-microsub/lib/controllers/timeline.js
Ricardo 4819c229cd feat: restore full microsub implementation with reader UI
Restores complete implementation from feat/endpoint-microsub branch:
- Reader UI with views (reader.njk, channel.njk, feeds.njk, etc.)
- Feed polling, parsing, and normalization
- WebSub subscriber
- SSE realtime updates
- Redis caching
- Search indexing
- Media proxy
- Webmention processing
2026-02-06 20:20:25 +01:00

128 lines
3.1 KiB
JavaScript

/**
* Timeline controller
* @module controllers/timeline
*/
import { IndiekitError } from "@indiekit/error";
import { proxyItemImages } from "../media/proxy.js";
import { getChannel } from "../storage/channels.js";
import {
getTimelineItems,
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=<uid>
* @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
const channelDocument = await getChannel(application, channel, userId);
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_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 };