Skip to content

feat: comment-moderation commands — report, ban, report-user, bans, moderation helpers - #18

Merged
griffinwork40 merged 3 commits into
griffinwork40:mainfrom
griffincapmisc:feat/comments-report-ban
Oct 12, 2026
Merged

griffinwork40 merged 3 commits into
griffinwork40:mainfrom
griffincapmisc:feat/comments-report-ban

Conversation

@griffincapmisc

Copy link
Copy Markdown
Contributor

Summary

Adds CLI commands to report and ban scam accounts impersonating Charlie Garcia on charliepgarcia.substack.com ("Charlie WhatsApp +1672..." pattern, 17–111 filler comments per burst).

New commands

Command Description
comments report <comment_id> Report a comment (spam/impersonation/etc)
comments report-user <user_id> Report a user profile (routes to substack.com)
comments ban <comment_id> Ban a commenter with expiry + visibility control
comments bans <user_id> Look up existing bans for a user (read-only)
moderation report-categories Fetch Substack report category tree (read-only)
moderation delete-reasons Fetch comment deletion reason codes (read-only)

All write commands:

  • Require SUBSTACK_ENABLE_WRITE=true
  • Default to dry-run: print exact {method, url, body} without firing
  • Pass --no-dry-run / --yes to execute

Endpoints (reverse-engineered from bundle bb5e503)

POST /api/v1/comment/{comment_id}/report
  {publication_id, details, reportCategory, reportedToSubstack, report_context}

POST https://substack.com/api/v1/users/{user_id}/profile/report
  {details, reportCategory, surface:"profile", reportedToSubstack, report_context}

POST /api/v1/comment_ban/{comment_id}
  {user_id, expiry, commentVisibility}

GET  /api/v1/user/{user_id}/bans
GET  https://substack.com/api/v1/reporting/flows
GET  /api/v1/comment/moderation/delete_reasons

Bundle findings

  • commentVisibility enum: "visible" | "delete_all_comments" — confirmed from ban UI toggle in bundle
  • reportCategory values: spam, impersonation, hate, private_information, plagiarism, illegal_activity, pornography, other — confirmed from bundle module 74588
  • expiry: ISO-8601 datetime string — inferred from isAfter() comparison in ban UI; not live-verified
  • report_context: shape not found in bundle; accepted as optional --report-context <JSON> string; omit in most cases

Smoke-test recipe (dry-run first, then real)

# Step 1: dry-run report
SUBSTACK_ENABLE_WRITE=true substack comments report <COMMENT_ID> \
  --category impersonation \
  --details "Scam account impersonating Charlie Garcia, soliciting WhatsApp contact"

# Step 2: fire for real
SUBSTACK_ENABLE_WRITE=true substack comments report <COMMENT_ID> \
  --category impersonation \
  --details "Scam account impersonating Charlie Garcia, soliciting WhatsApp contact" \
  --no-dry-run

# Step 3: dry-run ban (permanent, delete comments)
SUBSTACK_ENABLE_WRITE=true substack comments ban <COMMENT_ID> \
  --user-id <USER_ID> \
  --expiry "2126-10-12T00:00:00Z" \
  --visibility delete_all_comments

# Step 4: execute ban
SUBSTACK_ENABLE_WRITE=true substack comments ban <COMMENT_ID> \
  --user-id <USER_ID> \
  --expiry "2126-10-12T00:00:00Z" \
  --visibility delete_all_comments \
  --no-dry-run

Tests

24 new tests in tests/test_moderation.py. Full suite: 385 passed.

Open questions / not live-verified

  • report_context shape: unknown, accepted as optional JSON string
  • expiry format: inferred ISO-8601, not exercised against real API
  • Profile report response shape: endpoint confirmed in bundle, not live-called
  • Notes/cross-domain comments may need host routing to substack.com (not yet exposed as a flag)

New commands
------------
  comments report <comment_id>       report a comment to publication owner + optionally Substack T&S
  comments report-user <user_id>     report a user profile (routes to substack.com host)
  comments ban <comment_id>          ban a commenter; expiry=ISO-8601, visibility=visible|delete_all_comments
  comments bans <user_id>            read existing bans for a user (read-only)
  moderation report-categories       fetch Substack reporting flow tree (read-only)
  moderation delete-reasons          fetch comment deletion reason codes (read-only)

All write commands:
  - gated by is_write_enabled() (SUBSTACK_ENABLE_WRITE=true required)
  - default to dry-run: print exact {method, url, body} without firing
  - pass --no-dry-run / --yes to execute

