136 lines
3.4 KiB
Go
136 lines
3.4 KiB
Go
package logic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image"
|
|
_ "image/gif" // member avatars
|
|
"image/jpeg"
|
|
_ "image/png" // member avatars
|
|
"strconv"
|
|
"strings"
|
|
|
|
"spark-server/internal/module/group/entity"
|
|
uploadentity "spark-server/internal/module/upload/entity"
|
|
userentity "spark-server/internal/module/user/entity"
|
|
"spark-server/internal/pkg/reqctx"
|
|
)
|
|
|
|
const collageJPEG = "image/jpeg"
|
|
|
|
func (s *Service) CanSetAvatar(ctx context.Context, userID, groupID int64) error {
|
|
if _, err := s.loadActive(ctx, groupID); err != nil {
|
|
return err
|
|
}
|
|
_, err := s.requireManage(ctx, groupID, userID)
|
|
return err
|
|
}
|
|
|
|
func validGroupAvatar(groupID int64, key string) bool {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return true
|
|
}
|
|
if groupID <= 0 || !uploadentity.ValidObjectKey(key) {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(key, uploadentity.PrefixGroup+strconv.FormatInt(groupID, 10)+"/")
|
|
}
|
|
|
|
func (s *Service) stampAvatar(ctx context.Context, g *entity.Group, ownerID int64, invitees []int64) {
|
|
if s.objects == nil || g == nil || g.ID <= 0 {
|
|
return
|
|
}
|
|
raw, err := encodeCollage(s.memberFaces(ctx, ownerID, invitees))
|
|
if err == nil && s.putAvatar(ctx, g, raw, collageJPEG, "jpg") {
|
|
return
|
|
}
|
|
s.stampFallbackAvatar(ctx, g)
|
|
}
|
|
|
|
func (s *Service) stampFallbackAvatar(ctx context.Context, g *entity.Group) {
|
|
raw, err := userentity.LetterAvatarPNG(g.Name)
|
|
if err == nil && s.putAvatar(ctx, g, raw, "image/png", "png") {
|
|
return
|
|
}
|
|
raw, err = encodeCollage([]image.Image{entity.PlaceholderFace()})
|
|
if err != nil {
|
|
return
|
|
}
|
|
s.putAvatar(ctx, g, raw, collageJPEG, "jpg")
|
|
}
|
|
|
|
func (s *Service) putAvatar(ctx context.Context, g *entity.Group, raw []byte, mime, ext string) bool {
|
|
if g == nil || len(raw) == 0 {
|
|
return false
|
|
}
|
|
key := uploadentity.BuildObjectKey(uploadentity.PurposeGroupAvatar, g.ID, reqctx.NewObjectName(), "", ext)
|
|
if err := s.objects.Put(ctx, key, raw, mime); err != nil {
|
|
return false
|
|
}
|
|
g.Avatar = key
|
|
if err := s.repo.UpdateProfile(ctx, g); err != nil {
|
|
g.Avatar = ""
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Service) memberFaces(ctx context.Context, ownerID int64, invitees []int64) []image.Image {
|
|
uids := append([]int64{ownerID}, invitees...)
|
|
if len(uids) > entity.MaxCollageFaces {
|
|
uids = uids[:entity.MaxCollageFaces]
|
|
}
|
|
out := make([]image.Image, 0, len(uids))
|
|
for _, uid := range uids {
|
|
out = append(out, s.faceOf(ctx, uid))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Service) faceOf(ctx context.Context, uid int64) image.Image {
|
|
u, err := s.users.Get(ctx, uid)
|
|
if err == nil && u != nil {
|
|
if img, ok := decodeAvatar(s.avatarBytes(ctx, u.Avatar)); ok {
|
|
return img
|
|
}
|
|
if raw, err := userentity.LetterAvatarPNG(u.Nickname); err == nil {
|
|
if img, ok := decodeAvatar(raw); ok {
|
|
return img
|
|
}
|
|
}
|
|
}
|
|
return entity.PlaceholderFace()
|
|
}
|
|
|
|
func (s *Service) avatarBytes(ctx context.Context, key string) []byte {
|
|
key = strings.TrimSpace(key)
|
|
if !uploadentity.ValidObjectKey(key) {
|
|
return nil
|
|
}
|
|
raw, err := s.objects.Get(ctx, key)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func decodeAvatar(raw []byte) (image.Image, bool) {
|
|
if len(raw) == 0 {
|
|
return nil, false
|
|
}
|
|
img, _, err := image.Decode(bytes.NewReader(raw))
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return img, true
|
|
}
|
|
|
|
func encodeCollage(faces []image.Image) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, entity.Collage(faces), &jpeg.Options{Quality: 85}); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|