twitter

package module
v0.6.27 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 41 Imported by: 1

README

go-twitter

Twitter/X scraping library for Go — multi-account pool, GraphQL API, anti-ban, CAPTCHA solving, TOTP 2FA, and session persistence.

Built on go-stealth for TLS fingerprinting, proxy rotation, and rate limiting.

Features

  • Account Pool — round-robin rotation with per-account health tracking, per-endpoint rate limits and per-account pacing
  • GraphQL API — profiles, timelines, followers, search, conversations, lists, communities, bookmarks, home timelines; likes, retweets, tweets, replies; DMs; media upload
  • Anti-Ban — TLS fingerprinting, header ordering, client hints, x-client-transaction-id (xtid), x-xp-forwarded-for (xpff)
  • Auth — multi-step login flow with password, TOTP 2FA, CAPTCHA (Capsolver)
  • Error Recovery — CSRF recovery from the server's ct0, session refresh behind a relogin circuit breaker, ban cooldown, proxy-failure backoff
  • Typed Errors — *APIError plus Is* predicates
  • Session Persistence — pluggable SessionStore; JSON file cache with TTL by default
  • Proxy Support — per-account proxy, exponential backoff on proxy failures

Install

go get github.com/anatolykoptev/go-twitter

Requires Go 1.26.3 or newer (see go.mod).

Quick Start

cfg := twitter.ClientConfig{
    Accounts: []*twitter.Account{
        {Username: "user1", Password: "pass1", TOTPSecret: "BASE32SECRET"},
        {Username: "user2", AuthToken: "existing_token", CT0: "existing_ct0"},
    },
    DefaultProxy:  "http://proxy:8080",
    CaptchaSolver: captcha.NewCapsolver(os.Getenv("CAPSOLVER_KEY")),
}

client, err := twitter.NewClient(cfg)

// Read (the pool picks an account)
user, _ := client.GetUserByScreenName(ctx, "elonmusk")
tweets, _ := client.GetUserTweets(ctx, user.ID, 50)
followers, _ := client.GetFollowers(ctx, user.ID, 100)

// Search
latest, _ := client.SearchTweets(ctx, "$BTC", twitter.SearchLatest, 20)
people, _ := client.SearchUsers(ctx, "go developer", 20)

// Write (account-specific)
tweetID, _ := client.PostWithAccount(ctx, "user1", "Hello Twitter!")
acc := client.AccountByUsername("user1")
_ = client.LikeTweet(ctx, acc, tweetID)

An account needs either AuthToken + CT0, a saved session, or Username + Password (plus TOTPSecret if 2FA is on). ParseAccounts("user:pass[:auth_token[:ct0[:totp_secret]]],...") builds the slice from a comma-separated string.

API

All methods below are on *twitter.Client. Every one of them needs an authenticated account; there is no guest path for any exported method. Auth requirements are defined by requiresAuth in request.go.

  • Pool — the pool picks the account, rotates on failure, and waits up to 5 minutes for an account whose endpoint limit allows the call.
  • Given — you pass the *Account that performs the call (account-pinned; no rotation).
Read
Method Account Description
GetUserByScreenName(ctx, handle) Pool Profile by handle. ErrNotFound for suspended or unavailable users.
GetUserTweets(ctx, userID, count) Pool A user's recent tweets.
GetTweetByID(ctx, tweetID) Pool One tweet (via TweetDetail). ErrNotFound if absent from the page.
GetTweetConversation(ctx, tweetID) Pool Focal tweet plus its conversation entries (replies and ancestors, in page order).
GetFollowers(ctx, userID, max) Pool Paginated follower list.
GetFollowing(ctx, userID, max) Pool Paginated following list.
GetVerifiedFollowers(ctx, userID, max) Pool Paginated blue-verified followers.
GetRetweeters(ctx, tweetID, max) Pool Users who retweeted a tweet.
GetListTweets(ctx, listID, max) Pool Latest tweets in a list.
GetCommunityTweets(ctx, communityID, max) Pool Latest tweets in a community.
GetBookmarks(ctx, max) Pool The pool account's bookmarks.
GetHomeTimeline(ctx, count) Pool Algorithmic "For you" timeline.
GetHomeLatestTimeline(ctx, count) Pool Reverse-chronological "Following" timeline.
Method Account Description
SearchTweets(ctx, query, product, count) Pool Tweet search on SearchTop or SearchLatest. Any other product is rejected before the request.
SearchTimeline(ctx, query, count) Pool Shorthand for SearchTweets on the Latest tab.
SearchUsers(ctx, query, count) Pool People search.

SearchPeople and SearchMedia exist as SearchProduct values; SearchUsers uses SearchPeople internally and SearchTweets accepts only Top and Latest.

Write
Method Account Description
CreateTweet(ctx, acc, text) Given Post a tweet; returns its ID.
PostWithAccount(ctx, username, text) By username CreateTweet for the pool account with that username (must be active).
ReplyTweet(ctx, acc, tweetID, text) Given Reply to a tweet; returns the reply ID.
LikeTweet(ctx, acc, tweetID) Given Like a tweet.
UnlikeTweet(ctx, acc, tweetID) Given Remove a like.
Retweet(ctx, acc, tweetID) Given Retweet; returns the new retweet ID.
Unretweet(ctx, acc, tweetID) Given Remove a retweet; takes the original tweet's ID.
DM
Method Account Description
GetDMInbox(ctx) Pool The pool account's DM conversations with the messages present in the initial inbox state (1.1 API).
SendDM(ctx, acc, conversationID, text) Given Send a text DM into an existing conversation; returns the message ID. conversationID is a conversation ID (1:1 form <recipientID>-<selfID>), not a user ID.
Media
Method Account Description
UploadMedia(ctx, acc, data, mimeType) Given Chunked upload (INIT/APPEND/FINALIZE, polls async video/GIF processing); returns the media ID.
UploadMediaFile(ctx, acc, path) Given Reads a file, infers the MIME type, uploads it.
CreateTweetWithMedia(ctx, acc, text, mediaIDs) Given Post a tweet with uploaded media attached.
PostWithMedia(ctx, username, text, paths...) By username Upload the files and post them from the named account.
Pool and account helpers
Method Description
AccountByUsername(username) Look up a pool account (case-insensitive); nil if absent.
HealthReport() Per-account totals, consecutive failures, and proxy backoff state.
Pool() The underlying go-stealth account pool.
ValidateAccount(ctx, acc) Check an account's credentials against the account settings endpoint.
SetAutoReloginGate(gate) Replace the relogin circuit breaker (see below).

The social package is a thin client for an external account-leasing service. SearchWithSocial, SearchTweetsWithSocial, SearchUsersWithSocial, GetUserWithSocial, GetUserTweetsWithSocial and GetTweetConversationWithSocial in the root package lease an account, run the call, retry on other accounts, and report the outcome. The repeated-header-less-88 escalation does not fire on this path: each attempt builds a fresh Account, so the count never reaches the threshold — a leased account that keeps answering 88 is reported to the credential service as auth_error, not banned.

A caller-side cancellation or deadline (context.Canceled, context.DeadlineExceeded, or the caller ctx ending mid-request) is NOT the leased account's fault: it reports nothing to go-social (the vocabulary has no neutral status) and stops leasing immediately. Reports are sent on a detached, bounded context, so a success verdict still lands when the caller's ctx died before the response arrived.

Error Handling

Every request makes up to 3 attempts, rotating accounts on pool-based calls. Recovery per error class:

