新增发推方法

This commit is contained in:
zyj
2025-11-20 11:42:48 +08:00
parent 9cefdb1804
commit f344de378b

View File

@@ -2,17 +2,22 @@ package services
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"go-account-register/libs" "go-account-register/libs"
"go-account-register/models" "go-account-register/models"
"go-account-register/utils" "go-account-register/utils"
"log" "log"
"math/rand"
"os"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/input"
"github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/proto"
) )
@@ -34,17 +39,18 @@ func (*TwitterService) Login(id int) *libs.ErrorInfo {
// log.Println("获取cookie", cookies) // log.Println("获取cookie", cookies)
// b, _ := json.MarshalIndent(cookies, "", " ") // b, _ := json.MarshalIndent(cookies, "", " ")
// log.Println(string(b)) // log.Println(string(b))
var token string
for _, v := range user.Cookies { // var token string
if v.Name == "auth_token" { // for _, v := range user.Cookies {
token = v.Value // if v.Name == "auth_token" {
} // token = v.Value
} // }
log.Println("当前账号Token", token) // }
// log.Println("当前账号Token", token)
cookies := []*proto.NetworkCookieParam{ cookies := []*proto.NetworkCookieParam{
{ {
Name: "auth_token", Name: "auth_token",
Value: token, Value: user.Token,
Domain: ".x.com", Domain: ".x.com",
Path: "/", Path: "/",
Expires: proto.TimeSinceEpoch(time.Now().Add(24 * time.Hour).Unix()), // 过期时间 Expires: proto.TimeSinceEpoch(time.Now().Add(24 * time.Hour).Unix()), // 过期时间
@@ -52,6 +58,7 @@ func (*TwitterService) Login(id int) *libs.ErrorInfo {
Secure: true, // 仅 HTTPS 传输 Secure: true, // 仅 HTTPS 传输
}, },
} }
log.Println("Cookie", cookies[0].Value)
// 全局设置 // 全局设置
browser.SetCookies(cookies) browser.SetCookies(cookies)
page.MustNavigate("https://x.com/home").MustWaitLoad() page.MustNavigate("https://x.com/home").MustWaitLoad()
@@ -102,15 +109,73 @@ func (*TwitterService) Login(id int) *libs.ErrorInfo {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
getAccountInfo(page, int64(id))
return libs.ErrorCode["LoginSuccessful"] 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) { func (*TwitterService) Logout(id int) {
Browser.CancelBrowser(id) Browser.CancelBrowser(id)
} }
// 注册账号
func (bit *TwitterService) Register() *libs.ErrorInfo { func (bit *TwitterService) Register() *libs.ErrorInfo {
// 随机获取代理 // 随机获取代理
proxyService := InitProxyService() proxyService := InitProxyService()
@@ -278,3 +343,369 @@ func (bit *TwitterService) Register() *libs.ErrorInfo {
return libs.ErrorCode["RegisterSuccessful"] 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"]
}