├── 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 | [](https://github.com/0xStarLabs/TwitterAPI/stargazers)
10 | [](https://github.com/0xStarLabs/TwitterAPI/watchers)
11 | 
12 | 
13 | [](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 |
--------------------------------------------------------------------------------