Skip to content

Post email-intake coupons to the c0upons.com/bbs forums - #82

Merged
ralyodio merged 1 commit into
masterfrom
feat/bbs-post-email-coupons
Oct 6, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/bbs-post-email-coupons

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

What

Every coupon that mail to submit@c0upons.com creates now also gets a thread on https://c0upons.com/bbs, in the forum that fits it, linking back to its coupon page. This applies to the email intake only: the nichedb, Flipp and Reddit syncs import in bulk and would flood the forums.

  • Forum choice: coupons have no category, so the forum is read from the offer's words.
    • Freebies: a free item. "Free shipping" doesn't count.
    • Topic forums: Grocery & Weekly Ads, Tech Deals, Home & Garden, Clothing & Beauty, Travel Deals.
    • Otherwise: Coupons & Promo Codes for a code, Hot Deals for a sale.
  • Thread: the title is Store: offer (code X). The body has store, code, deal and expiry, plus "Get this deal on c0upons".
  • Once only: the bbs_posts table makes sure a coupon is posted once.
  • Pacing: posts go out 16 s apart, with one retry on tsbb's flooding refusal (its guard is 15 s per account). The webhook doesn't wait for this: it answers Forward Email first.
  • Account: posts are made as board user c0upons (Pages cache with empty data — Next.js Full Route Cache not bypassed #4) using a tsbb_ token.
    • Already done: ops/bbs/mint-poster.ts was run on the live board, and the token is in vault c0upons--prod as BBS_POST_TOKEN.
    • Verified: GET /bbs/api/v1/me with the token answers c0upons.
  • Without the token, nothing is posted.

Verified

  • bun test test: 77 pass, including 7 new ones covering:
    • forum choice
    • thread text
    • once-only posting
    • flood-guard retry and spacing
    • no token
    • a non-flood refusal recorded without retrying
    • the intake handing over the coupon ids it created
  • tsc --noEmit and eslint on the changed files are clean.
  • No live test thread was posted: the email log has no real coupons yet, only welcome mails and the earlier test.

After merge

CI deploys to dev2, and the deploy's vault step puts BBS_POST_TOKEN into app.env. The next real offer mail will open a thread.

🤖 Generated with Claude Code

Each coupon submit@c0upons.com creates gets a thread on the tsbb board, in the
forum its words fit (freebies, grocery, tech, home, fashion, travel; else
Coupons & Promo Codes for a code, Hot Deals for a sale), linking to its page.

- lib/bbs-post.ts: pickForum, topicFor, postCouponsToBbs (POST
  /api/v1/forums/<slug>/topics with BBS_POST_TOKEN; bbs_posts table so a
  coupon is posted once; 16 s spacing and one retry for tsbb's flood guard)
- inbound-email: returns `created` (id, store slug, code, discount, expiry)
  for the coupons it inserted; the webhook posts them after answering
- ops/bbs/mint-poster.ts: board user `c0upons` + its token (already run;
  token in vault c0upons--prod)
- 7 tests (forums, thread text, once-only, flood retry, no token, refusals,
  intake hand-over)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread apps/web/lib/bbs-post.ts
let last = 0;
for (const c of coupons) {
const forum = pickForum(c);
const prior = await db.sql`SELECT status, topic_url FROM bbs_posts WHERE coupon_id = ${c.id} LIMIT 1`;
});
result.posted.push({ store: name, code: o.code, title: o.title });
// upsertCoupon returns nothing; the row's id is what its page and its thread link to.
const row = await db.sql`SELECT id FROM coupons WHERE source = ${SOURCE} AND source_id = ${sourceId} LIMIT 1`;
Comment thread test/bbs-post.test.mjs
const out = await postCouponsToBbs(db, [coupon({ id: 301 })], { fetch: b.fetch, token: '', sleep: noSleep });
assert.equal(out[0].status, 'disabled');
assert.equal(b.calls.length, 0);
const rows = await db.sql`SELECT * FROM bbs_posts WHERE coupon_id = ${301}`.catch(() => []);
Comment thread test/bbs-post.test.mjs
assert.equal(out[0].status, 'failed');
assert.match(out[0].error, /403/);
assert.equal(calls.length, 1);
const [row] = await db.sql`SELECT status FROM bbs_posts WHERE coupon_id = ${401}`;
Comment thread test/bbs-post.test.mjs
);
assert.equal(res.outcome, 'posted');
assert.equal(res.created.length, 1);
const [row] = await db.sql`SELECT id, code FROM coupons WHERE id = ${res.created[0].id}`;
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

37 finding(s)

MEDIUM: 31 | LOW: 6

Severity Rule Location
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:18
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:34
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:43
MEDIUM sql-template-interpolation apps/web/app/api/bounties/route.ts:81
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:68
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:75
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:31
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:33
MEDIUM sql-template-interpolation apps/web/app/api/stores/[slug]/route.ts:9
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:83
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:90
MEDIUM js-open-redirect apps/web/app/bounties/new/page.tsx:57
MEDIUM js-unescaped-html-sink apps/web/app/coupons/[id]/page.tsx:57
MEDIUM js-unescaped-html-sink apps/web/app/layout.tsx:88
MEDIUM sql-template-interpolation apps/web/app/stores/[slug]/page.tsx:15
MEDIUM sql-template-interpolation apps/web/lib/bbs-post.ts:164
MEDIUM sql-template-interpolation apps/web/lib/flipp-sync.ts:445
MEDIUM sql-template-interpolation apps/web/lib/inbound-email.ts:573
MEDIUM sql-template-interpolation apps/web/lib/inbound-email.ts:643
MEDIUM sql-template-interpolation apps/web/lib/mcp-tools.ts:81
MEDIUM sql-template-interpolation apps/web/lib/members.ts:54
MEDIUM sql-template-interpolation apps/web/lib/nichedb-sync.ts:186
MEDIUM sql-template-interpolation apps/web/lib/nichedb-sync.ts:206
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:336
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:355
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:361
MEDIUM sql-template-interpolation apps/web/lib/reveal-coupon.ts:115
MEDIUM sql-template-interpolation apps/web/lib/reveal-coupon.ts:170
MEDIUM sh-predictable-temp-path apps/web/public/install.sh:37
MEDIUM sh-predictable-temp-path apps/web/public/install.sh:39
MEDIUM sql-template-interpolation apps/web/scripts/migrate.mjs:192
LOW sql-template-interpolation test/bbs-post.test.mjs:129
LOW sql-template-interpolation test/bbs-post.test.mjs:143
LOW sql-template-interpolation test/bbs-post.test.mjs:164
LOW sql-template-interpolation test/inbound-email.test.mjs:139
LOW sql-template-interpolation test/reveal-sweep.test.mjs:69
LOW sql-template-interpolation test/reveal-sweep.test.mjs:112

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 1bb70a8 into master Oct 6, 2026
5 checks passed
@ralyodio
ralyodio deleted the feat/bbs-post-email-coupons branch October 6, 2026 08:49
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.

2 participants