Skip to content

Add Reddit strategy for posting to specific subreddits - #167

Draft
nzakas with Copilot wants to merge 4 commits into
mainfrom
copilot/implement-reddit-posting-strategy
Draft

nzakas with Copilot wants to merge 4 commits into
mainfrom
copilot/implement-reddit-posting-strategy

Conversation

Copilot AI commented Apr 20, 2026 •

Copy link
Copy Markdown
Contributor
  • Review PR feedback and identify requested README change for Reddit token setup
  • Inspect existing README structure for service-specific credential setup sections
  • Add concise README instructions for obtaining a Reddit access token for REDDIT_ACCESS_TOKEN
  • Verify formatting/readability of README update
  • Run final validation and reply to review comment with commit hash

Copilot AI linked an issue Apr 20, 2026 that may be closed by this pull request
Copilot AI and others added 2 commits April 20, 2026 13:24
Copilot AI changed the title [WIP] Add strategy to post to Reddit communities Add Reddit strategy for posting to specific subreddits Apr 20, 2026
Copilot AI requested a review from nzakas April 20, 2026 13:37

@nzakas nzakas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot weneed to add instructions to the README for obtaining a Reddit token to use.

Copilot AI commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot weneed to add instructions to the README for obtaining a Reddit token to use.

Added README instructions for obtaining a Reddit OAuth token, including a concrete token-generation example, required env vars, and message title/body behavior details. Addressed in commit e653300.

Copilot AI requested a review from nzakas April 20, 2026 22:31

@nzakas nzakas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. The shape follows the existing strategies: id/name, post(message, postOptions), validatePostOptions(), rejecting images the same way NostrStrategy does, signal passed to fetch, getUrlFromResponse, and MAX_MESSAGE_LENGTH/calculateMessageLength. The exports in src/index.ts, the CLI flag, and the README env var list are wired up too. The MCP server is strategy-agnostic, so it needs no changes. There are a few problems to address before this is usable, though. In order of severity:

  1. Access tokens expire after one hour. Per Reddit's OAuth2 docs, "All bearer tokens expire after 1 hour." Taking a static REDDIT_ACCESS_TOKEN means the CLI and MCP server stop working an hour after setup. The strategy should take long-lived credentials and get a token itself. For a script app that means client ID, client secret, username, and password with grant_type=password. Alternatively, it could take a refresh token with grant_type=refresh_token. See inline comments.
  2. The README setup steps probably no longer work for new users. Reddit's Responsible Builder Policy (Nov 2025) ended self-service creation of new Data API credentials, and new apps now need manual approval. I couldn't load Reddit's own help page from here, so please verify, but the "click create another app" steps are likely out of date.
  3. The title is derived with no validation. Reddit caps titles at 300 characters. A single-line message becomes the whole title, which is the common crosspost case, so anything over 300 characters will be rejected by Reddit. A leading blank line produces an empty title. MAX_MESSAGE_LENGTH/calculateMessageLength don't account for this, so the MCP check-message-length tool will report messages as fitting when they will fail.
  4. Error handling. response.json() runs before the ok check, so a non-JSON error body turns into an opaque SyntaxError. Reddit's non-json.errors error bodies (e.g. 401 {"message":"Unauthorized","error":401}) are reported as "Unknown Reddit API error."
  5. The User-Agent version will go stale. src/strategies/reddit.js isn't in release-please-config.json extra-files, so the x-release-please-version marker is never updated.
  6. Test gaps. Missing coverage for realistic auth errors, non-JSON bodies, title-only and CRLF messages, and the User-Agent header.

The core submit call is right: POST https://oauth.reddit.com/api/submit with api_type=json, kind=self, sr, title, text, and a check for json.errors on a 200 response.

