refactor: unify isDirectMessage and computeVisibility into single function
Deploy Indiekit Server / deploy (push) Successful in 1m18s
Deploy Indiekit Server / deploy (push) Successful in 1m18s
computeVisibility() now accepts optional actor context ({ourActorUrl,
followersUrl}) to correctly return "direct" instead of "private" for DMs.
Replaces the separate isDirectMessage() guard with a computeVisibility
check, eliminating the dual-function inconsistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* Patch: Unify isDirectMessage() and computeVisibility() in inbox-handlers.js.
|
||||||
|
*
|
||||||
|
* Problem: Two overlapping functions for DM detection. computeVisibility()
|
||||||
|
* returns "private" for DMs because it lacks actor context. isDirectMessage()
|
||||||
|
* correctly detects DMs but is separate.
|
||||||
|
*
|
||||||
|
* Fix: Extend computeVisibility() to accept optional {ourActorUrl, followersUrl}.
|
||||||
|
* When provided and DM conditions are met, return "direct" instead of "private".
|
||||||
|
* Replace the standalone isDirectMessage() guard with computeVisibility() check.
|
||||||
|
* Update all call sites inside handleCreate() to pass actor context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { access, readFile, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
const AP_BASE = "@rmdes/indiekit-endpoint-activitypub";
|
||||||
|
const AP_ROOTS = [
|
||||||
|
`node_modules/${AP_BASE}`,
|
||||||
|
`node_modules/@indiekit/indiekit/node_modules/${AP_BASE}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
function apPath(rel) {
|
||||||
|
return AP_ROOTS.map(r => `${r}/${rel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileExists(p) {
|
||||||
|
try { await access(p); return true; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCRIPT = "patch-ap-unify-dm-visibility";
|
||||||
|
const MARKER = "// [patch] unify-dm-visibility";
|
||||||
|
const CANDIDATES = apPath("lib/inbox-handlers.js");
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
// ── Patch 1: Replace computeVisibility to accept actor context ──────────────
|
||||||
|
|
||||||
|
const OLD_COMPUTE_VISIBILITY = `function computeVisibility(object) {
|
||||||
|
const to = new Set((object.toIds || []).map((u) => u.href));
|
||||||
|
const cc = new Set((object.ccIds || []).map((u) => u.href));
|
||||||
|
|
||||||
|
if (to.has(PUBLIC)) return "public";
|
||||||
|
if (cc.has(PUBLIC)) return "unlisted";
|
||||||
|
// Without knowing the remote actor's followers URL, we can't distinguish
|
||||||
|
// "private" (followers-only) from "direct". Both are non-public.
|
||||||
|
if (to.size > 0 || cc.size > 0) return "private";
|
||||||
|
return "direct";
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const NEW_COMPUTE_VISIBILITY = `function computeVisibility(object, actorContext) { ${MARKER}
|
||||||
|
const to = new Set((object.toIds || []).map((u) => u.href)); ${MARKER}
|
||||||
|
const cc = new Set((object.ccIds || []).map((u) => u.href)); ${MARKER}
|
||||||
|
${MARKER}
|
||||||
|
if (to.has(PUBLIC)) return "public"; ${MARKER}
|
||||||
|
if (cc.has(PUBLIC)) return "unlisted"; ${MARKER}
|
||||||
|
${MARKER}
|
||||||
|
// When actor context is available, use isDirectMessage logic to distinguish ${MARKER}
|
||||||
|
// "direct" (addressed to specific actors only) from "private" (followers-only). ${MARKER}
|
||||||
|
if (actorContext?.ourActorUrl) { ${MARKER}
|
||||||
|
const allAddressed = [ ${MARKER}
|
||||||
|
...to, ...cc, ${MARKER}
|
||||||
|
...(object.btoIds || []).map((u) => u.href), ${MARKER}
|
||||||
|
...(object.bccIds || []).map((u) => u.href), ${MARKER}
|
||||||
|
]; ${MARKER}
|
||||||
|
const hasPublic = allAddressed.some((u) => u === PUBLIC || u === "as:Public"); ${MARKER}
|
||||||
|
const hasFollowers = actorContext.followersUrl && allAddressed.includes(actorContext.followersUrl); ${MARKER}
|
||||||
|
if (!hasPublic && !hasFollowers && allAddressed.includes(actorContext.ourActorUrl)) { ${MARKER}
|
||||||
|
return "direct"; ${MARKER}
|
||||||
|
} ${MARKER}
|
||||||
|
} ${MARKER}
|
||||||
|
${MARKER}
|
||||||
|
// Without actor context, can't distinguish "private" from "direct". ${MARKER}
|
||||||
|
if (to.size > 0 || cc.size > 0) return "private"; ${MARKER}
|
||||||
|
return "direct"; ${MARKER}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// ── Patch 2: Replace isDirectMessage() guard with computeVisibility() check ─
|
||||||
|
|
||||||
|
const OLD_DM_GUARD = ` if (isDirectMessage(object, ourActorUrl, followersUrl)) {`;
|
||||||
|
|
||||||
|
const NEW_DM_GUARD = ` if (computeVisibility(object, { ourActorUrl, followersUrl }) === "direct") { ${MARKER}`;
|
||||||
|
|
||||||
|
// ── Patch 3: Update computeVisibility call sites inside handleCreate ────────
|
||||||
|
// These are after the DM guard, so the object is NOT a DM. But passing context
|
||||||
|
// lets computeVisibility correctly return "private" vs "direct" for edge cases.
|
||||||
|
// The 4 call sites all follow the pattern: timelineItem.visibility = computeVisibility(object);
|
||||||
|
// We need to add actor context only to those inside handleCreate (lines 812, 842, 866)
|
||||||
|
// and handleAnnounce (line 586). The fetchReplyChain call (line 212) has no context.
|
||||||
|
|
||||||
|
// For handleCreate calls — ourActorUrl and followersUrl are in scope
|
||||||
|
const OLD_VISIBILITY_CALL = `timelineItem.visibility = computeVisibility(object);`;
|
||||||
|
const NEW_VISIBILITY_CALL_WITH_CTX = `timelineItem.visibility = computeVisibility(object, { ourActorUrl, followersUrl }); ${MARKER}`;
|
||||||
|
|
||||||
|
for (const f of CANDIDATES) {
|
||||||
|
if (!(await fileExists(f))) continue;
|
||||||
|
const src = await readFile(f, "utf8");
|
||||||
|
if (src.includes(MARKER)) {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: already applied in ${f}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = src;
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
// Patch 1: Replace computeVisibility function
|
||||||
|
if (updated.includes(OLD_COMPUTE_VISIBILITY)) {
|
||||||
|
updated = updated.replace(OLD_COMPUTE_VISIBILITY, NEW_COMPUTE_VISIBILITY);
|
||||||
|
changed = true;
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: replaced computeVisibility()`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[postinstall] ${SCRIPT}: computeVisibility() anchor not found in ${f}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch 2: Replace isDirectMessage guard
|
||||||
|
if (updated.includes(OLD_DM_GUARD)) {
|
||||||
|
updated = updated.replace(OLD_DM_GUARD, NEW_DM_GUARD);
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: replaced isDirectMessage() guard`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[postinstall] ${SCRIPT}: isDirectMessage() guard not found in ${f}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch 3: Update computeVisibility call sites with actor context
|
||||||
|
// We need to be selective — only replace calls inside handleCreate/handleAnnounce
|
||||||
|
// where ourActorUrl and followersUrl are in scope.
|
||||||
|
// The fetchReplyChain call (line ~212) does NOT have these in scope — leave it.
|
||||||
|
//
|
||||||
|
// Strategy: split file at the handleCreate function boundary and only replace
|
||||||
|
// within that section. handleAnnounce also has the vars via ctx but through a
|
||||||
|
// different scope — safer to leave those unchanged since boosts aren't DMs.
|
||||||
|
|
||||||
|
// Find the handleCreate function start
|
||||||
|
const handleCreateStart = updated.indexOf("export async function handleCreate(");
|
||||||
|
if (handleCreateStart > -1) {
|
||||||
|
const before = updated.substring(0, handleCreateStart);
|
||||||
|
const after = updated.substring(handleCreateStart);
|
||||||
|
|
||||||
|
// Replace all computeVisibility(object) calls within handleCreate
|
||||||
|
const afterPatched = after.replaceAll(OLD_VISIBILITY_CALL, NEW_VISIBILITY_CALL_WITH_CTX);
|
||||||
|
if (afterPatched !== after) {
|
||||||
|
updated = before + afterPatched;
|
||||||
|
const count = (afterPatched.match(/unify-dm-visibility/g) || []).length -
|
||||||
|
(after.match(/unify-dm-visibility/g) || []).length;
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: updated ${count} computeVisibility() call site(s) in handleCreate`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed && updated !== src) {
|
||||||
|
await writeFile(f, updated, "utf8");
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: applied to ${f}`);
|
||||||
|
total++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: no target file found or no changes needed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);
|
||||||
Reference in New Issue
Block a user