mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-27 06:40:32 +00:00
[feature] Custom emoji updates (serve emoji via s2s api, tune db models) (#805)
* migrate emojis * add get emoji to s2s (federation) API * add new emoji db + cache functions * add shortcodeDomain lookup for emojis * check existing emojis w/cache, not w/constraints * go fmt * add putEmoji func * use new db emoji funcs instead of where * remove emojistringstotags func * add unique constraint back in * fix up broken migration * update index
This commit is contained in:
parent
ee01e030d4
commit
a872ddebe6
21 changed files with 773 additions and 62 deletions
|
@ -29,8 +29,6 @@ import (
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,8 +77,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreate() {
|
||||||
suite.True(apiEmoji.VisibleInPicker)
|
suite.True(apiEmoji.VisibleInPicker)
|
||||||
|
|
||||||
// emoji should be in the db
|
// emoji should be in the db
|
||||||
dbEmoji := >smodel.Emoji{}
|
dbEmoji, err := suite.db.GetEmojiByShortcodeDomain(context.Background(), apiEmoji.Shortcode, "")
|
||||||
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "shortcode", Value: "new_emoji"}}, dbEmoji)
|
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// check fields on the emoji
|
// check fields on the emoji
|
||||||
|
|
53
internal/api/s2s/emoji/emoji.go
Normal file
53
internal/api/s2s/emoji/emoji.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
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 emoji
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EmojiIDKey is for emoji IDs
|
||||||
|
EmojiIDKey = "id"
|
||||||
|
// EmojiBasePath is the base path for serving information about Emojis eg https://example.org/emoji
|
||||||
|
EmojiWithIDPath = "/" + uris.EmojiPath + "/:" + EmojiIDKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module implements the FederationModule interface
|
||||||
|
type Module struct {
|
||||||
|
processor processing.Processor
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a emoji module
|
||||||
|
func New(processor processing.Processor) api.FederationModule {
|
||||||
|
return &Module{
|
||||||
|
processor: processor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route satisfies the RESTAPIModule interface
|
||||||
|
func (m *Module) Route(s router.Router) error {
|
||||||
|
s.AttachHandler(http.MethodGet, EmojiWithIDPath, m.EmojiGetHandler)
|
||||||
|
return nil
|
||||||
|
}
|
74
internal/api/s2s/emoji/emojiget.go
Normal file
74
internal/api/s2s/emoji/emojiget.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
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 emoji
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmojiGetHandler
|
||||||
|
func (m *Module) EmojiGetHandler(c *gin.Context) {
|
||||||
|
// usernames on our instance are always lowercase
|
||||||
|
requestedEmojiID := strings.ToUpper(c.Param(EmojiIDKey))
|
||||||
|
if requestedEmojiID == "" {
|
||||||
|
err := errors.New("no emoji id specified in request")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier))
|
||||||
|
if signed {
|
||||||
|
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature))
|
||||||
|
if signed {
|
||||||
|
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, errWithCode := m.processor.GetFediEmoji(ctx, requestedEmojiID, c.Request.URL)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, format, b)
|
||||||
|
}
|
136
internal/api/s2s/emoji/emojiget_test.go
Normal file
136
internal/api/s2s/emoji/emojiget_test.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
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 emoji_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/emoji"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/security"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmojiGetTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
db db.DB
|
||||||
|
tc typeutils.TypeConverter
|
||||||
|
mediaManager media.Manager
|
||||||
|
federator federation.Federator
|
||||||
|
emailSender email.Sender
|
||||||
|
processor processing.Processor
|
||||||
|
storage storage.Driver
|
||||||
|
oauthServer oauth.Server
|
||||||
|
securityModule *security.Module
|
||||||
|
|
||||||
|
testEmojis map[string]*gtsmodel.Emoji
|
||||||
|
testAccounts map[string]*gtsmodel.Account
|
||||||
|
|
||||||
|
emojiModule *emoji.Module
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmojiGetTestSuite) SetupSuite() {
|
||||||
|
suite.testAccounts = testrig.NewTestAccounts()
|
||||||
|
suite.testEmojis = testrig.NewTestEmojis()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmojiGetTestSuite) SetupTest() {
|
||||||
|
testrig.InitTestConfig()
|
||||||
|
testrig.InitTestLog()
|
||||||
|
|
||||||
|
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
|
||||||
|
suite.db = testrig.NewTestDB()
|
||||||
|
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||||
|
suite.storage = testrig.NewInMemoryStorage()
|
||||||
|
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
|
||||||
|
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
|
||||||
|
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
|
||||||
|
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||||
|
suite.emojiModule = emoji.New(suite.processor).(*emoji.Module)
|
||||||
|
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||||
|
suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module)
|
||||||
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
|
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmojiGetTestSuite) TearDownTest() {
|
||||||
|
testrig.StandardDBTeardown(suite.db)
|
||||||
|
testrig.StandardStorageTeardown(suite.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmojiGetTestSuite) TestGetEmoji() {
|
||||||
|
// the dereference we're gonna use
|
||||||
|
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||||
|
signedRequest := derefRequests["foss_satan_dereference_emoji"]
|
||||||
|
targetEmoji := suite.testEmojis["rainbow"]
|
||||||
|
|
||||||
|
// setup request
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodGet, targetEmoji.URI, nil) // the endpoint we're hitting
|
||||||
|
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||||
|
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||||
|
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||||
|
|
||||||
|
// we need to pass the context through signature check first to set appropriate values on it
|
||||||
|
suite.securityModule.SignatureCheck(ctx)
|
||||||
|
|
||||||
|
// normally the router would populate these params from the path values,
|
||||||
|
// but because we're calling the function directly, we need to set them manually.
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: emoji.EmojiIDKey,
|
||||||
|
Value: targetEmoji.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger the function being tested
|
||||||
|
suite.emojiModule.EmojiGetHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Contains(string(b), `"icon":{"mediaType":"image/png","type":"Image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"},"id":"http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ","name":":rainbow:","type":"Emoji"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmojiGetTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(EmojiGetTestSuite))
|
||||||
|
}
|
116
internal/cache/emoji.go
vendored
Normal file
116
internal/cache/emoji.go
vendored
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
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 cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/gruf/go-cache/v2"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmojiCache is a cache wrapper to provide ID and URI lookups for gtsmodel.Emoji
|
||||||
|
type EmojiCache struct {
|
||||||
|
cache cache.LookupCache[string, string, *gtsmodel.Emoji]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEmojiCache returns a new instantiated EmojiCache object
|
||||||
|
func NewEmojiCache() *EmojiCache {
|
||||||
|
c := &EmojiCache{}
|
||||||
|
c.cache = cache.NewLookup(cache.LookupCfg[string, string, *gtsmodel.Emoji]{
|
||||||
|
RegisterLookups: func(lm *cache.LookupMap[string, string]) {
|
||||||
|
lm.RegisterLookup("uri")
|
||||||
|
lm.RegisterLookup("shortcodedomain")
|
||||||
|
},
|
||||||
|
|
||||||
|
AddLookups: func(lm *cache.LookupMap[string, string], emoji *gtsmodel.Emoji) {
|
||||||
|
if uri := emoji.URI; uri != "" {
|
||||||
|
lm.Set("uri", uri, emoji.URI)
|
||||||
|
lm.Set("shortcodedomain", shortcodeDomainKey(emoji.Shortcode, emoji.Domain), emoji.ID)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
DeleteLookups: func(lm *cache.LookupMap[string, string], emoji *gtsmodel.Emoji) {
|
||||||
|
if uri := emoji.URI; uri != "" {
|
||||||
|
lm.Delete("uri", uri)
|
||||||
|
lm.Delete("shortcodedomain", shortcodeDomainKey(emoji.Shortcode, emoji.Domain))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
c.cache.SetTTL(time.Minute*5, false)
|
||||||
|
c.cache.Start(time.Second * 10)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID attempts to fetch an emoji from the cache by its ID, you will receive a copy for thread-safety
|
||||||
|
func (c *EmojiCache) GetByID(id string) (*gtsmodel.Emoji, bool) {
|
||||||
|
return c.cache.Get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByURI attempts to fetch an emoji from the cache by its URI, you will receive a copy for thread-safety
|
||||||
|
func (c *EmojiCache) GetByURI(uri string) (*gtsmodel.Emoji, bool) {
|
||||||
|
return c.cache.GetBy("uri", uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EmojiCache) GetByShortcodeDomain(shortcode string, domain string) (*gtsmodel.Emoji, bool) {
|
||||||
|
return c.cache.GetBy("shortcodedomain", shortcodeDomainKey(shortcode, domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put places an emoji in the cache, ensuring that the object place is a copy for thread-safety
|
||||||
|
func (c *EmojiCache) Put(emoji *gtsmodel.Emoji) {
|
||||||
|
if emoji == nil || emoji.ID == "" {
|
||||||
|
panic("invalid emoji")
|
||||||
|
}
|
||||||
|
c.cache.Set(emoji.ID, copyEmoji(emoji))
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyEmoji performs a surface-level copy of emoji, only keeping attached IDs intact, not the objects.
|
||||||
|
// due to all the data being copied being 99% primitive types or strings (which are immutable and passed by ptr)
|
||||||
|
// this should be a relatively cheap process
|
||||||
|
func copyEmoji(emoji *gtsmodel.Emoji) *gtsmodel.Emoji {
|
||||||
|
return >smodel.Emoji{
|
||||||
|
ID: emoji.ID,
|
||||||
|
CreatedAt: emoji.CreatedAt,
|
||||||
|
UpdatedAt: emoji.UpdatedAt,
|
||||||
|
Shortcode: emoji.Shortcode,
|
||||||
|
Domain: emoji.Domain,
|
||||||
|
ImageRemoteURL: emoji.ImageRemoteURL,
|
||||||
|
ImageStaticRemoteURL: emoji.ImageStaticRemoteURL,
|
||||||
|
ImageURL: emoji.ImageURL,
|
||||||
|
ImageStaticURL: emoji.ImageStaticURL,
|
||||||
|
ImagePath: emoji.ImagePath,
|
||||||
|
ImageStaticPath: emoji.ImageStaticPath,
|
||||||
|
ImageContentType: emoji.ImageContentType,
|
||||||
|
ImageStaticContentType: emoji.ImageStaticContentType,
|
||||||
|
ImageFileSize: emoji.ImageFileSize,
|
||||||
|
ImageStaticFileSize: emoji.ImageStaticFileSize,
|
||||||
|
ImageUpdatedAt: emoji.ImageUpdatedAt,
|
||||||
|
Disabled: copyBoolPtr(emoji.Disabled),
|
||||||
|
URI: emoji.URI,
|
||||||
|
VisibleInPicker: copyBoolPtr(emoji.VisibleInPicker),
|
||||||
|
CategoryID: emoji.CategoryID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortcodeDomainKey(shortcode string, domain string) string {
|
||||||
|
if domain != "" {
|
||||||
|
return shortcode + "@" + domain
|
||||||
|
}
|
||||||
|
return shortcode
|
||||||
|
}
|
|
@ -154,6 +154,7 @@ func NewBunDBService(ctx context.Context) (db.DB, error) {
|
||||||
// Create DB structs that require ptrs to each other
|
// Create DB structs that require ptrs to each other
|
||||||
accounts := &accountDB{conn: conn, cache: cache.NewAccountCache()}
|
accounts := &accountDB{conn: conn, cache: cache.NewAccountCache()}
|
||||||
status := &statusDB{conn: conn, cache: cache.NewStatusCache()}
|
status := &statusDB{conn: conn, cache: cache.NewStatusCache()}
|
||||||
|
emoji := &emojiDB{conn: conn, cache: cache.NewEmojiCache()}
|
||||||
timeline := &timelineDB{conn: conn}
|
timeline := &timelineDB{conn: conn}
|
||||||
|
|
||||||
// Setup DB cross-referencing
|
// Setup DB cross-referencing
|
||||||
|
@ -188,9 +189,7 @@ func NewBunDBService(ctx context.Context) (db.DB, error) {
|
||||||
conn: conn,
|
conn: conn,
|
||||||
cache: blockCache,
|
cache: blockCache,
|
||||||
},
|
},
|
||||||
Emoji: &emojiDB{
|
Emoji: emoji,
|
||||||
conn: conn,
|
|
||||||
},
|
|
||||||
Instance: &instanceDB{
|
Instance: &instanceDB{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
},
|
},
|
||||||
|
@ -440,22 +439,3 @@ func (ps *bunDBService) TagStringsToTags(ctx context.Context, tags []string, ori
|
||||||
}
|
}
|
||||||
return newTags, nil
|
return newTags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *bunDBService) EmojiStringsToEmojis(ctx context.Context, emojis []string) ([]*gtsmodel.Emoji, error) {
|
|
||||||
newEmojis := []*gtsmodel.Emoji{}
|
|
||||||
for _, e := range emojis {
|
|
||||||
emoji := >smodel.Emoji{}
|
|
||||||
err := ps.conn.NewSelect().Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
// no result found for this username/domain so just don't include it as an emoji and carry on about our business
|
|
||||||
log.Debugf("no emoji found with shortcode %s, skipping it", e)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// a serious error has happened so bail
|
|
||||||
return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err)
|
|
||||||
}
|
|
||||||
newEmojis = append(newEmojis, emoji)
|
|
||||||
}
|
|
||||||
return newEmojis, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,27 +20,136 @@ package bundb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type emojiDB struct {
|
type emojiDB struct {
|
||||||
conn *DBConn
|
conn *DBConn
|
||||||
|
cache *cache.EmojiCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e emojiDB) GetCustomEmojis(ctx context.Context) ([]*gtsmodel.Emoji, db.Error) {
|
func (e *emojiDB) newEmojiQ(emoji *gtsmodel.Emoji) *bun.SelectQuery {
|
||||||
emojis := []*gtsmodel.Emoji{}
|
return e.conn.
|
||||||
|
NewSelect().
|
||||||
|
Model(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) db.Error {
|
||||||
|
if _, err := e.conn.NewInsert().Model(emoji).Exec(ctx); err != nil {
|
||||||
|
return e.conn.ProcessError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.cache.Put(emoji)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) GetCustomEmojis(ctx context.Context) ([]*gtsmodel.Emoji, db.Error) {
|
||||||
|
emojiIDs := []string{}
|
||||||
|
|
||||||
q := e.conn.
|
q := e.conn.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
Model(&emojis).
|
Table("emojis").
|
||||||
|
Column("id").
|
||||||
Where("visible_in_picker = true").
|
Where("visible_in_picker = true").
|
||||||
Where("disabled = false").
|
Where("disabled = false").
|
||||||
|
Where("domain IS NULL").
|
||||||
Order("shortcode ASC")
|
Order("shortcode ASC")
|
||||||
|
|
||||||
if err := q.Scan(ctx); err != nil {
|
if err := q.Scan(ctx, &emojiIDs); err != nil {
|
||||||
return nil, e.conn.ProcessError(err)
|
return nil, e.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return e.emojisFromIDs(ctx, emojiIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) GetEmojiByID(ctx context.Context, id string) (*gtsmodel.Emoji, db.Error) {
|
||||||
|
return e.getEmoji(
|
||||||
|
ctx,
|
||||||
|
func() (*gtsmodel.Emoji, bool) {
|
||||||
|
return e.cache.GetByID(id)
|
||||||
|
},
|
||||||
|
func(emoji *gtsmodel.Emoji) error {
|
||||||
|
return e.newEmojiQ(emoji).Where("emoji.id = ?", id).Scan(ctx)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) GetEmojiByURI(ctx context.Context, uri string) (*gtsmodel.Emoji, db.Error) {
|
||||||
|
return e.getEmoji(
|
||||||
|
ctx,
|
||||||
|
func() (*gtsmodel.Emoji, bool) {
|
||||||
|
return e.cache.GetByURI(uri)
|
||||||
|
},
|
||||||
|
func(emoji *gtsmodel.Emoji) error {
|
||||||
|
return e.newEmojiQ(emoji).Where("emoji.uri = ?", uri).Scan(ctx)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) GetEmojiByShortcodeDomain(ctx context.Context, shortcode string, domain string) (*gtsmodel.Emoji, db.Error) {
|
||||||
|
return e.getEmoji(
|
||||||
|
ctx,
|
||||||
|
func() (*gtsmodel.Emoji, bool) {
|
||||||
|
return e.cache.GetByShortcodeDomain(shortcode, domain)
|
||||||
|
},
|
||||||
|
func(emoji *gtsmodel.Emoji) error {
|
||||||
|
q := e.newEmojiQ(emoji)
|
||||||
|
|
||||||
|
if domain != "" {
|
||||||
|
q = q.Where("emoji.shortcode = ?", shortcode)
|
||||||
|
q = q.Where("emoji.domain = ?", domain)
|
||||||
|
} else {
|
||||||
|
q = q.Where("emoji.shortcode = ?", strings.ToLower(shortcode))
|
||||||
|
q = q.Where("emoji.domain IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Scan(ctx)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) getEmoji(ctx context.Context, cacheGet func() (*gtsmodel.Emoji, bool), dbQuery func(*gtsmodel.Emoji) error) (*gtsmodel.Emoji, db.Error) {
|
||||||
|
// Attempt to fetch cached emoji
|
||||||
|
emoji, cached := cacheGet()
|
||||||
|
|
||||||
|
if !cached {
|
||||||
|
emoji = >smodel.Emoji{}
|
||||||
|
|
||||||
|
// Not cached! Perform database query
|
||||||
|
err := dbQuery(emoji)
|
||||||
|
if err != nil {
|
||||||
|
return nil, e.conn.ProcessError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place in the cache
|
||||||
|
e.cache.Put(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoji, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emojiDB) emojisFromIDs(ctx context.Context, emojiIDs []string) ([]*gtsmodel.Emoji, db.Error) {
|
||||||
|
// Catch case of no emojis early
|
||||||
|
if len(emojiIDs) == 0 {
|
||||||
|
return nil, db.ErrNoEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
emojis := make([]*gtsmodel.Emoji, 0, len(emojiIDs))
|
||||||
|
|
||||||
|
for _, id := range emojiIDs {
|
||||||
|
emoji, err := e.GetEmojiByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("emojisFromIDs: error getting emoji %q: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emojis = append(emojis, emoji)
|
||||||
|
}
|
||||||
|
|
||||||
return emojis, nil
|
return emojis, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
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 migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20220905150505_custom_emoji_updates"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
up := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
// create the new emojis table
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateTable().
|
||||||
|
Model(>smodel.Emoji{}).
|
||||||
|
ModelTableExpr("new_emojis").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// move all old emojis to the new table
|
||||||
|
currentEmojis := []*gtsmodel.Emoji{}
|
||||||
|
if err := tx.
|
||||||
|
NewSelect().
|
||||||
|
Model(¤tEmojis).
|
||||||
|
Scan(ctx); err != nil && err != sql.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, currentEmoji := range currentEmojis {
|
||||||
|
if _, err := tx.
|
||||||
|
NewInsert().
|
||||||
|
Model(currentEmoji).
|
||||||
|
ModelTableExpr("new_emojis").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have all the data we need from the old table, so we can safely drop it now
|
||||||
|
if _, err := tx.NewDropTable().Model(>smodel.Emoji{}).Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename the new table to the same name as the old table was
|
||||||
|
if _, err := tx.ExecContext(ctx, "ALTER TABLE new_emojis RENAME TO emojis;"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add indexes to the new table
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateIndex().
|
||||||
|
Model(>smodel.Emoji{}).
|
||||||
|
Index("emojis_id_idx").
|
||||||
|
Column("id").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateIndex().
|
||||||
|
Model(>smodel.Emoji{}).
|
||||||
|
Index("emojis_uri_idx").
|
||||||
|
Column("uri").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateIndex().
|
||||||
|
Model(>smodel.Emoji{}).
|
||||||
|
Index("emojis_available_custom_idx").
|
||||||
|
Column("visible_in_picker", "disabled", "shortcode").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
down := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Migrations.Register(up, down); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
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 gtsmodel
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens.
|
||||||
|
type Emoji struct {
|
||||||
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
|
CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
|
UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||||
|
Shortcode string `validate:"required" bun:",nullzero,notnull,unique:shortcodedomain"` // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ eg., 'blob_hug' 'purple_heart' Must be unique with domain.
|
||||||
|
Domain string `validate:"omitempty,fqdn" bun:",nullzero,unique:shortcodedomain"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||||
|
ImageRemoteURL string `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved remotely? Null for local emojis.
|
||||||
|
ImageStaticRemoteURL string `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
||||||
|
ImageURL string `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
|
ImageStaticURL string `validate:"required_without=ImageStaticRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
|
ImagePath string `validate:"required,file" bun:",nullzero,notnull"` // Path of the emoji image in the server storage system.
|
||||||
|
ImageStaticPath string `validate:"required,file" bun:",nullzero,notnull"` // Path of a static version of the emoji image in the server storage system
|
||||||
|
ImageContentType string `validate:"required" bun:",nullzero,notnull"` // MIME content type of the emoji image
|
||||||
|
ImageStaticContentType string `validate:"required" bun:",nullzero,notnull"` // MIME content type of the static version of the emoji image.
|
||||||
|
ImageFileSize int `validate:"required,min=1" bun:",nullzero,notnull"` // Size of the emoji image file in bytes, for serving purposes.
|
||||||
|
ImageStaticFileSize int `validate:"required,min=1" bun:",nullzero,notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
|
||||||
|
ImageUpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated?
|
||||||
|
Disabled *bool `validate:"-" bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
|
||||||
|
URI string `validate:"url" bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
|
||||||
|
VisibleInPicker *bool `validate:"-" bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
||||||
|
CategoryID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // In which emoji category is this emoji visible?
|
||||||
|
}
|
|
@ -57,12 +57,4 @@ type DB interface {
|
||||||
// Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking
|
// Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking
|
||||||
// if they exist in the db already, and conveniently returning them, or creating new tag structs.
|
// if they exist in the db already, and conveniently returning them, or creating new tag structs.
|
||||||
TagStringsToTags(ctx context.Context, tags []string, originAccountID string) ([]*gtsmodel.Tag, error)
|
TagStringsToTags(ctx context.Context, tags []string, originAccountID string) ([]*gtsmodel.Tag, error)
|
||||||
|
|
||||||
// EmojiStringsToEmojis takes a slice of deduplicated, lowercase emojis in the form ":emojiname:", which have been
|
|
||||||
// used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then
|
|
||||||
// returns a slice of *model.Emoji corresponding to the given emojis.
|
|
||||||
//
|
|
||||||
// Note: this func doesn't/shouldn't do any manipulation of the emoji in the DB, it's just for checking
|
|
||||||
// if they exist in the db and conveniently returning them if they do.
|
|
||||||
EmojiStringsToEmojis(ctx context.Context, emojis []string) ([]*gtsmodel.Emoji, error)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,13 @@ import (
|
||||||
|
|
||||||
// Emoji contains functions for getting emoji in the database.
|
// Emoji contains functions for getting emoji in the database.
|
||||||
type Emoji interface {
|
type Emoji interface {
|
||||||
|
// PutEmoji puts one emoji in the database.
|
||||||
|
PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) Error
|
||||||
// GetCustomEmojis gets all custom emoji for the instance
|
// GetCustomEmojis gets all custom emoji for the instance
|
||||||
GetCustomEmojis(ctx context.Context) ([]*gtsmodel.Emoji, Error)
|
GetCustomEmojis(ctx context.Context) ([]*gtsmodel.Emoji, Error)
|
||||||
|
// GetEmojiByID gets a specific emoji by its database ID.
|
||||||
|
GetEmojiByID(ctx context.Context, id string) (*gtsmodel.Emoji, Error)
|
||||||
|
// GetEmojiByShortcodeDomain gets an emoji based on its shortcode and domain.
|
||||||
|
// For local emoji, domain should be an empty string.
|
||||||
|
GetEmojiByShortcodeDomain(ctx context.Context, shortcode string, domain string) (*gtsmodel.Emoji, Error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ type Emoji struct {
|
||||||
CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||||
Shortcode string `validate:"required" bun:",nullzero,notnull,unique:shortcodedomain"` // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ eg., 'blob_hug' 'purple_heart' Must be unique with domain.
|
Shortcode string `validate:"required" bun:",nullzero,notnull,unique:shortcodedomain"` // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ eg., 'blob_hug' 'purple_heart' Must be unique with domain.
|
||||||
Domain string `validate:"omitempty,fqdn" bun:",notnull,default:'',unique:shortcodedomain"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
Domain string `validate:"omitempty,fqdn" bun:",nullzero,unique:shortcodedomain"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||||
ImageRemoteURL string `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved remotely? Null for local emojis.
|
ImageRemoteURL string `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved remotely? Null for local emojis.
|
||||||
ImageStaticRemoteURL string `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
ImageStaticRemoteURL string `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
||||||
ImageURL string `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
ImageURL string `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error
|
||||||
|
|
||||||
// store the result in the database before returning it
|
// store the result in the database before returning it
|
||||||
if !p.insertedInDB {
|
if !p.insertedInDB {
|
||||||
if err := p.database.Put(ctx, p.emoji); err != nil {
|
if err := p.database.PutEmoji(ctx, p.emoji); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p.insertedInDB = true
|
p.insertedInDB = true
|
||||||
|
|
|
@ -20,7 +20,6 @@ package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
@ -37,9 +36,13 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account,
|
||||||
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
|
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := func(innerCtx context.Context) (io.Reader, int, error) {
|
maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
|
||||||
f, err := form.Image.Open()
|
if maybeExisting != nil {
|
||||||
return f, int(form.Image.Size), err
|
return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && err != db.ErrNoEntries {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking existence of emoji with shortcode %s: %s", form.Shortcode, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID, err := id.NewRandomULID()
|
emojiID, err := id.NewRandomULID()
|
||||||
|
@ -49,6 +52,11 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account,
|
||||||
|
|
||||||
emojiURI := uris.GenerateURIForEmoji(emojiID)
|
emojiURI := uris.GenerateURIForEmoji(emojiID)
|
||||||
|
|
||||||
|
data := func(innerCtx context.Context) (io.Reader, int, error) {
|
||||||
|
f, err := form.Image.Open()
|
||||||
|
return f, int(form.Image.Size), err
|
||||||
|
}
|
||||||
|
|
||||||
processingEmoji, err := p.mediaManager.ProcessEmoji(ctx, data, nil, form.Shortcode, emojiID, emojiURI, nil)
|
processingEmoji, err := p.mediaManager.ProcessEmoji(ctx, data, nil, form.Shortcode, emojiID, emojiURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji")
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji")
|
||||||
|
@ -56,10 +64,6 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account,
|
||||||
|
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
emoji, err := processingEmoji.LoadEmoji(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var alreadyExistsError *db.ErrAlreadyExists
|
|
||||||
if errors.As(err, &alreadyExistsError) {
|
|
||||||
return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode))
|
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji")
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,10 @@ func (p *processor) GetFediOutbox(ctx context.Context, requestedUsername string,
|
||||||
return p.federationProcessor.GetOutbox(ctx, requestedUsername, page, maxID, minID, requestURL)
|
return p.federationProcessor.GetOutbox(ctx, requestedUsername, page, maxID, minID, requestURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) GetFediEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
|
||||||
|
return p.federationProcessor.GetEmoji(ctx, requestedEmojiID, requestURL)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
|
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
|
||||||
return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername)
|
return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername)
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,9 @@ type Processor interface {
|
||||||
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
||||||
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
||||||
|
|
||||||
|
// GetFediEmoji handles the GET for a federated emoji originating from this instance.
|
||||||
|
GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||||
|
|
||||||
// GetNodeInfoRel returns a well known response giving the path to node info.
|
// GetNodeInfoRel returns a well known response giving the path to node info.
|
||||||
GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
||||||
|
|
||||||
|
|
59
internal/processing/federation/getemoji.go
Normal file
59
internal/processing/federation/getemoji.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
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 federation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
|
||||||
|
if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedEmoji, err := p.db.GetEmojiByID(ctx, requestedEmojiID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestedEmoji.Domain != "" {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s doesn't belong to this instance (domain %s)", requestedEmojiID, requestedEmoji.Domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
if *requestedEmoji.Disabled {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID))
|
||||||
|
}
|
||||||
|
|
||||||
|
apEmoji, err := p.tc.EmojiToAS(ctx, requestedEmoji)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := streams.Serialize(apEmoji)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -231,8 +231,8 @@ func (p *processor) getEmojiContent(ctx context.Context, wantedEmojiID string, e
|
||||||
emojiContent := &apimodel.Content{}
|
emojiContent := &apimodel.Content{}
|
||||||
var storagePath string
|
var storagePath string
|
||||||
|
|
||||||
e := >smodel.Emoji{}
|
e, err := p.db.GetEmojiByID(ctx, wantedEmojiID)
|
||||||
if err := p.db.GetByID(ctx, wantedEmojiID, e); err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedEmojiID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedEmojiID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,8 @@ type Processor interface {
|
||||||
GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||||
// GetFediOutbox returns the public outbox of the requested user, with the given parameters.
|
// GetFediOutbox returns the public outbox of the requested user, with the given parameters.
|
||||||
GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||||
|
// GetFediEmoji returns the AP representation of an emoji on this instance.
|
||||||
|
GetFediEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||||
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
||||||
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
||||||
// GetNodeInfoRel returns a well known response giving the path to node info.
|
// GetNodeInfoRel returns a well known response giving the path to node info.
|
||||||
|
|
|
@ -249,18 +249,27 @@ func (p *processor) ProcessTags(ctx context.Context, form *apimodel.AdvancedStat
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) ProcessEmojis(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
func (p *processor) ProcessEmojis(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||||
gtsEmojis, err := p.db.EmojiStringsToEmojis(ctx, util.DeriveEmojisFromText(form.Status))
|
// for each emoji shortcode in the text, check if it's an enabled
|
||||||
|
// emoji on this instance, and if so, add it to the status
|
||||||
|
emojiShortcodes := util.DeriveEmojisFromText(form.Status)
|
||||||
|
status.Emojis = make([]*gtsmodel.Emoji, 0, len(emojiShortcodes))
|
||||||
|
status.EmojiIDs = make([]string, 0, len(emojiShortcodes))
|
||||||
|
|
||||||
|
for _, shortcode := range emojiShortcodes {
|
||||||
|
emoji, err := p.db.GetEmojiByShortcodeDomain(ctx, shortcode, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error generating emojis from status: %s", err)
|
if err != db.ErrNoEntries {
|
||||||
|
log.Errorf("error getting local emoji with shortcode %s: %s", shortcode, err)
|
||||||
}
|
}
|
||||||
emojis := make([]string, 0, len(gtsEmojis))
|
continue
|
||||||
for _, e := range gtsEmojis {
|
|
||||||
emojis = append(emojis, e.ID)
|
|
||||||
}
|
}
|
||||||
// add full populated gts emojis to the status for passing them around conveniently
|
|
||||||
status.Emojis = gtsEmojis
|
if *emoji.VisibleInPicker && !*emoji.Disabled {
|
||||||
// add just the ids of the used emojis to the status for putting in the db
|
status.Emojis = append(status.Emojis, emoji)
|
||||||
status.EmojiIDs = emojis
|
status.EmojiIDs = append(status.EmojiIDs, emoji.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2056,6 +2056,7 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
|
||||||
var sig, digest, date string
|
var sig, digest, date string
|
||||||
var target *url.URL
|
var target *url.URL
|
||||||
statuses := NewTestStatuses()
|
statuses := NewTestStatuses()
|
||||||
|
emojis := NewTestEmojis()
|
||||||
|
|
||||||
target = URLMustParse(accounts["local_account_1"].URI)
|
target = URLMustParse(accounts["local_account_1"].URI)
|
||||||
sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
|
sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
|
||||||
|
@ -2137,6 +2138,14 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
|
||||||
DateHeader: date,
|
DateHeader: date,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target = URLMustParse(emojis["rainbow"].URI)
|
||||||
|
sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
|
||||||
|
fossSatanDereferenceEmoji := ActivityWithSignature{
|
||||||
|
SignatureHeader: sig,
|
||||||
|
DigestHeader: digest,
|
||||||
|
DateHeader: date,
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]ActivityWithSignature{
|
return map[string]ActivityWithSignature{
|
||||||
"foss_satan_dereference_zork": fossSatanDereferenceZork,
|
"foss_satan_dereference_zork": fossSatanDereferenceZork,
|
||||||
"foss_satan_dereference_zork_public_key": fossSatanDereferenceZorkPublicKey,
|
"foss_satan_dereference_zork_public_key": fossSatanDereferenceZorkPublicKey,
|
||||||
|
@ -2148,6 +2157,7 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
|
||||||
"foss_satan_dereference_zork_outbox": fossSatanDereferenceZorkOutbox,
|
"foss_satan_dereference_zork_outbox": fossSatanDereferenceZorkOutbox,
|
||||||
"foss_satan_dereference_zork_outbox_first": fossSatanDereferenceZorkOutboxFirst,
|
"foss_satan_dereference_zork_outbox_first": fossSatanDereferenceZorkOutboxFirst,
|
||||||
"foss_satan_dereference_zork_outbox_next": fossSatanDereferenceZorkOutboxNext,
|
"foss_satan_dereference_zork_outbox_next": fossSatanDereferenceZorkOutboxNext,
|
||||||
|
"foss_satan_dereference_emoji": fossSatanDereferenceEmoji,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue