mirror of
				https://github.com/superseriousbusiness/gotosocial
				synced 2025-06-05 21:59:39 +02:00 
			
		
		
		
	* start updating media manager interface ready for storing attachments / emoji right away
* store emoji and media as uncached immediately, then (re-)cache on Processing{}.Load()
* remove now unused media workers
* fix tests and issues
* fix another test!
* fix emoji activitypub uri setting behaviour, fix remainder of test compilation issues
* fix more tests
* fix (most of) remaining tests, add debouncing to repeatedly failing media / emojis
* whoops, rebase issue
* remove kim's whacky experiments
* do some reshuffling, ensure emoji uri gets set
* ensure marked as not cached on cleanup
* tweaks to media / emoji processing to handle context canceled better
* ensure newly fetched emojis actually get set in returned slice
* use different varnames to be a bit more obvious
* move emoji refresh rate limiting to dereferencer
* add exported dereferencer functions for remote media, use these for recaching in processor
* add check for nil attachment in updateAttachment()
* remove unused emoji and media fields + columns
* see previous commit
* fix old migrations expecting image_updated_at to exists (from copies of old models)
* remove freshness checking code (seems to be broken...)
* fix error arg causing nil ptr exception
* finish documentating functions with comments, slight tweaks to media / emoji deref error logic
* remove some extra unneeded boolean checking
* finish writing documentation (code comments) for exported media manager methods
* undo changes to migration snapshot gtsmodels, updated failing migration to have its own snapshot
* move doesColumnExist() to util.go in migrations package
		
	
		
			
				
	
	
		
			582 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			582 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// GoToSocial
 | 
						|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
						|
// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
						|
//
 | 
						|
// This program is free software: you can redistribute it and/or modify
 | 
						|
// it under the terms of the GNU Affero General Public License as published by
 | 
						|
// the Free Software Foundation, either version 3 of the License, or
 | 
						|
// (at your option) any later version.
 | 
						|
//
 | 
						|
// This program is distributed in the hope that it will be useful,
 | 
						|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
// GNU Affero General Public License for more details.
 | 
						|
//
 | 
						|
// You should have received a copy of the GNU Affero General Public License
 | 
						|
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
						|
 | 
						|
package admin
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"mime/multipart"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/id"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
						|
)
 | 
						|
 | 
						|
// EmojiCreate creates a custom emoji on this instance.
 | 
						|
func (p *Processor) EmojiCreate(
 | 
						|
	ctx context.Context,
 | 
						|
	account *gtsmodel.Account,
 | 
						|
	form *apimodel.EmojiCreateRequest,
 | 
						|
) (*apimodel.Emoji, gtserror.WithCode) {
 | 
						|
 | 
						|
	// Simply read provided form data for emoji data source.
 | 
						|
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						|
		f, err := form.Image.Open()
 | 
						|
		return f, form.Image.Size, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Attempt to create the new local emoji.
 | 
						|
	emoji, errWithCode := p.createEmoji(ctx,
 | 
						|
		form.Shortcode,
 | 
						|
		form.CategoryName,
 | 
						|
		data,
 | 
						|
	)
 | 
						|
	if errWithCode != nil {
 | 
						|
		return nil, errWithCode
 | 
						|
	}
 | 
						|
 | 
						|
	apiEmoji, err := p.converter.EmojiToAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return &apiEmoji, nil
 | 
						|
}
 | 
						|
 | 
						|
// EmojisGet returns an admin view of custom
 | 
						|
// emojis, filtered with the given parameters.
 | 
						|
