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 }