diff --git a/pkg/internal/database/migrator.go b/pkg/internal/database/migrator.go index 48a1894..3a18d47 100644 --- a/pkg/internal/database/migrator.go +++ b/pkg/internal/database/migrator.go @@ -11,7 +11,7 @@ var DatabaseAutoActionRange = []any{ &models.Channel{}, &models.ChannelMember{}, &models.Call{}, - &models.Message{}, + &models.Event{}, } func RunMigration(source *gorm.DB) error { diff --git a/pkg/internal/models/accounts.go b/pkg/internal/models/accounts.go index 8fef2e8..7e3dc46 100644 --- a/pkg/internal/models/accounts.go +++ b/pkg/internal/models/accounts.go @@ -1,8 +1,8 @@ package models // Account profiles basically fetched from Hydrogen.Identity -// But cache at here for better usage -// At the same time this model can make relations between local models +// But cached at here for better usage +// At the same time, this model can make relations between local models type Account struct { BaseModel diff --git a/pkg/internal/models/channels.go b/pkg/internal/models/channels.go index fa40264..d754e93 100644 --- a/pkg/internal/models/channels.go +++ b/pkg/internal/models/channels.go @@ -14,7 +14,7 @@ type Channel struct { Name string `json:"name"` Description string `json:"description"` Members []ChannelMember `json:"members"` - Messages []Message `json:"messages"` + Messages []Event `json:"messages"` Calls []Call `json:"calls"` Type ChannelType `json:"type"` Account Account `json:"account"` @@ -44,6 +44,6 @@ type ChannelMember struct { Notify NotifyLevel `json:"notify"` PowerLevel int `json:"power_level"` - Calls []Call `json:"calls" gorm:"foreignKey:FounderID"` - Messages []Message `json:"messages" gorm:"foreignKey:SenderID"` + Calls []Call `json:"calls" gorm:"foreignKey:FounderID"` + Events []Event `json:"events" gorm:"foreignKey:SenderID"` } diff --git a/pkg/internal/models/events.go b/pkg/internal/models/events.go new file mode 100644 index 0000000..af73d73 --- /dev/null +++ b/pkg/internal/models/events.go @@ -0,0 +1,33 @@ +package models + +import "gorm.io/datatypes" + +const ( + EventMessageNew = "messages.new" + EventMessageEdit = "messages.edit" + EventMessageDelete = "messages.delete" + EventSystemChanges = "system.changes" +) + +type Event struct { + BaseModel + + Uuid string `json:"uuid"` + Body datatypes.JSONMap `json:"body"` + Type string `json:"type"` + Channel Channel `json:"channel"` + Sender ChannelMember `json:"sender"` + ChannelID uint `json:"channel_id"` + SenderID uint `json:"sender_id"` +} + +// Event Payloads + +type EventMessageBody struct { + Text string `json:"text,omitempty"` + Algorithm string `json:"algorithm,omitempty"` + Attachments []uint `json:"attachments,omitempty"` + QuoteEvent uint `json:"quote_event,omitempty"` + RelatedEvent uint `json:"related_event,omitempty"` + RelatedUsers []uint `json:"related_users,omitempty"` +} diff --git a/pkg/internal/models/messages.go b/pkg/internal/models/messages.go deleted file mode 100644 index d489262..0000000 --- a/pkg/internal/models/messages.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -import "gorm.io/datatypes" - -const ( - MessageTextType = "m.text" -) - -type Message struct { - BaseModel - - Uuid string `json:"uuid"` - Content datatypes.JSONMap `json:"content"` - Type string `json:"type"` - Attachments datatypes.JSONSlice[uint] `json:"attachments"` - Channel Channel `json:"channel"` - Sender ChannelMember `json:"sender"` - ReplyID *uint `json:"reply_id"` - ReplyTo *Message `json:"reply_to" gorm:"foreignKey:ReplyID"` - ChannelID uint `json:"channel_id"` - SenderID uint `json:"sender_id"` -} diff --git a/pkg/internal/server/api/calls_api.go b/pkg/internal/server/api/calls_api.go index 824bbd3..8b50565 100644 --- a/pkg/internal/server/api/calls_api.go +++ b/pkg/internal/server/api/calls_api.go @@ -6,6 +6,7 @@ import ( "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" "git.solsynth.dev/hydrogen/messaging/pkg/internal/services" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" "github.com/spf13/viper" ) @@ -71,6 +72,16 @@ func startCall(c *fiber.Ctx) error { if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } else { + _, _ = services.NewEvent(models.Event{ + Uuid: uuid.NewString(), + Body: map[string]any{}, + Type: "calls.start", + Channel: channel, + Sender: membership, + ChannelID: channel.ID, + SenderID: membership.ID, + }) + return c.JSON(call) } } @@ -107,6 +118,16 @@ func endCall(c *fiber.Ctx) error { if call, err := services.EndCall(call); err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } else { + _, _ = services.NewEvent(models.Event{ + Uuid: uuid.NewString(), + Body: map[string]any{"last": call.EndedAt.Unix() - call.CreatedAt.Unix()}, + Type: "calls.end", + Channel: channel, + Sender: membership, + ChannelID: channel.ID, + SenderID: membership.ID, + }) + return c.JSON(call) } } diff --git a/pkg/internal/server/api/events_api.go b/pkg/internal/server/api/events_api.go new file mode 100644 index 0000000..7452de9 --- /dev/null +++ b/pkg/internal/server/api/events_api.go @@ -0,0 +1,126 @@ +package api + +import ( + "fmt" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/gap" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/services" + "github.com/gofiber/fiber/v2" +) + +func getEvent(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + alias := c.Params("channel") + id, _ := c.ParamsInt("eventId") + + var err error + var channel models.Channel + if val, ok := c.Locals("realm").(models.Realm); ok { + channel, err = services.GetChannelWithAlias(alias, val.ID) + } else { + channel, err = services.GetChannelWithAlias(alias) + } + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil { + return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err)) + } + + event, err := services.GetEvent(channel, uint(id)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + return c.JSON(event) +} + +func listEvent(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + alias := c.Params("channel") + + var err error + var channel models.Channel + if val, ok := c.Locals("realm").(models.Realm); ok { + channel, err = services.GetChannelWithAlias(alias, val.ID) + } else { + channel, err = services.GetChannelWithAlias(alias) + } + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil { + return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err)) + } + + count := services.CountEvent(channel) + events, err := services.ListEvent(channel, take, offset) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": events, + }) +} + +func newRawEvent(c *fiber.Ctx) error { + if err := gap.H.EnsureGrantedPerm(c, "CreateMessagingRawEvent", true); err != nil { + return err + } + user := c.Locals("user").(models.Account) + alias := c.Params("channel") + + var data struct { + Uuid string `json:"uuid" validate:"required"` + Type string `json:"type" validate:"required"` + Body map[string]any `json:"body"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } else if len(data.Uuid) < 36 { + return fiber.NewError(fiber.StatusBadRequest, "message uuid was not valid") + } + + var err error + var channel models.Channel + var member models.ChannelMember + if val, ok := c.Locals("realm").(models.Realm); ok { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + } else { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if member.PowerLevel < 0 { + return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to send message") + } + } + + event := models.Event{ + Uuid: data.Uuid, + Body: data.Body, + Type: data.Type, + Sender: member, + Channel: channel, + ChannelID: channel.ID, + SenderID: member.ID, + } + + if event, err = services.NewEvent(event); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(event) +} diff --git a/pkg/internal/server/api/events_message_api.go b/pkg/internal/server/api/events_message_api.go new file mode 100644 index 0000000..690c63c --- /dev/null +++ b/pkg/internal/server/api/events_message_api.go @@ -0,0 +1,157 @@ +package api + +import ( + "git.solsynth.dev/hydrogen/messaging/pkg/internal/gap" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/services" + "github.com/gofiber/fiber/v2" + jsoniter "github.com/json-iterator/go" +) + +func newMessageEvent(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + alias := c.Params("channel") + + var data struct { + Uuid string `json:"uuid" validate:"required"` + Type string `json:"type" validate:"required"` + Body models.EventMessageBody `json:"body"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } else if len(data.Uuid) < 36 { + return fiber.NewError(fiber.StatusBadRequest, "message uuid was not valid") + } + + if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "you cannot send an empty message") + } + + var err error + var channel models.Channel + var member models.ChannelMember + if val, ok := c.Locals("realm").(models.Realm); ok { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + } else { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if member.PowerLevel < 0 { + return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to send message") + } + } + + var parsed map[string]any + raw, _ := jsoniter.Marshal(data.Body) + _ = jsoniter.Unmarshal(raw, &parsed) + + event := models.Event{ + Uuid: data.Uuid, + Body: parsed, + Type: data.Type, + Sender: member, + Channel: channel, + ChannelID: channel.ID, + SenderID: member.ID, + } + + if event, err = services.NewEvent(event); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(event) +} + +func editMessageEvent(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + alias := c.Params("channel") + messageId, _ := c.ParamsInt("messageId", 0) + + var data struct { + Uuid string `json:"uuid" validate:"required"` + Type string `json:"type" validate:"required"` + Body models.EventMessageBody `json:"body"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "you cannot send an empty message") + } + + var err error + var channel models.Channel + var member models.ChannelMember + if val, ok := c.Locals("realm").(models.Realm); ok { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + } else { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + } + + var event models.Event + if event, err = services.GetEventWithSender(channel, member, uint(messageId)); err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + event, err = services.EditMessage(event, data.Body) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(event) +} + +func deleteMessageEvent(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + alias := c.Params("channel") + messageId, _ := c.ParamsInt("messageId", 0) + + var err error + var channel models.Channel + var member models.ChannelMember + if val, ok := c.Locals("realm").(models.Realm); ok { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + } else { + channel, member, err = services.GetAvailableChannelWithAlias(alias, user) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + } + + var event models.Event + if event, err = services.GetEventWithSender(channel, member, uint(messageId)); err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + event, err = services.DeleteMessage(event) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(event) +} diff --git a/pkg/internal/server/api/messages_ws.go b/pkg/internal/server/api/events_ws.go similarity index 100% rename from pkg/internal/server/api/messages_ws.go rename to pkg/internal/server/api/events_ws.go diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index fba6996..96a3adc 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -33,10 +33,13 @@ func MapAPIs(app *fiber.App) { channels.Delete("/:channel/members", removeChannelMember) channels.Delete("/:channel/members/me", leaveChannel) - channels.Get("/:channel/messages", listMessage) - channels.Post("/:channel/messages", newMessage) - channels.Put("/:channel/messages/:messageId", editMessage) - channels.Delete("/:channel/messages/:messageId", deleteMessage) + channels.Get("/:channel/events", listEvent) + channels.Get("/:channel/events/:eventId", getEvent) + channels.Post("/:channel/events", newRawEvent) + + channels.Post("/:channel/messages", newMessageEvent) + channels.Put("/:channel/messages/:messageId", editMessageEvent) + channels.Delete("/:channel/messages/:messageId", deleteMessageEvent) channels.Get("/:channel/calls", listCall) channels.Get("/:channel/calls/ongoing", getOngoingCall) diff --git a/pkg/internal/server/api/messages_api.go b/pkg/internal/server/api/messages_api.go deleted file mode 100644 index aa673e7..0000000 --- a/pkg/internal/server/api/messages_api.go +++ /dev/null @@ -1,221 +0,0 @@ -package api - -import ( - "fmt" - "git.solsynth.dev/hydrogen/messaging/pkg/internal/database" - "git.solsynth.dev/hydrogen/messaging/pkg/internal/gap" - "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" - "git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts" - "git.solsynth.dev/hydrogen/messaging/pkg/internal/services" - "github.com/gofiber/fiber/v2" - "strings" -) - -func listMessage(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - alias := c.Params("channel") - - var err error - var channel models.Channel - if val, ok := c.Locals("realm").(models.Realm); ok { - channel, err = services.GetChannelWithAlias(alias, val.ID) - } else { - channel, err = services.GetChannelWithAlias(alias) - } - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil { - return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err)) - } - - count := services.CountMessage(channel) - messages, err := services.ListMessage(channel, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - return c.JSON(fiber.Map{ - "count": count, - "data": messages, - }) -} - -func newMessage(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - alias := c.Params("channel") - - var data struct { - Uuid string `json:"uuid" validate:"required"` - Type string `json:"type" validate:"required"` - Content map[string]any `json:"content"` - Attachments []uint `json:"attachments"` - ReplyTo *uint `json:"reply_to"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } else if len(data.Uuid) < 36 { - return fiber.NewError(fiber.StatusBadRequest, "message uuid was not valid") - } - - if len(data.Attachments) == 0 { - if data.Type == models.MessageTextType { - if val, ok := data.Content["value"].(string); ok && len(strings.TrimSpace(val)) == 0 { - return fiber.NewError(fiber.StatusBadRequest, "you cannot send an empty message") - } else if !ok { - return fiber.NewError(fiber.StatusBadRequest, "invalid content of text message") - } - } - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "m.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - var err error - var channel models.Channel - var member models.ChannelMember - if val, ok := c.Locals("realm").(models.Realm); ok { - channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - } else { - channel, member, err = services.GetAvailableChannelWithAlias(alias, user) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } else if member.PowerLevel < 0 { - return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to send message") - } - } - - message := models.Message{ - Uuid: data.Uuid, - Content: data.Content, - Sender: member, - Channel: channel, - ChannelID: channel.ID, - SenderID: member.ID, - Attachments: data.Attachments, - Type: data.Type, - } - - var replyTo models.Message - if data.ReplyTo != nil { - if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("message to reply was not found: %v", err)) - } else { - message.ReplyTo = &replyTo - message.ReplyID = &replyTo.ID - } - } - - if message, err = services.NewMessage(message); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(message) -} - -func editMessage(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - alias := c.Params("channel") - messageId, _ := c.ParamsInt("messageId", 0) - - var data struct { - Type string `json:"type" validate:"required"` - Content map[string]any `json:"content"` - Attachments []uint `json:"attachments"` - ReplyTo *uint `json:"reply_to"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "m.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - var err error - var channel models.Channel - var member models.ChannelMember - if val, ok := c.Locals("realm").(models.Realm); ok { - channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - } else { - channel, member, err = services.GetAvailableChannelWithAlias(alias, user) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - } - - var message models.Message - if message, err = services.GetMessageWithPrincipal(channel, member, uint(messageId)); err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - message.Attachments = data.Attachments - message.Content = data.Content - message.Type = data.Type - - message, err = services.EditMessage(message) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(message) -} - -func deleteMessage(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - alias := c.Params("channel") - messageId, _ := c.ParamsInt("messageId", 0) - - var err error - var channel models.Channel - var member models.ChannelMember - if val, ok := c.Locals("realm").(models.Realm); ok { - channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - } else { - channel, member, err = services.GetAvailableChannelWithAlias(alias, user) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - } - - var message models.Message - if message, err = services.GetMessageWithPrincipal(channel, member, uint(messageId)); err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - message, err = services.DeleteMessage(message) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(message) -} diff --git a/pkg/internal/services/events.go b/pkg/internal/services/events.go new file mode 100644 index 0000000..eed3692 --- /dev/null +++ b/pkg/internal/services/events.go @@ -0,0 +1,152 @@ +package services + +import ( + "fmt" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/database" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" + jsoniter "github.com/json-iterator/go" + "github.com/rs/zerolog/log" + "github.com/samber/lo" + "strings" +) + +func CountEvent(channel models.Channel) int64 { + var count int64 + if err := database.C.Where(models.Event{ + ChannelID: channel.ID, + }).Model(&models.Event{}).Count(&count).Error; err != nil { + return 0 + } else { + return count + } +} + +func ListEvent(channel models.Channel, take int, offset int) ([]models.Event, error) { + if take > 100 { + take = 100 + } + + var events []models.Event + if err := database.C. + Where(models.Event{ + ChannelID: channel.ID, + }).Limit(take).Offset(offset). + Order("created_at DESC"). + Preload("Sender"). + Preload("Sender.Account"). + Find(&events).Error; err != nil { + return events, err + } else { + return events, nil + } +} + +func GetEvent(channel models.Channel, id uint) (models.Event, error) { + var event models.Event + if err := database.C. + Where(models.Event{ + BaseModel: models.BaseModel{ID: id}, + ChannelID: channel.ID, + }). + Preload("Sender"). + Preload("Sender.Account"). + First(&event).Error; err != nil { + return event, err + } else { + return event, nil + } +} + +func GetEventWithSender(channel models.Channel, member models.ChannelMember, id uint) (models.Event, error) { + var event models.Event + if err := database.C.Where(models.Event{ + BaseModel: models.BaseModel{ID: id}, + ChannelID: channel.ID, + SenderID: member.ID, + }).First(&event).Error; err != nil { + return event, err + } else { + return event, nil + } +} + +func NewEvent(event models.Event) (models.Event, error) { + var members []models.ChannelMember + if err := database.C.Save(&event).Error; err != nil { + return event, err + } else if err = database.C.Where(models.ChannelMember{ + ChannelID: event.ChannelID, + }).Preload("Account").Find(&members).Error; err != nil { + // Couldn't get channel members, skip notifying + return event, nil + } + + event, _ = GetEvent(event.Channel, event.ID) + for _, member := range members { + PushCommand(member.AccountID, models.UnifiedCommand{ + Action: "events.new", + Payload: event, + }) + } + + if strings.HasPrefix(event.Type, "messages") { + NotifyMessageEvent(members, event) + } + + return event, nil +} + +func NotifyMessageEvent(members []models.ChannelMember, event models.Event) { + var body models.EventMessageBody + raw, _ := jsoniter.Marshal(event.Body) + _ = jsoniter.Unmarshal(raw, &body) + + for _, member := range members { + if member.ID != event.SenderID { + switch member.Notify { + case models.NotifyLevelNone: + continue + case models.NotifyLevelMentioned: + if len(body.RelatedUsers) == 0 || !lo.Contains(body.RelatedUsers, member.AccountID) { + continue + } + default: + break + } + + var displayText string + if body.Algorithm == "plain" { + displayText = body.Text + } + + if len(displayText) == 0 { + displayText = fmt.Sprintf("%d attachment(s)", len(body.Attachments)) + } + + err := NotifyAccountMessager(member.Account, + "incomingMessage", + fmt.Sprintf("%s in #%s", event.Sender.Account.Nick, event.Channel.Alias), + fmt.Sprintf("%s", displayText), + true, + false, + ) + if err != nil { + log.Warn().Err(err).Msg("An error occurred when trying notify user.") + } + } + } +} + +func EditEvent(event models.Event) (models.Event, error) { + if err := database.C.Save(&event).Error; err != nil { + return event, err + } + return event, nil +} + +func DeleteEvent(event models.Event) (models.Event, error) { + if err := database.C.Delete(&event).Error; err != nil { + return event, err + } + return event, nil +} diff --git a/pkg/internal/services/messages.go b/pkg/internal/services/messages.go index 744f28f..e935d83 100644 --- a/pkg/internal/services/messages.go +++ b/pkg/internal/services/messages.go @@ -1,181 +1,60 @@ package services import ( - "fmt" - - "git.solsynth.dev/hydrogen/messaging/pkg/internal/database" "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" - "github.com/rs/zerolog/log" - "github.com/samber/lo" + "github.com/google/uuid" + jsoniter "github.com/json-iterator/go" ) -func CountMessage(channel models.Channel) int64 { - var count int64 - if err := database.C.Where(models.Message{ - ChannelID: channel.ID, - }).Model(&models.Message{}).Count(&count).Error; err != nil { - return 0 - } else { - return count - } +func EncodeMessageBody(body models.EventMessageBody) map[string]any { + var parsed map[string]any + raw, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(raw, &parsed) + return parsed } -func ListMessage(channel models.Channel, take int, offset int) ([]models.Message, error) { - if take > 100 { - take = 100 +func EditMessage(event models.Event, body models.EventMessageBody) (models.Event, error) { + event.Body = EncodeMessageBody(body) + event, err := EditEvent(event) + if err != nil { + return event, err + } + body.RelatedEvent = event.ID + _, err = NewEvent(models.Event{ + Uuid: uuid.NewString(), + Body: EncodeMessageBody(body), + Type: models.EventMessageEdit, + Channel: event.Channel, + Sender: event.Sender, + ChannelID: event.ChannelID, + SenderID: event.SenderID, + }) + if err != nil { + return event, err } - var messages []models.Message - if err := database.C. - Where(models.Message{ - ChannelID: channel.ID, - }).Limit(take).Offset(offset). - Order("created_at DESC"). - Preload("ReplyTo"). - Preload("ReplyTo.Sender"). - Preload("ReplyTo.Sender.Account"). - Preload("Sender"). - Preload("Sender.Account"). - Find(&messages).Error; err != nil { - return messages, err - } else { - return messages, nil - } + return event, nil } -func GetMessage(channel models.Channel, id uint) (models.Message, error) { - var message models.Message - if err := database.C. - Where(models.Message{ - BaseModel: models.BaseModel{ID: id}, - ChannelID: channel.ID, - }). - Preload("ReplyTo"). - Preload("ReplyTo.Sender"). - Preload("ReplyTo.Sender.Account"). - Preload("Sender"). - Preload("Sender.Account"). - First(&message).Error; err != nil { - return message, err - } else { - return message, nil +func DeleteMessage(event models.Event) (models.Event, error) { + _, err := DeleteEvent(event) + if err != nil { + return event, err } -} - -func GetMessageWithPrincipal(channel models.Channel, member models.ChannelMember, id uint) (models.Message, error) { - var message models.Message - if err := database.C.Where(models.Message{ - BaseModel: models.BaseModel{ID: id}, - ChannelID: channel.ID, - SenderID: member.ID, - }).First(&message).Error; err != nil { - return message, err - } else { - return message, nil - } -} - -func NewMessage(message models.Message) (models.Message, error) { - var members []models.ChannelMember - if err := database.C.Save(&message).Error; err != nil { - return message, err - } else if err = database.C.Where(models.ChannelMember{ - ChannelID: message.ChannelID, - }).Preload("Account").Find(&members).Error; err == nil { - channel := message.Channel - message, _ = GetMessage(message.Channel, message.ID) - for _, member := range members { - PushCommand(member.AccountID, models.UnifiedCommand{ - Action: "messages.new", - Payload: message, - }) - - if member.ID != message.SenderID { - switch member.Notify { - case models.NotifyLevelNone: - continue - case models.NotifyLevelMentioned: - if message.ReplyTo != nil && member.ID == message.ReplyTo.SenderID { - break - } - if val, ok := message.Content["mentioned_users"]; ok { - if usernames, ok := val.([]string); ok { - if lo.Contains(usernames, member.Account.Name) { - break - } - } - } - default: - break - } - - var displayText string - if message.Content["algorithm"] == "plain" { - displayText, _ = message.Content["value"].(string) - } else { - displayText = "*encrypted message*" - } - - if len(displayText) == 0 { - displayText = fmt.Sprintf("%d attachment(s)", len(message.Attachments)) - } - - err = NotifyAccountMessager(member.Account, - "incomingMessage", - fmt.Sprintf("%s in #%s", message.Sender.Account.Nick, channel.Alias), - fmt.Sprintf("%s", displayText), - true, - false, - ) - if err != nil { - log.Warn().Err(err).Msg("An error occurred when trying notify user.") - } - } - } + _, err = NewEvent(models.Event{ + Uuid: uuid.NewString(), + Body: EncodeMessageBody(models.EventMessageBody{ + RelatedEvent: event.ID, + }), + Type: models.EventMessageDelete, + Channel: event.Channel, + Sender: event.Sender, + ChannelID: event.ChannelID, + SenderID: event.SenderID, + }) + if err != nil { + return event, err } - return message, nil -} - -func EditMessage(message models.Message) (models.Message, error) { - var members []models.ChannelMember - if err := database.C.Save(&message).Error; err != nil { - return message, err - } else if err = database.C.Where(models.ChannelMember{ - ChannelID: message.ChannelID, - }).Find(&members).Error; err == nil { - message, _ = GetMessage(models.Channel{ - BaseModel: models.BaseModel{ID: message.Channel.ID}, - }, message.ID) - for _, member := range members { - PushCommand(member.AccountID, models.UnifiedCommand{ - Action: "messages.update", - Payload: message, - }) - } - } - - return message, nil -} - -func DeleteMessage(message models.Message) (models.Message, error) { - prev, _ := GetMessage(models.Channel{ - BaseModel: models.BaseModel{ID: message.Channel.ID}, - }, message.ID) - - var members []models.ChannelMember - if err := database.C.Delete(&message).Error; err != nil { - return message, err - } else if err = database.C.Where(models.ChannelMember{ - ChannelID: message.ChannelID, - }).Find(&members).Error; err == nil { - for _, member := range members { - PushCommand(member.AccountID, models.UnifiedCommand{ - Action: "messages.burnt", - Payload: prev, - }) - } - } - - return message, nil + return event, nil }