Comment thread src/strategies/reddit.js
* @throws {Error} When options are missing.
*/
constructor(options) {
const { accessToken, subreddit } = options;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reddit bearer tokens expire after one hour ("All bearer tokens expire after 1 hour" in the OAuth2 docs). With accessToken as the only credential, a user who follows the README gets a working setup for 60 minutes, and every later post fails with 401.

Suggestion: accept script-app credentials (clientId, clientSecret, username, password) and fetch a token lazily in post(). That means POST https://www.reddit.com/api/v1/access_token with HTTP Basic auth clientId:clientSecret, body grant_type=password&username=...&password=..., the same User-Agent, and the same signal. Cache the token until shortly before expires_in runs out. A refresh-token variant (grant_type=refresh_token) would also work if you'd rather not store the account password. Either way, the constructor validation, the RedditOptions typedef, the CLI env vars, and the README all need to follow.

Comment thread src/bin.js
if (flags.reddit) {
strategies.push(
new RedditStrategy({
accessToken: env.require("REDDIT_ACCESS_TOKEN"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the token expires after an hour (see comment on reddit.js), a REDDIT_ACCESS_TOKEN env var doesn't suit a CLI/MCP server that people configure once in .env. Once the strategy takes long-lived credentials, this should become something like REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USERNAME, REDDIT_PASSWORD (or REDDIT_REFRESH_TOKEN), plus REDDIT_SUBREDDIT. Update the README env var list to match.

Comment thread src/strategies/reddit.js
}

const [firstLine, ...remainingLines] = message.split(/\r?\n/g);
const title = firstLine.trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title derivation needs validation:

  • Reddit caps title at 300 characters. For a single-line message (the typical crosspost case, since the same text goes to Bluesky/Mastodon/etc.), the whole message becomes the title. Anything over 300 characters is rejected by Reddit after a round trip.
  • A message that starts with a newline (e.g. "\nHello") produces an empty title, and Reddit rejects that too.

Suggestions: skip leading blank lines when picking the title, and throw a clear local error when the title is empty or longer than 300 code points. You could also fall back to using the full message as the body when the first line is too long, but that's a product decision.

Comment thread src/strategies/reddit.js
* @type {number}
* @const
*/
MAX_MESSAGE_LENGTH = 40300;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40300 (40,000-character body plus a 300-character title) is compared against calculateMessageLength(message), which counts the whole message. Because of the 300-character title limit, the MCP check-message-length tool will report a 1,000-character single-line message as fitting (1000/40300) when Reddit will actually reject it. The MCP resize-message flow relies on these values, so it would be good to make the length check reflect the title constraint. For example, calculateMessageLength could return a value that exceeds MAX_MESSAGE_LENGTH when the first line is over 300, or post() could at least fail fast with a clear message (see the comment on the title line).

Comment thread src/strategies/reddit.js
signal: postOptions?.signal,
});

const result = /** @type {RedditSubmitResponse} */ (await response.json());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.json() runs before the response.ok check. When Reddit returns a non-JSON body (e.g. an HTML block/rate-limit page from the edge, or a 5xx), this throws a SyntaxError, and the user never sees the HTTP status.

Also, most OAuth-level errors don't use the json.errors shape. A bad or expired token returns 401 with {"message": "Unauthorized", "error": 401}, so the !response.ok branch reports "Unknown Reddit API error."

Suggestion: read the body as text first, try JSON.parse, and in the !ok branch fall back to body.message, or to the raw text when there's no JSON. On 429, including X-Ratelimit-Reset in the message would also help.

Comment thread src/strategies/reddit.js
Authorization: `Bearer ${this.#options.accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Crosspost (https://github.com/humanwhocodes/crosspost, v1.0.4)", // x-release-please-version

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The // x-release-please-version marker only gets updated for files listed in release-please-config.json under extra-files. discord.js, discord-webhook.js, and telegram.js are listed there, but src/strategies/reddit.js isn't, so this will say v1.0.4 forever. Please add it to extra-files.

Minor: Reddit's API rules recommend <platform>:<app ID>:<version string> (by /u/<reddit username>) and say generic user agents get lower rate limits. The current string is honest and descriptive, so it's fine, but if you add a username option for the password grant you could use it here (e.g. nodejs:crosspost:v1.0.4 (by /u/<username>)). The README's curl example uses a different UA format than the code does.

Comment thread README.md

To enable posting to Reddit:

1. Go to [Reddit Apps](https://www.reddit.com/prefs/apps) and click "create another app...".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under Reddit's Responsible Builder Policy (published Nov 11, 2025), self-service creation of new Data API credentials was reportedly closed, and new apps now go through a manual approval request. I couldn't load the policy page on reddithelp.com directly, so please double-check, but if it holds, a new user following these steps won't get a client ID/secret. The README should at least note that approval is required, or that existing script-app credentials are needed.

Comment thread README.md

```shell
curl -u "<CLIENT_ID>:<CLIENT_SECRET>" \
-d "grant_type=password&username=<REDDIT_USERNAME>&password=<REDDIT_PASSWORD>" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues with this step:

  1. The token this produces expires after one hour (expires_in: 3600 in the response), so copying it into REDDIT_ACCESS_TOKEN only works briefly. This section will need rewriting once the strategy fetches its own token.
  2. Passing the Reddit account password with -d on the command line leaves it in shell history and visible in the process list. If a manual curl step remains, suggest reading the credentials from env vars or a file (e.g. -d @body.txt). Also note that the password grant doesn't work on accounts with 2FA enabled unless the TOTP code is appended to the password, which makes it unusable for unattended use.


it("should handle API errors", async () => {
server.post("/api/submit", {
status: 403,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture (a 403 whose body carries json.errors with RATELIMIT) doesn't match what Reddit sends. RATELIMIT comes back as a 200 with json.errors, which the next test already covers. Non-2xx responses look like {"message": "Unauthorized", "error": 401}. Suggest replacing this with a realistic 401 case asserting the message is surfaced, plus a test where the error body isn't JSON (e.g. text/html) to cover the response.json() ordering issue.

server.post(
{
url: "/api/submit",
headers: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage gaps worth adding here:

  • Assert the user-agent header is sent. Reddit requires a descriptive UA, so a regression there matters.
  • A title-only message ("Hello") should send text= empty.
  • A CRLF message ("Title\r\nBody") splits correctly.
  • Title validation: a leading blank line, or a first line over 300 characters, should be rejected locally once that validation exists.
  • If the token-fetch flow is added: token request body/auth header, token caching across two posts, and re-fetch after expiry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Strategy to post to Reddit

2 participants