gtsocial-umbx

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

internaltofrontend.go (43581B)


      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 typeutils
     19 
     20 import (
     21 	"context"
     22 	"errors"
     23 	"fmt"
     24 	"math"
     25 	"strconv"
     26 	"strings"
     27 
     28 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
     29 	"github.com/superseriousbusiness/gotosocial/internal/config"
     30 	"github.com/superseriousbusiness/gotosocial/internal/db"
     31 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     32 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     33 	"github.com/superseriousbusiness/gotosocial/internal/log"
     34 	"github.com/superseriousbusiness/gotosocial/internal/media"
     35 	"github.com/superseriousbusiness/gotosocial/internal/util"
     36 )
     37 
     38 const (
     39 	instanceStatusesCharactersReservedPerURL    = 25
     40 	instanceMediaAttachmentsImageMatrixLimit    = 16777216 // width * height
     41 	instanceMediaAttachmentsVideoMatrixLimit    = 16777216 // width * height
     42 	instanceMediaAttachmentsVideoFrameRateLimit = 60
     43 	instancePollsMinExpiration                  = 300     // seconds
     44 	instancePollsMaxExpiration                  = 2629746 // seconds
     45 	instanceAccountsMaxFeaturedTags             = 10
     46 	instanceAccountsMaxProfileFields            = 6 // FIXME: https://github.com/superseriousbusiness/gotosocial/issues/1876
     47 	instanceSourceURL                           = "https://github.com/superseriousbusiness/gotosocial"
     48 )
     49 
     50 var instanceStatusesSupportedMimeTypes = []string{
     51 	string(apimodel.StatusContentTypePlain),
     52 	string(apimodel.StatusContentTypeMarkdown),
     53 }
     54 
     55 func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
     56 	// we can build this sensitive account easily by first getting the public account....
     57 	apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
     58 	if err != nil {
     59 		return nil, err
     60 	}
     61 
     62 	// then adding the Source object to it...
     63 
     64 	// check pending follow requests aimed at this account
     65 	frc, err := c.db.CountAccountFollowRequests(ctx, a.ID)
     66 	if err != nil {
     67 		return nil, fmt.Errorf("error counting follow requests: %s", err)
     68 	}
     69 
     70 	statusContentType := string(apimodel.StatusContentTypeDefault)
     71 	if a.StatusContentType != "" {
     72 		statusContentType = a.StatusContentType
     73 	}
     74 
     75 	apiAccount.Source = &apimodel.Source{
     76 		Privacy:             c.VisToAPIVis(ctx, a.Privacy),
     77 		Sensitive:           *a.Sensitive,
     78 		Language:            a.Language,
     79 		StatusContentType:   statusContentType,
     80 		Note:                a.NoteRaw,
     81 		Fields:              c.fieldsToAPIFields(a.FieldsRaw),
     82 		FollowRequestsCount: frc,
     83 	}
     84 
     85 	return apiAccount, nil
     86 }
     87 
     88 func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
     89 	if err := c.db.PopulateAccount(ctx, a); err != nil {
     90 		log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
     91 	}
     92 
     93 	// Basic account stats:
     94 	//   - Followers count
     95 	//   - Following count
     96 	//   - Statuses count
     97 	//   - Last status time
     98 
     99 	followersCount, err := c.db.CountAccountFollowers(ctx, a.ID)
    100 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    101 		return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting followers: %w", err)
    102 	}
    103 
    104 	followingCount, err := c.db.CountAccountFollows(ctx, a.ID)
    105 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    106 		return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting following: %w", err)
    107 	}
    108 
    109 	statusesCount, err := c.db.CountAccountStatuses(ctx, a.ID)
    110 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    111 		return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err)
    112 	}
    113 
    114 	var lastStatusAt *string
    115 	lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false)
    116 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    117 		return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err)
    118 	}
    119 
    120 	if !lastPosted.IsZero() {
    121 		lastStatusAt = func() *string { t := util.FormatISO8601(lastPosted); return &t }()
    122 	}
    123 
    124 	// Profile media + nice extras:
    125 	//   - Avatar
    126 	//   - Header
    127 	//   - Fields
    128 	//   - Emojis
    129 
    130 	var (
    131 		aviURL          string
    132 		aviURLStatic    string
    133 		headerURL       string
    134 		headerURLStatic string
    135 	)
    136 
    137 	if a.AvatarMediaAttachment != nil {
    138 		aviURL = a.AvatarMediaAttachment.URL
    139 		aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL
    140 	}
    141 
    142 	if a.HeaderMediaAttachment != nil {
    143 		headerURL = a.HeaderMediaAttachment.URL
    144 		headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL
    145 	}
    146 
    147 	// convert account gts model fields to front api model fields
    148 	fields := c.fieldsToAPIFields(a.Fields)
    149 
    150 	// GTS model emojis -> frontend.
    151 	apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, a.Emojis, a.EmojiIDs)
    152 	if err != nil {
    153 		log.Errorf(ctx, "error converting account emojis: %v", err)
    154 	}
    155 
    156 	// Bits that vary between remote + local accounts:
    157 	//   - Account (acct) string.
    158 	//   - Role.
    159 
    160 	var (
    161 		acct string
    162 		role *apimodel.AccountRole
    163 	)
    164 
    165 	if a.IsRemote() {
    166 		// Domain may be in Punycode,
    167 		// de-punify it just in case.
    168 		d, err := util.DePunify(a.Domain)
    169 		if err != nil {
    170 			return nil, fmt.Errorf("AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err)
    171 		}
    172 
    173 		acct = a.Username + "@" + d
    174 	} else {
    175 		// This is a local account, try to
    176 		// fetch more info. Skip for instance
    177 		// accounts since they have no user.
    178 		if !a.IsInstance() {
    179 			user, err := c.db.GetUserByAccountID(ctx, a.ID)
    180 			if err != nil {
    181 				return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err)
    182 			}
    183 
    184 			switch {
    185 			case *user.Admin:
    186 				role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin}
    187 			case *user.Moderator:
    188 				role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator}
    189 			default:
    190 				role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
    191 			}
    192 		}
    193 
    194 		acct = a.Username // omit domain
    195 	}
    196 
    197 	// Remaining properties are simple and
    198 	// can be populated directly below.
    199 
    200 	accountFrontend := &apimodel.Account{
    201 		ID:             a.ID,
    202 		Username:       a.Username,
    203 		Acct:           acct,
    204 		DisplayName:    a.DisplayName,
    205 		Locked:         *a.Locked,
    206 		Discoverable:   *a.Discoverable,
    207 		Bot:            *a.Bot,
    208 		CreatedAt:      util.FormatISO8601(a.CreatedAt),
    209 		Note:           a.Note,
    210 		URL:            a.URL,
    211 		Avatar:         aviURL,
    212 		AvatarStatic:   aviURLStatic,
    213 		Header:         headerURL,
    214 		HeaderStatic:   headerURLStatic,
    215 		FollowersCount: followersCount,
    216 		FollowingCount: followingCount,
    217 		StatusesCount:  statusesCount,
    218 		LastStatusAt:   lastStatusAt,
    219 		Emojis:         apiEmojis,
    220 		Fields:         fields,
    221 		Suspended:      !a.SuspendedAt.IsZero(),
    222 		CustomCSS:      a.CustomCSS,
    223 		EnableRSS:      *a.EnableRSS,
    224 		Role:           role,
    225 	}
    226 
    227 	// Bodge default avatar + header in,
    228 	// if we didn't have one already.
    229 	c.ensureAvatar(accountFrontend)
    230 	c.ensureHeader(accountFrontend)
    231 
    232 	return accountFrontend, nil
    233 }
    234 
    235 func (c *converter) fieldsToAPIFields(f []*gtsmodel.Field) []apimodel.Field {
    236 	fields := make([]apimodel.Field, len(f))
    237 
    238 	for i, field := range f {
    239 		mField := apimodel.Field{
    240 			Name:  field.Name,
    241 			Value: field.Value,
    242 		}
    243 
    244 		if !field.VerifiedAt.IsZero() {
    245 			mField.VerifiedAt = func() *string { s := util.FormatISO8601(field.VerifiedAt); return &s }()
    246 		}
    247 
    248 		fields[i] = mField
    249 	}
    250 
    251 	return fields
    252 }
    253 
    254 func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
    255 	var (
    256 		acct string
    257 		role *apimodel.AccountRole
    258 	)
    259 
    260 	if a.IsRemote() {
    261 		// Domain may be in Punycode,
    262 		// de-punify it just in case.
    263 		d, err := util.DePunify(a.Domain)
    264 		if err != nil {
    265 			return nil, fmt.Errorf("AccountToAPIAccountBlocked: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err)
    266 		}
    267 
    268 		acct = a.Username + "@" + d
    269 	} else {
    270 		// This is a local account, try to
    271 		// fetch more info. Skip for instance
    272 		// accounts since they have no user.
    273 		if !a.IsInstance() {
    274 			user, err := c.db.GetUserByAccountID(ctx, a.ID)
    275 			if err != nil {
    276 				return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err)
    277 			}
    278 
    279 			switch {
    280 			case *user.Admin:
    281 				role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin}
    282 			case *user.Moderator:
    283 				role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator}
    284 			default:
    285 				role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
    286 			}
    287 		}
    288 
    289 		acct = a.Username // omit domain
    290 	}
    291 
    292 	return &apimodel.Account{
    293 		ID:          a.ID,
    294 		Username:    a.Username,
    295 		Acct:        acct,
    296 		DisplayName: a.DisplayName,
    297 		Bot:         *a.Bot,
    298 		CreatedAt:   util.FormatISO8601(a.CreatedAt),
    299 		URL:         a.URL,
    300 		Suspended:   !a.SuspendedAt.IsZero(),
    301 		Role:        role,
    302 	}, nil
    303 }
    304 
    305 func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Account) (*apimodel.AdminAccountInfo, error) {
    306 	var (
    307 		email                  string
    308 		ip                     *string
    309 		domain                 *string
    310 		locale                 string
    311 		confirmed              bool
    312 		inviteRequest          *string
    313 		approved               bool
    314 		disabled               bool
    315 		role                   = apimodel.AccountRole{Name: apimodel.AccountRoleUser} // assume user by default
    316 		createdByApplicationID string
    317 	)
    318 
    319 	if a.IsRemote() {
    320 		// Domain may be in Punycode,
    321 		// de-punify it just in case.
    322 		d, err := util.DePunify(a.Domain)
    323 		if err != nil {
    324 			return nil, fmt.Errorf("AccountToAdminAPIAccount: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err)
    325 		}
    326 
    327 		domain = &d
    328 	} else if !a.IsInstance() {
    329 		// This is a local, non-instance
    330 		// acct; we can fetch more info.
    331 		user, err := c.db.GetUserByAccountID(ctx, a.ID)
    332 		if err != nil {
    333 			return nil, fmt.Errorf("AccountToAdminAPIAccount: error getting user from database for account id %s: %w", a.ID, err)
    334 		}
    335 
    336 		if user.Email != "" {
    337 			email = user.Email
    338 		} else {
    339 			email = user.UnconfirmedEmail
    340 		}
    341 
    342 		if i := user.CurrentSignInIP.String(); i != "<nil>" {
    343 			ip = &i
    344 		}
    345 
    346 		locale = user.Locale
    347 		if user.Account.Reason != "" {
    348 			inviteRequest = &user.Account.Reason
    349 		}
    350 
    351 		if *user.Admin {
    352 			role.Name = apimodel.AccountRoleAdmin
    353 		} else if *user.Moderator {
    354 			role.Name = apimodel.AccountRoleModerator
    355 		}
    356 
    357 		confirmed = !user.ConfirmedAt.IsZero()
    358 		approved = *user.Approved
    359 		disabled = *user.Disabled
    360 		createdByApplicationID = user.CreatedByApplicationID
    361 	}
    362 
    363 	apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
    364 	if err != nil {
    365 		return nil, fmt.Errorf("AccountToAdminAPIAccount: error converting account to api account for account id %s: %w", a.ID, err)
    366 	}
    367 
    368 	return &apimodel.AdminAccountInfo{
    369 		ID:                     a.ID,
    370 		Username:               a.Username,
    371 		Domain:                 domain,
    372 		CreatedAt:              util.FormatISO8601(a.CreatedAt),
    373 		Email:                  email,
    374 		IP:                     ip,
    375 		IPs:                    []interface{}{}, // not implemented,
    376 		Locale:                 locale,
    377 		InviteRequest:          inviteRequest,
    378 		Role:                   role,
    379 		Confirmed:              confirmed,
    380 		Approved:               approved,
    381 		Disabled:               disabled,
    382 		Silenced:               !a.SilencedAt.IsZero(),
    383 		Suspended:              !a.SuspendedAt.IsZero(),
    384 		Account:                apiAccount,
    385 		CreatedByApplicationID: createdByApplicationID,
    386 		InvitedByAccountID:     "", // not implemented (yet)
    387 	}, nil
    388 }
    389 
    390 func (c *converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) {
    391 	return &apimodel.Application{
    392 		ID:           a.ID,
    393 		Name:         a.Name,
    394 		Website:      a.Website,
    395 		RedirectURI:  a.RedirectURI,
    396 		ClientID:     a.ClientID,
    397 		ClientSecret: a.ClientSecret,
    398 	}, nil
    399 }
    400 
    401 func (c *converter) AppToAPIAppPublic(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) {
    402 	return &apimodel.Application{
    403 		Name:    a.Name,
    404 		Website: a.Website,
    405 	}, nil
    406 }
    407 
    408 func (c *converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.MediaAttachment) (apimodel.Attachment, error) {
    409 	apiAttachment := apimodel.Attachment{
    410 		ID:         a.ID,
    411 		Type:       strings.ToLower(string(a.Type)),
    412 		TextURL:    a.URL,
    413 		PreviewURL: a.Thumbnail.URL,
    414 		Meta: apimodel.MediaMeta{
    415 			Original: apimodel.MediaDimensions{
    416 				Width:  a.FileMeta.Original.Width,
    417 				Height: a.FileMeta.Original.Height,
    418 			},
    419 			Small: apimodel.MediaDimensions{
    420 				Width:  a.FileMeta.Small.Width,
    421 				Height: a.FileMeta.Small.Height,
    422 				Size:   strconv.Itoa(a.FileMeta.Small.Width) + "x" + strconv.Itoa(a.FileMeta.Small.Height),
    423 				Aspect: float32(a.FileMeta.Small.Aspect),
    424 			},
    425 		},
    426 		Blurhash: a.Blurhash,
    427 	}
    428 
    429 	// nullable fields
    430 	if i := a.URL; i != "" {
    431 		apiAttachment.URL = &i
    432 	}
    433 
    434 	if i := a.RemoteURL; i != "" {
    435 		apiAttachment.RemoteURL = &i
    436 	}
    437 
    438 	if i := a.Thumbnail.RemoteURL; i != "" {
    439 		apiAttachment.PreviewRemoteURL = &i
    440 	}
    441 
    442 	if i := a.Description; i != "" {
    443 		apiAttachment.Description = &i
    444 	}
    445 
    446 	// type specific fields
    447 	switch a.Type {
    448 	case gtsmodel.FileTypeImage:
    449 		apiAttachment.Meta.Original.Size = strconv.Itoa(a.FileMeta.Original.Width) + "x" + strconv.Itoa(a.FileMeta.Original.Height)
    450 		apiAttachment.Meta.Original.Aspect = float32(a.FileMeta.Original.Aspect)
    451 		apiAttachment.Meta.Focus = &apimodel.MediaFocus{
    452 			X: a.FileMeta.Focus.X,
    453 			Y: a.FileMeta.Focus.Y,
    454 		}
    455 	case gtsmodel.FileTypeVideo:
    456 		if i := a.FileMeta.Original.Duration; i != nil {
    457 			apiAttachment.Meta.Original.Duration = *i
    458 		}
    459 
    460 		if i := a.FileMeta.Original.Framerate; i != nil {
    461 			// the masto api expects this as a string in
    462 			// the format `integer/1`, so 30fps is `30/1`
    463 			round := math.Round(float64(*i))
    464 			fr := strconv.FormatInt(int64(round), 10)
    465 			apiAttachment.Meta.Original.FrameRate = fr + "/1"
    466 		}
    467 
    468 		if i := a.FileMeta.Original.Bitrate; i != nil {
    469 			apiAttachment.Meta.Original.Bitrate = int(*i)
    470 		}
    471 	}
    472 
    473 	return apiAttachment, nil
    474 }
    475 
    476 func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (apimodel.Mention, error) {
    477 	if m.TargetAccount == nil {
    478 		targetAccount, err := c.db.GetAccountByID(ctx, m.TargetAccountID)
    479 		if err != nil {
    480 			return apimodel.Mention{}, err
    481 		}
    482 		m.TargetAccount = targetAccount
    483 	}
    484 
    485 	var acct string
    486 	if m.TargetAccount.IsLocal() {
    487 		acct = m.TargetAccount.Username
    488 	} else {
    489 		// Domain may be in Punycode,
    490 		// de-punify it just in case.
    491 		d, err := util.DePunify(m.TargetAccount.Domain)
    492 		if err != nil {
    493 			err = fmt.Errorf("MentionToAPIMention: error de-punifying domain %s for account id %s: %w", m.TargetAccount.Domain, m.TargetAccountID, err)
    494 			return apimodel.Mention{}, err
    495 		}
    496 
    497 		acct = m.TargetAccount.Username + "@" + d
    498 	}
    499 
    500 	return apimodel.Mention{
    501 		ID:       m.TargetAccount.ID,
    502 		Username: m.TargetAccount.Username,
    503 		URL:      m.TargetAccount.URL,
    504 		Acct:     acct,
    505 	}, nil
    506 }
    507 
    508 func (c *converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (apimodel.Emoji, error) {
    509 	var category string
    510 	if e.CategoryID != "" {
    511 		if e.Category == nil {
    512 			var err error
    513 			e.Category, err = c.db.GetEmojiCategory(ctx, e.CategoryID)
    514 			if err != nil {
    515 				return apimodel.Emoji{}, err
    516 			}
    517 		}
    518 		category = e.Category.Name
    519 	}
    520 
    521 	return apimodel.Emoji{
    522 		Shortcode:       e.Shortcode,
    523 		URL:             e.ImageURL,
    524 		StaticURL:       e.ImageStaticURL,
    525 		VisibleInPicker: *e.VisibleInPicker,
    526 		Category:        category,
    527 	}, nil
    528 }
    529 
    530 func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*apimodel.AdminEmoji, error) {
    531 	emoji, err := c.EmojiToAPIEmoji(ctx, e)
    532 	if err != nil {
    533 		return nil, err
    534 	}
    535 
    536 	if e.Domain != "" {
    537 		// Domain may be in Punycode,
    538 		// de-punify it just in case.
    539 		var err error
    540 		e.Domain, err = util.DePunify(e.Domain)
    541 		if err != nil {
    542 			err = fmt.Errorf("EmojiToAdminAPIEmoji: error de-punifying domain %s for emoji id %s: %w", e.Domain, e.ID, err)
    543 			return nil, err
    544 		}
    545 	}
    546 
    547 	return &apimodel.AdminEmoji{
    548 		Emoji:         emoji,
    549 		ID:            e.ID,
    550 		Disabled:      *e.Disabled,
    551 		Domain:        e.Domain,
    552 		UpdatedAt:     util.FormatISO8601(e.UpdatedAt),
    553 		TotalFileSize: e.ImageFileSize + e.ImageStaticFileSize,
    554 		ContentType:   e.ImageContentType,
    555 		URI:           e.URI,
    556 	}, nil
    557 }
    558 
    559 func (c *converter) EmojiCategoryToAPIEmojiCategory(ctx context.Context, category *gtsmodel.EmojiCategory) (*apimodel.EmojiCategory, error) {
    560 	return &apimodel.EmojiCategory{
    561 		ID:   category.ID,
    562 		Name: category.Name,
    563 	}, nil
    564 }
    565 
    566 func (c *converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (apimodel.Tag, error) {
    567 	return apimodel.Tag{
    568 		Name: t.Name,
    569 		URL:  t.URL,
    570 	}, nil
    571 }
    572 
    573 func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) {
    574 	if err := c.db.PopulateStatus(ctx, s); err != nil {
    575 		// Ensure author account present + correct;
    576 		// can't really go further without this!
    577 		if s.Account == nil {
    578 			return nil, fmt.Errorf("error(s) populating status, cannot continue: %w", err)
    579 		}
    580 
    581 		log.Errorf(ctx, "error(s) populating status, will continue: %v", err)
    582 	}
    583 
    584 	apiAuthorAccount, err := c.AccountToAPIAccountPublic(ctx, s.Account)
    585 	if err != nil {
    586 		return nil, fmt.Errorf("error converting status author: %w", err)
    587 	}
    588 
    589 	repliesCount, err := c.db.CountStatusReplies(ctx, s)
    590 	if err != nil {
    591 		return nil, fmt.Errorf("error counting replies: %w", err)
    592 	}
    593 
    594 	reblogsCount, err := c.db.CountStatusReblogs(ctx, s)
    595 	if err != nil {
    596 		return nil, fmt.Errorf("error counting reblogs: %w", err)
    597 	}
    598 
    599 	favesCount, err := c.db.CountStatusFaves(ctx, s)
    600 	if err != nil {
    601 		return nil, fmt.Errorf("error counting faves: %w", err)
    602 	}
    603 
    604 	interacts, err := c.interactionsWithStatusForAccount(ctx, s, requestingAccount)
    605 	if err != nil {
    606 		log.Errorf(ctx, "error getting interactions for status %s for account %s: %v", s.ID, requestingAccount.ID, err)
    607 
    608 		// Ensure a non nil object
    609 		interacts = &statusInteractions{}
    610 	}
    611 
    612 	apiAttachments, err := c.convertAttachmentsToAPIAttachments(ctx, s.Attachments, s.AttachmentIDs)
    613 	if err != nil {
    614 		log.Errorf(ctx, "error converting status attachments: %v", err)
    615 	}
    616 
    617 	apiMentions, err := c.convertMentionsToAPIMentions(ctx, s.Mentions, s.MentionIDs)
    618 	if err != nil {
    619 		log.Errorf(ctx, "error converting status mentions: %v", err)
    620 	}
    621 
    622 	apiTags, err := c.convertTagsToAPITags(ctx, s.Tags, s.TagIDs)
    623 	if err != nil {
    624 		log.Errorf(ctx, "error converting status tags: %v", err)
    625 	}
    626 
    627 	apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, s.Emojis, s.EmojiIDs)
    628 	if err != nil {
    629 		log.Errorf(ctx, "error converting status emojis: %v", err)
    630 	}
    631 
    632 	apiStatus := &apimodel.Status{
    633 		ID:                 s.ID,
    634 		CreatedAt:          util.FormatISO8601(s.CreatedAt),
    635 		InReplyToID:        nil,
    636 		InReplyToAccountID: nil,
    637 		Sensitive:          *s.Sensitive,
    638 		SpoilerText:        s.ContentWarning,
    639 		Visibility:         c.VisToAPIVis(ctx, s.Visibility),
    640 		Language:           nil,
    641 		URI:                s.URI,
    642 		URL:                s.URL,
    643 		RepliesCount:       repliesCount,
    644 		ReblogsCount:       reblogsCount,
    645 		FavouritesCount:    favesCount,
    646 		Favourited:         interacts.Faved,
    647 		Bookmarked:         interacts.Bookmarked,
    648 		Muted:              interacts.Muted,
    649 		Reblogged:          interacts.Reblogged,
    650 		Pinned:             interacts.Pinned,
    651 		Content:            s.Content,
    652 		Reblog:             nil,
    653 		Application:        nil,
    654 		Account:            apiAuthorAccount,
    655 		MediaAttachments:   apiAttachments,
    656 		Mentions:           apiMentions,
    657 		Tags:               apiTags,
    658 		Emojis:             apiEmojis,
    659 		Card:               nil, // TODO: implement cards
    660 		Poll:               nil, // TODO: implement polls
    661 		Text:               s.Text,
    662 	}
    663 
    664 	// Nullable fields.
    665 
    666 	if s.InReplyToID != "" {
    667 		apiStatus.InReplyToID = func() *string { i := s.InReplyToID; return &i }()
    668 	}
    669 
    670 	if s.InReplyToAccountID != "" {
    671 		apiStatus.InReplyToAccountID = func() *string { i := s.InReplyToAccountID; return &i }()
    672 	}
    673 
    674 	if s.Language != "" {
    675 		apiStatus.Language = func() *string { i := s.Language; return &i }()
    676 	}
    677 
    678 	if s.BoostOf != nil {
    679 		apiBoostOf, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount)
    680 		if err != nil {
    681 			return nil, fmt.Errorf("error converting boosted status: %w", err)
    682 		}
    683 
    684 		apiStatus.Reblog = &apimodel.StatusReblogged{Status: apiBoostOf}
    685 	}
    686 
    687 	if appID := s.CreatedWithApplicationID; appID != "" {
    688 		app := &gtsmodel.Application{}
    689 		if err := c.db.GetByID(ctx, appID, app); err != nil {
    690 			return nil, fmt.Errorf("error getting application %s: %w", appID, err)
    691 		}
    692 
    693 		apiApp, err := c.AppToAPIAppPublic(ctx, app)
    694 		if err != nil {
    695 			return nil, fmt.Errorf("error converting application %s: %w", appID, err)
    696 		}
    697 
    698 		apiStatus.Application = apiApp
    699 	}
    700 
    701 	// Normalization.
    702 
    703 	if s.URL == "" {
    704 		// URL was empty for some reason;
    705 		// provide AP URI as fallback.
    706 		s.URL = s.URI
    707 	}
    708 
    709 	return apiStatus, nil
    710 }
    711 
    712 // VisToapi converts a gts visibility into its api equivalent
    713 func (c *converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apimodel.Visibility {
    714 	switch m {
    715 	case gtsmodel.VisibilityPublic:
    716 		return apimodel.VisibilityPublic
    717 	case gtsmodel.VisibilityUnlocked:
    718 		return apimodel.VisibilityUnlisted
    719 	case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
    720 		return apimodel.VisibilityPrivate
    721 	case gtsmodel.VisibilityDirect:
    722 		return apimodel.VisibilityDirect
    723 	}
    724 	return ""
    725 }
    726 
    727 func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error) {
    728 	instance := &apimodel.InstanceV1{
    729 		URI:              i.URI,
    730 		AccountDomain:    config.GetAccountDomain(),
    731 		Title:            i.Title,
    732 		Description:      i.Description,
    733 		ShortDescription: i.ShortDescription,
    734 		Email:            i.ContactEmail,
    735 		Version:          config.GetSoftwareVersion(),
    736 		Languages:        []string{}, // todo: not supported yet
    737 		Registrations:    config.GetAccountsRegistrationOpen(),
    738 		ApprovalRequired: config.GetAccountsApprovalRequired(),
    739 		InvitesEnabled:   false, // todo: not supported yet
    740 		MaxTootChars:     uint(config.GetStatusesMaxChars()),
    741 	}
    742 
    743 	// configuration
    744 	instance.Configuration.Statuses.MaxCharacters = config.GetStatusesMaxChars()
    745 	instance.Configuration.Statuses.MaxMediaAttachments = config.GetStatusesMediaMaxFiles()
    746 	instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
    747 	instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes
    748 	instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes
    749 	instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaImageMaxSize())
    750 	instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit
    751 	instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaVideoMaxSize())
    752 	instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit
    753 	instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit
    754 	instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions()
    755 	instance.Configuration.Polls.MaxCharactersPerOption = config.GetStatusesPollOptionMaxChars()
    756 	instance.Configuration.Polls.MinExpiration = instancePollsMinExpiration
    757 	instance.Configuration.Polls.MaxExpiration = instancePollsMaxExpiration
    758 	instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS()
    759 	instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags
    760 	instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields
    761 	instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize())
    762 
    763 	// URLs
    764 	instance.URLs.StreamingAPI = "wss://" + i.Domain
    765 
    766 	// statistics
    767 	stats := make(map[string]int, 3)
    768 	userCount, err := c.db.CountInstanceUsers(ctx, i.Domain)
    769 	if err != nil {
    770 		return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting counting instance users: %w", err)
    771 	}
    772 	stats["user_count"] = userCount
    773 
    774 	statusCount, err := c.db.CountInstanceStatuses(ctx, i.Domain)
    775 	if err != nil {
    776 		return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting counting instance statuses: %w", err)
    777 	}
    778 	stats["status_count"] = statusCount
    779 
    780 	domainCount, err := c.db.CountInstanceDomains(ctx, i.Domain)
    781 	if err != nil {
    782 		return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting counting instance domains: %w", err)
    783 	}
    784 	stats["domain_count"] = domainCount
    785 	instance.Stats = stats
    786 
    787 	// thumbnail
    788 	iAccount, err := c.db.GetInstanceAccount(ctx, "")
    789 	if err != nil {
    790 		return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting instance account: %w", err)
    791 	}
    792 
    793 	if iAccount.AvatarMediaAttachmentID != "" {
    794 		if iAccount.AvatarMediaAttachment == nil {
    795 			avi, err := c.db.GetAttachmentByID(ctx, iAccount.AvatarMediaAttachmentID)
    796 			if err != nil {
    797 				return nil, fmt.Errorf("InstanceToAPIInstance: error getting instance avatar attachment with id %s: %w", iAccount.AvatarMediaAttachmentID, err)
    798 			}
    799 			iAccount.AvatarMediaAttachment = avi
    800 		}
    801 
    802 		instance.Thumbnail = iAccount.AvatarMediaAttachment.URL
    803 		instance.ThumbnailType = iAccount.AvatarMediaAttachment.File.ContentType
    804 		instance.ThumbnailDescription = iAccount.AvatarMediaAttachment.Description
    805 	} else {
    806 		instance.Thumbnail = config.GetProtocol() + "://" + i.Domain + "/assets/logo.png" // default thumb
    807 	}
    808 
    809 	// contact account
    810 	if i.ContactAccountID != "" {
    811 		if i.ContactAccount == nil {
    812 			contactAccount, err := c.db.GetAccountByID(ctx, i.ContactAccountID)
    813 			if err != nil {
    814 				return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting instance contact account %s: %w", i.ContactAccountID, err)
    815 			}
    816 			i.ContactAccount = contactAccount
    817 		}
    818 
    819 		account, err := c.AccountToAPIAccountPublic(ctx, i.ContactAccount)
    820 		if err != nil {
    821 			return nil, fmt.Errorf("InstanceToAPIV1Instance: error converting instance contact account %s: %w", i.ContactAccountID, err)
    822 		}
    823 		instance.ContactAccount = account
    824 	}
    825 
    826 	return instance, nil
    827 }
    828 
    829 func (c *converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV2, error) {
    830 	instance := &apimodel.InstanceV2{
    831 		Domain:        i.Domain,
    832 		AccountDomain: config.GetAccountDomain(),
    833 		Title:         i.Title,
    834 		Version:       config.GetSoftwareVersion(),
    835 		SourceURL:     instanceSourceURL,
    836 		Description:   i.Description,
    837 		Usage:         apimodel.InstanceV2Usage{}, // todo: not implemented
    838 		Languages:     []string{},                 // todo: not implemented
    839 		Rules:         []interface{}{},            // todo: not implemented
    840 	}
    841 
    842 	// thumbnail
    843 	thumbnail := apimodel.InstanceV2Thumbnail{}
    844 
    845 	iAccount, err := c.db.GetInstanceAccount(ctx, "")
    846 	if err != nil {
    847 		return nil, fmt.Errorf("InstanceToAPIV2Instance: db error getting instance account: %w", err)
    848 	}
    849 
    850 	if iAccount.AvatarMediaAttachmentID != "" {
    851 		if iAccount.AvatarMediaAttachment == nil {
    852 			avi, err := c.db.GetAttachmentByID(ctx, iAccount.AvatarMediaAttachmentID)
    853 			if err != nil {
    854 				return nil, fmt.Errorf("InstanceToAPIV2Instance: error getting instance avatar attachment with id %s: %w", iAccount.AvatarMediaAttachmentID, err)
    855 			}
    856 			iAccount.AvatarMediaAttachment = avi
    857 		}
    858 
    859 		thumbnail.URL = iAccount.AvatarMediaAttachment.URL
    860 		thumbnail.Type = iAccount.AvatarMediaAttachment.File.ContentType
    861 		thumbnail.Description = iAccount.AvatarMediaAttachment.Description
    862 		thumbnail.Blurhash = iAccount.AvatarMediaAttachment.Blurhash
    863 	} else {
    864 		thumbnail.URL = config.GetProtocol() + "://" + i.Domain + "/assets/logo.png" // default thumb
    865 	}
    866 
    867 	instance.Thumbnail = thumbnail
    868 
    869 	// configuration
    870 	instance.Configuration.URLs.Streaming = "wss://" + i.Domain
    871 	instance.Configuration.Statuses.MaxCharacters = config.GetStatusesMaxChars()
    872 	instance.Configuration.Statuses.MaxMediaAttachments = config.GetStatusesMediaMaxFiles()
    873 	instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
    874 	instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes
    875 	instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes
    876 	instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaImageMaxSize())
    877 	instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit
    878 	instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaVideoMaxSize())
    879 	instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit
    880 	instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit
    881 	instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions()
    882 	instance.Configuration.Polls.MaxCharactersPerOption = config.GetStatusesPollOptionMaxChars()
    883 	instance.Configuration.Polls.MinExpiration = instancePollsMinExpiration
    884 	instance.Configuration.Polls.MaxExpiration = instancePollsMaxExpiration
    885 	instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS()
    886 	instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags
    887 	instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields
    888 	instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize())
    889 
    890 	// registrations
    891 	instance.Registrations.Enabled = config.GetAccountsRegistrationOpen()
    892 	instance.Registrations.ApprovalRequired = config.GetAccountsApprovalRequired()
    893 	instance.Registrations.Message = nil // todo: not implemented
    894 
    895 	// contact
    896 	instance.Contact.Email = i.ContactEmail
    897 	if i.ContactAccountID != "" {
    898 		if i.ContactAccount == nil {
    899 			contactAccount, err := c.db.GetAccountByID(ctx, i.ContactAccountID)
    900 			if err != nil {
    901 				return nil, fmt.Errorf("InstanceToAPIV2Instance: db error getting instance contact account %s: %w", i.ContactAccountID, err)
    902 			}
    903 			i.ContactAccount = contactAccount
    904 		}
    905 
    906 		account, err := c.AccountToAPIAccountPublic(ctx, i.ContactAccount)
    907 		if err != nil {
    908 			return nil, fmt.Errorf("InstanceToAPIV2Instance: error converting instance contact account %s: %w", i.ContactAccountID, err)
    909 		}
    910 		instance.Contact.Account = account
    911 	}
    912 
    913 	return instance, nil
    914 }
    915 
    916 func (c *converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error) {
    917 	return &apimodel.Relationship{
    918 		ID:                  r.ID,
    919 		Following:           r.Following,
    920 		ShowingReblogs:      r.ShowingReblogs,
    921 		Notifying:           r.Notifying,
    922 		FollowedBy:          r.FollowedBy,
    923 		Blocking:            r.Blocking,
    924 		BlockedBy:           r.BlockedBy,
    925 		Muting:              r.Muting,
    926 		MutingNotifications: r.MutingNotifications,
    927 		Requested:           r.Requested,
    928 		DomainBlocking:      r.DomainBlocking,
    929 		Endorsed:            r.Endorsed,
    930 		Note:                r.Note,
    931 	}, nil
    932 }
    933 
    934 func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*apimodel.Notification, error) {
    935 	if n.TargetAccount == nil {
    936 		tAccount, err := c.db.GetAccountByID(ctx, n.TargetAccountID)
    937 		if err != nil {
    938 			return nil, fmt.Errorf("NotificationToapi: error getting target account with id %s from the db: %s", n.TargetAccountID, err)
    939 		}
    940 		n.TargetAccount = tAccount
    941 	}
    942 
    943 	if n.OriginAccount == nil {
    944 		ogAccount, err := c.db.GetAccountByID(ctx, n.OriginAccountID)
    945 		if err != nil {
    946 			return nil, fmt.Errorf("NotificationToapi: error getting origin account with id %s from the db: %s", n.OriginAccountID, err)
    947 		}
    948 		n.OriginAccount = ogAccount
    949 	}
    950 
    951 	apiAccount, err := c.AccountToAPIAccountPublic(ctx, n.OriginAccount)
    952 	if err != nil {
    953 		return nil, fmt.Errorf("NotificationToapi: error converting account to api: %s", err)
    954 	}
    955 
    956 	var apiStatus *apimodel.Status
    957 	if n.StatusID != "" {
    958 		if n.Status == nil {
    959 			status, err := c.db.GetStatusByID(ctx, n.StatusID)
    960 			if err != nil {
    961 				return nil, fmt.Errorf("NotificationToapi: error getting status with id %s from the db: %s", n.StatusID, err)
    962 			}
    963 			n.Status = status
    964 		}
    965 
    966 		if n.Status.Account == nil {
    967 			if n.Status.AccountID == n.TargetAccount.ID {
    968 				n.Status.Account = n.TargetAccount
    969 			} else if n.Status.AccountID == n.OriginAccount.ID {
    970 				n.Status.Account = n.OriginAccount
    971 			}
    972 		}
    973 
    974 		var err error
    975 		apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount)
    976 		if err != nil {
    977 			return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err)
    978 		}
    979 	}
    980 
    981 	if apiStatus != nil && apiStatus.Reblog != nil {
    982 		// use the actual reblog status for the notifications endpoint
    983 		apiStatus = apiStatus.Reblog.Status
    984 	}
    985 
    986 	return &apimodel.Notification{
    987 		ID:        n.ID,
    988 		Type:      string(n.NotificationType),
    989 		CreatedAt: util.FormatISO8601(n.CreatedAt),
    990 		Account:   apiAccount,
    991 		Status:    apiStatus,
    992 	}, nil
    993 }
    994 
    995 func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) {
    996 	// Domain may be in Punycode,
    997 	// de-punify it just in case.
    998 	d, err := util.DePunify(b.Domain)
    999 	if err != nil {
   1000 		return nil, fmt.Errorf("DomainBlockToAPIDomainBlock: error de-punifying domain %s: %w", b.Domain, err)
   1001 	}
   1002 
   1003 	domainBlock := &apimodel.DomainBlock{
   1004 		Domain: apimodel.Domain{
   1005 			Domain:        d,
   1006 			PublicComment: b.PublicComment,
   1007 		},
   1008 	}
   1009 
   1010 	// if we're exporting a domain block, return it with minimal information attached
   1011 	if !export {
   1012 		domainBlock.ID = b.ID
   1013 		domainBlock.Obfuscate = *b.Obfuscate
   1014 		domainBlock.PrivateComment = b.PrivateComment
   1015 		domainBlock.SubscriptionID = b.SubscriptionID
   1016 		domainBlock.CreatedBy = b.CreatedByAccountID
   1017 		domainBlock.CreatedAt = util.FormatISO8601(b.CreatedAt)
   1018 	}
   1019 
   1020 	return domainBlock, nil
   1021 }
   1022 
   1023 func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error) {
   1024 	report := &apimodel.Report{
   1025 		ID:          r.ID,
   1026 		CreatedAt:   util.FormatISO8601(r.CreatedAt),
   1027 		ActionTaken: !r.ActionTakenAt.IsZero(),
   1028 		Category:    "other", // todo: only support default 'other' category right now
   1029 		Comment:     r.Comment,
   1030 		Forwarded:   *r.Forwarded,
   1031 		StatusIDs:   r.StatusIDs,
   1032 		RuleIDs:     []int{}, // todo: not supported yet
   1033 	}
   1034 
   1035 	if !r.ActionTakenAt.IsZero() {
   1036 		actionTakenAt := util.FormatISO8601(r.ActionTakenAt)
   1037 		report.ActionTakenAt = &actionTakenAt
   1038 	}
   1039 
   1040 	if actionComment := r.ActionTaken; actionComment != "" {
   1041 		report.ActionTakenComment = &actionComment
   1042 	}
   1043 
   1044 	if r.TargetAccount == nil {
   1045 		tAccount, err := c.db.GetAccountByID(ctx, r.TargetAccountID)
   1046 		if err != nil {
   1047 			return nil, fmt.Errorf("ReportToAPIReport: error getting target account with id %s from the db: %s", r.TargetAccountID, err)
   1048 		}
   1049 		r.TargetAccount = tAccount
   1050 	}
   1051 
   1052 	apiAccount, err := c.AccountToAPIAccountPublic(ctx, r.TargetAccount)
   1053 	if err != nil {
   1054 		return nil, fmt.Errorf("ReportToAPIReport: error converting target account to api: %s", err)
   1055 	}
   1056 	report.TargetAccount = apiAccount
   1057 
   1058 	return report, nil
   1059 }
   1060 
   1061 func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Report, requestingAccount *gtsmodel.Account) (*apimodel.AdminReport, error) {
   1062 	var (
   1063 		err                  error
   1064 		actionTakenAt        *string
   1065 		actionTakenComment   *string
   1066 		actionTakenByAccount *apimodel.AdminAccountInfo
   1067 	)
   1068 
   1069 	if !r.ActionTakenAt.IsZero() {
   1070 		ata := util.FormatISO8601(r.ActionTakenAt)
   1071 		actionTakenAt = &ata
   1072 	}
   1073 
   1074 	if r.Account == nil {
   1075 		r.Account, err = c.db.GetAccountByID(ctx, r.AccountID)
   1076 		if err != nil {
   1077 			return nil, fmt.Errorf("ReportToAdminAPIReport: error getting account with id %s from the db: %w", r.AccountID, err)
   1078 		}
   1079 	}
   1080 	account, err := c.AccountToAdminAPIAccount(ctx, r.Account)
   1081 	if err != nil {
   1082 		return nil, fmt.Errorf("ReportToAdminAPIReport: error converting account with id %s to adminAPIAccount: %w", r.AccountID, err)
   1083 	}
   1084 
   1085 	if r.TargetAccount == nil {
   1086 		r.TargetAccount, err = c.db.GetAccountByID(ctx, r.TargetAccountID)
   1087 		if err != nil {
   1088 			return nil, fmt.Errorf("ReportToAdminAPIReport: error getting target account with id %s from the db: %w", r.TargetAccountID, err)
   1089 		}
   1090 	}
   1091 	targetAccount, err := c.AccountToAdminAPIAccount(ctx, r.TargetAccount)
   1092 	if err != nil {
   1093 		return nil, fmt.Errorf("ReportToAdminAPIReport: error converting target account with id %s to adminAPIAccount: %w", r.TargetAccountID, err)
   1094 	}
   1095 
   1096 	if r.ActionTakenByAccountID != "" {
   1097 		if r.ActionTakenByAccount == nil {
   1098 			r.ActionTakenByAccount, err = c.db.GetAccountByID(ctx, r.ActionTakenByAccountID)
   1099 			if err != nil {
   1100 				return nil, fmt.Errorf("ReportToAdminAPIReport: error getting action taken by account with id %s from the db: %w", r.ActionTakenByAccountID, err)
   1101 			}
   1102 		}
   1103 
   1104 		actionTakenByAccount, err = c.AccountToAdminAPIAccount(ctx, r.ActionTakenByAccount)
   1105 		if err != nil {
   1106 			return nil, fmt.Errorf("ReportToAdminAPIReport: error converting action taken by account with id %s to adminAPIAccount: %w", r.ActionTakenByAccountID, err)
   1107 		}
   1108 	}
   1109 
   1110 	statuses := make([]*apimodel.Status, 0, len(r.StatusIDs))
   1111 	if len(r.StatusIDs) != 0 && len(r.Statuses) == 0 {
   1112 		r.Statuses, err = c.db.GetStatuses(ctx, r.StatusIDs)
   1113 		if err != nil {
   1114 			return nil, fmt.Errorf("ReportToAdminAPIReport: error getting statuses from the db: %w", err)
   1115 		}
   1116 	}
   1117 	for _, s := range r.Statuses {
   1118 		status, err := c.StatusToAPIStatus(ctx, s, requestingAccount)
   1119 		if err != nil {
   1120 			return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
   1121 		}
   1122 		statuses = append(statuses, status)
   1123 	}
   1124 
   1125 	if ac := r.ActionTaken; ac != "" {
   1126 		actionTakenComment = &ac
   1127 	}
   1128 
   1129 	return &apimodel.AdminReport{
   1130 		ID:                   r.ID,
   1131 		ActionTaken:          !r.ActionTakenAt.IsZero(),
   1132 		ActionTakenAt:        actionTakenAt,
   1133 		Category:             "other", // todo: only support default 'other' category right now
   1134 		Comment:              r.Comment,
   1135 		Forwarded:            *r.Forwarded,
   1136 		CreatedAt:            util.FormatISO8601(r.CreatedAt),
   1137 		UpdatedAt:            util.FormatISO8601(r.UpdatedAt),
   1138 		Account:              account,
   1139 		TargetAccount:        targetAccount,
   1140 		AssignedAccount:      actionTakenByAccount,
   1141 		ActionTakenByAccount: actionTakenByAccount,
   1142 		ActionTakenComment:   actionTakenComment,
   1143 		Statuses:             statuses,
   1144 		Rules:                []interface{}{}, // not implemented
   1145 	}, nil
   1146 }
   1147 
   1148 func (c *converter) ListToAPIList(ctx context.Context, l *gtsmodel.List) (*apimodel.List, error) {
   1149 	return &apimodel.List{
   1150 		ID:            l.ID,
   1151 		Title:         l.Title,
   1152 		RepliesPolicy: string(l.RepliesPolicy),
   1153 	}, nil
   1154 }
   1155 
   1156 // convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied.
   1157 func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]apimodel.Attachment, error) {
   1158 	var errs gtserror.MultiError
   1159 
   1160 	if len(attachments) == 0 {
   1161 		// GTS model attachments were not populated
   1162 
   1163 		// Preallocate expected GTS slice
   1164 		attachments = make([]*gtsmodel.MediaAttachment, 0, len(attachmentIDs))
   1165 
   1166 		// Fetch GTS models for attachment IDs
   1167 		for _, id := range attachmentIDs {
   1168 			attachment, err := c.db.GetAttachmentByID(ctx, id)
   1169 			if err != nil {
   1170 				errs.Appendf("error fetching attachment %s from database: %v", id, err)
   1171 				continue
   1172 			}
   1173 			attachments = append(attachments, attachment)
   1174 		}
   1175 	}
   1176 
   1177 	// Preallocate expected frontend slice
   1178 	apiAttachments := make([]apimodel.Attachment, 0, len(attachments))
   1179 
   1180 	// Convert GTS models to frontend models
   1181 	for _, attachment := range attachments {
   1182 		apiAttachment, err := c.AttachmentToAPIAttachment(ctx, attachment)
   1183 		if err != nil {
   1184 			errs.Appendf("error converting attchment %s to api attachment: %v", attachment.ID, err)
   1185 			continue
   1186 		}
   1187 		apiAttachments = append(apiAttachments, apiAttachment)
   1188 	}
   1189 
   1190 	return apiAttachments, errs.Combine()
   1191 }
   1192 
   1193 // convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
   1194 func (c *converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsmodel.Emoji, emojiIDs []string) ([]apimodel.Emoji, error) {
   1195 	var errs gtserror.MultiError
   1196 
   1197 	if len(emojis) == 0 {
   1198 		// GTS model attachments were not populated
   1199 
   1200 		// Preallocate expected GTS slice
   1201 		emojis = make([]*gtsmodel.Emoji, 0, len(emojiIDs))
   1202 
   1203 		// Fetch GTS models for emoji IDs
   1204 		for _, id := range emojiIDs {
   1205 			emoji, err := c.db.GetEmojiByID(ctx, id)
   1206 			if err != nil {
   1207 				errs.Appendf("error fetching emoji %s from database: %v", id, err)
   1208 				continue
   1209 			}
   1210 			emojis = append(emojis, emoji)
   1211 		}
   1212 	}
   1213 
   1214 	// Preallocate expected frontend slice
   1215 	apiEmojis := make([]apimodel.Emoji, 0, len(emojis))
   1216 
   1217 	// Convert GTS models to frontend models
   1218 	for _, emoji := range emojis {
   1219 		apiEmoji, err := c.EmojiToAPIEmoji(ctx, emoji)
   1220 		if err != nil {
   1221 			errs.Appendf("error converting emoji %s to api emoji: %v", emoji.ID, err)
   1222 			continue
   1223 		}
   1224 		apiEmojis = append(apiEmojis, apiEmoji)
   1225 	}
   1226 
   1227 	return apiEmojis, errs.Combine()
   1228 }
   1229 
   1230 // convertMentionsToAPIMentions will convert a slice of GTS model mentions to frontend API model mentions, falling back to IDs if no GTS models supplied.
   1231 func (c *converter) convertMentionsToAPIMentions(ctx context.Context, mentions []*gtsmodel.Mention, mentionIDs []string) ([]apimodel.Mention, error) {
   1232 	var errs gtserror.MultiError
   1233 
   1234 	if len(mentions) == 0 {
   1235 		var err error
   1236 
   1237 		// GTS model mentions were not populated
   1238 		//
   1239 		// Fetch GTS models for mention IDs
   1240 		mentions, err = c.db.GetMentions(ctx, mentionIDs)
   1241 		if err != nil {
   1242 			errs.Appendf("error fetching mentions from database: %v", err)
   1243 		}
   1244 	}
   1245 
   1246 	// Preallocate expected frontend slice
   1247 	apiMentions := make([]apimodel.Mention, 0, len(mentions))
   1248 
   1249 	// Convert GTS models to frontend models
   1250 	for _, mention := range mentions {
   1251 		apiMention, err := c.MentionToAPIMention(ctx, mention)
   1252 		if err != nil {
   1253 			errs.Appendf("error converting mention %s to api mention: %v", mention.ID, err)
   1254 			continue
   1255 		}
   1256 		apiMentions = append(apiMentions, apiMention)
   1257 	}
   1258 
   1259 	return apiMentions, errs.Combine()
   1260 }
   1261 
   1262 // convertTagsToAPITags will convert a slice of GTS model tags to frontend API model tags, falling back to IDs if no GTS models supplied.
   1263 func (c *converter) convertTagsToAPITags(ctx context.Context, tags []*gtsmodel.Tag, tagIDs []string) ([]apimodel.Tag, error) {
   1264 	var errs gtserror.MultiError
   1265 
   1266 	if len(tags) == 0 {
   1267 		// GTS model tags were not populated
   1268 
   1269 		// Preallocate expected GTS slice
   1270 		tags = make([]*gtsmodel.Tag, 0, len(tagIDs))
   1271 
   1272 		// Fetch GTS models for tag IDs
   1273 		for _, id := range tagIDs {
   1274 			tag := new(gtsmodel.Tag)
   1275 			if err := c.db.GetByID(ctx, id, tag); err != nil {
   1276 				errs.Appendf("error fetching tag %s from database: %v", id, err)
   1277 				continue
   1278 			}
   1279 			tags = append(tags, tag)
   1280 		}
   1281 	}
   1282 
   1283 	// Preallocate expected frontend slice
   1284 	apiTags := make([]apimodel.Tag, 0, len(tags))
   1285 
   1286 	// Convert GTS models to frontend models
   1287 	for _, tag := range tags {
   1288 		apiTag, err := c.TagToAPITag(ctx, tag)
   1289 		if err != nil {
   1290 			errs.Appendf("error converting tag %s to api tag: %v", tag.ID, err)
   1291 			continue
   1292 		}
   1293 		apiTags = append(apiTags, apiTag)
   1294 	}
   1295 
   1296 	return apiTags, errs.Combine()
   1297 }