Enable automatic browsing upvotes

This commit is contained in:
jjbrunton committed 2026-08-11 21:52:54 +01:00
1 parent 27012399ec
commit 7bf1aa9d24
26 files changed
+339 -57

No files matched your search

+52
View File
@@ -31,6 +31,14 @@ export type CommentPostResult =
/** Anything else: transient, safe to retry later. */
| { status: 'failed'; reason: string };
/** Result of a single upvote. A failed/uncertain request is never retried. */
export type VoteResult =
| { status: 'submitted' }
| { status: 'rate-limited'; retryAfterMs: number; reason: string }
| { status: 'session-invalid'; reason: string }
| { status: 'blocked'; reason: string }
| { status: 'failed'; reason: string };
/** Why a modhash lookup failed, so the caller can pick a proportionate response. */
type ModhashResult =
| { ok: true; modhash: string }
@@ -206,6 +214,50 @@ export class RedditCommentClient {
return { status: 'posted', fullname: created.fullname, permalink: created.permalink };
}
/** Submit one upvote through the same authenticated browser session as comments. */
async upvote(thingFullname: string): Promise<VoteResult> {
if (!/^t[13]_[a-z0-9]+$/i.test(thingFullname)) {
return { status: 'failed', reason: `"${thingFullname}" is not a post or comment fullname` };
}
const modhash = await this.ensureModhash();
if (!modhash.ok) return modhash.kind === 'signed-out'
? { status: 'session-invalid', reason: modhash.reason }
: { status: 'failed', reason: modhash.reason };
let response: HttpTextResponse;
try {
response = await this.scheduler.schedule(() => this.transport.postForm(
`${REDDIT_WRITE_ORIGIN}/api/vote`,
{ id: thingFullname, dir: '1', api_type: 'json', uh: modhash.modhash },
{ 'X-Modhash': modhash.modhash, 'Content-Type': 'application/x-www-form-urlencoded' },
));
} catch (error) {
return { status: 'failed', reason: errorMessage(error) };
}
if (response.status === 429) {
const retryAfterMs = 10 * 60_000;
this.scheduler.extendCooldown(retryAfterMs);
return { status: 'rate-limited', retryAfterMs, reason: 'Reddit returned HTTP 429 for the vote' };
}
if (response.status === 403) {
if (!looksLikeJson(response.body)) return { status: 'blocked', reason: blockPageReason(response) };
this.modhash = null;
return { status: 'session-invalid', reason: 'Reddit rejected the vote with HTTP 403' };
}
if (response.status >= 500) return { status: 'failed', reason: `Reddit returned HTTP ${response.status}` };
const payload = parseJson(response.body);
if (!payload) return { status: 'failed', reason: `Reddit returned an unreadable vote response (HTTP ${response.status})` };
const errors = readErrors(payload);
if (errors.length > 0) {
const classified = this.classifyError(errors[0], 'the selected community');
if (classified.status === 'rate-limited') return { status: 'rate-limited', retryAfterMs: classified.retryAfterMs, reason: classified.reason };
if (classified.status === 'session-invalid' || classified.status === 'blocked') return classified;
return { status: 'failed', reason: 'reason' in classified ? classified.reason : 'Reddit rejected the vote' };
}
this.logger.info(`Submitted Reddit upvote for ${thingFullname}`);
return { status: 'submitted' };
}
/**
* Current score and removal state for previously posted comments, from the
* account's own authenticated view. Batched because Reddit accepts up to 100