twitter

package module
v0.8.13 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 48 Imported by: 1

README

go-twitter

Go Reference CI Release

An unofficial Go client for the X (Twitter) web API.

Give it one or more logged-in sessions and call typed methods for timelines, search, profiles, posts and DMs. The client spreads requests across accounts, follows the rate limits X reports, picks up changed GraphQL operation IDs on its own, and exposes what it does through OpenTelemetry metrics.

Install

go get github.com/anatolykoptev/go-twitter

Requires Go 1.27.3 or newer.

Quick start

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	twitter "github.com/anatolykoptev/go-twitter"
)

func main() {
	client, err := twitter.NewClient(twitter.ClientConfig{
		Accounts: []*twitter.Account{{
			Username:  "your_handle",
			AuthToken: os.Getenv("X_AUTH_TOKEN"),
			CT0:       os.Getenv("X_CT0"),
		}},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	tweets, err := client.SearchTweets(context.Background(), "golang", twitter.SearchLatest, 20)
	if err != nil {
		log.Fatal(err)
	}
	for _, t := range tweets {
		fmt.Printf("@%s: %s\n", t.AuthorHandle, t.Text)
	}
}

auth_token and ct0 are the cookies of a logged-in x.com browser session. They give full access to the account, so keep them out of source code and logs.

What it covers

Area Methods
Profiles and tweets GetUserByScreenName, GetUserTweets, GetTweetByID, GetTweetConversation
Timelines GetHomeTimeline, GetHomeLatestTimeline, GetListTweets, GetCommunityTweets, GetBookmarks
Search SearchTweets, SearchUsers
Relationships GetFollowers, GetFollowing, GetVerifiedFollowers, GetRetweeters, FriendshipState
Notifications GetDeviceNotifications, SetDeviceNotifications
Posting CreateTweet, ReplyTweet, LikeTweet, Retweet, FollowUser, UploadMedia
Direct messages GetDMInbox, SendDM

The API reference has the full list.

Built for long-running services

  • Account pool. Rotates accounts, tracks the health of each one, and moves on when an account fails or runs out of budget.
  • Rate limits. Follows X's x-rate-limit-* headers per account and endpoint, with per-account spend shares, in-flight caps and request spacing.
  • Typed errors. Every failure is an *APIError with predicates such as IsRateLimited and IsLocked.
  • Operation ID refresh. Reads current GraphQL IDs from x.com's bundles at runtime, so a rotated ID recovers without a release.
  • Session storage. Saves sessions to files by default, or anywhere else through the SessionStore interface.
  • Observability. OpenTelemetry metrics, a request observer hook and per-request tracing.

Status and limits

The project is pre-1.0, so the API can change between minor versions. It relies on web endpoints that X can change without notice. Known gaps:

  • Tweet and TwitterUser are compact: there is no media, and no reply, retweet or quote markers yet (#63).
  • Timelines leave out pinned tweets (#52).
  • Pagination has no exported cursors (#67).
  • Rate-limit state lives in memory and resets on restart (#66).
  • x.com is moving to a new web client, and only part of it is handled so far (#119, inventory).
  • Password login uses X's older flow and is not tested against the current one. Use session cookies.

Documentation

Guide Contents
API reference Every method, its account handling, and the types
Configuration Accounts, ClientConfig, environment variables
Error handling Recovery per error class, predicates, relogin breaker
Rate limiting Windows, spend shares, holds, pacing, request observer
Session storage The SessionStore contract and a database example
Request format What the client sends and how to configure it
Metrics OpenTelemetry instruments and attributes
Maintenance Keeping GraphQL IDs current, tools, CI

Release history is in the changelog.

Contributing

See CONTRIBUTING.md. To report a vulnerability, follow SECURITY.md instead of opening a public issue.

Disclaimer

This is an independent project. It is not affiliated with, endorsed by or sponsored by X Corp. It uses X's web endpoints rather than the official X API. You are responsible for following X's terms and the laws that apply to your use.

License

MIT

Documentation

Overview

Package twitter is an unofficial client for the X (Twitter) web API.

A Client holds a pool of accounts and exposes typed methods for timelines, search, profiles, posting and direct messages. Reads pick an account from the pool and rotate on failure; writes run on the account you pass. The client follows the rate limits X reports per account and endpoint, and failures surface as *APIError values with predicates such as IsRateLimited.

The response types (Tweet, TwitterUser, DMConversation, DMMessage) are defined in package model and aliased here, so either import path works.

Accounts authenticate with the auth_token and ct0 cookies of a logged-in x.com session. Treat both as passwords.

See the docs directory of the repository for the guides on configuration, errors, rate limits and metrics.

Index

Examples

Constants

View Source
const (
	WireChrome = "chrome"
	WireLegacy = "legacy"
)

Wire names of the generations, as WireFor and Account.Wire report them.

View Source
const RateLimitHeadroom = 2

RateLimitHeadroom is the number of requests X must still report as remaining for an endpoint to stay eligible. An endpoint whose window shows remaining <= RateLimitHeadroom is held back until the window resets. It is exported so consumers do not have to duplicate the constant to compute an account's effective budget (see Account.SetBudgetPct).

Why 2 and not 0: a response's remaining counts requests X had served when it answered, not requests already in flight. With the 50-per-window caps X gives UserTweets/SearchTimeline, two spare slots absorb the handful of concurrent calls that share an account (4% of the window) so those calls do not land on a 429, which is what draws X's attention to an account. Why not nitter's 10: on a 50-request window that would waste a fifth of the budget.

Variables

View Source
var BearerToken = bearerTokens[0]

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

View Source
var Endpoints = endpointsFromTable(operations.Table())

Endpoints maps operation names to their current GraphQL IDs and feature flags. The committed table and the generated queryIDs live in internal/operations.

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 410, 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 ErrLeaseIdentity = errors.New("twitter: leased account's browser identity is unusable")

ErrLeaseIdentity marks a lease whose stated browser identity cannot be presented as stated: it is incomplete, or names a TLS profile this build does not know. The lease is skipped rather than served under a different browser.

View Source
var ErrNotFound = model.ErrNotFound

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).

View Source
var ErrUnexpectedResponse = model.ErrUnexpectedResponse

ErrUnexpectedResponse marks a 2xx response the library cannot read as the requested data. Either X answered with only errors the library does not recognise: the query ran — the errors[] envelope proves the committed request shape executed — but no error entry matches a known class or a deterministic-miss marker. The class is terminal and account/endpoint-neutral: no retry, no rotation, no guest fallback, no relogin, no account charge, and NOT an endpoint fault. Or the data arrived without the timeline object a timeline read requires (for example {"data":{"user":{}}}): an empty list there would read as "no new tweets", so the parser returns this error instead. Match with errors.Is.

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, _ int)

AssignBrowserProfile gives acc the browser profile derived from its username, and the profile's User-Agent. The profile is a function of the account, so it is the same on every run and whatever the account's position in a list.

The index parameter is ignored and kept only for source compatibility: an index-based pick changed an account's fingerprint whenever the pool was reordered or resized.

func EndpointLimits added in v0.7.2

func EndpointLimits() map[string]int

EndpointLimits returns the measured per-account-per-15-min request caps the client seeds every account's per-endpoint limiter with (see endpointDefaultLimit). It returns a COPY: mutating the result does not change the table or what new accounts are seeded with. The map holds only measured ceilings; an endpoint absent from it falls back to the account's configured limiter cap (ClientConfig.RateLimit.RequestsPerWindow), not to a fixed constant.

func EndpointURL

func EndpointURL(operation string) (string, error)

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

func FlushSocialReports added in v0.8.4

func FlushSocialReports(ctx context.Context) error

FlushSocialReports waits until the usage reports that *WithSocial calls have queued in the background are delivered, or ctx is done. The helpers return as soon as their result is ready and send the report afterwards; call this at shutdown (with a short deadline) so a final report is not lost with the process. Reports submitted while it waits may or may not be waited for; it returns once nothing is in flight. It returns ctx.Err() when reports are still in flight at the deadline.

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, opts ...ClientOption) (*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 IsAutomated added in v0.8.11

func IsAutomated(err error) bool

IsAutomated reports X's "this request looks like it might be automated" verdict (code 226). The serving account has been benched for a cooldown; callers should treat it as an account-scoped warning, not retry it.

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 37 / 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.

func WireFor added in v0.8.9

func WireFor(p stealth.TLSProfile) string

WireFor reports which request shape an account on TLS profile p sends: WireChrome for the current desktop Chrome profiles, WireLegacy (the frozen v0.8.6 shape) for every other profile. It is the rule the client itself applies, so a consumer labelling identities calls this instead of keeping its own copy of the profile set.

func WithRequestTrace added in v0.7.0

func WithRequestTrace(ctx context.Context, t *RequestTrace) context.Context

WithRequestTrace returns a context that makes client calls record their phase timeline into t.

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, ErrBadRequest matches errBadRequest, ErrNotFound matches errNotFound and ErrUnexpectedResponse matches errUnexpected, 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) BudgetPct added in v0.7.2

func (a *Account) BudgetPct() int

BudgetPct returns the percentage of each endpoint's window this account may spend: the SetBudgetPct value, or 100 when never set or set out of range.

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 — by an explicit block, an exhausted window, a spent local budget, or an account hold.

func (*Account) MarkEndpointRateLimited

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

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

func (*Account) PickPace added in v0.8.0

func (a *Account) PickPace() stealth.Jitter

PickPace returns the SetPickPace range; the zero Jitter means off.

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) SetActive

func (a *Account) SetActive(v bool)

SetActive implements pool.Identity.

func (*Account) SetBudgetPct added in v0.7.2

func (a *Account) SetBudgetPct(pct int)

SetBudgetPct sets the percentage of each endpoint's per-account X window this account may spend (1-100). Every out-of-range value — zero, negative, above 100, including the never-set zero value — means 100: the FULL window (today's behaviour). A share of zero cannot be expressed here on purpose; benching an account entirely is Client.HoldAccount's job. It is safe to call at runtime on a live pooled account: endpointGate re-derives each endpoint's gate under the account lock on every eligibility check — and never persists the budget band — so the next pick sees the new share, including the release of a budget-only hold (X's own exhaustion still stands until the window resets).

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) SetPickPace added in v0.8.0

func (a *Account) SetPickPace(j stealth.Jitter)

SetPickPace sets the pace of pool picks for this account: after each pick the pool skips the account for a random gap drawn uniformly from [Min, Max) (stealth.Jitter, the shared range type), so an account in warm-up is picked at most once per gap. A call that has no other account to go to waits (ctx-bounded) for exactly the end of the drawn gap.

The zero Jitter means off, which is the default. A degenerate range (Max <= Min, e.g. {3s, 3s}) is a FIXED gap of Min, unlike Jitter.Duration, which returns 0 for it. A negative Min counts as 0; Min and Max both <= 0 is off. It is safe to call at runtime on a live pooled account: the gap of the current interval is drawn again from the new range (so a lower range releases the account sooner, and the zero value releases it at once), and every later pick draws from it. Pool picks only: account-pinned writes and media uploads never consult it (like Client.HoldAccount).

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 (the prior cap stands). The raw cap is what is recorded; the limiter enforces its budget-scaled value.

func (*Account) Wire added in v0.8.9

func (a *Account) Wire() string

Wire reports the request shape the account sends, decided from the TLS profile stated on it. NewClient completes an account's identity; an account no Client has seen yet and that states no profile reports WireLegacy until then.

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 CallTrace added in v0.7.0

type CallTrace struct {
	Endpoint string
	Entry    time.Time // doPoolRequest entered
	// JitterDone is after the opt-in pre-request jitter (WithRequestJitter);
	// it equals Entry's instant when no jitter is configured.
	JitterDone time.Time
	// Picked is after the pool handed out an account (includes any pool wait).
	Picked time.Time
	// Paced is after the per-account human-pace wait (accountPacer).
	Paced time.Time
	// HeadersStart/Ready bracket x-client-transaction-id + x-xp-forwarded-for
	// generation; Ready is the instant before the transport is called.
	HeadersStart time.Time
	Ready        time.Time
	// Done is when the transport returned the full response body.
	Done     time.Time
	Attempts int
	Status   int
	Bytes    int
	Proxied  bool // the serving account has its own proxy
	// contains filtered or unexported fields
}

CallTrace is the timeline of one logical request (one doGET/doPOST, including its internal retries). Phases are wall-clock instants; a zero value means the phase was not reached. Entry and JitterDone are first-write; the rest describe the LAST HTTP attempt, so a retried request still has a coherent final-attempt timeline (Attempts says how many there were).

type Client

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

Client is the top-level Twitter scraping client.

func NewClient

func NewClient(cfg ClientConfig, clientOpts ...ClientOption) (*Client, error)

NewClient creates a fully-wired Twitter client.

Optional ClientOptions (WithRequestJitter, WithoutRequestJitter, WithAccountPace) adjust the config after it is passed in. Without WithRequestJitter pooled requests are sent without a pre-request delay.

Example

This example has no Output comment, so it is compiled but not run: it needs a real session.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	twitter "github.com/anatolykoptev/go-twitter"
)

func main() {
	client, err := twitter.NewClient(twitter.ClientConfig{
		Accounts: []*twitter.Account{{
			Username:  "your_handle",
			AuthToken: os.Getenv("X_AUTH_TOKEN"),
			CT0:       os.Getenv("X_CT0"),
		}},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	tweets, err := client.SearchTweets(context.Background(), "golang", twitter.SearchLatest, 20)
	if err != nil {
		log.Fatal(err)
	}
	for _, t := range tweets {
		fmt.Printf("@%s: %s\n", t.AuthorHandle, t.Text)
	}
}

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) CheckSession added in v0.8.11

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

CheckSession verifies acc's own session the way the web app does on load: it asks X for the signed-in viewer's settings with the account's own identity, cookie jar and proxy. A fixed public profile lookup proves only that a token is accepted; this proves THIS account's session is alive, and it is the request a real browser makes on load.

The result is classified by the same taxonomy as every other call: 32 IsAuthExpired, 64 IsSuspended, 326 IsLocked, 226 IsAutomated, 353 IsCSRF, a 429 IsRateLimited. CheckSession reports the verdict and leaves the pool alone — it never relogins and never benches — so the caller decides.

The call spends from the account's per-endpoint window like any other and is refused with IsRateLimited while that window is exhausted or held.

func (*Client) Close added in v0.6.13

func (c *Client) Close() error

Close closes the idle connections of every HTTP client this Client built (the shared one, each account's, and the open-account login client) and unregisters the 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 or background xtid / queryId refreshes, and it does not close the HTTP clients: the Client stays usable, a connection opened after Close idles out on its own, and the process-wide xtid manager, pacer and fetch client of the *WithSocial path are left alone.

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) FollowUser added in v0.7.0

func (c *Client) FollowUser(ctx context.Context, acc *Account, userID string) error

FollowUser follows userID from acc through the REST 1.1 friendships/create endpoint. It is a write: one attempt, pinned to acc, no retry and no relogin, so a failure surfaces to the caller instead of repeating.

func (*Client) FriendshipState added in v0.7.0

func (c *Client) FriendshipState(ctx context.Context, acc *Account, userID string) (following, notificationsEnabled bool, err error)

FriendshipState reports acc's relationship to userID — whether acc follows it and whether device notifications are on — read from one friendships/show.json call (target_id only; the source is the authenticating account). Same one-shot pinned-account pattern as FollowUser: no retry, no relogin. It is the per-target alternative to the GetFollowing list when that GraphQL op's rate window is contended by co-tenants.

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). Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

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). Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

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) GetDeviceFollowTweets added in v0.7.0

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

