diff --git a/cmd/tests.sh b/cmd/tests.sh index 27caf27..2f0a909 100755 --- a/cmd/tests.sh +++ b/cmd/tests.sh @@ -348,6 +348,11 @@ tw --session Offline_Twatter search "from:michaelmalice constitution" test $(sqlite3 twitter.db "select count(*) from tweets where user_id = 44067298 and text like '%constitution%'") -gt "30" # Not sure exactly how many +# Test liking and unliking +tw --session Offline_Twatter like_tweet https://twitter.com/elonmusk/status/1589023388676554753 +tw --session Offline_Twatter unlike_tweet https://twitter.com/elonmusk/status/1589023388676554753 + + # TODO: Maybe this file should be broken up into multiple test scripts echo -e "\033[32mAll tests passed. Finished successfully.\033[0m" diff --git a/cmd/twitter/help_message.txt b/cmd/twitter/help_message.txt index f8a3f2d..15af4f4 100644 --- a/cmd/twitter/help_message.txt +++ b/cmd/twitter/help_message.txt @@ -60,7 +60,12 @@ This application downloads tweets from twitter and saves them in a SQLite databa search is the search query. Should be wrapped in quotes if it has spaces. + (Requires authentication) + like_tweet + unlike_tweet + "Like" or un-"like" the tweet indicated by . + (Requires authentication) : -h, --help Print this message, then exit. diff --git a/cmd/twitter/main.go b/cmd/twitter/main.go index 6404ac5..ff8d949 100644 --- a/cmd/twitter/main.go +++ b/cmd/twitter/main.go @@ -144,6 +144,10 @@ func main() { follow_user(target, false) case "list_followed": list_followed() + case "like_tweet": + like_tweet(target) + case "unlike_tweet": + unlike_tweet(target) default: die(fmt.Sprintf("Invalid operation: %s", operation), true, 3) } @@ -318,6 +322,30 @@ func follow_user(handle string, is_followed bool) { } } +func unlike_tweet(tweet_identifier string) { + tweet_id, err := extract_id_from(tweet_identifier) + if err != nil { + die(err.Error(), false, -1) + } + err = scraper.UnlikeTweet(tweet_id) + if err != nil { + die(err.Error(), false, -10) + } + happy_exit("Unliked the tweet.") +} + +func like_tweet(tweet_identifier string) { + tweet_id, err := extract_id_from(tweet_identifier) + if err != nil { + die(err.Error(), false, -1) + } + err = scraper.LikeTweet(tweet_id) + if err != nil { + die(err.Error(), false, -10) + } + happy_exit("Liked the tweet.") +} + func list_followed() { for _, handle := range profile.GetAllFollowedUsers() { fmt.Println(handle) diff --git a/scraper/api_request_utils.go b/scraper/api_request_utils.go index 78381fc..5aac198 100644 --- a/scraper/api_request_utils.go +++ b/scraper/api_request_utils.go @@ -218,6 +218,12 @@ func (api *API) do_http_POST(url string, body string, result interface{}) error api.add_authentication_headers(req) + log.Debug(fmt.Sprintf("POST: %s\n", req.URL.String())) + for header := range req.Header { + log.Debug(fmt.Sprintf(" %s: %s\n", header, req.Header.Get(header))) + } + log.Debug(" " + body) + resp, err := api.Client.Do(req) if err != nil { return fmt.Errorf("Error executing HTTP POST request:\n %w", err) diff --git a/scraper/api_types_posting.go b/scraper/api_types_posting.go new file mode 100644 index 0000000..301fcd6 --- /dev/null +++ b/scraper/api_types_posting.go @@ -0,0 +1,89 @@ +package scraper + +import ( + "errors" + "fmt" + "strings" + + log "github.com/sirupsen/logrus" +) + +var AlreadyLikedThisTweet error = errors.New("already liked this tweet") +var HaventLikedThisTweet error = errors.New("Haven't liked this tweet") + +func (api API) LikeTweet(id TweetID) error { + type LikeResponse struct { + Data struct { + FavoriteTweet string `json:"favorite_tweet"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + Code int `json:"code"` + Kind string `json:"kind"` + Name string `json:"name"` + } `json:"errors"` + } + var result LikeResponse + err := api.do_http_POST( + "https://twitter.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet", + "{\"variables\":{\"tweet_id\":\""+fmt.Sprint(id)+"\"},\"queryId\":\"lI07N6Otwv1PhnEgXILM7A\"}", + &result, + ) + if err != nil { + return fmt.Errorf("Error executing the HTTP POST request:\n %w", err) + } + if len(result.Errors) > 0 { + if strings.Contains(result.Errors[0].Message, "has already favorited tweet") { + return AlreadyLikedThisTweet + } + } + if result.Data.FavoriteTweet != "Done" { + panic(fmt.Sprintf("Dunno why but it failed with value %q", result.Data.FavoriteTweet)) + } + return nil +} + +func (api API) UnlikeTweet(id TweetID) error { + type UnlikeResponse struct { + Data struct { + UnfavoriteTweet string `json:"unfavorite_tweet"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + Code int `json:"code"` + Kind string `json:"kind"` + Name string `json:"name"` + } `json:"errors"` + } + var result UnlikeResponse + err := api.do_http_POST( + "https://twitter.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet", + "{\"variables\":{\"tweet_id\":\""+fmt.Sprint(id)+"\"},\"queryId\":\"ZYKSe-w7KEslx3JhSIk5LA\"}", + &result, + ) + if err != nil { + return fmt.Errorf("Error executing the HTTP POST request:\n %w", err) + } + if len(result.Errors) > 0 { + if strings.Contains(result.Errors[0].Message, "not found in actor's") { + return HaventLikedThisTweet + } + } + if result.Data.UnfavoriteTweet != "Done" { + panic(fmt.Sprintf("Dunno why but it failed with value %q", result.Data.UnfavoriteTweet)) + } + return nil +} + +func LikeTweet(id TweetID) error { + if !the_api.IsAuthenticated { + log.Fatalf("Must be authenticated!") + } + return the_api.LikeTweet(id) +} +func UnlikeTweet(id TweetID) error { + if !the_api.IsAuthenticated { + log.Fatalf("Must be authenticated!") + } + return the_api.UnlikeTweet(id) +}