diff --git a/apps/docs/content/docs/en/tools/x.mdx b/apps/docs/content/docs/en/tools/x.mdx index 234dc3649c2..7ab8a64d080 100644 --- a/apps/docs/content/docs/en/tools/x.mdx +++ b/apps/docs/content/docs/en/tools/x.mdx @@ -29,113 +29,814 @@ In Sim, the X integration enables sophisticated social media automation scenario ## Usage Instructions -Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile. +Integrate X into the workflow. Search tweets, manage bookmarks, follow/block/mute users, like and retweet, view trends, and more. ## Tools -### `x_write` +### `x_create_tweet` -Post new tweets, reply to tweets, or create polls on X (Twitter) +Create a new tweet, reply, or quote tweet on X #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `text` | string | Yes | The text content of your tweet \(max 280 characters\) | -| `replyTo` | string | No | ID of the tweet to reply to \(e.g., 1234567890123456789\) | -| `mediaIds` | array | No | Array of media IDs to attach to the tweet | -| `poll` | object | No | Poll configuration for the tweet | +| `text` | string | Yes | The text content of the tweet \(max 280 characters\) | +| `replyToTweetId` | string | No | Tweet ID to reply to | +| `quoteTweetId` | string | No | Tweet ID to quote | +| `mediaIds` | string | No | Comma-separated media IDs to attach \(up to 4\) | +| `replySettings` | string | No | Who can reply: "mentionedUsers", "following", "subscribers", or "verified" | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweet` | object | The newly created tweet data | +| `id` | string | The ID of the created tweet | +| `text` | string | The text of the created tweet | + +### `x_delete_tweet` + +Delete a tweet authored by the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tweetId` | string | Yes | The ID of the tweet to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the tweet was successfully deleted | + +### `x_search_tweets` + +Search for recent tweets using keywords, hashtags, or advanced query operators + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search query \(supports operators like "from:", "to:", "#hashtag", "has:images", "is:retweet", "lang:"\) | +| `maxResults` | number | No | Maximum number of results \(10-100, default 10\) | +| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format \(e.g., 2024-01-01T00:00:00Z\) | +| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format | +| `sinceId` | string | No | Returns tweets with ID greater than this | +| `untilId` | string | No | Returns tweets with ID less than this | +| `sortOrder` | string | No | Sort order: "recency" or "relevancy" | +| `nextToken` | string | No | Pagination token for next page of results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of tweets matching the search query | | ↳ `id` | string | Tweet ID | -| ↳ `text` | string | Tweet content text | +| ↳ `text` | string | Tweet text content | | ↳ `createdAt` | string | Tweet creation timestamp | -| ↳ `authorId` | string | ID of the tweet author | +| ↳ `authorId` | string | Author user ID | | ↳ `conversationId` | string | Conversation thread ID | -| ↳ `attachments` | object | Media or poll attachments | -| ↳ `mediaKeys` | array | Media attachment keys | -| ↳ `pollId` | string | Poll ID if poll attached | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `includes` | object | Additional data including user profiles | +| ↳ `users` | array | Array of user objects referenced in tweets | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Search metadata including result count and pagination tokens | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `newestId` | string | ID of the newest tweet | +| ↳ `oldestId` | string | ID of the oldest tweet | +| ↳ `nextToken` | string | Pagination token for next page | + +### `x_get_tweets_by_ids` + +Look up multiple tweets by their IDs (up to 100) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `ids` | string | Yes | Comma-separated tweet IDs \(up to 100\) | -### `x_read` +#### Output -Read tweet details, including replies and conversation context +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of tweets matching the provided IDs | +| ↳ `id` | string | Tweet ID | +| ↳ `text` | string | Tweet text content | +| ↳ `createdAt` | string | Tweet creation timestamp | +| ↳ `authorId` | string | Author user ID | +| ↳ `conversationId` | string | Conversation thread ID | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `includes` | object | Additional data including user profiles | +| ↳ `users` | array | Array of user objects referenced in tweets | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | + +### `x_get_quote_tweets` + +Get tweets that quote a specific tweet #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tweetId` | string | Yes | ID of the tweet to read \(e.g., 1234567890123456789\) | -| `includeReplies` | boolean | No | Whether to include replies to the tweet | +| `tweetId` | string | Yes | The tweet ID to get quote tweets for | +| `maxResults` | number | No | Maximum number of results \(10-100, default 10\) | +| `paginationToken` | string | No | Pagination token for next page | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweet` | object | The main tweet data | +| `tweets` | array | Array of quote tweets | | ↳ `id` | string | Tweet ID | -| ↳ `text` | string | Tweet content text | +| ↳ `text` | string | Tweet text content | | ↳ `createdAt` | string | Tweet creation timestamp | -| ↳ `authorId` | string | ID of the tweet author | -| `context` | object | Conversation context including parent and root tweets | +| ↳ `authorId` | string | Author user ID | +| ↳ `conversationId` | string | Conversation thread ID | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | -### `x_search` +### `x_hide_reply` -Search for tweets using keywords, hashtags, or advanced queries +Hide or unhide a reply to a tweet authored by the authenticated user #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `query` | string | Yes | Search query \(e.g., "AI news", "#technology", "from:username"\). Supports X search operators | -| `maxResults` | number | No | Maximum number of results to return \(e.g., 10, 25, 50\). Default: 10, max: 100 | -| `startTime` | string | No | Start time for search \(ISO 8601 format\) | -| `endTime` | string | No | End time for search \(ISO 8601 format\) | -| `sortOrder` | string | No | Sort order for results \(recency or relevancy\) | +| `tweetId` | string | Yes | The reply tweet ID to hide or unhide | +| `hidden` | boolean | Yes | Set to true to hide the reply, false to unhide | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweets` | array | Array of tweets matching the search query | +| `hidden` | boolean | Whether the reply is now hidden | + +### `x_get_user_tweets` + +Get tweets authored by a specific user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID whose tweets to retrieve | +| `maxResults` | number | No | Maximum number of results \(5-100, default 10\) | +| `paginationToken` | string | No | Pagination token for next page of results | +| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format | +| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format | +| `sinceId` | string | No | Returns tweets with ID greater than this | +| `untilId` | string | No | Returns tweets with ID less than this | +| `exclude` | string | No | Comma-separated types to exclude: "retweets", "replies" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of tweets by the user | +| ↳ `id` | string | Tweet ID | +| ↳ `text` | string | Tweet text content | +| ↳ `createdAt` | string | Tweet creation timestamp | +| ↳ `authorId` | string | Author user ID | +| ↳ `conversationId` | string | Conversation thread ID | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `includes` | object | Additional data including user profiles | +| ↳ `users` | array | Array of user objects referenced in tweets | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `newestId` | string | ID of the newest tweet | +| ↳ `oldestId` | string | ID of the oldest tweet | +| ↳ `nextToken` | string | Token for next page | +| ↳ `previousToken` | string | Token for previous page | + +### `x_get_user_mentions` + +Get tweets that mention a specific user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID whose mentions to retrieve | +| `maxResults` | number | No | Maximum number of results \(5-100, default 10\) | +| `paginationToken` | string | No | Pagination token for next page of results | +| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format | +| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format | +| `sinceId` | string | No | Returns tweets with ID greater than this | +| `untilId` | string | No | Returns tweets with ID less than this | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of tweets mentioning the user | +| ↳ `id` | string | Tweet ID | +| ↳ `text` | string | Tweet text content | +| ↳ `createdAt` | string | Tweet creation timestamp | +| ↳ `authorId` | string | Author user ID | +| ↳ `conversationId` | string | Conversation thread ID | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `includes` | object | Additional data including user profiles | +| ↳ `users` | array | Array of user objects referenced in tweets | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `newestId` | string | ID of the newest tweet | +| ↳ `oldestId` | string | ID of the oldest tweet | +| ↳ `nextToken` | string | Token for next page | +| ↳ `previousToken` | string | Token for previous page | + +### `x_get_user_timeline` + +Get the reverse chronological home timeline for the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `maxResults` | number | No | Maximum number of results \(1-100, default 10\) | +| `paginationToken` | string | No | Pagination token for next page of results | +| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format | +| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format | +| `sinceId` | string | No | Returns tweets with ID greater than this | +| `untilId` | string | No | Returns tweets with ID less than this | +| `exclude` | string | No | Comma-separated types to exclude: "retweets", "replies" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of timeline tweets | +| ↳ `id` | string | Tweet ID | +| ↳ `text` | string | Tweet text content | +| ↳ `createdAt` | string | Tweet creation timestamp | +| ↳ `authorId` | string | Author user ID | +| ↳ `conversationId` | string | Conversation thread ID | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `includes` | object | Additional data including user profiles | +| ↳ `users` | array | Array of user objects referenced in tweets | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `newestId` | string | ID of the newest tweet | +| ↳ `oldestId` | string | ID of the oldest tweet | +| ↳ `nextToken` | string | Token for next page | +| ↳ `previousToken` | string | Token for previous page | + +### `x_manage_like` + +Like or unlike a tweet on X + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `tweetId` | string | Yes | The tweet ID to like or unlike | +| `action` | string | Yes | Action to perform: "like" or "unlike" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `liked` | boolean | Whether the tweet is now liked | + +### `x_manage_retweet` + +Retweet or unretweet a tweet on X + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `tweetId` | string | Yes | The tweet ID to retweet or unretweet | +| `action` | string | Yes | Action to perform: "retweet" or "unretweet" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `retweeted` | boolean | Whether the tweet is now retweeted | + +### `x_get_liked_tweets` + +Get tweets liked by a specific user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID whose liked tweets to retrieve | +| `maxResults` | number | No | Maximum number of results \(5-100\) | +| `paginationToken` | string | No | Pagination token for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of liked tweets | | ↳ `id` | string | Tweet ID | | ↳ `text` | string | Tweet content | | ↳ `createdAt` | string | Creation timestamp | | ↳ `authorId` | string | Author user ID | -| `includes` | object | Additional data including user profiles and media | -| `meta` | object | Search metadata including result count and pagination tokens | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | + +### `x_get_liking_users` + +Get the list of users who liked a specific tweet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tweetId` | string | Yes | The tweet ID to get liking users for | +| `maxResults` | number | No | Maximum number of results \(1-100, default 100\) | +| `paginationToken` | string | No | Pagination token for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of users who liked the tweet | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | + +### `x_get_retweeted_by` + +Get the list of users who retweeted a specific tweet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tweetId` | string | Yes | The tweet ID to get retweeters for | +| `maxResults` | number | No | Maximum number of results \(1-100, default 100\) | +| `paginationToken` | string | No | Pagination token for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of users who retweeted the tweet | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | + +### `x_get_bookmarks` + +Get bookmarked tweets for the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `maxResults` | number | No | Maximum number of results \(1-100\) | +| `paginationToken` | string | No | Pagination token for next page of results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tweets` | array | Array of bookmarked tweets | +| ↳ `id` | string | Tweet ID | +| ↳ `text` | string | Tweet text content | +| ↳ `createdAt` | string | Tweet creation timestamp | +| ↳ `authorId` | string | Author user ID | +| ↳ `conversationId` | string | Conversation thread ID | +| ↳ `inReplyToUserId` | string | User ID being replied to | +| ↳ `publicMetrics` | object | Engagement metrics | +| ↳ `retweetCount` | number | Number of retweets | +| ↳ `replyCount` | number | Number of replies | +| ↳ `likeCount` | number | Number of likes | +| ↳ `quoteCount` | number | Number of quotes | +| `includes` | object | Additional data including user profiles | +| ↳ `users` | array | Array of user objects referenced in tweets | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | | ↳ `resultCount` | number | Number of results returned | | ↳ `newestId` | string | ID of the newest tweet | | ↳ `oldestId` | string | ID of the oldest tweet | +| ↳ `nextToken` | string | Token for next page | +| ↳ `previousToken` | string | Token for previous page | + +### `x_create_bookmark` + +Bookmark a tweet for the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `tweetId` | string | Yes | The tweet ID to bookmark | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmarked` | boolean | Whether the tweet was successfully bookmarked | + +### `x_delete_bookmark` + +Remove a tweet from the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `tweetId` | string | Yes | The tweet ID to remove from bookmarks | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmarked` | boolean | Whether the tweet is still bookmarked \(should be false after deletion\) | + +### `x_get_me` + +Get the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | Authenticated user profile | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | + +### `x_search_users` + +Search for X users by name, username, or bio + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search keyword \(1-50 chars, matches name, username, or bio\) | +| `maxResults` | number | No | Maximum number of results \(1-1000, default 100\) | +| `nextToken` | string | No | Pagination token for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of users matching the search query | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Search metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Pagination token for next page | + +### `x_get_followers` + +Get the list of followers for a user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID whose followers to retrieve | +| `maxResults` | number | No | Maximum number of results \(1-1000, default 100\) | +| `paginationToken` | string | No | Pagination token for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of follower user profiles | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | + +### `x_get_following` + +Get the list of users that a user is following + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID whose following list to retrieve | +| `maxResults` | number | No | Maximum number of results \(1-1000, default 100\) | +| `paginationToken` | string | No | Pagination token for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of users being followed | +| ↳ `id` | string | User ID | +| ↳ `username` | string | Username without @ symbol | +| ↳ `name` | string | Display name | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | +| ↳ `verified` | boolean | Whether the user is verified | +| ↳ `metrics` | object | User statistics | +| ↳ `followersCount` | number | Number of followers | +| ↳ `followingCount` | number | Number of users following | +| ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | + +### `x_manage_follow` + +Follow or unfollow a user on X -### `x_user` +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `targetUserId` | string | Yes | The user ID to follow or unfollow | +| `action` | string | Yes | Action to perform: "follow" or "unfollow" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `following` | boolean | Whether you are now following the user | +| `pendingFollow` | boolean | Whether the follow request is pending \(for protected accounts\) | + +### `x_manage_block` + +Block or unblock a user on X + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `targetUserId` | string | Yes | The user ID to block or unblock | +| `action` | string | Yes | Action to perform: "block" or "unblock" | + +#### Output -Get user profile information +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `blocking` | boolean | Whether you are now blocking the user | + +### `x_get_blocking` + +Get the list of users blocked by the authenticated user #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `username` | string | Yes | Username to look up without @ symbol \(e.g., elonmusk, openai\) | +| `userId` | string | Yes | The authenticated user ID | +| `maxResults` | number | No | Maximum number of results \(1-1000\) | +| `paginationToken` | string | No | Pagination token for next page | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `user` | object | X user profile information | +| `users` | array | Array of blocked user profiles | | ↳ `id` | string | User ID | | ↳ `username` | string | Username without @ symbol | | ↳ `name` | string | Display name | -| ↳ `description` | string | User bio/description | +| ↳ `description` | string | User bio | +| ↳ `profileImageUrl` | string | Profile image URL | | ↳ `verified` | boolean | Whether the user is verified | | ↳ `metrics` | object | User statistics | | ↳ `followersCount` | number | Number of followers | | ↳ `followingCount` | number | Number of users following | | ↳ `tweetCount` | number | Total number of tweets | +| `meta` | object | Pagination metadata | +| ↳ `resultCount` | number | Number of results returned | +| ↳ `nextToken` | string | Token for next page | + +### `x_manage_mute` + +Mute or unmute a user on X + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The authenticated user ID | +| `targetUserId` | string | Yes | The user ID to mute or unmute | +| `action` | string | Yes | Action to perform: "mute" or "unmute" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `muting` | boolean | Whether you are now muting the user | + +### `x_get_trends_by_woeid` + +Get trending topics for a specific location by WOEID (e.g., 1 for worldwide, 23424977 for US) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `woeid` | string | Yes | Yahoo Where On Earth ID \(e.g., "1" for worldwide, "23424977" for US, "23424975" for UK\) | +| `maxTrends` | number | No | Maximum number of trends to return \(1-50, default 20\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `trends` | array | Array of trending topics | +| ↳ `trendName` | string | Name of the trending topic | +| ↳ `tweetCount` | number | Number of tweets for this trend | + +### `x_get_personalized_trends` + +Get personalized trending topics for the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `trends` | array | Array of personalized trending topics | +| ↳ `trendName` | string | Name of the trending topic | +| ↳ `postCount` | number | Number of posts for this trend | +| ↳ `category` | string | Category of the trend | +| ↳ `trendingSince` | string | ISO 8601 timestamp of when the topic started trending | + +### `x_get_usage` + +Get the API usage data for your X project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `days` | number | No | Number of days of usage data to return \(1-90, default 7\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `capResetDay` | number | Day of month when usage cap resets | +| `projectId` | string | The project ID | +| `projectCap` | number | The project tweet consumption cap | +| `projectUsage` | number | Total tweets consumed in current period | +| `dailyProjectUsage` | array | Daily project usage breakdown | +| ↳ `date` | string | Usage date in ISO 8601 format | +| ↳ `usage` | number | Number of tweets consumed | +| `dailyClientAppUsage` | array | Daily per-app usage breakdown | +| ↳ `clientAppId` | string | Client application ID | +| ↳ `usage` | array | Daily usage entries for this app | +| ↳ `date` | string | Usage date in ISO 8601 format | +| ↳ `usage` | number | Number of tweets consumed | diff --git a/apps/sim/blocks/blocks/x.ts b/apps/sim/blocks/blocks/x.ts index 9b297a064fe..7f187e7aacf 100644 --- a/apps/sim/blocks/blocks/x.ts +++ b/apps/sim/blocks/blocks/x.ts @@ -1,18 +1,17 @@ import { xIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' -import type { XResponse } from '@/tools/x/types' -export const XBlock: BlockConfig = { +export const XBlock: BlockConfig = { type: 'x', name: 'X', description: 'Interact with X', authMode: AuthMode.OAuth, longDescription: - 'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile.', + 'Integrate X into the workflow. Search tweets, manage bookmarks, follow/block/mute users, like and retweet, view trends, and more.', docsLink: 'https://docs.sim.ai/tools/x', category: 'tools', - bgColor: '#000000', // X's black color + bgColor: '#000000', icon: xIcon, subBlocks: [ { @@ -20,13 +19,46 @@ export const XBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ - { label: 'Post a New Tweet', id: 'x_write' }, - { label: 'Get Tweet Details', id: 'x_read' }, - { label: 'Search Tweets', id: 'x_search' }, - { label: 'Get User Profile', id: 'x_user' }, + // Tweet Operations + { label: 'Create Tweet', id: 'x_create_tweet' }, + { label: 'Delete Tweet', id: 'x_delete_tweet' }, + { label: 'Search Tweets', id: 'x_search_tweets' }, + { label: 'Get Tweets by IDs', id: 'x_get_tweets_by_ids' }, + { label: 'Get Quote Tweets', id: 'x_get_quote_tweets' }, + { label: 'Hide Reply', id: 'x_hide_reply' }, + // User Tweet Operations + { label: 'Get User Tweets', id: 'x_get_user_tweets' }, + { label: 'Get User Mentions', id: 'x_get_user_mentions' }, + { label: 'Get User Timeline', id: 'x_get_user_timeline' }, + // Engagement Operations + { label: 'Like / Unlike', id: 'x_manage_like' }, + { label: 'Retweet / Unretweet', id: 'x_manage_retweet' }, + { label: 'Get Liked Tweets', id: 'x_get_liked_tweets' }, + { label: 'Get Liking Users', id: 'x_get_liking_users' }, + { label: 'Get Retweeted By', id: 'x_get_retweeted_by' }, + // Bookmark Operations + { label: 'Get Bookmarks', id: 'x_get_bookmarks' }, + { label: 'Create Bookmark', id: 'x_create_bookmark' }, + { label: 'Delete Bookmark', id: 'x_delete_bookmark' }, + // User Operations + { label: 'Get My Profile', id: 'x_get_me' }, + { label: 'Search Users', id: 'x_search_users' }, + { label: 'Get Followers', id: 'x_get_followers' }, + { label: 'Get Following', id: 'x_get_following' }, + // User Relationship Operations + { label: 'Follow / Unfollow', id: 'x_manage_follow' }, + { label: 'Block / Unblock', id: 'x_manage_block' }, + { label: 'Get Blocked Users', id: 'x_get_blocking' }, + { label: 'Mute / Unmute', id: 'x_manage_mute' }, + // Trends Operations + { label: 'Get Trends by Location', id: 'x_get_trends_by_woeid' }, + { label: 'Get Personalized Trends', id: 'x_get_personalized_trends' }, + // Usage Operations + { label: 'Get API Usage', id: 'x_get_usage' }, ], - value: () => 'x_write', + value: () => 'x_create_tweet', }, + // --- OAuth Credential --- { id: 'credential', title: 'X Account', @@ -34,7 +66,23 @@ export const XBlock: BlockConfig = { serviceId: 'x', canonicalParamId: 'oauthCredential', mode: 'basic', - requiredScopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'], + requiredScopes: [ + 'tweet.read', + 'tweet.write', + 'tweet.moderate.write', + 'users.read', + 'follows.read', + 'follows.write', + 'bookmark.read', + 'bookmark.write', + 'like.read', + 'like.write', + 'block.read', + 'block.write', + 'mute.read', + 'mute.write', + 'offline.access', + ], placeholder: 'Select X account', }, { @@ -45,79 +93,270 @@ export const XBlock: BlockConfig = { mode: 'advanced', placeholder: 'Enter credential ID', }, + // --- Create Tweet fields --- { id: 'text', title: 'Tweet Text', type: 'long-input', placeholder: "What's happening?", - condition: { field: 'operation', value: 'x_write' }, + condition: { field: 'operation', value: 'x_create_tweet' }, required: true, }, { - id: 'replyTo', - title: 'Reply To (Tweet ID)', + id: 'replyToTweetId', + title: 'Reply To Tweet ID', type: 'short-input', placeholder: 'Enter tweet ID to reply to', - condition: { field: 'operation', value: 'x_write' }, + condition: { field: 'operation', value: 'x_create_tweet' }, + }, + { + id: 'quoteTweetId', + title: 'Quote Tweet ID', + type: 'short-input', + placeholder: 'Enter tweet ID to quote', + condition: { field: 'operation', value: 'x_create_tweet' }, }, { id: 'mediaIds', title: 'Media IDs', type: 'short-input', - placeholder: 'Enter comma-separated media IDs', - condition: { field: 'operation', value: 'x_write' }, + placeholder: 'Comma-separated media IDs (up to 4)', + condition: { field: 'operation', value: 'x_create_tweet' }, }, + { + id: 'replySettings', + title: 'Reply Settings', + type: 'dropdown', + options: [ + { label: 'Everyone', id: '' }, + { label: 'Mentioned Users', id: 'mentionedUsers' }, + { label: 'Following', id: 'following' }, + { label: 'Subscribers', id: 'subscribers' }, + { label: 'Verified', id: 'verified' }, + ], + value: () => '', + condition: { field: 'operation', value: 'x_create_tweet' }, + }, + // --- Tweet ID field (shared by multiple operations) --- { id: 'tweetId', title: 'Tweet ID', type: 'short-input', - placeholder: 'Enter tweet ID to read', - condition: { field: 'operation', value: 'x_read' }, - required: true, + placeholder: 'Enter tweet ID', + condition: { + field: 'operation', + value: [ + 'x_delete_tweet', + 'x_get_quote_tweets', + 'x_hide_reply', + 'x_manage_like', + 'x_manage_retweet', + 'x_get_liking_users', + 'x_get_retweeted_by', + 'x_create_bookmark', + 'x_delete_bookmark', + ], + }, + required: { + field: 'operation', + value: [ + 'x_delete_tweet', + 'x_get_quote_tweets', + 'x_hide_reply', + 'x_manage_like', + 'x_manage_retweet', + 'x_get_liking_users', + 'x_get_retweeted_by', + 'x_create_bookmark', + 'x_delete_bookmark', + ], + }, }, + // --- Hide Reply toggle --- { - id: 'includeReplies', - title: 'Include Replies', + id: 'hidden', + title: 'Hidden', type: 'dropdown', options: [ - { label: 'true', id: 'true' }, - { label: 'false', id: 'false' }, + { label: 'Hide', id: 'true' }, + { label: 'Unhide', id: 'false' }, ], - value: () => 'false', - condition: { field: 'operation', value: 'x_read' }, + value: () => 'true', + condition: { field: 'operation', value: 'x_hide_reply' }, }, + // --- Tweet IDs (batch lookup) --- { - id: 'query', - title: 'Search Query', + id: 'ids', + title: 'Tweet IDs', type: 'long-input', - placeholder: 'Enter search terms (supports X search operators)', - condition: { field: 'operation', value: 'x_search' }, + placeholder: 'Comma-separated tweet IDs (up to 100)', + condition: { field: 'operation', value: 'x_get_tweets_by_ids' }, required: true, }, + // --- Search query fields --- { - id: 'maxResults', - title: 'Max Results', - type: 'short-input', - placeholder: '10', - condition: { field: 'operation', value: 'x_search' }, + id: 'query', + title: 'Search Query', + type: 'long-input', + placeholder: 'Enter search terms (supports X search operators)', + condition: { field: 'operation', value: ['x_search_tweets', 'x_search_users'] }, + required: { field: 'operation', value: ['x_search_tweets', 'x_search_users'] }, }, { id: 'sortOrder', title: 'Sort Order', type: 'dropdown', options: [ - { label: 'recency', id: 'recency' }, - { label: 'relevancy', id: 'relevancy' }, + { label: 'Recent', id: 'recency' }, + { label: 'Relevant', id: 'relevancy' }, ], value: () => 'recency', - condition: { field: 'operation', value: 'x_search' }, + condition: { field: 'operation', value: 'x_search_tweets' }, + }, + // --- User ID field (shared by many operations) --- + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Enter user ID', + condition: { + field: 'operation', + value: [ + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_get_liked_tweets', + 'x_get_bookmarks', + 'x_create_bookmark', + 'x_delete_bookmark', + 'x_get_followers', + 'x_get_following', + 'x_get_blocking', + 'x_manage_follow', + 'x_manage_block', + 'x_manage_mute', + 'x_manage_like', + 'x_manage_retweet', + ], + }, + required: { + field: 'operation', + value: [ + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_get_liked_tweets', + 'x_get_bookmarks', + 'x_create_bookmark', + 'x_delete_bookmark', + 'x_get_followers', + 'x_get_following', + 'x_get_blocking', + 'x_manage_follow', + 'x_manage_block', + 'x_manage_mute', + 'x_manage_like', + 'x_manage_retweet', + ], + }, + }, + // --- Target User ID (for follow/block/mute) --- + { + id: 'targetUserId', + title: 'Target User ID', + type: 'short-input', + placeholder: 'Enter target user ID', + condition: { + field: 'operation', + value: ['x_manage_follow', 'x_manage_block', 'x_manage_mute'], + }, + required: { + field: 'operation', + value: ['x_manage_follow', 'x_manage_block', 'x_manage_mute'], + }, + }, + // --- Action dropdowns for manage operations --- + { + id: 'action', + title: 'Action', + type: 'dropdown', + options: [ + { label: 'Like', id: 'like' }, + { label: 'Unlike', id: 'unlike' }, + ], + value: () => 'like', + condition: { field: 'operation', value: 'x_manage_like' }, + }, + { + id: 'retweetAction', + title: 'Action', + type: 'dropdown', + options: [ + { label: 'Retweet', id: 'retweet' }, + { label: 'Unretweet', id: 'unretweet' }, + ], + value: () => 'retweet', + condition: { field: 'operation', value: 'x_manage_retweet' }, + }, + { + id: 'followAction', + title: 'Action', + type: 'dropdown', + options: [ + { label: 'Follow', id: 'follow' }, + { label: 'Unfollow', id: 'unfollow' }, + ], + value: () => 'follow', + condition: { field: 'operation', value: 'x_manage_follow' }, + }, + { + id: 'blockAction', + title: 'Action', + type: 'dropdown', + options: [ + { label: 'Block', id: 'block' }, + { label: 'Unblock', id: 'unblock' }, + ], + value: () => 'block', + condition: { field: 'operation', value: 'x_manage_block' }, + }, + { + id: 'muteAction', + title: 'Action', + type: 'dropdown', + options: [ + { label: 'Mute', id: 'mute' }, + { label: 'Unmute', id: 'unmute' }, + ], + value: () => 'mute', + condition: { field: 'operation', value: 'x_manage_mute' }, + }, + // --- Exclude filter (for user tweets/timeline) --- + { + id: 'exclude', + title: 'Exclude', + type: 'short-input', + placeholder: 'Comma-separated: retweets, replies', + condition: { + field: 'operation', + value: ['x_get_user_tweets', 'x_get_user_timeline'], + }, }, + // --- Time range fields (shared by tweet search and user tweet operations) --- { id: 'startTime', title: 'Start Time', type: 'short-input', placeholder: 'YYYY-MM-DDTHH:mm:ssZ', - condition: { field: 'operation', value: 'x_search' }, + condition: { + field: 'operation', + value: [ + 'x_search_tweets', + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + ], + }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp based on the user's description. @@ -138,7 +377,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'End Time', type: 'short-input', placeholder: 'YYYY-MM-DDTHH:mm:ssZ', - condition: { field: 'operation', value: 'x_search' }, + condition: { + field: 'operation', + value: [ + 'x_search_tweets', + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + ], + }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp based on the user's description. @@ -154,55 +401,149 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, generationType: 'timestamp', }, }, + // --- Max Results (shared by many operations) --- + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: '10', + condition: { + field: 'operation', + value: [ + 'x_search_tweets', + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_get_liked_tweets', + 'x_get_bookmarks', + 'x_get_quote_tweets', + 'x_get_liking_users', + 'x_get_retweeted_by', + 'x_search_users', + 'x_get_followers', + 'x_get_following', + 'x_get_blocking', + ], + }, + }, + // --- Pagination Token (shared by many operations) --- + { + id: 'paginationToken', + title: 'Pagination Token', + type: 'short-input', + placeholder: 'Token for next page', + condition: { + field: 'operation', + value: [ + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_get_liked_tweets', + 'x_get_bookmarks', + 'x_get_quote_tweets', + 'x_get_liking_users', + 'x_get_retweeted_by', + 'x_get_followers', + 'x_get_following', + 'x_get_blocking', + ], + }, + }, + // --- Next Token (for search operations that use nextToken instead of paginationToken) --- + { + id: 'nextToken', + title: 'Pagination Token', + type: 'short-input', + placeholder: 'Token for next page', + condition: { + field: 'operation', + value: ['x_search_tweets', 'x_search_users'], + }, + }, + // --- Trends fields --- { - id: 'username', - title: 'Username', + id: 'woeid', + title: 'WOEID', type: 'short-input', - placeholder: 'Enter username (without @)', - condition: { field: 'operation', value: 'x_user' }, + placeholder: '1 (worldwide), 23424977 (US)', + condition: { field: 'operation', value: 'x_get_trends_by_woeid' }, required: true, }, + { + id: 'maxTrends', + title: 'Max Trends', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: 'x_get_trends_by_woeid' }, + }, + // --- Usage fields --- + { + id: 'days', + title: 'Days', + type: 'short-input', + placeholder: '7 (1-90)', + condition: { field: 'operation', value: 'x_get_usage' }, + }, ], tools: { - access: ['x_write', 'x_read', 'x_search', 'x_user'], + access: [ + 'x_create_tweet', + 'x_delete_tweet', + 'x_search_tweets', + 'x_get_tweets_by_ids', + 'x_get_quote_tweets', + 'x_hide_reply', + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_manage_like', + 'x_manage_retweet', + 'x_get_liked_tweets', + 'x_get_liking_users', + 'x_get_retweeted_by', + 'x_get_bookmarks', + 'x_create_bookmark', + 'x_delete_bookmark', + 'x_get_me', + 'x_search_users', + 'x_get_followers', + 'x_get_following', + 'x_manage_follow', + 'x_manage_block', + 'x_get_blocking', + 'x_manage_mute', + 'x_get_trends_by_woeid', + 'x_get_personalized_trends', + 'x_get_usage', + ], config: { - tool: (params) => { - switch (params.operation) { - case 'x_write': - return 'x_write' - case 'x_read': - return 'x_read' - case 'x_search': - return 'x_search' - case 'x_user': - return 'x_user' - default: - return 'x_write' - } - }, + tool: (params) => params.operation, params: (params) => { const { oauthCredential, ...rest } = params - const parsedParams: Record = { + const parsedParams: Record = { credential: oauthCredential, } - Object.keys(rest).forEach((key) => { - const value = rest[key] + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue - if (value === 'true' || value === 'false') { - parsedParams[key] = value === 'true' - } else if (key === 'maxResults' && value) { + if (key === 'maxResults' || key === 'maxTrends' || key === 'days') { parsedParams[key] = Number.parseInt(value as string, 10) - } else if (key === 'mediaIds' && typeof value === 'string') { - parsedParams[key] = value - .split(',') - .map((id) => id.trim()) - .filter((id) => id !== '') + } else if (key === 'hidden') { + parsedParams[key] = value === 'true' + } else if (key === 'retweetAction') { + parsedParams.action = value + } else if (key === 'followAction') { + parsedParams.action = value + } else if (key === 'blockAction') { + parsedParams.action = value + } else if (key === 'muteAction') { + parsedParams.action = value } else { parsedParams[key] = value } - }) + } return parsedParams }, @@ -211,64 +552,207 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'X account credential' }, + // Tweet fields text: { type: 'string', description: 'Tweet text content' }, - replyTo: { type: 'string', description: 'Reply to tweet ID' }, - mediaIds: { type: 'string', description: 'Media identifiers' }, - poll: { type: 'json', description: 'Poll configuration' }, + replyToTweetId: { type: 'string', description: 'Tweet ID to reply to' }, + quoteTweetId: { type: 'string', description: 'Tweet ID to quote' }, + mediaIds: { type: 'string', description: 'Comma-separated media IDs' }, + replySettings: { type: 'string', description: 'Reply permission setting' }, tweetId: { type: 'string', description: 'Tweet identifier' }, - includeReplies: { type: 'boolean', description: 'Include replies' }, - query: { type: 'string', description: 'Search query terms' }, - maxResults: { type: 'number', description: 'Maximum search results' }, - startTime: { type: 'string', description: 'Search start time' }, - endTime: { type: 'string', description: 'Search end time' }, - sortOrder: { type: 'string', description: 'Result sort order' }, - username: { type: 'string', description: 'User profile name' }, - includeRecentTweets: { type: 'boolean', description: 'Include recent tweets' }, + ids: { type: 'string', description: 'Comma-separated tweet IDs' }, + hidden: { type: 'string', description: 'Hide or unhide reply' }, + // User fields + userId: { type: 'string', description: 'User identifier' }, + targetUserId: { type: 'string', description: 'Target user identifier' }, + // Action fields + action: { type: 'string', description: 'Action to perform (like/unlike, etc.)' }, + retweetAction: { type: 'string', description: 'Retweet action' }, + followAction: { type: 'string', description: 'Follow action' }, + blockAction: { type: 'string', description: 'Block action' }, + muteAction: { type: 'string', description: 'Mute action' }, + // Search/filter fields + query: { type: 'string', description: 'Search query' }, + sortOrder: { type: 'string', description: 'Sort order' }, + exclude: { type: 'string', description: 'Exclusion filter' }, + // Time/pagination fields + startTime: { type: 'string', description: 'Start time filter' }, + endTime: { type: 'string', description: 'End time filter' }, + maxResults: { type: 'number', description: 'Maximum results' }, + paginationToken: { type: 'string', description: 'Pagination token' }, + nextToken: { type: 'string', description: 'Next page token' }, + // Trends fields + woeid: { type: 'string', description: 'Where On Earth ID' }, + maxTrends: { type: 'number', description: 'Maximum trends to return' }, + // Usage fields + days: { type: 'number', description: 'Days of usage data' }, }, outputs: { - // Write and Read operation outputs - tweet: { - type: 'json', - description: 'Tweet data including contextAnnotations and publicMetrics', - condition: { field: 'operation', value: ['x_write', 'x_read'] }, + // Create Tweet outputs + id: { + type: 'string', + description: 'Created tweet ID', + condition: { field: 'operation', value: 'x_create_tweet' }, }, - // Read operation outputs - replies: { - type: 'json', - description: 'Tweet replies (when includeReplies is true)', - condition: { field: 'operation', value: 'x_read' }, + text: { + type: 'string', + description: 'Text of the created tweet', + condition: { field: 'operation', value: 'x_create_tweet' }, }, - context: { - type: 'json', - description: 'Tweet context (parent and quoted tweets)', - condition: { field: 'operation', value: 'x_read' }, + // Delete Tweet output + deleted: { + type: 'boolean', + description: 'Whether the tweet was deleted', + condition: { field: 'operation', value: 'x_delete_tweet' }, + }, + // Bookmark outputs + bookmarked: { + type: 'boolean', + description: 'Whether the tweet is bookmarked', + condition: { field: 'operation', value: ['x_create_bookmark', 'x_delete_bookmark'] }, + }, + // Hide Reply output + hidden: { + type: 'boolean', + description: 'Whether the reply is hidden', + condition: { field: 'operation', value: 'x_hide_reply' }, + }, + // Like output + liked: { + type: 'boolean', + description: 'Whether the tweet is liked', + condition: { field: 'operation', value: 'x_manage_like' }, }, - // Search operation outputs + // Retweet output + retweeted: { + type: 'boolean', + description: 'Whether the tweet is retweeted', + condition: { field: 'operation', value: 'x_manage_retweet' }, + }, + // Follow output + following: { + type: 'boolean', + description: 'Whether following the user', + condition: { field: 'operation', value: 'x_manage_follow' }, + }, + pendingFollow: { + type: 'boolean', + description: 'Whether a follow request is pending', + condition: { field: 'operation', value: 'x_manage_follow' }, + }, + // Block output + blocking: { + type: 'boolean', + description: 'Whether blocking the user', + condition: { field: 'operation', value: 'x_manage_block' }, + }, + // Mute output + muting: { + type: 'boolean', + description: 'Whether muting the user', + condition: { field: 'operation', value: 'x_manage_mute' }, + }, + // Tweet list outputs (shared by many operations) tweets: { type: 'json', - description: 'Tweets data including contextAnnotations and publicMetrics', - condition: { field: 'operation', value: 'x_search' }, + description: 'Array of tweets', + condition: { + field: 'operation', + value: [ + 'x_search_tweets', + 'x_get_tweets_by_ids', + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_get_liked_tweets', + 'x_get_bookmarks', + 'x_get_quote_tweets', + ], + }, }, - includes: { + // User list outputs + users: { type: 'json', - description: 'Additional data (users, media, polls)', - condition: { field: 'operation', value: 'x_search' }, + description: 'Array of users', + condition: { + field: 'operation', + value: [ + 'x_search_users', + 'x_get_followers', + 'x_get_following', + 'x_get_blocking', + 'x_get_liking_users', + 'x_get_retweeted_by', + ], + }, }, + // Single user output + user: { + type: 'json', + description: 'User profile data', + condition: { field: 'operation', value: 'x_get_me' }, + }, + // Pagination metadata meta: { type: 'json', - description: 'Response metadata', - condition: { field: 'operation', value: 'x_search' }, + description: 'Pagination metadata (resultCount, nextToken)', + condition: { + field: 'operation', + value: [ + 'x_search_tweets', + 'x_get_user_tweets', + 'x_get_user_mentions', + 'x_get_user_timeline', + 'x_get_liked_tweets', + 'x_get_bookmarks', + 'x_get_quote_tweets', + 'x_get_liking_users', + 'x_get_retweeted_by', + 'x_search_users', + 'x_get_followers', + 'x_get_following', + 'x_get_blocking', + ], + }, }, - // User operation outputs - user: { + // Trends outputs + trends: { type: 'json', - description: 'User profile data', - condition: { field: 'operation', value: 'x_user' }, + description: 'Array of trending topics', + condition: { + field: 'operation', + value: ['x_get_trends_by_woeid', 'x_get_personalized_trends'], + }, + }, + // Usage outputs + capResetDay: { + type: 'number', + description: 'Day of month when usage cap resets', + condition: { field: 'operation', value: 'x_get_usage' }, + }, + projectId: { + type: 'string', + description: 'Project identifier', + condition: { field: 'operation', value: 'x_get_usage' }, + }, + projectCap: { + type: 'number', + description: 'Monthly project usage cap', + condition: { field: 'operation', value: 'x_get_usage' }, + }, + projectUsage: { + type: 'number', + description: 'Current project usage count', + condition: { field: 'operation', value: 'x_get_usage' }, + }, + dailyProjectUsage: { + type: 'json', + description: 'Daily usage breakdown', + condition: { field: 'operation', value: 'x_get_usage' }, }, - recentTweets: { + dailyClientAppUsage: { type: 'json', - description: 'Recent tweets data', - condition: { field: 'operation', value: 'x_user' }, + description: 'Daily client app usage breakdown', + condition: { field: 'operation', value: 'x_get_usage' }, }, }, } diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 2b9a96aca8c..67f44a7342b 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -310,7 +310,23 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'x', icon: xIcon, baseProviderIcon: xIcon, - scopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'], + scopes: [ + 'tweet.read', + 'tweet.write', + 'tweet.moderate.write', + 'users.read', + 'follows.read', + 'follows.write', + 'bookmark.read', + 'bookmark.write', + 'like.read', + 'like.write', + 'block.read', + 'block.write', + 'mute.read', + 'mute.write', + 'offline.access', + ], }, }, defaultService: 'x', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d8302770c3d..9f245b64124 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2058,7 +2058,40 @@ import { wordpressUploadMediaTool, } from '@/tools/wordpress' import { workflowExecutorTool } from '@/tools/workflow' -import { xReadTool, xSearchTool, xUserTool, xWriteTool } from '@/tools/x' +import { + xCreateBookmarkTool, + xCreateTweetTool, + xDeleteBookmarkTool, + xDeleteTweetTool, + xGetBlockingTool, + xGetBookmarksTool, + xGetFollowersTool, + xGetFollowingTool, + xGetLikedTweetsTool, + xGetLikingUsersTool, + xGetMeTool, + xGetPersonalizedTrendsTool, + xGetQuoteTweetsTool, + xGetRetweetedByTool, + xGetTrendsByWoeidTool, + xGetTweetsByIdsTool, + xGetUsageTool, + xGetUserMentionsTool, + xGetUserTimelineTool, + xGetUserTweetsTool, + xHideReplyTool, + xManageBlockTool, + xManageFollowTool, + xManageLikeTool, + xManageMuteTool, + xManageRetweetTool, + xReadTool, + xSearchTool, + xSearchTweetsTool, + xSearchUsersTool, + xUserTool, + xWriteTool, +} from '@/tools/x' import { youtubeChannelInfoTool, youtubeChannelPlaylistsTool, @@ -2539,6 +2572,34 @@ export const tools: Record = { x_read: xReadTool, x_search: xSearchTool, x_user: xUserTool, + x_search_tweets: xSearchTweetsTool, + x_get_user_tweets: xGetUserTweetsTool, + x_get_user_mentions: xGetUserMentionsTool, + x_get_user_timeline: xGetUserTimelineTool, + x_get_tweets_by_ids: xGetTweetsByIdsTool, + x_get_bookmarks: xGetBookmarksTool, + x_create_bookmark: xCreateBookmarkTool, + x_delete_bookmark: xDeleteBookmarkTool, + x_create_tweet: xCreateTweetTool, + x_delete_tweet: xDeleteTweetTool, + x_get_me: xGetMeTool, + x_search_users: xSearchUsersTool, + x_get_followers: xGetFollowersTool, + x_get_following: xGetFollowingTool, + x_manage_follow: xManageFollowTool, + x_get_blocking: xGetBlockingTool, + x_manage_block: xManageBlockTool, + x_get_liked_tweets: xGetLikedTweetsTool, + x_get_liking_users: xGetLikingUsersTool, + x_manage_like: xManageLikeTool, + x_manage_retweet: xManageRetweetTool, + x_get_retweeted_by: xGetRetweetedByTool, + x_get_quote_tweets: xGetQuoteTweetsTool, + x_get_trends_by_woeid: xGetTrendsByWoeidTool, + x_get_personalized_trends: xGetPersonalizedTrendsTool, + x_get_usage: xGetUsageTool, + x_hide_reply: xHideReplyTool, + x_manage_mute: xManageMuteTool, pinecone_fetch: pineconeFetchTool, pinecone_generate_embeddings: pineconeGenerateEmbeddingsTool, pinecone_search_text: pineconeSearchTextTool, diff --git a/apps/sim/tools/x/create_bookmark.ts b/apps/sim/tools/x/create_bookmark.ts new file mode 100644 index 00000000000..4d0179fa363 --- /dev/null +++ b/apps/sim/tools/x/create_bookmark.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XCreateBookmarkParams, XCreateBookmarkResponse } from '@/tools/x/types' + +const logger = createLogger('XCreateBookmarkTool') + +export const xCreateBookmarkTool: ToolConfig = { + id: 'x_create_bookmark', + name: 'X Create Bookmark', + description: 'Bookmark a tweet for the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to bookmark', + }, + }, + + request: { + url: (params) => `https://api.x.com/2/users/${params.userId.trim()}/bookmarks`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + tweet_id: params.tweetId.trim(), + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Create Bookmark API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to bookmark tweet', + output: { + bookmarked: false, + }, + } + } + + return { + success: true, + output: { + bookmarked: data.data.bookmarked ?? false, + }, + } + }, + + outputs: { + bookmarked: { + type: 'boolean', + description: 'Whether the tweet was successfully bookmarked', + }, + }, +} diff --git a/apps/sim/tools/x/create_tweet.ts b/apps/sim/tools/x/create_tweet.ts new file mode 100644 index 00000000000..b1b6ef07a2c --- /dev/null +++ b/apps/sim/tools/x/create_tweet.ts @@ -0,0 +1,129 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XCreateTweetParams, XCreateTweetResponse } from '@/tools/x/types' + +const logger = createLogger('XCreateTweetTool') + +export const xCreateTweetTool: ToolConfig = { + id: 'x_create_tweet', + name: 'X Create Tweet', + description: 'Create a new tweet, reply, or quote tweet on X', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text content of the tweet (max 280 characters)', + }, + replyToTweetId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Tweet ID to reply to', + }, + quoteTweetId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Tweet ID to quote', + }, + mediaIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated media IDs to attach (up to 4)', + }, + replySettings: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Who can reply: "mentionedUsers", "following", "subscribers", or "verified"', + }, + }, + + request: { + url: 'https://api.x.com/2/tweets', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + text: params.text, + } + + if (params.replyToTweetId) { + body.reply = { in_reply_to_tweet_id: params.replyToTweetId.trim() } + } + + if (params.quoteTweetId) { + body.quote_tweet_id = params.quoteTweetId.trim() + } + + if (params.mediaIds) { + const ids = params.mediaIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + if (ids.length > 0) { + body.media = { media_ids: ids } + } + } + + if (params.replySettings) { + body.reply_settings = params.replySettings + } + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Create Tweet API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to create tweet', + output: { + id: '', + text: '', + }, + } + } + + return { + success: true, + output: { + id: data.data.id ?? '', + text: data.data.text ?? '', + }, + } + }, + + outputs: { + id: { + type: 'string', + description: 'The ID of the created tweet', + }, + text: { + type: 'string', + description: 'The text of the created tweet', + }, + }, +} diff --git a/apps/sim/tools/x/delete_bookmark.ts b/apps/sim/tools/x/delete_bookmark.ts new file mode 100644 index 00000000000..51d2421879a --- /dev/null +++ b/apps/sim/tools/x/delete_bookmark.ts @@ -0,0 +1,77 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XDeleteBookmarkParams, XDeleteBookmarkResponse } from '@/tools/x/types' + +const logger = createLogger('XDeleteBookmarkTool') + +export const xDeleteBookmarkTool: ToolConfig = { + id: 'x_delete_bookmark', + name: 'X Delete Bookmark', + description: "Remove a tweet from the authenticated user's bookmarks", + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to remove from bookmarks', + }, + }, + + request: { + url: (params) => + `https://api.x.com/2/users/${params.userId.trim()}/bookmarks/${params.tweetId.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Delete Bookmark API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to remove bookmark', + output: { + bookmarked: false, + }, + } + } + + return { + success: true, + output: { + bookmarked: data.data.bookmarked ?? false, + }, + } + }, + + outputs: { + bookmarked: { + type: 'boolean', + description: 'Whether the tweet is still bookmarked (should be false after deletion)', + }, + }, +} diff --git a/apps/sim/tools/x/delete_tweet.ts b/apps/sim/tools/x/delete_tweet.ts new file mode 100644 index 00000000000..8dd2263f24e --- /dev/null +++ b/apps/sim/tools/x/delete_tweet.ts @@ -0,0 +1,70 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XDeleteTweetParams, XDeleteTweetResponse } from '@/tools/x/types' + +const logger = createLogger('XDeleteTweetTool') + +export const xDeleteTweetTool: ToolConfig = { + id: 'x_delete_tweet', + name: 'X Delete Tweet', + description: 'Delete a tweet authored by the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the tweet to delete', + }, + }, + + request: { + url: (params) => `https://api.x.com/2/tweets/${params.tweetId.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Delete Tweet API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to delete tweet', + output: { + deleted: false, + }, + } + } + + return { + success: true, + output: { + deleted: data.data.deleted ?? false, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the tweet was successfully deleted', + }, + }, +} diff --git a/apps/sim/tools/x/get_blocking.ts b/apps/sim/tools/x/get_blocking.ts new file mode 100644 index 00000000000..267b96f6484 --- /dev/null +++ b/apps/sim/tools/x/get_blocking.ts @@ -0,0 +1,128 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetBlockingParams, XUserListResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetBlockingTool') + +export const xGetBlockingTool: ToolConfig = { + id: 'x_get_blocking', + name: 'X Get Blocking', + description: 'Get the list of users blocked by the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-1000)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'user.fields': 'created_at,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(1000, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/users/${params.userId.trim()}/blocking?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Blocking API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + users: [], + meta: { resultCount: 0, nextToken: null }, + }, + error: data.errors?.[0]?.detail ?? 'No blocked users found or invalid response', + } + } + + return { + success: true, + output: { + users: data.data.map(transformUser), + meta: { + resultCount: data.meta?.result_count ?? data.data.length, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of blocked user profiles', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_bookmarks.ts b/apps/sim/tools/x/get_bookmarks.ts new file mode 100644 index 00000000000..7e5b25d1f80 --- /dev/null +++ b/apps/sim/tools/x/get_bookmarks.ts @@ -0,0 +1,183 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetBookmarksParams, XTweetListResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetBookmarksTool') + +export const xGetBookmarksTool: ToolConfig = { + id: 'x_get_bookmarks', + name: 'X Get Bookmarks', + description: 'Get bookmarked tweets for the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page of results', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids', + 'tweet.fields': + 'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/users/${params.userId.trim()}/bookmarks?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Bookmarks API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No bookmarks found or invalid response', + output: { + tweets: [], + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + previousToken: null, + }, + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + previousToken: data.meta?.previous_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of bookmarked tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles', + optional: true, + properties: { + users: { + type: 'array', + description: 'Array of user objects referenced in tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + newestId: { type: 'string', description: 'ID of the newest tweet', optional: true }, + oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + previousToken: { type: 'string', description: 'Token for previous page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_followers.ts b/apps/sim/tools/x/get_followers.ts new file mode 100644 index 00000000000..8298158fbc3 --- /dev/null +++ b/apps/sim/tools/x/get_followers.ts @@ -0,0 +1,128 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetFollowersParams, XUserListResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetFollowersTool') + +export const xGetFollowersTool: ToolConfig = { + id: 'x_get_followers', + name: 'X Get Followers', + description: 'Get the list of followers for a user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID whose followers to retrieve', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-1000, default 100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'user.fields': 'created_at,description,profile_image_url,verified,public_metrics,location', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(1000, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/users/${params.userId.trim()}/followers?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Followers API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No followers found or invalid response', + output: { + users: [], + meta: { resultCount: 0, nextToken: null }, + }, + } + } + + return { + success: true, + output: { + users: data.data.map(transformUser), + meta: { + resultCount: data.meta?.result_count ?? data.data.length, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of follower user profiles', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_following.ts b/apps/sim/tools/x/get_following.ts new file mode 100644 index 00000000000..7a0fa041330 --- /dev/null +++ b/apps/sim/tools/x/get_following.ts @@ -0,0 +1,128 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetFollowingParams, XUserListResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetFollowingTool') + +export const xGetFollowingTool: ToolConfig = { + id: 'x_get_following', + name: 'X Get Following', + description: 'Get the list of users that a user is following', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID whose following list to retrieve', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-1000, default 100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'user.fields': 'created_at,description,profile_image_url,verified,public_metrics,location', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(1000, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/users/${params.userId.trim()}/following?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Following API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No following data found or invalid response', + output: { + users: [], + meta: { resultCount: 0, nextToken: null }, + }, + } + } + + return { + success: true, + output: { + users: data.data.map(transformUser), + meta: { + resultCount: data.meta?.result_count ?? data.data.length, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of users being followed', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_liked_tweets.ts b/apps/sim/tools/x/get_liked_tweets.ts new file mode 100644 index 00000000000..317ff97bf7d --- /dev/null +++ b/apps/sim/tools/x/get_liked_tweets.ts @@ -0,0 +1,131 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetLikedTweetsParams, XTweetListResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetLikedTweetsTool') + +export const xGetLikedTweetsTool: ToolConfig = { + id: 'x_get_liked_tweets', + name: 'X Get Liked Tweets', + description: 'Get tweets liked by a specific user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID whose liked tweets to retrieve', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (5-100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + expansions: 'author_id,attachments.media_keys', + 'tweet.fields': 'created_at,conversation_id,public_metrics,context_annotations', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(5, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/users/${params.userId.trim()}/liked_tweets?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Liked Tweets API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + tweets: [], + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + previousToken: null, + }, + }, + error: data.errors?.[0]?.detail ?? 'No liked tweets found or invalid response', + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + previousToken: data.meta?.previous_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of liked tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet content' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_liking_users.ts b/apps/sim/tools/x/get_liking_users.ts new file mode 100644 index 00000000000..5eca3afdf12 --- /dev/null +++ b/apps/sim/tools/x/get_liking_users.ts @@ -0,0 +1,128 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetLikingUsersParams, XUserListResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetLikingUsersTool') + +export const xGetLikingUsersTool: ToolConfig = { + id: 'x_get_liking_users', + name: 'X Get Liking Users', + description: 'Get the list of users who liked a specific tweet', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to get liking users for', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-100, default 100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'user.fields': 'created_at,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/tweets/${params.tweetId.trim()}/liking_users?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Liking Users API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + users: [], + meta: { resultCount: 0, nextToken: null }, + }, + error: data.errors?.[0]?.detail ?? 'No liking users found or invalid response', + } + } + + return { + success: true, + output: { + users: data.data.map(transformUser), + meta: { + resultCount: data.meta?.result_count ?? data.data.length, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of users who liked the tweet', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_me.ts b/apps/sim/tools/x/get_me.ts new file mode 100644 index 00000000000..5942c35dcbb --- /dev/null +++ b/apps/sim/tools/x/get_me.ts @@ -0,0 +1,88 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetMeParams, XGetMeResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetMeTool') + +export const xGetMeTool: ToolConfig = { + id: 'x_get_me', + name: 'X Get Me', + description: "Get the authenticated user's profile information", + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + }, + + request: { + url: () => { + const queryParams = new URLSearchParams({ + 'user.fields': + 'created_at,description,profile_image_url,verified,public_metrics,location,url', + }) + return `https://api.x.com/2/users/me?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Get Me API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to get authenticated user info', + output: { + user: {} as XGetMeResponse['output']['user'], + }, + } + } + + return { + success: true, + output: { + user: transformUser(data.data), + }, + } + }, + + outputs: { + user: { + type: 'object', + description: 'Authenticated user profile', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_personalized_trends.ts b/apps/sim/tools/x/get_personalized_trends.ts new file mode 100644 index 00000000000..ed5ba8ab4c7 --- /dev/null +++ b/apps/sim/tools/x/get_personalized_trends.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetPersonalizedTrendsParams, XPersonalizedTrendListResponse } from '@/tools/x/types' +import { transformPersonalizedTrend } from '@/tools/x/types' + +const logger = createLogger('XGetPersonalizedTrendsTool') + +export const xGetPersonalizedTrendsTool: ToolConfig< + XGetPersonalizedTrendsParams, + XPersonalizedTrendListResponse +> = { + id: 'x_get_personalized_trends', + name: 'X Get Personalized Trends', + description: 'Get personalized trending topics for the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + }, + + request: { + url: 'https://api.x.com/2/users/personalized_trends?personalized_trend.fields=category,post_count,trend_name,trending_since', + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Personalized Trends API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No personalized trends found or invalid response', + output: { + trends: [], + }, + } + } + + return { + success: true, + output: { + trends: data.data.map(transformPersonalizedTrend), + }, + } + }, + + outputs: { + trends: { + type: 'array', + description: 'Array of personalized trending topics', + items: { + type: 'object', + properties: { + trendName: { type: 'string', description: 'Name of the trending topic' }, + postCount: { + type: 'number', + description: 'Number of posts for this trend', + optional: true, + }, + category: { + type: 'string', + description: 'Category of the trend', + optional: true, + }, + trendingSince: { + type: 'string', + description: 'ISO 8601 timestamp of when the topic started trending', + optional: true, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_quote_tweets.ts b/apps/sim/tools/x/get_quote_tweets.ts new file mode 100644 index 00000000000..c432235c132 --- /dev/null +++ b/apps/sim/tools/x/get_quote_tweets.ts @@ -0,0 +1,152 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetQuoteTweetsParams, XTweetListResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetQuoteTweetsTool') + +export const xGetQuoteTweetsTool: ToolConfig = { + id: 'x_get_quote_tweets', + name: 'X Get Quote Tweets', + description: 'Get tweets that quote a specific tweet', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to get quote tweets for', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (10-100, default 10)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + expansions: 'author_id,attachments.media_keys', + 'tweet.fields': 'created_at,conversation_id,public_metrics,context_annotations', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(10, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/tweets/${params.tweetId.trim()}/quote_tweets?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Quote Tweets API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No quote tweets found or invalid response', + output: { + tweets: [], + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + previousToken: null, + }, + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + previousToken: data.meta?.previous_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of quote tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { + type: 'string', + description: 'Conversation thread ID', + optional: true, + }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_retweeted_by.ts b/apps/sim/tools/x/get_retweeted_by.ts new file mode 100644 index 00000000000..8e390ad6f48 --- /dev/null +++ b/apps/sim/tools/x/get_retweeted_by.ts @@ -0,0 +1,128 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetRetweetedByParams, XUserListResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetRetweetedByTool') + +export const xGetRetweetedByTool: ToolConfig = { + id: 'x_get_retweeted_by', + name: 'X Get Retweeted By', + description: 'Get the list of users who retweeted a specific tweet', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to get retweeters for', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-100, default 100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'user.fields': 'created_at,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + + return `https://api.x.com/2/tweets/${params.tweetId.trim()}/retweeted_by?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Retweeted By API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No retweeters found or invalid response', + output: { + users: [], + meta: { resultCount: 0, nextToken: null }, + }, + } + } + + return { + success: true, + output: { + users: data.data.map(transformUser), + meta: { + resultCount: data.meta?.result_count ?? data.data.length, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of users who retweeted the tweet', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_trends_by_woeid.ts b/apps/sim/tools/x/get_trends_by_woeid.ts new file mode 100644 index 00000000000..c9207c4b6eb --- /dev/null +++ b/apps/sim/tools/x/get_trends_by_woeid.ts @@ -0,0 +1,100 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetTrendsByWoeidParams, XTrendListResponse } from '@/tools/x/types' +import { transformTrend } from '@/tools/x/types' + +const logger = createLogger('XGetTrendsByWoeidTool') + +export const xGetTrendsByWoeidTool: ToolConfig = { + id: 'x_get_trends_by_woeid', + name: 'X Get Trends By WOEID', + description: + 'Get trending topics for a specific location by WOEID (e.g., 1 for worldwide, 23424977 for US)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + woeid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Yahoo Where On Earth ID (e.g., "1" for worldwide, "23424977" for US, "23424975" for UK)', + }, + maxTrends: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of trends to return (1-50, default 20)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'trend.fields': 'trend_name,tweet_count', + }) + + if (params.maxTrends) { + queryParams.append('max_trends', Number(params.maxTrends).toString()) + } + + return `https://api.x.com/2/trends/by/woeid/${params.woeid.trim()}?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Trends API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No trends found or invalid response', + output: { + trends: [], + }, + } + } + + return { + success: true, + output: { + trends: data.data.map(transformTrend), + }, + } + }, + + outputs: { + trends: { + type: 'array', + description: 'Array of trending topics', + items: { + type: 'object', + properties: { + trendName: { type: 'string', description: 'Name of the trending topic' }, + tweetCount: { + type: 'number', + description: 'Number of tweets for this trend', + optional: true, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_tweets_by_ids.ts b/apps/sim/tools/x/get_tweets_by_ids.ts new file mode 100644 index 00000000000..f5362da08ae --- /dev/null +++ b/apps/sim/tools/x/get_tweets_by_ids.ts @@ -0,0 +1,141 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetTweetsByIdsParams, XGetTweetsByIdsResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetTweetsByIdsTool') + +export const xGetTweetsByIdsTool: ToolConfig = { + id: 'x_get_tweets_by_ids', + name: 'X Get Tweets By IDs', + description: 'Look up multiple tweets by their IDs (up to 100)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + ids: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated tweet IDs (up to 100)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + ids: params.ids.trim(), + expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids', + 'tweet.fields': + 'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + return `https://api.x.com/2/tweets?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get Tweets By IDs API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No tweets found or invalid response', + output: { + tweets: [], + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of tweets matching the provided IDs', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles', + optional: true, + properties: { + users: { + type: 'array', + description: 'Array of user objects referenced in tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_usage.ts b/apps/sim/tools/x/get_usage.ts new file mode 100644 index 00000000000..def85b858b3 --- /dev/null +++ b/apps/sim/tools/x/get_usage.ts @@ -0,0 +1,151 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetUsageParams, XGetUsageResponse } from '@/tools/x/types' + +const logger = createLogger('XGetUsageTool') + +export const xGetUsageTool: ToolConfig = { + id: 'x_get_usage', + name: 'X Get Usage', + description: 'Get the API usage data for your X project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + days: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of days of usage data to return (1-90, default 7)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + 'usage.fields': + 'cap_reset_day,daily_client_app_usage,daily_project_usage,project_cap,project_id,project_usage', + }) + + if (params.days) { + queryParams.append('days', Number(params.days).toString()) + } + + return `https://api.x.com/2/usage/tweets?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Get Usage API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to get usage data', + output: { + capResetDay: null, + projectId: '', + projectCap: null, + projectUsage: null, + dailyProjectUsage: [], + dailyClientAppUsage: [], + }, + } + } + + return { + success: true, + output: { + capResetDay: data.data.cap_reset_day ?? null, + projectId: String(data.data.project_id ?? ''), + projectCap: data.data.project_cap ?? null, + projectUsage: data.data.project_usage ?? null, + dailyProjectUsage: (data.data.daily_project_usage?.usage ?? []).map( + (u: { date: string; usage: number }) => ({ + date: u.date, + usage: u.usage ?? 0, + }) + ), + dailyClientAppUsage: (data.data.daily_client_app_usage ?? []).map( + (app: { client_app_id: string; usage: { date: string; usage: number }[] }) => ({ + clientAppId: String(app.client_app_id ?? ''), + usage: (app.usage ?? []).map((u: { date: string; usage: number }) => ({ + date: u.date, + usage: u.usage ?? 0, + })), + }) + ), + }, + } + }, + + outputs: { + capResetDay: { + type: 'number', + description: 'Day of month when usage cap resets', + optional: true, + }, + projectId: { + type: 'string', + description: 'The project ID', + }, + projectCap: { + type: 'number', + description: 'The project tweet consumption cap', + optional: true, + }, + projectUsage: { + type: 'number', + description: 'Total tweets consumed in current period', + optional: true, + }, + dailyProjectUsage: { + type: 'array', + description: 'Daily project usage breakdown', + items: { + type: 'object', + properties: { + date: { type: 'string', description: 'Usage date in ISO 8601 format' }, + usage: { type: 'number', description: 'Number of tweets consumed' }, + }, + }, + }, + dailyClientAppUsage: { + type: 'array', + description: 'Daily per-app usage breakdown', + items: { + type: 'object', + properties: { + clientAppId: { type: 'string', description: 'Client application ID' }, + usage: { + type: 'array', + description: 'Daily usage entries for this app', + items: { + type: 'object', + properties: { + date: { type: 'string', description: 'Usage date in ISO 8601 format' }, + usage: { type: 'number', description: 'Number of tweets consumed' }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_user_mentions.ts b/apps/sim/tools/x/get_user_mentions.ts new file mode 100644 index 00000000000..7f7d115f1a4 --- /dev/null +++ b/apps/sim/tools/x/get_user_mentions.ts @@ -0,0 +1,211 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetUserMentionsParams, XTweetListResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetUserMentionsTool') + +export const xGetUserMentionsTool: ToolConfig = { + id: 'x_get_user_mentions', + name: 'X Get User Mentions', + description: 'Get tweets that mention a specific user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID whose mentions to retrieve', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (5-100, default 10)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page of results', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Oldest UTC timestamp in ISO 8601 format', + }, + endTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Newest UTC timestamp in ISO 8601 format', + }, + sinceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID greater than this', + }, + untilId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID less than this', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids', + 'tweet.fields': + 'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(5, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + if (params.startTime) queryParams.append('start_time', params.startTime) + if (params.endTime) queryParams.append('end_time', params.endTime) + if (params.sinceId) queryParams.append('since_id', params.sinceId) + if (params.untilId) queryParams.append('until_id', params.untilId) + + return `https://api.x.com/2/users/${params.userId.trim()}/mentions?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get User Mentions API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No mentions found or invalid response', + output: { + tweets: [], + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + previousToken: null, + }, + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + previousToken: data.meta?.previous_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of tweets mentioning the user', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles', + optional: true, + properties: { + users: { + type: 'array', + description: 'Array of user objects referenced in tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + newestId: { type: 'string', description: 'ID of the newest tweet', optional: true }, + oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + previousToken: { type: 'string', description: 'Token for previous page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_user_timeline.ts b/apps/sim/tools/x/get_user_timeline.ts new file mode 100644 index 00000000000..ebd0c34ef54 --- /dev/null +++ b/apps/sim/tools/x/get_user_timeline.ts @@ -0,0 +1,218 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetUserTimelineParams, XTweetListResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetUserTimelineTool') + +export const xGetUserTimelineTool: ToolConfig = { + id: 'x_get_user_timeline', + name: 'X Get User Timeline', + description: 'Get the reverse chronological home timeline for the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-100, default 10)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page of results', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Oldest UTC timestamp in ISO 8601 format', + }, + endTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Newest UTC timestamp in ISO 8601 format', + }, + sinceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID greater than this', + }, + untilId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID less than this', + }, + exclude: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated types to exclude: "retweets", "replies"', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids', + 'tweet.fields': + 'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + if (params.startTime) queryParams.append('start_time', params.startTime) + if (params.endTime) queryParams.append('end_time', params.endTime) + if (params.sinceId) queryParams.append('since_id', params.sinceId) + if (params.untilId) queryParams.append('until_id', params.untilId) + if (params.exclude) queryParams.append('exclude', params.exclude) + + return `https://api.x.com/2/users/${params.userId.trim()}/timelines/reverse_chronological?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get User Timeline API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No timeline data found or invalid response', + output: { + tweets: [], + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + previousToken: null, + }, + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + previousToken: data.meta?.previous_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of timeline tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles', + optional: true, + properties: { + users: { + type: 'array', + description: 'Array of user objects referenced in tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + newestId: { type: 'string', description: 'ID of the newest tweet', optional: true }, + oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + previousToken: { type: 'string', description: 'Token for previous page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/get_user_tweets.ts b/apps/sim/tools/x/get_user_tweets.ts new file mode 100644 index 00000000000..082cee921ad --- /dev/null +++ b/apps/sim/tools/x/get_user_tweets.ts @@ -0,0 +1,218 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XGetUserTweetsParams, XTweetListResponse } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XGetUserTweetsTool') + +export const xGetUserTweetsTool: ToolConfig = { + id: 'x_get_user_tweets', + name: 'X Get User Tweets', + description: 'Get tweets authored by a specific user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID whose tweets to retrieve', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (5-100, default 10)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page of results', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Oldest UTC timestamp in ISO 8601 format', + }, + endTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Newest UTC timestamp in ISO 8601 format', + }, + sinceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID greater than this', + }, + untilId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID less than this', + }, + exclude: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated types to exclude: "retweets", "replies"', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids', + 'tweet.fields': + 'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(5, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken) + if (params.startTime) queryParams.append('start_time', params.startTime) + if (params.endTime) queryParams.append('end_time', params.endTime) + if (params.sinceId) queryParams.append('since_id', params.sinceId) + if (params.untilId) queryParams.append('until_id', params.untilId) + if (params.exclude) queryParams.append('exclude', params.exclude) + + return `https://api.x.com/2/users/${params.userId.trim()}/tweets?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Get User Tweets API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No tweets found or invalid response', + output: { + tweets: [], + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + previousToken: null, + }, + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + previousToken: data.meta?.previous_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of tweets by the user', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles', + optional: true, + properties: { + users: { + type: 'array', + description: 'Array of user objects referenced in tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Pagination metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + newestId: { type: 'string', description: 'ID of the newest tweet', optional: true }, + oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true }, + nextToken: { type: 'string', description: 'Token for next page', optional: true }, + previousToken: { type: 'string', description: 'Token for previous page', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/hide_reply.ts b/apps/sim/tools/x/hide_reply.ts new file mode 100644 index 00000000000..ce1b8de2e27 --- /dev/null +++ b/apps/sim/tools/x/hide_reply.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XHideReplyParams, XHideReplyResponse } from '@/tools/x/types' + +const logger = createLogger('XHideReplyTool') + +export const xHideReplyTool: ToolConfig = { + id: 'x_hide_reply', + name: 'X Hide Reply', + description: 'Hide or unhide a reply to a tweet authored by the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The reply tweet ID to hide or unhide', + }, + hidden: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Set to true to hide the reply, false to unhide', + }, + }, + + request: { + url: (params) => `https://api.x.com/2/tweets/${params.tweetId.trim()}/hidden`, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + hidden: params.hidden, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Hide Reply API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to hide/unhide reply', + output: { + hidden: false, + }, + } + } + + return { + success: true, + output: { + hidden: data.data?.hidden ?? false, + }, + } + }, + + outputs: { + hidden: { + type: 'boolean', + description: 'Whether the reply is now hidden', + }, + }, +} diff --git a/apps/sim/tools/x/index.ts b/apps/sim/tools/x/index.ts index 3c29709a5bf..cfb0ee04c6f 100644 --- a/apps/sim/tools/x/index.ts +++ b/apps/sim/tools/x/index.ts @@ -7,3 +7,33 @@ export { xReadTool } export { xWriteTool } export { xSearchTool } export { xUserTool } + +export { xCreateBookmarkTool } from '@/tools/x/create_bookmark' +export { xCreateTweetTool } from '@/tools/x/create_tweet' +export { xDeleteBookmarkTool } from '@/tools/x/delete_bookmark' +export { xDeleteTweetTool } from '@/tools/x/delete_tweet' +export { xGetBlockingTool } from '@/tools/x/get_blocking' +export { xGetBookmarksTool } from '@/tools/x/get_bookmarks' +export { xGetFollowersTool } from '@/tools/x/get_followers' +export { xGetFollowingTool } from '@/tools/x/get_following' +export { xGetLikedTweetsTool } from '@/tools/x/get_liked_tweets' +export { xGetLikingUsersTool } from '@/tools/x/get_liking_users' +export { xGetMeTool } from '@/tools/x/get_me' +export { xGetPersonalizedTrendsTool } from '@/tools/x/get_personalized_trends' +export { xGetQuoteTweetsTool } from '@/tools/x/get_quote_tweets' +export { xGetRetweetedByTool } from '@/tools/x/get_retweeted_by' +export { xGetTrendsByWoeidTool } from '@/tools/x/get_trends_by_woeid' +export { xGetTweetsByIdsTool } from '@/tools/x/get_tweets_by_ids' +export { xGetUsageTool } from '@/tools/x/get_usage' +export { xGetUserMentionsTool } from '@/tools/x/get_user_mentions' +export { xGetUserTimelineTool } from '@/tools/x/get_user_timeline' +export { xGetUserTweetsTool } from '@/tools/x/get_user_tweets' +export { xHideReplyTool } from '@/tools/x/hide_reply' +export { xManageBlockTool } from '@/tools/x/manage_block' +export { xManageFollowTool } from '@/tools/x/manage_follow' +export { xManageLikeTool } from '@/tools/x/manage_like' +export { xManageMuteTool } from '@/tools/x/manage_mute' +export { xManageRetweetTool } from '@/tools/x/manage_retweet' +export { xSearchTweetsTool } from '@/tools/x/search_tweets' +export { xSearchUsersTool } from '@/tools/x/search_users' +export * from '@/tools/x/types' diff --git a/apps/sim/tools/x/manage_block.ts b/apps/sim/tools/x/manage_block.ts new file mode 100644 index 00000000000..759c88519ac --- /dev/null +++ b/apps/sim/tools/x/manage_block.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XManageBlockParams, XManageBlockResponse } from '@/tools/x/types' + +const logger = createLogger('XManageBlockTool') + +export const xManageBlockTool: ToolConfig = { + id: 'x_manage_block', + name: 'X Manage Block', + description: 'Block or unblock a user on X', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + targetUserId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to block or unblock', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action to perform: "block" or "unblock"', + }, + }, + + request: { + url: (params) => { + if (params.action === 'unblock') { + return `https://api.x.com/2/users/${params.userId.trim()}/blocking/${params.targetUserId.trim()}` + } + return `https://api.x.com/2/users/${params.userId.trim()}/blocking` + }, + method: (params) => (params.action === 'unblock' ? 'DELETE' : 'POST'), + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (params.action === 'unblock') return undefined + return { + target_user_id: params.targetUserId.trim(), + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Manage Block API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + blocking: false, + }, + error: data.errors?.[0]?.detail ?? 'Failed to manage block', + } + } + + return { + success: true, + output: { + blocking: data.data?.blocking ?? false, + }, + } + }, + + outputs: { + blocking: { + type: 'boolean', + description: 'Whether you are now blocking the user', + }, + }, +} diff --git a/apps/sim/tools/x/manage_follow.ts b/apps/sim/tools/x/manage_follow.ts new file mode 100644 index 00000000000..6c360a44747 --- /dev/null +++ b/apps/sim/tools/x/manage_follow.ts @@ -0,0 +1,99 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XManageFollowParams, XManageFollowResponse } from '@/tools/x/types' + +const logger = createLogger('XManageFollowTool') + +export const xManageFollowTool: ToolConfig = { + id: 'x_manage_follow', + name: 'X Manage Follow', + description: 'Follow or unfollow a user on X', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + targetUserId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to follow or unfollow', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action to perform: "follow" or "unfollow"', + }, + }, + + request: { + url: (params) => { + if (params.action === 'unfollow') { + return `https://api.x.com/2/users/${params.userId.trim()}/following/${params.targetUserId.trim()}` + } + return `https://api.x.com/2/users/${params.userId.trim()}/following` + }, + method: (params) => (params.action === 'unfollow' ? 'DELETE' : 'POST'), + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (params.action === 'unfollow') return undefined + return { + target_user_id: params.targetUserId.trim(), + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Manage Follow API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + following: false, + pendingFollow: false, + }, + error: data.errors?.[0]?.detail ?? 'Failed to manage follow', + } + } + + return { + success: true, + output: { + following: data.data?.following ?? false, + pendingFollow: data.data?.pending_follow ?? false, + }, + } + }, + + outputs: { + following: { + type: 'boolean', + description: 'Whether you are now following the user', + }, + pendingFollow: { + type: 'boolean', + description: 'Whether the follow request is pending (for protected accounts)', + }, + }, +} diff --git a/apps/sim/tools/x/manage_like.ts b/apps/sim/tools/x/manage_like.ts new file mode 100644 index 00000000000..4785b6939ce --- /dev/null +++ b/apps/sim/tools/x/manage_like.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XManageLikeParams, XManageLikeResponse } from '@/tools/x/types' + +const logger = createLogger('XManageLikeTool') + +export const xManageLikeTool: ToolConfig = { + id: 'x_manage_like', + name: 'X Manage Like', + description: 'Like or unlike a tweet on X', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to like or unlike', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action to perform: "like" or "unlike"', + }, + }, + + request: { + url: (params) => { + if (params.action === 'unlike') { + return `https://api.x.com/2/users/${params.userId.trim()}/likes/${params.tweetId.trim()}` + } + return `https://api.x.com/2/users/${params.userId.trim()}/likes` + }, + method: (params) => (params.action === 'unlike' ? 'DELETE' : 'POST'), + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (params.action === 'unlike') return undefined + return { + tweet_id: params.tweetId.trim(), + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Manage Like API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + liked: false, + }, + error: data.errors?.[0]?.detail ?? 'Failed to manage like', + } + } + + return { + success: true, + output: { + liked: data.data?.liked ?? false, + }, + } + }, + + outputs: { + liked: { + type: 'boolean', + description: 'Whether the tweet is now liked', + }, + }, +} diff --git a/apps/sim/tools/x/manage_mute.ts b/apps/sim/tools/x/manage_mute.ts new file mode 100644 index 00000000000..118e6ddc39b --- /dev/null +++ b/apps/sim/tools/x/manage_mute.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XManageMuteParams, XManageMuteResponse } from '@/tools/x/types' + +const logger = createLogger('XManageMuteTool') + +export const xManageMuteTool: ToolConfig = { + id: 'x_manage_mute', + name: 'X Manage Mute', + description: 'Mute or unmute a user on X', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + targetUserId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to mute or unmute', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action to perform: "mute" or "unmute"', + }, + }, + + request: { + url: (params) => { + if (params.action === 'unmute') { + return `https://api.x.com/2/users/${params.userId.trim()}/muting/${params.targetUserId.trim()}` + } + return `https://api.x.com/2/users/${params.userId.trim()}/muting` + }, + method: (params) => (params.action === 'unmute' ? 'DELETE' : 'POST'), + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (params.action === 'unmute') return undefined + return { + target_user_id: params.targetUserId.trim(), + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Manage Mute API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'Failed to mute/unmute user', + output: { + muting: false, + }, + } + } + + return { + success: true, + output: { + muting: data.data?.muting ?? false, + }, + } + }, + + outputs: { + muting: { + type: 'boolean', + description: 'Whether you are now muting the user', + }, + }, +} diff --git a/apps/sim/tools/x/manage_retweet.ts b/apps/sim/tools/x/manage_retweet.ts new file mode 100644 index 00000000000..266dd172726 --- /dev/null +++ b/apps/sim/tools/x/manage_retweet.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XManageRetweetParams, XManageRetweetResponse } from '@/tools/x/types' + +const logger = createLogger('XManageRetweetTool') + +export const xManageRetweetTool: ToolConfig = { + id: 'x_manage_retweet', + name: 'X Manage Retweet', + description: 'Retweet or unretweet a tweet on X', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The authenticated user ID', + }, + tweetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The tweet ID to retweet or unretweet', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action to perform: "retweet" or "unretweet"', + }, + }, + + request: { + url: (params) => { + if (params.action === 'unretweet') { + return `https://api.x.com/2/users/${params.userId.trim()}/retweets/${params.tweetId.trim()}` + } + return `https://api.x.com/2/users/${params.userId.trim()}/retweets` + }, + method: (params) => (params.action === 'unretweet' ? 'DELETE' : 'POST'), + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (params.action === 'unretweet') return undefined + return { + tweet_id: params.tweetId.trim(), + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data) { + logger.error('X Manage Retweet API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + output: { + retweeted: false, + }, + error: data.errors?.[0]?.detail ?? 'Failed to manage retweet', + } + } + + return { + success: true, + output: { + retweeted: data.data?.retweeted ?? false, + }, + } + }, + + outputs: { + retweeted: { + type: 'boolean', + description: 'Whether the tweet is now retweeted', + }, + }, +} diff --git a/apps/sim/tools/x/search_tweets.ts b/apps/sim/tools/x/search_tweets.ts new file mode 100644 index 00000000000..fd256c662b0 --- /dev/null +++ b/apps/sim/tools/x/search_tweets.ts @@ -0,0 +1,239 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XSearchTweetsParams, XTweet, XUser } from '@/tools/x/types' +import { transformTweet, transformUser } from '@/tools/x/types' + +const logger = createLogger('XSearchTweetsTool') + +interface XSearchTweetsResponse { + success: boolean + output: { + tweets: XTweet[] + includes?: { users: XUser[] } + meta: { + resultCount: number + newestId: string | null + oldestId: string | null + nextToken: string | null + } + } +} + +export const xSearchTweetsTool: ToolConfig = { + id: 'x_search_tweets', + name: 'X Search Tweets', + description: 'Search for recent tweets using keywords, hashtags, or advanced query operators', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query (supports operators like "from:", "to:", "#hashtag", "has:images", "is:retweet", "lang:")', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (10-100, default 10)', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Oldest UTC timestamp in ISO 8601 format (e.g., 2024-01-01T00:00:00Z)', + }, + endTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Newest UTC timestamp in ISO 8601 format', + }, + sinceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID greater than this', + }, + untilId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Returns tweets with ID less than this', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: "recency" or "relevancy"', + }, + nextToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page of results', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + query: params.query, + expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids', + 'tweet.fields': + 'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics', + 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', + }) + + if (params.maxResults) { + const max = Math.max(10, Math.min(100, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.startTime) queryParams.append('start_time', params.startTime) + if (params.endTime) queryParams.append('end_time', params.endTime) + if (params.sinceId) queryParams.append('since_id', params.sinceId) + if (params.untilId) queryParams.append('until_id', params.untilId) + if (params.sortOrder) queryParams.append('sort_order', params.sortOrder) + if (params.nextToken) queryParams.append('next_token', params.nextToken) + + return `https://api.x.com/2/tweets/search/recent?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Search Tweets API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: + data.errors?.[0]?.detail || + data.errors?.[0]?.title || + 'No results found or invalid response from X API', + output: { + tweets: [], + includes: { users: [] }, + meta: { + resultCount: 0, + newestId: null, + oldestId: null, + nextToken: null, + }, + }, + } + } + + return { + success: true, + output: { + tweets: data.data.map(transformTweet), + includes: { + users: data.includes?.users?.map(transformUser) ?? [], + }, + meta: { + resultCount: data.meta?.result_count ?? 0, + newestId: data.meta?.newest_id ?? null, + oldestId: data.meta?.oldest_id ?? null, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + tweets: { + type: 'array', + description: 'Array of tweets matching the search query', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet text content' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + inReplyToUserId: { + type: 'string', + description: 'User ID being replied to', + optional: true, + }, + publicMetrics: { + type: 'object', + description: 'Engagement metrics', + optional: true, + properties: { + retweetCount: { type: 'number', description: 'Number of retweets' }, + replyCount: { type: 'number', description: 'Number of replies' }, + likeCount: { type: 'number', description: 'Number of likes' }, + quoteCount: { type: 'number', description: 'Number of quotes' }, + }, + }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles', + optional: true, + properties: { + users: { + type: 'array', + description: 'Array of user objects referenced in tweets', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Search metadata including result count and pagination tokens', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + newestId: { type: 'string', description: 'ID of the newest tweet', optional: true }, + oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true }, + nextToken: { + type: 'string', + description: 'Pagination token for next page', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/search_users.ts b/apps/sim/tools/x/search_users.ts new file mode 100644 index 00000000000..f56bb6a75e4 --- /dev/null +++ b/apps/sim/tools/x/search_users.ts @@ -0,0 +1,133 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { XSearchUsersParams, XUserListResponse } from '@/tools/x/types' +import { transformUser } from '@/tools/x/types' + +const logger = createLogger('XSearchUsersTool') + +export const xSearchUsersTool: ToolConfig = { + id: 'x_search_users', + name: 'X Search Users', + description: 'Search for X users by name, username, or bio', + version: '1.0.0', + + oauth: { + required: true, + provider: 'x', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'X OAuth access token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search keyword (1-50 chars, matches name, username, or bio)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-1000, default 100)', + }, + nextToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams({ + query: params.query, + 'user.fields': 'created_at,description,profile_image_url,verified,public_metrics,location', + }) + + if (params.maxResults) { + const max = Math.max(1, Math.min(1000, Number(params.maxResults))) + queryParams.append('max_results', max.toString()) + } + if (params.nextToken) queryParams.append('next_token', params.nextToken) + + return `https://api.x.com/2/users/search?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.data || !Array.isArray(data.data)) { + logger.error('X Search Users API Error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.errors?.[0]?.detail || 'No users found or invalid response', + output: { + users: [], + meta: { resultCount: 0, nextToken: null }, + }, + } + } + + return { + success: true, + output: { + users: data.data.map(transformUser), + meta: { + resultCount: data.meta?.result_count ?? data.data.length, + nextToken: data.meta?.next_token ?? null, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of users matching the search query', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio', optional: true }, + profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + meta: { + type: 'object', + description: 'Search metadata', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + nextToken: { + type: 'string', + description: 'Pagination token for next page', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/x/types.ts b/apps/sim/tools/x/types.ts index ac4cf2b7017..2a20a9bd341 100644 --- a/apps/sim/tools/x/types.ts +++ b/apps/sim/tools/x/types.ts @@ -184,3 +184,339 @@ export const transformUser = (user: any): XUser => ({ tweetCount: user.public_metrics?.tweet_count || 0, }, }) + +/** + * Trend object from X API (WOEID trends) + */ +export interface XTrend { + trendName: string + tweetCount: number | null +} + +/** + * Personalized trend object from X API + */ +export interface XPersonalizedTrend { + trendName: string + postCount: number | null + category: string | null + trendingSince: string | null +} + +/** + * Transforms raw X API trend data (WOEID) into the XTrend format + */ +export const transformTrend = (trend: any): XTrend => ({ + trendName: trend.trend_name ?? trend.name ?? '', + tweetCount: trend.tweet_count ?? null, +}) + +/** + * Transforms raw X API personalized trend data into the XPersonalizedTrend format + */ +export const transformPersonalizedTrend = (trend: any): XPersonalizedTrend => ({ + trendName: trend.trend_name ?? '', + postCount: trend.post_count ?? null, + category: trend.category ?? null, + trendingSince: trend.trending_since ?? null, +}) + +// --- New Tool Parameter Interfaces --- + +export interface XSearchTweetsParams extends XBaseParams { + query: string + maxResults?: number + startTime?: string + endTime?: string + sinceId?: string + untilId?: string + sortOrder?: string + nextToken?: string +} + +export interface XGetUserTweetsParams extends XBaseParams { + userId: string + maxResults?: number + startTime?: string + endTime?: string + sinceId?: string + untilId?: string + exclude?: string + paginationToken?: string +} + +export interface XGetUserMentionsParams extends XBaseParams { + userId: string + maxResults?: number + startTime?: string + endTime?: string + sinceId?: string + untilId?: string + paginationToken?: string +} + +export interface XGetUserTimelineParams extends XBaseParams { + userId: string + maxResults?: number + startTime?: string + endTime?: string + sinceId?: string + untilId?: string + exclude?: string + paginationToken?: string +} + +export interface XGetTweetsByIdsParams extends XBaseParams { + ids: string +} + +export interface XGetTweetsByIdsResponse extends ToolResponse { + output: { + tweets: XTweet[] + } +} + +export interface XGetBookmarksParams extends XBaseParams { + userId: string + maxResults?: number + paginationToken?: string +} + +export interface XCreateBookmarkParams extends XBaseParams { + userId: string + tweetId: string +} + +export interface XCreateBookmarkResponse extends ToolResponse { + output: { + bookmarked: boolean + } +} + +export interface XDeleteBookmarkParams extends XBaseParams { + userId: string + tweetId: string +} + +export interface XDeleteBookmarkResponse extends ToolResponse { + output: { + bookmarked: boolean + } +} + +export interface XCreateTweetParams extends XBaseParams { + text: string + replyToTweetId?: string + quoteTweetId?: string + mediaIds?: string + replySettings?: string +} + +export interface XCreateTweetResponse extends ToolResponse { + output: { + id: string + text: string + } +} + +export interface XDeleteTweetParams extends XBaseParams { + tweetId: string +} + +export interface XDeleteTweetResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +export interface XGetMeParams extends XBaseParams {} + +export interface XGetMeResponse extends ToolResponse { + output: { + user: XUser + } +} + +export interface XSearchUsersParams extends XBaseParams { + query: string + maxResults?: number + nextToken?: string +} + +export interface XGetFollowersParams extends XBaseParams { + userId: string + maxResults?: number + paginationToken?: string +} + +export interface XGetFollowingParams extends XBaseParams { + userId: string + maxResults?: number + paginationToken?: string +} + +export interface XManageFollowParams extends XBaseParams { + userId: string + targetUserId: string + action: string +} + +export interface XManageFollowResponse extends ToolResponse { + output: { + following: boolean + pendingFollow: boolean + } +} + +export interface XGetBlockingParams extends XBaseParams { + userId: string + maxResults?: number + paginationToken?: string +} + +export interface XManageBlockParams extends XBaseParams { + userId: string + targetUserId: string + action: string +} + +export interface XManageBlockResponse extends ToolResponse { + output: { + blocking: boolean + } +} + +export interface XGetLikedTweetsParams extends XBaseParams { + userId: string + maxResults?: number + paginationToken?: string +} + +export interface XGetLikingUsersParams extends XBaseParams { + tweetId: string + maxResults?: number + paginationToken?: string +} + +export interface XManageLikeParams extends XBaseParams { + userId: string + tweetId: string + action: string +} + +export interface XManageLikeResponse extends ToolResponse { + output: { + liked: boolean + } +} + +export interface XManageRetweetParams extends XBaseParams { + userId: string + tweetId: string + action: string +} + +export interface XManageRetweetResponse extends ToolResponse { + output: { + retweeted: boolean + } +} + +export interface XGetRetweetedByParams extends XBaseParams { + tweetId: string + maxResults?: number + paginationToken?: string +} + +export interface XGetQuoteTweetsParams extends XBaseParams { + tweetId: string + maxResults?: number + paginationToken?: string +} + +export interface XGetTrendsByWoeidParams extends XBaseParams { + woeid: string + maxTrends?: number +} + +export interface XGetPersonalizedTrendsParams extends XBaseParams {} + +export interface XGetUsageParams extends XBaseParams { + days?: number +} + +export interface XGetUsageResponse extends ToolResponse { + output: { + capResetDay: number | null + projectId: string + projectCap: number | null + projectUsage: number | null + dailyProjectUsage: Array<{ date: string; usage: number }> + dailyClientAppUsage: Array<{ + clientAppId: string + usage: Array<{ date: string; usage: number }> + }> + } +} + +export interface XHideReplyParams extends XBaseParams { + tweetId: string + hidden: boolean +} + +export interface XHideReplyResponse extends ToolResponse { + output: { + hidden: boolean + } +} + +export interface XManageMuteParams extends XBaseParams { + userId: string + targetUserId: string + action: string +} + +export interface XManageMuteResponse extends ToolResponse { + output: { + muting: boolean + } +} + +// Common response types for list endpoints +export interface XTweetListResponse extends ToolResponse { + output: { + tweets: XTweet[] + includes?: { + users: XUser[] + } + meta: { + resultCount: number + newestId: string | null + oldestId: string | null + nextToken: string | null + previousToken: string | null + } + } +} + +export interface XUserListResponse extends ToolResponse { + output: { + users: XUser[] + meta: { + resultCount: number + nextToken: string | null + } + } +} + +export interface XTrendListResponse extends ToolResponse { + output: { + trends: XTrend[] + } +} + +export interface XPersonalizedTrendListResponse extends ToolResponse { + output: { + trends: XPersonalizedTrend[] + } +}