Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.0.0 - 2026-10-02
See UPGRADING.md for the changes that code written for 0.19 needs.
Added
- Split the gem into five gems released in lockstep:
x-core,x-uploader,x-streaming,x-objects, andxx-coreis the HTTP client and declaresX::Error, the base of every error the gems raisex-uploaderuploads media, profile images, and banners;x-streamingreads streams and filtered-stream rulesx-objectsholds the resource objects and makes its requests through the client it is givenxdepends on all four and mixes the object, upload, and streaming methods intoX::Client- Every public class is named directly under
X; the gems depend on each other with>= 1.0.0, < 2
- Add
X::Objects::Error,X::Uploader::Error, andX::Streaming::Errorto rescue the failures of one gem alone - Ship a
CHANGELOG.mdwithx-core,x-uploader,x-streaming, andx-objects, which each gemspec'schangelog_urinames - Publish the API documentation of every gem at https://sferik.github.io/x-ruby/api/, which the
documentation_uriofxnames- Each gem ships a
.yardopts, so its documentation on rubydoc.info leaves out the private API
- Each gem ships a
- Add
X.gem_versionand thegem_versionofX::Core,X::Uploader,X::Streaming, andX::Objects, each aGem::Version - Load
x-uploaderfromrequire "x", soX::Uploader::MediaUploadandX::Uploader::Accountneed no further require - Add
X::Client#get_stream, which sends a GET and yields theNet::HTTPResponsebefore its body is read- Sends the client's credentials, headers, timeouts, and proxy, and refreshes a rejected token as any request does
- Raises the
X::HTTPErrorof a failed response; otherwise returns what the block returns - A socket error while reading the body raises
X::NetworkError; an error of the block reaches the caller as raised x-streamingreads the streams of the API with it
- Add
X::Client#with_retries,memoized, andmemoize, kept throughout 1.x for the gems that extend a clientx-uploadersends each chunk of an upload withwith_retries;x-objectskeeps the authenticated user withmemoize
- Pass a block to
get,post,put, anddeleteto receive theX::Responseof each response the request gets- It is passed what
on_responseis passed, at the same points, refused responses and retries included - The client's
on_responseruns first
- It is passed what
- Pass query parameters to
get,post,put,delete, andstreamasparams:- nil values are dropped, Arrays are joined with commas, and a
Timeis sent in UTC as ISO 8601
- nil values are dropped, Arrays are joined with commas, and a
- Encode a
postorputbody that is not a String, such as a Hash or an Array, as JSON - Send a form body with the
form:ofpostandput, whose fields are encoded asparams:are- Passing both a body and
form:raisesArgumentError
- Passing both a body and
- Add the
headers:option ofX::Client.new, headers sent with every request and stream of the client- A Hash of String or Symbol names to String values; anything else raises
ArgumentErrorwhen the client is built - A header passed to a request or
get_streamreplaces the client's, which replaces the gem's default of that name - Names are case-insensitive, copies made with
withkeep them, and credential headers are dropped on a cross-origin redirect X::Client#headersreads them back frozen, each named by the String it is sent with, as{"user-agent" => ...}- Token requests carry them too: the app-only token fetch, OAuth 2.0 refreshes, and the code exchange of
X::OAuth2Authorization - The token request's own
AuthorizationandContent-Typewin, and a client'sAuthorizationheader is never sent with one
- A Hash of String or Symbol names to String values; anything else raises
- Pass an
X::Responseto theon_responseof a client after every request, and for each object a stream deliversX::Response#resource_counts,rate_limits, andrate_limitread what the response spent- An
on_responsethat is neither nil nor responds tocallraisesArgumentErrorwhen the client is built
- Add
X::Response#headersandX::HTTPError#headers, frozen Hashes with lowercase names and repeated fields comma-joined- Read each
Set-Cookiewithhttp_response.get_fields("set-cookie")
- Read each
- Retry a request refused for a rate limit up to
max_rate_limit_retriestimes (default 0)- It waits as long as the refusal asks, up to
max_rate_limit_waitseconds (default 900), plus up to 5 seconds of jitter - A refusal that asks no wait and names no reset waits a minute, doubling for each retry after
- A refusal for the usage cap of the project, which
X::Problem#usage_capped?tells, raises at once
- It waits as long as the refusal asks, up to
- Retry a failed request up to the
max_retries:ofX::Client.new(default 2; 0 raises at once)- A
GET,PUT, orDELETEis retried afterX::ServerError,X::RequestTimeout, or a network error before it was sent - The wait starts at up to a second and doubles, up to a minute, with jitter; a longer
Retry-Afteris waited out - A
Retry-Afterof more than a minute raises at once; aPOSTand any other 4xx are never retried - An error raised by
on_response, a request's block, orfrom_responseis never retried
- A
- Add the defaults of
X::Clientas constants:DEFAULT_MAX_REDIRECTS,DEFAULT_MAX_RATE_LIMIT_RETRIES,DEFAULT_MAX_RATE_LIMIT_WAIT,DEFAULT_MAX_RETRIESX::Client::DEFAULT_OPEN_TIMEOUT,DEFAULT_READ_TIMEOUT,DEFAULT_WRITE_TIMEOUT, andDEFAULT_KEEP_ALIVE_TIMEOUTreplace those ofX::ConnectionX::StreamingClient::DEFAULT_MAX_RECONNECTSsits beside itsDEFAULT_READ_TIMEOUT
- Keep up to 16 idle connections open per host between requests, for
keep_alive_timeoutseconds (default 30)keep_alive_timeoutis a setting ofX::ClientandX::OAuth2Authorization- Copies made with
withthat connect the same way share the client's connections X::Client#closecloses them, and those of the copies that share them; a later request opens one again
- Copy a client with some options changed with
X::Client#with, which takes the options ofX::Client.new- e.g.
client.with(base_url: "https://api.x.com/1.1/"), orclient.with(access_token: nil, access_token_secret: nil)for app-only - A copy given no new credentials shares the client's authenticator, and so its fetched or refreshed tokens
- A copy that shares an OAuth 2.0 authenticator raises
ArgumentErrorfor anexpires_at - A copy with other credentials holds
refresh_token,expires_at,scopes,save_tokens, andload_tokensonly when given them X::Client#expires_atreads the expiration of the last refresh
- e.g.
- Authenticate a client with an authenticator built elsewhere, given as the
authenticator:ofX::Client.neworwith- Raises
ArgumentErrorbeside credentials or anexpires_at, or for anything that is not anX::Authenticator - A custom authenticator subclasses
X::Authenticatorand overrides its publicheaders headersis passed a request answeringhttp_method,uri,body, and[], typed asX::_AuthenticatorRequest- The refreshes of a shared
X::OAuth2Authenticatorreach thesave_tokensof every client that uses it - A client given an
X::OAuth2Authenticatorholds no app credentials, so itsapp_onlyraisesX::UnsupportedOperation
- Raises
- Add
X::Client#app_only, a copy of a client that authenticates as the app- It sends the client's bearer token, or one it fetches once with the API key and secret, shared with copies
- It raises
X::UnsupportedOperationfor an OAuth 2.0 user client that holds no bearer token, API key, or secret
- Authenticate as the app given an
api_keyandapi_key_secretwithout access tokens, throughX::AppOnlyAuthenticator- It fetches a bearer token on the first request, and fetches another and resends once when the API answers 401
- Refresh an OAuth 2.0 access token when it expires, given the
expires_at:(aTime) ofX::Client, or when the API answers 401- The request is sent again with the new token; a token refreshed less than a minute ago is not refreshed again
- A 401 from another origin refreshes nothing
- Store the tokens of each OAuth 2.0 refresh with the
save_tokens:ofX::Client.new, a callable passed anX::OAuth2Tokens- It is called once the refresh releases its lock, in order, skipping a refresh another has replaced
- If a hook raises, the others still run, then
X::TokenReportFailedis raised, holding thetokensandclient - A
save_tokensthat does not respond tocallraisesArgumentErrorwhen the client is built - An authenticator built by hand reports its refreshes to no hook
- Add
X::OAuth2Tokens, the frozenaccess_token,refresh_token,expires_at, andscopesof a refresh or code exchangeas_json/to_jsonwrite it, andX::OAuth2Tokens.from_jsonreads it back, with String or Symbol keys- It marshals, and writes YAML, in a versioned format every 1.x release reads
X::OAuth2Tokens.newraisesArgumentErrorfor an empty or non-String token or anexpires_atthat is not aTime
- Share a user's tokens among processes with
load_tokens:, a callable that returns the storedX::OAuth2Tokensor nil- Taken by
X::Client.new,X::Client#with,X::OAuth2Authorization#client, andX::OAuth2Authenticator.new - A refresh reads the store first and takes newer tokens there rather than spend its own refresh token
- A refresh refused with
invalid_requestorinvalid_grantreads the store again before raising - One that does not respond to
callraisesArgumentError; one that returns anything else raisesTypeError
- Taken by
- Read the scopes X granted an OAuth 2.0 token with
scopesonX::OAuth2Tokens,X::OAuth2Authenticator, andX::Client- A frozen Array of Strings, or nil when not known
X::Client.new,with, andX::OAuth2Authenticator.newtakescopes:, soX::Client.new(client_id:, **tokens.to_h)works- Scopes that are not an Array of scope names, or given beside other than OAuth 2.0 credentials, raise
ArgumentError
- Authorize an app with the OAuth 2.0 authorization code flow and PKCE with
X::OAuth2Authorization- It builds the authorization URL, and exchanges the code of the redirect for
tokensor aclient - The tokens hold no app credentials; build a client as
X::Client.new(client_id:, client_secret:, **tokens.to_h) - It raises
X::AuthorizationDeniedwhen the user declines, the state does not match, or the redirect is invalid - It raises
X::AuthorizationErrorwhen X refuses the code, andArgumentErrorfor an empty state, client ID, or redirect URI clientchecks its options before spending the code, and passes the first tokens to itssave_tokens- It takes
headers:, which the code is exchanged with and the client it builds is given, as a gateway may require
- It builds the authorization URL, and exchanges the code of the redirect for
- Refresh the tokens of a public OAuth 2.0 client, given a
client_id,access_token, andrefresh_tokenwithout aclient_secret - Authenticate with an OAuth 2.0 access token that is not refreshed, given a
client_idandaccess_tokenwithout arefresh_token- An access token the API rejects raises
X::Unauthorized, andrefresh!raisesX::UnsupportedOperation
- An access token the API rejects raises
- Add
X::Authenticator#user_id, the user an OAuth 1.0a access token acts for, or nil - Add
inspecttoX::Clientand the authenticators that never reveals credentials - Add
X::HTTPError#status,body,problems, andproblem, andhttp_responsefor theNet::HTTPResponsestatusis an Integer, asX::Response#statusisproblemis the problem the body describes, or else the first error it names
- Add
X::Problem, what the API said of a request, answered byX::HTTPError#problemand reported byx-objects- Reads
title,detail,type,resource_type,resource_id,parameter,value, andmessage about?tells whether it names a resource, or an Integer or String identifier, comparing them as Strings- Equals a problem of the same attributes, writes JSON with
as_json/to_json, and marshals in a versioned format
- Reads
- Add
X::UnsupportedMarshalFormat, anX::Errorraised when reading back a marshal format a release does not read - Raise
X::PaymentRequired(402),X::MethodNotAllowed(405),X::RequestTimeout(408),X::UnsupportedMediaType(415), andX::UnavailableForLegalReasons(451)- Each is an
X::ClientError; X sends 402 when the account that pays for the app has no credit left
- Each is an
- Name the request an error was raised for:
X::HTTPError,X::NetworkError,X::InvalidResponse, andX::TooManyRedirectsreadhttp_methodanduri- The message names it without its query, as
GET /2/users/1: Could not find user - A redirected request is named by the last request sent
- The message names it without its query, as
- Add
X::InvalidResponse#body,status, andheadersfor a successful response that is not JSONbodyfalls back to the body of the response once it has been read whole, and never reads a stream
- Add
X::TooManyRequests#exhausted_rate_limitsand#limiting_rate_limit - Add
X::UnsupportedOperation, anX::Errordeclared byx-core, raised for anything the API offers no way to do- e.g. hydrating or refreshing an
X::Pollor anX::Placethat is not hydrated, as in "X::Poll cannot be fetched by id" - A finder the API lacks is not defined:
X::PollandX::Placeanswer none,X::List,X::Community, andX::DirectMessageonlyfindandfind!
- e.g. hydrating or refreshing an
- Add the upload methods to
X::Client:upload_media,chunked_upload_media,await_media_processing, andawait_media_processing!- And
add_alt_text,add_subtitles,update_profile_image, andupdate_profile_banner, each calling an uploader with the client - As in
client.create_post("Look", media_ids: [client.upload_media("cat.jpg")]) chunked_upload_mediareturns theX::UploadedMediaonce it is finalized, without waiting for processingawait_media_processing!raises if processing failed, whereawait_media_processingreturns the status- Media that already says its processing ended, or an image's upload response, is returned without a request
- They come from
X::Uploader::API, which code usingx-coreandx-uploaderalone can include; aclient:option raisesArgumentError
- And
- Return an
X::UploadedMediain place of a Hash from the uploaders and their client methods- From
upload,chunked_upload,await_processing,await_processing!,add_alt_text, andadd_subtitles - It reads
idandmedia_id(Integers),media_key,bytesize,expires_after_secs,processing_info,state, andcheck_after_secs - It tells
processing?,failed?, andready?, and still reads as a Hash with[],fetch,dig,key?, andto_h - It is frozen, writes
as_json/to_json, and marshals in a versioned format; one without an"id"of 1 to 19 digits raisesArgumentError add_alt_textandadd_subtitlesreturn the media they describe, ready to attach to a post
- From
- Infer the media category of an upload from the bytes the media begins with, or else its extension, so
uploadtakes any file- Videos and large GIFs upload in chunks, and
uploadwaits for X to process them uploadtakesmedia_type:,chunk_size:, andconcurrency:, and raisesArgumentErrorfor any other unknown keyword
- Videos and large GIFs upload in chunks, and
- Take the media as the positional argument of the
X::Uploader::MediaUploadandX::Uploader::Accountmethods, withclient:as a keyword - Describe uploaded media with the
alt_text:ofX::Uploader::MediaUpload.uploador withX::Uploader::Metadata.add_alt_text- Alt text that is not a String, not UTF-8, empty, or over 1,000 characters raises
ArgumentErrorbefore a request - It is sent again after a server or network error
- An upload that cannot add its alt text raises
X::AltTextFailed, which holds the uploadedmedia, whatever the error
- Alt text that is not a String, not UTF-8, empty, or over 1,000 characters raises
- Attach uploaded subtitles to a video with
X::Uploader::Metadata.add_subtitlesmedia_category:defaults to"tweet_video", and takes"amplify_video"in any case, orTweetVideo/AmplifyVideo- Any other category raises
ArgumentError - It is sent again after a server or network error
- Share uploaded media, or give it other owners, with the
shared:andadditional_owners:ofupload,chunked_upload, andupload_media- Media given
shared: trueuploads in chunks; an image of unknown type, such as HEIC, is sent as JPEG - A
sharedother than true, false, or nil, oradditional_ownersthat are not user IDs, raiseArgumentError
- Media given
- Add
X::Uploader::MediaUpload::DEFAULT_CONCURRENCY(4) andMAX_CONCURRENCY(16), the chunks a chunked upload sends at once - Add
X::Uploader::MediaUpload::DEFAULT_PROCESSING_TIMEOUT(600 seconds) andAMPLIFY_VIDEO, besideTWEET_VIDEO - Name the media category of an upload with a Symbol, in any case, as in
media_category: :tweet_video - Rescue the errors
x-uploaderraises of its own withX::Uploader::Error- It is the base of
X::AltTextFailed,X::ChunkedUploadFailed,X::InvalidMedia, andX::MissingMediaData - And of
X::MediaProcessingCheckFailed,X::MediaProcessingFailed, andX::MediaProcessingTimeout X::InvalidMediaType, anX::InvalidMedia, is raised for media of a type the API does not take- A bad category, chunk size, concurrency, alt text, or processing timeout raises
ArgumentErrorinstead - Each error that holds media takes an optional message and
media:;X::MediaProcessingTimeouttakestimeout:too
- It is the base of
- Raise
X::MediaProcessingCheckFailed, which holds the uploadedmedia, when a check of its processing fails - Raise
X::ChunkedUploadFailed, which holds the initializedmedia, when a chunk or the finalize fails- Such as a server error, a file deleted, closed, or shrunk mid-upload, or a finalize response without media
- It and
X::MediaProcessingCheckFailedare raised for anyStandardError, as thecause, such as oneon_responseraises - A
Timeout::Error, anX::MediaProcessingTimeout, or an interrupt is raised as it is
- Take an upload's result, a media ID, a media key, or anything that answers
media_keyinawait_processing,await_processing!,add_alt_text, andadd_subtitles- Anything else, or an ID that is not 1 to 19 digits, raises
ArgumentErrorbefore a request - The signatures type what answers
media_keyasX::Uploader::_MediaKeyed
- Anything else, or an ID that is not 1 to 19 digits, raises
- Upload a GIF with a single frame as an image, since X fails to process it as a GIF
- Stream with
X::StreamingClient, whichX::Client#streamingbuilds with the client's credentials, base URL, classes, andon_responsestreamingtakesread_timeout:,max_reconnects:, andon_reconnect:; other settings come from a copy, asclient.with(open_timeout: 2).streamingstreamraisesArgumentErrorwhen given no block, before it opens the stream
- Stop the streams of a streaming client from any thread, or the trap of a signal, with
X::StreamingClient#stop- Each stream stops the next time it waits on the API, at once if idle or waiting to reconnect
- A stopped streaming client stays stopped: a stream asked of it later returns nil at once, without a request
- Each stopped
streamcall returns nil,stopreturns nil, andstopped?tells a stopped streaming client - A block,
on_response, oron_reconnectrunning at that moment runs to its end first streamingbuilds a new streaming client each time, so keep the one you stop in a variable
- Stop a stream from its block:
breakstops it and returns its value, andthrowunwinds past it- An error of the block,
on_response, or the object class stops it and reaches the caller,StopIterationincluded
- An error of the block,
- Read and change the rules of the filtered stream with
rules,add_rules, anddelete_rulesonX::StreamingClient- A rule is a frozen
X::StreamRuleof itsvalue,tag, andid(an Integer, or nil for a rule to add) - An
idis an Integer that is not negative or a String of digits alone; anything else raisesArgumentError - Add an
X::StreamRule, a Hash ofvalueandtag, or a String; delete by rule,id, or value - An
X::MatchingRuledeletes the rule it names, sodelete_rules(post.matching_rules)deletes the rules a post matched - Anything else, such as a post, raises
ArgumentErrorbefore a request dry_run: truechecks the rules and changes none;rulestakesparams:and reads every page- Rules the API does not add or delete are yielded as
X::Problems, or without a block raiseX::RulesRejected X::RulesRejected#addedand#deleted_counthold what was done; no rules send no request
- A rule is a frozen
- Stream, and read and change stream rules, as the app from a client that signs with OAuth 1.0a, which the stream endpoints refuse
- A client of an OAuth 2.0 user without app credentials streams as the user, so X's 403 raises
X::Forbidden
- A client of an OAuth 2.0 user without app credentials streams as the user, so X's 403 raises
- Reconnect a stream that ends, drops, or sends a line that is not JSON, backing off as X recommends
- Up to the
max_reconnectsof the streaming client in a row (unlimited by default), reset by each object or keep-alive - Then it raises the last error:
X::NetworkErrorfor a stream that ended or dropped,X::InvalidResponsefor a non-JSON line - A rate limit backs off from a minute, up to 320 seconds, but raises
X::TooManyRequestspastmax_rate_limit_waitor for the usage cap - A server error, 408, or 409 backs off from 5 seconds, up to 320 seconds, or waits longer for its
Retry-After - A
Retry-Afterpastmax_rate_limit_waitraises the error at once - What arrived of a line the stream ended within is dropped, unparsed and unreported, and the stream reconnects as one that ended
- An error
on_responseraises for a failed response stops the stream and reaches the caller, as one for an object does - A certificate that does not verify raises its
X::NetworkErrorat once, since it would not verify on the next attempt
- Up to the
- Report each reconnect of a stream to
on_reconnect:, a callable passed the error and the seconds it waits- It is passed an error every time, never nil, and is called with those two arguments and no others throughout 1.x
- It can call
stopto give up, as on a host that never resolves, and the stream returns nil - An error it raises stops the stream and reaches the caller
- Read a stream with the
read_timeoutof the streaming client, 30 seconds by default, so a quiet stream reconnects - Raise
X::StreamError, anX::Streaming::Error, for a stream line that holds errors and no data- It holds the
problems,http_method, anduri;on_responseis passed the line first - A line of
operational-disconnects alone, whichX::Problem#disconnect?tells, reconnects; other problems stop the stream
- It holds the
- Add immutable, thread-safe resource classes, which descend from
X::ResourceX::User,X::Post(aliasedX::Tweet),X::List,X::DirectMessage,X::Space,X::Media,X::Poll,X::Place, andX::Community- Lists of resources or identifiers are frozen Arrays; copy one to change it
- A list the response omits, such as
post.urls, reads as an empty Array;user.connection_statusreads nil unless requested - Text reads as the API sends it, so
post.textanddirect_message.texthold&,<, and>throughout 1.x - Nested data, such as
post.entitiesandpost.public_metrics, reads as frozen Hashes keyed by String throughout 1.x - Nested data the API sends as anything but an object, or a list of them, raises
X::InvalidAttribute
- Rescue the failures of the object layer with
X::Objects::Error- It is the base of
X::MissingResource,X::UnreadableResponse,X::InvalidAttribute,X::MissingClient, andX::PageLimitReached
- It is the base of
- Include the object methods in a class of your own with
X::Objects::API- The class answers
get,post,put, anddeleteasX::Objects::_Clienttypes them, taking keywords it does not read - The other modules and constants of
X::Objectsare private
- The class answers
- Compare resources by class and ID with
==,eql?, andhash, so the same resource from different requests is equal - Resolve references such as
post.authorandpost.replied_toto included objects or ID stubs, one object per resource in a response - Add
hydrate, which fetches and memoizes the full resource, andrefresh, which fetches it again- A resource fetched with fewer than the default fields or expansions is not hydrated; one fetched with more is
- A resource without a client, such as one read back with
Marshal, raisesX::MissingClientfrom any request FIELDSandEXPANSIONSmay grow in a minor release; add todefault_paramsrather than list every value
- Hydrate the stubs of a page of
stubs, or of a cursor that requests identifiers alone, together, in batch lookups of up to 100- Lists, communities, and direct messages hydrate one at a time, as does a reference a page did not include;
hydrate_allbatches any
- Lists, communities, and direct messages hydrate one at a time, as does a reference a page did not include;
- Refer to a resource without a request with
X::User.from_idand its equivalents, and tell such stubs apart withstub? - Add
X::Cursor, anEnumerablecollection that fetches pages lazily at the maximum page size and caches themrefreshandprefetchfetch pages again or ahead- A page a prefetch failed to fetch raises the prefetch's error once it is reached, rather than be requested again
- A page that names as its next a token already read raises
X::UnreadableResponse pagetakes an Integer index, raisingTypeErrorfor any other andArgumentErrorfor a negative oneX::Cursor.newis private; cursors come from the collections, searches, and lookups
- Answer
X::Cursor#first,take,any?,none?,one?, andempty?from as few resources as they need- They read from pages already fetched, and request a page no larger than needed, raised to the endpoint's minimum
- So
any?,none?, andempty?request one resource andone?two, or the 5 or 10 an endpoint such as a search takes at least - A count that is not an Integer is converted as
Array#firstdoes; a String, or nil totake, raisesTypeError
- Count a collection without paging it with
X::Cursor#published_count, the number the API publishes- Followers, followed users, and list memberships of a user, and members and followers of a list; nil for others
- Read a collection a page at a time with
X::Cursor#each_page, yieldingX::Pages- A page holds
items,meta,result_count,next_token,previous_token, andproblems, and reads like an Array X::Page.newraisesArgumentErrorfor problems that are notX::Problems
- A page holds
- Request identifiers alone from a cursor with
ids, as inuser.followers.ids, and scan stubs withstubs - Type-check the resources of a collection:
X::CursorandX::Pageare generic in their signatures - Read the collections of a resource as cursors
- A user's
followers,following,affiliates,posts,mentions,liked_posts,owned_lists,list_memberships, andfollowed_lists - A list's
members,followers, andposts, a post'squotes, and a space'spostsandbuyers(OAuth 2.0 user only)
- A user's
- Add
home_timeline,blocking, andmutingcursors toX::User - Read the authenticated user's reposted posts with
reposts_of_meon the client andX::Post, aliased asretweets_of_me - Tell a post that replies, quotes, or reposts with
reply?,quote?, andrepost?, and read its target withreplied_to,quoted, andreposted - Add
X::Post#liked_by,reposted_by, andreposts, andreferenceson posts and direct messages - Look up users and posts by ID in parallel batches of 100 with
X::User.find_allandX::Post.find_all- They return one resource per ID found, in the order asked, an ID given twice coming back twice
- Once a batch fails, no further batch is sent and its error is raised
- Say how many batches a lookup requests at once with
concurrency:(default 4)- Taken by
find_all,find_all_by_username,hydrate_all, andX::Space.find_all_by_creator - And by
find_all_users,find_all_users_by_username,find_all_posts,find_all_spaces,find_all_media, andfind_all_spaces_by_creator - Anything but an Integer of at least 1 raises
ArgumentError
- Taken by
- Hydrate many resources in parallel batches with
hydrate_allonX::User,X::Post,X::Space, andX::Media- It drops nil and resources not found, skips hydrated ones, and stores what it found in each resource
- A resource of another class raises
ArgumentErrorbefore a request
- Add lookup, search, and action methods for the resources to
X::Clientfind_user,find_all_users,current_user!,find_post,find_all_posts,find_list, andfind_spacesearch_posts,search_all_posts,create_post,delete_post,direct_messages, andcreate_direct_messagefollow,unfollow,like,unlike,repost, andunrepost- The finders have
find_tweetaliases - Actions return true or false;
followreturns true once it has asked to follow a protected user
- Look up a mix of IDs and usernames with
find_all_users, and usernames alone, even all-digit ones, withfind_all_users_by_username - Look up a user by ID when given an Integer and by username when given a String
- Say which with
X::User.find_by_username,find_by_username!,find_all_by_username,find_by_id,find_by_id!, andfind_all_by_id - Or on the client with
find_user_by_username,find_user_by_username!,find_all_users_by_username,find_user_by_id,find_user_by_id!, andfind_all_users_by_id - Elsewhere, such as
followorfind_post, an ID that is not a number raisesArgumentError - So does a resource of another class, as
client.like(user), or any other object that answersid
- Say which with
- Accept a username with a leading
@infind_user,find_all_users, andX::User.find_all_by_username - Search users with
X::User.searchandclient.search_users - Request the largest page each search allows: 500 posts from
search_all_posts(100 with context annotations), and 1,000 users fromsearch_users - Look up direct messages with
find_direct_message, many spaces withfind_all_spaces, and a conversation withdirect_messages_with - Look up the spaces of many creators with
X::Space.find_all_by_creatorandfind_all_spaces_by_creator, in batches of 100 - Search spaces with
X::Space.searchandsearch_spaceson the client, a cursor over live or scheduled spaces - Look up and search spaces, and read their posts, whatever the client authenticates with
- An OAuth 1.0a client, which the space endpoints refuse, requests them as the app; an OAuth 2.0 user client as the user
- Read the topics of a space with
X::Space#topics, each anX::Topicwith anameanddescription - Look up media by media key with
X::Media.find,find!, andfind_all, andfind_media,find_media!, andfind_all_media- Each takes a media key, media, or an upload's result; a numeric media ID raises
ArgumentError
- Each takes a media key, media, or an upload's result; a numeric media ID raises
- Read the numeric ID of media with
X::Media#media_id, asX::UploadedMedia#media_idreads it;X::Media#idis the media key - Add
X::Community, withfind_community,find_community!,search_communities,post.community, and thecommunity:ofcreate_post - Alias every client method named for direct messages with
dm:find_dm,find_dm!,dms,dms_with,dms_in,create_dm,create_group_dm,create_dm_in, anddelete_dm - Raise
X::MissingResource, anX::Objects::Error, fromcurrent_user!,X::User.current!,find!, and the bang finders of the clientfind_user!,find_user_by_username!,find_post!,find_list!,find_space!,find_media!,find_community!, andfind_direct_message!- The message names what was looked up, as "Could not find X::User @sferik";
problemsexplains why - It is not
X::NotFound, which is the 404 of an endpoint that is not there
- Add
current_userandcurrent_user!to the client, asX::User.currentand.current!;current_userreturns nil if not found - Take the authenticated user's ID from an OAuth 1.0a access token with
current_user_id, without a request - Name the interface after posts:
post_count,pinned_post_id,most_recent_post_id,edit_history_post_ids,note_post,referenced_posts, andrepost_count- The tweet-named methods, such as
create_tweet,tweets, andretweet_count, remain as aliases - A response that names fields after tweets, as a stream does, is still read
- The tweet-named methods, such as
- Request fields and expansions by the names the X API documentation gives, such as
post.fieldsandreferenced_posts - Request no
edit_history_post_idsorentities.mentions.usernameexpansion, which the object layer never reads - Read the IDs of users, posts, lists, direct messages, communities, and polls, and the attributes that refer to them, as Integers
- The IDs of spaces, places, and media, media keys,
dm_conversation_id, and usernames are Strings X::Problem#resource_idand#valueare Strings, so match a problem to a resource withproblem.about?(user)
- The IDs of spaces, places, and media, media keys,
- Raise
X::MissingResource, holding the response's problems, when a request that creates a resource is answered without it- From
X::Post.create,X::List.create,X::DirectMessage.create,create_group,create_in, and their client methods - So
create_post,create_list,create_dm, and the rest never return nil - A response whose data names no identifier raises it too
- From
- Build the
reply,media, and quote of a new post with thereply_to:,media_ids:, andquote:ofcreate_postandX::Post.create- A field passed with a String key, such as
"reply"or"attachments", is read as its Symbol, so it is sent once
- A field passed with a String key, such as
- Take an upload's result, media, or a media key in the
media_ids:ofcreate_post, reading its media ID- An ID that is not 1 to 19 digits, or anything else, raises
ArgumentErrorbefore the request
- An ID that is not 1 to 19 digits, or anything else, raises
- Post media without text: the text of
create_post,create_direct_message, and their equivalents is optional- A post or message with neither text nor any other field raises
ArgumentError
- A post or message with neither text nor any other field raises
- Attach uploaded media to a direct message with
media_ids:- Passing both
media_ids:andattachments:raisesArgumentError; an emptymedia_ids:attaches nothing
- Passing both
- Start a group conversation with
X::DirectMessage.create_groupandcreate_group_direct_message, aliasedcreate_group_dm - Send to and read any conversation with
X::DirectMessage.create_inand.in, andcreate_direct_message_inanddirect_messages_in- The client methods have
create_dm_inanddms_inaliases
- The client methods have
- Add
X::DirectMessage.delete,message.delete, anddelete_direct_messageon the client - Add
X::DirectMessage#peer(user), the other participant of a one-to-one conversation, nil for a group conversation - Add
X::DirectMessage#from?, false for a message that does not name its sender - Add
X::Post#coordinates, andpermalinkanduri, the x.com address of a post, user, list, or community - Add
X::Post#urlsandX::Post#expanded_text, the text with each shortened link expanded, every link in one passexpanded_textHTML-escapes each URL it puts in, so the whole text reads escaped, astextdoes
- Read the full text of a post longer than 280 characters with
X::Post#text, from itsnote_postentitiesandurlsof a long post read from the note alone
- Add
X::Post#matching_rules, the filtered-stream rules a post matched, each a frozenX::MatchingRulewith anidandtag- An
idthat is not a String of digits or a non-negative Integer raisesArgumentError
- An
- Add
X::User#profile_banner_url,parody?,identity_verified?,subscription_type,verified_followers_count,subscriber_count, andmedia_count - Add
X::User#receives_your_dm?,subscribes_to_you?, andsubscription, nil unlessuser.fieldsnames them - Add
X::Post#media_source_posts, aliasedmedia_source_tweets, the posts its attached media was first posted with- Resolved from the
attachments.media_source_tweetexpansion, which lookups request
- Resolved from the
- Add
X::Post#display_text_range, an exclusiveRange, nil when absent, andscopes,card_uri,article,article_title,media_metadata, andpaid_partnership?, andX::DirectMessage#entities - Add
X::User#affiliation,affiliated_with_ids, andaffiliated_with; a user included in another resource holds none until hydrated - Read
is_identity_verifiedandis_ticketedasX::User#identity_verifiedandX::Space#ticketed, with?predicates - Match resources against
case/inpatterns withdeconstruct_keys, as inpost in {like_count: 100..}- Tweet-named aliases match too, as in
user in {pinned_tweet_id: Integer} X::Trend,X::PersonalizedTrend, andX::PostUsagematch by their readers
- Tweet-named aliases match too, as in
- Write a resource, problem, trend, usage, matching rule, or page as JSON with
as_jsonandto_json, never with credentials- Each marshals, and writes YAML, in a versioned format every 1.x release reads, coming back frozen
- A resource keeps the included objects it refers to, but not its client
- Read each but a page as a Hash with
to_h - A cursor raises
X::UnsupportedOperationfromas_json,to_json, andto_h, andTypeErrorfromMarshal.dump; serializeto_a
- Report the partial errors of a successful response as
X::Problemsproblemson a resource holds those about it, the resources it refers to directly, or no identifierproblemson a page holds every problem of its response; a finder's block receives each one
- Count posts matching a query with
X::Post.count,count_all,count_by_period, andcount_all_by_period- On the client as
count_posts,count_all_posts,count_posts_by_period, andcount_all_posts_by_period, with tweet-named aliases - Counts by period are keyed by the
RangeofTimeeach period spans - A count that is not a String of digits or a non-negative Integer raises
X::InvalidAttribute - An OAuth 1.0a client counts as the app; an OAuth 2.0 user client without app credentials counts as the user
- On the client as
- Report the post usage of the app's project with
X::PostUsage.currentandclient.post_usage- They return nil, yielding the response's problems to a block, when it holds no usage
X::PostUsage.current!andclient.post_usage!raiseX::MissingResourceinstead- Includes its monthly cap, reset day, and usage by day and by app
- Read the trends of a place with
X::Trend.atandtrendson the client, given its WOEID, such as 1 for the world- An ID that is not a number raises
ArgumentError;max_trendslimits the 50 returned - An
X::Trendreads itsnameandpost_count; an OAuth 1.0a client requests trends as the app
- An ID that is not a number raises
- Read the trends X picks for the authenticated user with
X::PersonalizedTrend.allandpersonalized_trends- An
X::PersonalizedTrendreads itsname,category,post_count_text, andtrending_since_text
- An
- Bookmark a post and remove the bookmark with
bookmarkandunbookmarkonX::UserandX::Client - Read bookmark folders with
X::User#bookmark_folders, a cursor ofX::BookmarkFolder, and a folder's posts withbookmarks(folder:) - Add
block,unblock,mute, andunmutetoX::UserandX::Client - Add
X::List.create,.update,.delete,list.add_member,remove_member,update, anddelete, with client equivalentscreate_list,update_list,delete_list,add_list_member, andremove_list_member- An update with no field to change raises
ArgumentErrorbefore a request
- Follow, unfollow, pin, and unpin a list with
follow_list,unfollow_list,pin_list, andunpin_list, and readX::User#pinned_lists - Hide and show a reply with
X::Post#hide_replyand#unhide_reply, their class methods, and the client'shide_replyandunhide_reply - Check whether a user follows another with
user.follows?, and a list's membership withlist.member?, without fetching every pagefollows?looks upconnection_statusonce when either user is the authenticated user, and scans otherwisemember?scans the smaller of a public list's members and the user's list memberships
- Limit the pages a scan or a count of posts reads with
max_pages:(default nil, no limit)- Taken by
X::List#member?,X::User#follows?,X::Post.count,count_all,count_by_period,count_all_by_period, and their client methods - Past the limit, with another page named, it raises
X::PageLimitReached, anX::Objects::Error - A value that is neither an Integer of at least 1 nor nil raises
ArgumentErrorbefore a request
- Taken by
- Raise
X::InvalidAttribute, anX::Objects::Error, for a response value that cannot be read as the API documents it- Such as a timestamp that is not ISO 8601, a count that is not a whole number, or a flag that is not a boolean
- Its cause is the
ArgumentErrorthat refused the value
- Validate the identifier of a resource when it is built, so
X::User.new({"id" => "abc"})raisesArgumentError- A space or place ID is word characters alone; a
dm_conversation_idthat is not digits, or two numbers joined by a hyphen, raisesX::InvalidAttribute
- A space or place ID is word characters alone; a
- Validate a username before building a path from it, so
find_user("../tweets/20")raisesArgumentErrorwithout a request
Changed
- Require Ruby 3.4 or later
- Hold
VERSIONin a String rather than aGem::Version; usegem_versionto compare versions - Send requests to
api.x.comrather thanapi.twitter.comby default - Name the gem in the
User-Agentheader of every request, token requests included, asx-ruby/1.0.0 ruby/3.4.0 (arm64-darwin24), where it namedX-Client - Move the HTTP client into
x-coreand the uploaders intox-uploader - Rename
X::MediaUploadertoX::Uploader::MediaUpload,X::AccountUploadertoX::Uploader::Account, andX::MediaUploadValidatortoX::Uploader::Validator - Rename
X::OAuthAuthenticatortoX::OAuth1Authenticator - Rename
X::ConnectionException, the error for 409 Conflict, toX::Conflict - Rename
X::HTTPError#responsetohttp_response, and make that ofX::RateLimitprivate - Move
streamfromX::ClienttoX::StreamingClient - Refresh OAuth 2.0 tokens with
X::OAuth2Authenticator#refresh!, in place ofrefresh_token!- It returns the frozen
X::OAuth2Tokensof the refresh rather than the Hash of the token response - It raises
X::UnsupportedOperationfor an authenticator that holds no refresh token
- It returns the frozen
- Keep secret credentials private on a client and its authenticators
api_key_secret,access_token,access_token_secret,bearer_token,client_secret, andrefresh_tokenno longer read off a client- Nor do the secrets and tokens of the authenticators;
api_key,client_id, andexpires_atremain public - An authenticator's
headersstill returns theAuthorizationheader it sends, which holds a bearer or OAuth 2.0 token - Store refreshed tokens from the
X::OAuth2Tokensthatsave_tokensis passed
- Raise
TypeErrorfromMarshal.dump,YAML.dump,as_json, andto_jsonof a client, streaming client, authenticator, orX::OAuth2Authorization - Keep the proxy of a client, streaming client, and connection private:
proxy_urland theproxy_*readers are goneinspectsummarizes the proxy URL without its user and password
- Send credentials only to the origin of the
base_url- An endpoint or stream naming another scheme, host, or port gets no
Authorization,Cookie, orProxy-Authorizationheader - Another API version on the same origin, such as
https://api.x.com/1.1/account/settings.json, still gets them
- An endpoint or stream naming another scheme, host, or port gets no
- Send a header named by a Symbol with its underscores as hyphens, wherever headers are taken
headers: {content_type: "text/plain"}sendscontent-type, replacing the defaultContent-Type- It is dropped on a cross-origin redirect like the header it names
- Raise
ArgumentErrorfromX::Client.newfor abase_urlthat is not an absolute http or https URL- Or that holds a user, password, query, or fragment
- Raise
ArgumentErrorfromX::Client.newfor credentials that do not form a complete set- Or for a credential that belongs to no complete set, such as a
client_idbeside abearer_token - Or for OAuth 2.0 credentials beside a complete set of OAuth 1.0a credentials
- Or for an empty String credential, or one that is neither a String nor nil
- Or for an
expires_atbeside credentials it does not belong to
- Or for a credential that belongs to no complete set, such as a
- Raise
ArgumentErrorfrom the authenticators for a required credential that is nil or empty, or anexpires_atthat is not aTime - Raise
ArgumentErrorfromX::Client.new,with, andstreamingfor settings out of range, naming the settingmax_redirects,max_rate_limit_retries, andmax_retriesmust be Integers of at least 0max_rate_limit_waitmust be a number of at least 0, not NaN;max_reconnectsan Integer of at least 0 orFloat::INFINITYopen_timeout,read_timeout, andwrite_timeoutmust be finite numbers of at least 0, or nil;keep_alive_timeouta finite number- A stream's
read_timeoutmust be at least 25 seconds, or nil default_array_classmust be a Class, anddefault_object_classa Class or respond tofrom_response
- Raise
ArgumentErrorfromget,post,put,delete,get_stream, andstreamfor an endpoint that is not a valid URL- Such as one that holds a space or bad
%escape, or does not resolve to an http or https URL with a host - An endpoint that is not a String, such as a Symbol or a URI, raises
ArgumentErrornaming its class
- Such as one that holds a space or bad
- Raise
ArgumentErrorfrompostandputfor an unknown keyword, asclient.post("tweets", text: "Hello") - Resolve an endpoint that begins with a slash against the base URL, so
client.get("/users/me")requests/2/users/me - Open a connection with a 10-second timeout rather than 60;
read_timeoutandwrite_timeoutstay 60 - Send each request once, turning off the automatic retry of
Net::HTTP;max_retriesdecides what is sent again - Send an idempotent request again on a new connection when a kept-alive connection had gone stale
- Only before the status and headers of its response are read; a response cut off after that is never sent again
- Raise
X::NetworkErrorfor a body cut off before its end, or shorter than itsContent-Length, rather than read what arrived - Wrap every network failure in
X::NetworkError, so a stream reconnects after it- Including
IOError,SystemCallError,Timeout::Error,Net::ProtocolError,Zlib::Error,Net::HTTPBadResponse,OpenSSL::SSL::SSLError, andSocketError
- Including
- Stop following redirects after exactly
max_redirectshops instead of one more, so 0 follows none - Request the token endpoints at the origin of the base URL, under the path it serves the API at, rather than at
api.x.com- The path is the base URL's minus a trailing API version segment
- With
base_url: "https://gateway.example/x/2/", an app-only token comes from/x/oauth2/tokenand a refresh goes to/x/2/oauth2/token X::OAuth2Authorizationexchanges its code at the base URL of the client it builds
- Raise
X::AuthorizationError, anX::ClientError, when X refuses a token refresh, code exchange, or app-only token- It holds the OAuth 2.0
error_code, such asinvalid_request, and thestatus,headers, andbodyof the response - A token endpoint that fails to answer raises the
X::HTTPErrorof its status, such asX::ServerError, instead - A refresh refused after a 401 raises it with the
X::Unauthorizedas its cause
- It holds the OAuth 2.0
- Raise
X::AuthorizationDenied, anX::Errorwith theerror_codethe redirect reported, for a redirect back from X that refuses - Raise
X::InvalidResponse, anX::HTTPError, for a successful response whose body is not JSON, where it returned nil - Tag the bodies of responses, errors, and stream lines as UTF-8, so comparing them with non-ASCII text works
- A body that is not valid UTF-8 keeps its bytes;
valid_encoding?tells it apart
- A body that is not valid UTF-8 keeps its bytes;
- Read only rate limits a response reports in full, in base 10
X::TooManyRequests#retry_afterno longer raisesKeyErrororArgumentErrorfor a missing or malformed header
- Return nil from
X::TooManyRequests#reset_at,#reset_in, and#retry_afterwhen the response does not say when the limit resets - Read
Retry-AfterwithX::HTTPError#retry_after, in seconds or as an HTTP date, or nilX::TooManyRequests#retry_afterreads it, falling back on#reset_in; retries and reconnects wait for it
- Report every rate limit a response names from
X::TooManyRequests#rate_limits, not just the exhausted ones#rate_limitreads the 15-minute limit; the exhausted limit that resets last is now#limiting_rate_limit
- Make
X::RateLimit.newand.reported?private - Make public, documented constructors for the errors of
x-coreandX::Response, so code that rescues them can be testedX::HTTPError.new(status:, headers:, body:, http_method: nil, uri: nil), orhttp_response:in their placeraise X::NotFoundandraise X::TooManyRequests, "slow down"work as for any exceptionX::Response.new(http_method:, uri:, status:, headers:, body:), orhttp_response:in their place- A status outside 100 to 599, or headers that are not a Hash, raise
ArgumentError
- Name the internal classes of
x-coreunderX::Coreand those ofx-streamingunderX::Streaming, as private constants - Make
X::Connectionthe internalX::Core::Connection- The authenticators take no
connection: X::OAuth2Authorizationtakesbase_url,proxy_url,open_timeout,read_timeout,write_timeout,debug_output, andheadersinstead
- The authenticators take no
- Make the constants that hold messages, patterns, or internal defaults of
x-coreprivateX::HTTPError::JSON_CONTENT_TYPE_REGEXP,X::OAuth2Authenticator::EXPIRATION_BUFFER, and its token endpoint (TOKEN_HOST,TOKEN_PATH)
- Make
X::Uploader::Validatorand the MIME type and size constants ofX::Uploader::MediaUploadprivateMIME_TYPES,MIME_TYPE_MAP, the*_MIME_TYPEconstants,BYTES_PER_MB,MAX_SIMPLE_UPLOAD_BYTES, and the endpoints ofX::Uploader::Account- The media category constants, such as
TWEET_IMAGE, remain public
- Make
infer_media_typeofX::Uploader::MediaUploadinternal; passmedia_type:to override the type an upload infers - Ship the signatures of the public interface alone in each gem
- Document every error class of
x-coreand draw the whole hierarchy onX::Error - Sign OAuth 1.0a requests, and build OAuth 2.0 token refreshes, with the simple_oauth gem
- Take media as a path (
StringorPathname) or an IO inupload_media,upload,chunked_upload,update_profile_image, andupdate_profile_banner- A
FileorTempfileis read a chunk at a time; another IO, such as aStringIO, is read whole - A String holding a NUL byte or a line break raises
ArgumentError; pass a path or aStringIO - An IO that cannot be read raises
X::InvalidMedia
- A
- Infer the type of media from the bytes it begins with before the name of its file
- GIF, PNG, JPEG, BMP, TIFF, WebP, MP4, QuickTime, WebM, MPEG-TS, and WebVTT are recognized
- Media of a type nothing names, such as HEIC or SubRip in a
StringIO, raisesX::InvalidMediaTypewithoutmedia_category: - A file whose signature does not match its extension, such as TypeScript named
.ts, raisesX::InvalidMediaType - A type the media category does not take, such as MP4 with
"tweet_gif", raisesX::InvalidMediaType
- Raise
X::MissingMediaDatainstead ofKeyErroror nil when an upload, status check, or metadata response holds no media- Its
problemshold the problems the response reported, the first named in its message
- Its
- Raise
X::InvalidMediafor a file that does not exist, andX::MediaProcessingFailedfor media that fails to process, instead ofRuntimeErrorX::MediaProcessingFailed#mediaholds what X reported as anX::UploadedMedia
- Raise
ArgumentErrorfromawait_processing,add_alt_text, andadd_subtitlesfor media without an"id" - Raise
ArgumentErrorfromadd_subtitlesfor a language code that is not two letters - Upload in chunks of 4 MB,
X::Uploader::MediaUpload::DEFAULT_CHUNK_SIZE, rather than 1 MB, so a video takes a quarter of the requests- A file larger than the 16 GB the API takes raises
X::InvalidMediabefore the upload chunk_size:replaceschunk_size_mb:, takes bytes, and defaults to nil, which uploads in chunks of 4 MB
- A file larger than the 16 GB the API takes raises
- Upload an animated GIF larger than 5 MB in chunks, which the API takes up to 15 MB of
- Validate the
alt_text:ofuploadbefore uploading, so media is not lost to a refused alt text - Post profile images and banners to the API v1.1 through the client's base URL, credentials, and connections
- Keep the state and helpers of
X::Clientin an internal object, so mixed-in methods never collide with them
Removed
- Remove
X::MediaUploader.upload_binary; pass aStringIOtoX::Uploader::MediaUpload.upload - Remove
upload_profile_image_binaryandupload_profile_banner_binary; pass an IO toupdate_profile_imageandupdate_profile_banner - Remove
require "x/media_uploader"andrequire "x/account_uploader"; requirex,x/uploader/media_upload, orx/uploader/account - Remove the
boundary:of the upload methods, which each upload now generates for itself - Remove
X::AccountUploader::MIME_TYPE_MAP - Remove
X::Uploader::MediaUpload::PROCESSING_INFO_STATES; useX::UploadedMedia#processing? - Remove
X::Uploader::MediaUpload::MAX_RETRIES; a chunk is retried up to the client'smax_retries - Remove
X::HTTPError#code; use#status, an Integer, orerror.http_response.code - Remove
X::HTTPError#error_messageand#message_from_json_response, and make#json?private - Remove
X::RateLimit#retry_after; read#reset_in - Remove the setters of
X::Client; derive a client that differs withX::Client#withapi_key=,api_key_secret=,access_token=,access_token_secret=,bearer_token=,client_id=,client_secret=,refresh_token=base_url=,default_array_class=,default_object_class=,open_timeout=,read_timeout=,write_timeout=,proxy_url=,debug_output=,max_redirects=
- Remove the setters of
X::Connection,X::RateLimit,X::BearerTokenAuthenticator,X::OAuth1Authenticator, andX::OAuth2Authenticator - Remove
X::Connection::DEFAULT_HOSTandDEFAULT_PORT - Remove
X::OAuth1Authenticator#access_token;user_idreads the user the token acts for - Remove
X::OAuthAuthenticator::OAUTH_SIGNATURE_ALGORITHM,OAUTH_VERSION, andOAUTH_SIGNATURE_METHOD - Remove
X::OAuth2Authenticator::REFRESH_GRANT_TYPE - Remove the
base64dependency
Fixed
- Keep the method and body of a
PUTorDELETEthat a 301 or 302 redirects, as RFC 9110 has it- A
POSTredirected by a 301 or 302, and any request redirected by a 303, is still followed with aGET
- A
- Resolve a relative redirect against the URL of the request, rather than the base URL
- Raise
X::HTTPErrorfor a redirect that cannot be followed, instead ofKeyError,URI::InvalidURIError, orArgumentError- A 300, 304, or 305, or one whose
Locationis missing, invalid, or not HTTP or HTTPS
- A 300, 304, or 305, or one whose
- Send the credentials on every redirected request, not only the first
- Drop the credentials and any
Authorization,Cookie, orProxy-Authorizationheader on a redirect to another origin - Preserve the headers passed to
get,post,put, anddeleteacross redirects - Send no
Authorizationheader from a client without credentials, rather than an empty one - Send a
Content-Typeheader only with a request that carries a body - Build the message of an
X::HTTPErrorfrom a body that is not JSON, or whose errors have nomessage- Instead of raising
JSON::ParserError,KeyError, orTypeErrorin place of the error
- Instead of raising
- Raise
X::ClientErrororX::ServerErrorfor an unnamed 4xx or 5xx status, such as 411 or 501, instead ofX::HTTPError - Raise the errors of
on_response, a request's block, or the object class as they were raised- The request is not retried, rate-limited, or refreshed, and a stream does not reconnect for them
- Send a query parameter without a value, as the
flagofget("users?flag"), without= - Connect to a host or proxy named by an IPv6 literal, such as
http://[::1]:8080/;x-coredepends on net-http 0.8 or later - End a
base_urlwithout a trailing slash with one, sohttps://api.x.com/2requests/2/users/me - Take the proxy from
https_proxyfor HTTPS requests andhttp_proxyfor HTTP, and honorno_proxy - Connect to an
https://proxy over TLS - Decode a percent-encoded proxy user and password
- Leave the proxy user and password out of the message of an invalid proxy URL, and raise
ArgumentErrorfor one - Sign a form-encoded request body with OAuth 1.0a
- Sign every value of a repeated query parameter
- Sign the normalized URL, so a request to a host with no path signs
/ - Form-encode the client credentials before Basic authentication on token refresh, as RFC 6749 Section 2.3.1 requires
- Refresh an OAuth 2.0 token through the connection of the client that sends the request, with its proxy and timeouts
- Declare
X::BadGatewayandX::GatewayTimeoutasX::ServerErrors in the signatures - Declare the standard libraries each gem's signatures refer to in its
sig/manifest.yaml - Link each gem's
changelog_urito themainbranch rather thanmaster - Parse uploader responses into Hashes and Arrays whatever the client's
default_object_classanddefault_array_class - Return nil from
update_profile_imageandupdate_profile_banner; look the user up to read what it holds - Raise
X::InvalidMediafromupdate_profile_imageandupdate_profile_bannerfor an empty or oversized file- A profile image larger than 700 KB or a banner larger than 5 MB
X::InvalidMediaTypefor one that is not a GIF, JPEG, or PNG;ArgumentErrorfor a non-integer banner offset or size
- Accept the
amplify_videomedia category, and subtitle such a video withadd_subtitles - Upload every media type the API documents: WebM, QuickTime, and MPEG-TS videos, WebVTT subtitles, and BMP, TIFF, and progressive JPEG images
- Upload an
.m4vfile as an MP4 video, in chunks - Raise
X::InvalidMediaTypebefore a request for.avi,.mkv(other than WebM),.glb, and.usdzfiles - Upload subtitles in chunks as
text/srt, the type the API names - Send the media category in lowercase, as the API documents it
- Raise
X::InvalidMediafrom the uploaders, before any request, for media that cannot be uploaded- An empty file, a directory, a file that cannot be read, or an IO open for writing alone
- A
Pathnameof a missing file, as for aString, instead ofTypeError - An image over 5 MB, a GIF over 15 MB, or subtitles over 1 MB
- Raise
X::MissingMediaDatabefore a chunk is uploaded when the initialize response holds no media, instead ofNoMethodError - Give up waiting for media to process after the
processing_timeout:(default 600 seconds), raisingX::MediaProcessingTimeout- Taken by
upload,await_processing,await_processing!,upload_media, andawait_media_processing(!) processing_timeout:takes a finite number of seconds of at least 0, or nil for no limit- Anything else,
Float::INFINITYincluded, raisesArgumentErrorbefore the first request - Checks wait at least a second apart when X asks for no wait
- Processing that ends in an undocumented state raises
X::MediaProcessingFailedfromuploadandawait_processing!
- Taken by
- Retry a failed chunk, and the finalize request, with the client's
max_retries, backing off rather than retrying at once - Upload the chunks of a video four at a time, or the
concurrency:ofchunked_upload(at most 16), instead of a thread per chunk- A chunk that fails stops the chunks not yet begun
- Stop the chunks of a chunked upload when the waiting thread is interrupted, such as by a timeout
- Including a
ThreadErroror interrupt while the chunk threads are being started
- Including a
- Raise
ArgumentErrorfromchunked_upload, before any request, for an invalidchunk_sizeorconcurrencychunk_sizemust be a positive Integer of at most 5,242,880 bytes, within the 10,000 segments the API numbersconcurrencymust be an Integer from 1 toMAX_CONCURRENCY(16)
- Read the size of chunked upload media once, so a growing file uploads the declared
total_bytes - Give a class that includes
X::Uploader::MediaUploadorX::Uploader::Accounttheir public methods alone, so its own methods do not break uploads
0.19.0 - 2026-03-01
- Add streaming support for filtered stream and volume stream endpoints
0.18.0 - 2026-01-06
- Add OAuth 2.0 authentication with token refresh support (d4c03cb)
- Add AccountUploader for profile image and banner uploads (7833dd2)
- Raise InvalidMediaType error for unsupported file extensions (2b6eacc)
- Prioritize errors array over title/detail in error messages (75279b9)
0.17.0 - 2025-12-02
- Add MediaUploader.upload_binary method (9f2f108)
- Don't forward filename during media upload (492214d)
0.16.0 - 2025-06-24
- Remove media_type parameter from non-chunked upload and append methods (f1f38b5)
- Fix media upload (dcb418a)
- Add await_processing! method to handle media upload failures (6cfc973)
- Move media_category in body for media upload (b790636)
0.15.4 - 2025-05-02
- Use dedicated endpoints for chunked media upload (d54d0d0)
0.15.3 - 2025-04-24
- Add missing base64 dependency (3ca8512)
- Set binary read for media files to be uploaded (fd066e6)
0.15.2 - 2025-03-28
- Use media_id instead of media_key to upload media (f1dd577)
0.15.1 - 2025-03-24
- Fix bug in MediaUploader#await_processing (136dff8)
- Refactor RedirectHandler#build_request (fd379c3)
- Escape space in query string as %20, not + (2d2df75)
- Don't escape commas in query parameters (e7d9056)
0.15.0 - 2025-02-06
- Change media upload to use the API v2 endpoints (eca2b88)
0.14.1 - 2023-12-20
- Fix infinite loop when an upload fails (5dfc604)
0.14.0 - 2023-12-08
- Allow passing custom objects per-request (768889f)
0.13.0 - 2023-12-04
- Introduce X::RateLimit, which is returned with X::TooManyRequests errors (196caec)
0.12.1 - 2023-11-28
- Ensure split chunks are written as binary (c6e257f)
- Require tmpdir in X::MediaUploader (9e7c7f1)
0.12.0 - 2023-11-02
- Ensure Authenticator is passed to RedirectHandler (fc8557b)
- Add AUTHENTICATION_HEADER to X::Authenticator base class (85a2818)
- Introduce X::HTTPError (90ae132)
- Add
codeattribute to error classes (b003639)
0.11.0 - 2023-10-24
- Add base Authenticator class (8c66ce2)
- Consistently use keyword arguments (3beb271)
- Use patern matching to build request (4d001c7)
- Rename ResponseHandler to ResponseParser (498e890)
- Rename methods to be more consistent (5b8c655)
- Rename MediaUpload to MediaUploader (84f0c15)
- Add mutant and kill mutants (b124968)
- Fix authentication bug with request URLs that contain spaces (8de3174)
- Refactor errors (853d39c)
- Make Connection class threadsafe (d95d285)
0.10.0 - 2023-10-08
- Add media upload helper methods (6c6a267)
- Add PayloadTooLargeError class (cd61850)
0.9.1 - 2023-10-06
- Allow successful empty responses (06bf7db)
- Update default User-Agent string (296b36a)
- Move query parameter escaping into RequestBuilder (56d6bd2)
0.9.0 - 2023-09-26
- Add support for HTTP proxies (3740f4f)
0.8.1 - 2023-09-20
- Fix bug where setting Connection#base_uri= doesn't update the HTTP client (d5a89db)
0.8.0 - 2023-09-14
- Add (back) bearer token authentication (62e141d)
- Follow redirects (90a8c55)
- Parse error responses with Content-Type: application/problem+json (0b697d9)
0.7.1 - 2023-09-02
- Fix bug in X::Authenticator#split_uri (ebc9d5f)
0.7.0 - 2023-09-02
- Remove OAuth gem (7c29bb1)
0.6.0 - 2023-08-30
- Add configurable debug output stream for logging (fd2d4b0)
- Remove bearer token authentication (efff940)
- Define RBS type signatures (d7f63ba)
0.5.1 - 2023-08-16
- Fix bearer token authentication (1a1ca93)
0.5.0 - 2023-08-10
- Add configurable write timeout (2a31f84)
- Use built-in Gem::Version class (066e0b6)
0.4.0 - 2023-08-06
- Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
- Add configurable open timeout (1000f9d)
- Allow configuration of content type (f33a732)
0.3.0 - 2023-08-04
- Add accessors to X::Client (e61fa73)
- Add configurable read timeout (41502b9)
- Handle network-related errors (9ed1fb4)
- Include response body in errors (a203e6a)
0.2.0 - 2023-08-02
- Allow configuration of base URL (4bc0531)
- Improve error handling (14dc0cd)
0.1.0 - 2023-08-02
- Initial release