Skip to content

feat(pinterest): add Pinterest adapter suite - #2177

Open
yeeway0609 wants to merge 2 commits into
jackwener:mainfrom
yeeway0609:feat/pinterest-adapter
Open

feat(pinterest): add Pinterest adapter suite#2177
yeeway0609 wants to merge 2 commits into
jackwener:mainfrom
yeeway0609:feat/pinterest-adapter

Conversation

@yeeway0609

@yeeway0609 yeeway0609 commented Jul 25, 2026

Copy link
Copy Markdown

Description

Adds a pinterest adapter covering both read and write surfaces — 19 commands over Pinterest's
internal resource API (POST /resource/<Name>Resource/<action>/, form-urlencoded source_url +
data, X-CSRFToken + X-Pinterest-PWS-Handler, feeds paginated by resource_response.bookmark).
Reads work on a logged-out session because Pinterest issues a csrftoken to anonymous visitors too;
only the writes need a login.

search-pins / search-boards / search-users        pin / user / user-pins / user-boards
board-pins / board-sections / download            save / pin-create / pin-update / pin-delete
board-create / board-update / board-delete        board-section-create / board-section-delete

Boards are addressed by <username>/<slug>, a board URL, or the numeric boardId from
user-boards; sections by slug or id. Deletes require --confirm and otherwise name the resolved
target and exit non-zero. --limit and flag combinations are validated before any request goes
out.

Related issue:

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🌐 New site adapter
  • 📝 Documentation
  • ♻️ Refactor
  • 🔧 CI / build / tooling

Checklist

  • I ran the checks relevant to this PR
  • I updated tests or docs if needed
  • I included output or screenshots when useful

Documentation (if adding/modifying an adapter)

  • Added doc page under docs/adapters/ (if new adapter)
  • Updated docs/adapters/index.md table (if new adapter)
  • Updated sidebar in docs/.vitepress/config.mts (if new adapter)
  • Updated README.md / README.zh-CN.md when command discoverability changed
  • Used positional args for the command's primary subject unless a named flag is clearly better
  • Normalized expected adapter failures to CliError subclasses instead of raw Error

Screenshots / Output

All 19 commands live-verified against a logged-in account; everything created during verification
was removed afterwards with the adapter's own delete commands.

$ opencli pinterest search-pins "nordic interior" --limit 2 -f json
[
  { "pinId": "703756189347071", "title": "Scandinavian Living Room Design- On a Budget", "pinner": "kathareineke",
    "board": "Wohnzimmer", "imageUrl": "https://i.pinimg.com/originals/36/5d/0a/365d0a842973e6bd8e009588f317cc86.png",
    "url": "https://www.pinterest.com/pin/703756189347071/" },
  { "pinId": "9781324187251346", "title": "", "pinner": "fabianamiyai", "board": "Estilo Escandinavo",
    "url": "https://www.pinterest.com/pin/9781324187251346/" }
]

$ opencli pinterest user yeeway0609 -f json
[ { "username": "yeeway0609", "fullName": "Alex", "followerCount": 11, "followingCount": 11,
    "interestFollowingCount": 4, "pinCount": 2254, "boardCount": 10 } ]

$ opencli pinterest board-sections yeeway0609/art -f json
[ { "sectionId": "5278516809142679899", "title": "Pixel", "slug": "pixel", "pinCount": 9 },
  { "sectionId": "5064878735868608231", "title": "Anime", "slug": "anime", "pinCount": 62 } ]

Full write cycle, on a scratch board created for the run:

$ opencli pinterest board-create "OpenCLI adapter test" --privacy secret -f json
[ { "boardId": "720576077820063683", "name": "OpenCLI adapter test", "privacy": "secret" } ]

$ opencli pinterest pin-create "https://images.unsplash.com/photo-1552053831-71594a27632d?w=1200" \
    --board yeeway0609/opencli-adapter-test --section dog --title "OpenCLI dog pin" -f json
