├── LICENSE ├── README.md ├── assets └── X_logo.jpg ├── client ├── account.go ├── addons │ ├── extract_tweet_id_from_string.go │ ├── get_twitter_username.go │ └── set_auth_cookies.go ├── client.go ├── comment.go ├── follow.go ├── get_user_info_by_username.go ├── like.go ├── poll.go ├── retweet.go ├── tweet.go ├── unfollow.go └── upload_media.go ├── go.mod ├── go.sum ├── main.go ├── models ├── account.go ├── config.go ├── constants.go ├── like.go ├── tweet.go └── user.go ├── twitter_utils └── generator.go └── utils ├── cookie_client.go ├── helpers.go ├── http_client.go ├── logger.go └── random.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 0xStarLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # 🐦 Twitter API SDK 6 | 7 | ### A Modern, Feature-Rich Go SDK for Twitter API Automation 8 | 9 | [![Stars](https://img.shields.io/github/stars/0xStarLabs/TwitterAPI?style=for-the-badge&logo=github&color=yellow)](https://github.com/0xStarLabs/TwitterAPI/stargazers) 10 | [![Watchers](https://img.shields.io/github/watchers/0xStarLabs/TwitterAPI?style=for-the-badge&logo=github)](https://github.com/0xStarLabs/TwitterAPI/watchers) 11 | ![Twitter API](https://img.shields.io/badge/Twitter-API-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white) 12 | ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.23-00ADD8?style=for-the-badge&logo=go&logoColor=white) 13 | [![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](LICENSE) 14 | 15 |

16 | Fast 🚀 Reliable 🛡️ Easy to Use 💡 17 |

18 | 19 |

20 | A powerful Go SDK that provides seamless interaction with Twitter's API. Built with performance and ease of use in mind. 21 |

