diff --git a/services/twitter_service.go b/services/twitter_service.go index 3392dc2..a5609f7 100644 --- a/services/twitter_service.go +++ b/services/twitter_service.go @@ -2,17 +2,22 @@ package services import ( "context" + "encoding/json" "errors" "fmt" "go-account-register/libs" "go-account-register/models" "go-account-register/utils" "log" + "math/rand" + "os" "strconv" + "strings" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" + "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/proto" ) @@ -34,17 +39,18 @@ func (*TwitterService) Login(id int) *libs.ErrorInfo { // log.Println("获取cookie", cookies) // b, _ := json.MarshalIndent(cookies, "", " ") // log.Println(string(b)) - var token string - for _, v := range user.Cookies { - if v.Name == "auth_token" { - token = v.Value - } - } - log.Println("当前账号Token", token) + + // var token string + // for _, v := range user.Cookies { + // if v.Name == "auth_token" { + // token = v.Value + // } + // } + // log.Println("当前账号Token", token) cookies := []*proto.NetworkCookieParam{ { Name: "auth_token", - Value: token, + Value: user.Token, Domain: ".x.com", Path: "/", Expires: proto.TimeSinceEpoch(time.Now().Add(24 * time.Hour).Unix()), // 过期时间 @@ -52,6 +58,7 @@ func (*TwitterService) Login(id int) *libs.ErrorInfo { Secure: true, // 仅 HTTPS 传输 }, } + log.Println("Cookie", cookies[0].Value) // 全局设置 browser.SetCookies(cookies) page.MustNavigate("https://x.com/home").MustWaitLoad() @@ -102,15 +109,73 @@ func (*TwitterService) Login(id int) *libs.ErrorInfo { if err != nil { fmt.Println(err) } - + getAccountInfo(page, int64(id)) return libs.ErrorCode["LoginSuccessful"] } +// 获取账号信息 +func getAccountInfo(page *rod.Page, id int64) { + profileButton := page.Timeout(10 * time.Second).MustElement(`a[data-testid="AppTabBar_Profile_Link"]`) + profileButton.MustClick() + // 等待新页面加载完成 + page.MustWaitLoad() + res := getApiResponse(page, "/UserByScreenName") + var dataBody map[string]interface{} + json.Unmarshal([]byte(res), &dataBody) + outerMap := dataBody["data"].(map[string]interface{}) + userMap := outerMap["user"].(map[string]interface{}) + resultMap := userMap["result"].(map[string]interface{}) + + // 获取头像 + avatarMap := resultMap["avatar"].(map[string]interface{}) + avatarImageURL := avatarMap["image_url"].(string) + legacyMap := resultMap["legacy"].(map[string]interface{}) + // 获取背景图 + backdropImgUrl := legacyMap["profile_banner_url"] + var backdropUrl string + if backdropImgUrl == nil { + backdropUrl = "" + } else { + backdropUrl = legacyMap["profile_banner_url"].(string) + } + + // 获取昵称 + coreMap := resultMap["core"].(map[string]interface{}) + nickname := coreMap["name"].(string) + screenName := coreMap["screen_name"].(string) + // 获取自我介绍 + bio := legacyMap["description"].(string) + + // 获取自己设置的地理位置 + locationObj := resultMap["location"].(map[string]interface{}) + location := locationObj["location"].(string) + // 获取粉丝数量 + followersCount := legacyMap["followers_count"].(float64) + // 获取关注数量 + friendsCount := legacyMap["friends_count"].(float64) + // 获取发布的帖子数量 + mediaCount := legacyMap["media_count"].(float64) + log.Println("头像地址", avatarImageURL) + log.Println("背景图地址", backdropUrl) + log.Println("昵称", nickname) + log.Println("用户名", screenName) + log.Println("自我介绍", bio) + log.Println("地理位置", location) + log.Println("粉丝数量", int64(followersCount)) + log.Println("关注数量", int64(friendsCount)) + log.Println("发布的帖子数量", int64(mediaCount)) + userService := InitTwitterAccountService() + userService.Update(id, &models.TwitterAccount{ + Uid: screenName, + }) +} + // 退出登录 func (*TwitterService) Logout(id int) { Browser.CancelBrowser(id) } +// 注册账号 func (bit *TwitterService) Register() *libs.ErrorInfo { // 随机获取代理 proxyService := InitProxyService() @@ -278,3 +343,369 @@ func (bit *TwitterService) Register() *libs.ErrorInfo { return libs.ErrorCode["RegisterSuccessful"] } + +// 发推 +func (*TwitterService) SendPost(accountId int, Content string, Img []string) *libs.ErrorInfo { + // 获取账号状态 + UserService := InitTwitterAccountService() + user := UserService.GetInfo(int64(accountId)) + if user.LoginStatus != "登录成功" { + return libs.ErrorCode["AccountHasBeenTakenOffline"] + } + // 获取实例 + obj := Browser.GetBrowser(accountId, &libs.Fingerprint{ + Proxy: user.Proxy, + }) + page := obj.Page + // 跳转到首页 + page.MustNavigate("https://x.com/home").MustWaitLoad() + + err := rod.Try(func() { + // 获取内容输入框 + textInput := page.Timeout(10 * time.Second).MustElement(`div.notranslate.public-DraftEditor-content`) + textInput.MustInput(Content) + // 检测字符串是否包含标签 + if strings.Contains(Content, "#") { + _, err := page.Timeout(5 * time.Second).Element(`div.css-175oi2r.r-14lw9ot.r-z2wwpe.r-1upvrn0.r-h3f8nf.r-11yh6sk.r-1rnoaur`) + time.Sleep(2 * time.Second) + if err == nil { + tagBuuton := page.Timeout(5 * time.Second).MustElement(`div.css-175oi2r.r-14lw9ot.r-z2wwpe.r-1upvrn0.r-h3f8nf.r-11yh6sk.r-1rnoaur div[data-testid="typeaheadResult"] button.css-175oi2r.r-x572qd.r-6dt33c.r-1loqt21.r-o7ynqc.r-6416eg.r-1ny4l3l`) + tagBuuton.MustClick() + time.Sleep(2 * time.Second) + } + } + // 循环保存图片到本地 + if len(Img) > 0 { + var file libs.File + for _, v := range Img { + tempFile, _ := file.DownloadImage(v) + // 结束时删除临时文件 + defer os.Remove(tempFile) + page.MustElement(`input[data-testid="fileInput"]`).MustSetFiles(tempFile) + time.Sleep(1 * time.Second) + } + time.Sleep(5 * time.Second) + } + }) + if errors.Is(err, context.DeadlineExceeded) { + fmt.Println("请求空闲等待超时!") + return libs.ErrorCode["SendPostFailed"] + } + + code := postButton(page) + if code == 200 { + return libs.ErrorCode["SendPostSuccessful"] + } + return libs.ErrorCode["SendPostFailed"] +} + +// 发推按钮点击之后监听请求 +func postButton(page *rod.Page) int { + // 定义一个通道接收消息 + router := page.HijackRequests() + defer router.MustStop() + ch := make(chan int, 1) + router.MustAdd("*/CreateTweet*", func(ctx *rod.Hijack) { + ctx.ContinueRequest(&proto.FetchContinueRequest{}) + ch <- ctx.Response.Payload().ResponseCode + }) + + go router.Run() + // 点击发布按钮 + postButton := page.Timeout(10 * time.Second).MustElement(`button[data-testid="tweetButtonInline"]`) + postButton.MustClick() + select { + case res := <-ch: + return res + case <-time.After(30 * time.Second): + log.Println("等待上传响应超时") + return 500 + } +} + +// 搜索 +func (*TwitterService) Search(accountId int, keyword string, twitterId string) *libs.ErrorInfo { + // 获取账号状态 + UserService := InitTwitterAccountService() + user := UserService.GetInfo(int64(accountId)) + if user.LoginStatus != "登录成功" { + return libs.ErrorCode["AccountHasBeenTakenOffline"] + } + // 获取实例 + obj := Browser.GetBrowser(accountId, &libs.Fingerprint{ + Proxy: user.Proxy, + }) + page := obj.Page + // 跳转到首页 + page.MustNavigate("https://x.com/home").MustWaitLoad() + err2 := rod.Try(func() { + forYou := page.Timeout(10 * time.Second).MustElement(`div[data-testid="ScrollSnap-List"] div.css-175oi2r.r-14tvyh0.r-cpa5s6.r-16y2uox:nth-child(1)`) + forYou.MustClick() + }) + if err2 != nil { + } + + page.MustWaitNavigation() + // 是否检测到帖子列表 + err := rod.Try(func() { + page.Timeout(5 * time.Second).MustElement(`article[data-testid="tweet"]`) + }) + + if errors.Is(err, context.DeadlineExceeded) { + fmt.Println("请求空闲等待超时!") + // 关闭浏览器 + Browser.CancelBrowser(accountId) + return libs.ErrorCode["NetworkTimeout"] + } + + page.MustNavigate("https://x.com/explore").MustWaitLoad() + + searchInput := page.Timeout(10 * time.Second).MustElement(`input[data-testid="SearchBox_Search_Input"]`) + // 聚焦 + searchInput.Focus() + searchInput.MustInput(keyword) + + searchCode := waitSearch(page) + if searchCode != 200 { + return libs.ErrorCode["SearchFailed"] + } + peopleButton := page.Timeout(10 * time.Second).MustElement(`div[data-testid="ScrollSnap-List"] div.css-175oi2r.r-14tvyh0.r-cpa5s6.r-16y2uox:nth-child(3)`) + peopleButton.MustClick() + + retry, err1 := page.Timeout(5 * time.Second).Element(`div.css-175oi2r.r-1awozwy.r-16y2uox.r-1777fci.r-dd0y9b.r-3o4zer.r-f8sm7e.r-13qz1uu.r-1ye8kvj`) + if retry != nil || err1 == nil { + return libs.ErrorCode["SearchFailed"] + } + + var isSearch bool = true + var searchCount int = 0 + var activeUser *rod.Element = nil + var nameArr []string = []string{} + var nameBool map[string]bool = make(map[string]bool) + for isSearch && searchCount < 10 { + searchCount++ + // 获取视口内的所有元素 + ids, err2 := getVisibleElements(page, `div.css-175oi2r.r-f8sm7e.r-13qz1uu.r-1ye8kvj div.css-146c3p1.r-dnmrzs.r-1udh08x.r-1udbk01.r-3s2u2q.r-bcqeeo.r-1ttztb7.r-qvutc0.r-37j5jr.r-a023e6.r-rjixqe.r-16dba41.r-18u37iz.r-1wvb978`) + if err2 != nil || len(ids) <= 0 { + return libs.ErrorCode["SearchFailed"] + } + for _, v := range ids { + text := v.MustText() + if !nameBool[text] { + nameBool[text] = true + nameArr = append(nameArr, text) + } + if text == "@"+twitterId { + activeUser = v + break + } + } + if activeUser != nil { + isSearch = false + log.Println("循环:") + log.Println(searchCount) + log.Println(len(nameArr)) + log.Println("找到用户") + } else { + log.Println("开始滚动") + scrollElementLocationTop(ids[len(ids)-1]) + time.Sleep(5 * time.Second) + } + } + + if searchCount >= 10 { + return libs.ErrorCode["SearchFailed"] + } + time.Sleep(1 * time.Second) + activeUser.MustClick() + // 等待5-8秒 + s := 5 + rand.Intn(3) + time.Sleep(time.Duration(s) * time.Second) + posts := page.Timeout(10 * time.Second).MustElements(`div.css-175oi2r.r-j5o65s.r-qklmqi.r-1adg3ll.r-1ny4l3l`) + var postArr []*rod.Element + for _, post := range posts { + aid := post.Timeout(10 * time.Second).MustElement(`div.css-146c3p1.r-dnmrzs.r-1udh08x.r-1udbk01.r-3s2u2q.r-bcqeeo.r-1ttztb7.r-qvutc0.r-37j5jr.r-a023e6.r-rjixqe.r-16dba41.r-18u37iz.r-1wvb978`) + text := aid.MustText() + if text == "@"+twitterId { + postArr = append(postArr, post) + } + } + if len(postArr) <= 0 { + return libs.ErrorCode["SearchSuccessful"] + } + // 随机选择一个 + activePost := postArr[rand.Intn(len(postArr))] + activePost.MustScrollIntoView() // 将元素滑动到页面中间 + tweetText := activePost.Timeout(10 * time.Second).MustElement(`div[data-testid="tweetText"]`) + // 禁用链接点击 + _, err3 := page.Evaluate(&rod.EvalOptions{ + JS: `(selector) => { + const el = document.querySelector(selector); + const links = el.querySelectorAll('a'); + links.forEach(a => { + a.style.pointerEvents = 'none'; + }); + }`, + JSArgs: []interface{}{`div[data-testid="tweetText"]`}, + }) + if err3 != nil { + } + tweetText.MustClick() + // 恢复链接的点击行为(如果需要) + _, _ = page.Evaluate(&rod.EvalOptions{ + JS: `(selector) => { + const el = document.querySelector(selector); + const links = el.querySelectorAll('a'); + links.forEach(a => { + a.style.pointerEvents = ''; + }); + }`, + JSArgs: []interface{}{`div[data-testid="tweetText"]`}, + }) + // 等待15-20秒 + s2 := 15 + rand.Intn(10) + time.Sleep(time.Duration(s2) * time.Second) + // 返回上一页 + // 模拟点击后退按钮 + page.MustNavigateBack() + // 等待新页面加载完成 + page.MustWaitLoad() + // 等待5-8秒 + time.Sleep(time.Duration(s) * time.Second) + return libs.ErrorCode["SearchSuccessful"] +} + +// 获取视口内可见的指定元素 +func getVisibleElements(page *rod.Page, selector string) ([]*rod.Element, error) { + // 1. 获取所有候选元素 + elements, err := page.Elements(selector) + if err != nil { + return nil, err + } + + // 2. 过滤出视口内可见的元素 + var visibleElements []*rod.Element + for _, el := range elements { + isVisible, err := el.Eval(`function() { + const rect = this.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; + + // 判断元素是否至少部分在视口内 + return ( + rect.top < viewportHeight && + rect.bottom > 0 && + rect.left < viewportWidth && + rect.right > 0 + ); + }`) + if err == nil && isVisible.Value.Bool() { + visibleElements = append(visibleElements, el) + } + } + + return visibleElements, nil +} + +// 将指定元素滑动到顶部 +func scrollElementLocationTop(el *rod.Element) { + // 执行JS:滚动到视口顶部(平滑滚动) + el.MustEval(`function() { + this.scrollIntoView({ + behavior: "smooth", + block: "start" // 顶部对齐 + }); + }`) + + // 等待滚动完成 + // time.Sleep(2 * time.Second) +} + +// 搜索按钮点击之后监听请求 +func waitSearch(page *rod.Page) int { + // 定义一个通道接收消息 + router := page.HijackRequests() + defer router.MustStop() + ch := make(chan int, 1) + router.MustAdd("*/SearchTimeline*", func(ctx *rod.Hijack) { + ctx.ContinueRequest(&proto.FetchContinueRequest{}) + ch <- ctx.Response.Payload().ResponseCode + }) + + go router.Run() + // 点击搜索按钮 + searchInput := page.Timeout(10 * time.Second).MustElement(`input[data-testid="SearchBox_Search_Input"]`) + searchInput.MustKeyActions().Press(input.Enter).MustDo() + select { + case res := <-ch: + return res + case <-time.After(30 * time.Second): + log.Println("等待上传响应超时") + return 500 + } +} + +// 获取指定接口的响应参数 +func getApiResponse(page *rod.Page, url string) string { + // 定义一个通道接收消息 + ch := make(chan string) + go page.EachEvent(func(e *proto.PageLoadEventFired) { + log.Println("1234567890") + }, func(e *proto.NetworkResponseReceived) { + if strings.Contains(e.Response.URL, url) { + reply, err := (proto.NetworkGetResponseBody{RequestID: e.RequestID}).Call(page) + if err != nil { + fmt.Println(err) + } + ch <- reply.Body + // fmt.Println(reply.Body) + } + })() + + select { + case res := <-ch: + return res + case <-time.After(30 * time.Second): + log.Println("等待上传响应超时") + return "" + } +} + +func (*TwitterService) LoopViewPost(id int) *libs.ErrorInfo { + UserService := InitTwitterAccountService() + user := UserService.GetInfo(int64(id)) + obj := Browser.GetBrowser(id, &libs.Fingerprint{ + Proxy: user.Proxy, + }) + page := obj.Page + // 获取推文列表 + postList := InitTwitterPostService().GetAll() + if len(postList) <= 0 { + return libs.ErrorCode["NoPostsToView"] + } + for _, v := range postList { + page.MustNavigate(v.Link).MustWaitLoad() + page.MustWaitNavigation() + page.MustWaitLoad() // DOM 加载完成 + page.MustWaitIdle() // 基本静止(减少早期误判) + page.MustActivate() // 前置 + // 等待5-10秒 + s := 5 + rand.Intn(5) + time.Sleep(time.Duration(s) * time.Second) + log.Println("浏览推文链接:", v.Link) + + if v.ContentLink != "" { + page.MustNavigate(v.ContentLink).MustWaitLoad() + page.MustWaitNavigation() + page.MustWaitLoad() // DOM 加载完成 + page.MustWaitIdle() // 基本静止(减少早期误判) + page.MustActivate() // 前置 + // 等待5-10秒 + s2 := 5 + rand.Intn(5) + time.Sleep(time.Duration(s2) * time.Second) + log.Println("浏览推文内容链接:", v.ContentLink) + } + + } + return libs.ErrorCode["ViewSuccessful"] +}