chore(patches): add mastodon disconnect flow and runtime guards
This commit is contained in:
+2
-2
@@ -4,8 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs",
|
||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-conversations-mastodon-disconnect.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs",
|
||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-conversations-mastodon-disconnect.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const conversationsIndexCandidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-conversations/index.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-conversations/index.js",
|
||||
];
|
||||
|
||||
const conversationsControllerCandidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-conversations/lib/controllers/conversations.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-conversations/lib/controllers/conversations.js",
|
||||
];
|
||||
|
||||
const conversationsSchedulerCandidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-conversations/lib/polling/scheduler.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-conversations/lib/polling/scheduler.js",
|
||||
];
|
||||
|
||||
const conversationsViewCandidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-conversations/views/conversations.njk",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-conversations/views/conversations.njk",
|
||||
];
|
||||
|
||||
const patchSpecs = [
|
||||
{
|
||||
name: "conversations-index-mastodon-disconnect-routes",
|
||||
candidates: conversationsIndexCandidates,
|
||||
oldSnippet: ` // Manual poll trigger (admin only)
|
||||
router.post("/poll", conversationsController.triggerPoll);
|
||||
|
||||
return router;`,
|
||||
newSnippet: ` // Manual poll trigger (admin only)
|
||||
router.post("/poll", conversationsController.triggerPoll);
|
||||
router.post("/mastodon/logout", conversationsController.logoutMastodon);
|
||||
router.post("/mastodon/reconnect", conversationsController.reconnectMastodon);
|
||||
|
||||
return router;`,
|
||||
},
|
||||
{
|
||||
name: "conversations-dashboard-connection-state",
|
||||
candidates: conversationsControllerCandidates,
|
||||
marker: "const connectionState = {",
|
||||
oldSnippet: ` // Get stats
|
||||
const totalItems = await getConversationCount(application);`,
|
||||
newSnippet: ` const connectionState = {
|
||||
mastodonEnabled:
|
||||
!!config.mastodonEnabled && !pollState?.mastodon_disabled,
|
||||
blueskyEnabled: !!config.blueskyEnabled,
|
||||
activitypubEnabled: !!config.activitypubEnabled,
|
||||
};
|
||||
|
||||
// Get stats
|
||||
const totalItems = await getConversationCount(application);`,
|
||||
},
|
||||
{
|
||||
name: "conversations-dashboard-render-connection-state",
|
||||
candidates: conversationsControllerCandidates,
|
||||
oldSnippet: ` config,
|
||||
pollState,
|
||||
totalItems,`,
|
||||
newSnippet: ` config,
|
||||
pollState,
|
||||
connectionState,
|
||||
totalItems,`,
|
||||
},
|
||||
{
|
||||
name: "conversations-dashboard-error-render-connection-state",
|
||||
candidates: conversationsControllerCandidates,
|
||||
oldSnippet: ` config: {},
|
||||
totalItems: 0,`,
|
||||
newSnippet: ` config: {},
|
||||
connectionState: {
|
||||
mastodonEnabled: false,
|
||||
blueskyEnabled: false,
|
||||
activitypubEnabled: false,
|
||||
},
|
||||
totalItems: 0,`,
|
||||
},
|
||||
{
|
||||
name: "conversations-status-mastodon-disabled-flag",
|
||||
candidates: conversationsControllerCandidates,
|
||||
oldSnippet: ` const totalItems = await getConversationCount(application);
|
||||
|
||||
response.json({
|
||||
status: "ok",
|
||||
mastodon: {
|
||||
enabled: !!config.mastodonEnabled,`,
|
||||
newSnippet: ` const totalItems = await getConversationCount(application);
|
||||
const mastodonEnabled =
|
||||
!!config.mastodonEnabled && !pollState?.mastodon_disabled;
|
||||
|
||||
response.json({
|
||||
status: "ok",
|
||||
mastodon: {
|
||||
enabled: mastodonEnabled,
|
||||
disabledByAdmin: !!pollState?.mastodon_disabled,`,
|
||||
},
|
||||
{
|
||||
name: "conversations-controller-mastodon-disconnect-handlers",
|
||||
candidates: conversationsControllerCandidates,
|
||||
marker: "async function logoutMastodon(request, response)",
|
||||
oldSnippet: `/**
|
||||
* Ingest a webmention`,
|
||||
newSnippet: `/**
|
||||
* Disable Mastodon polling (dashboard logout/disconnect action)
|
||||
* POST /conversations/mastodon/logout
|
||||
*/
|
||||
async function logoutMastodon(request, response) {
|
||||
const { application } = request.app.locals;
|
||||
const config = application?.conversations || {};
|
||||
const stateCollection = application?.collections?.get("conversation_state");
|
||||
|
||||
try {
|
||||
if (stateCollection) {
|
||||
await stateCollection.findOneAndUpdate(
|
||||
{ _id: "poll_cursors" },
|
||||
{
|
||||
$set: {
|
||||
mastodon_disabled: true,
|
||||
mastodon_last_error: null,
|
||||
mastodon_last_poll: new Date().toISOString(),
|
||||
mastodon_last_disabled_at: new Date().toISOString(),
|
||||
},
|
||||
$unset: {
|
||||
mastodon_since_id: "",
|
||||
},
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
response.redirect((config.mountPath || "/conversations") + "?mastodon=logged_out");
|
||||
} catch (error) {
|
||||
console.error("[Conversations] Mastodon logout error:", error.message);
|
||||
response.redirect(
|
||||
(config.mountPath || "/conversations") + "?error=mastodon_logout_failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enable Mastodon polling after dashboard disconnect
|
||||
* POST /conversations/mastodon/reconnect
|
||||
*/
|
||||
async function reconnectMastodon(request, response) {
|
||||
const { application } = request.app.locals;
|
||||
const config = application?.conversations || {};
|
||||
const stateCollection = application?.collections?.get("conversation_state");
|
||||
|
||||
try {
|
||||
if (stateCollection) {
|
||||
await stateCollection.findOneAndUpdate(
|
||||
{ _id: "poll_cursors" },
|
||||
{
|
||||
$unset: {
|
||||
mastodon_disabled: "",
|
||||
},
|
||||
$set: {
|
||||
mastodon_last_error: null,
|
||||
mastodon_last_poll: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
response.redirect((config.mountPath || "/conversations") + "?mastodon=reconnected");
|
||||
} catch (error) {
|
||||
console.error("[Conversations] Mastodon reconnect error:", error.message);
|
||||
response.redirect(
|
||||
(config.mountPath || "/conversations") + "?error=mastodon_reconnect_failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest a webmention`,
|
||||
},
|
||||
{
|
||||
name: "conversations-controller-export-disconnect-handlers",
|
||||
candidates: conversationsControllerCandidates,
|
||||
oldSnippet: ` triggerPoll,
|
||||
ingest,`,
|
||||
newSnippet: ` triggerPoll,
|
||||
logoutMastodon,
|
||||
reconnectMastodon,
|
||||
ingest,`,
|
||||
},
|
||||
{
|
||||
name: "conversations-scheduler-mastodon-disabled-check",
|
||||
candidates: conversationsSchedulerCandidates,
|
||||
oldSnippet: ` const mastodonToken = process.env.MASTODON_ACCESS_TOKEN;
|
||||
const hasMastodon = mastodonUrl && mastodonToken;`,
|
||||
newSnippet: ` const mastodonToken = process.env.MASTODON_ACCESS_TOKEN;
|
||||
const mastodonDisabled = state.mastodon_disabled === true;
|
||||
const hasMastodon = !mastodonDisabled && mastodonUrl && mastodonToken;`,
|
||||
},
|
||||
{
|
||||
name: "conversations-scheduler-mastodon-403-backoff",
|
||||
candidates: conversationsSchedulerCandidates,
|
||||
oldSnippet: ` if (error.status === 429 || error.status === 401) {`,
|
||||
newSnippet: ` if (error.status === 429 || error.status === 401 || error.status === 403) {`,
|
||||
},
|
||||
{
|
||||
name: "conversations-view-mastodon-connection-state",
|
||||
candidates: conversationsViewCandidates,
|
||||
oldSnippet: "config.mastodonEnabled",
|
||||
newSnippet: "connectionState.mastodonEnabled",
|
||||
replaceAll: true,
|
||||
},
|
||||
{
|
||||
name: "conversations-view-bluesky-connection-state",
|
||||
candidates: conversationsViewCandidates,
|
||||
oldSnippet: "config.blueskyEnabled",
|
||||
newSnippet: "connectionState.blueskyEnabled",
|
||||
replaceAll: true,
|
||||
},
|
||||
{
|
||||
name: "conversations-view-activitypub-connection-state",
|
||||
candidates: conversationsViewCandidates,
|
||||
oldSnippet: "config.activitypubEnabled",
|
||||
newSnippet: "connectionState.activitypubEnabled",
|
||||
replaceAll: true,
|
||||
},
|
||||
{
|
||||
name: "conversations-view-mastodon-logout-button",
|
||||
candidates: conversationsViewCandidates,
|
||||
marker: "action=\"{{ baseUrl }}/mastodon/logout\"",
|
||||
oldSnippet: ` <p style="font-size: 0.85em; margin: 0.25rem 0">
|
||||
{{ platformCounts.mastodon or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
|
||||
</p>`,
|
||||
newSnippet: ` <p style="font-size: 0.85em; margin: 0.25rem 0">
|
||||
{{ platformCounts.mastodon or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
|
||||
</p>
|
||||
<form method="post" action="{{ baseUrl }}/mastodon/logout" style="margin-top: 0.75rem">
|
||||
<button type="submit" class="button">Logout Mastodon</button>
|
||||
</form>`,
|
||||
},
|
||||
{
|
||||
name: "conversations-view-mastodon-disabled-state",
|
||||
candidates: conversationsViewCandidates,
|
||||
marker: "Mastodon polling is disconnected for this dashboard.",
|
||||
oldSnippet: ` {% else %}
|
||||
<p style="font-size: 0.85em; color: #6b7280; margin: 0.25rem 0">
|
||||
{{ __("conversations.dashboard.mastodonHint") }}
|
||||
</p>
|
||||
{% endif %}`,
|
||||
newSnippet: ` {% else %}
|
||||
{% if pollState and pollState.mastodon_disabled %}
|
||||
<p style="font-size: 0.85em; color: #6b7280; margin: 0.25rem 0">
|
||||
Mastodon polling is disconnected for this dashboard.
|
||||
</p>
|
||||
<form method="post" action="{{ baseUrl }}/mastodon/reconnect" style="margin-top: 0.75rem">
|
||||
<button type="submit" class="button">Reconnect Mastodon</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p style="font-size: 0.85em; color: #6b7280; margin: 0.25rem 0">
|
||||
{{ __("conversations.dashboard.mastodonHint") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}`,
|
||||
},
|
||||
];
|
||||
|
||||
async function exists(filePath) {
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const checkedFiles = new Set();
|
||||
const patchedFiles = new Set();
|
||||
|
||||
for (const spec of patchSpecs) {
|
||||
let foundAnyTarget = false;
|
||||
|
||||
for (const filePath of spec.candidates) {
|
||||
if (!(await exists(filePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foundAnyTarget = true;
|
||||
checkedFiles.add(filePath);
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
|
||||
if (spec.marker && source.includes(spec.marker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!source.includes(spec.oldSnippet)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let updated;
|
||||
if (spec.replaceAll) {
|
||||
updated = source.split(spec.oldSnippet).join(spec.newSnippet);
|
||||
} else {
|
||||
updated = source.replace(spec.oldSnippet, spec.newSnippet);
|
||||
}
|
||||
|
||||
if (updated === source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
patchedFiles.add(filePath);
|
||||
}
|
||||
|
||||
if (!foundAnyTarget) {
|
||||
console.log(`[postinstall] ${spec.name}: no target files found`);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkedFiles.size === 0) {
|
||||
console.log("[postinstall] No conversations mastodon disconnect files found");
|
||||
} else if (patchedFiles.size === 0) {
|
||||
console.log("[postinstall] conversations mastodon disconnect patches already applied");
|
||||
} else {
|
||||
console.log(
|
||||
`[postinstall] Patched conversations mastodon disconnect in ${patchedFiles.size}/${checkedFiles.size} file(s)`,
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,34 @@ const patchSpecs = [
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/sync.js",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "funkwhale-latest-date-coercion",
|
||||
marker: "Invalid listenedAt in latest record; falling back to full sync",
|
||||
oldSnippet: ` const latest = await collection.findOne({}, { sort: { listenedAt: -1 } });
|
||||
const latestDate = latest?.listenedAt || new Date(0);
|
||||
|
||||
console.log(
|
||||
\`[Funkwhale] Syncing listenings since: \${latestDate.toISOString()}\`
|
||||
);`,
|
||||
newSnippet: ` const latest = await collection.findOne({}, { sort: { listenedAt: -1 } });
|
||||
const latestRawDate = latest?.listenedAt;
|
||||
let latestDate = latestRawDate ? new Date(latestRawDate) : new Date(0);
|
||||
|
||||
if (Number.isNaN(latestDate.getTime())) {
|
||||
console.warn(
|
||||
"[Funkwhale] Invalid listenedAt in latest record; falling back to full sync"
|
||||
);
|
||||
latestDate = new Date(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
\`[Funkwhale] Syncing listenings since: \${latestDate.toISOString()}\`
|
||||
);`,
|
||||
candidates: [
|
||||
"node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/sync.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/sync.js",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "funkwhale-now-playing-fallback",
|
||||
marker: "degrade to empty now-playing response when upstream endpoint is missing",
|
||||
|
||||
Reference in New Issue
Block a user