22 | 23 |
24 | 25 | --- 26 | 27 | ## Features 28 | 29 | - 🔐 **Account Management** 30 | - Account validation 31 | - Support for auth tokens and JSON cookies 32 | - Proxy support 33 | - 👥 **User Interactions** 34 | - Follow/Unfollow users 35 | - Like/Retweet tweets 36 | - Vote on polls 37 | - Get user information 38 | - Handle both usernames and user IDs 39 | - 📝 **Content Creation** 40 | - Post tweets 41 | - Reply to tweets 42 | - Add media to posts 43 | - ⚡ **Performance** 44 | - Efficient cookie management 45 | - Automatic CSRF token handling 46 | - Smart error handling 47 | 48 | ## Installation 49 | 50 | ```bash 51 | go get github.com/0xStarLabs/TwitterAPI 52 | ``` 53 | 54 | ## Quick Start 55 | 56 | ```go 57 | package main 58 | 59 | import ( 60 | "fmt" 61 | "github.com/0xStarLabs/TwitterAPI/client" 62 | ) 63 | 64 | func main() { 65 | // Create a new account 66 | account := client.NewAccount("auth_token_here", "", "") 67 | 68 | // Initialize Twitter client 69 | twitter, err := client.NewTwitter(account) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | // Check if account is valid 75 | info, resp := twitter.IsValid() 76 | if resp.Success { 77 | fmt.Printf("Account %s is valid\n", info.Username) 78 | } 79 | } 80 | ``` 81 | 82 | ## Usage Examples 83 | 84 | ### Following Users 85 | 86 | ```go 87 | // Follow by username 88 | resp := twitter.Follow("username") 89 | if resp.Success { 90 | fmt.Println("Successfully followed user") 91 | } 92 | 93 | // Unfollow by username or ID 94 | resp = twitter.Unfollow("username") 95 | if resp.Success { 96 | fmt.Println("Successfully unfollowed user") 97 | } 98 | ``` 99 | 100 | ### Posting Comments 101 | 102 | ```go 103 | // Simple comment 104 | resp := twitter.Comment("Great tweet!", "1234567890", nil) 105 | 106 | // Comment with media 107 | resp = twitter.Comment("Check this out!", "1234567890", &client.CommentOptions{ 108 | MediaBase64: imageBase64, 109 | }) 110 | ``` 111 | 112 | ### Getting User Info 113 | 114 | ```go 115 | info, resp := twitter.GetUserInfoByUsername("username") 116 | if resp.Success { 117 | fmt.Printf("User ID: %s\n", info.Data.User.Result.RestID) 118 | fmt.Printf("Followers: %d\n", info.Data.User.Result.Legacy.FollowersCount) 119 | } 120 | ``` 121 | 122 | ## Advanced Configuration 123 | 124 | ### Using Proxies 125 | 126 | ```go 127 | account := client.NewAccount( 128 | "auth_token_here", 129 | "csrf_token", // optional 130 | "user:pass@host:port", // proxy 131 | ) 132 | ``` 133 | 134 | ### Using JSON Cookies 135 | 136 | ```go 137 | authToken, csrfToken, err := client.SetAuthCookies( 138 | 0, // account index 139 | cookieClient, 140 | `[{"name":"auth_token","value":"token"}]`, 141 | ) 142 | ``` 143 | 144 | ## Error Handling 145 | 146 | The SDK uses a consistent error handling pattern: 147 | 148 | ```go 149 | resp := twitter.Follow("username") 150 | if !resp.Success { 151 | switch resp.Status { 152 | case models.StatusAuthError: 153 | fmt.Println("Authentication failed") 154 | case models.StatusLocked: 155 | fmt.Println("Account is locked") 156 | default: 157 | fmt.Printf("Error: %v\n", resp.Error) 158 | } 159 | } 160 | ``` 161 | 162 | ## Contributing 163 | 164 | Contributions are welcome! Please feel free to submit a Pull Request. 165 | 166 | ## License 167 | 168 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 169 | 170 | ## Disclaimer 171 | 172 | This project is not affiliated with Twitter. Use at your own risk and ensure compliance with Twitter's Terms of Service. -------------------------------------------------------------------------------- /assets/X_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xStarLabs/TwitterAPI/1ff5f12ec8162b81fbac1385401495cb2c54939b/assets/X_logo.jpg -------------------------------------------------------------------------------- /client/account.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/models" 9 | "github.com/0xStarLabs/TwitterAPI/utils" 10 | ) 11 | 12 | // NewAccount creates a new Twitter account instance. 13 | // 14 | // Parameters: 15 | // - authToken: the auth token for the account (required) 16 | // - ct0: the x-csrf-token for the account (optional, "" by default) 17 | // - proxy: the proxy in user:pass@host:port format (optional, "" by default) 18 | // 19 | // Returns: 20 | // - Account: the configured account instance 21 | // 22 | // Example: 23 | // 24 | // // Create account with just auth token 25 | // account := twitter.NewAccount("auth_token_here", "", "") 26 | // 27 | // // Create account with auth token and proxy 28 | // account := twitter.NewAccount("auth_token_here", "", "user:pass@host:port") 29 | // 30 | // // Create account with all parameters 31 | // account := twitter.NewAccount("auth_token_here", "csrf_token", "user:pass@host:port") 32 | func NewAccount(authToken, ct0, proxy string) *models.Account { 33 | return &models.Account{ 34 | AuthToken: authToken, 35 | Ct0: ct0, 36 | Proxy: proxy, 37 | } 38 | } 39 | 40 | // AccountInfo represents detailed information about a Twitter account 41 | type AccountInfo struct { 42 | Name string 43 | Username string 44 | CreationDate string 45 | Suspended bool 46 | Protected bool 47 | Verified bool 48 | FollowedBy bool 49 | Followers int 50 | IsFollowing bool 51 | FriendsCount int 52 | TweetCount int 53 | } 54 | 55 | // User represents a user entry in the multi-user response 56 | type User struct { 57 | UserID string `json:"user_id"` 58 | Name string `json:"name"` 59 | ScreenName string `json:"screen_name"` 60 | AvatarURL string `json:"avatar_image_url"` 61 | IsSuspended bool `json:"is_suspended"` 62 | IsVerified bool `json:"is_verified"` 63 | IsProtected bool `json:"is_protected"` 64 | IsAuthValid bool `json:"is_auth_valid"` 65 | } 66 | 67 | // MultiUserResponse represents the response from Twitter's multi-user list endpoint 68 | type MultiUserResponse struct { 69 | Users []User `json:"users"` 70 | } 71 | 72 | // IsValid checks if the account is valid and retrieves its status. 73 | // 74 | // Returns: 75 | // - AccountInfo: containing account details like: 76 | // - Username and display name 77 | // - Account status (suspended, protected, verified) 78 | // - ActionResponse: containing: 79 | // - Success: true if check was successful 80 | // - Error: any error that occurred 81 | // - Status: the status of the action 82 | // 83 | // Example: 84 | // 85 | // info, resp := twitter.IsValid() 86 | // if resp.Success { 87 | // if info.Suspended { 88 | // fmt.Println("Account is suspended") 89 | // } else { 90 | // fmt.Printf("Account %s is valid\n", info.Username) 91 | // } 92 | // } 93 | func (t *Twitter) IsValid() (*AccountInfo, *models.ActionResponse) { 94 | baseURL := fmt.Sprintf("https://api.x.com/1.1/account/multi/list.json") 95 | // Create request config 96 | reqConfig := utils.DefaultConfig() 97 | reqConfig.Method = "GET" 98 | reqConfig.URL = baseURL 99 | reqConfig.Headers = append(reqConfig.Headers, 100 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 101 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 102 | utils.HeaderPair{Key: "content-type", Value: "application/x-www-form-urlencoded"}, 103 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 104 | utils.HeaderPair{Key: "origin", Value: "https://x.com"}, 105 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 106 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 107 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 108 | utils.HeaderPair{Key: "user-agent", Value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"}, 109 | ) 110 | 111 | // Make the request 112 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 113 | if err != nil { 114 | t.Logger.Error("%s | Failed to get account info: %v", t.Account.Username, err) 115 | return nil, &models.ActionResponse{ 116 | Success: false, 117 | Error: err, 118 | Status: models.StatusUnknown, 119 | } 120 | } 121 | 122 | // Update cookies 123 | t.Cookies.SetCookieFromResponse(resp) 124 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 125 | t.Account.Ct0 = newCt0 126 | } 127 | 128 | bodyString := string(bodyBytes) 129 | 130 | // Handle successful responses 131 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 132 | var response MultiUserResponse 133 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 134 | t.Logger.Error("%s | Failed to parse account info response: %v", t.Account.Username, err) 135 | return nil, &models.ActionResponse{ 136 | Success: false, 137 | Error: err, 138 | Status: models.StatusUnknown, 139 | } 140 | } 141 | 142 | // Find the current user in the response 143 | var currentUser *User 144 | for _, user := range response.Users { 145 | if strings.EqualFold(user.ScreenName, t.Account.Username) { 146 | currentUser = &user 147 | break 148 | } 149 | } 150 | 151 | if currentUser == nil { 152 | return nil, &models.ActionResponse{ 153 | Success: false, 154 | Error: fmt.Errorf("account not found in response"), 155 | Status: models.StatusUnknown, 156 | } 157 | } 158 | 159 | // Check if account is valid and not suspended 160 | if !currentUser.IsAuthValid { 161 | t.Logger.Error("%s | Account authentication is invalid", t.Account.Username) 162 | return &AccountInfo{ 163 | Username: currentUser.ScreenName, 164 | Suspended: currentUser.IsSuspended, 165 | }, &models.ActionResponse{ 166 | Success: false, 167 | Error: models.ErrAuthFailed, 168 | Status: models.StatusAuthError, 169 | } 170 | } 171 | 172 | info := &AccountInfo{ 173 | Username: currentUser.ScreenName, 174 | Name: currentUser.Name, 175 | Suspended: currentUser.IsSuspended, 176 | Protected: currentUser.IsProtected, 177 | Verified: currentUser.IsVerified, 178 | } 179 | 180 | if info.Suspended { 181 | t.Logger.Warning("%s | Account is suspended", t.Account.Username) 182 | } else { 183 | t.Logger.Success("%s | Account is active and valid", t.Account.Username) 184 | } 185 | 186 | return info, &models.ActionResponse{ 187 | Success: true, 188 | Status: models.StatusSuccess, 189 | } 190 | } 191 | 192 | // Handle error responses 193 | switch { 194 | case strings.Contains(bodyString, "this account is temporarily locked"): 195 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 196 | return nil, &models.ActionResponse{ 197 | Success: false, 198 | Error: models.ErrAccountLocked, 199 | Status: models.StatusLocked, 200 | } 201 | case strings.Contains(bodyString, "Could not authenticate you"): 202 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 203 | return nil, &models.ActionResponse{ 204 | Success: false, 205 | Error: models.ErrAuthFailed, 206 | Status: models.StatusAuthError, 207 | } 208 | case strings.Contains(bodyString, "User has been suspended"): 209 | t.Logger.Error("%s | Account is suspended", t.Account.Username) 210 | return &AccountInfo{ 211 | Username: t.Account.Username, 212 | Suspended: true, 213 | }, &models.ActionResponse{ 214 | Success: true, 215 | Status: models.StatusSuccess, 216 | } 217 | default: 218 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 219 | return nil, &models.ActionResponse{ 220 | Success: false, 221 | Error: fmt.Errorf("unknown response: %s", bodyString), 222 | Status: models.StatusUnknown, 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /client/addons/extract_tweet_id_from_string.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/0xStarLabs/TwitterAPI/utils" 8 | ) 9 | 10 | // ExtractTweetID extracts the numeric tweet ID from a tweet URL or ID string. 11 | // 12 | // Parameters: 13 | // - tweetLink: can be a full tweet URL or just the ID 14 | // - username: account username for logging purposes 15 | // - logger: logger instance for error reporting 16 | // 17 | // Returns: 18 | // - string: the extracted tweet ID 19 | // - error: any error that occurred during extraction 20 | // 21 | // Example: 22 | // 23 | // // Extract from URL 24 | // id, err := ExtractTweetID("https://twitter.com/user/status/1234567890", username, logger) 25 | // 26 | // // Extract from ID string 27 | // id, err := ExtractTweetID("1234567890", username, logger) 28 | func ExtractTweetID(tweetLink string, username string, logger utils.Logger) (string, error) { 29 | tweetLink = strings.TrimSpace(tweetLink) 30 | 31 | var tweetID string 32 | if strings.Contains(tweetLink, "tweet_id=") { 33 | parts := strings.Split(tweetLink, "tweet_id=") 34 | tweetID = strings.Split(parts[1], "&")[0] 35 | } else if strings.Contains(tweetLink, "?") { 36 | parts := strings.Split(tweetLink, "status/") 37 | tweetID = strings.Split(parts[1], "?")[0] 38 | } else if strings.Contains(tweetLink, "status/") { 39 | parts := strings.Split(tweetLink, "status/") 40 | tweetID = parts[1] 41 | } else { 42 | logger.Error("%s | Failed to get tweet ID from your link: %s", username, tweetLink) 43 | return "", fmt.Errorf("failed to get tweet ID from your link: %s", tweetLink) 44 | } 45 | 46 | return tweetID, nil 47 | } -------------------------------------------------------------------------------- /client/addons/get_twitter_username.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/models" 9 | "github.com/0xStarLabs/TwitterAPI/utils" 10 | tlsClient "github.com/bogdanfinn/tls-client" 11 | ) 12 | 13 | // GetTwitterUsername retrieves the username of a Twitter account. 14 | // 15 | // Parameters: 16 | // - httpClient: the HTTP client to make requests with 17 | // - cookieClient: manages cookies for the request 18 | // - config: contains Twitter API configuration and constants 19 | // - logger: handles logging of operations 20 | // - csrfToken: CSRF token for request authentication 21 | // 22 | // Returns: 23 | // - string: the account's username (empty if error) 24 | // - string: new CSRF token from response 25 | // - error: any error that occurred, including account status errors 26 | // - models.ActionStatus: the status of the account 27 | func GetTwitterUsername(httpClient tlsClient.HttpClient, cookieClient *utils.CookieClient, config *models.Config, logger utils.Logger, csrfToken string) (string, string, error, models.ActionStatus) { 28 | for i := 0; i < config.MaxRetries; i++ { 29 | if i > 0 { // Don't sleep on first try 30 | utils.RandomSleep(1, 5) 31 | } 32 | 33 | // Build URL with query parameters 34 | baseURL := "https://api.x.com/graphql/UhddhjWCl-JMqeiG4vPtvw/Viewer" 35 | params := url.Values{} 36 | params.Add("variables", `{"withCommunitiesMemberships":true}`) 37 | params.Add("features", `{"rweb_tipjar_consumption_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true}`) 38 | params.Add("fieldToggles", `{"isDelegate":false,"withAuxiliaryUserLabels":false}`) 39 | fullURL := baseURL + "?" + params.Encode() 40 | 41 | // Create request config with required headers 42 | reqConfig := utils.DefaultConfig() 43 | reqConfig.Method = "GET" 44 | reqConfig.URL = fullURL 45 | reqConfig.Headers = append(reqConfig.Headers, 46 | utils.HeaderPair{Key: "authorization", Value: config.Constants.BearerToken}, 47 | utils.HeaderPair{Key: "cookie", Value: cookieClient.CookiesToHeader()}, 48 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 49 | utils.HeaderPair{Key: "referer", Value: "https://twitter.com/"}, 50 | utils.HeaderPair{Key: "x-csrf-token", Value: csrfToken}, 51 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "no"}, 52 | utils.HeaderPair{Key: "user-agent", Value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"}, 53 | ) 54 | 55 | bodyBytes, resp, err := utils.MakeRequest(httpClient, reqConfig) 56 | if err != nil { 57 | logger.Warning("Unknown | Failed to make get username request: %s", err.Error()) 58 | continue 59 | } 60 | 61 | // Update cookies from response 62 | cookieClient.SetCookieFromResponse(resp) 63 | 64 | // Get new CSRF token 65 | newCsrfToken, ok := cookieClient.GetCookieValue("ct0") 66 | if !ok { 67 | logger.Error("Unknown | Failed to get new csrf token") 68 | continue 69 | } 70 | 71 | // Parse response and handle different account states 72 | switch { 73 | case strings.Contains(string(bodyBytes), "screen_name"): 74 | var responseData getUsernameJSON 75 | if err := json.Unmarshal(bodyBytes, &responseData); err != nil { 76 | logger.Error("Unknown | Failed to unmarshal response: %s", err.Error()) 77 | continue 78 | } 79 | username := responseData.Data.Viewer.UserResults.Result.Legacy.ScreenName 80 | logger.Success("%s | Successfully got username", username) 81 | return username, newCsrfToken, nil, models.StatusSuccess 82 | 83 | case strings.Contains(string(bodyBytes), "this account is temporarily locked"): 84 | logger.Error("Unknown | Account is temporarily locked!") 85 | return "", newCsrfToken, models.ErrAccountLocked, models.StatusLocked 86 | 87 | case strings.Contains(string(bodyBytes), "Could not authenticate you"): 88 | logger.Error("Unknown | Could not authenticate you. Token is invalid!") 89 | return "", newCsrfToken, models.ErrInvalidToken, models.StatusAuthError 90 | 91 | default: 92 | logger.Error("Unknown | Unknown response: %s", string(bodyBytes)) 93 | } 94 | } 95 | 96 | logger.Error("Unknown | Unable to get twitter username after %d retries", config.MaxRetries) 97 | return "", "", models.ErrUnknown, models.StatusUnknown 98 | } 99 | 100 | // getUsernameJSON represents the JSON response structure from Twitter's GraphQL API 101 | type getUsernameJSON struct { 102 | Data struct { 103 | Viewer struct { 104 | UserResults struct { 105 | Result struct { 106 | Legacy struct { 107 | ScreenName string `json:"screen_name"` 108 | } `json:"legacy"` 109 | } `json:"result"` 110 | } `json:"user_results"` 111 | } `json:"viewer"` 112 | } `json:"data"` 113 | } 114 | -------------------------------------------------------------------------------- /client/addons/set_auth_cookies.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/0xStarLabs/TwitterAPI/twitter_utils" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | http "github.com/bogdanfinn/fhttp" 12 | ) 13 | 14 | // SetAuthCookies sets authentication cookies for a Twitter client. 15 | // Supports both JSON cookie format and simple auth token. 16 | // 17 | // Parameters: 18 | // - accountIndex: index of the account for logging purposes 19 | // - cookieClient: client's cookie manager 20 | // - twitterAuth: either JSON cookies or auth token string 21 | // 22 | // Returns: 23 | // - string: auth token 24 | // - string: CSRF token 25 | // - error: any error that occurred 26 | // 27 | // Example: 28 | // 29 | // // Using auth token 30 | // authToken, csrfToken, err := SetAuthCookies(0, cookieClient, "auth_token_here") 31 | // 32 | // // Using JSON cookies 33 | // authToken, csrfToken, err := SetAuthCookies(0, cookieClient, "[{\"name\":\"auth_token\",\"value\":\"token\"}]") 34 | func SetAuthCookies(accountIndex int, cookieClient *utils.CookieClient, twitterAuth string) (string, string, error) { 35 | csrfToken := "" 36 | authToken := "" 37 | var err error 38 | 39 | // json cookies 40 | if strings.Contains(twitterAuth, "[") && strings.Contains(twitterAuth, "]") { 41 | jsonPart := strings.Split(strings.Split(twitterAuth, "[")[1], "]")[0] 42 | var cookiesJson []map[string]string 43 | if err := json.Unmarshal([]byte(jsonPart), &cookiesJson); err != nil { 44 | return "", "", fmt.Errorf("%d | Failed to decode account json cookies: %v", accountIndex, err) 45 | } 46 | 47 | for _, cookie := range cookiesJson { 48 | if name, ok := cookie["name"]; ok { 49 | value := cookie["value"] 50 | cookieClient.AddCookies([]http.Cookie{{Name: name, Value: value}}) 51 | if name == "ct0" { 52 | csrfToken = value 53 | } 54 | if name == "auth_token" { 55 | authToken = value 56 | } 57 | } 58 | } 59 | 60 | // auth token 61 | } else if len(twitterAuth) < 60 { 62 | csrfToken, err = twitter_utils.GenerateCSRFToken() 63 | if err != nil { 64 | return "", "", fmt.Errorf("%d | Failed to generate CSRF token: %v", accountIndex, err) 65 | } 66 | 67 | cookieClient.AddCookies([]http.Cookie{ 68 | {Name: "auth_token", Value: twitterAuth}, 69 | {Name: "ct0", Value: csrfToken}, 70 | {Name: "des_opt_in", Value: "Y"}, 71 | }) 72 | authToken = twitterAuth 73 | } 74 | 75 | if csrfToken == "" { 76 | return "", "", errors.New("failed to get csrf token") 77 | } 78 | 79 | return authToken, csrfToken, nil 80 | } 81 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/0xStarLabs/TwitterAPI/client/addons" 7 | "github.com/0xStarLabs/TwitterAPI/models" 8 | "github.com/0xStarLabs/TwitterAPI/utils" 9 | tlsClient "github.com/bogdanfinn/tls-client" 10 | ) 11 | 12 | // Twitter represents a Twitter API client instance 13 | type Twitter struct { 14 | Account *models.Account 15 | Client tlsClient.HttpClient 16 | Logger utils.Logger 17 | Config *models.Config 18 | Cookies *utils.CookieClient 19 | } 20 | 21 | // NewTwitter creates a new Twitter API client instance 22 | func NewTwitter(account *models.Account, config *models.Config) (*Twitter, error) { 23 | // If no config provided, use default 24 | if config == nil { 25 | config = models.NewConfig() 26 | } 27 | 28 | twitter := &Twitter{ 29 | Account: account, 30 | Logger: utils.NewLogger(config.LogLevel), 31 | Config: config, 32 | Cookies: utils.NewCookieClient(), 33 | } 34 | 35 | // Initialize the client 36 | if err := twitter.init(); err != nil { 37 | return nil, fmt.Errorf("failed to initialize Twitter client: %w", err) 38 | } 39 | 40 | return twitter, nil 41 | } 42 | 43 | // init initializes the Twitter client 44 | func (t *Twitter) init() error { 45 | for i := 0; i < t.Config.MaxRetries; i++ { 46 | if i > 0 { // Don't sleep on first try 47 | utils.RandomSleep(1, 5) 48 | } 49 | 50 | // Create HTTP client 51 | client, err := utils.CreateHttpClient(t.Account.Proxy) 52 | if err != nil { 53 | t.Logger.Error("Failed to create HTTP client: %s", err) 54 | continue 55 | } 56 | t.Client = client 57 | 58 | // Set auth cookies 59 | authToken, ct0, err := addons.SetAuthCookies(i, t.Cookies, t.Account.AuthToken) 60 | if err != nil { 61 | t.Logger.Error("Failed to set auth cookies: %s", err) 62 | continue 63 | } 64 | t.Account.AuthToken = authToken 65 | t.Account.Ct0 = ct0 66 | 67 | // Get username and verify account 68 | username, newCsrfToken, err, status := addons.GetTwitterUsername(t.Client, t.Cookies, t.Config, t.Logger, t.Account.Ct0) 69 | if err != nil { 70 | switch status { 71 | case models.StatusLocked: 72 | return fmt.Errorf("account is locked: %w", err) 73 | case models.StatusAuthError: 74 | return fmt.Errorf("authentication failed: %w", err) 75 | case models.StatusInvalidToken: 76 | return fmt.Errorf("invalid token: %w", err) 77 | case models.StatusUnknown: 78 | t.Logger.Error("Unknown error getting username: %s", err) 79 | continue 80 | } 81 | } 82 | 83 | // Update account info 84 | t.Account.Username = username 85 | t.Account.Ct0 = newCsrfToken 86 | 87 | t.Logger.Success("%s | Successfully initialized Twitter client and got username", username) 88 | return nil 89 | } 90 | 91 | return fmt.Errorf("failed to initialize after %d retries", t.Config.MaxRetries) 92 | } 93 | -------------------------------------------------------------------------------- /client/comment.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/client/addons" 9 | "github.com/0xStarLabs/TwitterAPI/models" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | ) 12 | 13 | // CommentOptions contains optional parameters for creating a comment. 14 | // Currently supports adding media (images) to comments. 15 | type CommentOptions struct { 16 | MediaBase64 string // Base64 encoded media (optional) 17 | } 18 | 19 | // Comment adds a comment to a tweet with optional media attachment. 20 | // 21 | // Parameters: 22 | // - content: the text content of the comment 23 | // - tweetID: the ID or URL of the tweet to comment on 24 | // - opts: optional parameters like media (can be nil) 25 | // 26 | // Returns an ActionResponse containing: 27 | // - Success: true if comment was posted 28 | // - Error: any error that occurred 29 | // - Status: the status of the action 30 | // 31 | // Example: 32 | // 33 | // // Simple comment 34 | // resp := twitter.Comment("Great tweet!", "1234567890", nil) 35 | // 36 | // // Comment with media 37 | // resp := twitter.Comment("Check this out!", "1234567890", &CommentOptions{ 38 | // MediaBase64: imageBase64, 39 | // }) 40 | // 41 | // if resp.Success { 42 | // fmt.Println("Successfully posted comment") 43 | // } 44 | func (t *Twitter) Comment(content string, tweetID string, opts *CommentOptions) *models.ActionResponse { 45 | // Extract tweet ID if URL was provided 46 | if strings.Contains(tweetID, "twitter.com") || strings.Contains(tweetID, "x.com") { 47 | var err error 48 | tweetID, err = addons.ExtractTweetID(tweetID, t.Account.Username, t.Logger) 49 | if err != nil { 50 | return &models.ActionResponse{ 51 | Success: false, 52 | Error: fmt.Errorf("invalid tweet URL: %w", err), 53 | Status: models.StatusUnknown, 54 | } 55 | } 56 | } 57 | 58 | // If media is provided, upload it first 59 | var mediaID string 60 | if opts != nil && opts.MediaBase64 != "" { 61 | var err error 62 | mediaID, err = t.UploadMedia(opts.MediaBase64) 63 | if err != nil { 64 | return &models.ActionResponse{ 65 | Success: false, 66 | Error: fmt.Errorf("failed to upload media: %w", err), 67 | Status: models.StatusUnknown, 68 | } 69 | } 70 | } 71 | 72 | // Build URL and request body 73 | baseURL := "https://twitter.com/i/api/graphql/" + t.Config.Constants.QueryID.Tweet + "/CreateTweet" 74 | 75 | // Build variables based on options 76 | variables := map[string]interface{}{ 77 | "tweet_text": content, 78 | "reply": map[string]interface{}{ 79 | "in_reply_to_tweet_id": tweetID, 80 | "exclude_reply_user_ids": []string{}, 81 | }, 82 | "dark_request": false, 83 | "semantic_annotation_ids": []string{}, 84 | } 85 | 86 | // Add media if provided 87 | if mediaID != "" { 88 | variables["media"] = map[string]interface{}{ 89 | "media_entities": []map[string]interface{}{ 90 | { 91 | "media_id": mediaID, 92 | "tagged_users": []string{}, 93 | }, 94 | }, 95 | "possibly_sensitive": false, 96 | } 97 | } else { 98 | variables["media"] = map[string]interface{}{ 99 | "media_entities": []string{}, 100 | "possibly_sensitive": false, 101 | } 102 | } 103 | 104 | // Build the full request body 105 | requestBody := map[string]interface{}{ 106 | "variables": variables, 107 | "features": map[string]interface{}{ 108 | "tweetypie_unmention_optimization_enabled": true, 109 | "responsive_web_edit_tweet_api_enabled": true, 110 | "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, 111 | "view_counts_everywhere_api_enabled": true, 112 | "longform_notetweets_consumption_enabled": true, 113 | "responsive_web_twitter_article_tweet_consumption_enabled": false, 114 | "tweet_awards_web_tipping_enabled": false, 115 | "freedom_of_speech_not_reach_fetch_enabled": true, 116 | "standardized_nudges_misinfo": true, 117 | "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, 118 | "longform_notetweets_rich_text_read_enabled": true, 119 | "longform_notetweets_inline_media_enabled": true, 120 | "responsive_web_graphql_exclude_directive_enabled": true, 121 | "verified_phone_label_enabled": false, 122 | "responsive_web_media_download_video_enabled": false, 123 | "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, 124 | "responsive_web_graphql_timeline_navigation_enabled": true, 125 | "c9s_tweet_anatomy_moderator_badge_enabled": true, 126 | "responsive_web_enhance_cards_enabled": true, 127 | "rweb_video_timestamps_enabled": true, 128 | }, 129 | "queryId": t.Config.Constants.QueryID.Tweet, 130 | } 131 | 132 | jsonBody, err := json.Marshal(requestBody) 133 | if err != nil { 134 | return &models.ActionResponse{ 135 | Success: false, 136 | Error: fmt.Errorf("failed to marshal request body: %w", err), 137 | Status: models.StatusUnknown, 138 | } 139 | } 140 | 141 | // Create request config 142 | reqConfig := utils.DefaultConfig() 143 | reqConfig.Method = "POST" 144 | reqConfig.URL = baseURL 145 | reqConfig.Body = strings.NewReader(string(jsonBody)) 146 | reqConfig.Headers = append(reqConfig.Headers, 147 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 148 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 149 | utils.HeaderPair{Key: "content-type", Value: "application/json"}, 150 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 151 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 152 | utils.HeaderPair{Key: "referer", Value: "https://twitter.com/compose/tweet"}, 153 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 154 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 155 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 156 | ) 157 | 158 | // Make the request 159 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 160 | if err != nil { 161 | t.Logger.Error("%s | Failed to comment: %v", t.Account.Username, err) 162 | return &models.ActionResponse{ 163 | Success: false, 164 | Error: err, 165 | Status: models.StatusUnknown, 166 | } 167 | } 168 | 169 | // Update cookies 170 | t.Cookies.SetCookieFromResponse(resp) 171 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 172 | t.Account.Ct0 = newCt0 173 | } 174 | 175 | bodyString := string(bodyBytes) 176 | 177 | // Handle successful responses 178 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 179 | if strings.Contains(bodyString, "duplicate") { 180 | t.Logger.Success("%s | Comment was already posted", t.Account.Username) 181 | return &models.ActionResponse{ 182 | Success: true, 183 | Status: models.StatusAlreadyDone, 184 | } 185 | } 186 | 187 | var response models.TweetGraphQLResponse 188 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 189 | t.Logger.Error("%s | Failed to parse comment response: %v", t.Account.Username, err) 190 | return &models.ActionResponse{ 191 | Success: false, 192 | Error: err, 193 | Status: models.StatusUnknown, 194 | } 195 | } 196 | 197 | if response.Data.CreateTweet.TweetResults.Result.RestID != "" { 198 | t.Logger.Success("%s | Successfully posted comment", t.Account.Username) 199 | return &models.ActionResponse{ 200 | Success: true, 201 | Status: models.StatusSuccess, 202 | } 203 | } 204 | } 205 | 206 | // Handle error responses 207 | switch { 208 | case strings.Contains(bodyString, "this account is temporarily locked"): 209 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 210 | return &models.ActionResponse{ 211 | Success: false, 212 | Error: models.ErrAccountLocked, 213 | Status: models.StatusLocked, 214 | } 215 | case strings.Contains(bodyString, "Could not authenticate you"): 216 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 217 | return &models.ActionResponse{ 218 | Success: false, 219 | Error: models.ErrAuthFailed, 220 | Status: models.StatusAuthError, 221 | } 222 | default: 223 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 224 | return &models.ActionResponse{ 225 | Success: false, 226 | Error: fmt.Errorf("unknown response: %s", bodyString), 227 | Status: models.StatusUnknown, 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /client/follow.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/0xStarLabs/TwitterAPI/models" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | ) 12 | 13 | // Follow follows a user by their username. 14 | // 15 | // Parameters: 16 | // - username: the Twitter username to follow 17 | // 18 | // Returns an ActionResponse containing: 19 | // - Success: true if follow was successful 20 | // - Error: any error that occurred 21 | // - Status: the status of the action (Success, AuthError, etc.) 22 | // 23 | // Example: 24 | // 25 | // resp := twitter.Follow("username") 26 | // if resp.Success { 27 | // fmt.Println("Successfully followed user") 28 | // } 29 | func (t *Twitter) Follow(username string) *models.ActionResponse { 30 | // Build URL and request body 31 | baseURL := "https://twitter.com/i/api/1.1/friendships/create.json" 32 | data := url.Values{} 33 | data.Set("include_profile_interstitial_type", "1") 34 | data.Set("include_blocking", "1") 35 | data.Set("include_blocked_by", "1") 36 | data.Set("include_followed_by", "1") 37 | data.Set("include_want_retweets", "1") 38 | data.Set("include_mute_edge", "1") 39 | data.Set("include_can_dm", "1") 40 | data.Set("include_can_media_tag", "1") 41 | data.Set("skip_status", "1") 42 | data.Set("screen_name", username) 43 | 44 | // Create request config 45 | reqConfig := utils.DefaultConfig() 46 | reqConfig.Method = "POST" 47 | reqConfig.URL = baseURL 48 | reqConfig.Body = strings.NewReader(data.Encode()) 49 | reqConfig.Headers = append(reqConfig.Headers, 50 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 51 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 52 | utils.HeaderPair{Key: "content-type", Value: "application/x-www-form-urlencoded"}, 53 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 54 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 55 | utils.HeaderPair{Key: "referer", Value: fmt.Sprintf("https://twitter.com/%s", username)}, 56 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 57 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 58 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 59 | ) 60 | 61 | // Make the request 62 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 63 | if err != nil { 64 | t.Logger.Error("%s | Failed to follow %s: %v", t.Account.Username, username, err) 65 | return &models.ActionResponse{ 66 | Success: false, 67 | Error: err, 68 | Status: models.StatusUnknown, 69 | } 70 | } 71 | 72 | // Update cookies 73 | t.Cookies.SetCookieFromResponse(resp) 74 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 75 | t.Account.Ct0 = newCt0 76 | } 77 | 78 | bodyString := string(bodyBytes) 79 | 80 | // Handle successful responses 81 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 82 | var response models.UserResponse 83 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 84 | t.Logger.Error("%s | Failed to parse follow response: %v", t.Account.Username, err) 85 | return &models.ActionResponse{ 86 | Success: false, 87 | Error: err, 88 | Status: models.StatusUnknown, 89 | } 90 | } 91 | 92 | // Check if we got a valid user response (contains screen_name) 93 | if response.ScreenName != "" { 94 | t.Logger.Success("%s | Successfully followed %s", t.Account.Username, username) 95 | return &models.ActionResponse{ 96 | Success: true, 97 | Status: models.StatusSuccess, 98 | } 99 | } 100 | } 101 | 102 | // Handle error responses 103 | switch { 104 | case strings.Contains(bodyString, "this account is temporarily locked"): 105 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 106 | return &models.ActionResponse{ 107 | Success: false, 108 | Error: models.ErrAccountLocked, 109 | Status: models.StatusLocked, 110 | } 111 | case strings.Contains(bodyString, "Could not authenticate you"): 112 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 113 | return &models.ActionResponse{ 114 | Success: false, 115 | Error: models.ErrAuthFailed, 116 | Status: models.StatusAuthError, 117 | } 118 | default: 119 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 120 | return &models.ActionResponse{ 121 | Success: false, 122 | Error: fmt.Errorf("unknown response: %s", bodyString), 123 | Status: models.StatusUnknown, 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /client/get_user_info_by_username.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/0xStarLabs/TwitterAPI/models" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | ) 12 | 13 | // UserInfoResponse represents the GraphQL response for user info 14 | type UserInfoResponse struct { 15 | Data struct { 16 | User struct { 17 | Result struct { 18 | RestID string `json:"rest_id"` 19 | Legacy struct { 20 | Following bool `json:"following"` 21 | CreatedAt string `json:"created_at"` 22 | Description string `json:"description"` 23 | FavouritesCount int `json:"favourites_count"` 24 | FollowersCount int `json:"followers_count"` 25 | FriendsCount int `json:"friends_count"` 26 | ListedCount int `json:"listed_count"` 27 | Location string `json:"location"` 28 | MediaCount int `json:"media_count"` 29 | Name string `json:"name"` 30 | NormalFollowersCount int `json:"normal_followers_count"` 31 | ScreenName string `json:"screen_name"` 32 | StatusesCount int `json:"statuses_count"` 33 | Verified bool `json:"verified"` 34 | } `json:"legacy"` 35 | IsBlueVerified bool `json:"is_blue_verified"` 36 | } `json:"result"` 37 | } `json:"user"` 38 | } `json:"data"` 39 | } 40 | 41 | // GetUserInfoByUsername retrieves detailed information about any Twitter user by their username. 42 | // 43 | // Parameters: 44 | // - username: the Twitter username to look up 45 | // 46 | // Returns: 47 | // - UserInfoResponse: containing detailed user information like: 48 | // - User ID and screen name 49 | // - Profile information 50 | // - Account statistics 51 | // - ActionResponse: containing: 52 | // - Success: true if lookup was successful 53 | // - Error: any error that occurred 54 | // - Status: the status of the action 55 | // 56 | // Example: 57 | // 58 | // info, resp := twitter.GetUserInfoByUsername("username") 59 | // if resp.Success { 60 | // fmt.Printf("User ID: %s\n", info.Data.User.Result.RestID) 61 | // fmt.Printf("Followers: %d\n", info.Data.User.Result.Legacy.FollowersCount) 62 | // } 63 | func (t *Twitter) GetUserInfoByUsername(username string) (*UserInfoResponse, *models.ActionResponse) { 64 | // Build URL with query parameters 65 | baseURL := "https://x.com/i/api/graphql/32pL5BWe9WKeSK1MoPvFQQ/UserByScreenName" 66 | variables := fmt.Sprintf(`{"screen_name":"%s"}`, username) 67 | features := `{"hidden_profile_subscriptions_enabled":true, "subscriptions_feature_can_gift_premium": true, "profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"subscriptions_verification_info_is_identity_verified_enabled":true,"subscriptions_verification_info_verified_since_enabled":true,"highlights_tweets_tab_ui_enabled":true,"responsive_web_twitter_article_notes_tab_enabled":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true}` 68 | fieldToggles := `{"withAuxiliaryUserLabels":false}` 69 | 70 | params := url.Values{} 71 | params.Add("variables", variables) 72 | params.Add("features", features) 73 | params.Add("fieldToggles", fieldToggles) 74 | fullURL := baseURL + "?" + params.Encode() 75 | 76 | // Create request config 77 | reqConfig := utils.DefaultConfig() 78 | reqConfig.Method = "GET" 79 | reqConfig.URL = fullURL 80 | reqConfig.Headers = append(reqConfig.Headers, 81 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 82 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 83 | utils.HeaderPair{Key: "content-type", Value: "application/json"}, 84 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 85 | utils.HeaderPair{Key: "referer", Value: fmt.Sprintf("https://x.com/%s", username)}, 86 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 87 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "no"}, 88 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 89 | utils.HeaderPair{Key: "x-twitter-client-language", Value: "en"}, 90 | ) 91 | 92 | // Make the request 93 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 94 | if err != nil { 95 | t.Logger.Error("%s | Failed to get user info for %s: %v", t.Account.Username, username, err) 96 | return nil, &models.ActionResponse{ 97 | Success: false, 98 | Error: err, 99 | Status: models.StatusUnknown, 100 | } 101 | } 102 | 103 | // Update cookies 104 | t.Cookies.SetCookieFromResponse(resp) 105 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 106 | t.Account.Ct0 = newCt0 107 | } 108 | 109 | bodyString := string(bodyBytes) 110 | 111 | // Handle successful responses 112 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 && strings.Contains(bodyString, "screen_name") { 113 | var response UserInfoResponse 114 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 115 | t.Logger.Error("%s | Failed to parse user info response: %v", t.Account.Username, err) 116 | return nil, &models.ActionResponse{ 117 | Success: false, 118 | Error: err, 119 | Status: models.StatusUnknown, 120 | } 121 | } 122 | 123 | if response.Data.User.Result.Legacy.ScreenName != "" { 124 | t.Logger.Success("%s | Successfully got user info for %s", t.Account.Username, username) 125 | return &response, &models.ActionResponse{ 126 | Success: true, 127 | Status: models.StatusSuccess, 128 | } 129 | } 130 | } 131 | 132 | // Handle error responses 133 | switch { 134 | case strings.Contains(bodyString, "this account is temporarily locked"): 135 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 136 | return nil, &models.ActionResponse{ 137 | Success: false, 138 | Error: models.ErrAccountLocked, 139 | Status: models.StatusLocked, 140 | } 141 | case strings.Contains(bodyString, "Could not authenticate you"): 142 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 143 | return nil, &models.ActionResponse{ 144 | Success: false, 145 | Error: models.ErrAuthFailed, 146 | Status: models.StatusAuthError, 147 | } 148 | default: 149 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 150 | return nil, &models.ActionResponse{ 151 | Success: false, 152 | Error: fmt.Errorf("unknown response: %s", bodyString), 153 | Status: models.StatusUnknown, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /client/like.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/client/addons" 9 | "github.com/0xStarLabs/TwitterAPI/models" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | ) 12 | 13 | // Like adds a like to a tweet 14 | // tweetID can be either a tweet URL or tweet ID 15 | func (t *Twitter) Like(tweetID string) *models.ActionResponse { 16 | // Extract tweet ID if URL was provided 17 | if strings.Contains(tweetID, "twitter.com") || strings.Contains(tweetID, "x.com") { 18 | var err error 19 | tweetID, err = addons.ExtractTweetID(tweetID, t.Account.Username, t.Logger) 20 | if err != nil { 21 | return &models.ActionResponse{ 22 | Success: false, 23 | Error: fmt.Errorf("invalid tweet URL: %w", err), 24 | Status: models.StatusUnknown, 25 | } 26 | } 27 | } 28 | 29 | // Build URL and request body 30 | baseURL := "https://twitter.com/i/api/graphql/" + t.Config.Constants.QueryID.Like + "/FavoriteTweet" 31 | requestBody := fmt.Sprintf(`{"variables":{"tweet_id":"%s"},"queryId":"%s"}`, 32 | tweetID, t.Config.Constants.QueryID.Like) 33 | 34 | // Create request config 35 | reqConfig := utils.DefaultConfig() 36 | reqConfig.Method = "POST" 37 | reqConfig.URL = baseURL 38 | reqConfig.Body = strings.NewReader(requestBody) 39 | reqConfig.Headers = append(reqConfig.Headers, 40 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 41 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 42 | utils.HeaderPair{Key: "content-type", Value: "application/json"}, 43 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 44 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 45 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 46 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 47 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 48 | ) 49 | 50 | // Make the request 51 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 52 | if err != nil { 53 | t.Logger.Error("%s | Failed to like tweet %s: %v", t.Account.Username, tweetID, err) 54 | return &models.ActionResponse{ 55 | Success: false, 56 | Error: err, 57 | Status: models.StatusUnknown, 58 | } 59 | } 60 | 61 | // Update cookies 62 | t.Cookies.SetCookieFromResponse(resp) 63 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 64 | t.Account.Ct0 = newCt0 65 | } 66 | 67 | bodyString := string(bodyBytes) 68 | 69 | // Handle successful responses 70 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 71 | if strings.Contains(bodyString, "already") { 72 | t.Logger.Success("%s | Tweet %s was already liked", t.Account.Username, tweetID) 73 | return &models.ActionResponse{ 74 | Success: true, 75 | Status: models.StatusAlreadyDone, 76 | } 77 | } 78 | 79 | var response models.LikeGraphQLResponse 80 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 81 | t.Logger.Error("%s | Failed to parse like response for tweet %s: %v", t.Account.Username, tweetID, err) 82 | return &models.ActionResponse{ 83 | Success: false, 84 | Error: err, 85 | Status: models.StatusUnknown, 86 | } 87 | } 88 | 89 | if response.Data.FavoriteTweet == "Done" { 90 | t.Logger.Success("%s | Successfully liked tweet %s", t.Account.Username, tweetID) 91 | return &models.ActionResponse{ 92 | Success: true, 93 | Status: models.StatusSuccess, 94 | } 95 | } else { 96 | t.Logger.Error("%s | Failed to like tweet %s: unexpected response", t.Account.Username, tweetID) 97 | return &models.ActionResponse{ 98 | Success: false, 99 | Error: fmt.Errorf("unexpected response: %s", response.Data.FavoriteTweet), 100 | Status: models.StatusUnknown, 101 | } 102 | } 103 | } 104 | 105 | // Handle error responses 106 | switch { 107 | case strings.Contains(bodyString, "this account is temporarily locked"): 108 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 109 | return &models.ActionResponse{ 110 | Success: false, 111 | Error: models.ErrAccountLocked, 112 | Status: models.StatusLocked, 113 | } 114 | case strings.Contains(bodyString, "Could not authenticate you"): 115 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 116 | return &models.ActionResponse{ 117 | Success: false, 118 | Error: models.ErrAuthFailed, 119 | Status: models.StatusAuthError, 120 | } 121 | default: 122 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 123 | return &models.ActionResponse{ 124 | Success: false, 125 | Error: fmt.Errorf("unknown response: %s", bodyString), 126 | Status: models.StatusUnknown, 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /client/poll.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/0xStarLabs/TwitterAPI/client/addons" 8 | "github.com/0xStarLabs/TwitterAPI/models" 9 | "github.com/0xStarLabs/TwitterAPI/utils" 10 | ) 11 | 12 | // VotePoll votes in a Twitter poll 13 | // tweetID can be either a tweet URL or tweet ID 14 | // answer is the poll option to vote for 15 | func (t *Twitter) VotePoll(tweetID string, answer string) *models.ActionResponse { 16 | // Extract tweet ID if URL was provided 17 | if strings.Contains(tweetID, "twitter.com") || strings.Contains(tweetID, "x.com") { 18 | var err error 19 | tweetID, err = addons.ExtractTweetID(tweetID, t.Account.Username, t.Logger) 20 | if err != nil { 21 | return &models.ActionResponse{ 22 | Success: false, 23 | Error: fmt.Errorf("invalid tweet URL: %w", err), 24 | Status: models.StatusUnknown, 25 | } 26 | } 27 | } 28 | 29 | // Get tweet details to extract poll info 30 | tweetDetails, err := t.getTweetDetails(tweetID) 31 | if err != nil { 32 | return &models.ActionResponse{ 33 | Success: false, 34 | Error: fmt.Errorf("failed to get tweet details: %w", err), 35 | Status: models.StatusUnknown, 36 | } 37 | } 38 | 39 | // Extract poll info from tweet details 40 | pollName := "poll" + strings.Split(strings.Split(tweetDetails, `"name":"poll`)[1], `"`)[0] 41 | cardID := strings.Split(strings.Split(tweetDetails, `card://`)[1], `"`)[0] 42 | 43 | // Build URL and request body 44 | baseURL := "https://caps.twitter.com/v2/capi/passthrough/1" 45 | data := fmt.Sprintf( 46 | "twitter%%3Astring%%3Acard_uri=card%%3A%%2F%%2F%s&"+ 47 | "twitter%%3Along%%3Aoriginal_tweet_id=%s&"+ 48 | "twitter%%3Astring%%3Aresponse_card_name=%s&"+ 49 | "twitter%%3Astring%%3Acards_platform=Web-12&"+ 50 | "twitter%%3Astring%%3Aselected_choice=%s", 51 | cardID, tweetID, pollName, answer, 52 | ) 53 | 54 | // Create request config 55 | reqConfig := utils.DefaultConfig() 56 | reqConfig.Method = "POST" 57 | reqConfig.URL = baseURL 58 | reqConfig.Body = strings.NewReader(data) 59 | reqConfig.Headers = append(reqConfig.Headers, 60 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 61 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 62 | utils.HeaderPair{Key: "content-type", Value: "application/x-www-form-urlencoded"}, 63 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 64 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 65 | utils.HeaderPair{Key: "referer", Value: "https://twitter.com/"}, 66 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 67 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 68 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 69 | ) 70 | 71 | // Make the request 72 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 73 | if err != nil { 74 | t.Logger.Error("%s | Failed to vote in poll: %v", t.Account.Username, err) 75 | return &models.ActionResponse{ 76 | Success: false, 77 | Error: err, 78 | Status: models.StatusUnknown, 79 | } 80 | } 81 | 82 | // Update cookies 83 | t.Cookies.SetCookieFromResponse(resp) 84 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 85 | t.Account.Ct0 = newCt0 86 | } 87 | 88 | bodyString := string(bodyBytes) 89 | 90 | // Handle successful responses 91 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 92 | t.Logger.Success("%s | Successfully voted in poll %s", t.Account.Username, tweetID) 93 | return &models.ActionResponse{ 94 | Success: true, 95 | Status: models.StatusSuccess, 96 | } 97 | } 98 | 99 | // Handle error responses 100 | switch { 101 | case strings.Contains(bodyString, "this account is temporarily locked"): 102 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 103 | return &models.ActionResponse{ 104 | Success: false, 105 | Error: models.ErrAccountLocked, 106 | Status: models.StatusLocked, 107 | } 108 | case strings.Contains(bodyString, "Could not authenticate you"): 109 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 110 | return &models.ActionResponse{ 111 | Success: false, 112 | Error: models.ErrAuthFailed, 113 | Status: models.StatusAuthError, 114 | } 115 | default: 116 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 117 | return &models.ActionResponse{ 118 | Success: false, 119 | Error: fmt.Errorf("unknown response: %s", bodyString), 120 | Status: models.StatusUnknown, 121 | } 122 | } 123 | } 124 | 125 | // getTweetDetails gets the details of a tweet, including poll information 126 | func (t *Twitter) getTweetDetails(tweetID string) (string, error) { 127 | baseURL := fmt.Sprintf( 128 | "https://twitter.com/i/api/graphql/B9_KmbkLhXt6jRwGjJrweg/TweetDetail?variables="+ 129 | "%%7B%%22focalTweetId%%22%%3A%%22%s%%22%%2C%%22with_rux_injections%%22%%3Afalse%%2C"+ 130 | "%%22includePromotedContent%%22%%3Atrue%%2C%%22withCommunity%%22%%3Atrue%%2C"+ 131 | "%%22withQuickPromoteEligibilityTweetFields%%22%%3Atrue%%2C%%22withBirdwatchNotes%%22%%3Atrue%%2C"+ 132 | "%%22withVoice%%22%%3Atrue%%2C%%22withV2Timeline%%22%%3Atrue%%7D&"+ 133 | "features=%%7B%%22responsive_web_graphql_exclude_directive_enabled%%22%%3Atrue%%2C"+ 134 | "%%22verified_phone_label_enabled%%22%%3Afalse%%2C"+ 135 | "%%22creator_subscriptions_tweet_preview_api_enabled%%22%%3Atrue%%2C"+ 136 | "%%22responsive_web_graphql_timeline_navigation_enabled%%22%%3Atrue%%2C"+ 137 | "%%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%%22%%3Afalse%%2C"+ 138 | "%%22tweetypie_unmention_optimization_enabled%%22%%3Atrue%%2C"+ 139 | "%%22responsive_web_edit_tweet_api_enabled%%22%%3Atrue%%2C"+ 140 | "%%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%%22%%3Atrue%%2C"+ 141 | "%%22view_counts_everywhere_api_enabled%%22%%3Atrue%%2C"+ 142 | "%%22longform_notetweets_consumption_enabled%%22%%3Atrue%%2C"+ 143 | "%%22tweet_awards_web_tipping_enabled%%22%%3Afalse%%2C"+ 144 | "%%22freedom_of_speech_not_reach_fetch_enabled%%22%%3Atrue%%2C"+ 145 | "%%22standardized_nudges_misinfo%%22%%3Atrue%%2C"+ 146 | "%%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%%22%%3Atrue%%2C"+ 147 | "%%22longform_notetweets_rich_text_read_enabled%%22%%3Atrue%%2C"+ 148 | "%%22longform_notetweets_inline_media_enabled%%22%%3Atrue%%2C"+ 149 | "%%22responsive_web_enhance_cards_enabled%%22%%3Afalse%%7D&"+ 150 | "fieldToggles=%%7B%%22withArticleRichContentState%%22%%3Atrue%%7D", 151 | tweetID, 152 | ) 153 | 154 | // Create request config 155 | reqConfig := utils.DefaultConfig() 156 | reqConfig.Method = "GET" 157 | reqConfig.URL = baseURL 158 | reqConfig.Headers = append(reqConfig.Headers, 159 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 160 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 161 | utils.HeaderPair{Key: "content-type", Value: "application/json"}, 162 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 163 | utils.HeaderPair{Key: "referer", Value: fmt.Sprintf("https://twitter.com/i/status/%s", tweetID)}, 164 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 165 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 166 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 167 | utils.HeaderPair{Key: "x-twitter-client-language", Value: "en"}, 168 | ) 169 | 170 | // Make the request 171 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 172 | if err != nil { 173 | return "", fmt.Errorf("failed to get tweet details: %w", err) 174 | } 175 | 176 | // Update cookies 177 | t.Cookies.SetCookieFromResponse(resp) 178 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 179 | t.Account.Ct0 = newCt0 180 | } 181 | 182 | return string(bodyBytes), nil 183 | } 184 | -------------------------------------------------------------------------------- /client/retweet.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/client/addons" 9 | "github.com/0xStarLabs/TwitterAPI/models" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | ) 12 | 13 | // Retweet retweets a tweet 14 | // tweetID can be either a tweet URL or tweet ID 15 | func (t *Twitter) Retweet(tweetID string) *models.ActionResponse { 16 | // Extract tweet ID if URL was provided 17 | if strings.Contains(tweetID, "twitter.com") || strings.Contains(tweetID, "x.com") { 18 | var err error 19 | tweetID, err = addons.ExtractTweetID(tweetID, t.Account.Username, t.Logger) 20 | if err != nil { 21 | return &models.ActionResponse{ 22 | Success: false, 23 | Error: fmt.Errorf("invalid tweet URL: %w", err), 24 | Status: models.StatusUnknown, 25 | } 26 | } 27 | } 28 | 29 | // Build URL and request body 30 | baseURL := "https://twitter.com/i/api/graphql/" + t.Config.Constants.QueryID.Retweet + "/CreateRetweet" 31 | requestBody := fmt.Sprintf(`{"variables":{"tweet_id":"%s","dark_request":false},"queryId":"%s"}`, 32 | tweetID, t.Config.Constants.QueryID.Retweet) 33 | 34 | // Create request config 35 | reqConfig := utils.DefaultConfig() 36 | reqConfig.Method = "POST" 37 | reqConfig.URL = baseURL 38 | reqConfig.Body = strings.NewReader(requestBody) 39 | reqConfig.Headers = append(reqConfig.Headers, 40 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 41 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 42 | utils.HeaderPair{Key: "content-type", Value: "application/json"}, 43 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 44 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 45 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 46 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 47 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 48 | ) 49 | 50 | // Make the request 51 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 52 | if err != nil { 53 | t.Logger.Error("%s | Failed to retweet: %v", t.Account.Username, err) 54 | return &models.ActionResponse{ 55 | Success: false, 56 | Error: err, 57 | Status: models.StatusUnknown, 58 | } 59 | } 60 | 61 | // Update cookies 62 | t.Cookies.SetCookieFromResponse(resp) 63 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 64 | t.Account.Ct0 = newCt0 65 | } 66 | 67 | bodyString := string(bodyBytes) 68 | 69 | // Handle successful responses 70 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 71 | if strings.Contains(bodyString, "already") { 72 | t.Logger.Success("%s | Tweet %s was already retweeted", t.Account.Username, tweetID) 73 | return &models.ActionResponse{ 74 | Success: true, 75 | Status: models.StatusAlreadyDone, 76 | } 77 | } 78 | 79 | var response models.RetweetGraphQLResponse 80 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 81 | t.Logger.Error("%s | Failed to parse retweet response: %v", t.Account.Username, err) 82 | return &models.ActionResponse{ 83 | Success: false, 84 | Error: err, 85 | Status: models.StatusUnknown, 86 | } 87 | } 88 | 89 | if response.Data.CreateRetweet.RetweetResults.Result.RestID != "" { 90 | t.Logger.Success("%s | Successfully retweeted %s", t.Account.Username, tweetID) 91 | return &models.ActionResponse{ 92 | Success: true, 93 | Status: models.StatusSuccess, 94 | } 95 | } 96 | } 97 | 98 | // Handle error responses 99 | switch { 100 | case strings.Contains(bodyString, "this account is temporarily locked"): 101 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 102 | return &models.ActionResponse{ 103 | Success: false, 104 | Error: models.ErrAccountLocked, 105 | Status: models.StatusLocked, 106 | } 107 | case strings.Contains(bodyString, "Could not authenticate you"): 108 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 109 | return &models.ActionResponse{ 110 | Success: false, 111 | Error: models.ErrAuthFailed, 112 | Status: models.StatusAuthError, 113 | } 114 | default: 115 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 116 | return &models.ActionResponse{ 117 | Success: false, 118 | Error: fmt.Errorf("unknown response: %s", bodyString), 119 | Status: models.StatusUnknown, 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /client/tweet.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/models" 9 | "github.com/0xStarLabs/TwitterAPI/utils" 10 | ) 11 | 12 | // TweetOptions contains optional parameters for creating a tweet 13 | type TweetOptions struct { 14 | QuoteTweetURL string // URL of tweet to quote (optional) 15 | MediaBase64 string // Base64 encoded media (optional) 16 | } 17 | 18 | // Tweet posts a new tweet with optional media or quote functionality. 19 | // 20 | // Examples: 21 | // 22 | // Regular tweet: 23 | // 24 | // twitter.Tweet("Hello, world!", nil) 25 | // 26 | // Tweet with media: 27 | // 28 | // twitter.Tweet("Check out this image!", &TweetOptions{ 29 | // MediaBase64: imageBase64String, 30 | // }) 31 | // 32 | // Quote tweet: 33 | // 34 | // twitter.Tweet("Look at this!", &TweetOptions{ 35 | // QuoteTweetURL: "https://twitter.com/user/status/123456789", 36 | // }) 37 | // 38 | // Tweet with both media and quote: 39 | // 40 | // twitter.Tweet("Amazing!", &TweetOptions{ 41 | // MediaBase64: imageBase64String, 42 | // QuoteTweetURL: "https://twitter.com/user/status/123456789", 43 | // }) 44 | // 45 | // Parameters: 46 | // - content: The text content of the tweet 47 | // - opts: Optional parameters for media and quote tweets (can be nil) 48 | // 49 | // Returns: 50 | // - *models.ActionResponse containing the success status and any errors 51 | func (t *Twitter) Tweet(content string, opts *TweetOptions) *models.ActionResponse { 52 | // If media is provided, upload it first 53 | var mediaID string 54 | if opts != nil && opts.MediaBase64 != "" { 55 | var err error 56 | mediaID, err = t.UploadMedia(opts.MediaBase64) 57 | if err != nil { 58 | return &models.ActionResponse{ 59 | Success: false, 60 | Error: fmt.Errorf("failed to upload media: %w", err), 61 | Status: models.StatusUnknown, 62 | } 63 | } 64 | } 65 | // Build URL and request body 66 | baseURL := "https://twitter.com/i/api/graphql/" + t.Config.Constants.QueryID.Tweet + "/CreateTweet" 67 | 68 | // Build variables based on options 69 | variables := map[string]interface{}{ 70 | "tweet_text": content, 71 | "dark_request": false, 72 | "semantic_annotation_ids": []string{}, 73 | } 74 | 75 | // Add media if provided 76 | if mediaID != "" { 77 | variables["media"] = map[string]interface{}{ 78 | "media_entities": []map[string]interface{}{ 79 | { 80 | "media_id": mediaID, 81 | "tagged_users": []string{}, 82 | }, 83 | }, 84 | "possibly_sensitive": false, 85 | } 86 | } else { 87 | variables["media"] = map[string]interface{}{ 88 | "media_entities": []string{}, 89 | "possibly_sensitive": false, 90 | } 91 | } 92 | 93 | // Add quote tweet URL if provided 94 | if opts != nil && opts.QuoteTweetURL != "" { 95 | variables["attachment_url"] = opts.QuoteTweetURL 96 | } 97 | 98 | // Build the full request body 99 | requestBody := map[string]interface{}{ 100 | "variables": variables, 101 | "features": map[string]interface{}{ 102 | "tweetypie_unmention_optimization_enabled": true, 103 | "responsive_web_edit_tweet_api_enabled": true, 104 | "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, 105 | "view_counts_everywhere_api_enabled": true, 106 | "longform_notetweets_consumption_enabled": true, 107 | "responsive_web_twitter_article_tweet_consumption_enabled": false, 108 | "tweet_awards_web_tipping_enabled": false, 109 | "freedom_of_speech_not_reach_fetch_enabled": true, 110 | "standardized_nudges_misinfo": true, 111 | "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, 112 | "longform_notetweets_rich_text_read_enabled": true, 113 | "longform_notetweets_inline_media_enabled": true, 114 | "responsive_web_graphql_exclude_directive_enabled": true, 115 | "verified_phone_label_enabled": false, 116 | "responsive_web_media_download_video_enabled": false, 117 | "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, 118 | "responsive_web_graphql_timeline_navigation_enabled": true, 119 | "rweb_video_timestamps_enabled": true, 120 | "c9s_tweet_anatomy_moderator_badge_enabled": true, 121 | "responsive_web_enhance_cards_enabled": true, 122 | }, 123 | "queryId": t.Config.Constants.QueryID.Tweet, 124 | } 125 | 126 | jsonBody, err := json.Marshal(requestBody) 127 | if err != nil { 128 | return &models.ActionResponse{ 129 | Success: false, 130 | Error: fmt.Errorf("failed to marshal request body: %w", err), 131 | Status: models.StatusUnknown, 132 | } 133 | } 134 | 135 | // Create request config 136 | reqConfig := utils.DefaultConfig() 137 | reqConfig.Method = "POST" 138 | reqConfig.URL = baseURL 139 | reqConfig.Body = strings.NewReader(string(jsonBody)) 140 | reqConfig.Headers = append(reqConfig.Headers, 141 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 142 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 143 | utils.HeaderPair{Key: "content-type", Value: "application/json"}, 144 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 145 | utils.HeaderPair{Key: "origin", Value: "https://twitter.com"}, 146 | utils.HeaderPair{Key: "referer", Value: "https://twitter.com/compose/tweet"}, 147 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 148 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 149 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 150 | ) 151 | 152 | // Make the request 153 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 154 | if err != nil { 155 | t.Logger.Error("%s | Failed to send tweet: %v", t.Account.Username, err) 156 | return &models.ActionResponse{ 157 | Success: false, 158 | Error: err, 159 | Status: models.StatusUnknown, 160 | } 161 | } 162 | 163 | // Update cookies 164 | t.Cookies.SetCookieFromResponse(resp) 165 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 166 | t.Account.Ct0 = newCt0 167 | } 168 | 169 | bodyString := string(bodyBytes) 170 | 171 | // Handle successful responses 172 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 173 | if strings.Contains(bodyString, "duplicate") { 174 | t.Logger.Success("%s | Tweet was already posted", t.Account.Username) 175 | return &models.ActionResponse{ 176 | Success: true, 177 | Status: models.StatusAlreadyDone, 178 | } 179 | } 180 | 181 | var response models.TweetGraphQLResponse 182 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 183 | t.Logger.Error("%s | Failed to parse tweet response: %v", t.Account.Username, err) 184 | return &models.ActionResponse{ 185 | Success: false, 186 | Error: err, 187 | Status: models.StatusUnknown, 188 | } 189 | } 190 | 191 | if response.Data.CreateTweet.TweetResults.Result.RestID != "" { 192 | t.Logger.Success("%s | Successfully posted tweet", t.Account.Username) 193 | return &models.ActionResponse{ 194 | Success: true, 195 | Status: models.StatusSuccess, 196 | } 197 | } 198 | } 199 | 200 | // Handle error responses 201 | switch { 202 | case strings.Contains(bodyString, "this account is temporarily locked"): 203 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 204 | return &models.ActionResponse{ 205 | Success: false, 206 | Error: models.ErrAccountLocked, 207 | Status: models.StatusLocked, 208 | } 209 | case strings.Contains(bodyString, "Could not authenticate you"): 210 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 211 | return &models.ActionResponse{ 212 | Success: false, 213 | Error: models.ErrAuthFailed, 214 | Status: models.StatusAuthError, 215 | } 216 | default: 217 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 218 | return &models.ActionResponse{ 219 | Success: false, 220 | Error: fmt.Errorf("unknown response: %s", bodyString), 221 | Status: models.StatusUnknown, 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /client/unfollow.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/0xStarLabs/TwitterAPI/models" 10 | "github.com/0xStarLabs/TwitterAPI/utils" 11 | ) 12 | 13 | // Unfollow unfollows a user by their user ID or username. 14 | // 15 | // Parameters: 16 | // - userIDOrUsername: can be either a numeric user ID or a Twitter username 17 | // 18 | // Returns an ActionResponse containing: 19 | // - Success: true if unfollow was successful 20 | // - Error: any error that occurred 21 | // - Status: the status of the action (Success, AuthError, etc.) 22 | // 23 | // Example: 24 | // 25 | // // Unfollow by username 26 | // resp := twitter.Unfollow("username") 27 | // 28 | // // Unfollow by ID 29 | // resp := twitter.Unfollow("1234567890") 30 | // 31 | // if resp.Success { 32 | // fmt.Println("Successfully unfollowed user") 33 | // } 34 | func (t *Twitter) Unfollow(userIDOrUsername string) *models.ActionResponse { 35 | // Check if the input is not a numeric ID 36 | if !utils.IsNumeric(userIDOrUsername) { 37 | // Get user info to get the numeric ID 38 | info, resp := t.GetUserInfoByUsername(userIDOrUsername) 39 | if !resp.Success { 40 | return resp 41 | } 42 | userIDOrUsername = info.Data.User.Result.RestID 43 | } 44 | 45 | // Build URL and request body 46 | baseURL := "https://x.com/i/api/1.1/friendships/destroy.json" 47 | data := url.Values{} 48 | data.Set("include_profile_interstitial_type", "1") 49 | data.Set("include_blocking", "1") 50 | data.Set("include_blocked_by", "1") 51 | data.Set("include_followed_by", "1") 52 | data.Set("include_want_retweets", "1") 53 | data.Set("include_mute_edge", "1") 54 | data.Set("include_can_dm", "1") 55 | data.Set("include_can_media_tag", "1") 56 | data.Set("include_ext_is_blue_verified", "1") 57 | data.Set("include_ext_verified_type", "1") 58 | data.Set("include_ext_profile_image_shape", "1") 59 | data.Set("skip_status", "1") 60 | data.Set("user_id", userIDOrUsername) 61 | 62 | // Create request config 63 | reqConfig := utils.DefaultConfig() 64 | reqConfig.Method = "POST" 65 | reqConfig.URL = baseURL 66 | reqConfig.Body = strings.NewReader(data.Encode()) 67 | reqConfig.Headers = append(reqConfig.Headers, 68 | utils.HeaderPair{Key: "accept", Value: "*/*"}, 69 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 70 | utils.HeaderPair{Key: "content-type", Value: "application/x-www-form-urlencoded"}, 71 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 72 | utils.HeaderPair{Key: "origin", Value: "https://x.com"}, 73 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 74 | utils.HeaderPair{Key: "x-twitter-active-user", Value: "yes"}, 75 | utils.HeaderPair{Key: "x-twitter-auth-type", Value: "OAuth2Session"}, 76 | ) 77 | 78 | // Make the request 79 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 80 | if err != nil { 81 | t.Logger.Error("%s | Failed to unfollow user %s: %v", t.Account.Username, userIDOrUsername, err) 82 | return &models.ActionResponse{ 83 | Success: false, 84 | Error: err, 85 | Status: models.StatusUnknown, 86 | } 87 | } 88 | 89 | // Update cookies 90 | t.Cookies.SetCookieFromResponse(resp) 91 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 92 | t.Account.Ct0 = newCt0 93 | } 94 | 95 | bodyString := string(bodyBytes) 96 | 97 | // Handle successful responses 98 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { 99 | var response models.UserResponse 100 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 101 | t.Logger.Error("%s | Failed to parse unfollow response: %v", t.Account.Username, err) 102 | return &models.ActionResponse{ 103 | Success: false, 104 | Error: err, 105 | Status: models.StatusUnknown, 106 | } 107 | } 108 | 109 | // Check if we got a valid user response (contains screen_name) 110 | if response.ScreenName != "" { 111 | t.Logger.Success("%s | Successfully unfollowed user %s", t.Account.Username, userIDOrUsername) 112 | return &models.ActionResponse{ 113 | Success: true, 114 | Status: models.StatusSuccess, 115 | } 116 | } 117 | } 118 | 119 | // Handle error responses 120 | switch { 121 | case strings.Contains(bodyString, "this account is temporarily locked"): 122 | t.Logger.Error("%s | Account is temporarily locked", t.Account.Username) 123 | return &models.ActionResponse{ 124 | Success: false, 125 | Error: models.ErrAccountLocked, 126 | Status: models.StatusLocked, 127 | } 128 | case strings.Contains(bodyString, "Could not authenticate you"): 129 | t.Logger.Error("%s | Could not authenticate you", t.Account.Username) 130 | return &models.ActionResponse{ 131 | Success: false, 132 | Error: models.ErrAuthFailed, 133 | Status: models.StatusAuthError, 134 | } 135 | default: 136 | t.Logger.Error("%s | Unknown response: %s", t.Account.Username, bodyString) 137 | return &models.ActionResponse{ 138 | Success: false, 139 | Error: fmt.Errorf("unknown response: %s", bodyString), 140 | Status: models.StatusUnknown, 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /client/upload_media.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/0xStarLabs/TwitterAPI/models" 9 | "github.com/0xStarLabs/TwitterAPI/utils" 10 | ) 11 | 12 | // UploadMedia uploads media to Twitter and returns the media ID. 13 | // Used internally by Tweet and Comment functions when including media. 14 | // 15 | // Parameters: 16 | // - mediaBase64: the base64-encoded image data 17 | // 18 | // Returns: 19 | // - string: the media ID if successful 20 | // - error: any error that occurred 21 | // 22 | // Example: 23 | // 24 | // mediaID, err := twitter.UploadMedia(imageBase64) 25 | // if err != nil { 26 | // log.Fatal(err) 27 | // } 28 | // // Use mediaID in Tweet or Comment function 29 | func (t *Twitter) UploadMedia(mediaBase64 string) (string, error) { 30 | mediaURL := "https://upload.twitter.com/1.1/media/upload.json" 31 | data := url.Values{} 32 | data.Set("media_data", mediaBase64) 33 | 34 | reqConfig := utils.DefaultConfig() 35 | reqConfig.Method = "POST" 36 | reqConfig.URL = mediaURL 37 | reqConfig.Body = strings.NewReader(data.Encode()) 38 | reqConfig.Headers = append(reqConfig.Headers, 39 | utils.HeaderPair{Key: "authorization", Value: t.Config.Constants.BearerToken}, 40 | utils.HeaderPair{Key: "content-type", Value: "application/x-www-form-urlencoded"}, 41 | utils.HeaderPair{Key: "cookie", Value: t.Cookies.CookiesToHeader()}, 42 | utils.HeaderPair{Key: "x-csrf-token", Value: t.Account.Ct0}, 43 | ) 44 | 45 | bodyBytes, resp, err := utils.MakeRequest(t.Client, reqConfig) 46 | if err != nil { 47 | t.Logger.Error("%s | Failed to upload media: %v", t.Account.Username, err) 48 | return "", err 49 | } 50 | 51 | // Update cookies 52 | t.Cookies.SetCookieFromResponse(resp) 53 | if newCt0, ok := t.Cookies.GetCookieValue("ct0"); ok { 54 | t.Account.Ct0 = newCt0 55 | } 56 | 57 | var response models.MediaUploadResponse 58 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 59 | t.Logger.Error("%s | Failed to parse media upload response: %v", t.Account.Username, err) 60 | return "", err 61 | } 62 | 63 | if response.MediaIDString == "" { 64 | t.Logger.Error("%s | No media ID in response", t.Account.Username) 65 | return "", err 66 | } 67 | 68 | t.Logger.Success("%s | Successfully uploaded media", t.Account.Username) 69 | return response.MediaIDString, nil 70 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/0xStarLabs/TwitterAPI 2 | 3 | go 1.23.4 4 | 5 | require github.com/bogdanfinn/fhttp v0.5.34 6 | 7 | require ( 8 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect 9 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 10 | ) 11 | 12 | require ( 13 | github.com/andybalholm/brotli v1.1.1 // indirect 14 | github.com/bogdanfinn/tls-client v1.8.0 15 | github.com/bogdanfinn/utls v1.6.5 // indirect 16 | github.com/cloudflare/circl v1.5.0 // indirect 17 | github.com/gookit/color v1.5.4 18 | github.com/klauspost/compress v1.17.11 // indirect 19 | github.com/quic-go/quic-go v0.48.1 // indirect 20 | golang.org/x/crypto v0.29.0 // indirect 21 | golang.org/x/net v0.31.0 // indirect 22 | golang.org/x/sys v0.27.0 // indirect 23 | golang.org/x/text v0.20.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/bogdanfinn/fhttp v0.5.34 h1:avRD2JNYqj6I6DqjSrI9tl8mP8Nk7T4CCmUsPz7afhg= 4 | github.com/bogdanfinn/fhttp v0.5.34/go.mod h1:BlcawVfXJ4uhk5yyNGOOY2bwo8UmMi6ccMszP1KGLkU= 5 | github.com/bogdanfinn/tls-client v1.8.0 h1:IB44SqKa0XKdx3GYXpRbkqN3+tsBtg9RJYRsl36boOA= 6 | github.com/bogdanfinn/tls-client v1.8.0/go.mod h1:ehNITC7JBFeh6S7QNWtfD+PBKm0RsqvizAyyij2d/6g= 7 | github.com/bogdanfinn/utls v1.6.5 h1:rVMQvhyN3zodLxKFWMRLt19INGBCZ/OM2/vBWPNIt1w= 8 | github.com/bogdanfinn/utls v1.6.5/go.mod h1:czcHxHGsc1q9NjgWSeSinQZzn6MR76zUmGVIGanSXO0= 9 | github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= 10 | github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 11 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 12 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 13 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 14 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 15 | github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA= 16 | github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 17 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= 18 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= 19 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= 20 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 21 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 22 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= 23 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 24 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 25 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 26 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 27 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 28 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 29 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | 7 | "github.com/0xStarLabs/TwitterAPI/client" 8 | "github.com/0xStarLabs/TwitterAPI/models" 9 | "github.com/0xStarLabs/TwitterAPI/utils" 10 | ) 11 | 12 | func main() { 13 | proxy := "user:pass@host:port" 14 | authToken := "auth_token_here" 15 | // Create account 16 | account := client.NewAccount(authToken, "", proxy) 17 | 18 | // Or with detailed logging 19 | verboseConfig := models.NewConfig() 20 | verboseConfig.LogLevel = utils.LogLevelDebug 21 | 22 | twitter, err := client.NewTwitter(account, verboseConfig) 23 | if err != nil { 24 | // Handle error your way 25 | log.Fatal(err) 26 | } 27 | 28 | info, status := twitter.IsValid() 29 | if status.Error != nil { 30 | log.Fatal(status.Error) 31 | } 32 | fmt.Println(info) 33 | } 34 | -------------------------------------------------------------------------------- /models/account.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | http "github.com/bogdanfinn/fhttp" 5 | ) 6 | 7 | // Account represents a Twitter account with all necessary credentials and information 8 | type Account struct { 9 | Ct0 string // CSRF token 10 | AuthToken string // auth_token cookie 11 | Proxy string // Format: "ip:port" or "user:pass@ip:port" 12 | 13 | // Account Info 14 | Username string 15 | DisplayName string 16 | UserID string 17 | Email string 18 | PhoneNumber string 19 | IsVerified bool 20 | CreatedAt string 21 | FollowCount int 22 | FollowerCount int 23 | 24 | // Session Data 25 | Cookies []*http.Cookie 26 | 27 | // Optional fields 28 | ProfileImageURL string 29 | Bio string 30 | Location string 31 | Website string 32 | 33 | Suspended bool 34 | Locked bool 35 | } 36 | -------------------------------------------------------------------------------- /models/config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/0xStarLabs/TwitterAPI/utils" 7 | ) 8 | 9 | // TwitterConstants holds all Twitter-specific constants 10 | type TwitterConstants struct { 11 | // API Constants 12 | BearerToken string 13 | UserAgent string 14 | 15 | // Query IDs 16 | QueryID struct { 17 | Like string 18 | Unlike string 19 | Retweet string 20 | Unretweet string 21 | Tweet string 22 | } 23 | } 24 | 25 | // Config holds Twitter client configuration 26 | type Config struct { 27 | // HTTP Client settings 28 | MaxRetries int 29 | Timeout time.Duration 30 | FollowRedirects bool 31 | 32 | // Logging options 33 | LogLevel utils.LogLevel // Level of logging detail 34 | 35 | // Twitter Constants 36 | Constants TwitterConstants 37 | } 38 | 39 | // NewConfig returns a Config with default settings 40 | func NewConfig() *Config { 41 | return &Config{ 42 | MaxRetries: 3, 43 | Timeout: 30 * time.Second, 44 | FollowRedirects: true, 45 | LogLevel: utils.LogLevelError, // By default, only log errors 46 | Constants: TwitterConstants{ 47 | UserAgent: UserAgent, 48 | BearerToken: BearerToken, 49 | QueryID: struct { 50 | Like string 51 | Unlike string 52 | Retweet string 53 | Unretweet string 54 | Tweet string 55 | }{ 56 | Like: QueryIDLike, 57 | Unlike: QueryIDUnlike, 58 | Retweet: QueryIDRetweet, 59 | Unretweet: QueryIDUnretweet, 60 | Tweet: QueryIDTweet, 61 | }, 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /models/constants.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "errors" 4 | 5 | // User Agent 6 | const ( 7 | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" 8 | ) 9 | 10 | // API Constants 11 | const ( 12 | BearerToken = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA" 13 | ) 14 | 15 | // Query IDs for different operations 16 | const ( 17 | QueryIDLike = "lI07N6Otwv1PhnEgXILM7A" 18 | QueryIDUnlike = "ZYKSe-w7KEslx3JhSIk5LA" 19 | QueryIDRetweet = "ojPdsZsimiJrUGLR1sjUtA" 20 | QueryIDUnretweet = "iQtK4dl5hBmXewYZLkNG9A" 21 | QueryIDTweet = "bDE2rBtZb3uyrczSZ_pI9g" 22 | ) 23 | 24 | // Common error types for Twitter operations 25 | var ( 26 | ErrAccountLocked = errors.New("account is temporarily locked") 27 | ErrAuthFailed = errors.New("authentication failed") 28 | ErrInvalidToken = errors.New("invalid token") 29 | ErrUnknown = errors.New("unable to complete operation") 30 | ) 31 | 32 | // ActionStatus represents the status of any Twitter action (like, retweet, etc.) 33 | type ActionStatus int 34 | 35 | const ( 36 | StatusSuccess ActionStatus = iota 37 | StatusAlreadyDone // Already liked, already retweeted, etc. 38 | StatusLocked // Account is locked 39 | StatusNotFound // Tweet/User not found 40 | StatusRateLimited // Rate limit exceeded 41 | StatusAuthError // Authentication error 42 | StatusInvalidToken // Invalid token 43 | StatusUnknown // Unknown error 44 | ) 45 | 46 | // ActionResponse represents the response from any Twitter action 47 | type ActionResponse struct { 48 | Success bool 49 | Error error 50 | Status ActionStatus 51 | } 52 | -------------------------------------------------------------------------------- /models/like.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // LikeStatus represents the status of a like action 4 | type LikeStatus int 5 | 6 | const ( 7 | LikeStatusSuccess LikeStatus = iota 8 | LikeStatusAlreadyLiked 9 | LikeStatusTweetNotFound 10 | LikeStatusRateLimited 11 | LikeStatusAuthError 12 | LikeStatusUnknown 13 | ) 14 | 15 | // LikeResponse represents the response from a like action 16 | type LikeResponse struct { 17 | Success bool 18 | Error error 19 | Status LikeStatus 20 | } 21 | 22 | // LikeGraphQLResponse represents the GraphQL response for a like action 23 | type LikeGraphQLResponse struct { 24 | Data struct { 25 | FavoriteTweet string `json:"favorite_tweet"` 26 | } `json:"data"` 27 | Errors []struct { 28 | Message string `json:"message"` 29 | Locations []struct { 30 | Line int `json:"line"` 31 | Column int `json:"column"` 32 | } `json:"locations"` 33 | Path []string `json:"path"` 34 | Extensions struct { 35 | Name string `json:"name"` 36 | Source string `json:"source"` 37 | Code int `json:"code"` 38 | Kind string `json:"kind"` 39 | Tracing struct { 40 | TraceID string `json:"trace_id"` 41 | } `json:"tracing"` 42 | } `json:"extensions"` 43 | } `json:"errors"` 44 | } 45 | 46 | // UnlikeGraphQLResponse represents the GraphQL response for an unlike action 47 | type UnlikeGraphQLResponse struct { 48 | Data struct { 49 | UnfavoriteTweet string `json:"unfavorite_tweet"` 50 | } `json:"data"` 51 | Errors []struct { 52 | Message string `json:"message"` 53 | Locations []struct { 54 | Line int `json:"line"` 55 | Column int `json:"column"` 56 | } `json:"locations"` 57 | Path []string `json:"path"` 58 | Extensions struct { 59 | Name string `json:"name"` 60 | Source string `json:"source"` 61 | Code int `json:"code"` 62 | Kind string `json:"kind"` 63 | Tracing struct { 64 | TraceID string `json:"trace_id"` 65 | } `json:"tracing"` 66 | } `json:"extensions"` 67 | } `json:"errors"` 68 | } 69 | 70 | // AlreadyLikedResponse represents the response when tweet is already liked 71 | type AlreadyLikedResponse struct { 72 | Errors []struct { 73 | Message string `json:"message"` 74 | Locations []struct { 75 | Line int `json:"line"` 76 | Column int `json:"column"` 77 | } `json:"locations"` 78 | Path []string `json:"path"` 79 | Extensions struct { 80 | Name string `json:"name"` 81 | Source string `json:"source"` 82 | Code int `json:"code"` 83 | Kind string `json:"kind"` 84 | Tracing struct { 85 | TraceID string `json:"trace_id"` 86 | } `json:"tracing"` 87 | } `json:"extensions"` 88 | } `json:"errors"` 89 | } 90 | -------------------------------------------------------------------------------- /models/tweet.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Tweet represents a Twitter tweet with its basic information 4 | type Tweet struct { 5 | ID string 6 | AuthorUsername string 7 | AuthorID string 8 | Text string 9 | CreatedAt string 10 | LikeCount int 11 | RetweetCount int 12 | QuoteCount int 13 | ReplyCount int 14 | IsLiked bool 15 | IsRetweeted bool 16 | IsQuoted bool 17 | IsReply bool 18 | ConversationID string 19 | InReplyToUserID string 20 | } 21 | 22 | // TweetGraphQLResponse represents the GraphQL response for a tweet action 23 | type TweetGraphQLResponse struct { 24 | Data struct { 25 | CreateTweet struct { 26 | TweetResults struct { 27 | Result struct { 28 | RestID string `json:"rest_id"` 29 | } `json:"result"` 30 | } `json:"tweet_results"` 31 | } `json:"create_tweet"` 32 | } `json:"data"` 33 | Errors []struct { 34 | Message string `json:"message"` 35 | Locations []struct { 36 | Line int `json:"line"` 37 | Column int `json:"column"` 38 | } `json:"locations"` 39 | Path []string `json:"path"` 40 | Extensions struct { 41 | Name string `json:"name"` 42 | Source string `json:"source"` 43 | Code int `json:"code"` 44 | Kind string `json:"kind"` 45 | Tracing struct { 46 | TraceID string `json:"trace_id"` 47 | } `json:"tracing"` 48 | } `json:"extensions"` 49 | } `json:"errors"` 50 | } 51 | 52 | // MediaUploadResponse represents the response from media upload 53 | type MediaUploadResponse struct { 54 | MediaIDString string `json:"media_id_string"` 55 | Size int `json:"size"` 56 | ExpiresAfter int `json:"expires_after_secs"` 57 | } 58 | 59 | // RetweetGraphQLResponse represents the GraphQL response for a retweet action 60 | type RetweetGraphQLResponse struct { 61 | Data struct { 62 | CreateRetweet struct { 63 | RetweetResults struct { 64 | Result struct { 65 | RestID string `json:"rest_id"` 66 | Legacy struct { 67 | FullText string `json:"full_text"` 68 | } `json:"legacy"` 69 | } `json:"result"` 70 | } `json:"retweet_results"` 71 | } `json:"create_retweet"` 72 | } `json:"data"` 73 | Errors []struct { 74 | Message string `json:"message"` 75 | Locations []struct { 76 | Line int `json:"line"` 77 | Column int `json:"column"` 78 | } `json:"locations"` 79 | Path []string `json:"path"` 80 | Extensions struct { 81 | Name string `json:"name"` 82 | Source string `json:"source"` 83 | Code int `json:"code"` 84 | Kind string `json:"kind"` 85 | Tracing struct { 86 | TraceID string `json:"trace_id"` 87 | } `json:"tracing"` 88 | } `json:"extensions"` 89 | } `json:"errors"` 90 | } 91 | 92 | // UnretweetGraphQLResponse represents the GraphQL response for an unretweet action 93 | type UnretweetGraphQLResponse struct { 94 | Data struct { 95 | Unretweet struct { 96 | SourceTweetResults struct { 97 | Result struct { 98 | RestID string `json:"rest_id"` 99 | Legacy struct { 100 | FullText string `json:"full_text"` 101 | } `json:"legacy"` 102 | } `json:"result"` 103 | } `json:"source_tweet_results"` 104 | } `json:"unretweet"` 105 | } `json:"data"` 106 | Errors []struct { 107 | Message string `json:"message"` 108 | Locations []struct { 109 | Line int `json:"line"` 110 | Column int `json:"column"` 111 | } `json:"locations"` 112 | Path []string `json:"path"` 113 | Extensions struct { 114 | Name string `json:"name"` 115 | Source string `json:"source"` 116 | Code int `json:"code"` 117 | Kind string `json:"kind"` 118 | Tracing struct { 119 | TraceID string `json:"trace_id"` 120 | } `json:"tracing"` 121 | } `json:"extensions"` 122 | } `json:"errors"` 123 | } 124 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // UserResponse represents the response from Twitter's user-related endpoints 4 | type UserResponse struct { 5 | ID int64 `json:"id"` 6 | IDStr string `json:"id_str"` 7 | Name string `json:"name"` 8 | ScreenName string `json:"screen_name"` 9 | Location string `json:"location"` 10 | Description string `json:"description"` 11 | URL any `json:"url"` 12 | Protected bool `json:"protected"` 13 | FollowersCount int `json:"followers_count"` 14 | FriendsCount int `json:"friends_count"` 15 | ListedCount int `json:"listed_count"` 16 | CreatedAt string `json:"created_at"` 17 | FavouritesCount int `json:"favourites_count"` 18 | Verified bool `json:"verified"` 19 | StatusesCount int `json:"statuses_count"` 20 | MediaCount int `json:"media_count"` 21 | Following bool `json:"following"` 22 | FollowRequestSent bool `json:"follow_request_sent"` 23 | Notifications bool `json:"notifications"` 24 | Entities struct { 25 | Description struct { 26 | Urls []any `json:"urls"` 27 | } `json:"description"` 28 | } `json:"entities"` 29 | } 30 | 31 | // AccountInfoResponse represents the response from Twitter's user lookup endpoint 32 | type AccountInfoResponse struct { 33 | ID int64 `json:"id"` 34 | IDStr string `json:"id_str"` 35 | Name string `json:"name"` 36 | ScreenName string `json:"screen_name"` 37 | Location string `json:"location"` 38 | Description string `json:"description"` 39 | FollowersCount int `json:"followers_count"` 40 | FriendsCount int `json:"friends_count"` 41 | ListedCount int `json:"listed_count"` 42 | CreatedAt string `json:"created_at"` 43 | FavouritesCount int `json:"favourites_count"` 44 | StatusesCount int `json:"statuses_count"` 45 | MediaCount int `json:"media_count"` 46 | Protected bool `json:"protected"` 47 | Verified bool `json:"verified"` 48 | Suspended bool `json:"suspended"` 49 | Following bool `json:"following"` 50 | FollowedBy bool `json:"followed_by"` 51 | FollowRequestSent bool `json:"follow_request_sent"` 52 | } 53 | -------------------------------------------------------------------------------- /twitter_utils/generator.go: -------------------------------------------------------------------------------- 1 | package twitter_utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | ) 8 | 9 | /* 10 | GenerateCSRFToken generates a new CSRF token for Twitter requests. 11 | The token is a 32-character hexadecimal string, matching Twitter's format. 12 | 13 | Example: 14 | token, err := GenerateCSRFToken() 15 | if err != nil { 16 | // handle error 17 | } 18 | // token = "1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p" 19 | 20 | Returns: 21 | - string: 32-character hexadecimal CSRF token 22 | - error: if token generation fails 23 | */ 24 | func GenerateCSRFToken() (string, error) { 25 | // Create a byte array of length 16 (same as Uint8Array(16) in JS) 26 | bytes := make([]byte, 16) 27 | 28 | // Fill bytes with random values (equivalent to crypto.getRandomValues in JS) 29 | if _, err := rand.Read(bytes); err != nil { 30 | return "", fmt.Errorf("failed to generate random bytes: %w", err) 31 | } 32 | 33 | // Convert bytes to hex string (equivalent to toString(16) in JS) 34 | // hex.EncodeToString automatically handles padding, similar to padStart(2, '0') 35 | token := hex.EncodeToString(bytes) 36 | 37 | return token, nil 38 | } 39 | -------------------------------------------------------------------------------- /utils/cookie_client.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | http "github.com/bogdanfinn/fhttp" 5 | "strings" 6 | ) 7 | 8 | type CookieClient struct { 9 | Cookies []http.Cookie 10 | } 11 | 12 | func NewCookieClient() *CookieClient { 13 | return &CookieClient{Cookies: []http.Cookie{}} 14 | } 15 | 16 | func (jar *CookieClient) GetCookieValue(name string) (string, bool) { 17 | for _, cookie := range jar.Cookies { 18 | if cookie.Name == name { 19 | return cookie.Value, true 20 | } 21 | } 22 | return "", false 23 | } 24 | 25 | func (jar *CookieClient) AddCookies(cookies []http.Cookie) { 26 | for _, cookie := range cookies { 27 | jar.Cookies = append(jar.Cookies, cookie) 28 | } 29 | } 30 | 31 | func (jar *CookieClient) SetCookieFromResponse(resp *http.Response) { 32 | for _, httpCookie := range resp.Cookies() { 33 | updated := false 34 | newCookie := http.Cookie{ 35 | Name: httpCookie.Name, 36 | Value: httpCookie.Value, 37 | } 38 | 39 | if httpCookie.Path != "" { 40 | newCookie.Path = httpCookie.Path 41 | } 42 | 43 | if httpCookie.Domain != "" { 44 | newCookie.Domain = httpCookie.Domain 45 | } 46 | 47 | if httpCookie.MaxAge > 0 { 48 | newCookie.MaxAge = httpCookie.MaxAge 49 | } 50 | 51 | if httpCookie.Secure { 52 | newCookie.Secure = httpCookie.Secure 53 | } 54 | 55 | if httpCookie.HttpOnly { 56 | newCookie.HttpOnly = httpCookie.HttpOnly 57 | } 58 | 59 | if !httpCookie.Expires.IsZero() { 60 | newCookie.Expires = httpCookie.Expires 61 | } 62 | 63 | for i, existingCookie := range jar.Cookies { 64 | if existingCookie.Name == httpCookie.Name { 65 | jar.Cookies[i] = newCookie 66 | updated = true 67 | break 68 | } 69 | } 70 | 71 | if !updated { 72 | jar.Cookies = append(jar.Cookies, newCookie) 73 | } 74 | } 75 | } 76 | 77 | // CookiesToHeader this function converts cookies from the []cycletls.Cookie{} format to a header format 78 | // that simply contains a string of all cookies in the name=value format; 79 | func (jar *CookieClient) CookiesToHeader() string { 80 | var cookieStrs []string 81 | for _, cookie := range jar.Cookies { 82 | cookieStrs = append(cookieStrs, cookie.Name+"="+cookie.Value) 83 | } 84 | return strings.Join(cookieStrs, "; ") 85 | } 86 | -------------------------------------------------------------------------------- /utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // IsNumeric checks if a string contains only numeric characters. 4 | // Used to distinguish between numeric IDs and usernames. 5 | // 6 | // Parameters: 7 | // - s: the string to check 8 | // 9 | // Returns: 10 | // - bool: true if string contains only digits 11 | // 12 | // Example: 13 | // 14 | // IsNumeric("123456") // returns true 15 | // IsNumeric("user123") // returns false 16 | // IsNumeric("") // returns false 17 | func IsNumeric(s string) bool { 18 | for _, c := range s { 19 | if c < '0' || c > '9' { 20 | return false 21 | } 22 | } 23 | return true 24 | } -------------------------------------------------------------------------------- /utils/http_client.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | http "github.com/bogdanfinn/fhttp" 9 | tlsClient "github.com/bogdanfinn/tls-client" 10 | "github.com/bogdanfinn/tls-client/profiles" 11 | ) 12 | 13 | func CreateHttpClient(proxies string) (tlsClient.HttpClient, error) { 14 | options := []tlsClient.HttpClientOption{ 15 | tlsClient.WithClientProfile(profiles.Chrome_133_PSK), 16 | tlsClient.WithRandomTLSExtensionOrder(), 17 | tlsClient.WithInsecureSkipVerify(), 18 | tlsClient.WithTimeoutSeconds(30), 19 | } 20 | if proxies != "" { 21 | options = append(options, tlsClient.WithProxyUrl(fmt.Sprintf("http://%s", proxies))) 22 | } 23 | 24 | client, err := tlsClient.NewHttpClient(tlsClient.NewNoopLogger(), options...) 25 | if err != nil { 26 | Logger{}.Error("Failed to create Http Client: %s", err) 27 | return nil, err 28 | } 29 | 30 | return client, nil 31 | } 32 | 33 | func CookiesToHeader(allCookies map[string][]*http.Cookie) string { 34 | var cookieStrs []string 35 | for _, cookies := range allCookies { 36 | for _, cookie := range cookies { 37 | cookieStrs = append(cookieStrs, cookie.Name+"="+cookie.Value) 38 | } 39 | } 40 | return strings.Join(cookieStrs, "; ") 41 | } 42 | 43 | // HeaderPair represents a header key-value pair 44 | type HeaderPair struct { 45 | Key string 46 | Value string 47 | } 48 | 49 | // RequestConfig contains all possible options for making a request 50 | type RequestConfig struct { 51 | Method string 52 | URL string 53 | Body io.Reader 54 | Headers []HeaderPair 55 | } 56 | 57 | // DefaultConfig returns common Twitter request headers and their order 58 | func DefaultConfig() RequestConfig { 59 | return RequestConfig{ 60 | Headers: []HeaderPair{ 61 | {Key: "accept", Value: "*/*"}, 62 | {Key: "accept-encoding", Value: "gzip, deflate, br"}, 63 | {Key: "content-type", Value: "application/x-www-form-urlencoded"}, 64 | {Key: "origin", Value: "https://twitter.com"}, 65 | {Key: "sec-ch-ua-mobile", Value: "?0"}, 66 | {Key: "sec-ch-ua-platform", Value: `"Windows"`}, 67 | {Key: "sec-fetch-dest", Value: "empty"}, 68 | {Key: "sec-fetch-mode", Value: "cors"}, 69 | {Key: "sec-fetch-site", Value: "same-origin"}, 70 | {Key: "user-agent", Value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"}, 71 | }, 72 | } 73 | } 74 | 75 | // MakeRequest handles HTTP requests with proper header ordering and error handling 76 | func MakeRequest(client tlsClient.HttpClient, config RequestConfig) ([]byte, *http.Response, error) { 77 | req, err := http.NewRequest(config.Method, config.URL, config.Body) 78 | if err != nil { 79 | return nil, nil, fmt.Errorf("failed to build request: %w", err) 80 | } 81 | 82 | // Set headers and maintain order 83 | req.Header = make(http.Header) 84 | var headerOrder []string 85 | for _, header := range config.Headers { 86 | req.Header.Set(header.Key, header.Value) 87 | headerOrder = append(headerOrder, header.Key) 88 | } 89 | 90 | req.Header[http.HeaderOrderKey] = headerOrder 91 | req.Header[http.PHeaderOrderKey] = []string{":authority", ":method", ":path", ":scheme"} 92 | 93 | // Make request 94 | resp, err := client.Do(req) 95 | if err != nil { 96 | return nil, nil, fmt.Errorf("failed to do request: %w", err) 97 | } 98 | defer resp.Body.Close() 99 | 100 | // Read response 101 | bodyBytes, err := io.ReadAll(resp.Body) 102 | if err != nil { 103 | return nil, resp, fmt.Errorf("failed to read response body: %w", err) 104 | } 105 | 106 | return bodyBytes, resp, nil 107 | } 108 | -------------------------------------------------------------------------------- /utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gookit/color" 8 | ) 9 | 10 | type Logger struct { 11 | level LogLevel 12 | } 13 | 14 | // LogLevel defines the verbosity of logging 15 | type LogLevel int 16 | 17 | const ( 18 | LogLevelNone LogLevel = iota // No logging 19 | LogLevelError // Only errors 20 | LogLevelWarning // Errors and warnings 21 | LogLevelSuccess // Errors, warnings, and success messages 22 | LogLevelInfo // Normal operational logs 23 | LogLevelDebug // Detailed debug information 24 | ) 25 | 26 | func NewLogger(level LogLevel) Logger { 27 | return Logger{level: level} 28 | } 29 | 30 | func (l Logger) Debug(format string, a ...any) error { 31 | if l.level >= LogLevelDebug { 32 | color.Printf("%s | DEBUG | - %s\n", 33 | time.Now().Format("15:04:05.000"), 34 | fmt.Sprintf(format, a...)) 35 | } 36 | return fmt.Errorf(format, a...) 37 | } 38 | 39 | func (l Logger) Info(format string, a ...any) error { 40 | if l.level >= LogLevelInfo { 41 | color.Printf("%s | INFO | - %s\n", 42 | time.Now().Format("15:04:05.000"), 43 | fmt.Sprintf(format, a...)) 44 | } 45 | return fmt.Errorf(format, a...) 46 | } 47 | 48 | func (l Logger) Error(format string, a ...any) error { 49 | err := fmt.Errorf(format, a...) 50 | if l.level >= LogLevelError { 51 | color.Printf("%s | ERROR | - %s\n", 52 | time.Now().Format("15:04:05.000"), 53 | err.Error()) 54 | } 55 | return err 56 | } 57 | 58 | func (l Logger) Success(format string, a ...any) error { 59 | if l.level >= LogLevelSuccess { 60 | color.Printf("%s | SUCCESS | - %s\n", 61 | time.Now().Format("15:04:05.000"), 62 | fmt.Sprintf(format, a...)) 63 | } 64 | return fmt.Errorf(format, a...) 65 | } 66 | 67 | func (l Logger) Warning(format string, a ...any) error { 68 | if l.level >= LogLevelWarning { 69 | color.Printf("%s | WARNING | - %s\n", 70 | time.Now().Format("15:04:05.000"), 71 | fmt.Sprintf(format, a...)) 72 | } 73 | return fmt.Errorf(format, a...) 74 | } 75 | -------------------------------------------------------------------------------- /utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // RandomSleep sleeps for a random duration between min and max seconds 9 | func RandomSleep(min, max int) { 10 | if min > max { 11 | min, max = max, min // Swap if min is greater than max 12 | } 13 | if min == max { 14 | time.Sleep(time.Duration(min) * time.Second) 15 | return 16 | } 17 | 18 | // Create a new random source each time for true randomness 19 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 20 | seconds := r.Intn(max-min+1) + min 21 | time.Sleep(time.Duration(seconds) * time.Second) 22 | } 23 | 24 | // Sleep sleeps for the exact number of seconds 25 | func Sleep(seconds int) { 26 | time.Sleep(time.Duration(seconds) * time.Second) 27 | } 28 | --------------------------------------------------------------------------------