GetDeviceFollowTweets fetches the merged device-notifications timeline via the legacy REST endpoint.

func (*Client) GetDeviceNotifications added in v0.7.0

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

GetDeviceNotifications polls the merged device-notifications feed: REST device_follow.json, with GraphQL tweetNotificationsQuery as the fallback when REST is gone (HTTP 404/410 or another endpoint fault). An empty REST answer is a normal quiet poll and does NOT trigger the fallback — that would double the request volume in the steady state and hide failures. If the fallback fails too, both sources are broken and the joined error is returned; a cancelled or expired ctx is always returned as is.

func (*Client) GetFollowers

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

GetFollowers fetches followers for a user (paginated). Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

func (*Client) GetFollowing

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

GetFollowing fetches accounts a user follows (paginated). Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

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). Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

func (*Client) GetRetweeters

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

GetRetweeters fetches users who retweeted a tweet (paginated). Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

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) GetTweetNotifications added in v0.7.0

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

GetTweetNotifications fetches the same merged feed via GraphQL tweetNotificationsQuery — the fallback when device_follow.json is gone or empty. It carries its own endpoint key so a fault on one source cannot quarantine the account from the other.

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 + parse.UserList parser. Paging ends at maxCount or the end of the list. It also ends early, returning what was collected with a nil error, on a page with no items, a repeated or terminal ("0|...") cursor, or several consecutive pages that add nothing new; those cases log a warning when they cut the read short of maxCount. The result may exceed maxCount by up to one page.

