Enable automatic browsing upvotes
This commit is contained in:
1 parent
27012399ec
commit
7bf1aa9d24
26 files changed
+339
-57
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user