diff --git a/api/twitter.go b/api/twitter.go index 24c69d5..33f78a3 100644 --- a/api/twitter.go +++ b/api/twitter.go @@ -3,16 +3,50 @@ package api import ( "fmt" "go-account-register/libs" + "go-account-register/services" "net/http" + "strconv" "github.com/gin-gonic/gin" ) -type Twitter struct{} +type Twitter struct { +} type RegisterParams struct { } +func (t *Twitter) Login(ctx *gin.Context) { + // 参数 + var res *libs.ErrorInfo + accountId := ctx.Query("accountId") + id, _ := strconv.Atoi(accountId) + + var TwitterService services.TwitterService + res = TwitterService.Login(id) + UserService := services.InitTwitterAccountService() + UserService.ChangeAccountLoginStatus(id, res.Msg) + ctx.JSON(http.StatusOK, gin.H{ + "code": res.Code, + "msg": res.Msg, + "data": res.Data, + }) +} + +func (t *Twitter) Logout(ctx *gin.Context) { + accountId := ctx.Query("accountId") + id, _ := strconv.Atoi(accountId) + var TwitterService services.TwitterService + TwitterService.Logout(id) + UserService := services.InitTwitterAccountService() + UserService.ChangeAccountLoginStatus(id, "账号下线") + ctx.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": "成功", + "data": "", + }) +} + func (*Twitter) Register(ctx *gin.Context) { var res *libs.ErrorInfo var paramsJson RegisterParams @@ -26,8 +60,8 @@ func (*Twitter) Register(ctx *gin.Context) { }) return } - // var TwitterService services.TwitterAccountService - // res = TwitterService.Register() + var BitBrowserService services.BitBrowserService + res = BitBrowserService.Register() ctx.JSON(http.StatusOK, gin.H{ "code": res.Code, "msg": res.Msg, diff --git a/cmd/main.go b/cmd/main.go index df668a9..c4fdb3e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,7 @@ import ( "fmt" "go-account-register/config" "go-account-register/router" + "go-account-register/services" "go-account-register/utils" "log" "os" @@ -37,7 +38,9 @@ func handleShutdownSignals() { // 阻塞等待信号 sig := <-sigChan log.Printf("接收到信号: %v, 关闭所有浏览器...", sig) - + services.Browser.CancelAllBrowser() + accountService := services.InitTwitterAccountService() + accountService.OfflineAllAccount() // 强制退出程序 os.Exit(0) } diff --git a/libs/error_code.go b/libs/error_code.go index 6c1a1ec..1510a78 100644 --- a/libs/error_code.go +++ b/libs/error_code.go @@ -31,4 +31,8 @@ var ErrorCode = map[string]*ErrorInfo{ "RegisterFailed": {Code: 0, Data: "", Msg: "注册失败"}, "RegisterSuccessful": {Code: 0, Data: "", Msg: "注册成功"}, "BrowserCreationFailed": {Code: 0, Data: "", Msg: "浏览器创建失败"}, + "MailboxUnavailable": {Code: 0, Data: "", Msg: "邮箱不可用"}, + "AccountLocked": {Code: 0, Data: "", Msg: "账号锁定"}, + "UnlockedSuccessfully": {Code: 200, Data: "", Msg: "解锁成功"}, + "UnlockedFailed": {Code: 0, Data: "", Msg: "解锁失败"}, } diff --git a/models/browser_instance.go b/models/browser_instance.go index f224e0a..a30149b 100644 --- a/models/browser_instance.go +++ b/models/browser_instance.go @@ -13,5 +13,5 @@ type BrowserInstance struct { // 实现 TableName 方法指定表名 func (BrowserInstance) TableName() string { - return "browser_instance" + return "ar_browser_instance" } diff --git a/plugin/task-account-unlock-plugin.go b/plugin/task-account-unlock-plugin.go new file mode 100644 index 0000000..bea6424 --- /dev/null +++ b/plugin/task-account-unlock-plugin.go @@ -0,0 +1,100 @@ +package plugin + +import ( + "go-account-register/config" + "go-account-register/models" + "go-account-register/services" + paramsTypes "go-account-register/types" + "log" + "math" + "runtime/debug" + "sync" + "time" +) + +type TaskAccountUnlockPlugin struct { +} + +func (*TaskAccountUnlockPlugin) Run(task *paramsTypes.Task) { + + // 查询当前所有账号 + accountService := services.InitTwitterAccountService() + // 判断是否是补偿任务 + var accountList []models.TwitterAccount + accountList = accountService.GetAvailableAccount(&models.TwitterAccount{ + Status: "账号锁定", + }) + if task.Data.Compensate == 1 { + // 补偿任务 + accountList = accountService.GetAccountListByStatus(&models.TwitterAccount{ + Status: "解锁失败", + }) + } else { + // 清空当前运行的账号状态 + accountService.ClearAccountStatus(&models.TwitterAccount{ + Status: "解锁成功", + }) + } + + // 获取并发数 + appConfig, _ := config.LoadConfig() + num := math.Ceil(float64(len(accountList)) / float64(appConfig.Limit.BrowserRunMax)) + sem := make(chan struct{}, appConfig.Limit.BrowserRunMax) // 全局并发上限 + var globalWait sync.WaitGroup + for i := 0; i < int(num); i++ { + select { + case <-task.Ctx.Done(): + log.Println("中断任务") + return + default: + } + log.Println("批次:", i+1) + for j := 0; j < appConfig.Limit.BrowserRunMax; j++ { + log.Println(i*appConfig.Limit.BrowserRunMax + j) + taskIndex := i*appConfig.Limit.BrowserRunMax + j + if taskIndex >= len(accountList) { + break + } + time.Sleep(10 * time.Second) + globalWait.Add(1) + sem <- struct{}{} // 阻塞直到有空位 + go func() { + defer func() { + <-sem // 释放信号量 + globalWait.Done() + }() + unlockWork(task, accountList[taskIndex].ID) + }() + } + } + globalWait.Wait() + + task.Message <- "Success" +} + +func unlockWork(task *paramsTypes.Task, id int64) { + AccountService := services.InitTwitterAccountService() + var TwitterService services.BitBrowserService + defer func() { + if r := recover(); r != nil { + log.Printf("Goroutine panic: %v\nStack: %s", r, debug.Stack()) + // AccountService.ChangeAccountLoginStatus(int(id), "账号下线") + // TwitterService.Logout(int(id)) + } + }() + res := TwitterService.AccountUnlock(id) + // 开始登录 + // res := TwitterService.Login(int(id)) + AccountService.ChangeAccountLoginStatus(int(id), res.Msg) + // if res.Code == 200 { + // // 开始拟人 + // // res = TwitterService.ChangeAccountInfo(int(id)) + // AccountService.ChangeAccountStatus(int(id), res.Msg) + // time.Sleep(3 * time.Second) + + // TwitterService.Logout(int(id)) + // AccountService.ChangeAccountLoginStatus(int(id), "账号下线") + // } + + log.Println(task.ID) +} diff --git a/router/index.go b/router/index.go index bee2232..161c485 100644 --- a/router/index.go +++ b/router/index.go @@ -5,10 +5,12 @@ import ( ) var task Task +var twitter Twitter type Router struct { } func Init(app *gin.Engine) { task.TaskRouterInit(app) + twitter.TwitterRouterInit(app) } diff --git a/router/twitter.go b/router/twitter.go index e9724cc..936a747 100644 --- a/router/twitter.go +++ b/router/twitter.go @@ -13,4 +13,6 @@ var twitterApi api.Twitter func (*Twitter) TwitterRouterInit(app *gin.Engine) { twitter := app.Group("/twitter") twitter.POST("/register", twitterApi.Register) + twitter.GET("/login", twitterApi.Login) // 登录账号 + twitter.GET("/logout", twitterApi.Logout) // 退出登录 } diff --git a/services/bit_browser_service.go b/services/bit_browser_service.go index 46f6aaf..07b49fa 100644 --- a/services/bit_browser_service.go +++ b/services/bit_browser_service.go @@ -20,7 +20,20 @@ type BitBrowserService struct { var Browser libs.Browser var Chrome libs.Chrome -func (*BitBrowserService) CreateBrowser(proxy *models.Proxy) string { +type BitCookieParam struct { + Domain string `json:"domain"` + ExpirationDate float64 `json:"expirationDate"` // 必须为 number + HttpOnly bool `json:"httpOnly"` + Name string `json:"name"` + Path string `json:"path"` + Secure bool `json:"secure"` + Session bool `json:"session"` + StoreID *string `json:"storeId"` // 允许 null,用指针 + Value string `json:"value"` + SameSite *string `json:"sameSite,omitempty"` // 可选字段,不填时省略 +} + +func (*BitBrowserService) CreateBrowser(proxy *models.Proxy, cookies []*proto.NetworkCookieParam) string { fingerprint := map[string]interface{}{ "name": fmt.Sprintf(`推特注册%s`, utils.RandString(10)), "proxyMethod": 2, @@ -42,6 +55,25 @@ func (*BitBrowserService) CreateBrowser(proxy *models.Proxy) string { "resolutionType": "0", }, } + if cookies != nil { + var cookie []BitCookieParam + for _, v := range cookies { + cookie = append(cookie, BitCookieParam{ + Domain: v.Domain, + ExpirationDate: float64(v.Expires), + HttpOnly: v.HTTPOnly, + Name: v.Name, + Path: v.Path, + Secure: v.Secure, + Session: false, + StoreID: nil, + Value: v.Value, + SameSite: (*string)(&v.SameSite), + }) + } + data, _ := json.Marshal(cookie) + fingerprint["cookie"] = data + } if proxy != nil { var proxyHandle utils.ProxyHandle protocol, username, password, ip, port, err2 := proxyHandle.ParseProxy(proxy.Type + "://" + proxy.Proxy) @@ -72,7 +104,7 @@ func (*BitBrowserService) CreateBrowser(proxy *models.Proxy) string { UpdateTime: time.Now().Unix(), CreateTime: time.Now().Unix(), } - if proxy != nil { + if proxy != nil && proxy.Type != "" { params.Proxy = proxy.Type + "://" + proxy.Proxy } browserInstanceService.Create(params) @@ -81,6 +113,7 @@ func (*BitBrowserService) CreateBrowser(proxy *models.Proxy) string { return "" } +// 获取当前浏览器的pid func (*BitBrowserService) GetPid(id string) int { // 获取当前 bitBrowserRequest := utils.NewBitBrowserRequest() @@ -151,9 +184,9 @@ func (bit *BitBrowserService) Register() *libs.ErrorInfo { var browserId string // 创建新的浏览器 if proxy != nil { - browserId = bit.CreateBrowser(proxy) + browserId = bit.CreateBrowser(proxy, nil) } else { - browserId = bit.CreateBrowser(nil) + browserId = bit.CreateBrowser(nil, nil) } // 打开浏览器 if browserId == "" { @@ -167,15 +200,19 @@ func (bit *BitBrowserService) Register() *libs.ErrorInfo { emailService := InitEmailService() emailInfo := emailService.GetEmailOne() + log.Println("当前邮箱信息", emailInfo) obj := Browser.GetBitBrowser(ws) page := obj.Page page.MustNavigate("https://x.com/i/flow/signup").MustWaitLoad() + // 点击创建账号 + createAccount := page.Timeout(5 * time.Second).MustElement(`button.css-175oi2r.r-sdzlij.r-1phboty.r-rs99b7.r-lrvibr.r-ywje51.r-184id4b.r-13qz1uu.r-2yi16.r-1qi8awa.r-3pj75a.r-1loqt21.r-o7ynqc.r-6416eg.r-1ny4l3l`) + createAccount.MustClick() // 判断当前是否是邮箱注册 - _, err := page.Element(`input[name="email"]`) - if err != nil { - switchButton := page.Timeout(5 * time.Second).MustElement(`button.css-146c3p1.r-bcqeeo.r-qvutc0.r-1qd0xha.r-1ff274t.r-a023e6.r-rjixqe.r-16dba41`) - switchButton.MustClick() - } + // _, err := page.Element(`input[name="email"]`) + // if err != nil { + switchButton := page.Timeout(5 * time.Second).MustElement(`button.css-146c3p1.r-bcqeeo.r-qvutc0.r-37j5jr.r-1ff274t.r-a023e6.r-rjixqe.r-16dba41`) + switchButton.MustClick() + // } // 获取邮箱输入框 inputEmail := page.Timeout(5 * time.Second).MustElement(`input[name="email"]`) inputEmail.MustFocus() @@ -189,7 +226,7 @@ func (bit *BitBrowserService) Register() *libs.ErrorInfo { } inputName.MustInput(nameStr) // 随机生成生日 - dateParams := utils.RandDate(18, 100) + dateParams := utils.RandDate(18, 80) dateSelects, _ := page.Timeout(5 * time.Second).Elements(`select.r-30o5oe.r-1niwhzg.r-17gur6a.r-1yadl64.r-18jsvk2.r-1loqt21.r-1inkyih.r-rjixqe.r-crgep1.r-1wzrnnt.r-1ny4l3l.r-t60dpp.r-xd6kpl.r-is05cd.r-ttdzmv`) // 月份 dateSelects[0].WaitVisible() @@ -206,13 +243,24 @@ func (bit *BitBrowserService) Register() *libs.ErrorInfo { ocfSignupNextLink.MustClick() // 判断是否需要验证码 time.Sleep(4 * time.Second) - codeInput, err1 := page.Timeout(5 * time.Second).Element(`input[name="verfication_code"]`) + _, err1 := page.Timeout(5 * time.Second).Element(`input[name="verfication_code"]`) if err1 != nil { // res := getApiResponse(page, "") } //TODO 对接获取邮箱信息接口 + time.Sleep(10 * time.Second) + codeValue := LoopGetEmailCode("pop.dragonsmail.com:993", emailInfo.Email, emailInfo.Password) + if codeValue == "0" { + emailService.Update(emailInfo.Email, models.Email{ + Status: 0, + }) + return libs.ErrorCode["MailboxUnavailable"] + } // 获取邮箱验证码 - codeInput.MustInput("") + log.Println("获取当前验证码", codeValue) + codeInput1, _ := page.Timeout(5 * time.Second).Element(`input[name="verfication_code"]`) + codeInput1.MustInput(codeValue) + time.Sleep(2 * time.Second) // 点击下一步 codeNextButton := page.Timeout(5 * time.Second).MustElement(`button.css-175oi2r.r-sdzlij.r-1phboty.r-rs99b7.r-lrvibr.r-19yznuf.r-64el8z.r-1fkl15p.r-1loqt21.r-o7ynqc.r-6416eg.r-1ny4l3l`) codeNextButton.MustClick() @@ -224,12 +272,15 @@ func (bit *BitBrowserService) Register() *libs.ErrorInfo { if err2 != nil { // res := getApiResponse(page, "") } + passwordInput.MustInput(emailInfo.Password) + time.Sleep(5 * time.Second) // 点击下一步 loginButton := page.Timeout(5 * time.Second).MustElement(`button[data-testid="LoginForm_Login_Button"]`) loginButton.MustClick() time.Sleep(5 * time.Second) page.MustNavigate("https://x.com").MustWaitLoad() + time.Sleep(10 * time.Second) // 获取cookie cookieArr := getPageCookie(page, "https://x.com") // 创建用户 @@ -243,13 +294,15 @@ func (bit *BitBrowserService) Register() *libs.ErrorInfo { Status: "注册成功", LoginStatus: "登录成功", Proxy: func() string { - if proxy != nil { + if proxy != nil && proxy.Type != "" { return proxy.Type + "://" + proxy.Proxy } return "" }(), }) - + emailService.Update(emailInfo.Email, models.Email{ + UseStatus: 1, + }) // 删除浏览器 bit.DeleteBrowser(browserId) return libs.ErrorCode["RegisterSuccessful"] @@ -273,3 +326,20 @@ func getPageCookie(page *rod.Page, url string) []*proto.NetworkCookieParam { } return cookies } + +// 循环获取3次验证码 +func LoopGetEmailCode(server, email, password string) string { + var emailImap utils.EmailImap + for i := 0; i < 3; i++ { + codeValue := emailImap.GetMailCode(server, email, password) + if codeValue != "0" { + return codeValue + } + time.Sleep(5 * time.Second) + } + return "0" +} + +func (*BitBrowserService) AccountUnlock(id int64) *libs.ErrorInfo { + return libs.ErrorCode["UnlockedSuccessfully"] +} diff --git a/services/email_service.go b/services/email_service.go index 77f2adf..65f0afb 100644 --- a/services/email_service.go +++ b/services/email_service.go @@ -19,9 +19,13 @@ func InitEmailService() *EmailService { func (e *EmailService) GetEmailOne() *models.Email { var email models.Email - err := e.db.Model(email).Where("use_status = ?", 0).First(&email).Error + err := e.db.Model(email).Where("use_status = ? and status=?", 0, 1).First(&email).Error if err != nil { return nil } return &email } + +func (e *EmailService) Update(email string, params models.Email) { + e.db.Model(&models.Email{}).Where("email = ?", email).Updates(params) +} diff --git a/services/twitter_account_service.go b/services/twitter_account_service.go index fc4bfd4..56cd733 100644 --- a/services/twitter_account_service.go +++ b/services/twitter_account_service.go @@ -22,3 +22,62 @@ func (t *TwitterAccountService) Create(params *models.TwitterAccount) { params.UpdateTime = time.Now().Unix() t.db.Create(params) } + +func (t *TwitterAccountService) GetInfo(id int64) *models.TwitterAccount { + var account models.TwitterAccount + result := t.db.Model(&models.TwitterAccount{}).Where("id = ?", id).First(&account) + if result.Error != nil { + return nil + } + return &account +} + +// 更改账号状态 +func (u *TwitterAccountService) ChangeAccountStatus(accountId int, status string) bool { + u.db.Model(&models.TwitterAccount{}).Where("id = ?", accountId).Updates(map[string]interface{}{"status": status, "update_time": time.Now().Unix()}) + return true +} + +// 更改账号登录状态 +func (u *TwitterAccountService) ChangeAccountLoginStatus(accountId int, loginStatus string) bool { + u.db.Model(&models.TwitterAccount{}).Where("id = ?", accountId).Updates(map[string]interface{}{"login_status": loginStatus, "update_time": time.Now().Unix()}) + return true +} + +// 下线所有账号 +func (u *TwitterAccountService) OfflineAllAccount() bool { + u.db.Model(&models.TwitterAccount{}).Where("login_status = ?", "登录成功").Updates(&models.TwitterAccount{ + LoginStatus: "账号下线", + UpdateTime: time.Now().Unix(), + }) + return true +} + +// 获取可用账号 +func (u *TwitterAccountService) GetAvailableAccount(params *models.TwitterAccount) []models.TwitterAccount { + var users []models.TwitterAccount + res := u.db.Model(&models.TwitterAccount{}).Where(`status = ? AND (login_status != "账号异常" OR login_status IS NULL)`, params.Status) + // if params.Username != "" { + // res.Where("username LIKE ?", "%"+params.Username+"%") + // } + + res.Order("id DESC").Find(&users) + return users +} + +// 获取养号未运行或者运行失败的账号 +func (u *TwitterAccountService) GetAccountListByStatus(params *models.TwitterAccount) []models.TwitterAccount { + var users []models.TwitterAccount + res := u.db.Model(&models.TwitterAccount{}).Where(`status = ? AND (login_status != "账号异常" OR login_status IS NULL)`, params.Status) + // if params.Username != "" { + // res.Where("username LIKE ?", "%"+params.Username+"%") + // } + res.Order("id DESC").Find(&users) + return users +} + +// 清空状态字段 +func (u *TwitterAccountService) ClearAccountStatus(params *models.TwitterAccount) bool { + u.db.Model(&models.TwitterAccount{}).Where("status = ?", params.Status).Update("status", nil) + return true +} diff --git a/services/twitter_service.go b/services/twitter_service.go new file mode 100644 index 0000000..51e0ae0 --- /dev/null +++ b/services/twitter_service.go @@ -0,0 +1,88 @@ +package services + +import ( + "context" + "errors" + "fmt" + "go-account-register/libs" + "log" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/cdp" +) + +type TwitterService struct{} + +// 登录 +func (*TwitterService) Login(id int) *libs.ErrorInfo { + UserService := InitTwitterAccountService() + user := UserService.GetInfo(int64(id)) + obj := Browser.GetBrowser(id, &libs.Fingerprint{ + Proxy: user.Proxy, + }) + browser := obj.Browser + page := obj.Page + + err3 := rod.Try(func() { + // 创建 Cookie 对象 + cookies := user.Cookies + log.Println("获取cookie", cookies) + // 全局设置 + browser.SetCookies(cookies) + page.MustNavigate("https://x.com/home").MustWaitLoad() + page.MustWaitNavigation() + }) + if e, ok := err3.(*cdp.Error); ok { + if e.Code == -32000 && e.Message == "Inspected target navigated or closed" { + Browser.CancelBrowser(id) + return libs.ErrorCode["LoginFailed"] + } + } + + time.Sleep(3 * time.Second) + url := page.MustEval("() => window.location.href").String() + if url == "https://x.com/account/access" { + Browser.CancelBrowser(id) + return libs.ErrorCode["AccountLocked"] + } + log.Println(url) + err1 := rod.Try(func() { + page.Timeout(5 * time.Second).MustElement(`input[autocomplete="username"]`) + }) + if err1 == nil { + Browser.CancelBrowser(id) + return libs.ErrorCode["LoginFailed"] + } + // 是否检测到侧边栏 + err2 := rod.Try(func() { + page.Timeout(5 * time.Second).MustElement(`a[data-testid="AppTabBar_Profile_Link"]`) + }) + + if err2 != nil { + Browser.CancelBrowser(id) + return libs.ErrorCode["LoginFailed"] + } + + // 是否检测到帖子列表 + err := rod.Try(func() { + page.Timeout(5 * time.Second).MustElement(`article[data-testid="tweet"]`) + }) + + if errors.Is(err, context.DeadlineExceeded) { + fmt.Println("请求空闲等待超时!") + // 关闭浏览器 + Browser.CancelBrowser(id) + return libs.ErrorCode["NetworkTimeout"] + } + if err != nil { + fmt.Println(err) + } + + return libs.ErrorCode["LoginSuccessful"] +} + +// 退出登录 +func (*TwitterService) Logout(id int) { + Browser.CancelBrowser(id) +} diff --git a/utils/email_imap.go b/utils/email_imap.go index 27bf878..a50198f 100644 --- a/utils/email_imap.go +++ b/utils/email_imap.go @@ -1,6 +1,7 @@ package utils import ( + "crypto/tls" "fmt" "io" "log" @@ -18,7 +19,8 @@ type EmailImap struct { // 获取最新一封邮件的正文内容 func (*EmailImap) GetLatestMailContent(server, username, password string) (string, error) { // 1. 连接 IMAP 服务器 (SSL/TLS, 端口 993) - c, err := client.DialTLS(server, nil) + tlsConfig := &tls.Config{InsecureSkipVerify: true} + c, err := client.DialTLS(server, tlsConfig) if err != nil { return "", fmt.Errorf("连接失败: %v", err) } @@ -66,7 +68,8 @@ func (*EmailImap) GetLatestMailContent(server, username, password string) (strin return "", fmt.Errorf("解析邮件失败: %v", err) } - var content string + var htmlContent, textContent string + for { p, err := mr.NextPart() if err == io.EOF { @@ -76,16 +79,23 @@ func (*EmailImap) GetLatestMailContent(server, username, password string) (strin return "", fmt.Errorf("读取邮件失败: %v", err) } - switch p.Header.(type) { + switch h := p.Header.(type) { case *mail.InlineHeader: + mediaType, _, _ := h.ContentType() b, _ := io.ReadAll(p.Body) - content = strings.TrimSpace(string(b)) - // 只取第一个正文即可 - return content, nil + if strings.HasPrefix(mediaType, "text/html") { + htmlContent = string(b) + } else if strings.HasPrefix(mediaType, "text/plain") { + textContent = string(b) + } } } - return content, nil + // 优先返回 HTML,没有就退回纯文本 + if htmlContent != "" { + return htmlContent, nil + } + return textContent, nil } func (e *EmailImap) GetMailCode(server, username, password string) string { @@ -95,7 +105,6 @@ func (e *EmailImap) GetMailCode(server, username, password string) string { } htmlStr := res - fmt.Println(htmlStr) doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlStr)) if err != nil { @@ -112,7 +121,7 @@ func (e *EmailImap) GetMailCode(server, username, password string) string { if code == "" { return "0" } - return code + return strings.TrimSpace(code) } func test() {