func (*Client) HealthReport

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

HealthReport returns health stats for all accounts in the pool.

func (*Client) HoldAccount added in v0.7.2

func (c *Client) HoldAccount(username string, until time.Time) bool

HoldAccount benches a pooled account until the given time: the pool pick skips it for every endpoint while the account clock is before until, and it becomes eligible again afterwards. Returns false when no pool account matches username (the match is case-insensitive, like AccountByUsername). Re-holding is max-wins: a later call with an earlier until never shortens an existing hold — a deliberate early lift goes through ReleaseAccount. The hold gates only the pool pick: account-pinned write paths that take the account directly (doPOST-backed writes, FollowUser, UploadMedia) do not consult it. A hold is not a failure — it applies no health penalty, no failure counters and no relogin, so it deliberately bypasses the SetActive/reactivateAt cooldown machinery. When every account is held the caller gets the same error a fully-spent pool returns today: "pool exhausted for <endpoint> (requires auth): all N items are unavailable".

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) ReleaseAccount added in v0.7.2

func (c *Client) ReleaseAccount(username string) bool

ReleaseAccount lifts a HoldAccount hold early: the account becomes eligible for the next pool pick at once. Returns false when no pool account matches username. Releasing an account that was not held is a no-op.

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 parse.CreateTweet 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 parse.CreateTweet'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) SetDeviceNotifications added in v0.7.0