Error Code Action
CSRF mismatch 353 Adopt the ct0 from the error response's set-cookie and retry. If x.com offers none, or the retry fails, relogin (subject to the breaker) and retry once. A random ct0 is never generated.
Auth expired 32 Relogin (subject to the breaker) and retry; on failure soft-deactivate the account for AuthCooldown.
Rate limited 429 Mark the endpoint rate-limited for that account until x-rate-limit-reset (15 minutes if absent) and try another account. A 200 whose x-rate-limit-remaining is already spent holds the endpoint the same way, before any 429 (see Rate Limiting).
Rate limited (in a 200 body) 88, x-rate-limit-remaining 0 Same as 429: mark only that endpoint rate-limited for that account until x-rate-limit-reset (15 minutes if absent; a reset in the past is raised to 1 second from now). The account stays active.
Rate limited, header-less (in a 200 body) 88, x-rate-limit-remaining absent or unparseable Same per-endpoint marking, but the response also advances a per-account count: 3 consecutive header-less 88s with no successful response in between are a ban signature — the account is soft-deactivated for BanCooldown and the call returns an IsBanned error. Any success restarts the count, and so does the bench itself.
Banned 88, x-rate-limit-remaining > 0 Soft-deactivate for BanCooldown.
Suspended 64 Permanent removal from the pool.
Locked 326 With a CaptchaSolver, relogin to solve the challenge and retry; otherwise, or on failure, soft-deactivate for BanCooldown.
Blocked / not authorized 161, 179, 219 Soft-deactivate for AuthCooldown.
Internal error 131 Treated as success if the body still carries data; otherwise retried.
Caller input rejected 400 + GraphQL code 214 ErrBadRequest — returned immediately: X rejected the request's own input (bad variable, cursor or handle), so another account would answer identically. No retry, no rotation, no account charge, no endpoint-fault bookkeeping.
Other non-200 — Counts against the account's health; past the threshold the account is soft-deactivated with a growing backoff (NonResponsiveCooldown, doubling, capped at 30 minutes).
Transport failure — For an account with its own Proxy, a proxy-looking error (including a proxy CONNECT 407) is charged to the proxy: exponential backoff (ProxyBackoffInitial to ProxyBackoffMax) and a proxy.down alert, not an account failure. Any other transport error counts against the account.

Observability note: each header-less 88 is still recorded as rate_limited in request.duration (and in MetricsHook the first two of the run are rate-limited calls) — that is the per-response truth. The escalation itself is signalled by the account banned (repeated header-less code 88) log warning, a non-rate-limited MetricsHook outcome on the third call, and the pool's alerts.item_deactivated alert — not by a banned outcome on the instrument.

Errors returned by the client wrap *twitter.APIError (Status, Codes, Message, Err). Test them with errors.As or these predicates, which unwrap for you:

Predicate True for
IsRateLimited HTTP 429, or code 88 with x-rate-limit-remaining 0 or absent (a header-less 88 stays IsRateLimited below the 3-in-a-row escalation)
IsBanned code 88 with x-rate-limit-remaining > 0, or the third consecutive header-less 88
IsSuspended code 64
IsLocked code 326
IsCSRF code 353
IsAuthExpired code 32
IsBlocked code 161
IsNotAuthorized codes 179, 219
IsInternal code 131
IsForbidden HTTP 403 with no recognized code

twitter.ErrNotFound marks deterministic misses (deleted tweet, suspended or unavailable user): the account worked, the resource is not there. Test it with errors.Is.

twitter.ErrBadRequest marks deterministic caller-input rejections: a GraphQL 400 whose error entries carry code 214 (top-level or inside extensions). A bad cursor, handle or variable is the caller's own error — it is never retried, never rotated to another account, never charged to account health, and never counted as an endpoint fault. A GraphQL 400 WITHOUT code 214 keeps the endpoint-fault classification: a stale features set answers kind:"Validation"/name:"BadRequestError" too (code 336), so the kind/name markers alone do not separate caller errors from stale request shapes — only the numeric code does.

twitter.ErrEndpoint (HTTP 404, or GraphQL-shaped 400 without code 214) and twitter.ErrOverloaded (HTTP 5xx, LoadShed) are endpoint-side faults: the committed request shape or X itself is at fault, so the serving account is never charged and the call rotates. Test all sentinels with errors.Is.

Relogin circuit breaker

Relogin clears an account's credentials before logging in again, so a login outage can destroy a still-valid session. To prevent that, NewClient installs a per-account breaker by default: after 3 consecutive failed relogins for an account, further relogins for it are refused for 30 minutes, and the account's credentials are left intact. A successful relogin resets the count. A refused relogin surfaces as a relogin blocked error on that attempt.

To opt out and always relogin, install an always-allow gate after NewClient:

client.SetAutoReloginGate(twitter.AutoReloginGateFunc(
    func(context.Context, string) (bool, string) { return true, "" }))

Any type implementing AutoReloginGate (Allowed(ctx, username) (bool, reason)) can replace the breaker; a nil gate also allows everything. A custom gate that does not implement RecordFailure/RecordSuccess receives no outcome feedback.

Rate Limiting

Limits are per account and per endpoint (operation name such as Followers), in 15-minute windows. Each account's limiter is seeded with caps measured from x.com's x-rate-limit-limit header:

Endpoint Seed (requests / 15 min / account)
UserByScreenName 150
Followers 187
Following 187
Retweeters 500
UserTweets 50

Endpoints without a seed use ClientConfig.RateLimit (default 50 per 15 minutes). On every response from a pool request, an account-pinned POST or a media upload that carries a positive x-rate-limit-limit, the account's cap for that endpoint is updated to it, so the limiter follows what x.com currently grants. The pool skips accounts whose limiter denies the endpoint.

x.com's headers are the authority. Every response (200 and non-200) that carries both x-rate-limit-remaining and x-rate-limit-reset updates that account's window for the endpoint. An older response never raises remaining inside one window (windows are ordered by their reset timestamp; within a window remaining only falls). When remaining is at most 2 (rateLimitHeadroom, spare slots for in-flight calls) and the reset is still ahead, the endpoint is held back on that account until the reset: the current call still succeeds, and the next pick rotates to another account, so a 429 that the headers announced never happens. While an endpoint's window is live (before its reset) the local request counter is ignored; the counter decides before the first header is seen (cold start), when responses carry no headers, and again once a window has expired without a renewing response. Writes and media uploads record the window but do not rotate. Observed values are exported as the twitter.client.ratelimit.* metrics (see docs/metrics.md).

The *WithSocial helpers lease a fresh single-account client per attempt, so that state would die with the lease. They therefore pass the operation to go-social as ?endpoint=<operation> on acquire and send the last observed window back with the usage report as rate_limit (endpoint, limit, remaining, reset); see social.Client.AcquireAccountFor and ReportUsageWithRateLimit. A go-social that predates these fields ignores them.

On top of that, each account is paced individually on pool requests: a random delay of AccountPaceMin to AccountPaceMin + AccountPaceRandom (default 0.8s to 2.0s) between consecutive requests by the same account. Pacing is keyed by account, so a busy caller does not delay callers using other accounts. Account-pinned POSTs and media uploads are not paced. Disable pacing with AccountPaceDisabled.

Independently of pacing, MaxInFlightPerAccount (default 2) bounds how many requests may be in flight on one account at once: the pool skips an account at the cap, and when every eligible account is full the call waits for a slot on the same ctx-bounded machinery as endpoint-limit waits (pool.wait metric); the account-pinned paths wait for their own account's slot. A negative value disables the cap. Live in-flight counts are exported as the twitter.client.pool.inflight gauge (see docs/metrics.md). Caveat for *WithSocial callers: each lease builds a fresh single-account client, so the cap cannot span concurrent leases of the same underlying account — cross-lease concurrency control belongs to go-social.

Anti-Detection

  • xtid — x-client-transaction-id generated from x.com page animation keys, refreshed every 30 minutes. Initialised lazily on the first request carrying an account's cookies (logged-out x.com serves a shell without the bundle). If initialization fails, the header is omitted, retries back off from 60 s to 15 min, and one xtid.init_failed alert per failure episode is sent to PoolAlertHook.
  • xpff — x-xp-forwarded-for generated from the session's guest_id and user agent.
  • CT0 — never rotated on a timer or replaced with a client-generated value on an established session; x.com validates it server-side, so doing so causes CSRF 353 errors. The client adopts the ct0 from each successful response's set-cookie, persists it to the session file, and on a 353 adopts the server-offered ct0 before falling back to relogin. A generated ct0 is used only at login, when x.com sets none.
  • TLS — browser-grade JA3 fingerprints via go-stealth, with a per-account browser profile (AssignBrowserProfile).
  • Headers — fixed header ordering and client hints derived from the account's user agent.
  • Jitter — randomized delay before every request.

Configuration

ClientConfig fields (zero values take the defaults shown):