[ { "pinId": "720576009177385394", "board": "OpenCLI adapter test", "title": "OpenCLI dog pin" } ]
  → pin 720576009177385394 section = "Dog"

$ opencli pinterest save 3096293491909700 --board yeeway0609/opencli-adapter-test --section dog -f json
[ { "pinId": "720576009177385389", "sourcePinId": "3096293491909700", "board": "OpenCLI adapter test" } ]

$ opencli pinterest save 3096293491909700 --board yeeway0609/opencli-adapter-test --section nope
error: ARGUMENT  No section matching "nope" on this board
  help: Pass a section slug or id — available: dog (3709610207346291072), cats (3709593716249242688)

$ opencli pinterest board-delete yeeway0609/opencli-adapter-test
error: ARGUMENT  Refusing to delete board "OpenCLI adapter test" (7 pins) without --confirm

$ opencli pinterest board-delete yeeway0609/opencli-adapter-test --confirm -f json
[ { "boardId": "720576077820063683", "name": "OpenCLI adapter test", "pinCount": 7, "deleted": true } ]

Field clearing and the delete preview, on a second scratch board:

$ opencli pinterest pin-update 720576009177386379 --description "" -f json
$ opencli pinterest pin 720576009177386379 -f json
[ { ..., "description": "", "link": "https://example.com/" } ]      # description cleared

$ opencli pinterest pin-update 720576009177386379 --link ""
error: AUTH_REQUIRED  Pinterest refused this write: 你無權存取該資源。   # Pinterest owns a scraped pin's link

$ opencli pinterest pin-delete 720576009177386379
error: ARGUMENT  Refusing to delete pin 720576009177386379 "Review test pin" from board "OpenCLI review test" without --confirm

Notes for review:

  • Sections can only be set by a follow-up move. PinResource/create and RepinResource/create
    accept a section key, answer HTTP 200, and file the pin at the board root anyway; only
    PinResource/update honours one, under board_section_id (a section_id key is likewise
    accepted and ignored). So save --section / pin-create --section create then move, and a failed
    move raises CommandExecutionError naming the pin already created instead of reporting success.
    Placement was verified on each pin's own section field, not the section's cached pin_count.
  • Board URLs are percent-decoded before use. Pinterest hands out encoded slugs for non-ASCII
    board names (/u/test-board-caf%C3%A9-%E6%B8%AC%E8%A9%A6/) and posting one verbatim answers
    HTTP 404. Slugs compare Unicode-normalized, so an NFD-composed accent still matches.
  • Identifiers only. A board display name is rejected rather than guessed at, since a bare name
    cannot say which account owns the board; BoardResource accepts board_id, which is what makes
    the numeric form work, and the board it returns is reused so that path costs no extra round trip.
    A /pin/<id>/ or /search/ URL is reported as such instead of being parsed as the board
    pin/<id>, and a board ref passed where a username belongs says so rather than 404ing on a
    mangled name.
  • Empty string clears, omitted leaves alone. Optional text flags carry no default, so
    --description "" clears the field while omitting it changes nothing. Pinterest refuses link
    edits on pins it scraped and answers 401 for that, so its own message is surfaced
    (Pinterest refused this write: 你無權存取該資源) rather than only "log in".
  • A 403 on a read is a rejected request (CommandExecutionError, hinting at a stale CSRF token); a
    403 on a write is AuthRequiredError. page.goto can resolve before the csrftoken cookie
    exists, so the in-page script waits for it instead of firing a doomed request.
  • save without --board posts a boardless repin, which Pinterest files under "Quick saves".
    pin-create cannot do that — a new pin always needs a board. Pinterest also overrides
    --description for scraped pins.
  • user exposes interestFollowingCount because Pinterest's following_count bundles followed
    people and topics.

