feat(twitter-services): timeline & profile support (#82)
This commit is contained in:
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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/"]')
|
||||
|
||||
Reference in New Issue
Block a user