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:
@@ -446,8 +446,11 @@ function detectProtocol(url) {
|
|||||||
if (!url || typeof url !== "string") return "web";
|
if (!url || typeof url !== "string") return "web";
|
||||||
const lower = url.toLowerCase();
|
const lower = url.toLowerCase();
|
||||||
if (lower.includes("bsky.app") || lower.includes("bluesky")) return "atmosphere";
|
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.") ||
|
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";
|
return "web";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,15 +513,36 @@ export async function compose(request, response) {
|
|||||||
? await getSyndicationTargets(application, token)
|
? 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);
|
const interactionUrl = ensureString(replyTo || reply || likeOf || like || repostOf || repost);
|
||||||
if (interactionUrl && syndicationTargets.length > 0) {
|
if (interactionUrl && syndicationTargets.length > 0 && !isLikeOrRepost) {
|
||||||
const protocol = detectProtocol(interactionUrl);
|
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) {
|
for (const target of syndicationTargets) {
|
||||||
const targetId = (target.uid || target.name || "").toLowerCase();
|
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"))) {
|
if (protocol === "atmosphere" && (targetId.includes("bluesky") || targetId.includes("bsky"))) {
|
||||||
target.checked = true;
|
target.checked = true;
|
||||||
} else if (protocol === "fediverse" && (targetId.includes("mastodon") || targetId.includes("mstdn"))) {
|
} else if (isMastodonTarget && (protocol === "fediverse" || mastodonHostnames.has(interactionHostname))) {
|
||||||
target.checked = true;
|
target.checked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -164,9 +164,24 @@ export async function fetchAndParseFeed(url, options = {}) {
|
|||||||
// Check if we got a parseable feed
|
// Check if we got a parseable feed
|
||||||
const feedType = detectFeedType(result.content, result.contentType);
|
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") {
|
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) {
|
if (fallbackFeed) {
|
||||||
// Fetch and parse the discovered feed
|
// Fetch and parse the discovered feed
|
||||||
const feedResult = await fetchFeed(fallbackFeed.url, options);
|
const feedResult = await fetchFeed(fallbackFeed.url, options);
|
||||||
|
|||||||
Reference in New Issue
Block a user