Skip to content

feat(sources): X/Twitter social listening (twikit connector) + README update - #175

Open
matthewdonsemail-lab wants to merge 6 commits into
obris-dev:mainfrom
matthewdonsemail-lab:feat/social-listening-x
Open

feat(sources): X/Twitter social listening (twikit connector) + README update#175
matthewdonsemail-lab wants to merge 6 commits into
obris-dev:mainfrom
matthewdonsemail-lab:feat/social-listening-x

Conversation

@matthewdonsemail-lab

Copy link
Copy Markdown

WIP feature: X/Twitter social listening (twikit connector) + README update

This branch carries the social-listening feature work done for the matthewdonsemail-lab fork, presented as a PR onto main:

1. feat(sources): add twitter_search connector (twikit-based) with schema parity (1dcf731)

  • TwitterSearchSourceSpec (query, mode Top/Latest, count) added to the builtin source-spec union; NewTweetPayload added to FeedItemData; schema.json regenerated (82 models), --check passes.
  • TwikitClient with the live-cookie credential chain (TWITTER_COOKIES_JSONauth_token/ct0 → cookies file → login fallback) so existing session cookie JSONs keep working — no proxy, no vendor API.
  • Registered in sources/registry.py + connectors/__init__.py; source-kind invariant extended to the known five kinds.
  • Infra: Dockerfile builder gains git for the git-pinned twikit dep.
  • 9 new connector tests + invariant tests green; CLI suite 145/145; ruff + ty clean.

2. fix(sources/twitter): per-call twikit client + retryable empty-404 mapping (e611a6c)

Verified against live cookies (no proxy, no vendor API), two issues fixed:

  • "Event loop is closed" on multi-source feed polls — twikit's httpx client was bound to the first event loop. Fix: construct the twikit client per call, inside the running loop (cookies still resolved once).
  • X's empty-body 404 is a transient flake (same query succeeds on retry) — now mapped retryable instead of "tweet deleted", with a regression test.

Validation: ruff/ty/migrations/schema checks + 587 tests OK; live poll of a 4-source twitter_search feed → feed → watch → webhook delivered 44/44 HTTP 200, payload matched field-for-field against the Twenty socialEvent intake contract.

3. docs(readme): platform diagram + what-we've-done outline (785cd4d)

  • Diagram + "Where it listens" now show X/Twitter as a shipped source; Facebook, TikTok, Instagram added as "soon to be added" (upcoming).
  • "What's shipped today" lists the twitter_search source kind.
  • New "What we've done" section: connector, live-cookie auth, live-poll reliability fixes, verified end-to-end webhook delivery.
  • Roadmap connector list updated to match.

Notes

  • Fork: matthewdonsemail-lab/openmagpie (public), upstream: obris-dev/openmagpie.
  • Remaining go-live items (outside this PR): LLM engine decision for semantic_filter (ENGINE_BASE_URL/ENGINE_MODEL), and the Twenty workflow webhook URL (server-generated at publish on deploy).

…a parity

Ports the listeningkit Twitter-search listener into openmagpie-core as a built-in source kind, with twikit added as an openmagpie-core library dependency (git pin, unclecode fork) per operator direction - not vendored from listeningkit.

- schema: TwitterSearchSourceSpec (query, mode Top/Latest, count) added to the _BuiltinSourceSpec union; NewTweetPayload added to FeedItemData; schema.json regenerated (82 models); --check passes.
- connector: TwikitClient with the live-cookie credential chain (TWITTER_COOKIES_JSON -> TWITTER_COOKIE_AUTH_TOKEN/CT0 -> cookies file -> login fallback) so the working listeningkit cookie JSONs keep functioning; error taxonomy translated to ConnectorParseError; tests use fakes only (no live I/O).
- registration: twitter_search registered in sources/registry.py + connectors/__init__.py; payload self-registers; source-kind invariant test extended to the known five.
- infra: Dockerfile builder gains git for the git-pinned dep; docker-compose.override.yml (local-only, gitignored) publishes Postgres on host 5433 to coexist with the buzz stack.
- validation: 9 new connector tests + invariant tests green; CLI suite 145/145; ruff + ty clean.

Co-authored-by: Matthew Don <matthewdonsemail@gmail.com>
Signed-off-by: Matthew Don <matthewdonsemail@gmail.com>
…pping

Live X polling through the connector exposed two issues (verified against
live cookies from listeningkit/backend/credentials/personal.json, no
proxy, no vendor API):

1. Event loop is closed on 2nd/3rd source in a multi-source feed poll.
   TwikitClient built one twikit Client at import time; twikit's Client
   creates an httpx.AsyncClient bound to the *currently running* event
   loop, and TwikitClient.search() drives it with a fresh asyncio.run
   loop per call. The shared client died with the first loop, so later
   sources in the same poll failed. Fix: construct the twikit Client
   inside _search_async (per call, inside the running loop); cookies are
   still resolved once in __init__.

2. X SearchTimeline intermittently 404s with an EMPTY body (reproduced:
   same query succeeds on immediate retry, session-independent). That is
   a transient upstream flake, not a deleted tweet/user; the old
   not_found/non-retryable mapping made ops treat a healthy source as
   dead. Fix: empty-body NotFound maps to search_timeline_unavailable
   (retryable=True, backoff); message-bearing NotFound stays not_found.

- client.py: per-call twikit Client construction + docstring.
- errors.py: empty-404 special case in map_twikit_error.
- tests_twitter.py: regression test for the retryable empty-404 mapping.

