Disable unavailable subreddits after Reddit 404
This commit is contained in:
1 parent
664aa5c098
commit
dcf57273e2
13 files changed
+153
-12
No files matched your search
+25
-3
@@ -1,4 +1,9 @@
|
||||
import { RedditRateLimitError, type RedditApiClient, type RedditPost } from '../reddit/api.js';
|
||||
import {
|
||||
RedditRateLimitError,
|
||||
RedditSubredditUnavailableError,
|
||||
type RedditApiClient,
|
||||
type RedditPost,
|
||||
} from '../reddit/api.js';
|
||||
import type { PostsRepository } from '../database/repositories.js';
|
||||
import type { Config } from '../config/index.js';
|
||||
import type { Logger } from '../utils/logger.js';
|
||||
@@ -17,6 +22,12 @@ interface ListingScreeningSummary {
|
||||
deferredReason: 'selected-recent-candidate' | 'older-than-fallback' | null;
|
||||
}
|
||||
|
||||
/** Coordinates permanent polling disablement across account runtimes. */
|
||||
export interface SubredditPollingControl {
|
||||
disableUnavailableSubreddit(subreddit: string): void;
|
||||
isSubredditEnabled(subreddit: string): boolean;
|
||||
}
|
||||
|
||||
export class MonitorService {
|
||||
// Flips on each call so that when both pools have candidates the bot
|
||||
// alternates between image and chat instead of always favouring whichever
|
||||
@@ -30,6 +41,7 @@ export class MonitorService {
|
||||
private config: Config,
|
||||
private logger: Logger,
|
||||
private delayBetweenFetchesMs: number = 1000,
|
||||
private readonly pollingControl?: SubredditPollingControl,
|
||||
) { }
|
||||
|
||||
async getUnprocessedPosts(): Promise<RedditPost[]> {
|
||||
@@ -151,6 +163,11 @@ export class MonitorService {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof RedditRateLimitError) throw error;
|
||||
if (error instanceof RedditSubredditUnavailableError) {
|
||||
this.pollingControl?.disableUnavailableSubreddit(subreddit.name);
|
||||
this.logger.warn(`Disabled r/${subreddit.name}: Reddit returned HTTP 404`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Failed to fetch posts from r/${subreddit.name}: ${errorMessage}`);
|
||||
@@ -182,10 +199,10 @@ export class MonitorService {
|
||||
*/
|
||||
private buildCheckOrder(): Config['reddit']['subreddits'] {
|
||||
const images = this.weightedShuffle(
|
||||
this.config.reddit.subreddits.filter((s) => s.type === 'image'),
|
||||
this.config.reddit.subreddits.filter((s) => s.type === 'image' && this.isSubredditEnabled(s.name)),
|
||||
);
|
||||
const chats = this.weightedShuffle(
|
||||
this.config.reddit.subreddits.filter((s) => s.type === 'chat'),
|
||||
this.config.reddit.subreddits.filter((s) => s.type === 'chat' && this.isSubredditEnabled(s.name)),
|
||||
);
|
||||
|
||||
const [a, b] = this.chatGoesFirst ? [chats, images] : [images, chats];
|
||||
@@ -200,6 +217,11 @@ export class MonitorService {
|
||||
return out;
|
||||
}
|
||||
|
||||
private isSubredditEnabled(subreddit: string): boolean {
|
||||
const local = this.config.reddit.subreddits.find((entry) => entry.name.toLowerCase() === subreddit.toLowerCase());
|
||||
return local?.enabled !== false && (this.pollingControl?.isSubredditEnabled(subreddit) ?? true);
|
||||
}
|
||||
|
||||
// Weighted random ordering (Efraimidis–Spirakis): key = random^(1/weight),
|
||||
// sorted descending. Heavier-weighted subs are more likely to land earlier
|
||||
// each cycle, while every sub still has a non-zero chance of any position.
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
type VerifiedAccountSession,
|
||||
} from './reddit-account-runtime.js';
|
||||
import { RecipientDeduplicator } from './recipient-deduplicator.js';
|
||||
import { ConfigStore } from '../config/persistence.js';
|
||||
import { createConfigBackedSubredditPollingControl } from './subreddit-polling-control.js';
|
||||
|
||||
export interface ConcreteRuntimeFactoryOptions {
|
||||
config: Config;
|
||||
@@ -116,6 +118,7 @@ async function buildRuntime(
|
||||
const aiClient = new OpenRouterClient(accountConfig, logger);
|
||||
const monitorService = new MonitorService(
|
||||
redditApi, postsRepo, recipientDeduplicator, accountConfig, logger,
|
||||
1000, createConfigBackedSubredditPollingControl(config, new ConfigStore(db), logger),
|
||||
);
|
||||
const processorService = new ProcessorService(redditApi, aiClient, accountConfig, logger);
|
||||
processorService.setStrategySelector(new StrategySelector(accountConfig, messagesRepo, logger));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Config } from '../config/index.js';
|
||||
import type { ConfigPersistence } from '../config/index.js';
|
||||
import type { Logger } from '../utils/logger.js';
|
||||
import type { SubredditPollingControl } from './monitor.js';
|
||||
|
||||
/** Persists a terminal Reddit listing failure so every account stops polling it. */
|
||||
export function createConfigBackedSubredditPollingControl(
|
||||
config: Config,
|
||||
configStore: ConfigPersistence,
|
||||
logger: Logger,
|
||||
): SubredditPollingControl {
|
||||
return {
|
||||
disableUnavailableSubreddit(subreddit: string): void {
|
||||
const entry = config.reddit.subreddits.find((candidate) => candidate.name.toLowerCase() === subreddit.toLowerCase());
|
||||
if (!entry || entry.enabled === false) return;
|
||||
|
||||
entry.enabled = false;
|
||||
configStore.save(config);
|
||||
logger.warn(`Persisted disabled status for unavailable subreddit r/${entry.name}`);
|
||||
},
|
||||
isSubredditEnabled(subreddit: string): boolean {
|
||||
return config.reddit.subreddits.find((entry) => entry.name.toLowerCase() === subreddit.toLowerCase())?.enabled !== false;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user