🔀 Merge pull request '✨ 基于事件的消息构成' (#1) from refactor/event-based-messages into master
Reviewed-on: Hydrogen/Messaging#1
This commit is contained in:
		| @@ -11,7 +11,7 @@ var DatabaseAutoActionRange = []any{ | ||||
| 	&models.Channel{}, | ||||
| 	&models.ChannelMember{}, | ||||
| 	&models.Call{}, | ||||
| 	&models.Message{}, | ||||
| 	&models.Event{}, | ||||
| } | ||||
|  | ||||
| func RunMigration(source *gorm.DB) error { | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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"` | ||||
| } | ||||
|   | ||||
							
								
								
									
										33
									
								
								pkg/internal/models/events.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								pkg/internal/models/events.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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"` | ||||
| } | ||||
| @@ -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"` | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										126
									
								
								pkg/internal/server/api/events_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								pkg/internal/server/api/events_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										157
									
								
								pkg/internal/server/api/events_message_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								pkg/internal/server/api/events_message_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
							
								
								
									
										152
									
								
								pkg/internal/services/events.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								pkg/internal/services/events.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user