diff --git a/README.md b/README.md index 34a4857..694fbbf 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,11 @@ Here's an example of the `config.json` file: "forceDescriptionEmbed": false, "removeDuplicate": false, "descriptionClearHTML": false, - "titleClearHTML": false + "titleClearHTML": false, + "adaptiveSpacing": false, + "spacingWindow": 600, + "minSpacing": 1, + "maxSpacing": 60 } ``` @@ -108,6 +112,10 @@ Here's an example of the `config.json` file: - `removeDuplicate`: Instead of using the last date to track which items need to be published, use a text-based database to track duplicate items. - `descriptionClearHTML`: Remove HTML from the description of the Open Graph description and RSS-provided description (to make it more readable). - `titleClearHTML`: Remove HTML from the title of the post (to make it more readable). +- `adaptiveSpacing`: Enable adaptive spacing between posts based on the queue size. +- `spacingWindow`: Time window (in seconds) used when calculating adaptive spacing. +- `minSpacing`: Minimum number of seconds between posts when adaptive spacing is enabled. +- `maxSpacing`: Maximum number of seconds between posts when adaptive spacing is enabled. A `docker-compose.yml` file can be found in the root directory as `docker-compose.example.yml`, which you can use to set up the RSS poster using Docker. diff --git a/app/types/index.d.ts b/app/types/index.d.ts index d9ee814..58cbee4 100644 --- a/app/types/index.d.ts +++ b/app/types/index.d.ts @@ -14,6 +14,10 @@ interface Config { imageAlt?: string; removeDuplicate?: boolean; titleClearHTML?: boolean; + adaptiveSpacing?: boolean; + spacingWindow?: number; + minSpacing?: number; + maxSpacing?: number; } interface Item { diff --git a/app/utils/bskyHandler.ts b/app/utils/bskyHandler.ts index 1caa41f..8a5e816 100644 --- a/app/utils/bskyHandler.ts +++ b/app/utils/bskyHandler.ts @@ -122,7 +122,7 @@ async function post({ let post: any; try { - post = await bskyAgent.post(record); + post = await bskyAgent.post(record as any); } catch (error: any) { // if (error instanceof XRPCError) { if (error.constructor.name == XRPCError.name) { diff --git a/app/utils/queueHandler.ts b/app/utils/queueHandler.ts index 60ec0d8..3298955 100644 --- a/app/utils/queueHandler.ts +++ b/app/utils/queueHandler.ts @@ -5,6 +5,7 @@ let queue: QueueItems[] = []; let rateLimited: boolean = false; let queueRunning: boolean = false; let queueSnapshot: QueueItems[] = []; +let lastPostTimestamp = 0; let config: Config = { string: "", @@ -22,6 +23,10 @@ let config: Config = { imageAlt: "", removeDuplicate: false, titleClearHTML: false, + adaptiveSpacing: false, + spacingWindow: 600, + minSpacing: 1, + maxSpacing: 60, }; async function start() { @@ -66,6 +71,17 @@ async function runQueue() { queue.splice(i, 1); queueSnapshot.splice(i, 1); i--; + if (config.minSpacing && lastPostTimestamp) { + const elapsed = Date.now() - lastPostTimestamp; + const waitMs = config.minSpacing * 1000 - elapsed; + if (waitMs > 0) { + const waitSec = Math.ceil(waitMs / 1000); + console.log( + `[${new Date().toUTCString()}] - [bsky.rss QUEUE] Waiting ${waitSec} seconds before next post` + ); + await sleep(waitMs); + } + } let post = await bsky.post({ content: item.content, embed: item.embed, @@ -89,6 +105,18 @@ async function runQueue() { })` ); db.writeDate(new Date(item.date)); + lastPostTimestamp = Date.now(); + if (config.adaptiveSpacing && queueSnapshot.length > 0) { + const remaining = queueSnapshot.length; + const delaySec = computeDelay(remaining + 1); + + if (delaySec > 0) { + console.log( + `[${new Date().toUTCString()}] - [bsky.rss QUEUE] Waiting ${delaySec} seconds before next post` + ); + await sleep(delaySec * 1000); + } + } if (i === queueSnapshot.length - 1) { queueRunning = false; queueSnapshot = []; @@ -121,4 +149,21 @@ async function writeQueue({ return queue; } +function clamp(x: number, lo: number, hi: number) { + return Math.max(lo, Math.min(hi, x)); +} + +function computeDelay(q: number) { + if (!config.adaptiveSpacing) return 0; + if (q <= 1) return 0; + const window = config.spacingWindow || 600; + const min = config.minSpacing || 1; + const max = config.maxSpacing || 60; + return clamp(window / q, min, max); +} + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + export default { writeQueue, start }; diff --git a/app/utils/rssHandler.ts b/app/utils/rssHandler.ts index 9646c14..82d4b8b 100644 --- a/app/utils/rssHandler.ts +++ b/app/utils/rssHandler.ts @@ -23,6 +23,10 @@ let config: Config = { forceDescriptionEmbed: false, removeDuplicate: false, titleClearHTML: false, + adaptiveSpacing: false, + spacingWindow: 600, + minSpacing: 1, + maxSpacing: 60, }; async function start() { diff --git a/data/config.example.json b/data/config.example.json index e660383..f26ba5e 100644 --- a/data/config.example.json +++ b/data/config.example.json @@ -13,5 +13,9 @@ "forceDescriptionEmbed": false, "removeDuplicate": false, "descriptionClearHTML": false, - "titleClearHTML": false + "titleClearHTML": false, + "adaptiveSpacing": false, + "spacingWindow": 600, + "minSpacing": 1, + "maxSpacing": 60 } diff --git a/tsconfig.json b/tsconfig.json index 5e57c9f..c247f16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "gts/tsconfig-google", "compilerOptions": { - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "outDir": "lib", "removeComments": true, "target": "ES6", @@ -18,7 +18,8 @@ "noImplicitReturns": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "skipLibCheck": true }, "include": ["./**/*.ts"], "exclude": ["node_modules/**/*"]