func (c *Client) SetDeviceNotifications(ctx context.Context, acc *Account, userID string, on bool) (bool, error)

SetDeviceNotifications turns acc's device notifications for userID on or off via friendships/update.json (the "bell" toggle) and returns the notifications_enabled flag it reads back from the response. It is a write: one attempt, pinned to acc, no retry and no relogin, so a failure surfaces to the caller instead of repeating. The readback is advisory only — X has answered notifications_enabled=false on a write that did land; verify the effect with FriendshipState when it matters.

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. It is also
	// the bench for a first "looks automated" (226) verdict.
	AuthCooldown time.Duration

	// BanCooldown is the soft-deactivation duration for banned/locked accounts.
	// A repeat 226 uses max(BanCooldown, 2*AuthCooldown), so a repeat is never
	// shorter than the first.
	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)

	// OnRequest, when set, is called once per HTTP attempt that was served by a
	// pooled account — on success AND on every error path — at the same
	// observation point that feeds the request-duration metric, after the
	// response is classified. Guest-token fallback requests carry no account
	// and emit no event; media-upload attempts emit under the "UploadMedia"
	// endpoint label. It is called synchronously on the request path and
	// without a recover, so it must not block (offload heavy work to a channel
	// or queue) and must not panic — a panic propagates to the request's
	// caller. nil = no-op, zero cost.
	OnRequest func(RequestEvent)

	// 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

	// ChromeWireForAccountlessClients opts the shared account-less client (the
	// guest-token fallback and warm fetches that carry no account) and the
	// open-account login client into the current Chrome request shape (header
	// order, fetch metadata, origin and priority as the X web client in Chrome
	// sends them). By default they send the request shape of v0.8.6 unchanged, so
	// a deployment that hosts only older accounts presents one consistent shape.
	// Accounts choose their shape by their own profile (chrome_146/150/152/156 get
	// the current one) and are not affected by this field. Default: false.
	//
	// The header shape is not the whole identity: these clients, and an account
	// whose caller-set user agent no built-in profile presents, take their TLS
	// profile and user agent from go-stealth's default, which is chrome_156 since
	// go-stealth v1.27.0 (chrome_152 before); only the header shape stays legacy.
	// An open-account login runs on the shape chosen here (legacy by default); the
	// account it mints then sends the shape of its own profile.
	ChromeWireForAccountlessClients 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

	// RequestJitter opts in to a random delay before every pool GET/POST (see
	// WithRequestJitter). nil (the default) and a zero Jitter send immediately.
	// The pinned write path (doPOST) and media upload keep stealth.DefaultJitter
	// regardless, and the per-account pacer is independent of this field.
	RequestJitter *stealth.Jitter
	// 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

	// XtidKeySource selects the page x-client-transaction-id keys are derived
	// from. The zero value keeps the account-cookie page; xtid.KeySourceLoggedOut
	// reads the cookie-less https://x.com/i/jf/ page like the reference
	// implementation. The GO_TWITTER_XTID_KEY_SOURCE=logged-out env var selects
	// the same for managers this client does not own. Default: account page.
	XtidKeySource xtid.KeySource
	// contains filtered or unexported fields
}

