diff --git a/cmd/twitter/main.go b/cmd/twitter/main.go index 45b7241..cf92a09 100644 --- a/cmd/twitter/main.go +++ b/cmd/twitter/main.go @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/term" "os" + "strconv" "strings" "syscall" @@ -163,6 +164,16 @@ func main() { fetch_inbox(*how_many) case "fetch_dm": fetch_dm(target, *how_many) + case "send_dm": + if len(args) == 3 { + send_dm(target, args[2], 0) + } else { + val, err := strconv.Atoi(args[3]) + if err != nil { + panic(err) + } + send_dm(target, args[2], val) + } default: die(fmt.Sprintf("Invalid operation: %s", operation), true, 3) } @@ -416,3 +427,14 @@ func fetch_dm(id string, how_many int) { profile.SaveDMTrove(trove) happy_exit(fmt.Sprintf("Saved %d messages from %d chats", len(trove.Messages), len(trove.Rooms))) } + +func send_dm(room_id string, text string, in_reply_to_id int) { + room, err := profile.GetChatRoom(scraper.DMChatRoomID(room_id)) + if err != nil { + panic(err) + } + + trove := scraper.SendDMMessage(room.ID, text, scraper.DMMessageID(in_reply_to_id)) + profile.SaveDMTrove(trove) + happy_exit(fmt.Sprintf("Saved %d messages from %d chats", len(trove.Messages), len(trove.Rooms))) +} diff --git a/go.mod b/go.mod index 1b635c0..f0f21dd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/andybalholm/cascadia v1.3.2 github.com/go-playground/form/v4 v4.2.1 github.com/go-test/deep v1.0.7 + github.com/google/uuid v1.1.1 github.com/jarcoal/httpmock v1.1.0 github.com/jmoiron/sqlx v1.3.4 github.com/mattn/go-sqlite3 v1.14.7 diff --git a/internal/webserver/handler_user_feed.go b/internal/webserver/handler_user_feed.go index 42850df..6f309fe 100644 --- a/internal/webserver/handler_user_feed.go +++ b/internal/webserver/handler_user_feed.go @@ -45,8 +45,8 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) { app.error_404(w) return } - app.Profile.SaveUser(&user) - app.Profile.DownloadUserContentFor(&user) + panic_if(app.Profile.SaveUser(&user)) + panic_if(app.Profile.DownloadUserContentFor(&user)) } if r.URL.Query().Has("scrape") { diff --git a/pkg/scraper/api_types_dms.go b/pkg/scraper/api_types_dms.go index a80ae39..e4b69a5 100644 --- a/pkg/scraper/api_types_dms.go +++ b/pkg/scraper/api_types_dms.go @@ -4,6 +4,8 @@ import ( "fmt" "net/url" "strings" + + "github.com/google/uuid" ) type APIDMReaction struct { @@ -422,3 +424,67 @@ func (api *API) PollInboxUpdates(cursor string) (APIInbox, error) { err = api.do_http(url.String(), "", &result) return result.UserEvents, err } + +func (api *API) SendDMMessage(room_id DMChatRoomID, text string, in_reply_to_id DMMessageID) (APIInbox, error) { + url, err := url.Parse("https://twitter.com/i/api/1.1/dm/new2.json") + if err != nil { + panic(err) + } + + query := url.Query() + query.Add("nsfw_filtering_enabled", "false") + query.Add("filter_low_quality", "true") + query.Add("include_quality", "all") + query.Add("dm_secret_conversations_enabled", "false") + query.Add("krs_registration_enabled", "true") + query.Add("cards_platform", "Web-12") + query.Add("include_cards", "1") + query.Add("include_ext_alt_text", "true") + query.Add("include_ext_limited_action_results", "true") + query.Add("include_quote_count", "true") + query.Add("include_reply_count", "1") + query.Add("tweet_mode", "extended") + query.Add("include_ext_views", "true") + query.Add("dm_users", "false") + query.Add("include_groups", "true") + query.Add("include_inbox_timelines", "true") + query.Add("include_ext_media_color", "true") + query.Add("supports_reactions", "true") + query.Add("include_ext_edit_control", "true") + query.Add("include_ext_business_affiliations_label", "true") + query.Add("ext", strings.Join([]string{ + "mediaColor", + "altText", + "businessAffiliationsLabel", + "mediaStats", + "highlightedLabel", + "hasNftAvatar", + "voiceInfo", + "birdwatchPivot", + "enrichments", + "superFollowMetadata", + "unmentionInfo", + "editControl", + "vibe", + }, ",")) + url.RawQuery = query.Encode() + + request_id, err := uuid.NewUUID() + if err != nil { + panic(err) + } + + replying_to_text := "" + if in_reply_to_id != 0 { + replying_to_text = fmt.Sprintf(`"reply_to_dm_id":"%d",`, in_reply_to_id) + } + + post_data := `{"conversation_id":"` + string(room_id) + + `","recipient_ids":false,"request_id":"` + request_id.String() + + `","text":"` + text + `",` + + replying_to_text + `"cards_platform":"Web-12","include_cards":1,"include_quote_count":true,"dm_users":false}` + + var result APIDMResponse + err = api.do_http_POST(url.String(), post_data, &result) + return result.UserEvents, err +} diff --git a/pkg/scraper/dm_trove.go b/pkg/scraper/dm_trove.go index bafc88f..1f8d892 100644 --- a/pkg/scraper/dm_trove.go +++ b/pkg/scraper/dm_trove.go @@ -100,3 +100,14 @@ func PollInboxUpdates(cursor string) (DMTrove, string) { return dm_response.ToDMTrove(), dm_response.Cursor } + +func SendDMMessage(room_id DMChatRoomID, text string, in_reply_to_id DMMessageID) DMTrove { + if !the_api.IsAuthenticated { + log.Fatalf("Fetching DMs can only be done when authenticated. Please provide `--session [user]`") + } + dm_response, err := the_api.SendDMMessage(room_id, text, in_reply_to_id) + if err != nil { + panic(err) + } + return dm_response.ToDMTrove() +}