// GoToSocial // Copyright (C) GoToSocial Authors admin@gotosocial.org // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package typeutils import ( "math/rand" "os" "path/filepath" "strings" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/log" ) const defaultHeaderPath = "/assets/default_header.webp" // populateDefaultAvatars returns a slice of standard avatars found // in the path [web-assets-base-dir]/default_avatars. The slice // entries correspond to the relative url via which they can be // retrieved from the server. // // So for example, an avatar called default.jpeg would be returned // in the slice as "/assets/default_avatars/default.jpeg". func populateDefaultAvatars() (defaultAvatars []string) { webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir()) if err != nil { log.Panicf(nil, "error getting abs path for web assets: %s", err) } defaultAvatarsAbsFilePath := filepath.Join(webAssetsAbsFilePath, "default_avatars") defaultAvatarFiles, err := os.ReadDir(defaultAvatarsAbsFilePath) if err != nil { log.Warnf(nil, "error reading default avatars at %s: %s", defaultAvatarsAbsFilePath, err) return } for _, f := range defaultAvatarFiles { // ignore directories if f.IsDir() { continue } // ignore files bigger than 50kb if i, err := f.Info(); err != nil || i.Size() > 50000 { continue } // get the name of the file, eg avatar.jpeg fileName := f.Name() // get just the .jpeg, for example, from avatar.jpeg extensionWithDot := filepath.Ext(fileName) // remove the leading . to just get, eg, jpeg extension := strings.TrimPrefix(extensionWithDot, ".") // take only files with simple extensions // that we know will work OK as avatars switch strings.ToLower(extension) { case "jpeg", "jpg", "gif", "png", "webp": avatarURL := config.GetProtocol() + "://" + config.GetHost() + "/assets/default_avatars/" + fileName defaultAvatars = append(defaultAvatars, avatarURL) default: continue } } return } // ensureAvatar ensures that the given account has a value set // for the avatar URL. // // If no value is set, an avatar will be selected at random from // the available default avatars. This selection is 'sticky', so // the same account will get the same result on subsequent calls. // // If a value for the avatar URL is already set, this function is // a no-op. // // If there are no default avatars available, this function is a // no-op. func (c *Converter) ensureAvatar(account *apimodel.Account) { if (account.Avatar != "" && account.AvatarStatic != "") || len(c.defaultAvatars) == 0 { return } var avatar string if avatarI, ok := c.randAvatars.Load(account.ID); ok { // we already have a default avatar stored for this account avatar, ok = avatarI.(string) if !ok { panic("avatarI was not a string") } } else { // select + store a default avatar for this account at random randomIndex := rand.Intn(len(c.defaultAvatars)) //nolint:gosec avatar = c.defaultAvatars[randomIndex] c.randAvatars.Store(account.ID, avatar) } account.Avatar = avatar account.AvatarStatic = avatar const defaultAviDesc = "Grayed-out line drawing of a cute sloth (default avatar)." account.AvatarDescription = defaultAviDesc } // ensureHeader ensures that the given account has a value set // for the header URL. // // If no value is set, the default header will be set. // // If a value for the header URL is already set, this function is // a no-op. func (c *Converter) ensureHeader(account *apimodel.Account) { if account.Header != "" && account.HeaderStatic != "" { return } h := config.GetProtocol() + "://" + config.GetHost() + defaultHeaderPath account.Header = h account.HeaderStatic = h const defaultHeaderDesc = "Flat gray background (default header)." account.HeaderDescription = defaultHeaderDesc }