clis/pinterest/*.test.js passes 131 / 131 (ref and board-id parsing, the auth / CSRF / HTTP-error
ladder, bookmark pagination with dedup, promoted-pin filtering, the --confirm gate on all three
deletes, the section path (no section key on create, board_section_id on the follow-up, a failed
move surfacing the pin id, a display title not resolving, --section without --board rejected),
and that a board id is not re-fetched. Full suite 6258 passed;
tsc --noEmit clean; opencli validate 0 errors; check:typed-error-lint and
check:silent-column-drop both new=0; check-doc-coverage.sh --strict 174/174.

@yeeway0609
yeeway0609 force-pushed the feat/pinterest-adapter branch from 340c2ef to 70b5984 Compare July 25, 2026 14:40
Adds `opencli pinterest` with 19 commands over Pinterest's internal resource API
(`POST /resource/<Name>Resource/<action>/`, form-urlencoded `source_url` + `data`,
`X-CSRFToken` + `X-Pinterest-PWS-Handler` headers, `bookmark` paging).

- Read: search-pins / search-boards / search-users, pin, user, user-pins,
  user-boards, board-pins, board-sections, download. Reads work anonymously
  because Pinterest issues a csrftoken to logged-out sessions too.
- Write: save (boardless repin lands in "Quick saves"), pin-create /
  pin-update / pin-delete, board-create / board-update / board-delete,
  board-section-create / board-section-delete.
- Deletes require `--confirm`; without it the command resolves and names the
  target, then exits non-zero via ArgumentError (pin title + board for
  pin-delete, pin count for board-delete, section title for
  board-section-delete).
- Boards are addressed by `<username>/<slug>`, a board URL, or the numeric
  `boardId` (BoardResource accepts `board_id` and reports the board's url, which
  is reused so the id path costs no extra round trip); sections by id or slug.
  Display names are not accepted — a name alone cannot say which account a board
  belongs to. A Pinterest site route such as a `/pin/<id>/` URL is rejected as
  such instead of being parsed as the board `pin/<id>`.
- Board URLs are percent-decoded before use: Pinterest hands out encoded slugs
  for non-ASCII board names, and posting those verbatim answers HTTP 404. Slugs
  are compared Unicode-normalized so an NFD-composed accent still matches.
- Sections can only be set by a follow-up move. PinResource/create and
  RepinResource/create accept a section key, answer HTTP 200, and file the pin
  at the board root anyway; only PinResource/update honours it (under
  `board_section_id`, not `section_id`). So `save --section` and
  `pin-create --section` create then move, and report the created pin id if the
  move fails rather than claiming success.
- Omitting an optional text flag leaves the field alone; passing an empty string
  clears it. Pinterest refuses link edits on pins it scraped, and answers 401
  for that, so its own message is surfaced rather than only "log in".
- Typed errors throughout: ArgumentError for bad refs, unknown sections,
  `--section` without `--board`, and limits (validated before any request, with
  no silent clamp); AuthRequiredError on 401 (and 403 on writes only, since
  reads are anonymous); CommandExecutionError for malformed payloads and
  unresolvable targets.

Live-verified end-to-end against a logged-in account: all 10 read commands, and
the full write cycle (board-create → board-section-create → board-update →
pin-create → pin-update → save → the three deletes, preview and confirmed),
including section placement checked on each pin's own `section` field, non-ASCII
board/section slugs, board-id addressing, and clearing a description. 131 tests;
full suite 6258 passed; `tsc --noEmit` clean; `opencli validate` 0 errors;
typed-error-lint and silent-column-drop both new=0; doc coverage 174/174.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@yeeway0609
yeeway0609 force-pushed the feat/pinterest-adapter branch from 70b5984 to 6fb51b3 Compare July 25, 2026 15:03
`coerceAndValidateArgs` applies an arg's default and then enforces `choices`
against it, so `default: ''` on a public|secret flag rejected every run that
omitted `--privacy`:

    $ opencli pinterest board-update janedoe/my-board --name Foo
    error: ARGUMENT  Argument "privacy" must be one of: public, secret. Received: ""

Leaving the default off keeps the flag optional; the command already reads it
as `String(kwargs.privacy ?? '')`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yeeway0609
yeeway0609 marked this pull request as ready for review July 25, 2026 15:20
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.

1 participant