Endpoints (reverse-engineered from Substack web bundle bb5e503)
---------------------------------------------------------------
  POST /api/v1/comment/{id}/report
    body: {publication_id, details, reportCategory, reportedToSubstack, report_context}
  POST https://substack.com/api/v1/users/{id}/profile/report
    body: {details, reportCategory, surface:"profile", reportedToSubstack, report_context}
  POST /api/v1/comment_ban/{comment_id}
    body: {user_id, expiry, commentVisibility}
  GET  /api/v1/user/{id}/bans
  GET  https://substack.com/api/v1/reporting/flows
  GET  /api/v1/comment/moderation/delete_reasons

Bundle findings
---------------
  commentVisibility enum: "visible" | "delete_all_comments" (confirmed)
  reportCategory values: spam, impersonation, hate, private_information,
    plagiarism, illegal_activity, pornography, other (confirmed from module 74588)
  expiry: ISO-8601 datetime string (inferred from isAfter() comparison in ban UI)
  report_context: shape not found in bundle; accepted as optional JSON string

Tests: 24 new tests in test_moderation.py; full suite 385 passed
Motivation: scam accounts impersonating Charlie Garcia with WhatsApp solicitation
  comments (17-111 per burst) need CLI-level report+ban for agent automation
…nt visibility

Substack's ban modal posts expiry as one of 1d/30d/90d/1y/1000y (bundle
module 92538) rather than an ISO datetime, and its comment_visibility select
has three options (bundle module 5646): visible, delete_this_comment,
delete_all_comments. Validate both in the client function and the CLI, and
update docs/tests.
@griffincapmisc

Copy link
Copy Markdown
Contributor Author

Correction pushed (2bfcbf8): ban expiry is not an ISO datetime. Substack's ban modal posts one of 1d, 30d, 90d, 1y, 1000y (1000y = Forever), per bundle module 92538. commentVisibility also has a third value, delete_this_comment (module 5646). Both are now validated in ban_commenter() and the CLI; README and tests updated (388 passed). The smoke-test commands in the description that use ISO dates should use --expiry 1000y instead.

@griffinwork40

Copy link
Copy Markdown
Owner

land round 1: 4 blocking findings @ 2bfcbf8

Local gate at 2bfcbf8: pytest -q → 388 passed. The comment-moderation commands themselves look solid: write gate fires before any client/network call, enums validated, host routing correct. Four things block the merge:

  1. medium · correctness · skills/substack/scripts/substack_cli/moderation.py:103-110, 281-283: comments report does live network GETs to /api/v1/publication/self even in dry-run mode (a probe saw 4 GET attempts, retries included, before the dry-run body printed). That breaks the "prints the request without sending" contract. _resolve_pub_id also swallows every exception, so publication_id gets dropped from the body without any notice. The endpoint is unverified: nothing else in the codebase uses it, and chat.py:29 / references/substack-api.md:65 say no publication-id resolver exists.
    Fix: remove _resolve_pub_id. Add an optional --publication-id <int> option that goes into the body only when given, so dry-run makes zero HTTP calls. Add a test asserting that dry-run issues no request.

  2. low (escalated: maintainer hard rule, no source file over 350 LOC) · moderation.py:1-523: the new file is 523 lines.
    Fix: move the enum constants, core functions (report_comment, report_user_profile, ban_commenter, get_*) and a shared body builder into a new substack_cli/moderation_core.py. Keep the CLI commands in moderation.py, with both files under 350 LOC. Build the CLI's dry-run body with the same builder the core function POSTs, so dry-run and live bodies can't drift. Update the test imports to match.

  3. low (escalated: project contract in AFK.md, "adding an endpoint → update SKILL.md and references/substack-api.md") · skills/substack/SKILL.md:149-155: the comments block doesn't list comments report | report-user | ban | bans or the moderation subapp.
    Fix: add them, with their write-gate and dry-run notes.

  4. low (escalated: same contract) · skills/substack/references/substack-api.md endpoint table: there are no rows for the six new routes.
    Fix: add one row per route with method, path, host (P/A), auth, confidence "bundle-derived, not live-verified", and quirks (expiry enum 1d|30d|90d|1y|1000y, commentVisibility enum).

Non-blocking items (invalid --report-context JSON is forwarded silently as a raw string; no CLI-layer tests for bans/report-categories/delete-reasons; stale "234 tests" README count) will be tracked as follow-ups unless they're fixed here.

…ion_core.py

Address land round 1 review on PR griffinwork40#18:

- comments report no longer calls the unverified /api/v1/publication/self
  endpoint (which fired even in dry-run and silently dropped publication_id
  on failure). publication_id is now an explicit optional --publication-id.
- Split enums, core API functions and shared body builders into
  substack_cli/moderation_core.py so dry-run and live bodies come from the
  same builder; both modules are under 350 LOC.
- Document the new commands in SKILL.md and the new routes in
  references/substack-api.md.
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