fix: improve fediverse detection and feed discovery

- fetcher.js: try <link rel="alternate"> HTML discovery before
  falling back to common feed paths (/feed, /rss.xml, etc.), fixing
  subscriptions to sites like Substack, econsoc.mpifg.de, and others
  whose feed URL is advertised via a link element but not at a
  predictable path

- reader.js: extend detectProtocol to recognise more fediverse
  domains (troet., social., hachyderm., infosec.exchange, chaos.social)

- reader.js: don't auto-check Mastodon syndication target for likes
  and reposts — those are handled natively by the AP endpoint; use
  service-name-aware target matching that works for any configured
  Mastodon instance even if its domain isn't in the hardcoded list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-16 12:13:44 +01:00
parent 9c1146d2ea
commit 53aaf50557
2 changed files with 45 additions and 6 deletions

View File

@@ -446,8 +446,11 @@ function detectProtocol(url) {
if (!url || typeof url !== "string") return "web";
const lower = url.toLowerCase();
if (lower.includes("bsky.app") || lower.includes("bluesky")) return "atmosphere";
// Well-known fediverse software domain patterns
if (lower.includes("mastodon.") || lower.includes("mstdn.") || lower.includes("fosstodon.") ||
lower.includes("pleroma.") || lower.includes("misskey.") || lower.includes("pixelfed.")) return "fediverse";
lower.includes("troet.") || lower.includes("social.") || lower.includes("pleroma.") ||
lower.includes("misskey.") || lower.includes("pixelfed.") || lower.includes("hachyderm.") ||
lower.includes("infosec.exchange") || lower.includes("chaos.social")) return "fediverse";
return "web";
}
@@ -510,15 +513,36 @@ export async function compose(request, response) {
? await getSyndicationTargets(application, token)
: [];
// Auto-select syndication target based on interaction URL protocol
// Auto-select syndication target based on interaction URL protocol.
// Likes and reposts on fediverse are handled natively by the AP endpoint —
// never auto-check Mastodon for those action types.
const isLikeOrRepost = !!(likeOf || like || repostOf || repost);
const interactionUrl = ensureString(replyTo || reply || likeOf || like || repostOf || repost);
if (interactionUrl && syndicationTargets.length > 0) {
if (interactionUrl && syndicationTargets.length > 0 && !isLikeOrRepost) {
const protocol = detectProtocol(interactionUrl);
// Build set of Mastodon instance hostnames from configured targets so we
// can match same-instance URLs even if not in the hardcoded pattern list.
const mastodonHostnames = new Set();
for (const t of syndicationTargets) {
if (t.service?.name?.toLowerCase() === "mastodon" && t.service?.url) {
try { mastodonHostnames.add(new URL(t.service.url).hostname.toLowerCase()); } catch { /* ignore */ }
}
}
let interactionHostname = "";
try { interactionHostname = new URL(interactionUrl).hostname.toLowerCase(); } catch { /* ignore */ }
for (const target of syndicationTargets) {
const targetId = (target.uid || target.name || "").toLowerCase();
// Identify a Mastodon target by service name (reliable) or legacy uid/name patterns
const isMastodonTarget =
target.service?.name?.toLowerCase() === "mastodon" ||
targetId.includes("mastodon") ||
targetId.includes("mstdn");
if (protocol === "atmosphere" && (targetId.includes("bluesky") || targetId.includes("bsky"))) {
target.checked = true;
} else if (protocol === "fediverse" && (targetId.includes("mastodon") || targetId.includes("mstdn"))) {
} else if (isMastodonTarget && (protocol === "fediverse" || mastodonHostnames.has(interactionHostname))) {
target.checked = true;
}
}

View File

@@ -164,9 +164,24 @@ export async function fetchAndParseFeed(url, options = {}) {
// Check if we got a parseable feed
const feedType = detectFeedType(result.content, result.contentType);
// If we got ActivityPub or unknown, try common feed paths
// If we got ActivityPub or unknown, try link-based discovery then common paths
if (feedType === "activitypub" || feedType === "unknown") {
const fallbackFeed = await tryCommonFeedPaths(url, options);
// 1. link-based discovery from HTML: parse <link rel="alternate" type="application/rss+xml|atom+xml">
let discoveredFeedUrl;
if (result.content) {
const { discoverFeeds } = await import("./hfeed.js");
const discovered = await discoverFeeds(result.content, url);
const rssOrAtom = discovered.find(
(f) => f.type === "rss" || f.type === "atom" || f.type === "jsonfeed",
);
if (rssOrAtom) discoveredFeedUrl = rssOrAtom.url;
}
// 2. Fall back to common feed paths (/feed, /rss.xml, etc.)
const fallbackFeed = discoveredFeedUrl
? { url: discoveredFeedUrl }
: await tryCommonFeedPaths(url, options);
if (fallbackFeed) {
// Fetch and parse the discovered feed
const feedResult = await fetchFeed(fallbackFeed.url, options);