516 lines
16 KiB
Go

package persistence
import (
"database/sql"
"errors"
"fmt"
"strings"
"github.com/jmoiron/sqlx"
. "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
)
func (p Profile) SaveChatRoom(r DMChatRoom) error {
_, err := p.DB.NamedExec(`
insert into chat_rooms (id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
avatar_image_remote_url, avatar_image_local_path)
values (:id, :type, :last_messaged_at, :is_nsfw, :created_at, :created_by_user_id, :name,
:avatar_image_remote_url, :avatar_image_local_path)
on conflict do update
set last_messaged_at=:last_messaged_at,
name=:name,
avatar_image_remote_url=:avatar_image_remote_url,
avatar_image_local_path=:avatar_image_local_path
`, 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 DMChatRoomID) (ret DMChatRoom, err error) {
err = p.DB.Get(&ret, `
select id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name, avatar_image_remote_url, avatar_image_local_path
from chat_rooms
where id = ?
`, id)
if err != nil {
return ret, fmt.Errorf("Error getting chat room (%s):\n %w", id, err)
}
participants := []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[UserID]DMChatParticipant)
for _, p := range participants {
ret.Participants[p.UserID] = p
}
return ret, nil
}
func (p Profile) SaveChatMessage(m DMMessage) error {
// The message itself
_, err := p.DB.NamedExec(`
insert into chat_messages (id, chat_room_id, sender_id, sent_at, request_id, in_reply_to_id, text, embedded_tweet_id)
values (:id, :chat_room_id, :sender_id, :sent_at, :request_id, :in_reply_to_id, :text, :embedded_tweet_id)
on conflict do nothing
`, m,
)
if err != nil {
return fmt.Errorf("Error saving message: %#v\n %w", m, err)
}
// Reactions
for _, reacc := range m.Reactions {
fmt.Println(reacc)
_, 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)
on conflict do nothing
`, reacc,
)
if err != nil {
return fmt.Errorf("Error saving message reaction (message %d, reacc %d): %#v\n %w", m.ID, reacc.ID, reacc, err)
}
}
// Images
for _, img := range m.Images {
_, err := p.DB.NamedExec(`
insert into chat_message_images (id, chat_message_id, width, height, remote_url, local_filename, is_downloaded)
values (:id, :chat_message_id, :width, :height, :remote_url, :local_filename, :is_downloaded)
on conflict do update
set is_downloaded=(is_downloaded or :is_downloaded)
`,
img,
)
if err != nil {
return fmt.Errorf("Error saving image (message ID %d):\n %w", img.DMMessageID, err)
}
}
// Videos
for _, vid := range m.Videos {
_, err := p.DB.NamedExec(`
insert into chat_message_videos
(id, chat_message_id, width, height, remote_url, local_filename, thumbnail_remote_url, thumbnail_local_filename,
duration, view_count, is_downloaded, is_blocked_by_dmca, is_gif)
values (:id, :chat_message_id, :width, :height, :remote_url, :local_filename, :thumbnail_remote_url,
:thumbnail_local_filename, :duration, :view_count, :is_downloaded, :is_blocked_by_dmca, :is_gif)
on conflict do update
set is_downloaded=(is_downloaded or :is_downloaded),
view_count=max(view_count, :view_count),
is_blocked_by_dmca = :is_blocked_by_dmca
`,
vid,
)
if err != nil {
return fmt.Errorf("Error saving video (message ID %d):\n %w", vid.DMMessageID, err)
}
}
// Urls
for _, url := range m.Urls {
_, err := p.DB.NamedExec(`
insert into chat_message_urls (chat_message_id, domain, text, short_text, title, description, creator_id, site_id,
thumbnail_width, thumbnail_height, thumbnail_remote_url, thumbnail_local_path, has_card,
has_thumbnail, is_content_downloaded)
values (:chat_message_id, :domain, :text, :short_text, :title, :description, :creator_id, :site_id, :thumbnail_width,
:thumbnail_height, :thumbnail_remote_url, :thumbnail_local_path, :has_card, :has_thumbnail, :is_content_downloaded
)
on conflict do update
set is_content_downloaded=(is_content_downloaded or :is_content_downloaded)
`, url)
if err != nil {
return fmt.Errorf("Error saving Url (message ID %d):\n %w", url.DMMessageID, err)
}
}
return nil
}
func (p Profile) GetChatMessage(id DMMessageID) (ret DMMessage, err error) {
err = p.DB.Get(&ret, `
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
from chat_messages
where id = ?
`, id,
)
if err != nil {
return ret, fmt.Errorf("Error getting chat message %d:\n %w", id, err)
}
// Reactions
reaccs := []DMReaction{}
err = p.DB.Select(&reaccs, `
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)
}
ret.Reactions = make(map[UserID]DMReaction)
for _, r := range reaccs {
ret.Reactions[r.SenderID] = r
}
// Images
err = p.DB.Select(&ret.Images, `
select id, chat_message_id, width, height, remote_url, local_filename, is_downloaded
from chat_message_images
where chat_message_id = ?
`, ret.ID)
if err != nil {
return ret, fmt.Errorf("Error getting images for chat messsage %d:\n %w", id, err)
}
// Videos
err = p.DB.Select(&ret.Videos, `
select id, chat_message_id, width, height, remote_url, local_filename, thumbnail_remote_url, thumbnail_local_filename,
duration, view_count, is_downloaded, is_blocked_by_dmca, is_gif
from chat_message_videos
where chat_message_id = ?
`, ret.ID)
if err != nil {
return ret, fmt.Errorf("Error getting videos for chat messsage %d:\n %w", id, err)
}
// Urls
err = p.DB.Select(&ret.Urls, `
select chat_message_id, domain, text, short_text, title, description, creator_id, site_id, thumbnail_width, thumbnail_height,
thumbnail_remote_url, thumbnail_local_path, has_card, has_thumbnail, is_content_downloaded
from chat_message_urls
where chat_message_id = ?
`, ret.ID)
if err != nil {
return ret, fmt.Errorf("Error getting urls for chat messsage %d:\n %w", id, err)
}
return ret, nil
}
type DMChatView struct {
DMTrove
RoomIDs []DMChatRoomID
MessageIDs []DMMessageID
ActiveRoomID DMChatRoomID
}
func NewDMChatView() DMChatView {
return DMChatView{
DMTrove: NewDMTrove(),
RoomIDs: []DMChatRoomID{},
MessageIDs: []DMMessageID{},
}
}
func (p Profile) GetChatRoomsPreview(id UserID) DMChatView {
ret := NewDMChatView()
var rooms []DMChatRoom
err := p.DB.Select(&rooms, `
select id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
avatar_image_remote_url, avatar_image_local_path
from chat_rooms
where exists (select 1 from chat_room_participants where chat_room_id = chat_rooms.id and user_id = ?)
order by last_messaged_at desc
`, id)
if err != nil {
panic(err)
}
for _, room := range rooms {
// Fetch the latest message
var msg DMMessage
q, args, err := sqlx.Named(`
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
from chat_messages
where chat_room_id = :room_id
and sent_at = (select max(sent_at) from chat_messages where chat_room_id = :room_id)
`, struct {
ID DMChatRoomID `db:"room_id"`
}{ID: room.ID})
if err != nil {
panic(err)
}
err = p.DB.Get(&msg, q, args...)
if errors.Is(err, sql.ErrNoRows) {
// TODO
fmt.Printf("No messages found in chat; skipping preview\n")
} else if err != nil {
panic(err)
}
// Fetch the participants
// DUPE chat-room-participants-SQL
var participants []struct {
DMChatParticipant
User
}
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, `+USERS_ALL_SQL_FIELDS+`
from chat_room_participants join users on chat_room_participants.user_id = users.id
where chat_room_id = ?
`, room.ID)
if err != nil {
panic(err)
}
room.Participants = make(map[UserID]DMChatParticipant)
for _, participant := range participants {
room.Participants[participant.User.ID] = participant.DMChatParticipant
ret.Users[participant.User.ID] = participant.User
}
// Add everything to the Trove
room.LastMessageID = msg.ID
ret.Rooms[room.ID] = room
ret.Messages[msg.ID] = msg
ret.RoomIDs = append(ret.RoomIDs, room.ID)
}
return ret
}
func (p Profile) GetChatRoomContents(id DMChatRoomID, latest_timestamp int) DMChatView {
ret := NewDMChatView()
var room DMChatRoom
err := p.DB.Get(&room, `
select id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
avatar_image_remote_url, avatar_image_local_path
from chat_rooms
where id = ?
`, id)
if err != nil {
panic(err)
}
// Fetch the participants
// DUPE chat-room-participants-SQL
var participants []struct {
DMChatParticipant
User
}
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, `+USERS_ALL_SQL_FIELDS+`
from chat_room_participants join users on chat_room_participants.user_id = users.id
where chat_room_id = ?
`, room.ID)
if err != nil {
panic(err)
}
room.Participants = make(map[UserID]DMChatParticipant)
for _, participant := range participants {
room.Participants[participant.User.ID] = participant.DMChatParticipant
ret.Users[participant.User.ID] = participant.User
}
// Fetch all messages
var msgs []DMMessage
err = p.DB.Select(&msgs, `
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
from chat_messages
where chat_room_id = ?
and sent_at > ?
order by sent_at desc
limit 50
`, room.ID, latest_timestamp)
if err != nil {
panic(err)
}
ret.MessageIDs = make([]DMMessageID, len(msgs))
for i, msg := range msgs {
ret.MessageIDs[len(ret.MessageIDs)-i-1] = msg.ID
msg.Reactions = make(map[UserID]DMReaction)
ret.Messages[msg.ID] = msg
}
// Set last message ID on chat room
if len(ret.MessageIDs) > 0 {
// If there's no messages, it should be OK to have LastMessageID = 0, since this is only used
// to generate previews
room.LastMessageID = ret.MessageIDs[len(ret.MessageIDs)-1]
}
// Put the room in the Trove
ret.Rooms[room.ID] = room
if len(ret.MessageIDs) > 0 {
// Fetch all reaccs
var reaccs []DMReaction
message_ids_copy := make([]interface{}, len(ret.MessageIDs))
for i, id := range ret.MessageIDs {
message_ids_copy[i] = id
}
err = p.DB.Select(&reaccs, `
select id, message_id, sender_id, sent_at, emoji
from chat_message_reactions
where message_id in (`+strings.Repeat("?,", len(ret.MessageIDs)-1)+`?)
`, message_ids_copy...)
if err != nil {
panic(err)
}
for _, reacc := range reaccs {
msg := ret.Messages[reacc.DMMessageID]
msg.Reactions[reacc.SenderID] = reacc
ret.Messages[reacc.DMMessageID] = msg
}
// Images
var images []Image
err = p.DB.Select(&images, `
select id, chat_message_id, width, height, remote_url, local_filename, is_downloaded
from chat_message_images
where chat_message_id in (`+strings.Repeat("?,", len(ret.MessageIDs)-1)+`?)
`, message_ids_copy...)
if err != nil {
panic(err)
}
for _, img := range images {
msg := ret.Messages[img.DMMessageID]
msg.Images = []Image{img}
ret.Messages[msg.ID] = msg
}
// Videos
var videos []Video
err = p.DB.Select(&videos, `
select id, chat_message_id, width, height, remote_url, local_filename, thumbnail_remote_url, thumbnail_local_filename,
duration, view_count, is_downloaded, is_blocked_by_dmca, is_gif
from chat_message_videos
where chat_message_id in (`+strings.Repeat("?,", len(ret.MessageIDs)-1)+`?)
`, message_ids_copy...)
if err != nil {
panic(err)
}
for _, vid := range videos {
println("asdfasfasdf")
msg := ret.Messages[vid.DMMessageID]
msg.Videos = []Video{vid}
ret.Messages[msg.ID] = msg
}
// Urls
var urls []Url
err = p.DB.Select(&urls, `
select chat_message_id, domain, text, short_text, title, description, creator_id, site_id, thumbnail_width, thumbnail_height,
thumbnail_remote_url, thumbnail_local_path, has_card, has_thumbnail, is_content_downloaded
from chat_message_urls
where chat_message_id in (`+strings.Repeat("?,", len(ret.MessageIDs)-1)+`?)
`, message_ids_copy...)
if err != nil {
panic(err)
}
for _, url := range urls {
msg := ret.Messages[url.DMMessageID]
msg.Urls = []Url{url}
ret.Messages[msg.ID] = msg
}
// Fetch all embedded tweets
embedded_tweet_ids := []interface{}{}
for _, m := range ret.Messages {
if m.EmbeddedTweetID != 0 {
embedded_tweet_ids = append(embedded_tweet_ids, m.EmbeddedTweetID)
}
}
if len(embedded_tweet_ids) > 0 {
var embedded_tweets []Tweet
err = p.DB.Select(&embedded_tweets, `
select `+TWEETS_ALL_SQL_FIELDS+`
from tweets
left join tombstone_types on tweets.tombstone_type = tombstone_types.rowid
left join likes on tweets.id = likes.tweet_id and likes.user_id = ?
where id in (`+strings.Repeat("?,", len(embedded_tweet_ids)-1)+`?)`,
append([]interface{}{UserID(0)}, embedded_tweet_ids...)...)
if err != nil {
panic(err)
}
for _, t := range embedded_tweets {
ret.Tweets[t.ID] = t
}
}
// Fetch replied-to message previews
replied_message_ids := []interface{}{}
for _, m := range ret.Messages {
if m.InReplyToID != 0 {
// Don't clobber if it's already been fetched
if _, is_ok := ret.Messages[m.InReplyToID]; !is_ok {
replied_message_ids = append(replied_message_ids, m.InReplyToID)
}
}
}
if len(replied_message_ids) > 0 {
var replied_msgs []DMMessage
err = p.DB.Select(&replied_msgs, `
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
from chat_messages
where id in (`+strings.Repeat("?,", len(replied_message_ids)-1)+`?)`,
replied_message_ids...)
if err != nil {
panic(err)
}
for _, msg := range replied_msgs {
msg.Reactions = make(map[UserID]DMReaction)
ret.Messages[msg.ID] = msg
}
}
p.fill_content(&ret.DMTrove.TweetTrove, UserID(0))
}
return ret
}