ClientConfig holds all configuration for the Twitter client.

type ClientOption added in v0.7.0

type ClientOption func(*ClientConfig)

ClientOption adjusts a ClientConfig before NewClient builds the client.

func WithAccountPace added in v0.7.0

func WithAccountPace(min, random time.Duration) ClientOption

WithAccountPace overrides the per-account human-pace spacing for this client only: consecutive requests of one account are spaced by [min, min+random). WithAccountPace(0, 0) disables the pacer. A zero min or zero random is treated as a 1ns floor (a zero field would otherwise be replaced by the 800ms / 1.2s default), so WithAccountPace(100ms, 0) spaces by exactly ~100ms. Removing the pace makes one account's request rhythm more regular; the per-endpoint rate limiter still applies.

func WithRequestJitter added in v0.7.0

func WithRequestJitter(min, max time.Duration) ClientOption

WithRequestJitter opts back in to a random delay before every pool-rotated GET/POST, drawn uniformly from [min, max). By default a client sends immediately: account rotation and quotas belong to the service that leases accounts, and the old 500ms-2.5s sleep dominated read latency (median ~1.5s of a ~2s call). The per-account pacer (WithAccountPace) is unaffected and still spaces consecutive requests of one account. The pinned write path (doPOST) and media upload keep their own human pacing regardless.

