feat(timeline,background): Add live remote post fetching and background sync

- Implement live post fetching from followed remote users with parallel requests and timeouts
- Add remote-sync background task to periodically cache posts from followed remote users
- Optimize swarm node detection and prioritize swarm API over ActivityPub for faster responses
- Add timeout wrapper (5s per node) to prevent slow remote nodes from blocking timeline requests
- Enhance scheduler to support background sync tasks with configurable intervals
- Transform remote posts to match local format with proper media and author metadata
- Improve error handling and logging for remote user post fetching
- Replace cached-only approach with hybrid live/cached strategy for fresher content
This commit is contained in:
Christomatt
2026-01-26 10:14:07 +01:00
parent e98221e81a
commit 728dda6b52
4 changed files with 249 additions and 48 deletions
+29 -34
View File
@@ -11,9 +11,10 @@
import { db, botContentItems, bots } from '@/db';
import { eq, and } from 'drizzle-orm';
import { ContentGenerator, type Bot as ContentGeneratorBot, type ContentItem } from './contentGenerator';
import { canPost, recordPost } from './rateLimiter';
import { canPost } from './rateLimiter';
import { decryptApiKey, deserializeEncryptedData } from './encryption';
import { parseScheduleConfig, isDue } from './scheduler';
import { triggerPost } from './posting';
// ============================================
// TYPES
@@ -446,9 +447,6 @@ export async function attemptAutonomousPost(
);
}
const contentGeneratorBot = toContentGeneratorBot(dbBot, dbBot.user.handle);
const generator = new ContentGenerator(contentGeneratorBot);
// Check if bot has content sources
const hasSourcesConfigured = await hasContentSources(botId);
@@ -471,25 +469,25 @@ export async function attemptAutonomousPost(
);
if (evaluation.shouldPost) {
const generatedContent = await generator.generatePost(contentItem);
// Use triggerPost to actually create the post in the database
const postResult = await triggerPost(botId, {
sourceContentId: contentItem.id,
skipRateLimitCheck, // Already checked above
});
// Record the post for rate limiting
if (!skipRateLimitCheck) {
await recordPost(botId);
if (postResult.success) {
return {
posted: true,
postId: postResult.post?.id,
postText: postResult.post?.content,
contentItem,
};
} else {
throw new AutonomousPostError(
postResult.error || 'Failed to create post',
postResult.errorCode as AutonomousPostErrorCode || 'POST_CREATION_FAILED'
);
}
await markContentItemProcessed(
contentItem.id,
'pending',
evaluation.interestScore,
evaluation.reason
);
return {
posted: true,
postText: generatedContent.text,
contentItem,
};
}
} catch (error) {
console.error(`Error evaluating content item ${contentItem.id}:`, error);
@@ -507,24 +505,21 @@ export async function attemptAutonomousPost(
}
// Generate original post based on personality (no content source needed)
try {
const generatedContent = await generator.generatePost(undefined, undefined);
// Record the post for rate limiting
if (!skipRateLimitCheck) {
await recordPost(botId);
}
// Use triggerPost which handles database insertion, rate limiting, and federation
const postResult = await triggerPost(botId, {
skipRateLimitCheck, // Already checked above
});
if (postResult.success) {
return {
posted: true,
postText: generatedContent.text,
postId: postResult.post?.id,
postText: postResult.post?.content,
};
} catch (error) {
if (error instanceof AutonomousPostError) throw error;
} else {
throw new AutonomousPostError(
`Failed to create post: ${error instanceof Error ? error.message : String(error)}`,
'POST_CREATION_FAILED',
error instanceof Error ? error : undefined
postResult.error || 'Failed to create post',
postResult.errorCode as AutonomousPostErrorCode || 'POST_CREATION_FAILED'
);
}
}