Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 47 additions & 20 deletions apps/sim/blocks/blocks/x.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,32 +145,23 @@ export const XBlock: BlockConfig<XResponse> = {
params: (params) => {
const { credential, ...rest } = params

// Convert string values to appropriate types
const parsedParams: Record<string, any> = {
credential: credential,
}

// Add other params
Object.keys(rest).forEach((key) => {
const value = rest[key]

// Convert string boolean values to actual booleans
if (value === 'true' || value === 'false') {
parsedParams[key] = value === 'true'
}
// Convert numeric strings to numbers where appropriate
else if (key === 'maxResults' && value) {
} else if (key === 'maxResults' && value) {
parsedParams[key] = Number.parseInt(value as string, 10)
}
// Handle mediaIds conversion from comma-separated string to array
else if (key === 'mediaIds' && typeof value === 'string') {
} else if (key === 'mediaIds' && typeof value === 'string') {
parsedParams[key] = value
.split(',')
.map((id) => id.trim())
.filter((id) => id !== '')
}
// Keep other values as is
else {
} else {
parsedParams[key] = value
}
})
Expand All @@ -197,13 +188,49 @@ export const XBlock: BlockConfig<XResponse> = {
includeRecentTweets: { type: 'boolean', description: 'Include recent tweets' },
},
outputs: {
tweet: { type: 'json', description: 'Tweet data' },
replies: { type: 'json', description: 'Tweet replies' },
context: { type: 'json', description: 'Tweet context' },
tweets: { type: 'json', description: 'Tweets data' },
includes: { type: 'json', description: 'Additional data' },
meta: { type: 'json', description: 'Response metadata' },
user: { type: 'json', description: 'User profile data' },
recentTweets: { type: 'json', description: 'Recent tweets data' },
// Write and Read operation outputs
tweet: {
type: 'json',
description: 'Tweet data including contextAnnotations and publicMetrics',
condition: { field: 'operation', value: ['x_write', 'x_read'] },
},
// Read operation outputs
replies: {
type: 'json',
description: 'Tweet replies (when includeReplies is true)',
condition: { field: 'operation', value: 'x_read' },
},
context: {
type: 'json',
description: 'Tweet context (parent and quoted tweets)',
condition: { field: 'operation', value: 'x_read' },
},
// Search operation outputs
tweets: {
type: 'json',
description: 'Tweets data including contextAnnotations and publicMetrics',
condition: { field: 'operation', value: 'x_search' },
},
includes: {
type: 'json',
description: 'Additional data (users, media, polls)',
condition: { field: 'operation', value: 'x_search' },
},
meta: {
type: 'json',
description: 'Response metadata',
condition: { field: 'operation', value: 'x_search' },
},
// User operation outputs
user: {
type: 'json',
description: 'User profile data',
condition: { field: 'operation', value: 'x_user' },
},
recentTweets: {
type: 'json',
description: 'Recent tweets data',
condition: { field: 'operation', value: 'x_user' },
},
},
}
110 changes: 90 additions & 20 deletions apps/sim/tools/x/read.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
import type { XReadParams, XReadResponse, XTweet } from '@/tools/x/types'
import { transformTweet } from '@/tools/x/types'

const logger = createLogger('XReadTool')

export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
id: 'x_read',
Expand Down Expand Up @@ -39,11 +43,36 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
'author_id',
'in_reply_to_user_id',
'referenced_tweets.id',
'referenced_tweets.id.author_id',
'attachments.media_keys',
'attachments.poll_ids',
].join(',')

return `https://api.twitter.com/2/tweets/${params.tweetId}?expansions=${expansions}`
const tweetFields = [
'created_at',
'conversation_id',
'in_reply_to_user_id',
'attachments',
'context_annotations',
'public_metrics',
].join(',')

const userFields = [
'name',
'username',
'description',
'profile_image_url',
'verified',
'public_metrics',
].join(',')

const queryParams = new URLSearchParams({
expansions,
'tweet.fields': tweetFields,
'user.fields': userFields,
})

