mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
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
129 lines
3.7 KiB
JavaScript
129 lines
3.7 KiB
JavaScript
/**
|
|
* Follow/unfollow controller
|
|
* @module controllers/follow
|
|
*/
|
|
|
|
import { IndiekitError } from "@indiekit/error";
|
|
|
|
import { refreshFeedNow } from "../polling/scheduler.js";
|
|
import { getChannel } from "../storage/channels.js";
|
|
import {
|
|
createFeed,
|
|
deleteFeed,
|
|
getFeedByUrl,
|
|
getFeedsForChannel,
|
|
} from "../storage/feeds.js";
|
|
import { getUserId } from "../utils/auth.js";
|
|
import { createFeedResponse } from "../utils/jf2.js";
|
|
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
import {
|
|
unsubscribe as websubUnsubscribe,
|
|
getCallbackUrl,
|
|
} from "../websub/subscriber.js";
|
|
|
|
/**
|
|
* List followed feeds for a channel
|
|
* GET ?action=follow&channel=<uid>
|
|
* @param {object} request - Express request
|
|
* @param {object} response - Express response
|
|
*/
|
|
export async function list(request, response) {
|
|
const { application } = request.app.locals;
|
|
const userId = getUserId(request);
|
|
const { channel } = request.query;
|
|
|
|
validateChannel(channel);
|
|
|
|
const channelDocument = await getChannel(application, channel, userId);
|
|
if (!channelDocument) {
|
|
throw new IndiekitError("Channel not found", { status: 404 });
|
|
}
|
|
|
|
const feeds = await getFeedsForChannel(application, channelDocument._id);
|
|
const items = feeds.map((feed) => createFeedResponse(feed));
|
|
|
|
response.json({ items });
|
|
}
|
|
|
|
/**
|
|
* Follow a feed URL
|
|
* POST ?action=follow
|
|
* @param {object} request - Express request
|
|
* @param {object} response - Express response
|
|
*/
|
|
export async function follow(request, response) {
|
|
const { application } = request.app.locals;
|
|
const userId = getUserId(request);
|
|
const { channel, url } = request.body;
|
|
|
|
validateChannel(channel);
|
|
validateUrl(url);
|
|
|
|
const channelDocument = await getChannel(application, channel, userId);
|
|
if (!channelDocument) {
|
|
throw new IndiekitError("Channel not found", { status: 404 });
|
|
}
|
|
|
|
// Create feed subscription
|
|
const feed = await createFeed(application, {
|
|
channelId: channelDocument._id,
|
|
url,
|
|
title: undefined, // Will be populated on first fetch
|
|
photo: undefined,
|
|
});
|
|
|
|
// Trigger immediate fetch in background (don't await)
|
|
// This will also discover and subscribe to WebSub hubs
|
|
refreshFeedNow(application, feed._id).catch((error) => {
|
|
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
});
|
|
|
|
response.status(201).json(createFeedResponse(feed));
|
|
}
|
|
|
|
/**
|
|
* Unfollow a feed URL
|
|
* POST ?action=unfollow
|
|
* @param {object} request - Express request
|
|
* @param {object} response - Express response
|
|
*/
|
|
export async function unfollow(request, response) {
|
|
const { application } = request.app.locals;
|
|
const userId = getUserId(request);
|
|
const { channel, url } = request.body;
|
|
|
|
validateChannel(channel);
|
|
validateUrl(url);
|
|
|
|
const channelDocument = await getChannel(application, channel, userId);
|
|
if (!channelDocument) {
|
|
throw new IndiekitError("Channel not found", { status: 404 });
|
|
}
|
|
|
|
// Get feed before deletion to check for WebSub subscription
|
|
const feed = await getFeedByUrl(application, channelDocument._id, url);
|
|
|
|
// Unsubscribe from WebSub hub if active
|
|
if (feed?.websub?.hub) {
|
|
const baseUrl = application.url;
|
|
if (baseUrl) {
|
|
const callbackUrl = getCallbackUrl(baseUrl, feed._id.toString());
|
|
websubUnsubscribe(application, feed, callbackUrl).catch((error) => {
|
|
console.error(
|
|
`[Microsub] WebSub unsubscribe error for ${url}:`,
|
|
error.message,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
const deleted = await deleteFeed(application, channelDocument._id, url);
|
|
if (!deleted) {
|
|
throw new IndiekitError("Feed not found", { status: 404 });
|
|
}
|
|
|
|
response.json({ result: "ok" });
|
|
}
|
|
|
|
export const followController = { list, follow, unfollow };
|