From 08e19b1979409861b5cf9e8a0e09406b920316fa Mon Sep 17 00:00:00 2001 From: LekkereLou <29013180+lekkerelou@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:11:31 +0100 Subject: [PATCH 1/2] feat: Add Space Search functionality and documentation Signed-off-by: LekkereLou <29013180+lekkerelou@users.noreply.github.com> --- README.md | 31 +++++++++ src/collections/Extractors.ts | 3 + src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/Space.ts | 26 ++++++++ src/enums/Resource.ts | 1 + src/index.ts | 1 + src/models/args/FetchArgs.ts | 4 ++ src/requests/Space.ts | 82 +++++++++++++++++++++++ src/services/public/FetcherService.ts | 2 +- src/services/public/SpaceService.ts | 94 +++++++++++++++++++++++++++ src/types/args/FetchArgs.ts | 16 +++++ src/types/raw/space/Search.ts | 10 +++ 13 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 src/types/raw/space/Search.ts diff --git a/README.md b/README.md index 58b03b30..4e54f432 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - User Timeline - User Unfollow - User Profile Update + - Space Search By default, Rettiwt-API uses 'guest' authentication. If however, access to the full set of resources is required, 'user' authentication can be used. This is done by using the cookies associated with your Twitter/X account, and encoding them into an `API_KEY` for convenience. The said `API_KEY` can be obtained by using a browser extension, as follows: @@ -298,6 +299,25 @@ Where, - `` is the username associated with the Twitter account. - `` is the password to the Twitter account. +### 5. Searching spaces + +`space.search(...)` returns an array of `Space` objects (`Space[]`). + +```ts +import { Rettiwt } from 'rettiwt-api'; + +const rettiwt = new Rettiwt({ apiKey: API_KEY }); + +// Automatically appends `filter:spaces` if omitted +rettiwt.space.search('from:tbvxyz lang:zxx', 20) +.then(spaces => { + console.log(spaces); // Space[] +}) +.catch(error => { + console.log(error); +}); +``` + ## Using a custom error handler Out of the box, `Rettiwt`'s error handling is bare-minimum, only able to parse basic error messages. For advanced scenarios, where full error response might be required, in order to diagnose error reason, it's recommended to use a custom error handler, by implementing the `IErrorHandler` interface, as follows: @@ -494,6 +514,11 @@ So far, the following operations are supported: - [Removing a member from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#removeMember) - [Getting the list of tweets from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#tweets) +### Spaces + +- [Getting the details of a given space](https://rishikant181.github.io/Rettiwt-API/classes/SpaceService.html#details) +- [Searching for spaces (returns `Space[]`)](https://rishikant181.github.io/Rettiwt-API/classes/SpaceService.html#search) + ### Tweets - [Bookmarking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#bookmark) @@ -557,6 +582,12 @@ Help for the CLI can be obtained from the CLI itself: - For help regarding the available commands, use the command `rettiwt help` - For help regarding a specific command, use the command `rettiwt help ` +Examples: + +- `rettiwt space details ` +- `rettiwt space search "" [count] [cursor]` +- `rettiwt space search "" [count] [cursor] --latest` + ## API Reference The complete API reference can be found at [this](https://rishikant181.github.io/Rettiwt-API/modules) page. diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index cb2a0145..f73038f5 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -20,6 +20,7 @@ import { IListMemberRemoveResponse } from '../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; import { IMediaInitializeUploadResponse } from '../types/raw/media/InitalizeUpload'; import { IAudioSpaceByIdResponse } from '../types/raw/space/AudioSpaceById'; +import { ISpaceSearchResponse } from '../types/raw/space/Search'; import { ITweetBookmarkResponse } from '../types/raw/tweet/Bookmark'; import { ITweetDetailsResponse } from '../types/raw/tweet/Details'; import { ITweetDetailsBulkResponse } from '../types/raw/tweet/DetailsBulk'; @@ -90,6 +91,8 @@ export const Extractors = { DM_INBOX_TIMELINE: (response: IInboxTimelineResponse): Inbox => new Inbox(response), SPACE_DETAILS: (response: IAudioSpaceByIdResponse): Space | undefined => Space.single(response), + SPACE_SEARCH: (response: ISpaceSearchResponse): CursoredData => + new CursoredData(response, BaseType.TWEET), TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => response?.data?.tweet_bookmark_put === 'Done', TWEET_DETAILS: (response: ITweetDetailsResponse, id: string): Tweet | undefined => Tweet.single(response, id), diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index fdabc441..78700592 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -25,6 +25,7 @@ export const FetchResourcesGroup = [ ResourceType.DM_INBOX_INITIAL_STATE, ResourceType.DM_INBOX_TIMELINE, ResourceType.SPACE_DETAILS, + ResourceType.SPACE_SEARCH, ResourceType.TWEET_DETAILS, ResourceType.TWEET_DETAILS_ALT, ResourceType.TWEET_DETAILS_BULK, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 6b1127d0..ac1a9fb9 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -37,6 +37,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | SPACE_DETAILS: (args: IFetchArgs) => SpaceRequests.details(args.id!, args.withReplays, args.withListeners, args.isMetatagsQuery), + SPACE_SEARCH: (args: IFetchArgs) => SpaceRequests.search(args.query!, args.count, args.cursor, args.top), TWEET_BOOKMARK: (args: IPostArgs) => TweetRequests.bookmark(args.id!), TWEET_DETAILS: (args: IFetchArgs) => TweetRequests.details(args.id!), diff --git a/src/commands/Space.ts b/src/commands/Space.ts index eec7a5e6..33ed94d4 100644 --- a/src/commands/Space.ts +++ b/src/commands/Space.ts @@ -9,6 +9,10 @@ interface ISpaceDetailsOptions { metatags?: boolean; } +interface ISpaceSearchOptions { + latest?: boolean; +} + /** * Creates a new 'space' command which uses the given Rettiwt instance. * @@ -40,6 +44,28 @@ function createSpaceCommand(rettiwt: Rettiwt): Command { } }); + // Search + space + .command('search') + .description('Search for spaces') + .argument('', 'The search query') + .argument('[count]', 'The number of results to fetch') + .argument('[cursor]', 'The cursor to the batch of results to fetch') + .option('--latest', 'Fetch latest results instead of top results') + .action(async (query: string, count?: string, cursor?: string, options?: ISpaceSearchOptions) => { + try { + const results = await rettiwt.space.search( + query, + count ? parseInt(count) : undefined, + cursor, + !options?.latest, + ); + output(results); + } catch (error) { + output(error); + } + }); + return space; } diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index d2b03034..cd127032 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -24,6 +24,7 @@ export enum ResourceType { // SPACE SPACE_DETAILS = 'SPACE_DETAILS', + SPACE_SEARCH = 'SPACE_SEARCH', // TWEET TWEET_BOOKMARK = 'TWEET_BOOKMARK', diff --git a/src/index.ts b/src/index.ts index 1064f867..9818e8b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,6 +90,7 @@ export { IMediaFinalizeUploadResponse as IRawMediaFinalizeUploadResponse } from export { IMediaInitializeUploadResponse as IRawMediaInitializeUploadResponse } from './types/raw/media/InitalizeUpload'; export { IMediaLiveVideoStreamResponse as IRawMediaLiveVideoStreamResponse } from './types/raw/media/LiveVideoStream'; export { IAudioSpaceByIdResponse as IRawSpaceDetailsResponse } from './types/raw/space/AudioSpaceById'; +export { ISpaceSearchResponse as IRawSpaceSearchResponse } from './types/raw/space/Search'; export { ITweetDetailsResponse as IRawTweetDetailsResponse } from './types/raw/tweet/Details'; export { ITweetDetailsBulkResponse as IRawTweetDetailsBulkResponse } from './types/raw/tweet/DetailsBulk'; export { ITweetLikeResponse as IRawTweetLikeResponse } from './types/raw/tweet/Like'; diff --git a/src/models/args/FetchArgs.ts b/src/models/args/FetchArgs.ts index 6cc0473d..dd480bd7 100644 --- a/src/models/args/FetchArgs.ts +++ b/src/models/args/FetchArgs.ts @@ -20,9 +20,11 @@ export class FetchArgs implements IFetchArgs { public isMetatagsQuery?: boolean; public maxId?: string; public metrics?: RawAnalyticsMetric[]; + public query?: string; public showVerifiedFollowers?: boolean; public sortBy?: TweetRepliesSortType; public toTime?: Date; + public top?: boolean; public withListeners?: boolean; public withReplays?: boolean; @@ -30,6 +32,8 @@ export class FetchArgs implements IFetchArgs { * @param args - Additional user-defined arguments for fetching the resource. */ public constructor(args: IFetchArgs) { + this.query = args.query; + this.top = args.top; this.id = args.id; this.ids = args.ids; this.isMetatagsQuery = args.isMetatagsQuery; diff --git a/src/requests/Space.ts b/src/requests/Space.ts index e35ae4f0..51d1886d 100644 --- a/src/requests/Space.ts +++ b/src/requests/Space.ts @@ -1,11 +1,29 @@ import { AxiosRequestConfig } from 'axios'; +import { RawTweetSearchResultType } from '../enums/raw/Tweet'; + /** * Collection of requests related to spaces. * * @public */ export class SpaceRequests { + /** + * Normalize search query for spaces. + * + * @param query - The query to normalize. + * @returns The normalized query. + */ + private static _normalizeSearchQuery(query: string): string { + const normalized = query.trim(); + + if (normalized.includes('filter:spaces')) { + return normalized; + } + + return `${normalized} filter:spaces`.trim(); + } + /** * @param id - The id of the space whose details are to be fetched. * @param withReplays - Whether to include replay information. @@ -73,4 +91,68 @@ export class SpaceRequests { paramsSerializer: { encode: encodeURIComponent }, }; } + + /** + * @param query - The search query. `filter:spaces` is appended if absent. + * @param count - The number of items to fetch. + * @param cursor - The cursor to the batch of results. + * @param top - Whether to fetch top results. + */ + public static search(query: string, count?: number, cursor?: string, top = true): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/f_A-Gyo204PRxixpkrchJg/SearchTimeline', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + rawQuery: SpaceRequests._normalizeSearchQuery(query), + count: count, + cursor: cursor, + querySource: 'typed_query', + product: top ? RawTweetSearchResultType.TOP : RawTweetSearchResultType.LATEST, + withGrokTranslatedBio: false, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: false, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + responsive_web_grok_annotations_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + post_ctas_fetch_enabled: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } } diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 8c1947ba..533c893f 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -254,7 +254,7 @@ export class FetcherService { * Makes an HTTP request according to the given parameters. * * @param resource - The requested resource. - * @param config - The request configuration. + * @param args - The arguments required for the requested resource. * * @typeParam T - The type of the returned response data. * diff --git a/src/services/public/SpaceService.ts b/src/services/public/SpaceService.ts index cffd501f..609de9bf 100644 --- a/src/services/public/SpaceService.ts +++ b/src/services/public/SpaceService.ts @@ -1,9 +1,11 @@ import { Extractors } from '../../collections/Extractors'; import { ResourceType } from '../../enums/Resource'; import { Space } from '../../models/data/Space'; +import { Tweet } from '../../models/data/Tweet'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { ISpaceDetailsOptions } from '../../types/args/FetchArgs'; import { IAudioSpaceByIdResponse } from '../../types/raw/space/AudioSpaceById'; +import { ISpaceSearchResponse } from '../../types/raw/space/Search'; import { FetcherService } from './FetcherService'; @@ -22,6 +24,40 @@ export class SpaceService extends FetcherService { super(config); } + /** + * Extract a space id from a space URL. + * + * @param url - The URL to inspect. + * @returns The extracted space id, if found. + */ + private static _extractSpaceIdFromUrl(url: string): string | undefined { + const match = url.match(/(?:https?:\/\/)?(?:www\.)?(?:x|twitter)\.com\/i\/spaces\/([a-zA-Z0-9]+)/i); + + return match ? match[1] : undefined; + } + + /** + * Extract unique space IDs from tweets. + * + * @param tweets - The list of tweets to inspect. + * @returns The list of unique space ids. + */ + private static _extractSpaceIds(tweets: Tweet[]): string[] { + const ids = new Set(); + + for (const tweet of tweets) { + for (const url of tweet.entities.urls) { + const id = SpaceService._extractSpaceIdFromUrl(url); + + if (id) { + ids.add(id); + } + } + } + + return Array.from(ids); + } + /** * Get the details of a space. * @@ -62,4 +98,62 @@ export class SpaceService extends FetcherService { return data; } + + /** + * Search for spaces using a raw query. + * + * @param query - The raw query. `filter:spaces` is appended if omitted. + * @param count - The number of results to fetch. + * @param cursor - The cursor to the batch of results to fetch. + * @param top - Whether to fetch top results. Defaults to `true`. + * + * @returns The list of spaces matching the query. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * rettiwt.space.search('from:tbvxyz lang:zxx', 20) + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async search(query: string, count?: number, cursor?: string, top = true): Promise { + const resource = ResourceType.SPACE_SEARCH; + + // Fetching raw search results + const response = await this.request(resource, { + query: query, + count: count, + cursor: cursor, + top: top, + }); + + // Extracting space ids from search tweets + const searchResults = Extractors[resource](response); + const spaceIds = SpaceService._extractSpaceIds(searchResults.list); + + // Fetching details for each space id + const spaces = await Promise.all( + spaceIds.map(async (id) => { + try { + return await this.details(id, { + withReplays: false, + withListeners: false, + }); + } catch { + return undefined; + } + }), + ); + + return spaces.filter((space): space is Space => space != undefined); + } } diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index 87058581..e4d82ed5 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -7,6 +7,22 @@ import { TweetRepliesSortType } from '../../enums/Tweet'; * @public */ export interface IFetchArgs { + /** + * The raw query string. + * + * @remarks + * - Only works for {@link ResourceType.SPACE_SEARCH}. + */ + query?: string; + + /** + * Whether to fetch top results. + * + * @remarks + * - Only works for {@link ResourceType.SPACE_SEARCH}. + */ + top?: boolean; + /** * The id of the active conversation. * diff --git a/src/types/raw/space/Search.ts b/src/types/raw/space/Search.ts new file mode 100644 index 00000000..1121211b --- /dev/null +++ b/src/types/raw/space/Search.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ + +import { ITweetSearchResponse } from '../tweet/Search'; + +/** + * The raw data received when searching spaces. + * + * @public + */ +export interface ISpaceSearchResponse extends ITweetSearchResponse {} From 062250d38830d63adb359eea35777150a8e9311b Mon Sep 17 00:00:00 2001 From: LekkereLou <29013180+lekkerelou@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:22:47 +0100 Subject: [PATCH 2/2] Update README.md Signed-off-by: LekkereLou <29013180+lekkerelou@users.noreply.github.com> --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4e54f432..a95dbd01 100644 --- a/README.md +++ b/README.md @@ -309,13 +309,14 @@ import { Rettiwt } from 'rettiwt-api'; const rettiwt = new Rettiwt({ apiKey: API_KEY }); // Automatically appends `filter:spaces` if omitted -rettiwt.space.search('from:tbvxyz lang:zxx', 20) -.then(spaces => { - console.log(spaces); // Space[] -}) -.catch(error => { - console.log(error); -}); +rettiwt.space + .search('from:tbvxyz lang:zxx', 20) + .then((spaces) => { + console.log(spaces); // Space[] + }) + .catch((error) => { + console.log(error); + }); ``` ## Using a custom error handler