Field Default Purpose
Accounts — Accounts for the pool.
DefaultProxy none Proxy for the shared client and for accounts without their own Proxy.
Account.Proxy none Per-account proxy. A malformed one disables only that account (account.proxy_client_failed); NewClient fails only when no account is usable, so a single-account client still fails.
SessionTTL 24h How long the default file store reuses a saved session. Ignored by a custom SessionStore.
SessionDir ~/.go-twitter/sessions Where the default file store persists sessions. Ignored when SessionStore is set.
SessionStore file store Where sessions are persisted; see Session storage.
AuthCooldown 1h Soft-deactivation after auth errors.
BanCooldown 6h Soft-deactivation after ban or lock.
NonResponsiveCooldown 5m Base backoff for repeated non-200 failures (doubles, capped at 30m, ±30% jitter).
CaptchaSolver none Solver for login challenges and locked accounts (captcha.NewCapsolver).
RateLimit 50 per 15m Cap for endpoints without a measured seed.
MaxInFlightPerAccount 2 Max concurrent requests per account. Pooled picks skip accounts at the cap and wait (bounded by the caller's ctx, on the same pool.wait machinery as endpoint limits) when every eligible account is full; the account-pinned POST and media paths wait for their account's slot. A negative value disables the cap. See Rate Limiting.
AccountPaceMin / AccountPaceRandom 800ms / 1.2s Per-account request spacing.
AccountPaceDisabled false Turn per-account pacing off.
ProxyBackoffInitial / ProxyBackoffMax 30s / 30m Backoff for failing proxies.
OpenAccountCount 0 Anonymous guest sessions to create at startup.
DisableGuestFallback false Refuse the guest-token fallback on endpoints that allow it.
PoolAlertHook log a warning func(topic string, payload any) called on pool events. This repo emits proxy.down (user, masked proxy, consecutive failures, backoff) and xtid.init_failed; the go-stealth pool emits its own topics, such as account deactivation. account.proxy_client_failed (a per-account proxy client failed to build; payload keys user, proxy (masked label), error (fixed text)).

Session storage

Sessions (auth_token + ct0) are loaded at startup, re-saved whenever x.com rotates ct0, and deleted on relogin. By default they live in <SessionDir>/<username>.json (FileSessionStore, atomic writes, 0600). Set ClientConfig.SessionStore to keep them elsewhere, for example in the database of your credential service:

type dbStore struct{ db *sql.DB }

func (s dbStore) Load(ctx context.Context, user string) (twitter.Session, error) {
    var ss twitter.Session
    err := s.db.QueryRowContext(ctx, `SELECT auth_token, ct0, saved_at FROM tw_sessions WHERE username=$1`, user).
        Scan(&ss.AuthToken, &ss.CT0, &ss.SavedAt)
    if errors.Is(err, sql.ErrNoRows) {
        return twitter.Session{}, twitter.ErrSessionNotFound
    }
    return ss, err
}
func (s dbStore) Save(ctx context.Context, user string, ss twitter.Session) error {
    _, err := s.db.ExecContext(ctx, `INSERT INTO tw_sessions (username, auth_token, ct0, saved_at) VALUES ($1,$2,$3,$4)
        ON CONFLICT (username) DO UPDATE SET auth_token=$2, ct0=$3, saved_at=$4`, user, ss.AuthToken, ss.CT0, ss.SavedAt)
    return err
}
func (s dbStore) Delete(ctx context.Context, user string) error {
    _, err := s.db.ExecContext(ctx, `DELETE FROM tw_sessions WHERE username=$1`, user)
    return err
}

c, err := twitter.NewClient(twitter.ClientConfig{Accounts: accs, SessionStore: dbStore{db}})

Contract: Load returns ErrSessionNotFound on a miss. Expiry is the store's job: the client applies no TTL, and SessionTTL only configures the default file store. Every call gets a context bounded to 5s, and a store error is logged (username only) but never fails an API request. Stores must be safe for concurrent use. Only credentials leased by the *WithSocial helpers bypass the store; static-config accounts still load the stored session first. A Load error other than ErrSessionNotFound is not treated as a miss: provided credentials are used as-is, and an account with none fails instead of logging in with its password.

Types

type TwitterUser struct {
    ID, Handle, DisplayName, Bio string
    Followers, Following, TweetCount, ListedCount int
    CreatedAt                      time.Time
    IsVerified, HasAvatar, HasBio  bool
}

type Tweet struct {
    ID, AuthorID, AuthorHandle, AuthorName, Text string
    CreatedAt                                    time.Time
    Views, Likes, Retweets, Quotes, ReplyCount   int
    TokenMentions                                []string // extracted $TICKER mentions
}

type DMConversation struct {
    ConversationID string
    Participants   []string // user IDs
    Messages       []DMMessage
}

type DMMessage struct {
    ID, ConversationID, SenderID, Text string
    CreatedAt                          time.Time
}

GraphQL ID maintenance (gql-sync)

x.com rotates GraphQL queryIDs and feature-flag defaults inside its JS bundle; a stale ID or flag set returns HTTP 400. cmd/gql-sync extracts the current values from x.com's live bundles and regenerates queryids_gen.go and features_gen.go. At runtime the effective queryID for an operation is resolved as environment override (TWITTER_QID_*) over runtime-refreshed over generated over committed literal.

Runtime refresh. A process that holds an authenticated account also re-reads the IDs from x.com's own bundle (the authenticated page it already fetches for x-client-transaction-id), so an X rotation heals without a release. A refresh runs in the background, on the first authenticated request, after 6 hours, and after 3 endpoint faults (HTTP 404 / GraphQL-shaped 400) on one operation within 2 minutes; refreshes are at least 15 minutes apart, and consecutive failed or rejected refreshes back off exponentially (15 min, 30 min, ... capped at 24 h; a success resets it; the fault trigger obeys it too). A later refresh overlays the earlier one: ids it did not find are carried forward, never reverted. The whole extraction is validated (every ID must match ^[A-Za-z0-9_-]{1,64}$, at least 40% of the known operations must be present, a malformed ID rejects it all) and swapped in atomically; on any failure the last good IDs stay and one queryid.refresh_failed pool alert fires per ok-to-failed transition (the hook is process-wide: the last NewClient that sets PoolAlertHook owns it). The request that hit the fault is not retried — the next call uses the new ID. Feature flags are only added to, never changed or removed. Endpoints[op] keeps the compiled value; the layered value is what requests use (EndpointURL). Set TWITTER_QID_REFRESH=off to disable. Metrics: docs/metrics.md. Operations that are not reachable from the public bundles (the authenticated read cluster and the like/retweet mutations) are maintained by hand, with the env override as the hotfix path.

go run ./cmd/gql-sync -proxy "$PROXY_URL" -fail-on-empty
# logged-out x.com serves a shell without the bundle; pass an account's cookies
# via the environment (never as a flag; sent to x.com and twitter.com hosts only, over https, never logged):
GQL_SYNC_COOKIES='auth_token=...; ct0=...' go run ./cmd/gql-sync -fail-on-empty

Flags: -out (repo root, default .), -proxy, -fixtures (read from a directory instead of the network), -date, -fail-on-empty (exit non-zero instead of leaving the files untouched when nothing is extracted).

CI

gql-sync drift check (.github/workflows/gql-sync-drift.yml) — runs weekly (Mon 06:00 UTC, plus manual workflow_dispatch). It runs gql-sync against x.com's live JS bundle and, if the generated files changed, opens a PR for human review (it never auto-merges generated values — a wrong queryID/flag default is an HTTP 400 on every call).

Requires repo secret TWITTER_PROXY: a proxy URL that can reach x.com from GitHub's runner IPs (bare datacenter IPs are blocked). The job runs with -fail-on-empty, so a missing proxy or a bundle break exits non-zero and fails closed instead of overwriting the committed IDs with nothing.

preflight (.github/workflows/preflight.yml) — make preflight (gofmt, vet, build, test) on every PR and push to main.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var BearerToken = bearerTokens[0]

BearerToken is the active bearer token (first in list).

View Source
var Endpoints = map[string]Endpoint{
	"UserByScreenName": {ID: "IGgvgiOx4QZndDHuD3x9TQ", Name: "UserByScreenName", Features: gqlFeatures()},
	"UserByRestId":     {ID: "VQfQ9wwYdk6j_u2O4vt64Q", Name: "UserByRestId", Features: gqlFeatures()},
	"Followers":        {ID: "9jsVJ9l2uXUIKslHvJqIhw", Name: "Followers", Features: followersFeatures()},
	"Following":        {ID: "OLm4oHZBfqWx8jbcEhWoFw", Name: "Following", Features: followersFeatures()},
	"UserTweets":       {ID: "FOlovQsiHGDls3c0Q_HaSQ", Name: "UserTweets", Features: gqlFeatures()},
	"SearchTimeline":   {ID: "GcXk9vN_d1jUfHNqLacXQA", Name: "SearchTimeline", Features: gqlFeatures()},
	"TweetDetail":      {ID: "VWFGPVAGkZMGRKGe3GFFnA", Name: "TweetDetail", Features: gqlFeatures()},
	"Retweeters":       {ID: "FeoLYPQ-q4bmjGLTZTGs0g", Name: "Retweeters", Features: gqlFeatures()},
	"CreateTweet":      {ID: "7TKRKCPuAGsmYde0CudbVg", Name: "CreateTweet", Features: gqlFeatures()},

	"Bookmarks": {ID: "i8QZ1qqy36ffA3bxfTaf7w", Name: "Bookmarks", Features: gqlFeatures()},

	"HomeTimeline":       {ID: "HCosKfLNW1AcOo3la3mMgg", Name: "HomeTimeline", Features: gqlFeatures()},
	"HomeLatestTimeline": {ID: "zhX91JE87mWvfprhYE97xA", Name: "HomeLatestTimeline", Features: gqlFeatures()},

	"ListLatestTweetsTimeline": {ID: "27HKUy8ulrflZ9Tole038g", Name: "ListLatestTweetsTimeline", Features: gqlFeatures()},
	"CommunityTweetsTimeline":  {ID: "Mvs5UOOEkpXVMDZtUcxR-Q", Name: "CommunityTweetsTimeline", Features: gqlFeatures()},
	"BlueVerifiedFollowers":    {ID: "OBBd6Dw-4qEYbsu3hGkyxg", Name: "BlueVerifiedFollowers", Features: gqlFeatures()},

	"FavoriteTweet":   {ID: "lI07N6Otwv1PhnEgXILM7A", Name: "FavoriteTweet", Features: gqlFeatures()},
	"UnfavoriteTweet": {ID: "ZYKSe-w7KEslx3JhSIk5LA", Name: "UnfavoriteTweet", Features: gqlFeatures()},
	"CreateRetweet":   {ID: "mbRO74GrOvSfRcJnlMapnQ", Name: "CreateRetweet", Features: gqlFeatures()},
	"DeleteRetweet":   {ID: "ZyZigVsNiFO6v1dEks1eWg", Name: "DeleteRetweet", Features: gqlFeatures()},
}

Endpoints maps operation names to their current GraphQL IDs and feature flags.

View Source
var ErrBadRequest = errors.New("twitter: bad request (input rejected)")

ErrBadRequest marks deterministic CALLER-side input errors: a 400 whose error entries carry X's input-validation code (214 — a bad variable, bad cursor, malformed request field). Rotating to another account reproduces the identical response, so the class is never retried, never charged to the serving account, and never counted as an endpoint fault. Match with errors.Is.

View Source
var ErrEndpoint = errors.New("twitter: endpoint fault")

ErrEndpoint marks endpoint-wide faults (HTTP 404, or 400 carrying a GraphQL-shaped errors body that is NOT a recognized caller-input rejection): the committed request shape is wrong for every account (stale queryId, missing xtid, feature mismatch), so the serving account must not be charged — reads rotate to the next account. Match with errors.Is.

View Source
var ErrNotFound = errors.New("twitter: resource not found")

ErrNotFound marks deterministic "the resource does not exist" outcomes — deleted tweets, suspended/unavailable handles. Pool callers treat it as a healthy-account response (no retry, no auth_error report): the credential worked; the thing asked for simply isn't there.

View Source
var ErrOverloaded = errors.New("twitter: x overloaded")

ErrOverloaded marks X-side overload (HTTP 5xx, or a LoadShed error entry — code -1 / message containing "LoadShed"): retryable on another account with backoff, never charged to the serving account. Match with errors.Is.

View Source
var ErrProxyClientUnavailable = errors.New("twitter: account proxy client unavailable")

ErrProxyClientUnavailable is returned when an account is pinned to its own proxy (Account.Proxy != "") but its per-account transport could not be built. The shared client may be direct, so falling back to it would send the account's cookies from this host's IP, ignoring the proxy the caller asked for: the request fails closed instead.

View Source
var ErrSessionNotFound = errors.New("twitter: session not found")

ErrSessionNotFound is returned by SessionStore.Load when no session is stored for the username (or a store-side policy such as a TTL has expired it).

Functions

func ApplyEnvOverrides

func ApplyEnvOverrides()

ApplyEnvOverrides reads TWITTER_QID_* env vars and overrides queryIds in Endpoints. Called automatically by init(); can also be called manually in tests.

func AssignBrowserProfile

func AssignBrowserProfile(acc *Account, idx int)

AssignBrowserProfile sets a browser profile based on index.

func EndpointURL

func EndpointURL(operation string) (string, error)

EndpointURL returns the URL for a named operation, or an error if unknown.

func GenerateCT0

func GenerateCT0() string

GenerateCT0 generates a random 32-byte hex string for use as a ct0 CSRF token.

func GetTweetConversationWithSocial added in v0.6.11

func GetTweetConversationWithSocial(ctx context.Context, sc *social.Client, tweetID string) (*Tweet, []*Tweet, error)

GetTweetConversationWithSocial fetches the focal tweet and its conversation entries (replies/ancestors in page order).

func IsAuthExpired added in v0.6.9

func IsAuthExpired(err error) bool

IsAuthExpired reports an expired-authentication condition (code 32).

func IsBanned added in v0.6.9

func IsBanned(err error) bool

IsBanned reports an account-banned condition (code 88 while the rate-limit window still has requests remaining).

func IsBlocked added in v0.6.9

func IsBlocked(err error) bool

IsBlocked reports a blocked-from-action condition (code 161).

func IsCSRF added in v0.6.9

func IsCSRF(err error) bool

IsCSRF reports a CSRF token mismatch (code 353).

func IsEndpointFault added in v0.6.19

func IsEndpointFault(err error) bool

IsEndpointFault reports an endpoint-wide fault — the failure belongs to the committed endpoint shape (404, GraphQL-shaped 400) or to X itself (5xx, LoadShed), never to the serving account. Covers ErrEndpoint and ErrOverloaded; callers must not charge the account or report it as an auth_error.

func IsForbidden added in v0.6.9

func IsForbidden(err error) bool

IsForbidden reports a 403 response without a recognized JSON code.

func IsInternal added in v0.6.9

func IsInternal(err error) bool

IsInternal reports a Twitter internal error (code 131).

func IsLocked added in v0.6.9

func IsLocked(err error) bool

IsLocked reports an account-locked condition (code 326).

func IsNotAuthorized added in v0.6.9

func IsNotAuthorized(err error) bool

IsNotAuthorized reports a not-authorized condition (codes 179 / 219).

func IsRateLimited added in v0.6.9

func IsRateLimited(err error) bool

IsRateLimited reports a rate-limit condition (HTTP 429, or code 88 with an exhausted window).

func IsSuspended added in v0.6.9

func IsSuspended(err error) bool

IsSuspended reports an account-suspended condition (code 64).

func SetSocialMeterProvider added in v0.6.15

func SetSocialMeterProvider(mp metric.MeterProvider)

SetSocialMeterProvider sets the OpenTelemetry MeterProvider that the *WithSocial helpers record to (request.duration, timeline.freshness, relogins, proxy.failures; see docs/metrics.md). Call it once at start-up. nil (the default) falls back to otel.GetMeterProvider(). The per-call clients behind these helpers never register the pool.accounts gauge.

func SnowflakeTime added in v0.6.13

func SnowflakeTime(id string) (time.Time, error)

SnowflakeTime returns the creation time encoded in a Twitter/X snowflake ID (a tweet ID): (id >> 22) + 1288834974657 ms. Consumers use it to compute their own publication-to-first-seen lag.

Types

type APIError added in v0.6.9

type APIError struct {
	Status  int
	Codes   []int
	Class   errorClass
	Message string
	Err     error
}

APIError represents a Twitter/X API error with classification.

func (*APIError) Error added in v0.6.9

func (e *APIError) Error() string

func (*APIError) Is added in v0.6.19

func (e *APIError) Is(target error) bool

Is reports sentinel membership: ErrEndpoint matches class errEndpoint, ErrOverloaded matches errOverloaded and ErrBadRequest matches errBadRequest, so errors.Is(err, ErrEndpoint) reaches through fmt.Errorf("...: %w") wrappers.

func (*APIError) Unwrap added in v0.6.9

func (e *APIError) Unwrap() error

Unwrap returns the underlying cause, if any.

type Account

type Account struct {
	Username   string
	Password   string
	AuthToken  string
	CT0        string
	TOTPSecret string
	Proxy      string
	UserAgent  string
	Profile    stealth.BrowserProfile

	pool.HealthTracker
	// contains filtered or unexported fields
}

Account represents a Twitter account with credentials for the pool.

func ParseAccounts

func ParseAccounts(raw string) []*Account

ParseAccounts parses a comma-separated list of accounts. Format: "user1:pass1,user2:pass2" or "user1:pass1:auth_token:ct0,..." or "user1:pass1:auth_token:ct0:totp_secret,...".

func (*Account) AllowRequest

func (a *Account) AllowRequest(endpoint string) bool

AllowRequest checks if this account can make a request to the given endpoint. While the endpoint's header window is live (see endpointGate) the account is allowed unless explicitly blocked: the headers are the authority and the local counter is skipped. Otherwise (cold start, headers absent, or the window expired) the local counter decides.

Under header authority each allowed pick spends one slot of the window's remaining estimate, so N concurrent callers cannot all pass on the strength of one stale response: when the estimate reaches the headroom the endpoint is held until the reset. The next response folds in by the recordWindow merge rule (minimum within a window), so the estimate never rises inside a window. A pick that never produces a response (transport error) keeps its slot spent until the window resets: conservative, bounded by the window.

func (*Account) CT0Age

func (a *Account) CT0Age() time.Duration

CT0Age returns the time since the ct0 token was last refreshed.

func (*Account) Credentials

func (a *Account) Credentials() (authToken, ct0, userAgent string)

Credentials returns a snapshot of (authToken, ct0, userAgent) under lock.

func (*Account) EndpointAvailableAt

func (a *Account) EndpointAvailableAt(endpoint string) time.Time

EndpointAvailableAt returns when this account will be available for the given endpoint.

func (*Account) ID

func (a *Account) ID() string

ID implements pool.Identity.

func (*Account) InFlight added in v0.6.24

func (a *Account) InFlight() int

InFlight reports the account's current in-flight request count. The twitter.client.pool.inflight gauge sums it across the pool; tests assert on it.

func (*Account) IsActive

func (a *Account) IsActive() bool

IsActive implements pool.Identity.

Guarded by a.mu: the pool flips these fields (SetActive/SetReactivateAt) under its own lock while readers such as the metrics gauge callback run unlocked relative to the pool, so the Account must synchronise them itself.

func (*Account) IsEndpointRateLimited

func (a *Account) IsEndpointRateLimited(endpoint string) bool

IsEndpointRateLimited returns true if the endpoint is currently blocked.

func (*Account) MarkEndpointRateLimited

func (a *Account) MarkEndpointRateLimited(endpoint string, until time.Time)

MarkEndpointRateLimited marks an endpoint as rate-limited for this account.

func (*Account) ReactivateAt

func (a *Account) ReactivateAt() time.Time

ReactivateAt implements pool.Identity.

func (*Account) RecordSuccess added in v0.6.18

func (a *Account) RecordSuccess()

RecordSuccess records a successful request and clears the consecutive header-less-88 counter: a successful response proves the credentials work, so a later run of header-less 88s starts from zero. It shadows the embedded HealthTracker method on purpose — every success path (200, post-relogin, CSRF recovery, accepted 131) funnels through it, so the reset cannot be missed at a new call site.

func (*Account) RotateCT0

func (a *Account) RotateCT0()

RotateCT0 generates a fresh ct0 token and updates the refresh timestamp.

func (*Account) SetActive

func (a *Account) SetActive(v bool)

SetActive implements pool.Identity.

func (*Account) SetCT0

func (a *Account) SetCT0(ct0 string)

SetCT0 updates the ct0 from a server response.

func (*Account) SetCredentials

func (a *Account) SetCredentials(authToken, ct0 string)

SetCredentials atomically updates auth_token and ct0.

func (*Account) SetReactivateAt

func (a *Account) SetReactivateAt(t time.Time)

SetReactivateAt implements pool.Identity.

func (*Account) SyncRateLimit added in v0.6.6

func (a *Account) SyncRateLimit(endpoint string, respHdrs map[string]string)

SyncRateLimit reads the x-rate-limit-limit header from a response and updates this account's per-endpoint cap when the header carries a valid positive value. A no-op when the header is absent or malformed (the prior cap stands). Called on every observed response (200 and 429) so the cap tracks X's real, possibly-shifting per-account budget.

func (*Account) UpdateEndpointLimit added in v0.6.6

func (a *Account) UpdateEndpointLimit(endpoint string, limit int)

UpdateEndpointLimit sets this account's per-window request cap for the given endpoint, overriding the static default. Driven by the x-rate-limit-limit response header so the limiter self-tunes to X's real per-account-per-endpoint budget. Non-positive limits are ignored by the underlying limiter.

type AccountHealth

type AccountHealth struct {
	Username    string
	Active      bool
	Total       int
	Failed      int
	ConsecFails int

	// ProxyConsecFails is the count of consecutive proxy-layer failures for this
	// account. Non-zero while a dead proxy is being backed off; reset to 0 on
	// the next successful HTTP response. Distinguishes a dead proxy from a dead
	// account — the go-twitter #43 incident mis-attributed 5881 proxy failures
	// to the account layer because this distinction was not observable.
	ProxyConsecFails int
	// ProxyBackoffUntil is when the account's proxy becomes eligible again after
	// exponential backoff. Zero when no proxy backoff is in effect.
	ProxyBackoffUntil time.Time
}

AccountHealth describes the health state of a single pool account.

type AutoReloginGate

type AutoReloginGate interface {
	Allowed(ctx context.Context, username string) (allowed bool, reason string)
}

AutoReloginGate lets an external controller veto automatic re-login flows. A nil gate is treated as "always allow" — preserves backward compatibility for consumers that don't use go-social.

type AutoReloginGateFunc added in v0.6.7

type AutoReloginGateFunc func(ctx context.Context, username string) (allowed bool, reason string)

AutoReloginGateFunc adapts a plain function to the AutoReloginGate interface.

OPT-OUT: as of v0.6.7 a client that never calls SetAutoReloginGate gets the internal per-account relogin breaker by default (see wireDefaultReloginGate), which stops auto-relogin for an account after reloginBreakerThreshold consecutive failures for reloginBreakerWindow. A consumer that wants the pre-v0.6.7 "always relogin, unlimited" behavior must install an always-allow gate explicitly:

c.SetAutoReloginGate(AutoReloginGateFunc(
	func(context.Context, string) (bool, string) { return true, "" }))

func (AutoReloginGateFunc) Allowed added in v0.6.7

func (f AutoReloginGateFunc) Allowed(ctx context.Context, username string) (bool, string)

Allowed implements AutoReloginGate.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the top-level Twitter scraping client.

func NewClient

func NewClient(cfg ClientConfig) (*Client, error)

NewClient creates a fully-wired Twitter client.

func (*Client) AccountByUsername

func (c *Client) AccountByUsername(username string) *Account

AccountByUsername returns the pool account matching the given username (case-insensitive). Returns nil if not found.

func (*Client) Close added in v0.6.13

func (c *Client) Close() error

Close unregisters the client's pool.accounts gauge callback so the client can be garbage collected and a replacement client can report on the same MeterProvider. It is idempotent and safe on a nil client; it does not stop in-flight requests.

func (*Client) CreateTweet

func (c *Client) CreateTweet(ctx context.Context, acc *Account, text string) (string, error)

CreateTweet posts a tweet from a specific account. Returns the tweet ID on success.

func (*Client) CreateTweetWithMedia added in v0.6.0

func (c *Client) CreateTweetWithMedia(ctx context.Context, acc *Account, text string, mediaIDs []string) (string, error)

CreateTweetWithMedia posts a tweet with attached media IDs from a specific account. With no media IDs it is equivalent to CreateTweet. Returns the tweet ID on success.

func (*Client) GetBookmarks added in v0.6.0

func (c *Client) GetBookmarks(ctx context.Context, maxCount int) ([]*Tweet, error)

GetBookmarks fetches the authenticated pool account's bookmarked tweets (paginated up to maxCount).

func (*Client) GetCommunityTweets added in v0.6.0

func (c *Client) GetCommunityTweets(ctx context.Context, communityID string, maxCount int) ([]*Tweet, error)

GetCommunityTweets fetches the latest tweets in a community (paginated up to maxCount).

func (*Client) GetDMInbox added in v0.6.0

func (c *Client) GetDMInbox(ctx context.Context) ([]*DMConversation, error)

GetDMInbox fetches the authenticated pool account's DM inbox. The inbox is per-account state, so doGET pins a real account from the pool (DMInbox is in requiresAuth — no guest fallback). Returns the conversations, each with its participants and the messages present in the initial inbox state. An empty inbox (0 conversations) is a valid, non-error result.

func (*Client) GetFollowers

func (c *Client) GetFollowers(ctx context.Context, userID string, maxCount int) ([]*TwitterUser, error)

GetFollowers fetches followers for a user (paginated).

func (*Client) GetFollowing

func (c *Client) GetFollowing(ctx context.Context, userID string, maxCount int) ([]*TwitterUser, error)

GetFollowing fetches accounts a user follows (paginated).

func (*Client) GetHomeLatestTimeline added in v0.6.0

func (c *Client) GetHomeLatestTimeline(ctx context.Context, count int) ([]*Tweet, error)

GetHomeLatestTimeline fetches the reverse-chronological "Following" home timeline. POST (pool), mirroring SearchTimeline.

func (*Client) GetHomeTimeline added in v0.6.0

func (c *Client) GetHomeTimeline(ctx context.Context, count int) ([]*Tweet, error)

GetHomeTimeline fetches the algorithmic "For you" home timeline. POST (pool), mirroring SearchTimeline.

func (*Client) GetListTweets added in v0.6.0

func (c *Client) GetListTweets(ctx context.Context, listID string, maxCount int) ([]*Tweet, error)

GetListTweets fetches the latest tweets in a list (paginated up to maxCount).

func (*Client) GetRetweeters

func (c *Client) GetRetweeters(ctx context.Context, tweetID string, maxCount int) ([]*TwitterUser, error)

GetRetweeters fetches users who retweeted a tweet (paginated).

func (*Client) GetTweetByID

func (c *Client) GetTweetByID(ctx context.Context, tweetID string) (*Tweet, error)

GetTweetByID fetches a single tweet by its ID.

func (*Client) GetTweetConversation added in v0.6.11

func (c *Client) GetTweetConversation(ctx context.Context, tweetID string) (*Tweet, []*Tweet, error)

GetTweetConversation fetches the focal tweet plus its conversation entries (replies and ancestors, in page order) via TweetDetail.

func (*Client) GetUserByScreenName

func (c *Client) GetUserByScreenName(ctx context.Context, handle string) (*TwitterUser, error)

GetUserByScreenName fetches a user profile by Twitter handle.

func (*Client) GetUserTweets

func (c *Client) GetUserTweets(ctx context.Context, userID string, count int) ([]*Tweet, error)

GetUserTweets fetches recent tweets for a user.

func (*Client) GetVerifiedFollowers added in v0.6.0

func (c *Client) GetVerifiedFollowers(ctx context.Context, userID string, maxCount int) ([]*TwitterUser, error)

GetVerifiedFollowers fetches a user's blue-verified followers (paginated up to maxCount). The response shares the followers shape (data.user.result.timeline.timeline), so it reuses the existing fetchUserList helper + parseUserList parser.

func (*Client) HealthReport

func (c *Client) HealthReport() []AccountHealth

HealthReport returns health stats for all accounts in the pool.

func (*Client) LikeTweet added in v0.6.0

func (c *Client) LikeTweet(ctx context.Context, acc *Account, tweetID string) error

LikeTweet favorites a tweet from a specific account. Returns nil on the "Done" ack, else an error.

func (*Client) Pool

func (c *Client) Pool() *pool.Pool[*Account]

Pool returns the underlying account pool.

func (*Client) PostWithAccount

func (c *Client) PostWithAccount(ctx context.Context, username, text string) (string, error)

PostWithAccount posts a tweet from a named account (by username). Returns the tweet ID on success.

func (*Client) PostWithMedia added in v0.6.0

func (c *Client) PostWithMedia(ctx context.Context, username, text string, paths ...string) (string, error)

PostWithMedia uploads the given files and posts a tweet attaching them, from a named account (by username). Returns the tweet ID on success.

func (*Client) ReplyTweet added in v0.6.0

func (c *Client) ReplyTweet(ctx context.Context, acc *Account, tweetID, text string) (string, error)

ReplyTweet posts a reply to tweetID from a specific account. Returns the reply tweet ID. It routes through the CreateTweet op (reply.in_reply_to_tweet_id set) and reuses parseCreateTweet for the result.

func (*Client) Retweet added in v0.6.0

func (c *Client) Retweet(ctx context.Context, acc *Account, tweetID string) (string, error)

Retweet retweets a tweet from a specific account. Returns the new retweet ID.

func (*Client) SearchTimeline

func (c *Client) SearchTimeline(ctx context.Context, query string, count int) ([]*Tweet, error)

SearchTimeline searches for tweets matching a query on the Latest tab. Uses POST (Twitter migrated this endpoint from GET in March 2026).

func (*Client) SearchTweets added in v0.6.11

func (c *Client) SearchTweets(ctx context.Context, query string, product SearchProduct, count int) ([]*Tweet, error)

SearchTweets searches tweets on an explicit tab — Top or Latest. Anything else is rejected before the request so a typo can't silently read Latest.

func (*Client) SearchUsers added in v0.6.11

func (c *Client) SearchUsers(ctx context.Context, query string, count int) ([]*TwitterUser, error)

SearchUsers searches the People tab.

func (*Client) SendDM added in v0.6.0

func (c *Client) SendDM(ctx context.Context, acc *Account, conversationID, text string) (string, error)

SendDM sends a text direct message into an existing conversation from a specific account (account-pinned write, mirroring CreateTweet/engagement mutations). Returns the created message id.

FAIL-CLOSED: a 200 response with no usable message id (empty body, a DM validation failure, or a surfaced API error) returns an error — never a silent "sent". Mirrors parseCreateTweet's empty-result guard. conversationID is a CONVERSATION id (1:1 form "<recipientID>-<selfID>", per twikit), NOT a user id; there is no derive helper — the caller forms it.

func (*Client) SetAutoReloginGate

func (c *Client) SetAutoReloginGate(g AutoReloginGate)

SetAutoReloginGate registers a gate checked before relogin attempts.

func (*Client) UnlikeTweet added in v0.6.0

func (c *Client) UnlikeTweet(ctx context.Context, acc *Account, tweetID string) error

UnlikeTweet un-favorites a tweet from a specific account. Returns nil on the "Done" ack, else an error.

func (*Client) Unretweet added in v0.6.0

func (c *Client) Unretweet(ctx context.Context, acc *Account, tweetID string) (string, error)

Unretweet removes a retweet from a specific account. Returns the source tweet ID. The DeleteRetweet op keys the target by "source_tweet_id" (the ID of the ORIGINAL tweet, not the retweet's own ID) — confirmed live 2026-06-20: sending "tweet_id" yields HTTP 422 GRAPHQL_VALIDATION_FAILED ("source_tweet_id must be defined"). Matches trevorhobenshield/twitter-api-client + dimdenGD/OldTweetDeck.

func (*Client) UploadMedia added in v0.6.0

func (c *Client) UploadMedia(ctx context.Context, acc *Account, data []byte, mediaType string) (string, error)

UploadMedia uploads raw media bytes via the chunked INIT/APPEND/FINALIZE flow using the given account's authenticated stealth client, and returns the media_id string for use in CreateTweetWithMedia. mediaType is the MIME type (e.g. "image/jpeg", "image/gif", "video/mp4").

func (*Client) UploadMediaFile added in v0.6.0

func (c *Client) UploadMediaFile(ctx context.Context, acc *Account, path string) (string, error)

UploadMediaFile reads a file from disk, infers its MIME type, and uploads it.

func (*Client) ValidateAccount

func (c *Client) ValidateAccount(ctx context.Context, acc *Account) error

ValidateAccount checks that an account's auth_token and ct0 are still valid by calling the authenticated account/settings.json REST endpoint. Returns nil on HTTP 200, an error on 401/403, or a wrapped error on network failure.

type ClientConfig

type ClientConfig struct {
	// Accounts is the list of Twitter accounts to use.
	Accounts []*Account

	// DefaultProxy is the proxy URL for accounts without per-account proxies.
	DefaultProxy string

	// SessionTTL controls how long saved sessions are considered valid. It
	// configures the default file store only; a custom SessionStore owns its
	// own expiry policy.
	SessionTTL time.Duration

	// AuthCooldown is the soft-deactivation duration for auth errors.
	AuthCooldown time.Duration

	// BanCooldown is the soft-deactivation duration for banned/locked accounts.
	BanCooldown time.Duration

	// NonResponsiveCooldown is the BASE backoff for accounts that trip the
	// consecutive-failure threshold against a transiently-broken endpoint. The
	// effective cooldown grows per trip (x2, capped at 30m, +/-30% jitter) so a
	// flapping upstream self-heals once it recovers instead of latching the pool
	// permanently. Default: 5m.
	NonResponsiveCooldown time.Duration

	// CaptchaSolver is the optional CAPTCHA solver for locked accounts.
	CaptchaSolver captcha.Solver

	// RateLimit configures per-account per-endpoint rate limiting.
	RateLimit ratelimit.Config

	// OpenAccountCount is the number of anonymous guest accounts to create at startup.
	OpenAccountCount int

	// MetricsHook is called on each API request for external metrics collection.
	// endpoint is the operation name, success and rateLimited indicate the outcome.
	//
	// Deprecated: use MeterProvider. MetricsHook carries no latency or error
	// class; it keeps working unchanged for existing consumers.
	MetricsHook func(endpoint string, success, rateLimited bool)

	// MeterProvider supplies the OpenTelemetry meter provider for the client's
	// metrics (see docs/metrics.md). nil = otel.GetMeterProvider(), which is a
	// no-op until the application installs an SDK provider.
	MeterProvider metric.MeterProvider

	// SessionDir overrides the default session persistence directory used by
	// the default file store. Ignored when SessionStore is set.
	// Default: ~/.go-twitter/sessions
	SessionDir string

	// SessionStore persists per-account sessions. nil = a FileSessionStore
	// built from SessionDir and SessionTTL (the historical behaviour). Set it
	// to make an external credential store the single source of truth.
	SessionStore SessionStore

	// ProxyBackoffInitial is the initial backoff for proxy failures.
	ProxyBackoffInitial time.Duration

	// ProxyBackoffMax is the maximum backoff for proxy failures.
	ProxyBackoffMax time.Duration

	// PoolAlertHook is called when the pool emits alerts (account deactivation, proxy failures, etc.).
	// topic is the alert type (e.g. "pool.deactivated"), payload contains details.
	PoolAlertHook func(topic string, payload any)

	// DisableGuestFallback disables the guest-token fallback path entirely.
	// When true, endpoints that would normally fall back to guest mode after
	// pool exhaustion will return an error instead. Recommended in production
	// where guest tokens from datacenter IPs return persistent 403 errors.
	// Default: false (guest fallback enabled for backward compatibility).
	DisableGuestFallback bool

	// AccountPaceMin is the minimum human-pace delay between consecutive requests
	// MADE BY THE SAME ACCOUNT, applied AFTER the pool selects an account and IN
	// ADDITION TO the anti-fingerprint jitter. Pacing is keyed by account, not by
	// domain: each account self-paces its own request rhythm via an independent
	// timestamp, so a low-frequency caller (seed) is never starved by a global
	// gate that a high-frequency caller (KOL/VC) holds. It is anti-burst stealth
	// spacing UNDER the per-account-per-endpoint rate-limit ceiling, never a
	// throughput cap below it.
	//
	// Together with AccountPaceRandom the realized inter-request spacing for one
	// account is [AccountPaceMin, AccountPaceMin+AccountPaceRandom) (variable,
	// never below AccountPaceMin). Default: 800ms. Set AccountPaceDisabled to off.
	AccountPaceMin time.Duration

	// AccountPaceRandom is the random jitter added on top of AccountPaceMin for
	// the per-account spacing (see AccountPaceMin). Default: 1.2s.
	AccountPaceRandom time.Duration

	// AccountPaceDisabled turns off the per-account human pace entirely. When
	// true only the anti-fingerprint jitter applies. Default: false (pacing
	// enabled with safe defaults).
	AccountPaceDisabled bool

	// MaxInFlightPerAccount caps the number of requests a single account may
	// have in flight at once — the in-process counterpart of nitter's
	// maxConcurrentReqs. A pool pick that would exceed the cap skips the
	// account; when every eligible account is at the cap the call waits for a
	// slot (bounded by the caller's ctx and the shared pool-wait budget, see
	// twitter.client.pool.wait.duration). Account-pinned paths (doPOST, media
	// upload) wait for the pinned account's slot the same way.
	// Default: 2. A negative value disables the cap.
	// The cap bounds in-flight SLOTS, not sockets: a cancelled request releases
	// its slot at once, but its socket can outlive the slot until go-stealth's
	// own request timeout.
	MaxInFlightPerAccount int
	// contains filtered or unexported fields
}

ClientConfig holds all configuration for the Twitter client.

type Cursor

type Cursor struct {
	Value  string
	IsNext bool
}

Cursor is used for paginated GraphQL requests.

type DMConversation added in v0.6.0

type DMConversation struct {
	ConversationID string
	Participants   []string
	Messages       []DMMessage
}

DMConversation is a single direct-message conversation from the 1.1 inbox. Participants holds the user IDs in the thread (the inbox shape carries participant IDs, not full user objects). Messages are the messages seen in the inbox_initial_state entries for this conversation.

type DMMessage added in v0.6.0

type DMMessage struct {
	ID             string
	ConversationID string
	SenderID       string
	Text           string
	CreatedAt      time.Time
}

DMMessage is a single direct message. Fields mirror the 1.1 inbox message_data shape (id, sender_id, text, time as unix millis).

type Endpoint

type Endpoint struct {
	ID       string
	Name     string
	Features map[string]any
}

Endpoint holds the operation ID, path template, and per-operation feature flags.

func (Endpoint) URL

func (e Endpoint) URL() string

URL returns the full URL for this endpoint. The variable ID segment is path-escaped as defense-in-depth: a poisoned queryID must never break out of its path segment. e.Name is a hardcoded const, so it is left verbatim.

type FileSessionStore added in v0.6.17

type FileSessionStore struct {
	// Dir is the session directory. Empty means ~/.go-twitter/sessions.
	Dir string
	// TTL expires sessions older than this on Load (reported as
	// ErrSessionNotFound). TTL <= 0 means no expiry; note NewClient defaults
	// ClientConfig.SessionTTL 0 to 24h before building the default store.
	TTL time.Duration
}

FileSessionStore keeps one JSON file per account at <Dir>/<username>.json. Writes are atomic (temp file + fsync + rename) so a concurrent Load sees the previous or the new complete file, never a truncated one. Files are 0600 in a 0700 directory.

func (*FileSessionStore) Delete added in v0.6.17

func (f *FileSessionStore) Delete(_ context.Context, username string) error

Delete implements SessionStore.

func (*FileSessionStore) Load added in v0.6.17

func (f *FileSessionStore) Load(_ context.Context, username string) (Session, error)

Load implements SessionStore. A missing, TTL-expired or undecodable (truncated/corrupt) file yields ErrSessionNotFound, as does a username that is not a valid file name: the file holds nothing usable, and treating it as a miss lets the client log in and overwrite it (Save re-creates a corrupt file; for an invalid name Save still rejects it and logs, exactly as before the store was pluggable). Genuine I/O failures, permission denied included, are returned: the file may hold a newer valid session that cannot be read, and a Save would most likely fail too, so the client must not log in blindly.

func (*FileSessionStore) Save added in v0.6.17

func (f *FileSessionStore) Save(_ context.Context, username string, s Session) error

Save implements SessionStore. A zero SavedAt is stamped with the current time.

type SearchProduct added in v0.6.11

type SearchProduct string

SearchProduct selects the SearchTimeline results tab.

const (
	SearchTop    SearchProduct = "Top"
	SearchLatest SearchProduct = "Latest"
	SearchPeople SearchProduct = "People"
	SearchMedia  SearchProduct = "Media"
)

type Session added in v0.6.17

type Session struct {
	AuthToken string    `json:"auth_token"`
	CT0       string    `json:"ct0"`
	SavedAt   time.Time `json:"saved_at"`
}

Session is the persisted credential pair of one account. The JSON tags are the on-disk format of FileSessionStore and are kept stable.

String and LogValue redact the tokens so a Session printed or logged by accident does not leak them; JSON marshalling is unaffected.

func (Session) GoString added in v0.6.17

func (s Session) GoString() string

GoString implements fmt.GoStringer so %#v does not leak the tokens.

func (Session) LogValue added in v0.6.17

func (s Session) LogValue() slog.Value

LogValue implements slog.LogValuer with the tokens redacted.

func (Session) String added in v0.6.17

func (s Session) String() string

String implements fmt.Stringer with the tokens redacted.

type SessionStore added in v0.6.17

type SessionStore interface {
	// Load returns the stored session, or ErrSessionNotFound when there is none.
	Load(ctx context.Context, username string) (Session, error)
	// Save stores the session, replacing any previous one.
	Save(ctx context.Context, username string, s Session) error
	// Delete removes the session; deleting a missing session is not an error.
	Delete(ctx context.Context, username string) error
}

SessionStore persists per-account sessions (auth_token + ct0) across process restarts and ct0 rotations. Implementations must be safe for concurrent use.

The client never lets a store failure fail an API request: errors are logged (username only, never tokens) and the request continues. Every call carries a context bounded to a few seconds.

Implementations MUST honour ctx: the client bounds every call with a 5s timeout and calls Save synchronously on the request path right after a ct0 rotation, so a store that ignores ctx stalls requests. Save is last-write-wins; the client sets Session.SavedAt (stores may compare it). Load must return ErrSessionNotFound for a miss; any other Load error is NOT treated as a miss: an account without provided credentials then fails instead of logging in with a password, so a flaky store cannot cause a login storm.

Session lifetime is the store's concern: the client applies no TTL of its own. FileSessionStore honours ClientConfig.SessionTTL; a database-backed store can ignore Session.SavedAt entirely or enforce its own policy.

Accounts leased by the *WithSocial helpers are credential-service owned and never touch the store.

type Tweet

type Tweet struct {
	ID            string
	AuthorID      string
	AuthorHandle  string // @screen_name (from core.user_results)
	AuthorName    string // display name (from core.user_results)
	Text          string
	CreatedAt     time.Time
	Views         int
	Likes         int
	Retweets      int
	Quotes        int
	ReplyCount    int
	TokenMentions []string // extracted $TICKER patterns, e.g. ["BTC", "ETH"]
	// contains filtered or unexported fields
}

Tweet represents a single tweet.

func GetTweetByIDWithSocial added in v0.6.25

func GetTweetByIDWithSocial(ctx context.Context, sc *social.Client, tweetID string) (*Tweet, error)

GetTweetByIDWithSocial fetches a single tweet via the TweetDetail page.

func GetUserTweetsByIDWithSocial added in v0.6.25

func GetUserTweetsByIDWithSocial(ctx context.Context, sc *social.Client, userID string, limit int) ([]*Tweet, error)

GetUserTweetsByIDWithSocial fetches recent tweets for a numeric user ID — the caller already resolved the handle, so no UserByScreenName lookup burns the lease's budget.

func GetUserTweetsWithSocial added in v0.6.11

func GetUserTweetsWithSocial(ctx context.Context, sc *social.Client, handle string, limit int) ([]*Tweet, error)

GetUserTweetsWithSocial resolves the handle and fetches its recent tweets — two GraphQL calls inside one account attempt.

func SearchTweetsWithSocial added in v0.6.11

func SearchTweetsWithSocial(ctx context.Context, sc *social.Client, query string, product SearchProduct, limit int) ([]*Tweet, error)

SearchTweetsWithSocial searches tweets on an explicit tab (Top or Latest).

func SearchWithSocial

func SearchWithSocial(ctx context.Context, sc *social.Client, query string, limit int) ([]*Tweet, error)

SearchWithSocial acquires accounts from go-social and searches Twitter on the Latest tab, retrying with different accounts on failure. go-social rotates accounts on each call, so retries naturally use different credentials.

type TwitterUser

type TwitterUser struct {
	ID          string
	Handle      string
	DisplayName string
	Bio         string
	Followers   int
	Following   int
	TweetCount  int
	ListedCount int
	CreatedAt   time.Time
	IsVerified  bool
	HasAvatar   bool
	HasBio      bool
}

TwitterUser represents a Twitter/X account profile.

func GetFollowersWithSocial added in v0.6.25

func GetFollowersWithSocial(ctx context.Context, sc *social.Client, userID string, limit int) ([]*TwitterUser, error)

GetFollowersWithSocial lists the accounts following userID.

func GetFollowingWithSocial added in v0.6.25

func GetFollowingWithSocial(ctx context.Context, sc *social.Client, userID string, limit int) ([]*TwitterUser, error)

GetFollowingWithSocial lists the accounts userID follows.

func GetRetweetersWithSocial added in v0.6.25

func GetRetweetersWithSocial(ctx context.Context, sc *social.Client, tweetID string, limit int) ([]*TwitterUser, error)

GetRetweetersWithSocial lists the accounts that retweeted tweetID.

func GetUserWithSocial added in v0.6.11

func GetUserWithSocial(ctx context.Context, sc *social.Client, handle string) (*TwitterUser, error)

GetUserWithSocial fetches a profile by handle.

func SearchUsersWithSocial added in v0.6.11

func SearchUsersWithSocial(ctx context.Context, sc *social.Client, query string, limit int) ([]*TwitterUser, error)

SearchUsersWithSocial searches the People tab.

Directories

Path Synopsis
cmd
gql-sync command
Command gql-sync auto-extracts Twitter GraphQL queryIDs from x.com's webpack bundles and regenerates queryids_gen.go — the "generated" layer of the endpoints override chain (env > generated > committed).
Command gql-sync auto-extracts Twitter GraphQL queryIDs from x.com's webpack bundles and regenerates queryids_gen.go — the "generated" layer of the endpoints override chain (env > generated > committed).
internal
bundle
Package bundle fetches x.com warm pages, reassembles the webpack chunk map (chunkID->name, chunkID->hash), resolves a module name to its abs.twimg.com bundle URL, BFS-follows import() refs, and caches the snapshot to disk with a TTL.
Package bundle fetches x.com warm pages, reassembles the webpack chunk map (chunkID->name, chunkID->hash), resolves a module name to its abs.twimg.com bundle URL, BFS-follows import() refs, and caches the snapshot to disk with a TTL.
gqlextract
Package gqlextract extracts GraphQL queryIDs and feature flags from x.com's own bundles.
Package gqlextract extracts GraphQL queryIDs and feature flags from x.com's own bundles.
Package slogsample provides sampling utilities for slog handlers.
Package slogsample provides sampling utilities for slog handlers.
Package social provides a thin HTTP client for the go-social account management API.
Package social provides a thin HTTP client for the go-social account management API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL