[feature] Set default header and avatar for API accounts to GtS ones (#799)

* validate web-asset-base-dir

* move default icons into converter

* always ensure avatar + header on api accounts

* update tests

* add default header

* don't return error from web module creation anymore

* tidy a bit

* use pngs for default avatars rather than svgs
This commit is contained in:
tobi 2022-09-04 14:41:42 +02:00 committed by GitHub
commit 006c8b604b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 205 additions and 1039 deletions

View file

@ -70,7 +70,7 @@ func (suite *GetTestSuite) TestGet() {
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
}
func TestGetTestSuite(t *testing.T) {

View file

@ -63,7 +63,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Example Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"someone@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Example Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"someone@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
}
func (suite *InstancePatchTestSuite) TestInstancePatch2() {
@ -93,7 +93,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Geoff's Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Geoff's Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
}
func (suite *InstancePatchTestSuite) TestInstancePatch3() {
@ -123,7 +123,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
}
func (suite *InstancePatchTestSuite) TestInstancePatch4() {
@ -214,7 +214,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
}
func (suite *InstancePatchTestSuite) TestInstancePatch7() {

View file

@ -62,6 +62,11 @@ func Validate() error {
errs = append(errs, fmt.Errorf("%s must be set to either http or https, provided value was %s", ProtocolFlag(), proto))
}
webAssetsBaseDir := GetWebAssetBaseDir()
if webAssetsBaseDir == "" {
errs = append(errs, fmt.Errorf("%s must be set", WebAssetBaseDirFlag()))
}
if len(errs) > 0 {
errStrings := []string{}
for _, err := range errs {

View file

@ -103,6 +103,15 @@ func (suite *ConfigValidateTestSuite) TestValidateConfigNoProtocol() {
suite.EqualError(err, "protocol must be set")
}
func (suite *ConfigValidateTestSuite) TestValidateConfigNoWebAssetBaseDir() {
testrig.InitTestConfig()
config.SetWebAssetBaseDir("")
err := config.Validate()
suite.EqualError(err, "web-asset-base-dir must be set")
}
func (suite *ConfigValidateTestSuite) TestValidateConfigNoProtocolOrHost() {
testrig.InitTestConfig()

View file

@ -52,7 +52,7 @@ func (suite *NotificationTestSuite) TestStreamNotification() {
suite.NoError(err)
msg := <-openStream.Messages
suite.Equal(`{"id":"01FH57SJCMDWQGEAJ0X08CE3WV","type":"follow","created_at":"2021-10-04T08:52:36.000Z","account":{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2021-09-20T10:40:37.000Z","emojis":[],"fields":[]}}`, msg.Payload)
suite.Equal(`{"id":"01FH57SJCMDWQGEAJ0X08CE3WV","type":"follow","created_at":"2021-10-04T08:52:36.000Z","account":{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2021-09-20T10:40:37.000Z","emojis":[],"fields":[]}}`, msg.Payload)
}
func TestNotificationTestSuite(t *testing.T) {

View file

@ -21,6 +21,7 @@ package typeutils
import (
"context"
"net/url"
"sync"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
@ -195,10 +196,15 @@ type TypeConverter interface {
}
type converter struct {
db db.DB
db db.DB
defaultAvatars []string
randAvatars sync.Map
}
// NewConverter returns a new Converter
func NewConverter(db db.DB) TypeConverter {
return &converter{db: db}
return &converter{
db: db,
defaultAvatars: populateDefaultAvatars(),
}
}

View file

@ -0,0 +1,138 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
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 <http://www.gnu.org/licenses/>.
*/
package typeutils
import (
"io/ioutil"
"math/rand"
"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.png"
// 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("populateDefaultAvatars: error getting abs path for web assets: %s", err)
}
defaultAvatarsAbsFilePath := filepath.Join(webAssetsAbsFilePath, "default_avatars")
defaultAvatarFiles, err := ioutil.ReadDir(defaultAvatarsAbsFilePath)
if err != nil {
log.Warnf("populateDefaultAvatars: 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 f.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":
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
}
// EnsureAvatar 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
}

View file

@ -190,6 +190,9 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
Suspended: suspended,
}
c.ensureAvatar(accountFrontend)
c.ensureHeader(accountFrontend)
return accountFrontend, nil
}

View file

@ -63,7 +63,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
b, err := json.Marshal(apiStatus)
suite.NoError(err)
suite.Equal(`{"id":"01F8MH75CBF9JFX4ZAD54N0W0R","created_at":"2021-10-20T11:36:45.000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","url":"http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","replies_count":0,"reblogs_count":0,"favourites_count":1,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"hello world! #welcome ! first post on the instance :rainbow: !","reblog":null,"application":{"name":"superseriousbusiness","website":"https://superserious.business"},"account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"media_attachments":[{"id":"01F8MH6NEM8D7527KZAECTCR76","type":"image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","text_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","preview_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg","remote_url":null,"preview_remote_url":null,"meta":{"original":{"width":1200,"height":630,"size":"1200x630","aspect":1.9047619},"small":{"width":256,"height":134,"size":"256x134","aspect":1.9104477},"focus":{"x":0,"y":0}},"description":"Black and white image of some 50's style text saying: Welcome On Board","blurhash":"LNJRdVM{00Rj%Mayt7j[4nWBofRj"}],"mentions":[],"tags":[{"name":"welcome","url":"http://localhost:8080/tags/welcome"}],"emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true}],"card":null,"poll":null}`, string(b))
suite.Equal(`{"id":"01F8MH75CBF9JFX4ZAD54N0W0R","created_at":"2021-10-20T11:36:45.000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","url":"http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","replies_count":0,"reblogs_count":0,"favourites_count":1,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"hello world! #welcome ! first post on the instance :rainbow: !","reblog":null,"application":{"name":"superseriousbusiness","website":"https://superserious.business"},"account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"media_attachments":[{"id":"01F8MH6NEM8D7527KZAECTCR76","type":"image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","text_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","preview_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg","remote_url":null,"preview_remote_url":null,"meta":{"original":{"width":1200,"height":630,"size":"1200x630","aspect":1.9047619},"small":{"width":256,"height":134,"size":"256x134","aspect":1.9104477},"focus":{"x":0,"y":0}},"description":"Black and white image of some 50's style text saying: Welcome On Board","blurhash":"LNJRdVM{00Rj%Mayt7j[4nWBofRj"}],"mentions":[],"tags":[{"name":"welcome","url":"http://localhost:8080/tags/welcome"}],"emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true}],"card":null,"poll":null}`, string(b))
}
func (suite *InternalToFrontendTestSuite) TestInstanceToFrontend() {
@ -108,7 +108,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceToFrontendWithAdminAccount
b, err := json.Marshal(apiInstance)
suite.NoError(err)
suite.Equal(`{"uri":"https://example.org","title":"example instance","description":"a much longer description","short_description":"a little description","email":"someone@example.org","version":"software-from-hell 0.666","registrations":false,"approval_required":false,"invites_enabled":false,"thumbnail":"","contact_account":{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]},"max_toot_chars":0}`, string(b))
suite.Equal(`{"uri":"https://example.org","title":"example instance","description":"a much longer description","short_description":"a little description","email":"someone@example.org","version":"software-from-hell 0.666","registrations":false,"approval_required":false,"invites_enabled":false,"thumbnail":"","contact_account":{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]},"max_toot_chars":0}`, string(b))
}
func TestInternalToFrontendTestSuite(t *testing.T) {

View file

@ -20,9 +20,12 @@ package web
import (
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
type fileSystem struct {
@ -49,7 +52,12 @@ func (fs fileSystem) Open(path string) (http.File, error) {
}
func (m *Module) mountAssetsFilesystem(group *gin.RouterGroup) {
fs := fileSystem{http.Dir(m.webAssetsAbsFilePath)}
webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
if err != nil {
log.Panicf("mountAssetsFilesystem: error getting absolute path of assets dir: %s", err)
}
fs := fileSystem{http.Dir(webAssetsAbsFilePath)}
// use the cache middleware on all handlers in this group
group.Use(m.cacheControlMiddleware(fs))

View file

@ -114,7 +114,7 @@ func (m *Module) cacheControlMiddleware(fs http.FileSystem) gin.HandlerFunc {
if !strings.HasPrefix(upath, "/") {
upath = "/" + upath
}
assetFilePath := strings.TrimPrefix(path.Clean(upath), assetsPath)
assetFilePath := strings.TrimPrefix(path.Clean(upath), assetsPathPrefix)
// either fetch etag from ttlcache or generate it
eTag, err := m.getAssetETag(assetFilePath, fs)

View file

@ -38,15 +38,15 @@ func (m *Module) UserPanelHandler(c *gin.Context) {
c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
"instance": instance,
"stylesheets": []string{
assetsPath + "/Fork-Awesome/css/fork-awesome.min.css",
assetsPath + "/dist/_colors.css",
assetsPath + "/dist/base.css",
assetsPath + "/dist/panels-base.css",
assetsPath + "/dist/panels-user-style.css",
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
assetsPathPrefix + "/dist/_colors.css",
assetsPathPrefix + "/dist/base.css",
assetsPathPrefix + "/dist/panels-base.css",
assetsPathPrefix + "/dist/panels-user-style.css",
},
"javascript": []string{
assetsPath + "/dist/bundle.js",
assetsPath + "/dist/user-panel.js",
assetsPathPrefix + "/dist/bundle.js",
assetsPathPrefix + "/dist/user-panel.js",
},
})
}
@ -63,15 +63,15 @@ func (m *Module) AdminPanelHandler(c *gin.Context) {
c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
"instance": instance,
"stylesheets": []string{
assetsPath + "/Fork-Awesome/css/fork-awesome.min.css",
assetsPath + "/dist/_colors.css",
assetsPath + "/dist/base.css",
assetsPath + "/dist/panels-base.css",
assetsPath + "/dist/panels-admin-style.css",
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
assetsPathPrefix + "/dist/_colors.css",
assetsPathPrefix + "/dist/base.css",
assetsPathPrefix + "/dist/panels-base.css",
assetsPathPrefix + "/dist/panels-admin-style.css",
},
"javascript": []string{
assetsPath + "/dist/bundle.js",
assetsPath + "/dist/admin-panel.js",
assetsPathPrefix + "/dist/bundle.js",
assetsPathPrefix + "/dist/admin-panel.js",
},
})
}

View file

@ -23,7 +23,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strings"
@ -100,21 +99,6 @@ func (m *Module) profileGETHandler(c *gin.Context) {
return
}
// pick a random dummy avatar if this account avatar isn't set yet
if account.Avatar == "" && len(m.defaultAvatars) > 0 {
//nolint:gosec
randomIndex := rand.Intn(len(m.defaultAvatars))
dummyAvatar := m.defaultAvatars[randomIndex]
account.Avatar = dummyAvatar
for _, i := range statusResp.Items {
s, ok := i.(*apimodel.Status)
if !ok {
panic("timelineable was not *apimodel.Status")
}
s.Account.Avatar = dummyAvatar
}
}
c.HTML(http.StatusOK, "profile.tmpl", gin.H{
"instance": instance,
"account": account,

View file

@ -23,7 +23,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strings"
@ -36,21 +35,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
var randAvatars = make(map[string]string)
func (m *Module) ensureAvatar(status apimodel.Status) {
if status.Account.Avatar == "" && len(m.defaultAvatars) > 0 {
avatar, ok := randAvatars[status.Account.ID]
if !ok {
//nolint:gosec
randomIndex := rand.Intn(len(m.defaultAvatars))
avatar = m.defaultAvatars[randomIndex]
randAvatars[status.Account.ID] = avatar
}
status.Account.Avatar = avatar
}
}
func (m *Module) threadGETHandler(c *gin.Context) {
ctx := c.Request.Context()
@ -120,16 +104,6 @@ func (m *Module) threadGETHandler(c *gin.Context) {
return
}
m.ensureAvatar(*status)
for _, status := range context.Descendants {
m.ensureAvatar(status)
}
for _, status := range context.Ancestors {
m.ensureAvatar(status)
}
c.HTML(http.StatusOK, "thread.tmpl", gin.H{
"instance": instance,
"status": status,

View file

@ -20,17 +20,12 @@ package web
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"strings"
"time"
"codeberg.org/gruf/go-cache/v2"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
@ -43,7 +38,7 @@ const (
statusPath = profilePath + "/statuses/:" + statusIDKey
adminPanelPath = "/admin"
userPanelpath = "/user"
assetsPath = "/assets"
assetsPathPrefix = "/assets"
tokenParam = "token"
usernameKey = "username"
@ -52,78 +47,26 @@ const (
// Module implements the api.ClientModule interface for web pages.
type Module struct {
processor processing.Processor
webAssetsAbsFilePath string
assetsETagCache cache.Cache[string, eTagCacheEntry]
defaultAvatars []string
processor processing.Processor
assetsETagCache cache.Cache[string, eTagCacheEntry]
}
// New returns a new api.ClientModule for web pages.
func New(processor processing.Processor) (api.ClientModule, error) {
webAssetsBaseDir := config.GetWebAssetBaseDir()
if webAssetsBaseDir == "" {
return nil, fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebAssetBaseDirFlag())
}
webAssetsAbsFilePath, err := filepath.Abs(webAssetsBaseDir)
if err != nil {
return nil, fmt.Errorf("error getting absolute path of %s: %s", webAssetsBaseDir, err)
}
defaultAvatarsAbsFilePath := filepath.Join(webAssetsAbsFilePath, "default_avatars")
defaultAvatarFiles, err := ioutil.ReadDir(defaultAvatarsAbsFilePath)
if err != nil {
return nil, fmt.Errorf("error reading default avatars at %s: %s", defaultAvatarsAbsFilePath, err)
}
defaultAvatars := []string{}
for _, f := range defaultAvatarFiles {
// ignore directories
if f.IsDir() {
continue
}
// ignore files bigger than 50kb
if f.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 "svg", "jpeg", "jpg", "gif", "png":
avatar := fmt.Sprintf("%s/default_avatars/%s", assetsPath, f.Name())
defaultAvatars = append(defaultAvatars, avatar)
default:
continue
}
}
func New(processor processing.Processor) api.ClientModule {
assetsETagCache := cache.New[string, eTagCacheEntry]()
assetsETagCache.SetTTL(time.Hour, false)
assetsETagCache.Start(time.Minute)
return &Module{
processor: processor,
webAssetsAbsFilePath: webAssetsAbsFilePath,
assetsETagCache: assetsETagCache,
defaultAvatars: defaultAvatars,
}, nil
processor: processor,
assetsETagCache: assetsETagCache,
}
}
// Route satisfies the RESTAPIModule interface
func (m *Module) Route(s router.Router) error {
// serve static files from assets dir at /assets
assetsGroup := s.AttachGroup(assetsPath)
assetsGroup := s.AttachGroup(assetsPathPrefix)
m.mountAssetsFilesystem(assetsGroup)
s.AttachHandler(http.MethodGet, adminPanelPath, m.AdminPanelHandler)