stealth.DefaultJitter reproduces the previous default:

twitter.WithRequestJitter(stealth.DefaultJitter.Min, stealth.DefaultJitter.Max)

func WithoutRequestJitter added in v0.7.0

func WithoutRequestJitter() ClientOption

WithoutRequestJitter removes the pre-request delay; equivalent to WithRequestJitter(0, 0). This is already the default and exists to state the intent explicitly (and to override an earlier WithRequestJitter).

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 = model.DMConversation

DMConversation is a single direct-message conversation from the 1.1 inbox.

type DMMessage added in v0.6.0

type DMMessage = model.DMMessage

DMMessage is a single direct message.

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 RequestEvent added in v0.7.2

type RequestEvent struct {
	Username  string
	Endpoint  string
	Status    int
	Outcome   string
	RateLimit RequestRateLimit
}

RequestEvent describes one HTTP attempt served by a pooled account, emitted to ClientConfig.OnRequest. Status is the HTTP status code (0 when the transport failed before a response). Outcome is the same bounded classification the client records to its metrics (e.g. "ok", "rate_limited", "transport") — see the twitter.outcome values in docs/metrics.md.

type RequestRateLimit added in v0.7.2

type RequestRateLimit struct {
	Limit     int
	Remaining int
	Reset     time.Time
}

