From 04bd41064384c6625df79442c6e42e4522989a40 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Sun, 16 Mar 2025 20:07:11 +0800 Subject: [PATCH] feat(twitter-services): timeline & profile support (#82) --- services/twitter-services/package.json | 5 +- .../src/adapters/mcp-adapter.ts | 1 + .../twitter-services/src/core/auth-service.ts | 34 ++++++++++- .../src/core/twitter-service.ts | 58 +++++++++++++++++-- services/twitter-services/src/main.ts | 2 +- .../src/parsers/tweet-parser.ts | 6 +- 6 files changed, 94 insertions(+), 12 deletions(-) diff --git a/services/twitter-services/package.json b/services/twitter-services/package.json index 054416436..4eefa9007 100644 --- a/services/twitter-services/package.json +++ b/services/twitter-services/package.json @@ -6,8 +6,9 @@ "author": "RainbowBird ", "license": "MIT", "scripts": { - "dev": "playwright install chromium && tsx src/main.ts", - "mcp:ui": "pnpx @modelcontextprotocol/inspector" + "dev": "tsx src/main.ts", + "mcp:ui": "pnpx @modelcontextprotocol/inspector", + "preinstall": "playwright install chromium" }, "dependencies": { "@browserbasehq/stagehand": "^1.14.0", diff --git a/services/twitter-services/src/adapters/mcp-adapter.ts b/services/twitter-services/src/adapters/mcp-adapter.ts index c1fb4e9a4..4d5f9a289 100644 --- a/services/twitter-services/src/adapters/mcp-adapter.ts +++ b/services/twitter-services/src/adapters/mcp-adapter.ts @@ -74,6 +74,7 @@ export class MCPAdapter { }) logger.mcp.withField('tweetCount', tweets.length).debug('Successfully retrieved timeline tweets') + logger.mcp.withFields({ tweets }).debug('Tweets') return { contents: tweets.map(tweet => ({ diff --git a/services/twitter-services/src/core/auth-service.ts b/services/twitter-services/src/core/auth-service.ts index f1c103dc9..a05373c5a 100644 --- a/services/twitter-services/src/core/auth-service.ts +++ b/services/twitter-services/src/core/auth-service.ts @@ -126,7 +126,7 @@ export class TwitterAuthService { /** * Verify if login was successful */ - private async verifyLogin(): Promise { + private async verifyLoginWithSelectors(): Promise { try { // Try multiple selectors to determine login status // First check for timeline which is definitive proof of being logged in @@ -202,12 +202,42 @@ export class TwitterAuthService { } } + /** + * Verify login with cookies + */ + private async verifyLogin(): Promise { + try { + // Convert cookies object to array format required by setCookies + const cookies = await this.exportCookies('object') + const cookieArray = Object.entries(cookies).map(([name, value]) => ({ + name, + value, + domain: '.x.com', + path: '/', + })) + // Set cookies in the browser + await this.page.context().addCookies(cookieArray) + + // Check for auth_token cookie which indicates login state + const authCookie = cookieArray.find(cookie => cookie.name === 'auth_token') + if (!authCookie) { + logger.auth.warn('No auth_token cookie found') + return false + } + + return true + } + catch (error) { + logger.auth.withError(error as Error).error('Error verifying login with cookies') + return false + } + } + /** * Check current login status */ async checkLoginStatus(): Promise { try { - await this.page.goto('https://x.com/home') const isLoggedIn = await this.verifyLogin() // If already logged in, update state and automatically save session diff --git a/services/twitter-services/src/core/twitter-service.ts b/services/twitter-services/src/core/twitter-service.ts index d7f91c0c1..b324c31ed 100644 --- a/services/twitter-services/src/core/twitter-service.ts +++ b/services/twitter-services/src/core/twitter-service.ts @@ -1,3 +1,4 @@ +import type { Page } from 'playwright' import type { PostOptions, SearchOptions, TimelineOptions, Tweet, TweetDetail, UserProfile } from '../types/twitter' import type { TwitterAuthService } from './auth-service' import type { TwitterTimelineService } from './timeline-service' @@ -8,10 +9,12 @@ export class TwitterService { private authService: TwitterAuthService private timelineService: TwitterTimelineService private sessionMonitorInterval: NodeJS.Timeout | null = null + private page: Page - constructor(authService: TwitterAuthService, timelineService: TwitterTimelineService) { + constructor(page: Page, authService: TwitterAuthService, timelineService: TwitterTimelineService) { this.authService = authService this.timelineService = timelineService + this.page = page } /** @@ -75,10 +78,57 @@ export class TwitterService { } /** - * Get user profile + * Get user profile information for a Twitter user + * @param username Twitter username to fetch profile for + * @returns Promise resolving to user profile data */ - async getUserProfile(_username: string): Promise { - throw new Error('Get user profile feature not yet implemented') + async getUserProfile(username: string): Promise { + this.ensureAuthenticated() + + try { + // Navigate to user profile page + await this.page.goto(`https://twitter.com/${username}`) + + // Wait for profile elements to load + await this.page.waitForSelector('[data-testid="UserName"]') + + // Get display name + const displayNameElement = await this.page.$('[data-testid="UserName"] div span') + const displayName = displayNameElement ? await displayNameElement.textContent() || username : username + + // Get bio + const bioElement = await this.page.$('[data-testid="UserDescription"]') + const bio = bioElement ? await bioElement.textContent() : undefined + + // Get avatar URL + const avatarElement = await this.page.$('img[src*="/profile_images/"]') + const avatarUrl = avatarElement ? await avatarElement.getAttribute('src') : undefined + + // Get follower/following counts + const followElement = await this.page.$('[href$="/followers"]') + const followingElement = await this.page.$('[href$="/following"]') + + const followerCount = followElement + ? Number.parseInt((await followElement.textContent() || '0').replace(/\D/g, '')) + : undefined + + const followingCount = followingElement + ? Number.parseInt((await followingElement.textContent() || '0').replace(/\D/g, '')) + : undefined + + return { + username, + displayName, + bio: bio || undefined, + avatarUrl: avatarUrl || undefined, + followersCount: followerCount || undefined, + followingCount: followingCount || undefined, + } + } + catch (error) { + logger.main.error('Error fetching user profile:', (error as Error).message) + throw new Error(`Failed to fetch profile for @${username}`) + } } /** diff --git a/services/twitter-services/src/main.ts b/services/twitter-services/src/main.ts index 1f338d75a..33433d128 100644 --- a/services/twitter-services/src/main.ts +++ b/services/twitter-services/src/main.ts @@ -42,7 +42,7 @@ async function initBrowser(config: Config): Promise<{ browser: Browser, context: async function initTwitterService(page: Page, context: BrowserContext, _config: Config): Promise { const authService = new TwitterAuthService(page, context) const timelineService = new TwitterTimelineService(page) - const twitterService = new TwitterService(authService, timelineService) + const twitterService = new TwitterService(page, authService, timelineService) // Check if we have a saved session try { diff --git a/services/twitter-services/src/parsers/tweet-parser.ts b/services/twitter-services/src/parsers/tweet-parser.ts index 1fb7d0b87..daade42f6 100644 --- a/services/twitter-services/src/parsers/tweet-parser.ts +++ b/services/twitter-services/src/parsers/tweet-parser.ts @@ -133,9 +133,9 @@ export class TweetParser { const displayName = displayNameElement ? await displayNameElement.textContent() || 'Unknown User' : 'Unknown User' // Get username - const usernameElement = await authorElement.$('span a[href^="/"]') - let username = usernameElement ? await usernameElement.textContent() : 'unknown' - username = username?.replace('@', '') || 'unknown' + const usernameElement = await authorElement.$('a[href^="/"]') + let username = usernameElement ? await usernameElement.getAttribute('href') : 'unknown' + username = username?.replace('/', '') || 'unknown' // Get avatar URL const avatarElement = await tweetElement.$('img[src*="/profile_images/"]')