- Questions
- Tools/Libraries/Etc
- Tweets
NotificationsTimelineAPI Deep Dive and Pagination Cursor / ID Structure Decoding (Dec 2025)- Notification Deep Dive and React-Based Tweet Counting (May 2025)
- Ideas for Improving Twitter Notification UX (Dec 2024)
- Manual Notification Triage and Workflow Notes (May 2024 – Dec 2024)
- Gists for Workflow Optimization and Sorting (May 2024)
- Twitter API Limitations and Browser-Based Alternatives (Apr 2024)
- Twitter Blue vs Building a Custom ‘Top Articles’ Tool (Jul 2022)
- Unsorted
- See Also
- Can we get a 'peek notifications' userscript without triggering marking them read?
- https://devcommunity.x.com/t/announcing-the-python-and-typescript-xdks-for-the-x-api-v2/250860
-
Announcing the Python and Typescript XDKs for the X API v2
- https://docs.x.com/xdks/overview
-
XDK Overview
- https://docs.x.com/xdks/typescript/overview
-
TypeScript XDK
-
A comprehensive TypeScript SDK for the X API (formerly Twitter API) with advanced features including smart pagination, multiple authentication methods, real-time streaming, and full type safety.
-
- https://docs.x.com/xdks/python/overview
-
Python XDK
-
The Python XDK (X Developer Kit) is our official client library for interacting with the X API v2 using Python. It allows developers to get started with our API quickly and build applications with it. It is generated based on our official OpenAPI specification. It abstracts away low-level HTTP details while providing fine-grained control when needed.
-
-
- https://github.com/xdevplatform
-
X Developer Platform Resources
- https://github.com/xdevplatform/xdk
-
XDK Project
-
SDK generator for the X APIs.
-
Welcome to the XDK project! This repository contains a collection of Rust crates designed for generating and managing SDKs (or XDKs) for the X APIs. The project focuses on creating high-quality, type-safe SDKs for various programming languages, starting with Python, from OpenAPI specifications.
-
-
-
- https://github.com/fa0311/TwitterFrontendFlow
-
Unofficial Client for Twitter Internal API
-
- https://github.com/tweepy/tweepy
-
Tweepy: Twitter for Python!
-
- https://github.com/tsukumijima/tweepy-authlib
-
A library for authenticating with a screen name and password on Tweepy using the internal API of the Twitter Web App (official web client).
-
- https://github.com/fa0311/twitter-openapi-typescript
-
Implementation of Twitter internal API (Twitter graphql API) in TypeScript
-
- https://github.com/fa0311/twitter_openapi_python
-
Implementation of Twitter internal API (Twitter graphql API) in Python with data validation by pydantic
-
- https://github.com/fa0311/twitter-openapi
-
OpenAPI(Swagger) specification of Twitter Internal API (Twitter graphql API)
-
- https://github.com/fa0311/TwitterInternalAPIDocument
-
Reverse engineering of the web version of Twitter.
-
- https://github.com/prinsss/twitter-web-exporter
-
Export tweets, bookmarks, lists and much more from Twitter(X) web app.
- https://greasyfork.org/en/scripts/492218-twitter-web-exporter
-
- https://github.com/Thytu/XRSS
-
🌟 XRSS - Your Twitter Feed, RSS-ified and Supercharged!
-
Transform tweets into an elegant RSS feed: custom filters, user tracking, and real-time updates.
-
Transform your Twitter experience into a delightful, distraction-free RSS feed!
-
XRSS is your passport to a cleaner, more organized Twitter experience. It transforms the chaotic Twitter timeline into a structured, filterable RSS feed that puts YOU in control. Perfect for researchers, developers, and anyone who loves their content well-organized!
-
- https://github.com/zedeus/nitter
-
Nitter
-
Alternative Twitter front-end
-
A free and open source alternative Twitter front-end focused on privacy and performance.
-
- https://x.com/search?q=%22twitter%22%20-%22twitter.com%22%20(from%3A_devalias)&src=typed_query&f=top
Analysis of X's NotificationsTimeline GraphQL API, including request parameters, instruction types, entry and cursor structures, and pagination behaviour. Covers cursor/ID encoding (Base64 / Apache Thrift) and shows that older notifications appear to be unretrievable via the API beyond a cutoff, with notes on unread-state mutation instructions and limited recovery via advanced search.
- https://x.com/_devalias/status/2003233313339900130
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
I didn't realise there was a cap on how far back twitter notifications go...
Guess I shouldn't have ignored looking through / triaging them for so long 😅
October 10th seems to be my oldest.
- https://x.com/_devalias/status/2003237160418410999
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
I wonder if this is just another UI limitation thing, and the raw requests would actually let me keep 'scrolling'.
@sucralose__ I feel like you might have explored something adjacent to this at one point; did you know off hand if it's possible by chance?
-
- https://x.com/_devalias/status/2003240898059223269
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Looks like the main GraphQL API request is to:
https://x.com/i/api/graphql/Ev6UMJRROInk_RMH2oVbBg/NotificationsTimeline?variable...&features=...With the
variablesparam set to:{ "timeline_type": "All", "count": 20 }(The
featuresparam is also a large encoded JSON object, but less directly relevant here.)
-
- https://x.com/_devalias/status/2003244816260645126
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
That returns a deep JSON object structure; where the most interesting parts (for this use case at least) seem to be at:
.data.viewer_v2.user_results.result.notification_timeline.timeline.instructions[]
-
- https://x.com/_devalias/status/2003245626012893338
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Within that array, there seem to be a bunch of different object types, such as:
TimelineClearCacheTimelineRemoveEntries(entry_ids: [])TimelineAddEntries(entries: [])TimelineClearEntriesUnreadStateTimelineMarkEntriesUnreadGreaterThanSortIndex(sort_index: "")
-
- https://x.com/_devalias/status/2003247614649860534
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
In
TimelineAddEntries's.entries, we can see that each entry is an object with the shape:{ "entryId": "", "sortIndex": "", "content": {} }Where
entryIdhas shapes like:cursor-top-[sortIndex-timestamp]notification-[id]cursor-bottom-[sortIndex-timestamp]
-
- https://x.com/_devalias/status/2003251212230246907
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
For the
cursor-[top/bottom]-[sortIndex]entries, theircontentobject looks like:{ "entryType": "TimelineTimelineCursor", "__typename": "TimelineTimelineCursor", "value": "", "cursorType": "" }Where
cursorTypeis one ofTop/Bottom, andvalueis the cursor ID.
-
- https://x.com/_devalias/status/2003275081490075953
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
While it's probably not super relevant, the cursor value doesn't seem to be fully opaque; it appears to be Base64 encoded bytes that seem to be Apache Thrift struct format.
-
- https://x.com/_devalias/status/2003275987283509537
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
If you want to know more about decoding Apache Thrift byte format into something more readable, check out this ~2018 post / tool (
readable-thrift) from @jamchamb_ / @NCCGroupInfosec
-
- https://x.com/_devalias/status/2003276696259383746
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
For our needs here, we probably don't need to bother going much deeper at this stage; though I will note that the i64 in field 1 appears to match the
.data.viewer_v2.user_results.result.rest_idvalue.
-
- https://x.com/_devalias/status/2003281283544416517
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Also the
.data.viewer_v2.user_results.result.notification_timeline.idappears to be a Base64 encoded string prefix `Timeline:``, followed by Apache Thrift struct bytes that contains 2 fields:- i64 (8 bytes) matching my user
rest_id - i32 (4 bytes) containing the number
1
- i64 (8 bytes) matching my user
-
- https://x.com/_devalias/status/2003287594646528158
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Climbing out of that rabbit hole.. back to trying the most basic thing, which is to just take the bottom cursor and query for more notification entries with that; we unfortunately seem to get an essentially empty result, which only includes a
cursor-top-[sortIndex]entry.
-
- https://x.com/_devalias/status/2003287893884948891
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
So it looks like, at least based on the above research, as far as accessing via this API goes; those older notifications are likely lost forever :(
Guess that is incentive for me to check more often... 😅
-
- https://x.com/_devalias/status/2003289523879575825
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Though something else I did notice is that the 'bottom' cursor I used, and the 'top' cursor I received in that empty response only differed by the Apache Thrift field ID.
- 1 (
DAAB...) is a 'Top' cursor - 2 (
DAAC...) is a 'Bottom' cursor
- 1 (
-
- https://x.com/_devalias/status/2003290867835285632
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Also worth noting for future is the
.data.viewer_v2.user_results.result.notification_timeline.timeline.instructions[]types:TimelineClearEntriesUnreadStateTimelineMarkEntriesUnreadGreaterThanSortIndexAs everything getting marked read when I open notifications is annoying.
-
- https://x.com/_devalias/status/2003291231129072099
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
There'll no doubt be corresponding API calls triggered based on that that actually updates the 'unread' status on the backend; but that can be a future distraction to explore.
-
- https://x.com/_devalias/status/2003295675518447966
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Back to the 'lost' notifications, I can use advanced search to attempt to figure out which where replies I never got back to at least:
(to:_devalias) (-from:_devalias) since:2025-01-01 until:2025-12-31And then click onto the 'Latest' tab to get chronological ordering.
-
-
A spontaneous attempt to catch up on two weeks of notifications leads into a DOM and React fibre deep-dive to extract and count tweet data, quantify the backlog, and lay groundwork for future tooling.
- https://x.com/_devalias/status/1924368431979335819
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
That feel when you ignore your 'notify on post' notifications for like 2 weeks, and then you finally go to procrastinate by catching up on them, and scroll and scroll, and then instead of actually reading them you start to wonder how you can count how many need triaging...
- https://x.com/_devalias/status/1924368434261012764
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...so you accidentally find yourself in the familiar rabbit hole of skimming through the DOM looking for relevant bits to build selectors off, and then remembering that the timeline only renders a slice of the list to the DOM so you can't just count rendered elements...
-
- https://x.com/_devalias/status/1924368436383326540
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...but then you remember about React fibres and start looking through those to see if you can find where the full list of notification tweets are, and you find it but it's just a list of IDs, so you crawl deeper into the rabbit hole until you get a reference to the datastore...
-
- https://x.com/_devalias/status/1924368440992932182
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...and then with all of that, you build up some little helper functions and gadgets to stitch the data together, then a few more to process that hydrated array and count the tweets per day/user/etc, and then finally end up knowing that you have ~560 tweets to look through...
-
- https://x.com/_devalias/status/1924368443606028586
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...but by that stage you've run out of time in the day to actually look through them all... 😅
At least I can quantify how much effort it will take now; which should help remove the 'do I have enough time for this' paralysis and maybe actually process them more often.
-
- https://x.com/_devalias/status/1924368445711515736
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
Today's deepdive was probably also the most productive one yet for finding the bits and pieces I need to pull together a user script / Chrome extension to help make this 'keep up to date' / 'triage' process much less painful / time consuming...
But that's a story for another day
-
- https://x.com/_devalias/status/1924369583751987497
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
Also, shoutout to the top 3 by post count over this period — forever keeping me busy trying to keep up with your interesting tidbits:
- @testingcatalog : 109 posts (19.5%)
- @btibor91 : 92 posts (16.4%)
- @EMostaque : 53 posts (9.5%)
-
- https://x.com/_devalias/status/1924656001812455848
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
Crossposted:
-
-
Suggestions for features like unread tracking, author grouping, and timeline recall for accounts marked “notify on post.” Includes related gists and prior threads.
- https://x.com/_devalias/status/1870292640320892955
-
Glenn 'devalias' Grant @_devalias Dec 21, 2024 Anyone know of any good browser userscripts or similar tools to make twitter notifications nicer to work with? I’d love to be able to easily see how many I have unread + jump back to the last one I saw.
- https://x.com/_devalias/status/1870292643231785338
-
Glenn 'devalias' Grant @_devalias Dec 21, 2024 Maybe also to group/count by author or similar too.
And potentially be able to mark some as ‘seen’ while leaving others ‘unread’ or otherwise ‘saved’ to review closer later.
-
- https://x.com/_devalias/status/1870292646373044590
-
Glenn 'devalias' Grant @_devalias Dec 21, 2024 I guess I should say that this is mostly for accounts I have on ‘notify on post’ specifically.
-
- https://x.com/_devalias/status/1870731056464429308
-
Glenn 'devalias' Grant @_devalias Dec 22, 2024 Got bored this afternoon and decided to update my "Notes on API/userscript to improve Twitter 'Notifications Timeline'" research gist related to this: https://gist.github.com/0xdevalias/8885b10795eb3267b703ed5943087953#notes-on-apiuserscript-to-improve-twitter-notifications-timeline
Specifically this section: https://gist.github.com/0xdevalias/8885b10795eb3267b703ed5943087953#timeline-notes-2
-
- https://x.com/_devalias/status/1870736469989654544
-
Glenn 'devalias' Grant @_devalias Dec 22, 2024 And here's one of my older threads on how painful this manual process is, including some stats about time taken, and musings on things that would be helpful along the way:
-
-
Walkthrough of the manual backlog review process, challenges of staying on top of notifications, and efforts to batch/sort tweet URLs for focused review.
- https://x.com/_devalias/status/1788441848274731052
-
Glenn 'devalias' Grant @_devalias May 9, 2024 That feel when you ignore twitter notifications for a week or two, and then have ??? of backlog posts to skim through and review.
Starting at 15:32, lets see how long it takes..
- https://x.com/_devalias/status/1788442402514203013
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Instantly distracted sidenote: does anyone have any better methods for keep up with Twitter updates/etc these days rather than the official site/client?
I would love to be able to see metadata like X new posts, from Y users since Z time.
-
- https://x.com/_devalias/status/1788442563382505764
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Bonus points if I could then drill down into that to see how many tweets per user, and be able to review those in batches/etc.
-
- https://x.com/_devalias/status/1788442973539377537
-
Glenn 'devalias' Grant @_devalias May 9, 2024 I tend to have a completionist mindset and so hate opening (and thus clearing) notifications when I can’t process them all.
But then when I leave it for a few days/weekend it becomes a big overwhelming unknown as I don’t know how many posts are there; so I put it off further
-
- https://x.com/_devalias/status/1788443083648233966
-
Glenn 'devalias' Grant @_devalias May 9, 2024 This isn't for all tweets/people I follow by the way; just the ones I have put on 'notify for all posts'
-
- https://x.com/_devalias/status/1788457390268109087
-
Glenn 'devalias' Grant @_devalias May 9, 2024 16:32, up to date, ~28 tweets/threads open for deeper review..
I guess ~1hr to catch up (or at least, filter down) ~2 weeks of not paying attention isn't the worst thing ever.. maybe that'll motivate me to not put it off so much in future..
-
- https://x.com/_devalias/status/1788459969857351705
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Then I extract all the tweet URLs to be processed, and sort them based on account then ID to vaguely contextually batch them (and avoid brain context switching):
pbpaste | sort -t '/' -k 4,4 -k 6n | awk -F'/' '{if (last && $4 != last) print ""; last=$4; print $0}' | subl
-
- https://x.com/_devalias/status/1788464333976191404
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Captured these sorting notes on a gist for easier future reference:
-
- https://x.com/_devalias/status/1791303467728028019
-
Glenn 'devalias' Grant @_devalias May 17, 2024 So it took me ~3-4min of scrolling + some arbitrary guessing to even get back to where I was last viewing from.. wish I could just jump straight to it.
-
- https://x.com/_devalias/status/1791353323687383296
-
Glenn 'devalias' Grant @_devalias May 17, 2024 4:20; finally caught up... for now.. 😮💨
Really need a more efficient way to keep on top of things, while also not being constantly distracted by it all..
-
- https://x.com/_devalias/status/1870736089050480770
-
Glenn 'devalias' Grant @_devalias Dec 22, 2024 And then... queue me more or less burning out on trying to keep up with things and half ignoring twitter for months on end except for the most basic of direct notifications...
Then eventually looking for a better solution again:
-
-
Links and commentary on scripts for sorting tweet URLs and improving the triage workflow.
- https://x.com/_devalias/status/1788753444494561542
-
Glenn 'devalias' Grant @_devalias May 10, 2024 Or I might be musing about how I can improve my efficiency/workflow in other ways, such as:
"Notes on API/userscript to improve Twitter 'Notifications Timeline'": https://gist.github.com/0xdevalias/8885b10795eb3267b703ed5943087953#notes-on-apiuserscript-to-improve-twitter-notifications-timeline
"Sorting Twitter URLs": https://gist.github.com/0xdevalias/0b56aef69e20bfcbf0f1eed3960f4db8#sorting-twitter-urls
-
Discussion with @sucralose_ on API restrictions and fallback approaches using browser scripting and frontend scraping._
- https://x.com/sucralose__/status/1783665885305061783
-
Michael Skyba @sucralose__ Apr 26, 2024 Wait can you not fetch Tweets at all in the Free tier? Elon why
- https://x.com/_devalias/status/1783675392488034540
-
Glenn 'devalias' Grant @_devalias Apr 26, 2024 Yeah, the Twitter API got butchered.. it's sad; so much potential, but for a random independent dev hacking on a personal project, there's just no way that it's worth that kind of $$
-
- https://x.com/sucralose__/status/1783678183092392316
-
Michael Skyba @sucralose__ Apr 26, 2024 Fortunately the frontend seems pretty simple in how it gets its posts. Not sure if I want to waste an account getting banned by trying it though
-
- https://x.com/_devalias/status/1783678936712400983
-
Glenn 'devalias' Grant @_devalias Apr 26, 2024 Yeah, that was kind of my view as well. Depends on the project idea, some I can probably achieve with browser userscripts (so that'll probably be the approach I take); but the fact that the API isn't available just sort of puts a damper on my mind coming up with other cool ideas
-
-
Considering whether to pay for Twitter Blue or recreate its link aggregation feature using the Twitter API and custom tooling.
- https://x.com/_devalias/status/1551033467924004864
-
Glenn 'devalias' Grant @_devalias Jul 24, 2022 Hrmm.. AU$4.49/mo for Twitter Blue to get the 'Top Articles' feature that I used to get for free from @nuzzel.. or spend ?? hours diving into the Twitter API docs and building something that extracts + orders all the links shared by people I follow..
https://help.twitter.com/en/using-twitter/twitter-blue-features#top-articles
-
On Twitter, from my notifications page:
When I click on the 'New post notifications for...' section, I am taken to a new page:
As part of this page, an API call is made to fetch the tweets to be shown:
This returns JSON data, from which we can extract the Tweets, and sort them by post time with something like jq:
pbpaste | jq '.globalObjects.tweets | to_entries | sort_by(.value.created_at | strptime("%a %b %d %H:%M:%S %z %Y")) | reverse | from_entries' | sublThis data will then look something like this:
{
"1734010714510049396": {
"created_at": "Mon Dec 11 00:42:25 +0000 2023",
"id": 1734010714510049300,
"id_str": "1734010714510049396",
"full_text": "Don’t be S-A-D! https://t.co/fkHsvfwuGG",
"truncated": false,
"display_text_range": [
0,
15
],
"entities": { /* ..snip.. */ },
"extended_entities": { /* ..snip.. */ },
"source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
"in_reply_to_status_id": null,
"in_reply_to_status_id_str": null,
"in_reply_to_user_id": null,
"in_reply_to_user_id_str": null,
"in_reply_to_screen_name": null,
"user_id": 44196397,
"user_id_str": "44196397",
"geo": null,
"coordinates": null,
"place": null,
"contributors": null,
"is_quote_status": false,
"retweet_count": 5203,
"favorite_count": 56940,
"reply_count": 3883,
"quote_count": 410,
"conversation_id": 1734010714510049300,
"conversation_id_str": "1734010714510049396",
"conversation_muted": false,
"favorited": false,
"retweeted": false,
"possibly_sensitive": false,
"lang": "en",
"ext": {
"superFollowMetadata": {
"r": {
"ok": {}
},
"ttl": -1
}
}
},
// ..snip..
}From that info (and matching against the DOM itself), it should be possible to implement a userscript/similar that can:
- mark where the last tweet I saw was (both automatically, and manually)
- hide/dim tweets older than that
- tell me how many tweets there are till I am 'caught up'
- etc
(ChatGPT Ref (private): https://chatgpt.com/c/67679a82-16fc-8008-8cbe-9c932f4cdfab)
On Twitter, from my notifications page:
When I click on the 'New post notifications for...' section, I am taken to a new page:
In the source of that page there is a script tag with a window.__INITIAL_STATE__ object containing lots of settings config type things. Among these are some keys mentioning graphql, including:
{
// ..snip..
graphql_is_translatable_rweb_tweet_is_translatable_enabled: {
value: true,
},
graphql_mutation_retweet_mode: { value: 'rest_only' },
graphql_mutation_update_mode: { value: 'graphql_only' },
graphql_timeline_v2_bookmark_timeline: { value: true },
graphql_timeline_v2_user_favorites_by_time_timeline: {
value: true,
},
graphql_timeline_v2_user_media_timeline: { value: true },
graphql_timeline_v2_user_profile_timeline: { value: true },
graphql_timeline_v2_user_profile_with_replies_timeline: {
value: true,
},
// ..snip..
}There are also additional script tags loading various bundles:
- https://abs.twimg.com/responsive-web/client-web/vendor.c14be00a.js
- https://abs.twimg.com/responsive-web/client-web/i18n/en.1e92731a.js
- https://abs.twimg.com/responsive-web/client-web/main.e541178a.js
In Chrome DevTools, using the 'Search' panel, we can search across the source files for graphql_timeline_v2_user_profile_timeline, which gives an interesting looking hit in the 'main' bundle:
- https://abs.twimg.com/responsive-web/client-web/main.e541178a.js
-
fetchUserTweets: ({count: d, cursor: t, userId: a}) => e.graphQL(M(), { userId: a, count: d, cursor: t, includePromotedContent: !0, withQuickPromoteEligibilityTweetFields: !0, ...(0, r.d)(n), withVoice: n.isTrue("voice_consumption_enabled"), withV2Timeline: n.isTrue("graphql_timeline_v2_user_profile_timeline") }, f, k(n)).then((e => { let n = y.cY; return e?.user?.result && "User" === e.user.result.__typename && (n = e.user.result.timeline?.timeline || e.user.result.timeline_v2?.timeline || y.cY), n } )),
- Also within that same module are similar functions:
fetchUserTweetsfetchUserTweetsAndRepliesfetchUserSuperFollowsTweetsfetchUserHighlightsTweetsfetchUserArticlesTweetsfetchUserPromotableTweetsfetchLikesfetchUserMediafetchBusinessProfileTeam
-
This could be an interesting place to look deeper. Also, of particular note is e.graphQL, which is likely the main GraphQL client, and could lead to us being able to find and/or monitor other interesting points of data fetching. In that same module, we can see that e is destructured from a named parameter apiClient:
, v = ({apiClient: e, featureSwitches: n}) => ({If we search for apiClient across the code, we get a LOT of hits across many different modules including:
- https://abs.twimg.com/responsive-web/client-web/main.e541178a.js
requestGuestToken:e.post("guest/activate"requestSsoInitToken:e.post("onboarding/sso_init"logout:e.post("account/logout"cleanupMulti:e.post("account/multi/clean"list:e.get("account/multi/list"add:e.post("account/multi/add"switch:e.post("account/multi/switch"logoutAll:e.post("account/multi/logout_all"authenticatePeriscopefetchBadgeCount:e.getURT("badge_count/badge_count"fetchTweetDetailacceptConversation:e.post("dm/conversation/addParticipants:e.post("dm/conversation/addWelcomeMessageToConversation:e.post("dm/welcome_messages/add_to_conversation"deleteConversations:e.post("dm/conversation/bulk_delete"editMessage:e.post("dm/edit"fetchConversation:e.get("dm/conversation/fetchConversationFromParticipants:e.get("dm/conversation"fetchPermissions:e.get("dm/permissions"fetchSecretPermissions:e.get("dm/permissions/secret"fetchUserInbox:e.get("dm/inbox_initial_state"fetchInboxHistory:e.get("dm/inbox_timeline/fetchUserUpdates:e.get("dm/user_updates"leaveConversation:e.post("dm/conversation/search:e.post("dm/search/query"sendMessage:e.post(kmarkRead:e.post("dm/conversation/reportSpam:e.post("direct_messages/report_spam"reportDSA:e.post("dm/report"updateConversationAvatar:e.post("dm/conversation/updateLastSeenEventId:e.post("dm/update_last_seen_event_id"updateConversationName:e.post("dm/conversation/disableNotifications:e.post("dm/conversation/enableNotifications:e.post("dm/conversation/updateMentionNotificationsSetting:e.post("dm/conversation/updateTyping:e.post("dm/conversation/muteDMUser:e.post("/dm/user/update_relationship_state"fetch:e.get("help/settings"fetchLanguages:e.get("help/languages"fetchFollowersYouFollow:e.get("friends/following/list"fetchPendingFollowers:e.get("friendships/incoming"fetchFollowing:e.get("friends/list"acceptPendingFollower:e.post("friendships/accept"declinePendingFollower:e.post("friendships/deny"updateFriendship:e.post("friendships/update"createAllFriendships:e.post("friendships/create_all"destroyAllFriendships:e.post("friendships/destroy_all"fetchBlockedAccountsImportedGraphqlfetchBlockedAccountsAllGraphqlfetchFollowersfetchFollowersYouKnowfetchFollowingfetchSuperFollowersfetchCreatorSubscriptionsfetchCreatorSubscribersfetchVerifiedFollowersfetchModeratedTimelinefetchMutedAccountsfetchGenericTimelineByIdfetchHomefetchHomeLatestfetchHomeCreatorSubscriptionsclientEvent:e.post("jot/client_event"errorLog:e.post("jot/error_log"externalReferer:e.post("jot/external_referer"fetchLiveEventMetadata:e.get("live_event/1updateRemindMeSubscription:e.post("live_event/1task:e.post("onboarding/task"syncContacts:e.post("onboarding/contacts_authorize"getContactsImportStatus:e.get("onboarding/contacts_import_status"getVerificationStatus:e.get("onboarding/verification_status"callInteractiveSpinnerPath:e.getcallOnboardingPath:e.post("onboarding/referer:e.post("onboarding/referrer"removeContacts:e.post("contacts/destroy/all"setUserPwaLaunched:e.put("strato/column/User/verifyUserIdentifier:e.post("onboarding/begin_verification"verificationLink:e.post("onboarding/verify"getBrowsableNuxRecommendations:e.post("onboarding/fetch_user_recommendations"fetchUserTweetsfetchUserTweetsAndRepliesfetchUserSuperFollowsTweetsfetchUserHighlightsTweetsfetchUserArticlesTweetsfetchUserPromotableTweetsfetchLikesfetchUserMediafetchBusinessProfileTeamlog:e.post("promoted_content/log"fetch:e.get("account/settings"fetchRateLimits:e.get("application/rate_limit_status"fetchHashflags:e.get("hashflags"update:e.post("account/settings"deleteSSOConnection:e.post("sso/delete_connection"deleteLocationData:e.post("geo/delete_location_data"deleteContacts:e.post("contacts/destroy/all"fetchNotificationFilters:e.get("mutes/advanced_filters"updateNotificationFilters:e.post("mutes/advanced_filters"updateProfile:e.post("account/update_profile"removeProfileBanner:e.post("account/remove_profile_banner"updateProfileAvatar:e.post("account/update_profile_image"updateProfileBanner:e.post("account/update_profile_banner"fetchPlaceTrendSettings:e.getURT("guide/get_explore_settings"updatePlaceTrendSettings:e.postURT("guide/set_explore_settings"usernameAvailable:e.dispatch({ path: "/i/users/username_available.json"fetchApplications:e.get("oauth/list"revokeApplication:e.post("oauth/revoke"revokeOauth2Token:e.postUnversioned("/2/oauth2/revoke_token_hash"changePassword:e.postI("account/change_password"deactivate:e.post("account/deactivate"fetchWoeTrendsLocations:e.get("trends/available"fetchPlaceTrendsLocations:e.getURT("guide/explore_locations_with_auto_complete"fetchLoginVerificationSettings:e.get("strato/column/User/{t}/account-security/twoFactorAuthSettings2"fetchBackupCode:e.get("account/backup_code"fetchNewBackupCode:e.post("account/backup_code"fetchTemporaryPassword:e.post("account/login_verification/temporary_password"fetchEncryptedDMsPublicKeysAndDevices:e.get("keyregistry/extract_public_keys/{d}"deregisterDevice:e.delete("keyregistry/delete/{n.registrationToken}"fetchSessionsfetchUserPreferencesenableVerifiedPhoneLabeldisableVerifiedPhoneLabelfetchUserProfilePhoneStaterevokeSession:e.postUnversioned("/account/sessions/revoke"revokeAllSessions:e.postUnversioned("/account/sessions/revoke_all"enrollIn2FA:e.post("bouncer/opt_in"disable2FA:e.delete("account/login_verification_enrollment"disable2FAMethod:e.post("account/login_verification/remove_method"rename2FASecurityKey:e.post("account/login_verification/rename_security_key_method"verifyPassword:e.post("account/verify_password"fetchAltTextPromptPreferenceupdateAltTextPromptPreferencefetchDataUsageSettingsupdateDataUsageSettingsupdateDmNsfwMediaFilterupdateSharingAudiospacesListeningDataWithFollowersfetchTopicLandingPagefetchTopicsManagementPagefetchOneTopicfetchTopicsPickerPagefetchViewingOtherUsersTopicsPagePagefollowunfollownotInterestedundoNotInterestedfetchTopicsToFollowSidebarn.post("statuses/update"fetchfetchMultipleshowfetchTranslationmuteunmutelikeunlikeretweetunretweetbookmarkunbookmarkhighlightundoHighlightpinunpinhideReplyGraphQLunhideReplyGraphQLremoveTagsendTweetdestroychangeConversationControlsremoveConversationControlsunmentionUserFromConversationfetchfetchCommunityInviteUsersfetchCommunityMembersfetchPaymentsUsersprefetchUsersfetchExplorefetchExploreGraphQLfetchTrendHistoryfetchExploreSidebarGraphQLfetchTrendRelevantUsersGraphQLfetchGlobalCommunitiesLatestPostSearchfetchGlobalCommunitiesPostSearchfetchExploreTopicfetchGenericfetchBookmarkSearchfetchListSearchfetchLiveEventTimelinefetchNotificationsfetchNotificationsUnreadCountfetchNUXUserRecommendationsfetchRichConnectTimelinefetchRichSuggestedTimelinefetchSearchfetchSearchGraphQLfetchSimilarPostsfetchReactiveInstructionsfetchTestFixturesfetchTestGraphqlFixturesfetchUserMomentspostCustomEndpointsubmitTimelinesFeedbackupdateNotificationsLastSeenCursorfetchUsersfollowunfollowcancelPendingFollowblockunblockdmBlockdmUnblockmuteunmutefetchProfileTranslationfetchViewerfetchOneUserByScreenNamefetchOneUserfetchUsersremoveFollowercreateLocalId:e.postUnversioned("/12/measurement/dcm_local_id"postfetchfetchDevicePermissionsState:e.get("strato/column/None/${r}"fetchInfo:e.get("users/email_phone_info"resendConfirmationEmail:e.post("account/resend_confirmation_email"removeDevice:e.post("device/unregister"updateDevicePermissionsState:e.put("strato/column/None/${r}"getNotificationSettingsLogin:e.post("notifications/settings/login"getNotificationSettings:e.post("notifications/settings/checkin"updateNotificationSettings:e.post("notifications/settings/save"removePushDevices:e.post("notifications/settings/logout"putClientEducationFlagfetchPreferences:e.get("account/personalization/p13n_preferences")updatePreferences:e.post("account/personalization/p13n_preferences")fetchData:e.get("account/personalization/p13n_data")fetchTwitterInterests:e.get("account/personalization/twitter_interests")fetchPartnerInterests:e.get("account/personalization/partner_interests")createAudienceDownload:e.post("account/personalization/email_advertiser_list")createDataDownload:e.post("account/personalization/email_your_data")updateCookies:e.get("account/personalization/set_optout_cookies")syncSettings:e.post("account/personalization/sync_optout_settings")fetchPinnedTimelinespinTimelineunpinTimelinefetchUserClaims
- https://abs.twimg.com/responsive-web/client-web/bundle.Grok.2fe94cba.js
- TODO: add summary of specific API names/endpoints here
- https://abs.twimg.com/responsive-web/client-web/loader.AppModules.9e1a436a.js
updateSubscriptions:e.post("live_pipeline/update_subscriptions", o, {}, a, "")metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/loader.DMDrawer.bb6db51a.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSlice
- https://abs.twimg.com/responsive-web/client-web/loader.HWCard.1d1419aa.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSlice
- https://abs.twimg.com/responsive-web/client-web/loader.SideNav.ed72f65a.js
metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/loader.TimelineCardHandler.f5e9e45a.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSlice
- https://abs.twimg.com/responsive-web/client-web/loader.Typeahead.4504647a.js
metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/modules.audio.44d4466a.js
spacebar:e.getUnversioned("/fleets/v1/fleetline"byIdsubscribeToScheduledSpaceByIdunsubscribeFromScheduledSpaceByIdfetchTopicssearchaddSharingdeleteSharingfetchPresence:e.getUnversioned("/fleets/v1/avatar_content"
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.Articles~bundle.AudioSpaceDetail~bundle.AudioSpaceDiscovery~bundle.AudioSpacebarScreen~bundle.B.6db0364a.js
addToListcreateListeditBannerImagedeleteListdeleteBannerImagefetchListfetchCombinedListsfetchListsManagementPageTimelinefetchTweetsGraphQLfetchMembersGraphQLfetchRecommendedUsersGraphQLfetchSubscribersGraphQLfetchSuggestedListsfetchOwnershipsGraphQLfetchMemberships:e.get("lists/memberships"fetchMembershipsGraphQLremoveFromListcreateSubscribersdestroySubscriberstoggleMuteeditListpinunpinfetch:e.post("videoads/v2/prerolls"
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.AudioSpaceDetail~bundle.AudioSpaceDiscovery~bundle.AudioSpacebarScreen~bundle.Birdwatch~bundle..e24934ea.js
fetchImmersiveMediafetchImmersiveProfilefetchImmersiveProfileByUserIdfetchTVHomeMixerGraphQLfetchTVUserProfileGraphQLfetchTVTrendGraphQLfetchTweetRelatedVideosGraphQLgeneratePinCodeGraphQLdeviceIsVerifiedGraphQL
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.Grok~bundle.ReaderMode~bundle.Birdwatch~bundle.TwitterArticles~bundle.Compose~bundle.Settings~b.03592eba.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSliceclearConversationssetPreferencesfetchConversationfetchGrokShareGraphQLfetchGrokHomefetchHistoryfetchMediaHistorysearchConversationsdeleteMessageuploadFile:e.postForm("grok/attachment"getQuickPromoteEligibilitygetCouponsgetBudgetsgetAudienceEstimategetBoostAudienceEstimategetPaymentMethodsdeletePaymentMethodsetDefaultPaymentMethodcreatePromotionenrollCoupongetAdAccountsfetchArticleDomainsGraphQLfromSharegetTargetableLocations:e.getUnversioned("/12/targeting_criteria/locations"getQueriedTargetableLocations:e.getUnversioned("/12/targeting_criteria/locations"
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.TwitterArticles~loader.TweetCurationActionMenu.956f177a.js
createDraftArticlefetchArticleEntitydeleteArticleEntityupdateArticleEntityContentupdateArticleEntityCoverMediaupdateArticleEntityTitlepublishArticleEntityunpublishArticleEntityfetchArticleEntitiesSlice
- https://abs.twimg.com/responsive-web/client-web/shared~loader.AppModules~loader.LoggedOutNotifications.94c3e97a.js
enableLoggedOutWebNotifications
- https://abs.twimg.com/responsive-web/client-web/shared~loader.AppModules~ondemand.SettingsRevamp~bundle.NotABot~bundle.TwitterBlue.a61dad2a.js
fetchSubscriptionProductDetailsfetchSubscriptionProductCheckoutUrlfetchNotABotCheckoutUrlfetchProductSubscriptionsswitchTier
- https://abs.twimg.com/responsive-web/client-web/shared~loader.AudioDock~loader.DashMenu~loader.DashModal~loader.DMDrawer~bundle.Grok~bundle.Account~bundle.Re.635a6b9a.js
fetchLiveVideoStreamStatus:e.get("live_video_stream/status/"metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/shared~loader.DMDrawer~bundle.LiveEvent~bundle.Compose~bundle.DirectMessages~bundle.DMRichTextCompose~bundle..d0f9ae9a.js
fetchBroadcastfetchLatestBroadcast
- https://abs.twimg.com/responsive-web/client-web/shared~loader.DMDrawer~ondemand.SettingsInternals~bundle.DirectMessages.6bf37dca.js
fetchDMAllSearchfetchDMGroupSearchfetchDMPeopleSearchfetchDMMutedUsers
- https://abs.twimg.com/responsive-web/client-web/shared~loader.DashMenu~loader.SideNav~loader.AppModules~loader.DMDrawer~bundle.Grok~bundle.MultiAccount~bundl.2aa3c46a.js
createCommunityfetchCommunityfetchCommunityWithoutRelayfetchAboutTimelinefetchCommunityLoggedOutTweetsfetchCommunityTweetsfetchCommunityMediaLoggedOutTweetsfetchCommunityMediaTweetsfetchCommunityRankedLoggedOutTweetsfetchCommunityMembershipsfetchRecentCommunityMembershipsfetchCommunitiesMembershipsSlicefetchModerationCasesSlicefetchTweetModerationLogSlicefetchCommunitiesMainDiscoveryModulefetchCommunitiesMainTimelinefetchCommunitiesRankedTimelinefetchCommunitiesExploreTimelinefetchCommunityHashtagsTimelinefetchCommunityDiscoveryTimelinekeepCommunityTweetjoinCommunityrequestToJoinCommunityleaveCommunityinviteToCommunityupdateCommunityRolecreateDraftTweetdeleteDraftTweeteditDraftTweetfetchDraftTweetsfetchPlace:e.get("geo/id/search:e.get("geo/places"scheduleTweetfetchScheduledTweetsdeleteScheduledTweeteditScheduledTweeteditCommunityNameeditCommunityPurposeeditCommunityQuestioneditCommunityRulecreateCommunityRuleremoveCommunityRulereorderCommunityRuleseditCommunityBannerMediaremoveCommunityBannerMediafetchAuthenticatedUserTFListscreateTrustedFriendsList
- https://abs.twimg.com/responsive-web/client-web/shared~loader.Typeahead~bundle.Search.24bc9bda.js
fetch:e.get("saved_searches/list"create:e.post("saved_searches/create"destroy:e.post("saved_searches/destroy/
- https://abs.twimg.com/responsive-web/client-web/shared~loader.WideLayout~loader.ProfileClusterFollow.d1ed818a.js
fetch:e.get("users/recommendations"fetchSidebarUserRecommendations
- https://abs.twimg.com/responsive-web/client-web/shared~ondemand.DirectMessagesCrypto~ondemand.SettingsRevamp.bcca9e0a.js
register:e.post("keyregistry/register"extractPublicKeys:e.get("keyregistry/extract_public_keys/
From that, we can see that some of the apiClient features seem to relate to REST (get, post, postForm, postUnversioned, delete) endpoints, and others to graphQL.
For the graphQL parts of the apiClient, we can see that the structure tends to look something like this:
// ..snip..
{
// ..snip..
705929: (e, t, o) => {
"use strict";
// ..snip..
var s =
// ..snip..
, m = o(403807)
, f = o.n(m)
// ..snip..
F = ({apiClient: e, featureSwitches: t}) => ({
bookmarkTweetToFolder: t => e.graphQL(f(), {
...t
}, (0, i.kj)((e => !e.bookmark_collection_tweet_put), "GQL Bookmark Folders: failed to Add Tweet to Bookmark Folder")),
// ..snip..
})
// ..snip..
}
// ..snip..
}
// ..snip..Here, we can see that the first param to graphQL is f(), which is the query/mutation. Tracing that backwards, we can find the module where it was imported from:
(From ChatGPT convo: https://chatgpt.com/c/6767a484-feb8-8008-ba03-5f802a5dd3ad)
In the
bookmarkTweetToFolderfunction:bookmarkTweetToFolder: t => e.graphQL(f(), { ...t }, (0, i.kj)((e => !e.bookmark_collection_tweet_put), "GQL Bookmark Folders: failed to Add Tweet to Bookmark Folder"))
f()provides the query/mutation details for this call.{ ...t }supplies the variables (likely includingtweet_idandbookmark_folder_id).- The third argument handles errors or conditions for failure.
f()is imported as:var f = o.n(m);Where
mrefers to:var m = o(403807);
In this case, that GraphQL query/mutation module definition looks like this:
403807: e => {
e.exports = {
queryId: "4KHZvvNbHNf07bsgnL9gWA",
operationName: "bookmarkTweetToFolder",
operationType: "mutation",
metadata: {
featureSwitches: [],
fieldToggles: []
}
}
}(From ChatGPT convo: https://chatgpt.com/c/6767a484-feb8-8008-ba03-5f802a5dd3ad)
Module
403807exports the following:e.exports = { queryId: "4KHZvvNbHNf07bsgnL9gWA", operationName: "bookmarkTweetToFolder", operationType: "mutation", metadata: { featureSwitches: [], fieldToggles: [] } };Key parts:
queryId:"4KHZvvNbHNf07bsgnL9gWA", uniquely identifying this mutation.operationName:"bookmarkTweetToFolder", the logical name of the mutation.operationType:"mutation", indicating that it modifies data.metadata: Additional metadata such as feature toggles, which are currently empty.
- The
queryId("4KHZvvNbHNf07bsgnL9gWA") is sent to the server via thegraphQLmethod.- On the server:
- The
queryIdis mapped to a stored GraphQL mutation string.- This mapping is typically maintained in:
- A server-side database or configuration file.
- A GraphQL operation registry.
- The server uses the mutation string mapped to the
queryIdto execute the operation with the provided variables.
- Efficiency: The client only sends the
queryIdinstead of the full mutation string, reducing payload size.- Flexibility: The server can update or optimize the mutation without requiring changes to the client code.
- Security: Prevents exposing the full query or mutation details in the client.
On Twitter, from my notifications page:
When I click on the 'New post notifications for...' section, I am taken to a new page:
A few potentially interesting data-testid / aria-label / etc type attributes we could potentially use for selectors:
<div data-testid="primaryColumn"
<div aria-label="Home timeline"
<section aria-labelledby="accessible-list-6" role="region" class="css-175oi2r">
<h1 dir="auto" aria-level="1" role="heading" class="css-146c3p1 r-4iw3lz r-1xk2f4g r-109y4c4 r-1udh08x r-wwvuq4 r-u8s1d r-92ng3h" id="accessible-list-6">Posts</h1>
<div aria-label="Timeline: Posts"
<div data-testid="cellInnerDiv"
<article data-testid="tweet"
<div data-testid="User-Name"
<time datetime="2025-05-06T07:20:52.000Z"
<a href="/REDACTED/status/REDACTED" dir="ltr" aria-label="May 6" role="link" class="css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-16dba41 r-xoduu5 r-1q142lx r-1w6e6rj r-9aw3ui r-3s2u2q r-1loqt21" style="color: rgb(113, 118, 123);">
<time datetime="2025-05-06T07:20:52.000Z">May 6</time>
</a>
<div data-testid="tweetText"
<div data-testid="tweetPhoto"
<div aria-label="Embedded video"
<div data-testid="previewInterstitial"From there, I decided to start diving a little deeper into the React Internals, specifically traversing through the fibers and props to get access the internal data stores/etc. The following were my snippets of exploratory code as I was investigating things, so it's more hacky prototype than clean final implementation:
const timelinePosts = $('div[aria-label="Timeline: Posts"]')
const reactPropsKey = Object.keys(timelinePosts).find(k => k.startsWith('__reactProps$'));
const timelinePostsProps = timelinePosts[reactPropsKey]
const seeminglyInterestingChild = timelinePostsProps.children[2]
const cacheKey = seeminglyInterestingChild.props.cacheKey
// Items seems to contain a slice of the tweets that are currently visible; or at least, their ID and some related metadata/etc
const items = seeminglyInterestingChild.props.items
console.log({ cacheKey, items, itemsLength: items.length })
// Other potentially interesting props/functions here:
// const { onAtEnd, onAtStart, onItemsRendered, onKeyboardRefresh, onNearEnd, onNearStart, onPositionRestored, renderer, sortIndexFunction, identityFunction, /* etc */ } = seeminglyInterestingChild.props
const interestingChildOwner = seeminglyInterestingChild._owner
const interestingChildOwnerElementTypeContextType = interestingChildOwner.elementType.contextType.$$typeof // Symbol(react.context)
const interestingChildOwnerElementTypeDefaultProps = interestingChildOwner.elementType.defaultProps // impressionCache, lingerCache, refreshControl, scroller, etc
const tweetNotificationsIDs = interestingChildOwnerElementTypeDefaultProps.impressionCache.get('tweet_notifications')
console.log({ tweetNotificationsIDs, tweetNotificationsIDsSize: tweetNotificationsIDs.size })
const interestingChildOwnerDependenciesFirstContextMemoizedValue = interestingChildOwner.dependencies.firstContext.memoizedValue // featureSwitches, history, isRestrictedSession, loggedInUserId, scrollManager, store, userAgent, userClaims, verifiedCrawlerName, viewerUserId
const store = interestingChildOwnerDependenciesFirstContextMemoizedValue.store // @@observable, dispatch, getState, replaceReducer, subscribe
const state = store.getState()
const stateKeys = Object.keys(state) // Some potentially interesting ones include: developer, directMessages, draftTweets, editTweet, entities, featureSwitch, grok, liveTweetCounts, session, settings, tweetComposer, urt, userClaim, etc
console.log({ state, stateKeys, stateKeysLength: stateKeys.length })
const stateEntities = state.entities
const stateEntitiesKeys = Object.keys(stateEntities) // Some potentially interesting ones include: cards, communities, conversations, genericNotifications, publishedArticles, tweets, users
console.log({ stateEntities, stateEntitiesKeys, stateEntitiesKeysLength: stateEntitiesKeys.length })
const stateEntitiesTweets = stateEntities.tweets
const stateEntitiesUsers = stateEntities.users
console.log({
stateEntitiesTweets,
stateEntitiesTweetsLength: Object.keys(stateEntitiesTweets.entities).length,
stateEntitiesUsers,
stateEntitiesUsersLength: Object.keys(stateEntitiesUsers.entities).length,
})
const tweetNotificationIDsArray = Array.from(tweetNotificationsIDs)
// const [entityType, entityKey] = tweetNotificationIDsArray[0].split('-')
const getEntity = (entityType, entityKey) => state.entities?.[`${entityType}s`]?.entities?.[entityKey]
const getUser = (userId) => {
const user = getEntity('user', userId)
const userLabel = !!user ? `${user.name} (@${user.screen_name})` : `Unknown (userId: ${userId})`
return {
userId,
user,
userLabel,
}
}
const getNotificationTweetByFullEntityId = (fullEntityId) => {
const [tweetEntityType, tweetEntityKey] = fullEntityId.split('-')
return getEntity(tweetEntityType, tweetEntityKey)
}
const getNotificationTweetByIndex = (notificationIndex) => getNotificationTweetByFullEntityId(tweetNotificationIDsArray[notificationIndex])
const tweetNotifications = tweetNotificationIDsArray.map(id => getNotificationTweetByFullEntityId(id))
console.log({
tweetNotifications,
tweetNotificationsLength: tweetNotifications.length,
tweetNotificationsNewestLoaded: tweetNotifications[0].created_at,
tweetNotificationsOldestLoaded: tweetNotifications[tweetNotifications.length - 1].created_at,
})
function getTweetCounts(tweets) {
const byUser = Object.create(null);
const byDay = Object.create(null);
const byUserDay = Object.create(null); // userLabel → { day → count, _total }
const byDayUser = Object.create(null); // day → { userLabel → count }
for (let i = 0; i < tweets.length; i++) {
const tweet = tweets[i];
const userId = tweet.user_id || tweet.user;
const { userLabel } = getUser(userId);
const day = tweet.created_at.slice(0, 10);
// Count by user
byUser[userLabel] = (byUser[userLabel] || 0) + 1;
// Count by day
byDay[day] = (byDay[day] || 0) + 1;
// Count by user → day, plus track total
if (!byUserDay[userLabel]) {
byUserDay[userLabel] = Object.create(null);
byUserDay[userLabel]._total = 0;
}
byUserDay[userLabel][day] = (byUserDay[userLabel][day] || 0) + 1;
byUserDay[userLabel]._total++;
// Count by day → user
if (!byDayUser[day]) byDayUser[day] = Object.create(null);
byDayUser[day][userLabel] = (byDayUser[day][userLabel] || 0) + 1;
}
return { total: tweets.length, byUser, byDay, byUserDay, byDayUser };
}
const {
total: tweetNotificationsTotal,
byUser: tweetNotificationsByUser,
byDay: tweetNotificationsByDay,
byUserDay: tweetNotificationsByUserDay,
byDayUser: tweetNotificationsByDayUser
} = getTweetCounts(tweetNotifications);
console.log({
tweetNotificationsTotal,
tweetNotificationsByUser,
tweetNotificationsByDay,
tweetNotificationsByUserDay,
tweetNotificationsByDayUser
});But since that's all a bit messy, here is a bit of a quick ChatGPT refactor of the code gadgets/etc into more logical groupings:
I will no doubt clean up / refine / enhance these more specifically myself in future; and then build them into a toolkit that I can use to further build out into relevant user scripts and/or a Chrome extension, etc.
- Twitter API Responses / etc (0xdevalias' secret gist)
- Twitter JavaScript scripts / etc (0xdevalias' secret gist)
- Sorting Twitter URLs (0xdevalias' gist)
- AI Agent Toolkit for Automated Browser Userscript Development (0xdevalias' gist)
- Deobfuscating / Unminifying Obfuscated Web App Code (0xdevalias gist)
- Reverse Engineering Webpack Apps (0xdevalias gist)
- React Server Components, Next.js v13+, and Webpack: Notes on Streaming Wire Format (
__next_f, etc) (0xdevalias' gist)) - Fingerprinting Minified JavaScript Libraries / AST Fingerprinting / Source Code Similarity / Etc (0xdevalias gist)
- Bypassing Cloudflare, Akamai, etc (0xdevalias gist)