diff --git a/pkg/internal/database/migrator.go b/pkg/internal/database/migrator.go index c94aea1..26781e5 100644 --- a/pkg/internal/database/migrator.go +++ b/pkg/internal/database/migrator.go @@ -8,6 +8,8 @@ import ( var AutoMaintainRange = []any{ &models.Account{}, &models.Attachment{}, + &models.Sticker{}, + &models.StickerPack{}, } func RunMigration(source *gorm.DB) error { diff --git a/pkg/internal/models/stickers.go b/pkg/internal/models/stickers.go new file mode 100644 index 0000000..0cae40f --- /dev/null +++ b/pkg/internal/models/stickers.go @@ -0,0 +1,25 @@ +package models + +type Sticker struct { + BaseModel + + Alias string `json:"alias"` + Name string `json:"name"` + AttachmentID uint `json:"attachment_id"` + Attachment Attachment `json:"attachment"` + PackID uint `json:"pack_id"` + Pack StickerPack `json:"pack"` + AccountID uint `json:"account_id"` + Account Account `json:"account"` +} + +type StickerPack struct { + BaseModel + + Prefix string `json:"prefix"` + Name string `json:"name"` + Description string `json:"description"` + Stickers []Sticker `json:"stickers" gorm:"constraint:OnDelete:DELETE"` + AccountID uint `json:"account_id"` + Account Account `json:"account"` +} diff --git a/pkg/internal/server/api/attachments_api.go b/pkg/internal/server/api/attachments_api.go index eba0f84..20efe37 100644 --- a/pkg/internal/server/api/attachments_api.go +++ b/pkg/internal/server/api/attachments_api.go @@ -80,11 +80,10 @@ func getAttachmentMeta(c *fiber.Ctx) error { } func createAttachment(c *fiber.Ctx) error { - var user models.Account if err := gap.H.EnsureAuthenticated(c); err != nil { return err } - user = c.Locals("user").(models.Account) + user := c.Locals("user").(models.Account) usage := c.FormValue("usage") if !lo.Contains(viper.GetStringSlice("accepts_usage"), usage) { diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 2812e0f..d2983d3 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -13,5 +13,15 @@ func MapAPIs(app *fiber.App, baseURL string) { api.Post("/attachments", createAttachment) api.Put("/attachments/:id", updateAttachmentMeta) api.Delete("/attachments/:id", deleteAttachment) + + api.Get("/stickers/packs", listStickerPacks) + api.Post("/stickers/packs", createStickerPack) + api.Put("/stickers/packs/:packId", updateStickerPack) + api.Delete("/stickers/packs/:packId", deleteStickerPack) + + api.Get("/stickers/:stickerId", getSticker) + api.Post("/stickers", createSticker) + api.Put("/stickers/:stickerId", updateSticker) + api.Delete("/stickers/:stickerId", deleteSticker) } } diff --git a/pkg/internal/server/api/sticker_packs_api.go b/pkg/internal/server/api/sticker_packs_api.go new file mode 100644 index 0000000..e204f1a --- /dev/null +++ b/pkg/internal/server/api/sticker_packs_api.go @@ -0,0 +1,100 @@ +package api + +import ( + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/gap" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/models" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/services" + "github.com/gofiber/fiber/v2" +) + +func listStickerPacks(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + + if take > 100 { + take = 100 + } + + stickers, err := services.ListStickerPackWithStickers(take, offset) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return c.JSON(stickers) +} + +func createStickerPack(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Prefix string `json:"prefix" validate:"required,alphanum,min=2,max=12"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + pack, err := services.NewStickerPack(user, data.Prefix, data.Name, data.Description) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(pack) +} + +func updateStickerPack(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Prefix string `json:"prefix" validate:"required,alphanum,min=2,max=12"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + id, _ := c.ParamsInt("packId", 0) + pack, err := services.GetStickerPackWithUser(uint(id), user.ID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + pack.Prefix = data.Prefix + pack.Name = data.Name + pack.Description = data.Description + + if pack, err = services.UpdateStickerPack(pack); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(pack) +} + +func deleteStickerPack(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + id, _ := c.ParamsInt("packId", 0) + pack, err := services.GetStickerPackWithUser(uint(id), user.ID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + if pack, err = services.DeleteStickerPack(pack); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(pack) +} diff --git a/pkg/internal/server/api/stickers_api.go b/pkg/internal/server/api/stickers_api.go new file mode 100644 index 0000000..699eeb9 --- /dev/null +++ b/pkg/internal/server/api/stickers_api.go @@ -0,0 +1,152 @@ +package api + +import ( + "fmt" + "strings" + + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/database" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/gap" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/models" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/services" + "github.com/gofiber/fiber/v2" +) + +func getSticker(c *fiber.Ctx) error { + id, _ := c.ParamsInt("stickerId", 0) + sticker, err := services.GetSticker(uint(id)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + return c.JSON(sticker) +} + +func createSticker(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Alias string `json:"alias" validate:"required,alphanum,min=2,max=12"` + Name string `json:"name" validate:"required"` + AttachmentID uint `json:"attachment_id"` + PackID uint `json:"pack_id"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + var attachment models.Attachment + if err := database.C.Where("id = ?", data.AttachmentID).First(&attachment).Error; err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find attachment: %v", err)) + } else if !attachment.IsAnalyzed { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be analyzed") + } + + if strings.SplitN(attachment.MimeType, "/", 2)[0] != "image" { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be an image") + } else if width, ok := attachment.Metadata["width"].(float64); !ok { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must has width metadata") + } else if height, ok := attachment.Metadata["height"].(float64); !ok { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must has height metadata") + } else if width != 28 || height != 28 { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be a 28x28 image") + } + + var pack models.StickerPack + if err := database.C.Where("id = ? AND account_id = ?", data.PackID, user.ID).First(&pack).Error; err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find pack: %v", err)) + } + + sticker, err := services.NewSticker(models.Sticker{ + Alias: data.Alias, + Name: data.Name, + Attachment: attachment, + AccountID: user.ID, + PackID: pack.ID, + AttachmentID: data.AttachmentID, + }) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(sticker) +} + +func updateSticker(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Alias string `json:"alias" validate:"required,alphanum,min=2,max=12"` + Name string `json:"name" validate:"required"` + AttachmentID uint `json:"attachment_id"` + PackID uint `json:"pack_id"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + id, _ := c.ParamsInt("stickerId", 0) + sticker, err := services.GetStickerWithUser(uint(id), user.ID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + var attachment models.Attachment + if err := database.C.Where("id = ?", data.AttachmentID).First(&attachment).Error; err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find attachment: %v", err)) + } else if !attachment.IsAnalyzed { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be analyzed") + } + + if strings.SplitN(attachment.MimeType, "/", 2)[0] != "image" { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be an image") + } else if width, ok := attachment.Metadata["width"].(float64); !ok { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must has width metadata") + } else if height, ok := attachment.Metadata["height"].(float64); !ok { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must has height metadata") + } else if width != 28 || height != 28 { + return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be a 28x28 image") + } + + var pack models.StickerPack + if err := database.C.Where("id = ? AND account_id = ?", data.PackID, user.ID).First(&pack).Error; err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find pack: %v", err)) + } + + sticker.Alias = data.Alias + sticker.Name = data.Name + sticker.PackID = data.PackID + sticker.AttachmentID = data.AttachmentID + + if sticker, err = services.UpdateSticker(sticker); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(sticker) +} + +func deleteSticker(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + id, _ := c.ParamsInt("stickerId", 0) + sticker, err := services.GetStickerWithUser(uint(id), user.ID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + if sticker, err = services.DeleteSticker(sticker); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(sticker) +} diff --git a/pkg/internal/services/sticker_packs.go b/pkg/internal/services/sticker_packs.go new file mode 100644 index 0000000..ad2d1b9 --- /dev/null +++ b/pkg/internal/services/sticker_packs.go @@ -0,0 +1,50 @@ +package services + +import ( + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/database" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/models" +) + +func GetStickerPackWithUser(id, userId uint) (models.StickerPack, error) { + var pack models.StickerPack + if err := database.C.Where("id = ? AND account_id = ?", id, userId).First(&pack).Error; err != nil { + return pack, err + } + return pack, nil +} + +func ListStickerPackWithStickers(take, offset int) ([]models.StickerPack, error) { + var packs []models.StickerPack + if err := database.C.Limit(take).Offset(offset).Preload("Stickers").Find(&packs).Error; err != nil { + return packs, err + } + return packs, nil +} + +func NewStickerPack(user models.Account, prefix, name, desc string) (models.StickerPack, error) { + pack := models.StickerPack{ + Prefix: prefix, + Name: name, + Description: desc, + AccountID: user.ID, + } + + if err := database.C.Save(&pack).Error; err != nil { + return pack, err + } + return pack, nil +} + +func UpdateStickerPack(pack models.StickerPack) (models.StickerPack, error) { + if err := database.C.Save(&pack).Error; err != nil { + return pack, err + } + return pack, nil +} + +func DeleteStickerPack(pack models.StickerPack) (models.StickerPack, error) { + if err := database.C.Delete(&pack).Error; err != nil { + return pack, err + } + return pack, nil +} diff --git a/pkg/internal/services/stickers.go b/pkg/internal/services/stickers.go new file mode 100644 index 0000000..ec0e659 --- /dev/null +++ b/pkg/internal/services/stickers.go @@ -0,0 +1,43 @@ +package services + +import ( + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/database" + "git.solsynth.dev/hydrogen/paperclip/pkg/internal/models" +) + +func GetSticker(id uint) (models.Sticker, error) { + var sticker models.Sticker + if err := database.C.Where("id = ?", id).First(&sticker).Error; err != nil { + return sticker, err + } + return sticker, nil +} + +func GetStickerWithUser(id, userId uint) (models.Sticker, error) { + var sticker models.Sticker + if err := database.C.Where("id = ? AND account_id = ?", id, userId).First(&sticker).Error; err != nil { + return sticker, err + } + return sticker, nil +} + +func NewSticker(sticker models.Sticker) (models.Sticker, error) { + if err := database.C.Save(&sticker).Error; err != nil { + return sticker, err + } + return sticker, nil +} + +func UpdateSticker(sticker models.Sticker) (models.Sticker, error) { + if err := database.C.Save(&sticker).Error; err != nil { + return sticker, err + } + return sticker, nil +} + +func DeleteSticker(sticker models.Sticker) (models.Sticker, error) { + if err := database.C.Delete(&sticker).Error; err != nil { + return sticker, err + } + return sticker, nil +} diff --git a/pkg/proto/attachments.proto b/pkg/proto/attachments.proto deleted file mode 100644 index 235ca0b..0000000 --- a/pkg/proto/attachments.proto +++ /dev/null @@ -1,33 +0,0 @@ -syntax = "proto3"; - -option go_package = ".;proto"; - -import "google/protobuf/empty.proto"; - -package proto; - -service Attachments { - rpc GetAttachment(AttachmentLookupRequest) returns (Attachment) {} - rpc CheckAttachmentExists(AttachmentLookupRequest) returns (google.protobuf.Empty) {} -} - -message Attachment { - uint64 id = 1; - string uuid = 2; - int64 size = 3; - string name = 4; - string alt = 5; - string usage = 6; - string mimetype = 7; - string hash = 8; - string destination = 9; - bytes metadata = 10; - bool is_mature = 11; - uint64 account_id = 12; -} - -message AttachmentLookupRequest { - optional uint64 id = 1; - optional string uuid = 2; - optional string usage = 3; -} diff --git a/settings.toml b/settings.toml index 1bb55ee..6bd0ba4 100644 --- a/settings.toml +++ b/settings.toml @@ -3,9 +3,14 @@ id = "paperclip01" bind = "0.0.0.0:8443" grpc_bind = "0.0.0.0:7443" domain = "usercontent.solsynth.dev" -secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" -accepts_usage = ["p.avatar", "p.banner", "i.attachment", "m.attachment"] +accepts_usage = [ + "p.avatar", + "p.banner", + "i.attachment", + "m.attachment", + "sticker", +] [workers] files_deletion = 4