fix(person): detect custom note quota block in connect_with_person#449
Conversation
d11bda3 to
21a9e3e
Compare
|
@greptileai review |
Greptile SummaryThis PR adds detection for LinkedIn Premium note-quota blocks in the connection flow. It changes:
Confidence Score: 5/5This looks safe to merge.
Reviews (5): Last reviewed commit: "Merge branch 'main' into fix/448-custom-..." | Re-trigger Greptile |
|
@greptileai review |
Closes stickerdaniel#448. When LinkedIn shows the Premium upsell modal because the free personalized invitation-note quota is exhausted, the previous code path silently funnelled the failure through `send_failed` while reporting `note_sent: true` (the note had been written into the textarea but was never actually delivered). Changes: - `_submit_invite_dialog` now returns a three-tuple `(submitted, note_sent, note_limit_blocked)`. `note_sent` reports *delivery* rather than *textarea fill*, so it is False on every failure path including the Premium-modal interception. The dialog cleanup contract is unchanged — callers still must not dismiss the dialog themselves. - New `_detect_premium_upsell_modal` helper. The signal is locale-independent: the upsell modal renders a `[role="dialog"]` whose primary anchor href contains `/premium/`. This matches the project's existing rule of keying on stable URL fragments rather than localized text (mirrors the `/preload/custom-invite/` gate used elsewhere in this module). - `_connection_result` accepts an optional `can_send_without_note` keyword that is surfaced on the response only when set, so existing status payloads keep their current shape. - `connect_with_person` returns a new `custom_note_limit_reached` status when `_submit_invite_dialog` reports the upsell interception. The response carries `note_sent: False` and `can_send_without_note: True`, giving the calling LLM enough structure to retry with `note=None`. - README-visible status list and the public docstring in `tools/person.py` updated with the new status and its semantics. - New `test_connect_with_person_custom_note_limit_reached` covers the tool-level contract; existing connect tests remain green.
LinkedIn can swap the invite dialog for the Premium upsell at submit time, leaving the original primary button detached or pointer-event covered. _click_dialog_primary_button returns False, the keyboard fallback also fails, and the not-sent branch dismissed the dialog without reading the upsell text, so the caller returned connect_unavailable instead of custom_note_limit_reached with LinkedIn's raw quota message. Move the upsell probe ahead of the dismiss, matching the three other Premium-detection checkpoints in _submit_invite_dialog. A new test covers the submit-click failure path. Refs stickerdaniel#448
6598add to
978ad34
Compare
| if btn_count >= 3: | ||
| try: | ||
| await buttons.nth(btn_count - 2).click() | ||
| except Exception: | ||
| logger.debug("Could not open invite note editor", exc_info=True) |
There was a problem hiding this comment.
When the no-anchor probe opens LinkedIn's current two-button invite gate, this guard skips the only safe Add a note button because btn_count is 2. The quota upsell can appear only after that button is clicked, so _probe_invite_note_limit() returns None and connect_with_person() reports connect_unavailable instead of custom_note_limit_reached for the path this PR is trying to cover. The submit path already handles this same two-button gate with btn_count >= 2; the probe should mirror that while still avoiding the primary send button.
| if btn_count >= 3: | |
| try: | |
| await buttons.nth(btn_count - 2).click() | |
| except Exception: | |
| logger.debug("Could not open invite note editor", exc_info=True) | |
| if btn_count >= 2: | |
| try: | |
| await buttons.nth(btn_count - 2).click() | |
| except Exception: | |
| logger.debug("Could not open invite note editor", exc_info=True) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 1861-1865
Comment:
**Probe skips two-button gate**
When the no-anchor probe opens LinkedIn's current two-button invite gate, this guard skips the only safe `Add a note` button because `btn_count` is `2`. The quota upsell can appear only after that button is clicked, so `_probe_invite_note_limit()` returns `None` and `connect_with_person()` reports `connect_unavailable` instead of `custom_note_limit_reached` for the path this PR is trying to cover. The submit path already handles this same two-button gate with `btn_count >= 2`; the probe should mirror that while still avoiding the primary send button.
```suggestion
if btn_count >= 2:
try:
await buttons.nth(btn_count - 2).click()
except Exception:
logger.debug("Could not open invite note editor", exc_info=True)
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
Fixes #448. When LinkedIn rejects an invite-with-note because the account's free personalized-note quota is exhausted, the old code path could return misleading/generic states such as
send_failedorconnect_unavailable.This PR keeps the response simple:
{ "status": "custom_note_limit_reached", "message": "<raw LinkedIn Premium dialog text>", "note_sent": false }No retry hints, no synthesized explanation. The
messageis read directly from the LinkedIn Premium upsell dialog.Behaviour changes
custom_note_limit_reachedonconnect_with_personwithnote_sent: false.can_send_without_note; callers only get the status plus LinkedIn's own message text.note_sentnow tracks delivery of the note, not textarea fill. Any failure path leaves itfalse.connected,accepted,send_failed,connect_unavailable, etc.) are unchanged.Detection signal
Locale-independent: the upsell renders a dialog whose body links to
/premium/.... The detector waits fordialog[open] a[href*="/premium/"]or[role="dialog"] a[href*="/premium/"], then reads the surrounding dialog text directly from LinkedIn.Implementation
_get_premium_upsell_messagewaits for the Premium link and returns the raw dialoginnerText/textContent._submit_invite_dialogreturns(submitted, note_sent, note_limit_message).Add noteopening the Premium upsell before a textarea appears,_probe_invite_note_limithandles no-anchor profiles by opening the deeplink only as a non-submitting probe and returning the raw dialog text if LinkedIn shows the upsell. It never clicks Send.tools/person.pydocstring updated.Testing
Latest head:
d11bda3.uv run --python 3.13.9 ruff check .clean.uv run --python 3.13.9 ruff format --check .clean.uv run --python 3.13.9 ty checkclean.34 passedfortests/test_scraping.py::TestConnectWithPersonandtests/test_tools.py::TestPersonTool.72 passedfor connect/person, network tool, and link metadata coverage.tests/test_browser_security.py::test_harden_linkedin_tree_noop_outside_linkedinon this machine (umask=002parent-dir mode); unaffected suite passes when that single known test is deselected.Synthetic prompt
Generated with Claude Opus 4.7
Closes #448