return `https://api.twitter.com/2/tweets/${params.tweetId}?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Expand All @@ -52,47 +81,88 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
}),
},

transformResponse: async (response) => {
transformResponse: async (response, params) => {
const data = await response.json()

const transformTweet = (tweet: any): XTweet => ({
id: tweet.id,
text: tweet.text,
createdAt: tweet.created_at,
authorId: tweet.author_id,
conversationId: tweet.conversation_id,
inReplyToUserId: tweet.in_reply_to_user_id,
attachments: {
mediaKeys: tweet.attachments?.media_keys,
pollId: tweet.attachments?.poll_ids?.[0],
},
})
if (data.errors && !data.data) {
logger.error('X Read API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || data.errors?.[0]?.message || 'Failed to fetch tweet',
output: {
tweet: {} as XTweet,
},
}
}

const mainTweet = transformTweet(data.data)
const context: { parentTweet?: XTweet; rootTweet?: XTweet } = {}

// Get parent and root tweets if available
if (data.includes?.tweets) {
const referencedTweets = data.data.referenced_tweets || []
const parentTweetRef = referencedTweets.find((ref: any) => ref.type === 'replied_to')
const rootTweetRef = referencedTweets.find((ref: any) => ref.type === 'replied_to_root')
const quotedTweetRef = referencedTweets.find((ref: any) => ref.type === 'quoted')

if (parentTweetRef) {
const parentTweet = data.includes.tweets.find((t: any) => t.id === parentTweetRef.id)
if (parentTweet) context.parentTweet = transformTweet(parentTweet)
}

if (rootTweetRef) {
const rootTweet = data.includes.tweets.find((t: any) => t.id === rootTweetRef.id)
if (rootTweet) context.rootTweet = transformTweet(rootTweet)
if (!parentTweetRef && quotedTweetRef) {
const quotedTweet = data.includes.tweets.find((t: any) => t.id === quotedTweetRef.id)
if (quotedTweet) context.rootTweet = transformTweet(quotedTweet)
}
}

let replies: XTweet[] = []
if (params?.includeReplies && mainTweet.id) {
try {
const repliesExpansions = ['author_id', 'referenced_tweets.id'].join(',')
const repliesTweetFields = [
'created_at',
'conversation_id',
'in_reply_to_user_id',
'public_metrics',
].join(',')

const conversationId = mainTweet.conversationId || mainTweet.id
const searchQuery = `conversation_id:${conversationId}`
const searchParams = new URLSearchParams({
query: searchQuery,
expansions: repliesExpansions,
'tweet.fields': repliesTweetFields,
max_results: '100', // Max allowed
})

const repliesResponse = await fetch(
`https://api.twitter.com/2/tweets/search/recent?${searchParams.toString()}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${params?.accessToken || ''}`,
'Content-Type': 'application/json',
},
}
)

const repliesData = await repliesResponse.json()

if (repliesData.data && Array.isArray(repliesData.data)) {
replies = repliesData.data
.filter((tweet: any) => tweet.id !== mainTweet.id)
.map(transformTweet)
}
} catch (error) {
logger.warn('Failed to fetch replies:', error)
}
}

return {
success: true,
output: {
tweet: mainTweet,
context,
replies: replies.length > 0 ? replies : undefined,
context: Object.keys(context).length > 0 ? context : undefined,
},
}
},
Expand Down
34 changes: 4 additions & 30 deletions apps/sim/tools/x/search.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
import type { XSearchParams, XSearchResponse, XTweet, XUser } from '@/tools/x/types'
import type { XSearchParams, XSearchResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'

const logger = createLogger('XSearchTool')

Expand Down Expand Up @@ -67,7 +68,8 @@ export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
const queryParams = new URLSearchParams({
query,
expansions,
'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,attachments',
'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',
})

Expand All @@ -92,7 +94,6 @@ export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
transformResponse: async (response) => {
const data = await response.json()

// Check if data.data is undefined/null or not an array
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Search API Error:', JSON.stringify(data, null, 2))
return {
Expand All @@ -118,33 +119,6 @@ export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
}
}

const transformTweet = (tweet: any): XTweet => ({
id: tweet.id,
text: tweet.text,
createdAt: tweet.created_at,
authorId: tweet.author_id,
conversationId: tweet.conversation_id,
inReplyToUserId: tweet.in_reply_to_user_id,
attachments: {
mediaKeys: tweet.attachments?.media_keys,
pollId: tweet.attachments?.poll_ids?.[0],
},
})

const transformUser = (user: any): XUser => ({
id: user.id,
username: user.username,
name: user.name,
description: user.description,
profileImageUrl: user.profile_image_url,
verified: user.verified,
metrics: {
followersCount: user.public_metrics.followers_count,
followingCount: user.public_metrics.following_count,
tweetCount: user.public_metrics.tweet_count,
},
})

return {
success: true,
output: {
Expand Down
Loading