mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 22:32:25 -06:00 
			
		
		
		
	* [bugfix] Fix Postgres emoji delete, emoji category change * revert trace logging * caching issue * update tests
		
			
				
	
	
		
			644 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			644 lines
		
	
	
	
		
			19 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/uris"
 | 
						|
	"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) {
 | 
						|
	// Ensure emoji with this shortcode
 | 
						|
	// doesn't already exist on the instance.
 | 
						|
	maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("error checking existence of emoji with shortcode %s: %w", form.Shortcode, err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if maybeExisting != nil {
 | 
						|
		err := fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode)
 | 
						|
		return nil, gtserror.NewErrorConflict(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Prepare data function for emoji processing
 | 
						|
	// (just read data from the submitted form).
 | 
						|
	data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
 | 
						|
		f, err := form.Image.Open()
 | 
						|
		return f, form.Image.Size, err
 | 
						|
	}
 | 
						|
 | 
						|
	// If category was supplied on the form,
 | 
						|
	// ensure the category exists and provide
 | 
						|
	// it as additional info to emoji processing.
 | 
						|
	var ai *media.AdditionalEmojiInfo
 | 
						|
	if form.CategoryName != "" {
 | 
						|
		category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName)
 | 
						|
		if err != nil {
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
 | 
						|
		ai = &media.AdditionalEmojiInfo{
 | 
						|
			CategoryID: &category.ID,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Generate new emoji ID and URI.
 | 
						|
	emojiID, err := id.NewRandomULID()
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error creating id for new emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	emojiURI := uris.URIForEmoji(emojiID)
 | 
						|
 | 
						|
	// Begin media processing.
 | 
						|
	processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
 | 
						|
		data, form.Shortcode, emojiID, emojiURI, ai, false,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error processing emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Complete processing immediately.
 | 
						|
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error loading emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
}
 | 
						|
 | 
						|
// 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()
 | 
						|
}
 | 
						|
 | 
						|
// 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,
 | 
						|
	id string,
 | 
						|
	form *apimodel.EmojiUpdateRequest,
 | 
						|
) (*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)
 | 
						|
	}
 | 
						|
 | 
						|
	switch t := form.Type; t {
 | 
						|
 | 
						|
	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:
 | 
						|
		err := fmt.Errorf("unrecognized emoji action type %s", t)
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// 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
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
	UTIL FUNCTIONS
 | 
						|
*/
 | 
						|
 | 
						|
// getOrCreateEmojiCategory 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) getOrCreateEmojiCategory(
 | 
						|
	ctx context.Context,
 | 
						|
	name string,
 | 
						|
) (*gtsmodel.EmojiCategory, error) {
 | 
						|
	category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		return nil, gtserror.Newf(
 | 
						|
			"database error trying get emoji category %s: %w",
 | 
						|
			name, err,
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	if category != nil {
 | 
						|
		// We had it already.
 | 
						|
		return category, nil
 | 
						|
	}
 | 
						|
 | 
						|
	// We don't have the category yet,
 | 
						|
	// create it with the given name.
 | 
						|
	categoryID, err := id.NewRandomULID()
 | 
						|
	if err != nil {
 | 
						|
		return nil, gtserror.Newf(
 | 
						|
			"error generating id for new emoji category %s: %w",
 | 
						|
			name, err,
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	category = >smodel.EmojiCategory{
 | 
						|
		ID:   categoryID,
 | 
						|
		Name: name,
 | 
						|
	}
 | 
						|
 | 
						|
	if err := p.state.DB.PutEmojiCategory(ctx, category); err != nil {
 | 
						|
		return nil, gtserror.Newf(
 | 
						|
			"db error putting new emoji category %s: %w",
 | 
						|
			name, err,
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	return category, 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,
 | 
						|
	targetEmoji *gtsmodel.Emoji,
 | 
						|
	shortcode *string,
 | 
						|
	category *string,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	if targetEmoji.IsLocal() {
 | 
						|
		err := fmt.Errorf("emoji %s is not a remote emoji, cannot copy it to local", targetEmoji.ID)
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	if shortcode == nil {
 | 
						|
		err := errors.New("no shortcode provided")
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	sc := *shortcode
 | 
						|
	if sc == "" {
 | 
						|
		err := errors.New("empty shortcode provided")
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Ensure we don't already have an emoji
 | 
						|
	// stored locally with this shortcode.
 | 
						|
	maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, sc, "")
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("db error checking for emoji with shortcode %s: %w", sc, err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if maybeExisting != nil {
 | 
						|
		err := fmt.Errorf("emoji with shortcode %s already exists on this instance", sc)
 | 
						|
		return nil, gtserror.NewErrorConflict(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// We don't have an emoji with this
 | 
						|
	// shortcode yet! Prepare to create it.
 | 
						|
 | 
						|
	// 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, targetEmoji.ImagePath)
 | 
						|
		return rc, int64(targetEmoji.ImageFileSize), err
 | 
						|
	}
 | 
						|
 | 
						|
	// Generate new emoji ID and URI.
 | 
						|
	emojiID, err := id.NewRandomULID()
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error creating id for new emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	emojiURI := uris.URIForEmoji(emojiID)
 | 
						|
 | 
						|
	// If category was supplied, ensure the
 | 
						|
	// category exists and provide it as
 | 
						|
	// additional info to emoji processing.
 | 
						|
	var ai *media.AdditionalEmojiInfo
 | 
						|
	if category != nil && *category != "" {
 | 
						|
		category, err := p.getOrCreateEmojiCategory(ctx, *category)
 | 
						|
		if err != nil {
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
 | 
						|
		ai = &media.AdditionalEmojiInfo{
 | 
						|
			CategoryID: &category.ID,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Begin media processing.
 | 
						|
	processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
 | 
						|
		data, sc, emojiID, emojiURI, ai, false,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error processing emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Complete processing immediately.
 | 
						|
	newEmoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error loading emoji: %w", err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, newEmoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji %s to admin emoji: %w", newEmoji.ID, err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return adminEmoji, 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 %s to admin emoji: %w", emoji.ID, 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,
 | 
						|
	category *string,
 | 
						|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
						|
	if !emoji.IsLocal() {
 | 
						|
		err := fmt.Errorf("emoji %s is not a local emoji, cannot update it via this endpoint", emoji.ID)
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Ensure there's actually something to update.
 | 
						|
	if image == nil && category == nil {
 | 
						|
		err := errors.New("neither new category nor new image set, cannot update")
 | 
						|
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Only update category
 | 
						|
	// if it's changed.
 | 
						|
	var (
 | 
						|
		newCategory      *gtsmodel.EmojiCategory
 | 
						|
		newCategoryID    string
 | 
						|
		updateCategoryID bool
 | 
						|
	)
 | 
						|
 | 
						|
	if category != nil {
 | 
						|
		catName := *category
 | 
						|
		if catName != "" {
 | 
						|
			// Set new category.
 | 
						|
			var err error
 | 
						|
			newCategory, err = p.getOrCreateEmojiCategory(ctx, catName)
 | 
						|
			if err != nil {
 | 
						|
				err := gtserror.Newf("error getting or creating category: %w", err)
 | 
						|
				return nil, gtserror.NewErrorInternalError(err)
 | 
						|
			}
 | 
						|
 | 
						|
			newCategoryID = newCategory.ID
 | 
						|
		} else {
 | 
						|
			// Clear existing category.
 | 
						|
			newCategoryID = ""
 | 
						|
		}
 | 
						|
 | 
						|
		updateCategoryID = emoji.CategoryID != newCategoryID
 | 
						|
	}
 | 
						|
 | 
						|
	// Only update image
 | 
						|
	// if one is provided.
 | 
						|
	var updateImage bool
 | 
						|
	if image != nil && image.Size != 0 {
 | 
						|
		updateImage = true
 | 
						|
	}
 | 
						|
 | 
						|
	if updateCategoryID && !updateImage {
 | 
						|
		// Only updating category; we only
 | 
						|
		// need to do a db update for this.
 | 
						|
		emoji.CategoryID = newCategoryID
 | 
						|
		emoji.Category = newCategory
 | 
						|
		if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
 | 
						|
			err := gtserror.Newf("db error updating emoji %s: %w", emoji.ID, err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
	} else if updateImage {
 | 
						|
		// Updating image and maybe categoryID.
 | 
						|
		// We can do both at the same time :)
 | 
						|
 | 
						|
		// Set data function to provided image.
 | 
						|
		data := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
						|
			i, err := image.Open()
 | 
						|
			return i, image.Size, err
 | 
						|
		}
 | 
						|
 | 
						|
		// If necessary, include
 | 
						|
		// update to categoryID too.
 | 
						|
		var ai *media.AdditionalEmojiInfo
 | 
						|
		if updateCategoryID {
 | 
						|
			ai = &media.AdditionalEmojiInfo{
 | 
						|
				CategoryID: &newCategoryID,
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Begin media processing.
 | 
						|
		processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
 | 
						|
			data, emoji.Shortcode, emoji.ID, emoji.URI, ai, false,
 | 
						|
		)
 | 
						|
		if err != nil {
 | 
						|
			err := gtserror.Newf("error processing emoji: %w", err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Replace emoji ptr with newly-processed version.
 | 
						|
		emoji, err = processingEmoji.LoadEmoji(ctx)
 | 
						|
		if err != nil {
 | 
						|
			err := gtserror.Newf("error loading emoji: %w", err)
 | 
						|
			return nil, gtserror.NewErrorInternalError(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
 | 
						|
		return nil, gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return adminEmoji, nil
 | 
						|
}
 |