2024-10-19 20:38:42 -06:00
|
|
|
package conversation
|
2024-02-21 21:55:38 -07:00
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2024-06-01 00:40:59 -06:00
|
|
|
"slices"
|
2024-02-21 21:55:38 -07:00
|
|
|
"strings"
|
2024-03-13 15:19:12 -06:00
|
|
|
"time"
|
2024-02-21 21:55:38 -07:00
|
|
|
|
|
|
|
sqids "github.com/sqids/sqids-go"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
)
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Repo exposes low-level message and conversation management. See
|
|
|
|
// Service for high-level helpers
|
|
|
|
type Repo interface {
|
2024-10-21 09:33:20 -06:00
|
|
|
LoadConversationList() (ConversationList, error)
|
2024-10-19 20:38:42 -06:00
|
|
|
|
|
|
|
FindConversationByShortName(shortName string) (*Conversation, error)
|
2024-02-21 21:55:38 -07:00
|
|
|
ConversationShortNameCompletions(search string) []string
|
2024-10-19 20:38:42 -06:00
|
|
|
GetConversationByID(int uint) (*Conversation, error)
|
|
|
|
GetRootMessages(conversationID uint) ([]Message, error)
|
|
|
|
|
|
|
|
CreateConversation(title string) (*Conversation, error)
|
|
|
|
UpdateConversation(*Conversation) error
|
|
|
|
DeleteConversation(*Conversation) error
|
|
|
|
|
|
|
|
GetMessageByID(messageID uint) (*Message, error)
|
|
|
|
|
|
|
|
SaveMessage(message Message) (*Message, error)
|
|
|
|
UpdateMessage(message *Message) error
|
|
|
|
DeleteMessage(message *Message, prune bool) error
|
|
|
|
CloneBranch(toClone Message) (*Message, uint, error)
|
|
|
|
Reply(to *Message, messages ...Message) ([]Message, error)
|
2024-02-21 21:55:38 -07:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
PathToRoot(message *Message) ([]Message, error)
|
|
|
|
PathToLeaf(message *Message) ([]Message, error)
|
2024-02-21 21:55:38 -07:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Retrieves and return the "selected thread" of the conversation.
|
|
|
|
// The "selected thread" of the conversation is a chain of messages
|
|
|
|
// starting from the Conversation's SelectedRoot Message, following each
|
|
|
|
// Message's SelectedReply until the tail Message is reached.
|
|
|
|
GetSelectedThread(*Conversation) ([]Message, error)
|
2024-02-21 21:55:38 -07:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Start a new conversation with the given messages
|
|
|
|
StartConversation(messages ...Message) (*Conversation, []Message, error)
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
CloneConversation(toClone Conversation) (*Conversation, uint, error)
|
2024-02-21 21:55:38 -07:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
type repo struct {
|
2024-02-21 21:55:38 -07:00
|
|
|
db *gorm.DB
|
|
|
|
sqids *sqids.Sqids
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func NewRepo(db *gorm.DB) (Repo, error) {
|
2024-02-21 21:55:38 -07:00
|
|
|
models := []any{
|
2024-10-19 20:38:42 -06:00
|
|
|
&Conversation{},
|
|
|
|
&Message{},
|
2024-02-21 21:55:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, x := range models {
|
|
|
|
err := db.AutoMigrate(x)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not perform database migrations: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_sqids, _ := sqids.New(sqids.Options{MinLength: 4})
|
2024-10-19 20:38:42 -06:00
|
|
|
return &repo{db, _sqids}, nil
|
2024-02-21 21:55:38 -07:00
|
|
|
}
|
|
|
|
|
2024-10-21 09:33:20 -06:00
|
|
|
type conversationListItem struct {
|
|
|
|
ID uint
|
|
|
|
ShortName string
|
|
|
|
Title string
|
|
|
|
LastMessageAt time.Time
|
|
|
|
}
|
2024-02-21 21:55:38 -07:00
|
|
|
|
2024-10-21 09:33:20 -06:00
|
|
|
type ConversationList struct {
|
|
|
|
Total int
|
|
|
|
Items []conversationListItem
|
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
|
2024-10-21 09:33:20 -06:00
|
|
|
// LoadConversationList loads existing conversations, ordered by the date
|
|
|
|
// of their latest message, from most recent to oldest.
|
|
|
|
func (s *repo) LoadConversationList() (ConversationList, error) {
|
|
|
|
list := ConversationList{}
|
2024-02-21 21:55:38 -07:00
|
|
|
|
2024-10-21 09:33:20 -06:00
|
|
|
var convos []Conversation
|
|
|
|
err := s.db.Order("last_message_at DESC").Find(&convos).Error
|
2024-05-20 12:12:44 -06:00
|
|
|
if err != nil {
|
2024-10-21 09:33:20 -06:00
|
|
|
return list, err
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-02-21 21:55:38 -07:00
|
|
|
|
2024-10-21 09:33:20 -06:00
|
|
|
for _, c := range convos {
|
|
|
|
list.Items = append(list.Items, conversationListItem{
|
|
|
|
ID: c.ID,
|
|
|
|
ShortName: c.ShortName.String,
|
|
|
|
Title: c.Title,
|
|
|
|
LastMessageAt: c.LastMessageAt,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
list.Total = len(list.Items)
|
|
|
|
return list, nil
|
2024-02-21 21:55:38 -07:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) FindConversationByShortName(shortName string) (*Conversation, error) {
|
|
|
|
if shortName == "" {
|
|
|
|
return nil, errors.New("shortName is empty")
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
var conversation Conversation
|
|
|
|
err := s.db.Preload("SelectedRoot").Where("short_name = ?", shortName).Find(&conversation).Error
|
|
|
|
return &conversation, err
|
2024-02-21 21:55:38 -07:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) ConversationShortNameCompletions(shortName string) []string {
|
|
|
|
var conversations []Conversation
|
2024-05-20 12:12:44 -06:00
|
|
|
// ignore error for completions
|
|
|
|
s.db.Find(&conversations)
|
|
|
|
completions := make([]string, 0, len(conversations))
|
2024-02-21 21:55:38 -07:00
|
|
|
for _, conversation := range conversations {
|
|
|
|
if shortName == "" || strings.HasPrefix(conversation.ShortName.String, shortName) {
|
|
|
|
completions = append(completions, fmt.Sprintf("%s\t%s", conversation.ShortName.String, conversation.Title))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return completions
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) GetConversationByID(id uint) (*Conversation, error) {
|
|
|
|
var conversation Conversation
|
|
|
|
err := s.db.Preload("SelectedRoot").Where("id = ?", id).Find(&conversation).Error
|
2024-05-20 12:12:44 -06:00
|
|
|
if err != nil {
|
2024-10-19 20:38:42 -06:00
|
|
|
return nil, fmt.Errorf("Cannot get conversation %d: %v", id, err)
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
rootMessages, err := s.GetRootMessages(id)
|
2024-05-20 12:12:44 -06:00
|
|
|
if err != nil {
|
2024-10-19 20:38:42 -06:00
|
|
|
return nil, fmt.Errorf("Could not load conversation's root messages %d: %v", id, err)
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
conversation.RootMessages = rootMessages
|
|
|
|
return &conversation, nil
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) CreateConversation(title string) (*Conversation, error) {
|
|
|
|
// Create the new conversation
|
|
|
|
c := &Conversation{Title: title}
|
|
|
|
err := s.db.Save(c).Error
|
2024-05-20 12:12:44 -06:00
|
|
|
if err != nil {
|
2024-10-19 20:38:42 -06:00
|
|
|
return nil, err
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
// Generate and save its "short name"
|
|
|
|
shortName, _ := s.sqids.Encode([]uint64{uint64(c.ID)})
|
|
|
|
c.ShortName = sql.NullString{String: shortName, Valid: true}
|
|
|
|
err = s.db.Updates(c).Error
|
2024-05-20 12:12:44 -06:00
|
|
|
if err != nil {
|
2024-10-19 20:38:42 -06:00
|
|
|
return nil, err
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
return c, nil
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) UpdateConversation(c *Conversation) error {
|
|
|
|
if c == nil || c.ID == 0 {
|
|
|
|
return fmt.Errorf("Conversation is nil or invalid (missing ID)")
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
return s.db.Updates(c).Error
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) DeleteConversation(c *Conversation) error {
|
|
|
|
if c == nil || c.ID == 0 {
|
|
|
|
return fmt.Errorf("Conversation is nil or invalid (missing ID)")
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
// Delete messages first
|
|
|
|
err := s.db.Where("conversation_id = ?", c.ID).Delete(&Message{}).Error
|
2024-06-08 22:10:26 -06:00
|
|
|
if err != nil {
|
2024-10-19 20:38:42 -06:00
|
|
|
return err
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
return s.db.Delete(c).Error
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) SaveMessage(m Message) (*Message, error) {
|
|
|
|
if m.Conversation == nil {
|
|
|
|
return nil, fmt.Errorf("Can't save a message without a conversation (this is a bug)")
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
newMessage := m
|
|
|
|
newMessage.ID = 0
|
|
|
|
return &newMessage, s.db.Create(&newMessage).Error
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) UpdateMessage(m *Message) error {
|
|
|
|
if m == nil || m.ID == 0 {
|
|
|
|
return fmt.Errorf("Message is nil or invalid (missing ID)")
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
return s.db.Updates(m).Error
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) DeleteMessage(message *Message, prune bool) error {
|
|
|
|
return s.db.Delete(&message).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *repo) GetMessageByID(messageID uint) (*Message, error) {
|
|
|
|
var message Message
|
|
|
|
err := s.db.Preload("Parent").Preload("Replies").Preload("SelectedReply").Where("id = ?", messageID).Find(&message).Error
|
|
|
|
return &message, err
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Reply to a message with a series of messages (each followed by the next)
|
|
|
|
func (s *repo) Reply(to *Message, messages ...Message) ([]Message, error) {
|
|
|
|
var savedMessages []Message
|
2024-05-20 12:12:44 -06:00
|
|
|
|
|
|
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
currentParent := to
|
|
|
|
for i := range messages {
|
2024-06-09 12:45:22 -06:00
|
|
|
parent := currentParent
|
2024-05-20 12:12:44 -06:00
|
|
|
message := messages[i]
|
2024-06-09 12:45:22 -06:00
|
|
|
message.Parent = parent
|
|
|
|
message.Conversation = parent.Conversation
|
2024-05-20 12:12:44 -06:00
|
|
|
message.ID = 0
|
|
|
|
message.CreatedAt = time.Time{}
|
|
|
|
|
|
|
|
if err := tx.Create(&message).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// update parent selected reply
|
2024-06-09 12:45:22 -06:00
|
|
|
parent.Replies = append(parent.Replies, message)
|
|
|
|
parent.SelectedReply = &message
|
|
|
|
if err := tx.Model(parent).Update("selected_reply_id", message.ID).Error; err != nil {
|
2024-05-20 12:12:44 -06:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
savedMessages = append(savedMessages, message)
|
|
|
|
currentParent = &message
|
|
|
|
}
|
2024-10-21 09:33:20 -06:00
|
|
|
|
|
|
|
to.Conversation.LastMessageAt = savedMessages[len(savedMessages)-1].CreatedAt
|
|
|
|
s.UpdateConversation(to.Conversation)
|
2024-05-20 12:12:44 -06:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
return savedMessages, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// CloneBranch returns a deep clone of the given message and its replies, returning
|
|
|
|
// a new message object. The new message will be attached to the same parent as
|
2024-06-08 15:28:29 -06:00
|
|
|
// the messageToClone
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) CloneBranch(messageToClone Message) (*Message, uint, error) {
|
2024-05-20 12:12:44 -06:00
|
|
|
newMessage := messageToClone
|
|
|
|
newMessage.ID = 0
|
|
|
|
newMessage.Replies = nil
|
|
|
|
newMessage.SelectedReplyID = nil
|
|
|
|
newMessage.SelectedReply = nil
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
originalReplies := messageToClone.Replies
|
2024-05-20 12:12:44 -06:00
|
|
|
|
|
|
|
if err := s.db.Create(&newMessage).Error; err != nil {
|
|
|
|
return nil, 0, fmt.Errorf("Could not clone message: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var replyCount uint = 0
|
|
|
|
for _, reply := range originalReplies {
|
|
|
|
replyCount++
|
|
|
|
|
|
|
|
newReply := reply
|
|
|
|
newReply.ConversationID = messageToClone.ConversationID
|
|
|
|
newReply.ParentID = &newMessage.ID
|
|
|
|
newReply.Parent = &newMessage
|
|
|
|
|
|
|
|
res, c, err := s.CloneBranch(newReply)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
newMessage.Replies = append(newMessage.Replies, *res)
|
|
|
|
replyCount += c
|
|
|
|
|
|
|
|
if reply.ID == *messageToClone.SelectedReplyID {
|
|
|
|
newMessage.SelectedReplyID = &res.ID
|
|
|
|
if err := s.UpdateMessage(&newMessage); err != nil {
|
|
|
|
return nil, 0, fmt.Errorf("Could not update parent select reply ID: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &newMessage, replyCount, nil
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func fetchMessages(db *gorm.DB) ([]Message, error) {
|
|
|
|
var messages []Message
|
2024-06-08 15:28:29 -06:00
|
|
|
if err := db.Preload("Conversation").Find(&messages).Error; err != nil {
|
2024-06-01 00:40:59 -06:00
|
|
|
return nil, fmt.Errorf("Could not fetch messages: %v", err)
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-06-01 00:40:59 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
messageMap := make(map[uint]Message)
|
2024-06-01 00:40:59 -06:00
|
|
|
for i, message := range messages {
|
|
|
|
messageMap[messages[i].ID] = message
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a map to store replies by their parent ID
|
2024-10-19 20:38:42 -06:00
|
|
|
repliesMap := make(map[uint][]Message)
|
2024-06-01 00:40:59 -06:00
|
|
|
for i, message := range messages {
|
|
|
|
if messages[i].ParentID != nil {
|
|
|
|
repliesMap[*messages[i].ParentID] = append(repliesMap[*messages[i].ParentID], message)
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-06-01 00:40:59 -06:00
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-06-01 00:40:59 -06:00
|
|
|
// Assign replies, parent, and selected reply to each message
|
|
|
|
for i := range messages {
|
|
|
|
if replies, exists := repliesMap[messages[i].ID]; exists {
|
2024-10-19 20:38:42 -06:00
|
|
|
messages[i].Replies = make([]Message, len(replies))
|
2024-06-01 00:40:59 -06:00
|
|
|
for j, m := range replies {
|
|
|
|
messages[i].Replies[j] = m
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if messages[i].ParentID != nil {
|
|
|
|
if parent, exists := messageMap[*messages[i].ParentID]; exists {
|
|
|
|
messages[i].Parent = &parent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if messages[i].SelectedReplyID != nil {
|
|
|
|
if selectedReply, exists := messageMap[*messages[i].SelectedReplyID]; exists {
|
|
|
|
messages[i].SelectedReply = &selectedReply
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
|
|
|
}
|
2024-06-01 00:40:59 -06:00
|
|
|
return messages, nil
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (r repo) GetRootMessages(conversationID uint) ([]Message, error) {
|
|
|
|
var rootMessages []Message
|
|
|
|
err := r.db.Where("conversation_id = ? AND parent_id IS NULL", conversationID).Find(&rootMessages).Error
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not retrieve root messages for conversation %d: %v", conversationID, err)
|
|
|
|
}
|
|
|
|
return rootMessages, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *repo) buildPath(message *Message, getNext func(*Message) *uint) ([]Message, error) {
|
|
|
|
var messages []Message
|
2024-06-01 00:40:59 -06:00
|
|
|
messages, err := fetchMessages(s.db.Where("conversation_id = ?", message.ConversationID))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a map to store messages by their ID
|
2024-10-19 20:38:42 -06:00
|
|
|
messageMap := make(map[uint]*Message, len(messages))
|
2024-06-01 00:40:59 -06:00
|
|
|
for i := range messages {
|
|
|
|
messageMap[messages[i].ID] = &messages[i]
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-06-01 00:40:59 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Construct Replies
|
|
|
|
repliesMap := make(map[uint][]*Message, len(messages))
|
|
|
|
for _, m := range messageMap {
|
|
|
|
if m.ParentID == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if p, ok := messageMap[*m.ParentID]; ok {
|
|
|
|
repliesMap[p.ID] = append(repliesMap[p.ID], m)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add replies to messages
|
|
|
|
for _, m := range messageMap {
|
|
|
|
if replies, ok := repliesMap[m.ID]; ok {
|
|
|
|
m.Replies = make([]Message, len(replies))
|
|
|
|
for idx, reply := range replies {
|
|
|
|
m.Replies[idx] = *reply
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-01 00:40:59 -06:00
|
|
|
// Build the path
|
2024-10-19 20:38:42 -06:00
|
|
|
var path []Message
|
2024-06-01 00:40:59 -06:00
|
|
|
nextID := &message.ID
|
|
|
|
|
2024-05-20 12:12:44 -06:00
|
|
|
for {
|
2024-06-01 00:40:59 -06:00
|
|
|
current, exists := messageMap[*nextID]
|
|
|
|
if !exists {
|
|
|
|
return nil, fmt.Errorf("Message with ID %d not found in conversation", *nextID)
|
|
|
|
}
|
|
|
|
|
2024-05-20 12:12:44 -06:00
|
|
|
path = append(path, *current)
|
2024-06-01 00:40:59 -06:00
|
|
|
|
|
|
|
nextID = getNext(current)
|
|
|
|
if nextID == nil {
|
2024-05-20 12:12:44 -06:00
|
|
|
break
|
|
|
|
}
|
2024-06-01 00:40:59 -06:00
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-06-01 00:40:59 -06:00
|
|
|
return path, nil
|
|
|
|
}
|
|
|
|
|
2024-06-08 15:28:29 -06:00
|
|
|
// PathToRoot traverses the provided message's Parent until reaching the tree
|
|
|
|
// root and returns a slice of all messages traversed in chronological order
|
|
|
|
// (starting with the root and ending with the message provided)
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) PathToRoot(message *Message) ([]Message, error) {
|
2024-06-01 00:40:59 -06:00
|
|
|
if message == nil || message.ID <= 0 {
|
|
|
|
return nil, fmt.Errorf("Message is nil or has invalid ID")
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
path, err := s.buildPath(message, func(m *Message) *uint {
|
2024-06-01 00:40:59 -06:00
|
|
|
return m.ParentID
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2024-05-20 12:12:44 -06:00
|
|
|
}
|
2024-06-01 00:40:59 -06:00
|
|
|
slices.Reverse(path)
|
|
|
|
|
2024-05-20 12:12:44 -06:00
|
|
|
return path, nil
|
|
|
|
}
|
|
|
|
|
2024-06-08 15:28:29 -06:00
|
|
|
// PathToLeaf traverses the provided message's SelectedReply until reaching a
|
|
|
|
// tree leaf and returns a slice of all messages traversed in chronological
|
|
|
|
// order (starting with the message provided and ending with the leaf)
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) PathToLeaf(message *Message) ([]Message, error) {
|
2024-06-01 00:40:59 -06:00
|
|
|
if message == nil || message.ID <= 0 {
|
|
|
|
return nil, fmt.Errorf("Message is nil or has invalid ID")
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
return s.buildPath(message, func(m *Message) *uint {
|
2024-06-01 00:40:59 -06:00
|
|
|
return m.SelectedReplyID
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
func (s *repo) StartConversation(messages ...Message) (*Conversation, []Message, error) {
|
|
|
|
if len(messages) == 0 {
|
|
|
|
return nil, nil, fmt.Errorf("Must provide at least 1 message")
|
|
|
|
}
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Create new conversation
|
|
|
|
conversation, err := s.CreateConversation("")
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
messages[0].Conversation = conversation
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Create first message
|
|
|
|
firstMessage, err := s.SaveMessage(messages[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
messages[0] = *firstMessage
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Update conversation's selected root message
|
|
|
|
conversation.RootMessages = []Message{messages[0]}
|
|
|
|
conversation.SelectedRoot = &messages[0]
|
2024-10-21 09:33:20 -06:00
|
|
|
conversation.LastMessageAt = messages[0].CreatedAt
|
2024-05-20 12:12:44 -06:00
|
|
|
|
2024-10-19 20:38:42 -06:00
|
|
|
// Add additional replies to conversation
|
|
|
|
if len(messages) > 1 {
|
|
|
|
newMessages, err := s.Reply(&messages[0], messages[1:]...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
messages = append([]Message{messages[0]}, newMessages...)
|
2024-10-21 09:33:20 -06:00
|
|
|
conversation.LastMessageAt = messages[len(messages)-1].CreatedAt
|
2024-10-19 20:38:42 -06:00
|
|
|
}
|
|
|
|
|
2024-10-21 09:33:20 -06:00
|
|
|
err = s.UpdateConversation(conversation)
|
|
|
|
return conversation, messages, err
|
|
|
|
}
|
2024-10-19 20:38:42 -06:00
|
|
|
|
|
|
|
// CloneConversation clones the given conversation and all of its meesages
|
|
|
|
func (s *repo) CloneConversation(toClone Conversation) (*Conversation, uint, error) {
|
|
|
|
rootMessages, err := s.GetRootMessages(toClone.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, fmt.Errorf("Could not create clone: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
clone, err := s.CreateConversation(toClone.Title + " - Clone")
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, fmt.Errorf("Could not create clone: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var errors []error
|
|
|
|
var messageCnt uint = 0
|
|
|
|
for _, root := range rootMessages {
|
|
|
|
messageCnt++
|
|
|
|
newRoot := root
|
|
|
|
newRoot.ConversationID = &clone.ID
|
|
|
|
|
|
|
|
cloned, count, err := s.CloneBranch(newRoot)
|
|
|
|
if err != nil {
|
|
|
|
errors = append(errors, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
messageCnt += count
|
|
|
|
|
|
|
|
if root.ID == *toClone.SelectedRootID {
|
|
|
|
clone.SelectedRootID = &cloned.ID
|
|
|
|
if err := s.UpdateConversation(clone); err != nil {
|
|
|
|
errors = append(errors, fmt.Errorf("Could not set selected root on clone: %v", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(errors) > 0 {
|
|
|
|
return nil, 0, fmt.Errorf("Messages failed to be cloned: %v", errors)
|
|
|
|
}
|
|
|
|
|
|
|
|
return clone, messageCnt, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *repo) GetSelectedThread(c *Conversation) ([]Message, error) {
|
|
|
|
if c.SelectedRoot == nil {
|
|
|
|
return nil, fmt.Errorf("No SelectedRoot on conversation - this is a bug")
|
|
|
|
}
|
|
|
|
return s.PathToLeaf(c.SelectedRoot)
|
2024-03-13 15:19:12 -06:00
|
|
|
}
|