Harden listening endpoint sync, stats, and runtime guards
This commit is contained in:
+12
-1
@@ -27,6 +27,17 @@ WEBMENTIONS_PROXY_MOUNT_PATH=/webmentions-api
|
|||||||
# Cache TTL in seconds for proxied webmention.io API responses
|
# Cache TTL in seconds for proxied webmention.io API responses
|
||||||
WEBMENTIONS_PROXY_CACHE_TTL=60
|
WEBMENTIONS_PROXY_CACHE_TTL=60
|
||||||
|
|
||||||
|
# Optional listening endpoint update cadence (milliseconds)
|
||||||
|
# Lower values increase freshness but add upstream API load.
|
||||||
|
LISTENING_CACHE_TTL=120000
|
||||||
|
LISTENING_SYNC_INTERVAL=180000
|
||||||
|
|
||||||
|
# Optional per-source listening overrides (milliseconds)
|
||||||
|
FUNKWHALE_CACHE_TTL=
|
||||||
|
FUNKWHALE_SYNC_INTERVAL=
|
||||||
|
LASTFM_CACHE_TTL=
|
||||||
|
LASTFM_SYNC_INTERVAL=
|
||||||
|
|
||||||
# Syndication endpoint mount path
|
# Syndication endpoint mount path
|
||||||
# Default in indiekit.config.mjs is /syndicate
|
# Default in indiekit.config.mjs is /syndicate
|
||||||
SYNDICATE_MOUNT_PATH=/syndicate
|
SYNDICATE_MOUNT_PATH=/syndicate
|
||||||
@@ -38,6 +49,6 @@ BLUESKY_PASSWORD=
|
|||||||
|
|
||||||
# Mastodon syndicator settings
|
# Mastodon syndicator settings
|
||||||
# MASTODON_USER should be your username without @
|
# MASTODON_USER should be your username without @
|
||||||
MASTODON_URL=https://mastodon.social
|
MASTODON_URL=
|
||||||
MASTODON_USER=
|
MASTODON_USER=
|
||||||
MASTODON_ACCESS_TOKEN=
|
MASTODON_ACCESS_TOKEN=
|
||||||
|
|||||||
@@ -38,6 +38,48 @@ const funkwhaleUsername = process.env.FUNKWHALE_USERNAME;
|
|||||||
const funkwhaleToken = process.env.FUNKWHALE_TOKEN;
|
const funkwhaleToken = process.env.FUNKWHALE_TOKEN;
|
||||||
const lastfmApiKey = process.env.LASTFM_API_KEY;
|
const lastfmApiKey = process.env.LASTFM_API_KEY;
|
||||||
const lastfmUsername = process.env.LASTFM_USERNAME;
|
const lastfmUsername = process.env.LASTFM_USERNAME;
|
||||||
|
const listeningCacheTtlRaw = Number.parseInt(
|
||||||
|
process.env.LISTENING_CACHE_TTL || "120000",
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const listeningCacheTtl = Number.isFinite(listeningCacheTtlRaw)
|
||||||
|
? Math.max(30000, listeningCacheTtlRaw)
|
||||||
|
: 120000;
|
||||||
|
const listeningSyncIntervalRaw = Number.parseInt(
|
||||||
|
process.env.LISTENING_SYNC_INTERVAL || "180000",
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const listeningSyncInterval = Number.isFinite(listeningSyncIntervalRaw)
|
||||||
|
? Math.max(60000, listeningSyncIntervalRaw)
|
||||||
|
: 180000;
|
||||||
|
const funkwhaleCacheTtlRaw = Number.parseInt(
|
||||||
|
process.env.FUNKWHALE_CACHE_TTL || String(listeningCacheTtl),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const funkwhaleCacheTtl = Number.isFinite(funkwhaleCacheTtlRaw)
|
||||||
|
? Math.max(30000, funkwhaleCacheTtlRaw)
|
||||||
|
: listeningCacheTtl;
|
||||||
|
const funkwhaleSyncIntervalRaw = Number.parseInt(
|
||||||
|
process.env.FUNKWHALE_SYNC_INTERVAL || String(listeningSyncInterval),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const funkwhaleSyncInterval = Number.isFinite(funkwhaleSyncIntervalRaw)
|
||||||
|
? Math.max(60000, funkwhaleSyncIntervalRaw)
|
||||||
|
: listeningSyncInterval;
|
||||||
|
const lastfmCacheTtlRaw = Number.parseInt(
|
||||||
|
process.env.LASTFM_CACHE_TTL || String(listeningCacheTtl),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const lastfmCacheTtl = Number.isFinite(lastfmCacheTtlRaw)
|
||||||
|
? Math.max(30000, lastfmCacheTtlRaw)
|
||||||
|
: listeningCacheTtl;
|
||||||
|
const lastfmSyncIntervalRaw = Number.parseInt(
|
||||||
|
process.env.LASTFM_SYNC_INTERVAL || String(listeningSyncInterval),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const lastfmSyncInterval = Number.isFinite(lastfmSyncIntervalRaw)
|
||||||
|
? Math.max(60000, lastfmSyncIntervalRaw)
|
||||||
|
: listeningSyncInterval;
|
||||||
const blueskyHandle = (process.env.BLUESKY_HANDLE || "")
|
const blueskyHandle = (process.env.BLUESKY_HANDLE || "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^@+/, "");
|
.replace(/^@+/, "");
|
||||||
@@ -281,11 +323,15 @@ export default {
|
|||||||
instanceUrl: funkwhaleInstance,
|
instanceUrl: funkwhaleInstance,
|
||||||
username: funkwhaleUsername,
|
username: funkwhaleUsername,
|
||||||
token: funkwhaleToken,
|
token: funkwhaleToken,
|
||||||
|
cacheTtl: funkwhaleCacheTtl,
|
||||||
|
syncInterval: funkwhaleSyncInterval,
|
||||||
},
|
},
|
||||||
"@rmdes/indiekit-endpoint-lastfm": {
|
"@rmdes/indiekit-endpoint-lastfm": {
|
||||||
mountPath: "/lastfmapi",
|
mountPath: "/lastfmapi",
|
||||||
apiKey: lastfmApiKey,
|
apiKey: lastfmApiKey,
|
||||||
username: lastfmUsername,
|
username: lastfmUsername,
|
||||||
|
cacheTtl: lastfmCacheTtl,
|
||||||
|
syncInterval: lastfmSyncInterval,
|
||||||
},
|
},
|
||||||
"@rmdes/indiekit-endpoint-podroll": {
|
"@rmdes/indiekit-endpoint-podroll": {
|
||||||
mountPath: podrollMountPath,
|
mountPath: podrollMountPath,
|
||||||
|
|||||||
@@ -200,6 +200,206 @@ const patchSpecs = [
|
|||||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/controllers/stats.js",
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/controllers/stats.js",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "funkwhale-stats-db-getter",
|
||||||
|
marker: "use application database getter for public stats routes",
|
||||||
|
oldSnippet: ` // Try database first, fall back to cache for public routes
|
||||||
|
const db = request.app.locals.database;
|
||||||
|
let stats;`,
|
||||||
|
newSnippet: ` // Try database first, fall back to cache for public routes
|
||||||
|
// use application database getter for public stats routes
|
||||||
|
const db =
|
||||||
|
request.app.locals.application.getFunkwhaleDb?.() ||
|
||||||
|
request.app.locals.database;
|
||||||
|
let stats;`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/controllers/stats.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/controllers/stats.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "funkwhale-trends-db-getter",
|
||||||
|
marker: "use application database getter for public trends routes",
|
||||||
|
oldSnippet: ` const db = request.app.locals.database;
|
||||||
|
const days = Math.min(parseInt(request.query.days) || 30, 90);`,
|
||||||
|
newSnippet: ` // use application database getter for public trends routes
|
||||||
|
const db =
|
||||||
|
request.app.locals.application.getFunkwhaleDb?.() ||
|
||||||
|
request.app.locals.database;
|
||||||
|
const days = Math.min(parseInt(request.query.days) || 30, 90);`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/controllers/stats.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/controllers/stats.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "funkwhale-sync-date-storage",
|
||||||
|
marker: "store listenedAt/syncedAt as Date objects",
|
||||||
|
oldSnippet: ` listenedAt: new Date(listening.creation_date).toISOString(),
|
||||||
|
syncedAt: new Date().toISOString(),`,
|
||||||
|
newSnippet: ` // store listenedAt/syncedAt as Date objects
|
||||||
|
listenedAt: new Date(listening.creation_date),
|
||||||
|
syncedAt: new Date(),`,
|
||||||
|
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-stats-date-coercion",
|
||||||
|
marker: "support string and Date listenedAt values in period filters",
|
||||||
|
oldSnippet: `function getDateMatch(period) {
|
||||||
|
const now = new Date();
|
||||||
|
switch (period) {
|
||||||
|
case "week":
|
||||||
|
return { listenedAt: { $gte: new Date(now - 7 * 24 * 60 * 60 * 1000) } };
|
||||||
|
case "month":
|
||||||
|
return { listenedAt: { $gte: new Date(now - 30 * 24 * 60 * 60 * 1000) } };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
newSnippet: `function getDateMatch(period) {
|
||||||
|
const now = new Date();
|
||||||
|
let threshold = null;
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case "week":
|
||||||
|
threshold = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case "month":
|
||||||
|
threshold = new Date(now - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// support string and Date listenedAt values in period filters
|
||||||
|
return {
|
||||||
|
$expr: {
|
||||||
|
$gte: [{ $toDate: "$listenedAt" }, threshold],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/stats.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/stats.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "funkwhale-trends-date-coercion",
|
||||||
|
marker: "support string and Date listenedAt values in trends aggregation",
|
||||||
|
oldSnippet: ` return collection
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { listenedAt: { $gte: startDate } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: { format: "%Y-%m-%d", date: "$listenedAt" },
|
||||||
|
},`,
|
||||||
|
newSnippet: ` return collection
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
// support string and Date listenedAt values in trends aggregation
|
||||||
|
$addFields: {
|
||||||
|
listenedAtDate: { $toDate: "$listenedAt" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $match: { listenedAtDate: { $gte: startDate } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: { format: "%Y-%m-%d", date: "$listenedAtDate" },
|
||||||
|
},`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/stats.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/stats.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lastfm-sync-date-storage",
|
||||||
|
marker: "store scrobbledAt/syncedAt as Date objects",
|
||||||
|
oldSnippet: ` scrobbledAt: scrobbledAtDate.toISOString(),
|
||||||
|
syncedAt: new Date().toISOString(),`,
|
||||||
|
newSnippet: ` // store scrobbledAt/syncedAt as Date objects
|
||||||
|
scrobbledAt: scrobbledAtDate,
|
||||||
|
syncedAt: new Date(),`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-lastfm/lib/sync.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-lastfm/lib/sync.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lastfm-stats-date-coercion",
|
||||||
|
marker: "support string and Date scrobbledAt values in period filters",
|
||||||
|
oldSnippet: `function getDateMatch(period) {
|
||||||
|
const now = new Date();
|
||||||
|
switch (period) {
|
||||||
|
case "week":
|
||||||
|
return { scrobbledAt: { $gte: new Date(now - 7 * 24 * 60 * 60 * 1000) } };
|
||||||
|
case "month":
|
||||||
|
return { scrobbledAt: { $gte: new Date(now - 30 * 24 * 60 * 60 * 1000) } };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
newSnippet: `function getDateMatch(period) {
|
||||||
|
const now = new Date();
|
||||||
|
let threshold = null;
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case "week":
|
||||||
|
threshold = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case "month":
|
||||||
|
threshold = new Date(now - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// support string and Date scrobbledAt values in period filters
|
||||||
|
return {
|
||||||
|
$expr: {
|
||||||
|
$gte: [{ $toDate: "$scrobbledAt" }, threshold],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-lastfm/lib/stats.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-lastfm/lib/stats.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lastfm-trends-date-coercion",
|
||||||
|
marker: "support string and Date scrobbledAt values in trends aggregation",
|
||||||
|
oldSnippet: ` return collection
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { scrobbledAt: { $gte: startDate } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: { format: "%Y-%m-%d", date: "$scrobbledAt" },
|
||||||
|
},`,
|
||||||
|
newSnippet: ` return collection
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
// support string and Date scrobbledAt values in trends aggregation
|
||||||
|
$addFields: {
|
||||||
|
scrobbledAtDate: { $toDate: "$scrobbledAt" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $match: { scrobbledAtDate: { $gte: startDate } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: { format: "%Y-%m-%d", date: "$scrobbledAtDate" },
|
||||||
|
},`,
|
||||||
|
candidates: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-lastfm/lib/stats.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-lastfm/lib/stats.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function exists(filePath) {
|
async function exists(filePath) {
|
||||||
|
|||||||
Reference in New Issue
Block a user