feat(twitter-services): timeline & profile support (#82)

This commit is contained in:
RainbowBird
2025-03-16 20:07:11 +08:00
committed by GitHub
parent 3ef246a9ab
commit 04bd410643
6 changed files with 94 additions and 12 deletions
+3 -2
View File
@@ -6,8 +6,9 @@
"author": "RainbowBird <rbxin2003@outlook.com>",
"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",
@@ -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 => ({
@@ -126,7 +126,7 @@ export class TwitterAuthService {
/**
* Verify if login was successful
*/
private async verifyLogin(): Promise<boolean> {
private async verifyLoginWithSelectors(): Promise<boolean> {
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<boolean> {
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<boolean> {
try {
await this.page.goto('https://x.com/home')
const isLoggedIn = await this.verifyLogin()
// If already logged in, update state and automatically save session
@@ -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<UserProfile> {
throw new Error('Get user profile feature not yet implemented')
async getUserProfile(username: string): Promise<UserProfile> {
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}`)
}
}
/**
+1 -1
View File
@@ -42,7 +42,7 @@ async function initBrowser(config: Config): Promise<{ browser: Browser, context:
async function initTwitterService(page: Page, context: BrowserContext, _config: Config): Promise<TwitterService> {
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 {
@@ -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/"]')