Enable automatic browsing upvotes
This commit is contained in:
1 parent
27012399ec
commit
7bf1aa9d24
26 files changed
+339
-57
No files matched your search
@@ -17,6 +17,10 @@ export interface BrowsingDecisionSink {
|
||||
recordSimulation(decision: BrowsingDecisionRecord): Promise<void>;
|
||||
}
|
||||
|
||||
export interface BrowsingVoter {
|
||||
upvote(thingFullname: string): Promise<{ status: 'submitted' | 'rate-limited' | 'session-invalid' | 'blocked' | 'failed'; reason?: string }>;
|
||||
}
|
||||
|
||||
export interface AccountBrowsingSimulationServiceOptions {
|
||||
accountId: string;
|
||||
repository: AccountBrowsingSimulationRepository;
|
||||
@@ -26,6 +30,7 @@ export interface AccountBrowsingSimulationServiceOptions {
|
||||
getBrowsingComments(subreddit: string, postId: string, limit: number): Promise<BrowsingThing[]>;
|
||||
};
|
||||
sink: BrowsingDecisionSink;
|
||||
voter?: BrowsingVoter;
|
||||
logger: Logger;
|
||||
now?: () => number;
|
||||
random?: () => number;
|
||||
@@ -50,19 +55,27 @@ export class AccountBrowsingSimulationService {
|
||||
this.random = options.random ?? Math.random;
|
||||
}
|
||||
|
||||
async tick(signal?: AbortSignal): Promise<BrowsingSimulationOutcome> {
|
||||
/** Runs one operator-requested, simulation-only selection without waiting for the schedule. */
|
||||
runNow(signal?: AbortSignal): Promise<BrowsingSimulationOutcome> {
|
||||
return this.tick({ force: true }, signal);
|
||||
}
|
||||
|
||||
async tick(
|
||||
{ allowVoting = false, force = false }: { allowVoting?: boolean; force?: boolean } = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<BrowsingSimulationOutcome> {
|
||||
if (signal?.aborted) throw new DOMException('Operation aborted', 'AbortError');
|
||||
const now = this.now();
|
||||
const settings = this.options.repository.getSettings(this.options.accountId, now);
|
||||
if (!settings.enabled) return { kind: 'idle', reason: 'Browsing simulation is disabled' };
|
||||
|
||||
if (settings.nextDueAt === null) {
|
||||
if (settings.nextDueAt === null && !force) {
|
||||
const nextDueAt = this.scheduleNext(settings, now);
|
||||
this.options.repository.setNextDueAt(this.options.accountId, nextDueAt, now);
|
||||
return { kind: 'scheduled', nextDueAt };
|
||||
}
|
||||
if (now < settings.nextDueAt) return { kind: 'idle', reason: 'Browsing simulation is not due' };
|
||||
if (isQuietTime(new Date(now), settings)) {
|
||||
if (!force && now < settings.nextDueAt) return { kind: 'idle', reason: 'Browsing simulation is not due' };
|
||||
if (!force && isQuietTime(new Date(now), settings)) {
|
||||
const nextDueAt = moveOutsideQuietHours(now, settings);
|
||||
this.options.repository.setNextDueAt(this.options.accountId, nextDueAt, now);
|
||||
return { kind: 'scheduled', nextDueAt };
|
||||
@@ -70,7 +83,7 @@ export class AccountBrowsingSimulationService {
|
||||
|
||||
const today = startOfLocalDay(now);
|
||||
const target = dailyTarget(this.options.accountId, today, settings);
|
||||
if (this.options.repository.countSelectedSince(this.options.accountId, today) >= target) {
|
||||
if (!force && this.options.repository.countSelectedSince(this.options.accountId, today) >= target) {
|
||||
const nextDueAt = moveOutsideQuietHours(today + 24 * 60 * 60_000, settings);
|
||||
this.options.repository.setNextDueAt(this.options.accountId, nextDueAt, now);
|
||||
return { kind: 'scheduled', nextDueAt };
|
||||
@@ -103,10 +116,22 @@ export class AccountBrowsingSimulationService {
|
||||
if (candidates.length === 0) return { kind: 'scheduled', nextDueAt };
|
||||
|
||||
const selected = candidates[Math.floor(this.random() * candidates.length)];
|
||||
const decision = toDecision(this.options.accountId, selected, 'simulated-upvote', 'Eligible simulated selection', sessionId, now);
|
||||
const decision = toDecision(this.options.accountId, selected, 'simulated-upvote', 'Eligible browsing selection', sessionId, now);
|
||||
await this.options.sink.recordSimulation(decision);
|
||||
if (settings.autoVoteEnabled && allowVoting && this.options.voter) {
|
||||
const result = await this.options.voter.upvote(selected.fullname);
|
||||
if (result.status === 'submitted') {
|
||||
this.options.repository.markVoteSubmitted(this.options.accountId, selected.fullname);
|
||||
decision.voteSubmitted = true;
|
||||
decision.reason = 'Reddit upvote submitted';
|
||||
this.options.logger.info(`Submitted browsing upvote for ${selected.fullname} in r/${selected.subreddit}`);
|
||||
} else {
|
||||
decision.reason = `Reddit upvote not submitted: ${result.reason ?? result.status}`;
|
||||
this.options.logger.warn(`Browsing upvote for ${selected.fullname} was not submitted: ${result.reason ?? result.status}`);
|
||||
}
|
||||
}
|
||||
this.options.onActivity?.(decision);
|
||||
this.options.logger.info(`Simulated browsing selection for ${selected.fullname} in r/${selected.subreddit}; no Reddit vote was submitted`);
|
||||
if (!decision.voteSubmitted) this.options.logger.info(`Simulated browsing selection for ${selected.fullname} in r/${selected.subreddit}; no Reddit vote was submitted`);
|
||||
return { kind: 'simulated', decision, nextDueAt };
|
||||
}
|
||||
|
||||
@@ -278,5 +303,6 @@ function toDecision(
|
||||
discoveredAt: now,
|
||||
decidedAt: now,
|
||||
scheduledSessionId: sessionId,
|
||||
voteSubmitted: false,
|
||||
};
|
||||
}
|
||||
@@ -298,11 +298,14 @@ async function buildRuntime(
|
||||
redditApi.getBrowsingComments(subreddit, postId, limit),
|
||||
},
|
||||
sink: browsingRepository,
|
||||
voter: commentClient,
|
||||
logger,
|
||||
onActivity: (decision) => activityLogRepo.insert({
|
||||
timestamp: decision.decidedAt,
|
||||
type: 'response',
|
||||
message: `Simulated ${decision.thingKind} upvote in r/${decision.subreddit}; no Reddit vote was submitted`,
|
||||
message: decision.voteSubmitted
|
||||
? `Submitted ${decision.thingKind} upvote in r/${decision.subreddit}`
|
||||
: `Simulated ${decision.thingKind} upvote in r/${decision.subreddit}; no Reddit vote was submitted`,
|
||||
postUrl: `https://www.reddit.com${decision.permalink}`,
|
||||
postTitle: `r/${decision.subreddit}`,
|
||||
}),
|
||||
@@ -502,12 +505,14 @@ export class AccountRuntimeLoop {
|
||||
this.logger.error('Account growth tick failed:', error);
|
||||
}
|
||||
}
|
||||
// Browsing simulation is strictly below comment growth. It records a
|
||||
// hypothetical decision only and runs in both scan and running modes.
|
||||
// Browsing selection is strictly below comment growth. It runs in scan
|
||||
// and running modes, but submits votes only while fully running and not dry-run.
|
||||
if (this.resources.browsingService && Date.now() >= nextBrowsingCheckAt) {
|
||||
nextBrowsingCheckAt = Date.now() + growthInterval;
|
||||
try {
|
||||
await this.resources.browsingService.tick(signal);
|
||||
await this.resources.browsingService.tick(
|
||||
{ allowVoting: allowsSending(mode) && !this.config.browser.dryRun }, signal,
|
||||
);
|
||||
} catch (error) {
|
||||
if (signal.aborted) return;
|
||||
this.logger.error('Browsing simulation tick failed:', error);
|
||||
|
||||
Reference in new issue
Block a user