diff --git a/cmd/twitter/main.go b/cmd/twitter/main.go index e636ad3..a4bfd2c 100644 --- a/cmd/twitter/main.go +++ b/cmd/twitter/main.go @@ -215,6 +215,15 @@ func main() { } send_dm(target, args[2], val) } + case "send_dm_reacc": + if len(args) != 4 { + die("", true, 1) + } + val, err := strconv.Atoi(args[2]) + if err != nil { + panic(err) + } + send_dm_reacc(args[1], val, args[3]) // room, message, emoji default: die(fmt.Sprintf("Invalid operation: %s", operation), true, 3) } @@ -546,3 +555,20 @@ func send_dm(room_id string, text string, in_reply_to_id int) { profile.SaveTweetTrove(trove, true) happy_exit(fmt.Sprintf("Saved %d messages from %d chats", len(trove.Messages), len(trove.Rooms)), nil) } + +func send_dm_reacc(room_id string, in_reply_to_id int, reacc string) { + room, err := profile.GetChatRoom(scraper.DMChatRoomID(room_id)) + if err != nil { + die(fmt.Sprintf("No such chat room: %d", in_reply_to_id), false, 1) + } + _, err = profile.GetChatMessage(scraper.DMMessageID(in_reply_to_id)) + if err != nil { + die(fmt.Sprintf("No such message: %d", in_reply_to_id), false, 1) + } + err = scraper.SendDMReaction(room.ID, scraper.DMMessageID(in_reply_to_id), reacc) + if err != nil { + die(fmt.Sprintf("Failed to react to message:\n %s", err.Error()), false, 1) + } + + happy_exit("Sent the reaction", nil) +} diff --git a/internal/webserver/handler_messages.go b/internal/webserver/handler_messages.go index 16161c2..e2c9ca5 100644 --- a/internal/webserver/handler_messages.go +++ b/internal/webserver/handler_messages.go @@ -8,6 +8,7 @@ import ( "net/http" "strconv" "strings" + "time" "gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence" "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" @@ -77,6 +78,30 @@ func (app *Application) message_detail(w http.ResponseWriter, r *http.Request) { return } + // Handle reactions + if len(parts) == 1 && parts[0] == "reacc" { + var data struct { + MessageID scraper.DMMessageID `json:"message_id,string"` + Reacc string `json:"reacc"` + } + data_, err := io.ReadAll(r.Body) + panic_if(err) + panic_if(json.Unmarshal(data_, &data)) + panic_if(scraper.SendDMReaction(room_id, data.MessageID, data.Reacc)) + + dm_message := global_data.Messages[data.MessageID] + dm_message.Reactions[app.ActiveUser.ID] = scraper.DMReaction{ + ID: 0, // Hopefully will be OK temporarily + DMMessageID: dm_message.ID, + SenderID: app.ActiveUser.ID, + SentAt: scraper.Timestamp{time.Now()}, + Emoji: data.Reacc, + } + global_data.Messages[dm_message.ID] = dm_message + app.buffered_render_htmx(w, "message", global_data, dm_message) + return + } + // First send a message, if applicable if is_sending { app.message_send(w, r) diff --git a/internal/webserver/tpl/tweet_page_includes/chat_view.tpl b/internal/webserver/tpl/tweet_page_includes/chat_view.tpl index 25dd415..6209538 100644 --- a/internal/webserver/tpl/tweet_page_includes/chat_view.tpl +++ b/internal/webserver/tpl/tweet_page_includes/chat_view.tpl @@ -1,7 +1,7 @@ {{define "message"}} {{$user := (user .SenderID)}} {{$is_us := (eq .SenderID (active_user).ID)}} -
+
{{template "circle-profile-img" $user}} @@ -65,6 +65,18 @@
{{end}}
+
+
+ +
+
{{range .Reactions}} diff --git a/pkg/scraper/api_types_dms.go b/pkg/scraper/api_types_dms.go index b5b5f68..6835909 100644 --- a/pkg/scraper/api_types_dms.go +++ b/pkg/scraper/api_types_dms.go @@ -522,6 +522,29 @@ func (api *API) SendDMMessage(room_id DMChatRoomID, text string, in_reply_to_id return result, err } +// Send a reacc +func (api *API) SendDMReaction(room_id DMChatRoomID, message_id DMMessageID, reacc string) error { + url := "https://twitter.com/i/api/graphql/VyDyV9pC2oZEj6g52hgnhA/useDMReactionMutationAddMutation" + body := `{"variables":{"conversationId":"` + string(room_id) + `","messageId":"` + fmt.Sprint(message_id) + + `","reactionTypes":["Emoji"],"emojiReactions":["` + reacc + `"]},"queryId":"VyDyV9pC2oZEj6g52hgnhA"}` + type SendDMResponse struct { + Data struct { + CreateDmReaction struct { + Typename string `json:"__typename"` + } `json:"create_dm_reaction"` + } `json:"data"` + } + var result SendDMResponse + err := api.do_http_POST(url, body, &result) + if err != nil { + return fmt.Errorf("Error executing HTTP POST:\n %w", err) + } + if result.Data.CreateDmReaction.Typename != "CreateDMReactionSuccess" { + return fmt.Errorf("Unexpected result sending DM reaction: %s", result.Data.CreateDmReaction.Typename) + } + return nil +} + // Mark a chat as read. func (api *API) MarkDMChatRead(room_id DMChatRoomID, read_message_id DMMessageID) { url := fmt.Sprintf("https://twitter.com/i/api/1.1/dm/conversation/%s/mark_read.json", room_id) diff --git a/pkg/scraper/dm_trove.go b/pkg/scraper/dm_trove.go index bd186d5..c789d5f 100644 --- a/pkg/scraper/dm_trove.go +++ b/pkg/scraper/dm_trove.go @@ -89,6 +89,12 @@ func SendDMMessage(room_id DMChatRoomID, text string, in_reply_to_id DMMessageID } return dm_response.ToTweetTrove() } +func SendDMReaction(room_id DMChatRoomID, message_id DMMessageID, reacc string) error { + if !the_api.IsAuthenticated { + log.Fatalf("Fetching DMs can only be done when authenticated. Please provide `--session [user]`") + } + return the_api.SendDMReaction(room_id, message_id, reacc) +} func MarkDMChatRead(room_id DMChatRoomID, read_message_id DMMessageID) { if !the_api.IsAuthenticated { log.Fatalf("Writing DMs can only be done when authenticated. Please provide `--session [user]`")