diff --git a/pkg/persistence/dm_queries.go b/pkg/persistence/dm_queries.go new file mode 100644 index 0000000..067a92e --- /dev/null +++ b/pkg/persistence/dm_queries.go @@ -0,0 +1,133 @@ +package persistence + +import ( + "fmt" + + "offline_twitter/scraper" +) + +func (p Profile) SaveChatRoom(r scraper.DMChatRoom) error { + _, err := p.DB.NamedExec(` + insert into chat_rooms (id, type, last_messaged_at, is_nsfw) + values (:id, :type, :last_messaged_at, :is_nsfw) + on conflict do update + set last_messaged_at=:last_messaged_at + `, r, + ) + if err != nil { + return fmt.Errorf("Error executing SaveChatRoom(ID %s). Info: %#v:\n %w", r.ID, r, err) + } + + for _, participant := range r.Participants { + _, err = p.DB.NamedExec(` + insert into chat_room_participants ( + chat_room_id, + user_id, + last_read_event_id, + is_chat_settings_valid, + is_notifications_disabled, + is_mention_notifications_disabled, + is_read_only, + is_trusted, + is_muted, + status) + values ( + :chat_room_id, + :user_id, + :last_read_event_id, + :is_chat_settings_valid, + :is_notifications_disabled, + :is_mention_notifications_disabled, + :is_read_only, + :is_trusted, + :is_muted, + :status) + on conflict do update + set last_read_event_id=:last_read_event_id, + is_chat_settings_valid=:is_chat_settings_valid, + is_notifications_disabled=:is_notifications_disabled, + is_mention_notifications_disabled=:is_mention_notifications_disabled, + is_read_only=:is_read_only, + is_trusted=:is_trusted, + is_muted=:is_muted, + status=:status + `, participant, + ) + } + if err != nil { + return fmt.Errorf("Error saving chat participant: %#v\n %w", r, err) + } + // } + return nil +} + +func (p Profile) GetChatRoom(id scraper.DMChatRoomID) (ret scraper.DMChatRoom, err error) { + err = p.DB.Get(&ret, ` + select id, type, last_messaged_at, is_nsfw + from chat_rooms + where id = ? + `, id) + if err != nil { + return ret, fmt.Errorf("Error getting chat room (%s):\n %w", id, err) + } + + participants := []scraper.DMChatParticipant{} + err = p.DB.Select(&participants, ` + select chat_room_id, user_id, last_read_event_id, is_chat_settings_valid, is_notifications_disabled, + is_mention_notifications_disabled, is_read_only, is_trusted, is_muted, status + from chat_room_participants + where chat_room_id = ? + `, id, + ) + if err != nil { + return ret, fmt.Errorf("Error getting chat room participants (%s):\n %w", id, err) + } + ret.Participants = make(map[scraper.UserID]scraper.DMChatParticipant) + for _, p := range participants { + ret.Participants[p.UserID] = p + } + return ret, nil +} + +func (p Profile) SaveChatMessage(m scraper.DMMessage) error { + _, err := p.DB.NamedExec(` + insert into chat_messages (id, chat_room_id, sender_id, sent_at, request_id, in_reply_to_id, text) + values (:id, :chat_room_id, :sender_id, :sent_at, :request_id, :in_reply_to_id, :text) + `, m, + ) + if err != nil { + return fmt.Errorf("Error saving message: %#v\n %w", m, err) + } + + _, err = p.DB.NamedExec(` + insert into chat_message_reactions (id, message_id, sender_id, sent_at, emoji) + values (:id, :message_id, :sender_id, :sent_at, :emoji) + `, m.Reactions, + ) + if err != nil { + return fmt.Errorf("Error saving message reactions: %#v\n %w", m, err) + } + return nil +} + +func (p Profile) GetChatMessage(id scraper.DMMessageID) (ret scraper.DMMessage, err error) { + err = p.DB.Get(&ret, ` + select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id + where id = ? + `, id, + ) + if err != nil { + return ret, fmt.Errorf("Error getting chat message (%d):\n %w", id, err) + } + + err = p.DB.Select(&ret.Reactions, ` + select id, message_id, sender_id, sent_at, emoji + from chat_message_reactions + where message_id = ? + `, id, + ) + if err != nil { + return ret, fmt.Errorf("Error getting reactions to chat message (%d):\n %w", id, err) + } + return ret, nil +} diff --git a/pkg/persistence/dm_queries_test.go b/pkg/persistence/dm_queries_test.go new file mode 100644 index 0000000..d130731 --- /dev/null +++ b/pkg/persistence/dm_queries_test.go @@ -0,0 +1,101 @@ +package persistence_test + +import ( + "fmt" + "testing" + + // "math/rand" + // "time" + + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "offline_twitter/scraper" +) + +func TestSaveAndLoadChatRoom(t *testing.T) { + require := require.New(t) + profile_path := "test_profiles/TestDMs" + profile := create_or_load_profile(profile_path) + + chat_room := create_dummy_chat_room() + chat_room.Type = "fnort" + primary_user, is_ok := chat_room.Participants[scraper.UserID(-1)] + require.True(is_ok) + primary_user.Status = fmt.Sprintf("status for %s", chat_room.ID) + chat_room.Participants[primary_user.UserID] = primary_user + + // Save it + err := profile.SaveChatRoom(chat_room) + require.NoError(err) + + // Reload it + new_chat_room, err := profile.GetChatRoom(chat_room.ID) + require.NoError(err) + + if diff := deep.Equal(chat_room, new_chat_room); diff != nil { + t.Error(diff) + } +} + +func TestModifyChatRoom(t *testing.T) { + require := require.New(t) + profile_path := "test_profiles/TestDMs" + profile := create_or_load_profile(profile_path) + + // Save it + chat_room := create_dummy_chat_room() + chat_room.LastMessagedAt = scraper.TimestampFromUnix(2) + err := profile.SaveChatRoom(chat_room) + require.NoError(err) + + // Modify it + chat_room.LastMessagedAt = scraper.TimestampFromUnix(35) + err = profile.SaveChatRoom(chat_room) + require.NoError(err) + + // Reload it + new_chat_room, err := profile.GetChatRoom(chat_room.ID) + require.NoError(err) + + assert.Equal(t, new_chat_room.LastMessagedAt, scraper.TimestampFromUnix(35)) +} + +func TestModifyChatParticipant(t *testing.T) { + require := require.New(t) + profile_path := "test_profiles/TestDMs" + profile := create_or_load_profile(profile_path) + + // Save it + chat_room := create_dummy_chat_room() + err := profile.SaveChatRoom(chat_room) + require.NoError(err) + + // Add a participant and modify the existing one + primary_user, is_ok := chat_room.Participants[scraper.UserID(-1)] + require.True(is_ok) + primary_user.IsReadOnly = true + primary_user.LastReadEventID = scraper.DMMessageID(1500) + chat_room.Participants[primary_user.UserID] = primary_user + new_user := create_dummy_user() + chat_room.Participants[new_user.ID] = scraper.DMChatParticipant{ + DMChatRoomID: chat_room.ID, + UserID: new_user.ID, + LastReadEventID: scraper.DMMessageID(0), + IsChatSettingsValid: false, + } + + // Save again + err = profile.SaveUser(&new_user) + require.NoError(err) + err = profile.SaveChatRoom(chat_room) + require.NoError(err) + + // Reload it + new_chat_room, err := profile.GetChatRoom(chat_room.ID) + require.NoError(err) + + if diff := deep.Equal(chat_room, new_chat_room); diff != nil { + t.Error(diff) + } +} diff --git a/pkg/persistence/utils_test.go b/pkg/persistence/utils_test.go index aab3dda..af16a78 100644 --- a/pkg/persistence/utils_test.go +++ b/pkg/persistence/utils_test.go @@ -29,6 +29,10 @@ func create_or_load_profile(profile_path string) persistence.Profile { panic(err) } err = profile.SaveRetweet(create_stable_retweet()) + if err != nil { + panic(err) + } + err = profile.SaveChatRoom(create_stable_chat_room()) } else { profile, err = persistence.LoadProfile(profile_path) } @@ -299,3 +303,54 @@ func create_dummy_like() scraper.Like { SortID: scraper.LikeSortID(12345), } } + +func create_stable_chat_room() scraper.DMChatRoom { + id := scraper.DMChatRoomID("some chat room ID") + + return scraper.DMChatRoom{ + ID: id, + Type: "ONE_ON_ONE", + LastMessagedAt: scraper.TimestampFromUnix(123), + IsNSFW: false, + Participants: map[scraper.UserID]scraper.DMChatParticipant{ + scraper.UserID(-1): { + DMChatRoomID: id, + UserID: scraper.UserID(-1), + LastReadEventID: scraper.DMMessageID(0), + IsChatSettingsValid: true, + IsNotificationsDisabled: false, + IsMentionNotificationsDisabled: false, + IsReadOnly: false, + IsTrusted: true, + IsMuted: false, + Status: "some status", + }, + }, + } +} + +func create_dummy_chat_room() scraper.DMChatRoom { + rand.Seed(time.Now().UnixNano()) + id := scraper.DMChatRoomID(fmt.Sprintf("Chat Room #%d", rand.Int())) + + return scraper.DMChatRoom{ + ID: id, + Type: "ONE_ON_ONE", + LastMessagedAt: scraper.TimestampFromUnix(10000), + IsNSFW: false, + Participants: map[scraper.UserID]scraper.DMChatParticipant{ + scraper.UserID(-1): { + DMChatRoomID: id, + UserID: scraper.UserID(-1), + LastReadEventID: scraper.DMMessageID(0), + IsChatSettingsValid: true, + IsNotificationsDisabled: false, + IsMentionNotificationsDisabled: false, + IsReadOnly: false, + IsTrusted: true, + IsMuted: false, + Status: "some status", + }, + }, + } +}