func (p *Processor) EmojisGet(
 | 
						|
	ctx context.Context,
 | 
						|
	account *gtsmodel.Account,
 | 
						|
	domain string,
 | 
						|
	includeDisabled bool,
 | 
						|
	includeEnabled bool,
 | 
						|
	shortcode string,
 | 
						|
	maxShortcodeDomain string,
 | 
						|
	minShortcodeDomain string,
 | 
						|
	limit int,
 | 
						|
) (*apimodel.PageableResponse, gtserror.WithCode) {
 | 
						|
	emojis, err := p.state.DB.GetEmojisBy(ctx,
 | 
						|
		domain,
 | 
						|
		includeDisabled,
 | 
						|
		includeEnabled,
 | 
						|
		shortcode,
 | 
						|
		maxShortcodeDomain,
 | 
						|
		minShortcodeDomain,
 | 
						|
		limit,
 | 
						|
	)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("db error: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	count := len(emojis)
 | 
						|
	if count == 0 {
 | 
						|
		return util.EmptyPageableResponse(), nil
 | 
						|
	}
 | 
						|
 | 
						|
	items := make([]interface{}, 0, count)
 | 
						|
	for _, emoji := range emojis {
 | 
						|
		adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
		if err != nil {
 | 
						|
			err := gtserror.Newf("error converting emoji to admin model emoji: %w", err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
		items = append(items, adminEmoji)
 | 
						|
	}
 | 
						|
 | 
						|
	return util.PackagePageableResponse(util.PageableResponseParams{
 | 
						|
		Items:          items,
 | 
						|
		Path:           "api/v1/admin/custom_emojis",
 | 
						|
		NextMaxIDKey:   "max_shortcode_domain",
 | 
						|
		NextMaxIDValue: util.ShortcodeDomain(emojis[count-1]),
 | 
						|
		PrevMinIDKey:   "min_shortcode_domain",
 | 
						|
		PrevMinIDValue: util.ShortcodeDomain(emojis[0]),
 | 
						|
		Limit:          limit,
 | 
						|
		ExtraQueryParams: []string{
 | 
						|
			emojisGetFilterParams(
 | 
						|
				shortcode,
 | 
						|
				domain,
 | 
						|
				includeDisabled,
 | 
						|
				includeEnabled,
 | 
						|
			),
 | 
						|
		},
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// EmojiGet returns the admin view of
 | 
						|
// one custom emoji with the given id.
 | 
						|
func (p *Processor) EmojiGet(
 | 
						|
	ctx context.Context,
 | 
						|
	account *gtsmodel.Account,
 | 
						|
	id string,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	emoji, err := p.state.DB.GetEmojiByID(ctx, id)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("db error: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if emoji == nil {
 | 
						|
		err := gtserror.Newf("no emoji with id %s found in the db", id)
 | 
						|
		return nil, gtserror.NewErrorNotFound(err)
 | 
						|
	}
 | 
						|
 | 
						|
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji to admin api emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return adminEmoji, nil
 | 
						|
}
 | 
						|
 | 
						|
// EmojiDelete deletes one *local* emoji
 | 
						|
// from the database, with the given id.
 | 
						|
func (p *Processor) EmojiDelete(
 | 
						|
	ctx context.Context,
 | 
						|
	id string,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	emoji, err := p.state.DB.GetEmojiByID(ctx, id)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("db error: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if emoji == nil {
 | 
						|
		err := gtserror.Newf("no emoji with id %s found in the db", id)
 | 
						|
		return nil, gtserror.NewErrorNotFound(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if !emoji.IsLocal() {
 | 
						|
		err := fmt.Errorf("emoji with id %s was not a local emoji, will not delete", id)
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Convert to admin emoji before deletion,
 | 
						|
	// so we can return the deleted emoji.
 | 
						|
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji to admin api emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err := p.state.DB.DeleteEmojiByID(ctx, id); err != nil {
 | 
						|
		err := gtserror.Newf("db error deleting emoji %s: %w", id, err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return adminEmoji, nil
 | 
						|
}
 | 
						|
 | 
						|
// EmojiUpdate updates one emoji with the
 | 
						|
// given id, using the provided form parameters.
 | 
						|
func (p *Processor) EmojiUpdate(
 | 
						|
	ctx context.Context,
 | 
						|
	emojiID string,
 | 
						|
	form *apimodel.EmojiUpdateRequest,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
 | 
						|
	// Get the emoji with given ID from the database.
 | 
						|
	emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("error fetching emoji from db: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Check found.
 | 
						|
	if emoji == nil {
 | 
						|
		const text = "emoji not found"
 | 
						|
		return nil, gtserror.NewErrorNotFound(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	switch form.Type {
 | 
						|
 | 
						|
	case apimodel.EmojiUpdateCopy:
 | 
						|
		return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
 | 
						|
 | 
						|
	case apimodel.EmojiUpdateDisable:
 | 
						|
		return p.emojiUpdateDisable(ctx, emoji)
 | 
						|
 | 
						|
	case apimodel.EmojiUpdateModify:
 | 
						|
		return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
 | 
						|
 | 
						|
	default:
 | 
						|
		const text = "unrecognized emoji update action type"
 | 
						|
		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// EmojiCategoriesGet returns all custom emoji
 | 
						|
// categories that exist on this instance.
 | 
						|
func (p *Processor) EmojiCategoriesGet(
 | 
						|
	ctx context.Context,
 | 
						|
) ([]*apimodel.EmojiCategory, gtserror.WithCode) {
 | 
						|
	categories, err := p.state.DB.GetEmojiCategories(ctx)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("db error getting emoji categories: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories))
 | 
						|
	for _, category := range categories {
 | 
						|
		apiCategory, err := p.converter.EmojiCategoryToAPIEmojiCategory(ctx, category)
 | 
						|
		if err != nil {
 | 
						|
			err := gtserror.Newf("error converting emoji category to api emoji category: %w", err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
		apiCategories = append(apiCategories, apiCategory)
 | 
						|
	}
 | 
						|
 | 
						|
	return apiCategories, nil
 | 
						|
}
 | 
						|
 | 
						|
// emojiUpdateCopy copies and stores the given
 | 
						|
// *remote* emoji as a *local* emoji, preserving
 | 
						|
// the same image, and using the provided shortcode.
 | 
						|
//
 | 
						|
// The provided emoji model must correspond to an
 | 
						|
// emoji already stored in the database + storage.
 | 
						|
func (p *Processor) emojiUpdateCopy(
 | 
						|
	ctx context.Context,
 | 
						|
	target *gtsmodel.Emoji,
 | 
						|
	shortcode *string,
 | 
						|
	categoryName *string,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	if target.IsLocal() {
 | 
						|
		const text = "target emoji is not remote; cannot copy to local"
 | 
						|
		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	// Ensure target emoji is locally cached.
 | 
						|
	target, err := p.federator.RefreshEmoji(
 | 
						|
		ctx,
 | 
						|
		target,
 | 
						|
 | 
						|
		// no changes we want to make.
 | 
						|
		media.AdditionalEmojiInfo{},
 | 
						|
		false,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error recaching emoji %s: %w", target.ImageRemoteURL, err)
 | 
						|
		return nil, gtserror.NewErrorNotFound(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Data function for copying just streams media
 | 
						|
	// out of storage into an additional location.
 | 
						|
	//
 | 
						|
	// This means that data for the copy persists even
 | 
						|
	// if the remote copied emoji gets deleted at some point.
 | 
						|
	data := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
						|
		rc, err := p.state.Storage.GetStream(ctx, target.ImagePath)
 | 
						|
		return rc, int64(target.ImageFileSize), err
 | 
						|
	}
 | 
						|
 | 
						|
	// Attempt to create the new local emoji.
 | 
						|
	emoji, errWithCode := p.createEmoji(ctx,
 | 
						|
		util.PtrValueOr(shortcode, ""),
 | 
						|
		util.PtrValueOr(categoryName, ""),
 | 
						|
		data,
 | 
						|
	)
 | 
						|
	if errWithCode != nil {
 | 
						|
		return nil, errWithCode
 | 
						|
	}
 | 
						|
 | 
						|
	apiEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return apiEmoji, nil
 | 
						|
}
 | 
						|
 | 
						|
// emojiUpdateDisable marks the given *remote*
 | 
						|
// emoji as disabled by setting disabled = true.
 | 
						|
//
 | 
						|
// The provided emoji model must correspond to an
 | 
						|
// emoji already stored in the database + storage.
 | 
						|
func (p *Processor) emojiUpdateDisable(
 | 
						|
	ctx context.Context,
 | 
						|
	emoji *gtsmodel.Emoji,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	if emoji.IsLocal() {
 | 
						|
		err := fmt.Errorf("emoji %s is not a remote emoji, cannot disable it via this endpoint", emoji.ID)
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Only bother with a db call
 | 
						|
	// if emoji not already disabled.
 | 
						|
	if !*emoji.Disabled {
 | 
						|
		emoji.Disabled = util.Ptr(true)
 | 
						|
		if err := p.state.DB.UpdateEmoji(ctx, emoji, "disabled"); err != nil {
 | 
						|
			err := gtserror.Newf("db error updating emoji %s: %w", emoji.ID, err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return adminEmoji, nil
 | 
						|
}
 | 
						|
 | 
						|
// emojiUpdateModify modifies the given *local* emoji.
 | 
						|
//
 | 
						|
// Either one of image or category must be non-nil,
 | 
						|
// otherwise there's nothing to modify. If category
 | 
						|
// is non-nil and dereferences to an empty string,
 | 
						|
// category will be cleared.
 | 
						|
//
 | 
						|
// The provided emoji model must correspond to an
 | 
						|
// emoji already stored in the database + storage.
 | 
						|
func (p *Processor) emojiUpdateModify(
 | 
						|
	ctx context.Context,
 | 
						|
	emoji *gtsmodel.Emoji,
 | 
						|
	image *multipart.FileHeader,
 | 
						|
	categoryName *string,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	if !emoji.IsLocal() {
 | 
						|
		const text = "cannot modify remote emoji"
 | 
						|
		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	// Ensure there's actually something to update.
 | 
						|
	if image == nil && categoryName == nil {
 | 
						|
		const text = "no changes were provided"
 | 
						|
		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	if categoryName != nil {
 | 
						|
		if *categoryName != "" {
 | 
						|
			// A category was provided, get / create relevant emoji category.
 | 
						|
			category, errWithCode := p.mustGetEmojiCategory(ctx, *categoryName)
 | 
						|
			if errWithCode != nil {
 | 
						|
				return nil, errWithCode
 | 
						|
			}
 | 
						|
 | 
						|
			if category.ID == emoji.CategoryID {
 | 
						|
				// There was no change,
 | 
						|
				// indicate this by unsetting
 | 
						|
				// the category name pointer.
 | 
						|
				categoryName = nil
 | 
						|
			} else {
 | 
						|
				// Update emoji category.
 | 
						|
				emoji.CategoryID = category.ID
 | 
						|
				emoji.Category = category
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			// Emoji category was unset.
 | 
						|
			emoji.CategoryID = ""
 | 
						|
			emoji.Category = nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Check whether any image changes were requested.
 | 
						|
	imageUpdated := (image != nil && image.Size > 0)
 | 
						|
 | 
						|
	if !imageUpdated && categoryName != nil {
 | 
						|
		// Only updating category; only a single database update required.
 | 
						|
		if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
 | 
						|
			err := gtserror.Newf("error updating emoji in db: %w", err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
	} else if imageUpdated {
 | 
						|
		var err error
 | 
						|
 | 
						|
		// Updating image and maybe categoryID.
 | 
						|
		// We can do both at the same time :)
 | 
						|
 | 
						|
		// Simply read provided form data for emoji data source.
 | 
						|
		data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						|
			f, err := image.Open()
 | 
						|
			return f, image.Size, err
 | 
						|
		}
 | 
						|
 | 
						|
		// Prepare emoji model for recache from new data.
 | 
						|
		processing := p.media.RecacheEmoji(emoji, data)
 | 
						|
 | 
						|
		// Load to trigger update + write.
 | 
						|
		emoji, err = processing.Load(ctx)
 | 
						|
		if err != nil {
 | 
						|
			err := gtserror.Newf("error processing emoji %s: %w", emoji.Shortcode, err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return adminEmoji, nil
 | 
						|
}
 | 
						|
 | 
						|
// createEmoji will create a new local emoji
 | 
						|
// with the given shortcode, attached category
 | 
						|
// name (if any) and data source function.
 | 
						|
func (p *Processor) createEmoji(
 | 
						|
	ctx context.Context,
 | 
						|
	shortcode string,
 | 
						|
	categoryName string,
 | 
						|
	data media.DataFunc,
 | 
						|
) (
 | 
						|
	*gtsmodel.Emoji,
 | 
						|
	gtserror.WithCode,
 | 
						|
) {
 | 
						|
	// Validate shortcode.
 | 
						|
	if shortcode == "" {
 | 
						|
		const text = "empty shortcode name"
 | 
						|
		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	// Look for an existing local emoji with shortcode to ensure this is new.
 | 
						|
	existing, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, shortcode, "")
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("error fetching emoji from db: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	} else if existing != nil {
 | 
						|
		const text = "emoji with shortcode already exists"
 | 
						|
		return nil, gtserror.NewErrorConflict(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	var categoryID *string
 | 
						|
 | 
						|
	if categoryName != "" {
 | 
						|
		// A category was provided, get / create relevant emoji category.
 | 
						|
		category, errWithCode := p.mustGetEmojiCategory(ctx, categoryName)
 | 
						|
		if errWithCode != nil {
 | 
						|
			return nil, errWithCode
 | 
						|
		}
 | 
						|
 | 
						|
		// Set category ID for emoji.
 | 
						|
		categoryID = &category.ID
 | 
						|
	}
 | 
						|
 | 
						|
	// Store to instance storage.
 | 
						|
	return p.c.StoreLocalEmoji(
 | 
						|
		ctx,
 | 
						|
		shortcode,
 | 
						|
		data,
 | 
						|
		media.AdditionalEmojiInfo{
 | 
						|
			CategoryID: categoryID,
 | 
						|
		},
 | 
						|
	)
 | 
						|
}
 | 
						|
 | 
						|
// mustGetEmojiCategory either gets an existing
 | 
						|
// category with the given name from the database,
 | 
						|
// or, if the category doesn't yet exist, it creates
 | 
						|
// the category and then returns it.
 | 
						|
func (p *Processor) mustGetEmojiCategory(
 | 
						|
	ctx context.Context,
 | 
						|
	name string,
 | 
						|
) (
 | 
						|
	*gtsmodel.EmojiCategory,
 | 
						|
	gtserror.WithCode,
 | 
						|
) {
 | 
						|
	// Look for an existing emoji category with name.
 | 
						|
	category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("error fetching emoji category from db: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if category != nil {
 | 
						|
		// We had it already.
 | 
						|
		return category, nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Create new ID.
 | 
						|
	id := id.NewULID()
 | 
						|
 | 
						|
	// Prepare new category for insertion.
 | 
						|
	category = >smodel.EmojiCategory{
 | 
						|
		ID:   id,
 | 
						|
		Name: name,
 | 
						|
	}
 | 
						|
 | 
						|
	// Insert new category into the database.
 | 
						|
	err = p.state.DB.PutEmojiCategory(ctx, category)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error inserting emoji category into db: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return category, nil
 | 
						|
}
 | 
						|
 | 
						|
// emojisGetFilterParams builds extra
 | 
						|
// query parameters to return as part
 | 
						|
// of an Emojis pageable response.
 | 
						|
//
 | 
						|
// The returned string will look like:
 | 
						|
//
 | 
						|
// "filter=domain:all,enabled,shortcode:example"
 | 
						|
func emojisGetFilterParams(
 | 
						|
	shortcode string,
 | 
						|
	domain string,
 | 
						|
	includeDisabled bool,
 | 
						|
	includeEnabled bool,
 | 
						|
) string {
 | 
						|
	var filterBuilder strings.Builder
 | 
						|
	filterBuilder.WriteString("filter=")
 | 
						|
 | 
						|
	switch domain {
 | 
						|
	case "", "local":
 | 
						|
		// Local emojis only.
 | 
						|
		filterBuilder.WriteString("domain:local")
 | 
						|
 | 
						|
	case db.EmojiAllDomains:
 | 
						|
		// Local or remote.
 | 
						|
		filterBuilder.WriteString("domain:all")
 | 
						|
 | 
						|
	default:
 | 
						|
		// Specific domain only.
 | 
						|
		filterBuilder.WriteString("domain:" + domain)
 | 
						|
	}
 | 
						|
 | 
						|
	if includeDisabled != includeEnabled {
 | 
						|
		if includeDisabled {
 | 
						|
			filterBuilder.WriteString(",disabled")
 | 
						|
		}
 | 
						|
		if includeEnabled {
 | 
						|
			filterBuilder.WriteString(",enabled")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if shortcode != "" {
 | 
						|
		// Specific shortcode only.
 | 
						|
		filterBuilder.WriteString(",shortcode:" + shortcode)
 | 
						|
	}
 | 
						|
 | 
						|
	return filterBuilder.String()
 | 
						|
}
 |