RequestRateLimit mirrors one response's x-rate-limit-* headers inside a RequestEvent: Limit 0, Remaining -1 and a zero Reset each mean "header absent or unparseable".

type RequestTrace added in v0.7.0

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

RequestTrace is an opt-in collector of per-phase timestamps for the requests issued under a context (see WithRequestTrace). It exists for latency diagnosis: nothing is recorded, and the request path pays only a nil check, unless a caller attaches one. Read the calls only after the traced client method has returned.

func (*RequestTrace) Calls added in v0.7.0

func (t *RequestTrace) Calls() []CallTrace

Calls returns the traced requests in issue order.

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 SessionInfo added in v0.8.11

type SessionInfo struct {
	// ScreenName is the handle the session is signed in as, as X reports it.
	ScreenName string
}

SessionInfo is the identity a live session belongs to.

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 = model.Tweet

Tweet represents a single tweet.

func GetTweetByIDWithSocial added in v0.6.25

func GetTweetByIDWithSocial(ctx context.Context, sc *social.Client, tweetID string, opts ...ClientOption) (*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, opts ...ClientOption) ([]*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, opts ...ClientOption) ([]*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, opts ...ClientOption) ([]*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, opts ...ClientOption) ([]*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 = model.TwitterUser

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, opts ...ClientOption) ([]*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, opts ...ClientOption) ([]*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, opts ...ClientOption) ([]*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, opts ...ClientOption) (*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, opts ...ClientOption) ([]*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 internal/operations/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 internal/operations/queryids_gen.go — the "generated" layer of the endpoints override chain (env > generated > committed).
xtid-probe command
Command xtid-probe measures which x-client-transaction-id source X accepts.
Command xtid-probe measures which x-client-transaction-id source X accepts.
xweb-report command
Command xweb-report crawls x.com's PUBLIC logged-out web shell, parses the x-web (Vite + Relay) operation artifacts out of its chunk graph and writes a markdown comparison against every GraphQL operation go-twitter issues (docs/xweb-migration.md).
Command xweb-report crawls x.com's PUBLIC logged-out web shell, parses the x-web (Vite + Relay) operation artifacts out of its chunk graph and writes a markdown comparison against every GraphQL operation go-twitter issues (docs/xweb-migration.md).
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.
operations
Package operations holds the compiled GraphQL operation data: the committed queryIDs and per-operation feature flags, the cmd/gql-sync generated layer (queryids_gen.go, features_gen.go) and the TWITTER_QID_* variable names.
Package operations holds the compiled GraphQL operation data: the committed queryIDs and per-operation feature flags, the cmd/gql-sync generated layer (queryids_gen.go, features_gen.go) and the TWITTER_QID_* variable names.
parse
Package parse decodes X API response bodies into the model types.
Package parse decodes X API response bodies into the model types.
telemetry
Package telemetry holds the OpenTelemetry instruments, their names and label values for go-twitter.
Package telemetry holds the OpenTelemetry instruments, their names and label values for go-twitter.
xtidprobe
Package xtidprobe is the seam between cmd/xtid-probe and the packages whose unexported internals it measures.
Package xtidprobe is the seam between cmd/xtid-probe and the packages whose unexported internals it measures.
xwebnative
Package xwebnative is the single list of GraphQL operations go-twitter issues against x.com's x-web (Vite + Relay) stack, as opposed to the legacy responsive-web stack.
Package xwebnative is the single list of GraphQL operations go-twitter issues against x.com's x-web (Vite + Relay) stack, as opposed to the legacy responsive-web stack.
Package model holds the response types returned by the go-twitter client.
Package model holds the response types returned by the go-twitter client.
Package slogsample provides sampling utilities for slog handlers.
Package slogsample provides sampling utilities for slog handlers.
Package social provides a thin HTTP client for an external account management service.
Package social provides a thin HTTP client for an external account management service.

Jump to

Keyboard shortcuts

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