diff --git a/api/ads.go b/api/ads.go new file mode 100644 index 0000000..68622d4 --- /dev/null +++ b/api/ads.go @@ -0,0 +1,38 @@ +package api + +import ( + "go-account-register/models" + "go-account-register/services" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +type AdsPower struct { +} + +// 导入账号 +func (a *AdsPower) ImportAccount(ctx *gin.Context) { + // 查询当前的账号列表 + var twitterAccountService = services.InitTwitterAccountService() + accountList := twitterAccountService.GetAvailableImportAccount(&models.TwitterAccount{ + Type: "raise", + }) + // 循环创建AdsPower实例 + var AdsPowerService services.AdsPowerService + log.Println("导入广告账号数量:", len(accountList)) + ticker := time.NewTicker(700 * time.Millisecond) + defer ticker.Stop() + for _, account := range accountList { + <-ticker.C + AdsPowerService.CreateBrowser(account.ID) + // 如果只需要每秒10次,可以限制最多处理10个 + } + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 200, + "msg": "成功", + "data": "", + }) +} diff --git a/libs/error_code.go b/libs/error_code.go index 1510a78..453beac 100644 --- a/libs/error_code.go +++ b/libs/error_code.go @@ -35,4 +35,6 @@ var ErrorCode = map[string]*ErrorInfo{ "AccountLocked": {Code: 0, Data: "", Msg: "账号锁定"}, "UnlockedSuccessfully": {Code: 200, Data: "", Msg: "解锁成功"}, "UnlockedFailed": {Code: 0, Data: "", Msg: "解锁失败"}, + "ViewSuccessful": {Code: 200, Data: "", Msg: "查看成功"}, + "NoPostsToView": {Code: 0, Data: "", Msg: "没有可查看的推文"}, } diff --git a/router/ads.go b/router/ads.go new file mode 100644 index 0000000..243f446 --- /dev/null +++ b/router/ads.go @@ -0,0 +1,16 @@ +package router + +import ( + "go-account-register/api" + + "github.com/gin-gonic/gin" +) + +type AdsPower struct{} + +var apiAdspower api.AdsPower + +func (a *AdsPower) AdsPowerRouterInit(app *gin.Engine) { + adsPower := app.Group("/adspower") + adsPower.POST("/import_account", apiAdspower.ImportAccount) +} diff --git a/router/index.go b/router/index.go index 161c485..4234f42 100644 --- a/router/index.go +++ b/router/index.go @@ -6,6 +6,7 @@ import ( var task Task var twitter Twitter +var adsPower AdsPower type Router struct { } @@ -13,4 +14,5 @@ type Router struct { func Init(app *gin.Engine) { task.TaskRouterInit(app) twitter.TwitterRouterInit(app) + adsPower.AdsPowerRouterInit(app) } diff --git a/services/ads_power_service.go b/services/ads_power_service.go new file mode 100644 index 0000000..f9df296 --- /dev/null +++ b/services/ads_power_service.go @@ -0,0 +1,475 @@ +package services + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "go-account-register/config" + "go-account-register/libs" + "go-account-register/models" + "go-account-register/utils" + "log" + "math/rand" + "os" + "strings" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/input" + "github.com/go-rod/rod/lib/proto" +) + +type AdsPowerService struct { +} + +func (*AdsPowerService) CreateBrowser(accountId int64) string { + TwitterAccount := InitTwitterAccountService() + user := TwitterAccount.GetInfo(accountId) + fingerprint := map[string]interface{}{ + "name": fmt.Sprintf(`推特账号%d`, accountId), + "group_id": "0", + "platform": "", + "user_proxy_config": map[string]interface{}{ + "proxy_soft": "no_proxy", + "proxy_type": "socks5", + }, + "fingerprint_config": map[string]interface{}{ + "webrtc": "proxy", + "random_ua": map[string]interface{}{ + "ua_browser": []string{ + "chrome", + }, + "ua_system_version": []string{ + "Windows 10", + "Windows 11", + "Mac OS X 15", + "Mac OS X 14", + }, + }, + }, + } + if user.Cookies != nil { + fingerprint["cookie"] = user.Cookies + } else { + cookieObj := []map[string]interface{}{ + { + "name": "auth_token", + "value": user.Token, + "domain": ".x.com", + "path": "/", + "httpOnly": false, + "secure": true, + "session": true, + "expires": proto.TimeSinceEpoch(time.Now().Add(24 * time.Hour).Unix()), + "sameSite": "unspecified", + }, + } + cookieJson, _ := json.Marshal(cookieObj) + fingerprint["cookie"] = string(cookieJson) + } + if user.Proxy != "" { + var proxyHandle utils.ProxyHandle + protocol, username, password, ip, port, err2 := proxyHandle.ParseProxy(user.Proxy) + if err2 == nil { + fingerprint["user_proxy_config"] = map[string]interface{}{ + "proxy_soft": "other", + "proxy_type": protocol, + "proxy_host": ip, + "proxy_port": port, + "proxy_user": username, + "proxy_password": password, + } + } + } + adsPowerRequest := utils.NewAdsPowerRequest() + jsonStr, _ := adsPowerRequest.CreateBrowser(fingerprint) + var rawData map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr.Body()), &rawData); err != nil { + panic(err) + } + log.Println("创建浏览器返回数据", rawData) + success := rawData["msg"].(string) + if success == "Success" { + // 如果成功则将数据存入数据库 + data := rawData["data"].(map[string]interface{}) + id := data["profile_id"].(string) + // jsonData, _ := json.Marshal(data) + TwitterAccount.Update(accountId, &models.TwitterAccount{ + BrowserId: id, + }) + return id + } + return "" +} + +func (*AdsPowerService) GetRunStatus(id string) int { + adsPowerRequest := utils.NewAdsPowerRequest() + jsonStr, _ := adsPowerRequest.GetRunStatus(id) + var rawData map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr.Body()), &rawData); err != nil { + panic(err) + } + success := rawData["msg"].(string) + if success == "success" { + data := rawData["data"].(map[string]interface{}) + status := data["status"].(string) + if status == "Active" { + return 1 + } + return 0 + } + return 0 +} + +// 打开浏览器 +func (*AdsPowerService) OpenBrowser(accountId int, browserId string) string { + // 获取当前 + adsPowerRequest := utils.NewAdsPowerRequest() + data := map[string]interface{}{ + "profile_id": browserId, + "delete_cache": "1", + // "last_opened_tabs": "0", + } + jsonStr, _ := adsPowerRequest.OpenBrowser(data) + var rawData map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr.Body()), &rawData); err != nil { + panic(err) + } + success := rawData["msg"].(string) + if success == "success" { + data := rawData["data"].(map[string]interface{}) + ws := data["ws"].(map[string]interface{}) + puppeteer := ws["puppeteer"].(string) + TwitterAccount := InitTwitterAccountService() + TwitterAccount.Update(int64(accountId), &models.TwitterAccount{ + BrowserUrl: puppeteer, + }) + return puppeteer + } + return "" +} + +// 删除浏览器 +func (*AdsPowerService) DeleteBrowser(browserId string) bool { + adsPowerRequest := utils.NewAdsPowerRequest() + // 先关闭浏览器 + _, err := adsPowerRequest.CloseBrowser(browserId) + if err != nil { + log.Println("关闭浏览器失败") + } + // 删除浏览器 + _, err2 := adsPowerRequest.DeleteBrowser(browserId) + log.Println("删除浏览器,", err2 == nil) + return err2 == nil +} + +// 关闭浏览器 +func (*AdsPowerService) CloseBrowser(browserId string) bool { + adsPowerRequest := utils.NewAdsPowerRequest() + jsonStr, _ := adsPowerRequest.CloseBrowser(browserId) + var result struct { + Data string `json:"data"` // 动态键名 → 值 + Success bool `json:"success"` + } + if err := json.Unmarshal([]byte(jsonStr.Body()), &result); err != nil { + return false + } + return true +} + +func randNum(min, max int) int { + return rand.Intn(max-min) + min +} + +func getKey(id string) input.Key { + keys := map[string]input.Key{ + "0": input.Numpad0, + "1": input.Numpad1, + "2": input.Numpad2, + "3": input.Numpad3, + "4": input.Numpad4, + "5": input.Numpad5, + "6": input.Numpad6, + "7": input.Numpad7, + "8": input.Numpad8, + "9": input.Numpad9, + "+": input.NumpadAdd, + } + return keys[id] +} + +// 将指定元素滑动到顶部 +func scrollElementLocationCenter(el *rod.Element) { + // 执行JS:滚动到视口顶部(平滑滚动) + el.MustEval(`function() { + this.scrollIntoView({ + behavior: "smooth", + block: "center", // 关键参数:垂直居中 + inline: "center" // 水平居中 + }); + }`) + + // 等待滚动完成 + // time.Sleep(2 * time.Second) +} + +// 发推 +func (*AdsPowerService) 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.GetBitBrowser(user.BrowserUrl) + 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 (*AdsPowerService) ViewPostsSpecifiedTime(accountId int) *libs.ErrorInfo { + UserService := InitTwitterAccountService() + user := UserService.GetInfo(int64(accountId)) + if user.LoginStatus != "登录成功" { + return libs.ErrorCode["AccountHasBeenTakenOffline"] + } + // 获取实例 + obj := Browser.GetBitBrowser(user.BrowserUrl) + page := obj.Page + // 跳转到首页 + page.MustNavigate("https://x.com/home").MustWaitLoad() + 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() + 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"] + } + var status chan string = make(chan string) + // 获取配置中的时间限制 + appConfig, _ := config.LoadConfig() + randTime := appConfig.Limit.ViweTimeMin + rand.Intn(appConfig.Limit.ViweTimeMax-appConfig.Limit.ViweTimeMin) + go loopViewPosts(page, accountId, status, randTime) + select { + case res := <-status: + if res == "fali" { + return libs.ErrorCode["MaintainAccountFailed"] + } + if res == "success" { + return libs.ErrorCode["MaintainAccountSuccessful"] + } + case <-time.After(time.Duration(randTime) * time.Minute): + return libs.ErrorCode["MaintainAccountSuccessful"] + } + return libs.ErrorCode["MaintainAccountSuccessful"] + // // 获取配置中的时间限制 + // appConfig, _ := config.LoadConfig() + // randTime := appConfig.Limit.ViweTimeMin + rand.Intn(appConfig.Limit.ViweTimeMax-appConfig.Limit.ViweTimeMin) + // // 获取当前时间戳 + // startTime := time.Now().Unix() + // var activeTime int64 = time.Now().Unix() + // // 函数报错则向这个通道发送消息 + // ch := make(chan string, 1) + // for activeTime-startTime < int64(randTime*60) { + // select { + // case <-ch: + // // 失败 + // return libs.ErrorCode["MaintainAccountFailed"] + // default: + // viewPosts(page, ch, accountId) + // } + // activeTime = time.Now().Unix() + // } + // return libs.ErrorCode["MaintainAccountSuccessful"] + + // timer := time.NewTimer(time.Duration(randTime) * time.Minute) + // defer timer.Stop() + + // ticker := time.NewTicker(1 * time.Second) + // defer ticker.Stop() + + // for { + // select { + // case <-timer.C: + // fmt.Println("30分钟已到,自动返回") + // return libs.ErrorCode["MaintainAccountSuccessful"] + // case <-ch: + // // 失败 + // return libs.ErrorCode["MaintainAccountFailed"] + // case <-ticker.C: + // viewPosts(page, ch, accountId) + // } + // } + +} + +func loopViewPosts(page *rod.Page, accountId int, status chan string, randTime int) { + // 获取当前时间戳 + startTime := time.Now().Unix() + var activeTime int64 = time.Now().Unix() + // 函数报错则向这个通道发送消息 + ch := make(chan string, 1) + for activeTime-startTime < int64(randTime*60) { + viewPosts(page, ch, accountId) + + activeTime = time.Now().Unix() + } + status <- "success" +} + +// 查看帖子 +func viewPosts(page *rod.Page, ch chan string, accountId int) bool { + // 滑动滚动条 + log.Println("开始滚动", accountId) + randomTrackScroll(page) + log.Println("结束滚动", accountId) + is := rand.Float64() // 生成随机数 + if is < 0.5 { + err := rod.Try(func() { + // 获取视口内的帖子列表 + list, err := getVisibleElements(page, `div[data-testid="tweetText"]`) + if err != nil || len(list) <= 0 { + return + } + // 等待两秒 + time.Sleep(2 * time.Second) + // 随机选择一个点击进入帖子 + tweetText := list[rand.Intn(len(list))] + log.Println("选择一个帖子:", accountId, tweetText) + scrollElementLocationCenter(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"]`}, + }) + page.MustWaitNavigation() + // 查看帖子 3-5分钟 + timeMin := 3 + rand.Intn(2) + time.Sleep(time.Duration(timeMin) * time.Minute) + // 退回到上一步 + // page.MustNavigateBack() + // 等待新页面加载完成 + // page.MustWaitLoad() + page.MustNavigate("https://x.com/home").MustWaitLoad() + + // 判断当前URL是否与预期的URL一致 + url := page.MustEval("() => window.location.href").String() + if url != "https://x.com/home" { + // 如果不一致 就直接跳转 + page.MustNavigate("https://x.com/home").MustWaitLoad() + } + }) + if err != nil { + ch <- "error" + } + return true + } + return false +} + +// 随机滚动 +func randomTrackScroll(page *rod.Page) { + // height, _ := page.Eval(`() => document.body.scrollHeight`) + // totalHeight := int(height.Value.Num()) + err := rod.Try(func() { + var totalHeight int = 1000 + rand.Intn(1000) + currentPos := 0 + log.Println("获取随机滚动的参数:", totalHeight) + for currentPos < totalHeight { + // 随机水平偏移(模拟人类不精确的滑动) + xOffset := rand.Intn(20) - 10 // -10到10之间的随机值 + yDistance := 50 + rand.Intn(150) + + err := page.Mouse.Scroll(float64(xOffset), float64(yDistance), 1) + if err != nil { + log.Printf("滚动失败: %v", err) + } + currentPos += yDistance + + // 随机等待和偶尔的回滑 + if rand.Float32() < 0.1 { // 10%概率回滑一点 + backStep := 10 + rand.Intn(30) + err := page.Mouse.Scroll(0, -float64(backStep), 1) + if err != nil { + log.Printf("回滑失败: %v", err) + } + currentPos -= backStep + } + + time.Sleep(time.Millisecond * time.Duration(50+rand.Intn(300))) + log.Println("滚动完成") + } + }) + if err != nil { + + } + +} diff --git a/services/twitter_account_service.go b/services/twitter_account_service.go index 35836d5..cc77ea0 100644 --- a/services/twitter_account_service.go +++ b/services/twitter_account_service.go @@ -108,3 +108,15 @@ func (u *TwitterAccountService) Update(id int64, params *models.TwitterAccount) func (u *TwitterAccountService) Delete(id int64) { u.db.Delete(&models.TwitterAccount{}, id) } + +// 获取可用账号 +func (u *TwitterAccountService) GetAvailableImportAccount(params *models.TwitterAccount) []models.TwitterAccount { + var users []models.TwitterAccount + res := u.db.Model(&models.TwitterAccount{}).Where(`(browser_id = '' OR browser_id IS NULL) AND (login_status != "账号异常" OR login_status IS NULL)`).Where(params) + // if params.Username != "" { + // res.Where("username LIKE ?", "%"+params.Username+"%") + // } + res.Order("id DESC").Find(&users) + + return users +} diff --git a/utils/ads_power_request.go b/utils/ads_power_request.go new file mode 100644 index 0000000..bf3e21d --- /dev/null +++ b/utils/ads_power_request.go @@ -0,0 +1,103 @@ +package utils + +import ( + "errors" + "go-account-register/config" + "log" + + "github.com/go-resty/resty/v2" +) + +type AdsPowerRequest struct { + client *resty.Client +} + +func NewAdsPowerRequest() *AdsPowerRequest { + appConfig, _ := config.LoadConfig() + client := resty.New() + client.SetBaseURL(appConfig.AdsPower.ApiUrl) + + // 响应拦截器 (类似 axios 拦截器) + client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error { + if resp.StatusCode() != 200 { + log.Println("请求失败,检查网络") + return errors.New("request failed") + } + return nil + }) + + return &AdsPowerRequest{client: client} +} + +// Request 发送通用请求 +func (h *AdsPowerRequest) Request(method, url string, body interface{}, queryParams map[string]string) (*resty.Response, error) { + req := h.client.R(). + SetHeader("Content-Type", "application/json") + + if body != nil { + req.SetBody(body) + } + if queryParams != nil { + req.SetQueryParams(queryParams) + } + + resp, err := req.Execute(method, url) + + if err != nil { + log.Println("请求失败了:", err) + return nil, err + } + return resp, nil +} + +/** +* 打开浏览器 + */ +func (h *AdsPowerRequest) OpenBrowser(data map[string]interface{}) (*resty.Response, error) { + return h.Request("POST", "/api/v2/browser-profile/start", data, nil) +} + +/** + * @description 关闭浏览器 + * @param {String} id + * @returns {*resty.Response, error} + */ +func (h *AdsPowerRequest) CloseBrowser(id string) (*resty.Response, error) { + return h.Request("POST", "/api/v2/browser-profile/stop", map[string]interface{}{"profile_id": id}, nil) +} + +/** + * @description 创建浏览器 + * @param {Object} data + * @returns {*resty.Response, error} + */ +func (h *AdsPowerRequest) CreateBrowser(data map[string]interface{}) (*resty.Response, error) { + return h.Request("POST", "/api/v2/browser-profile/create", data, nil) +} + +/** + * @description 查询浏览器运行状态 + * @param {String} id + * @returns {*resty.Response, error} + */ +func (h *AdsPowerRequest) GetRunStatus(id string) (*resty.Response, error) { + return h.Request("GET", "/api/v2/browser-profile/active", nil, map[string]string{"profile_id": id}) +} + +/** + * @description 删除浏览器 + * @param {String} id + * @returns {*resty.Response, error} + */ +func (h *AdsPowerRequest) DeleteBrowser(id string) (*resty.Response, error) { + return h.Request("POST", "/api/v2/browser-profile/delete", map[string]interface{}{"profile_id": []string{id}}, nil) +} + +/** + * @description 更新浏览器配置 + * @param {Object} data + * @returns {*resty.Response, error} + */ +func (h *AdsPowerRequest) UpdateBrowser(data map[string]interface{}) (*resty.Response, error) { + return h.Request("POST", "/api/v2/browser-profile/update", data, nil) +}