gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

emoji.go (17034B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package admin
     19 
     20 import (
     21 	"context"
     22 	"errors"
     23 	"fmt"
     24 	"io"
     25 	"mime/multipart"
     26 	"strings"
     27 
     28 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
     29 	"github.com/superseriousbusiness/gotosocial/internal/db"
     30 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     31 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     32 	"github.com/superseriousbusiness/gotosocial/internal/id"
     33 	"github.com/superseriousbusiness/gotosocial/internal/media"
     34 	"github.com/superseriousbusiness/gotosocial/internal/uris"
     35 	"github.com/superseriousbusiness/gotosocial/internal/util"
     36 )
     37 
     38 // EmojiCreate creates a custom emoji on this instance.
     39 func (p *Processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) {
     40 	if !*user.Admin {
     41 		return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
     42 	}
     43 
     44 	maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
     45 	if maybeExisting != nil {
     46 		return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode))
     47 	}
     48 
     49 	if err != nil && err != db.ErrNoEntries {
     50 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking existence of emoji with shortcode %s: %s", form.Shortcode, err))
     51 	}
     52 
     53 	emojiID, err := id.NewRandomULID()
     54 	if err != nil {
     55 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new emoji: %s", err), "error creating emoji ID")
     56 	}
     57 
     58 	emojiURI := uris.GenerateURIForEmoji(emojiID)
     59 
     60 	data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
     61 		f, err := form.Image.Open()
     62 		return f, form.Image.Size, err
     63 	}
     64 
     65 	var ai *media.AdditionalEmojiInfo
     66 	if form.CategoryName != "" {
     67 		category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName)
     68 		if err != nil {
     69 			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting id in category: %s", err), "error putting id in category")
     70 		}
     71 
     72 		ai = &media.AdditionalEmojiInfo{
     73 			CategoryID: &category.ID,
     74 		}
     75 	}
     76 
     77 	processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, form.Shortcode, emojiID, emojiURI, ai, false)
     78 	if err != nil {
     79 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji")
     80 	}
     81 
     82 	emoji, err := processingEmoji.LoadEmoji(ctx)
     83 	if err != nil {
     84 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji")
     85 	}
     86 
     87 	apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji)
     88 	if err != nil {
     89 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting emoji: %s", err), "error converting emoji to api representation")
     90 	}
     91 
     92 	return &apiEmoji, nil
     93 }
     94 
     95 // EmojisGet returns an admin view of custom emojis, filtered with the given parameters.
     96 func (p *Processor) EmojisGet(
     97 	ctx context.Context,
     98 	account *gtsmodel.Account,
     99 	user *gtsmodel.User,
    100 	domain string,
    101 	includeDisabled bool,
    102 	includeEnabled bool,
    103 	shortcode string,
    104 	maxShortcodeDomain string,
    105 	minShortcodeDomain string,
    106 	limit int,
    107 ) (*apimodel.PageableResponse, gtserror.WithCode) {
    108 	if !*user.Admin {
    109 		return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
    110 	}
    111 
    112 	emojis, err := p.state.DB.GetEmojis(ctx, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit)
    113 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    114 		err := fmt.Errorf("EmojisGet: db error: %s", err)
    115 		return nil, gtserror.NewErrorInternalError(err)
    116 	}
    117 
    118 	count := len(emojis)
    119 	if count == 0 {
    120 		return util.EmptyPageableResponse(), nil
    121 	}
    122 
    123 	items := make([]interface{}, 0, count)
    124 	for _, emoji := range emojis {
    125 		adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
    126 		if err != nil {
    127 			err := fmt.Errorf("EmojisGet: error converting emoji to admin model emoji: %s", err)
    128 			return nil, gtserror.NewErrorInternalError(err)
    129 		}
    130 		items = append(items, adminEmoji)
    131 	}
    132 
    133 	filterBuilder := strings.Builder{}
    134 	filterBuilder.WriteString("filter=")
    135 
    136 	switch domain {
    137 	case "", "local":
    138 		filterBuilder.WriteString("domain:local")
    139 	case db.EmojiAllDomains:
    140 		filterBuilder.WriteString("domain:all")
    141 	default:
    142 		filterBuilder.WriteString("domain:")
    143 		filterBuilder.WriteString(domain)
    144 	}
    145 
    146 	if includeDisabled != includeEnabled {
    147 		if includeDisabled {
    148 			filterBuilder.WriteString(",disabled")
    149 		}
    150 		if includeEnabled {
    151 			filterBuilder.WriteString(",enabled")
    152 		}
    153 	}
    154 
    155 	if shortcode != "" {
    156 		filterBuilder.WriteString(",shortcode:")
    157 		filterBuilder.WriteString(shortcode)
    158 	}
    159 
    160 	return util.PackagePageableResponse(util.PageableResponseParams{
    161 		Items:            items,
    162 		Path:             "api/v1/admin/custom_emojis",
    163 		NextMaxIDKey:     "max_shortcode_domain",
    164 		NextMaxIDValue:   util.ShortcodeDomain(emojis[count-1]),
    165 		PrevMinIDKey:     "min_shortcode_domain",
    166 		PrevMinIDValue:   util.ShortcodeDomain(emojis[0]),
    167 		Limit:            limit,
    168 		ExtraQueryParams: []string{filterBuilder.String()},
    169 	})
    170 }
    171 
    172 // EmojiGet returns the admin view of one custom emoji with the given id.
    173 func (p *Processor) EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
    174 	if !*user.Admin {
    175 		return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
    176 	}
    177 
    178 	emoji, err := p.state.DB.GetEmojiByID(ctx, id)
    179 	if err != nil {
    180 		if errors.Is(err, db.ErrNoEntries) {
    181 			err = fmt.Errorf("EmojiGet: no emoji with id %s found in the db", id)
    182 			return nil, gtserror.NewErrorNotFound(err)
    183 		}
    184 		err := fmt.Errorf("EmojiGet: db error: %s", err)
    185 		return nil, gtserror.NewErrorInternalError(err)
    186 	}
    187 
    188 	adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
    189 	if err != nil {
    190 		err = fmt.Errorf("EmojiGet: error converting emoji to admin api emoji: %s", err)
    191 		return nil, gtserror.NewErrorInternalError(err)
    192 	}
    193 
    194 	return adminEmoji, nil
    195 }
    196 
    197 // EmojiDelete deletes one emoji from the database, with the given id.
    198 func (p *Processor) EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
    199 	emoji, err := p.state.DB.GetEmojiByID(ctx, id)
    200 	if err != nil {
    201 		if errors.Is(err, db.ErrNoEntries) {
    202 			err = fmt.Errorf("EmojiDelete: no emoji with id %s found in the db", id)
    203 			return nil, gtserror.NewErrorNotFound(err)
    204 		}
    205 		err := fmt.Errorf("EmojiDelete: db error: %s", err)
    206 		return nil, gtserror.NewErrorInternalError(err)
    207 	}
    208 
    209 	if emoji.Domain != "" {
    210 		err = fmt.Errorf("EmojiDelete: emoji with id %s was not a local emoji, will not delete", id)
    211 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    212 	}
    213 
    214 	adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
    215 	if err != nil {
    216 		err = fmt.Errorf("EmojiDelete: error converting emoji to admin api emoji: %s", err)
    217 		return nil, gtserror.NewErrorInternalError(err)
    218 	}
    219 
    220 	if err := p.state.DB.DeleteEmojiByID(ctx, id); err != nil {
    221 		err := fmt.Errorf("EmojiDelete: db error: %s", err)
    222 		return nil, gtserror.NewErrorInternalError(err)
    223 	}
    224 
    225 	return adminEmoji, nil
    226 }
    227 
    228 // EmojiUpdate updates one emoji with the given id, using the provided form parameters.
    229 func (p *Processor) EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) {
    230 	emoji, err := p.state.DB.GetEmojiByID(ctx, id)
    231 	if err != nil {
    232 		if errors.Is(err, db.ErrNoEntries) {
    233 			err = fmt.Errorf("EmojiUpdate: no emoji with id %s found in the db", id)
    234 			return nil, gtserror.NewErrorNotFound(err)
    235 		}
    236 		err := fmt.Errorf("EmojiUpdate: db error: %s", err)
    237 		return nil, gtserror.NewErrorInternalError(err)
    238 	}
    239 
    240 	switch form.Type {
    241 	case apimodel.EmojiUpdateCopy:
    242 		return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
    243 	case apimodel.EmojiUpdateDisable:
    244 		return p.emojiUpdateDisable(ctx, emoji)
    245 	case apimodel.EmojiUpdateModify:
    246 		return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
    247 	default:
    248 		err := errors.New("unrecognized emoji action type")
    249 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    250 	}
    251 }
    252 
    253 // EmojiCategoriesGet returns all custom emoji categories that exist on this instance.
    254 func (p *Processor) EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) {
    255 	categories, err := p.state.DB.GetEmojiCategories(ctx)
    256 	if err != nil {
    257 		err := fmt.Errorf("EmojiCategoriesGet: db error: %s", err)
    258 		return nil, gtserror.NewErrorInternalError(err)
    259 	}
    260 
    261 	apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories))
    262 	for _, category := range categories {
    263 		apiCategory, err := p.tc.EmojiCategoryToAPIEmojiCategory(ctx, category)
    264 		if err != nil {
    265 			err := fmt.Errorf("EmojiCategoriesGet: error converting emoji category to api emoji category: %s", err)
    266 			return nil, gtserror.NewErrorInternalError(err)
    267 		}
    268 		apiCategories = append(apiCategories, apiCategory)
    269 	}
    270 
    271 	return apiCategories, nil
    272 }
    273 
    274 /*
    275 	UTIL FUNCTIONS
    276 */
    277 
    278 func (p *Processor) getOrCreateEmojiCategory(ctx context.Context, name string) (*gtsmodel.EmojiCategory, error) {
    279 	category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
    280 	if err == nil {
    281 		return category, nil
    282 	}
    283 
    284 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    285 		err = fmt.Errorf("GetOrCreateEmojiCategory: database error trying get emoji category by name: %s", err)
    286 		return nil, err
    287 	}
    288 
    289 	// we don't have the category yet, just create it with the given name
    290 	categoryID, err := id.NewRandomULID()
    291 	if err != nil {
    292 		err = fmt.Errorf("GetOrCreateEmojiCategory: error generating id for new emoji category: %s", err)
    293 		return nil, err
    294 	}
    295 
    296 	category = &gtsmodel.EmojiCategory{
    297 		ID:   categoryID,
    298 		Name: name,
    299 	}
    300 
    301 	if err := p.state.DB.PutEmojiCategory(ctx, category); err != nil {
    302 		err = fmt.Errorf("GetOrCreateEmojiCategory: error putting new emoji category in the database: %s", err)
    303 		return nil, err
    304 	}
    305 
    306 	return category, nil
    307 }
    308 
    309 // copy an emoji from remote to local
    310 func (p *Processor) emojiUpdateCopy(ctx context.Context, emoji *gtsmodel.Emoji, shortcode *string, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) {
    311 	if emoji.Domain == "" {
    312 		err := fmt.Errorf("emojiUpdateCopy: emoji %s is not a remote emoji, cannot copy it to local", emoji.ID)
    313 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    314 	}
    315 
    316 	if shortcode == nil {
    317 		err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, no shortcode provided", emoji.ID)
    318 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    319 	}
    320 
    321 	maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, *shortcode, "")
    322 	if maybeExisting != nil {
    323 		err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, emoji with shortcode %s already exists on this instance", emoji.ID, *shortcode)
    324 		return nil, gtserror.NewErrorConflict(err, err.Error())
    325 	}
    326 
    327 	if err != nil && err != db.ErrNoEntries {
    328 		err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error checking existence of emoji with shortcode %s: %s", emoji.ID, *shortcode, err)
    329 		return nil, gtserror.NewErrorInternalError(err)
    330 	}
    331 
    332 	newEmojiID, err := id.NewRandomULID()
    333 	if err != nil {
    334 		err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error creating id for new emoji: %s", emoji.ID, err)
    335 		return nil, gtserror.NewErrorInternalError(err)
    336 	}
    337 
    338 	newEmojiURI := uris.GenerateURIForEmoji(newEmojiID)
    339 
    340 	data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) {
    341 		rc, err := p.state.Storage.GetStream(ctx, emoji.ImagePath)
    342 		return rc, int64(emoji.ImageFileSize), err
    343 	}
    344 
    345 	var ai *media.AdditionalEmojiInfo
    346 	if categoryName != nil {
    347 		category, err := p.getOrCreateEmojiCategory(ctx, *categoryName)
    348 		if err != nil {
    349 			err = fmt.Errorf("emojiUpdateCopy: error getting or creating category: %s", err)
    350 			return nil, gtserror.NewErrorInternalError(err)
    351 		}
    352 
    353 		ai = &media.AdditionalEmojiInfo{
    354 			CategoryID: &category.ID,
    355 		}
    356 	}
    357 
    358 	processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, *shortcode, newEmojiID, newEmojiURI, ai, false)
    359 	if err != nil {
    360 		err = fmt.Errorf("emojiUpdateCopy: error processing emoji %s: %s", emoji.ID, err)
    361 		return nil, gtserror.NewErrorInternalError(err)
    362 	}
    363 
    364 	newEmoji, err := processingEmoji.LoadEmoji(ctx)
    365 	if err != nil {
    366 		err = fmt.Errorf("emojiUpdateCopy: error loading processed emoji %s: %s", emoji.ID, err)
    367 		return nil, gtserror.NewErrorInternalError(err)
    368 	}
    369 
    370 	adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, newEmoji)
    371 	if err != nil {
    372 		err = fmt.Errorf("emojiUpdateCopy: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
    373 		return nil, gtserror.NewErrorInternalError(err)
    374 	}
    375 
    376 	return adminEmoji, nil
    377 }
    378 
    379 // disable a remote emoji
    380 func (p *Processor) emojiUpdateDisable(ctx context.Context, emoji *gtsmodel.Emoji) (*apimodel.AdminEmoji, gtserror.WithCode) {
    381 	if emoji.Domain == "" {
    382 		err := fmt.Errorf("emojiUpdateDisable: emoji %s is not a remote emoji, cannot disable it via this endpoint", emoji.ID)
    383 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    384 	}
    385 
    386 	emojiDisabled := true
    387 	emoji.Disabled = &emojiDisabled
    388 	updatedEmoji, err := p.state.DB.UpdateEmoji(ctx, emoji, "disabled")
    389 	if err != nil {
    390 		err = fmt.Errorf("emojiUpdateDisable: error updating emoji %s: %s", emoji.ID, err)
    391 		return nil, gtserror.NewErrorInternalError(err)
    392 	}
    393 
    394 	adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji)
    395 	if err != nil {
    396 		err = fmt.Errorf("emojiUpdateDisable: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
    397 		return nil, gtserror.NewErrorInternalError(err)
    398 	}
    399 
    400 	return adminEmoji, nil
    401 }
    402 
    403 // modify a local emoji
    404 func (p *Processor) emojiUpdateModify(ctx context.Context, emoji *gtsmodel.Emoji, image *multipart.FileHeader, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) {
    405 	if emoji.Domain != "" {
    406 		err := fmt.Errorf("emojiUpdateModify: emoji %s is not a local emoji, cannot do a modify action on it", emoji.ID)
    407 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    408 	}
    409 
    410 	var updatedEmoji *gtsmodel.Emoji
    411 
    412 	// keep existing categoryID unless a new one is defined
    413 	var (
    414 		updatedCategoryID = emoji.CategoryID
    415 		updateCategoryID  bool
    416 	)
    417 	if categoryName != nil {
    418 		category, err := p.getOrCreateEmojiCategory(ctx, *categoryName)
    419 		if err != nil {
    420 			err = fmt.Errorf("emojiUpdateModify: error getting or creating category: %s", err)
    421 			return nil, gtserror.NewErrorInternalError(err)
    422 		}
    423 
    424 		updatedCategoryID = category.ID
    425 		updateCategoryID = true
    426 	}
    427 
    428 	// only update image if provided with one
    429 	var updateImage bool
    430 	if image != nil && image.Size != 0 {
    431 		updateImage = true
    432 	}
    433 
    434 	if !updateImage {
    435 		// only updating fields, we only need
    436 		// to do a database update for this
    437 		var columns []string
    438 
    439 		if updateCategoryID {
    440 			emoji.CategoryID = updatedCategoryID
    441 			columns = append(columns, "category_id")
    442 		}
    443 
    444 		var err error
    445 		updatedEmoji, err = p.state.DB.UpdateEmoji(ctx, emoji, columns...)
    446 		if err != nil {
    447 			err = fmt.Errorf("emojiUpdateModify: error updating emoji %s: %s", emoji.ID, err)
    448 			return nil, gtserror.NewErrorInternalError(err)
    449 		}
    450 	} else {
    451 		// new image, so we need to reprocess the emoji
    452 		data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) {
    453 			i, err := image.Open()
    454 			return i, image.Size, err
    455 		}
    456 
    457 		var ai *media.AdditionalEmojiInfo
    458 		if updateCategoryID {
    459 			ai = &media.AdditionalEmojiInfo{
    460 				CategoryID: &updatedCategoryID,
    461 			}
    462 		}
    463 
    464 		processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, emoji.Shortcode, emoji.ID, emoji.URI, ai, true)
    465 		if err != nil {
    466 			err = fmt.Errorf("emojiUpdateModify: error processing emoji %s: %s", emoji.ID, err)
    467 			return nil, gtserror.NewErrorInternalError(err)
    468 		}
    469 
    470 		updatedEmoji, err = processingEmoji.LoadEmoji(ctx)
    471 		if err != nil {
    472 			err = fmt.Errorf("emojiUpdateModify: error loading processed emoji %s: %s", emoji.ID, err)
    473 			return nil, gtserror.NewErrorInternalError(err)
    474 		}
    475 	}
    476 
    477 	adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji)
    478 	if err != nil {
    479 		err = fmt.Errorf("emojiUpdateModify: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
    480 		return nil, gtserror.NewErrorInternalError(err)
    481 	}
    482 
    483 	return adminEmoji, nil
    484 }