mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-28 13:52:25 -05:00 
			
		
		
		
	# Description Updates our dereferencer emoji handling to work asynchronously when going through the route of account or status dereferencing. closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4485 ## Checklist - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have not leveraged AI to create the proposed changes. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [ ] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. - [ ] I/we have added tests that cover new code. - [ ] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4486 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
		
			
				
	
	
		
			636 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			636 lines
		
	
	
	
		
			18 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 "code.superseriousbusiness.org/gotosocial/internal/api/model"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/config"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/db"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/gtserror"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/id"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/media"
 | |
| 	"code.superseriousbusiness.org/gotosocial/internal/util"
 | |
| 	"codeberg.org/gruf/go-iotools"
 | |
| )
 | |
| 
 | |
| // 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) {
 | |
| 
 | |
| 	// Get maximum supported local emoji size.
 | |
| 	maxsz := config.GetMediaEmojiLocalMaxSize()
 | |
| 	maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
 | |
| 
 | |
| 	// Ensure media within size bounds.
 | |
| 	if form.Image.Size > maxszInt64 {
 | |
| 		text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz)
 | |
| 		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | |
| 	}
 | |
| 
 | |
| 	// Open multipart file reader.
 | |
| 	mpfile, err := form.Image.Open()
 | |
| 	if err != nil {
 | |
| 		err := gtserror.Newf("error opening multipart file: %w", err)
 | |
| 		return nil, gtserror.NewErrorInternalError(err)
 | |
| 	}
 | |
| 
 | |
| 	// Wrap the multipart file reader to ensure is limited to max.
 | |
| 	rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, maxszInt64)
 | |
| 	data := func(context.Context) (io.ReadCloser, error) {
 | |
| 		return rc, nil
 | |
| 	}
 | |
| 
 | |
| 	// 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: emojis[count-1].ShortcodeDomain(),
 | |
| 		PrevMinIDKey:   "min_shortcode_domain",
 | |
| 		PrevMinIDValue: emojis[0].ShortcodeDomain(),
 | |
| 		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.RecacheEmoji(ctx,
 | |
| 		target,
 | |
| 		false,
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		err := gtserror.Newf("error recaching emoji %s: %w", target.ImageRemoteURL, err)
 | |
| 		return nil, gtserror.NewErrorNotFound(err)
 | |
| 	}
 | |
| 
 | |
| 	// Get maximum supported local emoji size.
 | |
| 	maxsz := config.GetMediaEmojiLocalMaxSize()
 | |
| 	maxszInt := int(maxsz) // #nosec G115 -- Already validated.
 | |
| 
 | |
| 	// Ensure target emoji image within size bounds.
 | |
| 	if target.ImageFileSize > maxszInt {
 | |
| 		text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz)
 | |
| 		return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | |
| 	}
 | |
| 
 | |
| 	// 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, error) {
 | |
| 		rc, err := p.state.Storage.GetStream(ctx, target.ImagePath)
 | |
| 		return rc, err
 | |
| 	}
 | |
| 
 | |
| 	// Attempt to create the new local emoji.
 | |
| 	emoji, errWithCode := p.createEmoji(ctx,
 | |
| 		util.PtrOrZero(shortcode),
 | |
| 		util.PtrOrZero(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)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we need to
 | |
| 	// set a new category ID.
 | |
| 	var newCategoryID *string
 | |
| 	switch {
 | |
| 	case categoryName == nil:
 | |
| 		// No changes.
 | |
| 
 | |
| 	case *categoryName == "":
 | |
| 		// Emoji category was unset.
 | |
| 		newCategoryID = util.Ptr("")
 | |
| 		emoji.CategoryID = ""
 | |
| 		emoji.Category = nil
 | |
| 
 | |
| 	case *categoryName != "":
 | |
| 		// A category was provided, get or create relevant emoji category.
 | |
| 		category, errWithCode := p.mustGetEmojiCategory(ctx, *categoryName)
 | |
| 		if errWithCode != nil {
 | |
| 			return nil, errWithCode
 | |
| 		}
 | |
| 
 | |
| 		// Update emoji category if
 | |
| 		// it's different from before.
 | |
| 		if category.ID != emoji.CategoryID {
 | |
| 			newCategoryID = &category.ID
 | |
| 			emoji.CategoryID = category.ID
 | |
| 			emoji.Category = category
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Check whether any image changes were requested.
 | |
| 	imageUpdated := (image != nil && image.Size > 0)
 | |
| 
 | |
| 	if !imageUpdated && newCategoryID != 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 :)
 | |
| 
 | |
| 		// Get maximum supported local emoji size.
 | |
| 		maxsz := config.GetMediaEmojiLocalMaxSize()
 | |
| 		maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
 | |
| 
 | |
| 		// Ensure media within size bounds.
 | |
| 		if image.Size > maxszInt64 {
 | |
| 			text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz)
 | |
| 			return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | |
| 		}
 | |
| 
 | |
| 		// Open multipart file reader.
 | |
| 		mpfile, err := image.Open()
 | |
| 		if err != nil {
 | |
| 			err := gtserror.Newf("error opening multipart file: %w", err)
 | |
| 			return nil, gtserror.NewErrorInternalError(err)
 | |
| 		}
 | |
| 
 | |
| 		// Wrap the multipart file reader to ensure is limited to max.
 | |
| 		rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) // #nosec G115 -- Already validated.
 | |
| 		data := func(context.Context) (io.ReadCloser, error) {
 | |
| 			return rc, nil
 | |
| 		}
 | |
| 
 | |
| 		// Include category ID
 | |
| 		// update if necessary.
 | |
| 		ai := media.AdditionalEmojiInfo{}
 | |
| 		ai.CategoryID = newCategoryID
 | |
| 
 | |
| 		// Prepare emoji model for update+recache from new data.
 | |
| 		processing, err := p.media.UpdateEmoji(ctx, emoji, data, ai)
 | |
| 		if err != nil {
 | |
| 			err := gtserror.Newf("error preparing recache: %w", err)
 | |
| 			return nil, gtserror.NewErrorInternalError(err)
 | |
| 		}
 | |
| 
 | |
| 		// 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()
 | |
| }
 |