Validation: ruff check + format (416 files), ty, makemigrations --check,
schema.json --check, full Django suite 587 tests OK, live poll of the
x-buying-signals feed (4 twitter_search sources) green end-to-end.

Signed-off-by: matthewdonsemail-lab <matthewdonsemail@gmail.com>
Co-authored-by: Matthew Don <matthewdonsemail@gmail.com>
…ms, and a what-we've-done outline

Brings the README in line with the twitter_search connector work shipped
on this branch:

- Diagram + 'Where it listens' now show X/Twitter as a shipped source,
  and Facebook, TikTok, and Instagram as 'soon to be added'.
- 'What's shipped today' lists the twitter_search source kind.
- New 'What we've done' section outlines the twikit connector, live-cookie
  auth, the live-poll reliability fixes, and the verified end-to-end
  feed -> watch -> webhook delivery (44/44 HTTP 200, payload matched
  against the Twenty socialEvent intake contract).
- Roadmap's connector list updated to match (X shipped; FB/TikTok/IG next).

Co-authored-by: Matthew Don <matthewdonsemail@gmail.com>
Signed-off-by: Matthew Don <matthewdonsemail@gmail.com>
Mermaid treats unquoted edge labels with spaces as separate tokens; quote
them to keep the diagram parseable on GitHub's renderer.

Co-authored-by: Matthew Don <matthewdonsemail@gmail.com>
Signed-off-by: Matthew Don <matthewdonsemail@gmail.com>
@matthewdonsemail-lab

Copy link
Copy Markdown
Author

@joshpanka alright there mate thought I'd throw together a PR as I was looking for a social listening tool myself thought about doing a whole redesign for one myself but found this and thought I'd contribute I like the concept of this here and I know other libraries to then intergrate I am specifically looking to only merge unofficial api clients into the product and if they're broken me and my team can maintain them aslong as this tools keep active maintainership! Really neat stuff here great work.

If there's issues flag them for me and I'll look through them I got status 200 from web search without proxies I think that would also be a good addition into the project aswell but I'll continue to work off of this PR here.

Adds optional pattern_id to the webhook body's per-item source object
(WebhookSource), sourced from FeedItem.source_meta (operator-supplied Source.meta
tags copied onto each item at record time). Receivers can now attribute yield by
listening pattern - e.g. the Twenty socialEvent intake's patternId field -
without deriving it by convention.

- schema: WebhookSource.pattern_id (str | None, default None) - the legacy
  contract (label+kind only) still validates; untagged sources emit null
- ActionItem gains source_meta (defaulted) so the action layer can read the tag
- run_inputs populates source_meta from the FeedItem column
- webhook _build_payload maps source_meta.pattern_id -> source.pattern_id
- tests: tagged -> pattern_id present; untagged -> null; legacy payload valid

Co-authored-by: Matthew Don <matthewdonsemail@gmail.com>
Signed-off-by: Matthew Don <matthewdonsemail@gmail.com>
@matthewdonsemail-lab

Copy link
Copy Markdown
Author

Status update — 5 commits, contract closed end-to-end, CI awaiting the first-time-contributor gate.

This PR now carries the full X/Twitter social-listening feature:

  • 1dcf731twitter_search connector (twikit, live-cookie auth; no proxy/vendor API)
  • e611a6c — live-poll reliability fixes (per-call twikit client; retryable empty-404)
  • 785cd4d + 928bd82 — README: X shipped, Facebook/TikTok/Instagram upcoming, what-we've-done outline
  • b8a8a17 — webhook payload now carries source.pattern_id (yield attribution; optional, backward compatible)

Verified live: 44 real tweets through feed → watch → webhook, 44/44 HTTP 200, payload matched against the receiving intake contract. Local gates green (ruff 416 files, ty, migrations, schema sync; full suite passed at the earlier commits).

For maintainers: CI runs are parked in action_required (first-time-contributor gate) — please click Approve and run on the workflow so checks execute. The source.pattern_id addition is consumed by the receiving social-event intake manifest (verified cross-repo) and is fully backward compatible (untagged sources emit null; legacy {label, kind} payloads still validate).

twikit 2.3.3 renders every HTTP error as `status: <code>, message:
"<body>"` (client/client.py and guest/client.py), so str(exc) is never
empty, not even for an empty body. The e611a6c guard
`not str(exc).strip()` therefore never fired: live polls showed the
transient X SearchTimeline empty-404 mapping to not_found
(non-retryable), which made ops treat a healthy source as dead.

Match the empty BODY instead: an empty body renders the trailing payload
`message: ""`; a real 404 carries response text.

- errors.py: empty-404 detection checks the rendered body suffix.
- tests_twitter.py: connector-level empty-404 test now routes through a
  real twikit NotFound + map_twikit_error; added map-level tests pinning
  empty-body 404 -> search_timeline_unavailable (retryable) and
  message-bearing 404 -> not_found (non-retryable).

Validation: ruff check + format, ty, whitespace check, twitter tests
(12) and full core suite (590) OK.

Co-authored-by: Matthew Don <matthewdonsemail@gmail.com>
Signed-off-by: Matthew Don <matthewdonsemail@gmail.com>
@matthewdonsemail-lab

Copy link
Copy Markdown
Author
image

Additional Context of our usecase we use it inside of https://github.com/block/buzz and we've got it ingesting content every 300 seconds and passing out via a webhook this here works in production now.

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