2023-03-12 16:00:57 +01:00
// 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/>.
2021-04-19 19:42:19 +02:00
2021-05-08 14:25:55 +02:00
package typeutils
2021-04-19 19:42:19 +02:00
import (
2021-08-25 15:34:33 +02:00
"context"
2023-05-07 19:53:21 +02:00
"errors"
2021-04-19 19:42:19 +02:00
"fmt"
2022-12-22 11:48:28 +01:00
"math"
"strconv"
2021-05-17 19:06:58 +02:00
"strings"
2021-04-19 19:42:19 +02:00
2023-01-02 13:10:50 +01:00
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
2021-12-07 13:31:39 +01:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2023-05-07 19:53:21 +02:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2022-12-22 11:48:28 +01:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-05-08 14:25:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2022-07-19 10:47:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2022-06-26 10:58:45 +02:00
"github.com/superseriousbusiness/gotosocial/internal/media"
2022-05-24 18:21:27 +02:00
"github.com/superseriousbusiness/gotosocial/internal/util"
2021-04-19 19:42:19 +02:00
)
2022-09-08 12:36:42 +02:00
const (
instanceStatusesCharactersReservedPerURL = 25
instanceMediaAttachmentsImageMatrixLimit = 16777216 // width * height
instanceMediaAttachmentsVideoMatrixLimit = 16777216 // width * height
instanceMediaAttachmentsVideoFrameRateLimit = 60
instancePollsMinExpiration = 300 // seconds
instancePollsMaxExpiration = 2629746 // seconds
2023-02-02 14:08:13 +01:00
instanceAccountsMaxFeaturedTags = 10
2023-06-13 12:21:26 +02:00
instanceAccountsMaxProfileFields = 6 // FIXME: https://github.com/superseriousbusiness/gotosocial/issues/1876
2023-02-02 14:08:13 +01:00
instanceSourceURL = "https://github.com/superseriousbusiness/gotosocial"
2023-07-21 19:49:13 +02:00
instanceMastodonVersion = "3.5.3"
2022-09-08 12:36:42 +02:00
)
2023-03-02 12:06:40 +01:00
var instanceStatusesSupportedMimeTypes = [ ] string {
string ( apimodel . StatusContentTypePlain ) ,
string ( apimodel . StatusContentTypeMarkdown ) ,
}
2023-07-21 19:49:13 +02:00
func toMastodonVersion ( in string ) string {
return instanceMastodonVersion + "+" + strings . ReplaceAll ( in , " " , "-" )
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) AccountToAPIAccountSensitive ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2021-04-19 19:42:19 +02:00
// we can build this sensitive account easily by first getting the public account....
2021-10-04 15:24:19 +02:00
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , a )
2021-04-19 19:42:19 +02:00
if err != nil {
return nil , err
}
// then adding the Source object to it...
// check pending follow requests aimed at this account
[performance] refactoring + add fave / follow / request / visibility caching (#1607)
* refactor visibility checking, add caching for visibility
* invalidate visibility cache items on account / status deletes
* fix requester ID passed to visibility cache nil ptr
* de-interface caches, fix home / public timeline caching + visibility
* finish adding code comments for visibility filter
* fix angry goconst linter warnings
* actually finish adding filter visibility code comments for timeline functions
* move home timeline status author check to after visibility
* remove now-unused code
* add more code comments
* add TODO code comment, update printed cache start names
* update printed cache names on stop
* start adding separate follow(request) delete db functions, add specific visibility cache tests
* add relationship type caching
* fix getting local account follows / followed-bys, other small codebase improvements
* simplify invalidation using cache hooks, add more GetAccountBy___() functions
* fix boosting to return 404 if not boostable but no error (to not leak status ID)
* remove dead code
* improved placement of cache invalidation
* update license headers
* add example follow, follow-request config entries
* add example visibility cache configuration to config file
* use specific PutFollowRequest() instead of just Put()
* add tests for all GetAccountBy()
* add GetBlockBy() tests
* update block to check primitive fields
* update and finish adding Get{Account,Block,Follow,FollowRequest}By() tests
* fix copy-pasted code
* update envparsing test
* whitespace
* fix bun struct tag
* add license header to gtscontext
* fix old license header
* improved error creation to not use fmt.Errorf() when not needed
* fix various rebase conflicts, fix account test
* remove commented-out code, fix-up mention caching
* fix mention select bun statement
* ensure mention target account populated, pass in context to customrenderer logging
* remove more uncommented code, fix typeutil test
* add statusfave database model caching
* add status fave cache configuration
* add status fave cache example config
* woops, catch missed error. nice catch linter!
* add back testrig panic on nil db
* update example configuration to match defaults, slight tweak to cache configuration defaults
* update envparsing test with new defaults
* fetch followingget to use the follow target account
* use accounnt.IsLocal() instead of empty domain check
* use constants for the cache visibility type check
* use bun.In() for notification type restriction in db query
* include replies when fetching PublicTimeline() (to account for single-author threads in Visibility{}.StatusPublicTimelineable())
* use bun query building for nested select statements to ensure working with postgres
* update public timeline future status checks to match visibility filter
* same as previous, for home timeline
* update public timeline tests to dynamically check for appropriate statuses
* migrate accounts to allow unique constraint on public_key
* provide minimal account with publicKey
---------
Signed-off-by: kim <grufwub@gmail.com>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2023-03-28 15:03:14 +02:00
frc , err := c . db . CountAccountFollowRequests ( ctx , a . ID )
2021-08-20 12:26:56 +02:00
if err != nil {
2023-03-20 19:10:08 +01:00
return nil , fmt . Errorf ( "error counting follow requests: %s" , err )
2021-04-19 19:42:19 +02:00
}
2023-03-02 12:06:40 +01:00
statusContentType := string ( apimodel . StatusContentTypeDefault )
if a . StatusContentType != "" {
statusContentType = a . StatusContentType
2022-08-06 12:09:21 +02:00
}
2023-01-02 13:10:50 +01:00
apiAccount . Source = & apimodel . Source {
2021-10-04 15:24:19 +02:00
Privacy : c . VisToAPIVis ( ctx , a . Privacy ) ,
2022-08-15 12:35:05 +02:00
Sensitive : * a . Sensitive ,
2021-04-19 19:42:19 +02:00
Language : a . Language ,
2023-03-02 12:06:40 +01:00
StatusContentType : statusContentType ,
2022-05-07 17:55:27 +02:00
Note : a . NoteRaw ,
2023-05-09 12:16:10 +02:00
Fields : c . fieldsToAPIFields ( a . FieldsRaw ) ,
2021-04-19 19:42:19 +02:00
FollowRequestsCount : frc ,
}
2021-10-04 15:24:19 +02:00
return apiAccount , nil
2021-04-19 19:42:19 +02:00
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) AccountToAPIAccountPublic ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2023-05-07 19:53:21 +02:00
if err := c . db . PopulateAccount ( ctx , a ) ; err != nil {
log . Errorf ( ctx , "error(s) populating account, will continue: %s" , err )
}
// Basic account stats:
// - Followers count
// - Following count
// - Statuses count
// - Last status time
[performance] refactoring + add fave / follow / request / visibility caching (#1607)
* refactor visibility checking, add caching for visibility
* invalidate visibility cache items on account / status deletes
* fix requester ID passed to visibility cache nil ptr
* de-interface caches, fix home / public timeline caching + visibility
* finish adding code comments for visibility filter
* fix angry goconst linter warnings
* actually finish adding filter visibility code comments for timeline functions
* move home timeline status author check to after visibility
* remove now-unused code
* add more code comments
* add TODO code comment, update printed cache start names
* update printed cache names on stop
* start adding separate follow(request) delete db functions, add specific visibility cache tests
* add relationship type caching
* fix getting local account follows / followed-bys, other small codebase improvements
* simplify invalidation using cache hooks, add more GetAccountBy___() functions
* fix boosting to return 404 if not boostable but no error (to not leak status ID)
* remove dead code
* improved placement of cache invalidation
* update license headers
* add example follow, follow-request config entries
* add example visibility cache configuration to config file
* use specific PutFollowRequest() instead of just Put()
* add tests for all GetAccountBy()
* add GetBlockBy() tests
* update block to check primitive fields
* update and finish adding Get{Account,Block,Follow,FollowRequest}By() tests
* fix copy-pasted code
* update envparsing test
* whitespace
* fix bun struct tag
* add license header to gtscontext
* fix old license header
* improved error creation to not use fmt.Errorf() when not needed
* fix various rebase conflicts, fix account test
* remove commented-out code, fix-up mention caching
* fix mention select bun statement
* ensure mention target account populated, pass in context to customrenderer logging
* remove more uncommented code, fix typeutil test
* add statusfave database model caching
* add status fave cache configuration
* add status fave cache example config
* woops, catch missed error. nice catch linter!
* add back testrig panic on nil db
* update example configuration to match defaults, slight tweak to cache configuration defaults
* update envparsing test with new defaults
* fetch followingget to use the follow target account
* use accounnt.IsLocal() instead of empty domain check
* use constants for the cache visibility type check
* use bun.In() for notification type restriction in db query
* include replies when fetching PublicTimeline() (to account for single-author threads in Visibility{}.StatusPublicTimelineable())
* use bun query building for nested select statements to ensure working with postgres
* update public timeline future status checks to match visibility filter
* same as previous, for home timeline
* update public timeline tests to dynamically check for appropriate statuses
* migrate accounts to allow unique constraint on public_key
* provide minimal account with publicKey
---------
Signed-off-by: kim <grufwub@gmail.com>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2023-03-28 15:03:14 +02:00
followersCount , err := c . db . CountAccountFollowers ( ctx , a . ID )
2023-05-07 19:53:21 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error counting followers: %w" , err )
2021-04-19 19:42:19 +02:00
}
[performance] refactoring + add fave / follow / request / visibility caching (#1607)
* refactor visibility checking, add caching for visibility
* invalidate visibility cache items on account / status deletes
* fix requester ID passed to visibility cache nil ptr
* de-interface caches, fix home / public timeline caching + visibility
* finish adding code comments for visibility filter
* fix angry goconst linter warnings
* actually finish adding filter visibility code comments for timeline functions
* move home timeline status author check to after visibility
* remove now-unused code
* add more code comments
* add TODO code comment, update printed cache start names
* update printed cache names on stop
* start adding separate follow(request) delete db functions, add specific visibility cache tests
* add relationship type caching
* fix getting local account follows / followed-bys, other small codebase improvements
* simplify invalidation using cache hooks, add more GetAccountBy___() functions
* fix boosting to return 404 if not boostable but no error (to not leak status ID)
* remove dead code
* improved placement of cache invalidation
* update license headers
* add example follow, follow-request config entries
* add example visibility cache configuration to config file
* use specific PutFollowRequest() instead of just Put()
* add tests for all GetAccountBy()
* add GetBlockBy() tests
* update block to check primitive fields
* update and finish adding Get{Account,Block,Follow,FollowRequest}By() tests
* fix copy-pasted code
* update envparsing test
* whitespace
* fix bun struct tag
* add license header to gtscontext
* fix old license header
* improved error creation to not use fmt.Errorf() when not needed
* fix various rebase conflicts, fix account test
* remove commented-out code, fix-up mention caching
* fix mention select bun statement
* ensure mention target account populated, pass in context to customrenderer logging
* remove more uncommented code, fix typeutil test
* add statusfave database model caching
* add status fave cache configuration
* add status fave cache example config
* woops, catch missed error. nice catch linter!
* add back testrig panic on nil db
* update example configuration to match defaults, slight tweak to cache configuration defaults
* update envparsing test with new defaults
* fetch followingget to use the follow target account
* use accounnt.IsLocal() instead of empty domain check
* use constants for the cache visibility type check
* use bun.In() for notification type restriction in db query
* include replies when fetching PublicTimeline() (to account for single-author threads in Visibility{}.StatusPublicTimelineable())
* use bun query building for nested select statements to ensure working with postgres
* update public timeline future status checks to match visibility filter
* same as previous, for home timeline
* update public timeline tests to dynamically check for appropriate statuses
* migrate accounts to allow unique constraint on public_key
* provide minimal account with publicKey
---------
Signed-off-by: kim <grufwub@gmail.com>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2023-03-28 15:03:14 +02:00
followingCount , err := c . db . CountAccountFollows ( ctx , a . ID )
2023-05-07 19:53:21 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error counting following: %w" , err )
2021-04-19 19:42:19 +02:00
}
2021-08-25 15:34:33 +02:00
statusesCount , err := c . db . CountAccountStatuses ( ctx , a . ID )
2023-05-07 19:53:21 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error counting statuses: %w" , err )
2021-04-19 19:42:19 +02:00
}
2022-11-13 21:38:01 +01:00
var lastStatusAt * string
2022-10-08 14:00:39 +02:00
lastPosted , err := c . db . GetAccountLastPosted ( ctx , a . ID , false )
2023-05-07 19:53:21 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error counting statuses: %w" , err )
2021-04-19 19:42:19 +02:00
}
2023-05-07 19:53:21 +02:00
if ! lastPosted . IsZero ( ) {
lastStatusAt = func ( ) * string { t := util . FormatISO8601 ( lastPosted ) ; return & t } ( )
2021-04-19 19:42:19 +02:00
}
2023-05-07 19:53:21 +02:00
// Profile media + nice extras:
// - Avatar
// - Header
// - Fields
// - Emojis
var (
aviURL string
aviURLStatic string
headerURL string
headerURLStatic string
)
if a . AvatarMediaAttachment != nil {
aviURL = a . AvatarMediaAttachment . URL
aviURLStatic = a . AvatarMediaAttachment . Thumbnail . URL
2021-04-19 19:42:19 +02:00
}
2023-05-07 19:53:21 +02:00
if a . HeaderMediaAttachment != nil {
headerURL = a . HeaderMediaAttachment . URL
headerURLStatic = a . HeaderMediaAttachment . Thumbnail . URL
}
2022-11-29 18:59:59 +01:00
2023-05-09 12:16:10 +02:00
// convert account gts model fields to front api model fields
fields := c . fieldsToAPIFields ( a . Fields )
2021-04-19 19:42:19 +02:00
2023-05-07 19:53:21 +02:00
// GTS model emojis -> frontend.
2022-11-29 18:59:59 +01:00
apiEmojis , err := c . convertEmojisToAPIEmojis ( ctx , a . Emojis , a . EmojiIDs )
if err != nil {
2023-02-17 12:02:29 +01:00
log . Errorf ( ctx , "error converting account emojis: %v" , err )
2022-09-26 11:56:01 +02:00
}
2021-05-27 16:06:24 +02:00
2023-05-07 19:53:21 +02:00
// Bits that vary between remote + local accounts:
// - Account (acct) string.
// - Role.
var (
acct string
role * apimodel . AccountRole
)
if a . IsRemote ( ) {
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( a . Domain )
if err != nil {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w" , a . Domain , a . ID , err )
}
2022-11-15 10:19:32 +01:00
2023-05-07 19:53:21 +02:00
acct = a . Username + "@" + d
2021-04-19 19:42:19 +02:00
} else {
2023-05-09 17:05:35 +02:00
// This is a local account, try to
// fetch more info. Skip for instance
// accounts since they have no user.
if ! a . IsInstance ( ) {
user , err := c . db . GetUserByAccountID ( ctx , a . ID )
if err != nil {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error getting user from database for account id %s: %w" , a . ID , err )
}
switch {
case * user . Admin :
role = & apimodel . AccountRole { Name : apimodel . AccountRoleAdmin }
case * user . Moderator :
role = & apimodel . AccountRole { Name : apimodel . AccountRoleModerator }
default :
role = & apimodel . AccountRole { Name : apimodel . AccountRoleUser }
}
2022-11-15 10:19:32 +01:00
}
2023-05-09 17:05:35 +02:00
acct = a . Username // omit domain
2021-04-19 19:42:19 +02:00
}
2023-05-07 19:53:21 +02:00
// Remaining properties are simple and
// can be populated directly below.
2021-07-11 16:22:21 +02:00
2023-01-02 13:10:50 +01:00
accountFrontend := & apimodel . Account {
2021-04-19 19:42:19 +02:00
ID : a . ID ,
Username : a . Username ,
Acct : acct ,
DisplayName : a . DisplayName ,
2022-08-15 12:35:05 +02:00
Locked : * a . Locked ,
2023-02-16 14:20:23 +01:00
Discoverable : * a . Discoverable ,
2022-08-15 12:35:05 +02:00
Bot : * a . Bot ,
2022-05-24 18:21:27 +02:00
CreatedAt : util . FormatISO8601 ( a . CreatedAt ) ,
2021-04-19 19:42:19 +02:00
Note : a . Note ,
URL : a . URL ,
Avatar : aviURL ,
AvatarStatic : aviURLStatic ,
Header : headerURL ,
HeaderStatic : headerURLStatic ,
FollowersCount : followersCount ,
FollowingCount : followingCount ,
StatusesCount : statusesCount ,
LastStatusAt : lastStatusAt ,
2022-11-29 18:59:59 +01:00
Emojis : apiEmojis ,
2021-04-19 19:42:19 +02:00
Fields : fields ,
2023-05-07 19:53:21 +02:00
Suspended : ! a . SuspendedAt . IsZero ( ) ,
2022-09-12 13:14:29 +02:00
CustomCSS : a . CustomCSS ,
2022-10-08 14:00:39 +02:00
EnableRSS : * a . EnableRSS ,
2022-11-15 10:19:32 +01:00
Role : role ,
2021-08-20 12:26:56 +02:00
}
2023-05-07 19:53:21 +02:00
// Bodge default avatar + header in,
// if we didn't have one already.
2022-09-04 14:41:42 +02:00
c . ensureAvatar ( accountFrontend )
c . ensureHeader ( accountFrontend )
2021-08-20 12:26:56 +02:00
return accountFrontend , nil
2021-07-11 16:22:21 +02:00
}
2023-05-09 12:16:10 +02:00
func ( c * converter ) fieldsToAPIFields ( f [ ] * gtsmodel . Field ) [ ] apimodel . Field {
fields := make ( [ ] apimodel . Field , len ( f ) )
for i , field := range f {
mField := apimodel . Field {
Name : field . Name ,
Value : field . Value ,
}
if ! field . VerifiedAt . IsZero ( ) {
mField . VerifiedAt = func ( ) * string { s := util . FormatISO8601 ( field . VerifiedAt ) ; return & s } ( )
}
fields [ i ] = mField
}
return fields
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) AccountToAPIAccountBlocked ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2023-05-07 19:53:21 +02:00
var (
acct string
role * apimodel . AccountRole
)
if a . IsRemote ( ) {
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( a . Domain )
if err != nil {
2023-05-09 17:05:35 +02:00
return nil , fmt . Errorf ( "AccountToAPIAccountBlocked: error de-punifying domain %s for account id %s: %w" , a . Domain , a . ID , err )
2023-05-07 19:53:21 +02:00
}
acct = a . Username + "@" + d
2021-07-11 16:22:21 +02:00
} else {
2023-05-09 17:05:35 +02:00
// This is a local account, try to
// fetch more info. Skip for instance
// accounts since they have no user.
if ! a . IsInstance ( ) {
user , err := c . db . GetUserByAccountID ( ctx , a . ID )
if err != nil {
return nil , fmt . Errorf ( "AccountToAPIAccountPublic: error getting user from database for account id %s: %w" , a . ID , err )
}
switch {
case * user . Admin :
role = & apimodel . AccountRole { Name : apimodel . AccountRoleAdmin }
case * user . Moderator :
role = & apimodel . AccountRole { Name : apimodel . AccountRoleModerator }
default :
role = & apimodel . AccountRole { Name : apimodel . AccountRoleUser }
}
2023-05-07 19:53:21 +02:00
}
2023-05-09 17:05:35 +02:00
acct = a . Username // omit domain
2021-07-11 16:22:21 +02:00
}
2023-01-02 13:10:50 +01:00
return & apimodel . Account {
2021-07-11 16:22:21 +02:00
ID : a . ID ,
Username : a . Username ,
Acct : acct ,
DisplayName : a . DisplayName ,
2022-08-15 12:35:05 +02:00
Bot : * a . Bot ,
2022-05-24 18:21:27 +02:00
CreatedAt : util . FormatISO8601 ( a . CreatedAt ) ,
2021-07-11 16:22:21 +02:00
URL : a . URL ,
2023-05-07 19:53:21 +02:00
Suspended : ! a . SuspendedAt . IsZero ( ) ,
Role : role ,
2021-04-19 19:42:19 +02:00
} , nil
}
2023-01-25 11:12:17 +01:00
func ( c * converter ) AccountToAdminAPIAccount ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . AdminAccountInfo , error ) {
var (
email string
ip * string
domain * string
locale string
confirmed bool
inviteRequest * string
approved bool
disabled bool
2023-02-20 17:00:44 +01:00
role = apimodel . AccountRole { Name : apimodel . AccountRoleUser } // assume user by default
2023-01-25 11:12:17 +01:00
createdByApplicationID string
)
2023-03-31 15:01:29 +02:00
if a . IsRemote ( ) {
2023-05-07 19:53:21 +02:00
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( a . Domain )
if err != nil {
return nil , fmt . Errorf ( "AccountToAdminAPIAccount: error de-punifying domain %s for account id %s: %w" , a . Domain , a . ID , err )
}
domain = & d
2023-05-09 17:05:35 +02:00
} else if ! a . IsInstance ( ) {
// This is a local, non-instance
// acct; we can fetch more info.
2023-01-25 11:12:17 +01:00
user , err := c . db . GetUserByAccountID ( ctx , a . ID )
if err != nil {
return nil , fmt . Errorf ( "AccountToAdminAPIAccount: error getting user from database for account id %s: %w" , a . ID , err )
}
if user . Email != "" {
email = user . Email
} else {
email = user . UnconfirmedEmail
}
if i := user . CurrentSignInIP . String ( ) ; i != "<nil>" {
ip = & i
}
locale = user . Locale
2023-03-31 15:01:29 +02:00
if user . Account . Reason != "" {
inviteRequest = & user . Account . Reason
}
2023-05-09 17:05:35 +02:00
2023-01-25 11:12:17 +01:00
if * user . Admin {
2023-02-20 17:00:44 +01:00
role . Name = apimodel . AccountRoleAdmin
2023-01-25 11:12:17 +01:00
} else if * user . Moderator {
2023-02-20 17:00:44 +01:00
role . Name = apimodel . AccountRoleModerator
2023-01-25 11:12:17 +01:00
}
2023-05-09 17:05:35 +02:00
2023-01-25 11:12:17 +01:00
confirmed = ! user . ConfirmedAt . IsZero ( )
approved = * user . Approved
disabled = * user . Disabled
createdByApplicationID = user . CreatedByApplicationID
}
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , a )
if err != nil {
return nil , fmt . Errorf ( "AccountToAdminAPIAccount: error converting account to api account for account id %s: %w" , a . ID , err )
}
return & apimodel . AdminAccountInfo {
ID : a . ID ,
Username : a . Username ,
Domain : domain ,
CreatedAt : util . FormatISO8601 ( a . CreatedAt ) ,
Email : email ,
IP : ip ,
IPs : [ ] interface { } { } , // not implemented,
Locale : locale ,
InviteRequest : inviteRequest ,
2023-02-20 17:00:44 +01:00
Role : role ,
2023-01-25 11:12:17 +01:00
Confirmed : confirmed ,
Approved : approved ,
Disabled : disabled ,
2023-05-07 19:53:21 +02:00
Silenced : ! a . SilencedAt . IsZero ( ) ,
Suspended : ! a . SuspendedAt . IsZero ( ) ,
2023-01-25 11:12:17 +01:00
Account : apiAccount ,
CreatedByApplicationID : createdByApplicationID ,
InvitedByAccountID : "" , // not implemented (yet)
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) AppToAPIAppSensitive ( ctx context . Context , a * gtsmodel . Application ) ( * apimodel . Application , error ) {
return & apimodel . Application {
2021-04-19 19:42:19 +02:00
ID : a . ID ,
Name : a . Name ,
Website : a . Website ,
RedirectURI : a . RedirectURI ,
ClientID : a . ClientID ,
ClientSecret : a . ClientSecret ,
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) AppToAPIAppPublic ( ctx context . Context , a * gtsmodel . Application ) ( * apimodel . Application , error ) {
return & apimodel . Application {
2021-04-19 19:42:19 +02:00
Name : a . Name ,
Website : a . Website ,
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) AttachmentToAPIAttachment ( ctx context . Context , a * gtsmodel . MediaAttachment ) ( apimodel . Attachment , error ) {
apiAttachment := apimodel . Attachment {
2022-07-22 12:48:19 +02:00
ID : a . ID ,
Type : strings . ToLower ( string ( a . Type ) ) ,
TextURL : a . URL ,
PreviewURL : a . Thumbnail . URL ,
2023-01-02 13:10:50 +01:00
Meta : apimodel . MediaMeta {
Original : apimodel . MediaDimensions {
2021-04-19 19:42:19 +02:00
Width : a . FileMeta . Original . Width ,
Height : a . FileMeta . Original . Height ,
} ,
2023-01-02 13:10:50 +01:00
Small : apimodel . MediaDimensions {
2021-04-19 19:42:19 +02:00
Width : a . FileMeta . Small . Width ,
Height : a . FileMeta . Small . Height ,
2023-01-16 16:19:17 +01:00
Size : strconv . Itoa ( a . FileMeta . Small . Width ) + "x" + strconv . Itoa ( a . FileMeta . Small . Height ) ,
2021-04-19 19:42:19 +02:00
Aspect : float32 ( a . FileMeta . Small . Aspect ) ,
} ,
} ,
2022-07-22 12:48:19 +02:00
Blurhash : a . Blurhash ,
}
// nullable fields
2022-12-22 11:48:28 +01:00
if i := a . URL ; i != "" {
2022-07-22 12:48:19 +02:00
apiAttachment . URL = & i
}
2022-12-22 11:48:28 +01:00
if i := a . RemoteURL ; i != "" {
2022-07-22 12:48:19 +02:00
apiAttachment . RemoteURL = & i
}
2022-12-22 11:48:28 +01:00
if i := a . Thumbnail . RemoteURL ; i != "" {
2022-07-22 12:48:19 +02:00
apiAttachment . PreviewRemoteURL = & i
}
2022-12-22 11:48:28 +01:00
if i := a . Description ; i != "" {
2022-07-22 12:48:19 +02:00
apiAttachment . Description = & i
}
2023-01-16 16:19:17 +01:00
// type specific fields
switch a . Type {
case gtsmodel . FileTypeImage :
apiAttachment . Meta . Original . Size = strconv . Itoa ( a . FileMeta . Original . Width ) + "x" + strconv . Itoa ( a . FileMeta . Original . Height )
apiAttachment . Meta . Original . Aspect = float32 ( a . FileMeta . Original . Aspect )
apiAttachment . Meta . Focus = & apimodel . MediaFocus {
X : a . FileMeta . Focus . X ,
Y : a . FileMeta . Focus . Y ,
}
case gtsmodel . FileTypeVideo :
if i := a . FileMeta . Original . Duration ; i != nil {
apiAttachment . Meta . Original . Duration = * i
}
2022-12-22 11:48:28 +01:00
2023-01-16 16:19:17 +01:00
if i := a . FileMeta . Original . Framerate ; i != nil {
// the masto api expects this as a string in
// the format `integer/1`, so 30fps is `30/1`
round := math . Round ( float64 ( * i ) )
fr := strconv . FormatInt ( int64 ( round ) , 10 )
apiAttachment . Meta . Original . FrameRate = fr + "/1"
}
2022-12-22 11:48:28 +01:00
2023-01-16 16:19:17 +01:00
if i := a . FileMeta . Original . Bitrate ; i != nil {
apiAttachment . Meta . Original . Bitrate = int ( * i )
}
2022-12-22 11:48:28 +01:00
}
2022-07-22 12:48:19 +02:00
return apiAttachment , nil
2021-04-19 19:42:19 +02:00
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) MentionToAPIMention ( ctx context . Context , m * gtsmodel . Mention ) ( apimodel . Mention , error ) {
2021-08-25 15:34:33 +02:00
if m . TargetAccount == nil {
targetAccount , err := c . db . GetAccountByID ( ctx , m . TargetAccountID )
if err != nil {
2023-01-02 13:10:50 +01:00
return apimodel . Mention { } , err
2021-08-25 15:34:33 +02:00
}
m . TargetAccount = targetAccount
2021-04-19 19:42:19 +02:00
}
var acct string
2023-05-07 19:53:21 +02:00
if m . TargetAccount . IsLocal ( ) {
2021-08-25 15:34:33 +02:00
acct = m . TargetAccount . Username
2021-04-19 19:42:19 +02:00
} else {
2023-05-07 19:53:21 +02:00
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( m . TargetAccount . Domain )
if err != nil {
err = fmt . Errorf ( "MentionToAPIMention: error de-punifying domain %s for account id %s: %w" , m . TargetAccount . Domain , m . TargetAccountID , err )
return apimodel . Mention { } , err
}
acct = m . TargetAccount . Username + "@" + d
2021-04-19 19:42:19 +02:00
}
2023-01-02 13:10:50 +01:00
return apimodel . Mention {
2021-08-25 15:34:33 +02:00
ID : m . TargetAccount . ID ,
Username : m . TargetAccount . Username ,
URL : m . TargetAccount . URL ,
2021-04-19 19:42:19 +02:00
Acct : acct ,
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) EmojiToAPIEmoji ( ctx context . Context , e * gtsmodel . Emoji ) ( apimodel . Emoji , error ) {
2022-11-14 23:47:27 +01:00
var category string
if e . CategoryID != "" {
if e . Category == nil {
var err error
e . Category , err = c . db . GetEmojiCategory ( ctx , e . CategoryID )
if err != nil {
2023-01-02 13:10:50 +01:00
return apimodel . Emoji { } , err
2022-11-14 23:47:27 +01:00
}
}
category = e . Category . Name
}
2023-01-02 13:10:50 +01:00
return apimodel . Emoji {
2021-04-19 19:42:19 +02:00
Shortcode : e . Shortcode ,
URL : e . ImageURL ,
StaticURL : e . ImageStaticURL ,
2022-08-15 12:35:05 +02:00
VisibleInPicker : * e . VisibleInPicker ,
2022-11-14 23:47:27 +01:00
Category : category ,
2021-04-19 19:42:19 +02:00
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) EmojiToAdminAPIEmoji ( ctx context . Context , e * gtsmodel . Emoji ) ( * apimodel . AdminEmoji , error ) {
2022-10-12 15:01:42 +02:00
emoji , err := c . EmojiToAPIEmoji ( ctx , e )
if err != nil {
return nil , err
}
2023-05-07 19:53:21 +02:00
if e . Domain != "" {
// Domain may be in Punycode,
// de-punify it just in case.
var err error
e . Domain , err = util . DePunify ( e . Domain )
if err != nil {
err = fmt . Errorf ( "EmojiToAdminAPIEmoji: error de-punifying domain %s for emoji id %s: %w" , e . Domain , e . ID , err )
return nil , err
}
}
2023-01-02 13:10:50 +01:00
return & apimodel . AdminEmoji {
2022-10-12 15:01:42 +02:00
Emoji : emoji ,
ID : e . ID ,
Disabled : * e . Disabled ,
Domain : e . Domain ,
UpdatedAt : util . FormatISO8601 ( e . UpdatedAt ) ,
TotalFileSize : e . ImageFileSize + e . ImageStaticFileSize ,
ContentType : e . ImageContentType ,
URI : e . URI ,
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) EmojiCategoryToAPIEmojiCategory ( ctx context . Context , category * gtsmodel . EmojiCategory ) ( * apimodel . EmojiCategory , error ) {
return & apimodel . EmojiCategory {
2022-11-14 23:47:27 +01:00
ID : category . ID ,
Name : category . Name ,
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) TagToAPITag ( ctx context . Context , t * gtsmodel . Tag ) ( apimodel . Tag , error ) {
return apimodel . Tag {
2021-04-19 19:42:19 +02:00
Name : t . Name ,
2021-08-25 15:34:33 +02:00
URL : t . URL ,
2021-04-19 19:42:19 +02:00
} , nil
}
2023-01-02 13:10:50 +01:00
func ( c * converter ) StatusToAPIStatus ( ctx context . Context , s * gtsmodel . Status , requestingAccount * gtsmodel . Account ) ( * apimodel . Status , error ) {
2023-05-09 13:25:48 +02:00
if err := c . db . PopulateStatus ( ctx , s ) ; err != nil {
// Ensure author account present + correct;
// can't really go further without this!
if s . Account == nil {
return nil , fmt . Errorf ( "error(s) populating status, cannot continue: %w" , err )
}
2021-04-19 19:42:19 +02:00
2023-05-09 13:25:48 +02:00
log . Errorf ( ctx , "error(s) populating status, will continue: %v" , err )
2021-04-19 19:42:19 +02:00
}
2023-05-09 13:25:48 +02:00
apiAuthorAccount , err := c . AccountToAPIAccountPublic ( ctx , s . Account )
2021-04-19 19:42:19 +02:00
if err != nil {
2023-05-09 13:25:48 +02:00
return nil , fmt . Errorf ( "error converting status author: %w" , err )
2021-04-19 19:42:19 +02:00
}
2023-05-09 13:25:48 +02:00
repliesCount , err := c . db . CountStatusReplies ( ctx , s )
if err != nil {
return nil , fmt . Errorf ( "error counting replies: %w" , err )
2021-05-08 15:16:24 +02:00
}
2021-04-19 19:42:19 +02:00
2023-05-09 13:25:48 +02:00
reblogsCount , err := c . db . CountStatusReblogs ( ctx , s )
if err != nil {
return nil , fmt . Errorf ( "error counting reblogs: %w" , err )
2021-04-19 19:42:19 +02:00
}
2023-05-09 13:25:48 +02:00
favesCount , err := c . db . CountStatusFaves ( ctx , s )
if err != nil {
return nil , fmt . Errorf ( "error counting faves: %w" , err )
2021-06-17 18:02:33 +02:00
}
2023-05-09 13:25:48 +02:00
interacts , err := c . interactionsWithStatusForAccount ( ctx , s , requestingAccount )
2021-04-19 19:42:19 +02:00
if err != nil {
2023-05-09 13:25:48 +02:00
log . Errorf ( ctx , "error getting interactions for status %s for account %s: %v" , s . ID , requestingAccount . ID , err )
// Ensure a non nil object
interacts = & statusInteractions { }
2021-04-19 19:42:19 +02:00
}
2022-11-29 18:59:59 +01:00
apiAttachments , err := c . convertAttachmentsToAPIAttachments ( ctx , s . Attachments , s . AttachmentIDs )
if err != nil {
2023-02-17 12:02:29 +01:00
log . Errorf ( ctx , "error converting status attachments: %v" , err )
2021-04-19 19:42:19 +02:00
}
2022-11-29 18:59:59 +01:00
apiMentions , err := c . convertMentionsToAPIMentions ( ctx , s . Mentions , s . MentionIDs )
if err != nil {
2023-02-17 12:02:29 +01:00
log . Errorf ( ctx , "error converting status mentions: %v" , err )
2021-04-19 19:42:19 +02:00
}
2022-11-29 18:59:59 +01:00
apiTags , err := c . convertTagsToAPITags ( ctx , s . Tags , s . TagIDs )
if err != nil {
2023-02-17 12:02:29 +01:00
log . Errorf ( ctx , "error converting status tags: %v" , err )
2021-04-19 19:42:19 +02:00
}
2022-11-29 18:59:59 +01:00
apiEmojis , err := c . convertEmojisToAPIEmojis ( ctx , s . Emojis , s . EmojiIDs )
if err != nil {
2023-02-17 12:02:29 +01:00
log . Errorf ( ctx , "error converting status emojis: %v" , err )
2021-04-19 19:42:19 +02:00
}
2023-01-02 13:10:50 +01:00
apiStatus := & apimodel . Status {
2021-04-19 19:42:19 +02:00
ID : s . ID ,
2022-05-24 18:21:27 +02:00
CreatedAt : util . FormatISO8601 ( s . CreatedAt ) ,
2022-09-02 17:00:11 +02:00
InReplyToID : nil ,
InReplyToAccountID : nil ,
2022-08-15 12:35:05 +02:00
Sensitive : * s . Sensitive ,
2021-04-19 19:42:19 +02:00
SpoilerText : s . ContentWarning ,
2021-10-04 15:24:19 +02:00
Visibility : c . VisToAPIVis ( ctx , s . Visibility ) ,
2023-05-09 13:25:48 +02:00
Language : nil ,
2021-04-19 19:42:19 +02:00
URI : s . URI ,
URL : s . URL ,
RepliesCount : repliesCount ,
ReblogsCount : reblogsCount ,
FavouritesCount : favesCount ,
2022-11-29 18:59:59 +01:00
Favourited : interacts . Faved ,
Bookmarked : interacts . Bookmarked ,
Muted : interacts . Muted ,
Reblogged : interacts . Reblogged ,
2023-02-25 13:16:30 +01:00
Pinned : interacts . Pinned ,
2021-04-19 19:42:19 +02:00
Content : s . Content ,
2022-09-02 17:00:11 +02:00
Reblog : nil ,
2023-05-09 13:25:48 +02:00
Application : nil ,
2021-10-04 15:24:19 +02:00
Account : apiAuthorAccount ,
MediaAttachments : apiAttachments ,
Mentions : apiMentions ,
Tags : apiTags ,
Emojis : apiEmojis ,
2022-09-02 17:00:11 +02:00
Card : nil , // TODO: implement cards
Poll : nil , // TODO: implement polls
2021-04-19 19:42:19 +02:00
Text : s . Text ,
2021-08-02 19:06:44 +02:00
}
2023-05-09 13:25:48 +02:00
// Nullable fields.
2022-09-02 17:00:11 +02:00
if s . InReplyToID != "" {
2023-05-09 13:25:48 +02:00
apiStatus . InReplyToID = func ( ) * string { i := s . InReplyToID ; return & i } ( )
2022-09-02 17:00:11 +02:00
}
if s . InReplyToAccountID != "" {
2023-05-09 13:25:48 +02:00
apiStatus . InReplyToAccountID = func ( ) * string { i := s . InReplyToAccountID ; return & i } ( )
2022-09-02 17:00:11 +02:00
}
2023-05-09 13:25:48 +02:00
if s . Language != "" {
apiStatus . Language = func ( ) * string { i := s . Language ; return & i } ( )
}
if s . BoostOf != nil {
apiBoostOf , err := c . StatusToAPIStatus ( ctx , s . BoostOf , requestingAccount )
if err != nil {
return nil , fmt . Errorf ( "error converting boosted status: %w" , err )
}
apiStatus . Reblog = & apimodel . StatusReblogged { Status : apiBoostOf }
}
if appID := s . CreatedWithApplicationID ; appID != "" {
app := & gtsmodel . Application { }
if err := c . db . GetByID ( ctx , appID , app ) ; err != nil {
return nil , fmt . Errorf ( "error getting application %s: %w" , appID , err )
}
apiApp , err := c . AppToAPIAppPublic ( ctx , app )
if err != nil {
return nil , fmt . Errorf ( "error converting application %s: %w" , appID , err )
}
apiStatus . Application = apiApp
}
// Normalization.
if s . URL == "" {
// URL was empty for some reason;
// provide AP URI as fallback.
s . URL = s . URI
2021-08-02 19:06:44 +02:00
}
return apiStatus , nil
2021-04-19 19:42:19 +02:00
}
2021-05-08 14:25:55 +02:00
2021-10-04 15:24:19 +02:00
// VisToapi converts a gts visibility into its api equivalent
2023-01-02 13:10:50 +01:00
func ( c * converter ) VisToAPIVis ( ctx context . Context , m gtsmodel . Visibility ) apimodel . Visibility {
2021-05-08 14:25:55 +02:00
switch m {
case gtsmodel . VisibilityPublic :
2023-01-02 13:10:50 +01:00
return apimodel . VisibilityPublic
2021-05-08 14:25:55 +02:00
case gtsmodel . VisibilityUnlocked :
2023-01-02 13:10:50 +01:00
return apimodel . VisibilityUnlisted
2021-05-08 14:25:55 +02:00
case gtsmodel . VisibilityFollowersOnly , gtsmodel . VisibilityMutualsOnly :
2023-01-02 13:10:50 +01:00
return apimodel . VisibilityPrivate
2021-05-08 14:25:55 +02:00
case gtsmodel . VisibilityDirect :
2023-01-02 13:10:50 +01:00
return apimodel . VisibilityDirect
2021-05-08 14:25:55 +02:00
}
return ""
}
2021-05-09 14:06:06 +02:00
2023-02-02 14:08:13 +01:00
func ( c * converter ) InstanceToAPIV1Instance ( ctx context . Context , i * gtsmodel . Instance ) ( * apimodel . InstanceV1 , error ) {
instance := & apimodel . InstanceV1 {
2021-05-09 20:34:27 +02:00
URI : i . URI ,
2023-02-02 14:08:13 +01:00
AccountDomain : config . GetAccountDomain ( ) ,
2021-05-09 20:34:27 +02:00
Title : i . Title ,
Description : i . Description ,
2021-05-09 14:06:06 +02:00
ShortDescription : i . ShortDescription ,
2021-05-09 20:34:27 +02:00
Email : i . ContactEmail ,
2023-02-02 14:08:13 +01:00
Version : config . GetSoftwareVersion ( ) ,
Languages : [ ] string { } , // todo: not supported yet
Registrations : config . GetAccountsRegistrationOpen ( ) ,
ApprovalRequired : config . GetAccountsApprovalRequired ( ) ,
InvitesEnabled : false , // todo: not supported yet
MaxTootChars : uint ( config . GetStatusesMaxChars ( ) ) ,
}
2023-07-21 19:49:13 +02:00
if config . GetInstanceInjectMastodonVersion ( ) {
instance . Version = toMastodonVersion ( instance . Version )
}
2023-02-02 14:08:13 +01:00
// configuration
instance . Configuration . Statuses . MaxCharacters = config . GetStatusesMaxChars ( )
instance . Configuration . Statuses . MaxMediaAttachments = config . GetStatusesMediaMaxFiles ( )
instance . Configuration . Statuses . CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
2023-03-02 12:06:40 +01:00
instance . Configuration . Statuses . SupportedMimeTypes = instanceStatusesSupportedMimeTypes
2023-02-02 14:08:13 +01:00
instance . Configuration . MediaAttachments . SupportedMimeTypes = media . SupportedMIMETypes
instance . Configuration . MediaAttachments . ImageSizeLimit = int ( config . GetMediaImageMaxSize ( ) )
instance . Configuration . MediaAttachments . ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit
instance . Configuration . MediaAttachments . VideoSizeLimit = int ( config . GetMediaVideoMaxSize ( ) )
instance . Configuration . MediaAttachments . VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit
instance . Configuration . MediaAttachments . VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit
instance . Configuration . Polls . MaxOptions = config . GetStatusesPollMaxOptions ( )
instance . Configuration . Polls . MaxCharactersPerOption = config . GetStatusesPollOptionMaxChars ( )
instance . Configuration . Polls . MinExpiration = instancePollsMinExpiration
instance . Configuration . Polls . MaxExpiration = instancePollsMaxExpiration
instance . Configuration . Accounts . AllowCustomCSS = config . GetAccountsAllowCustomCSS ( )
instance . Configuration . Accounts . MaxFeaturedTags = instanceAccountsMaxFeaturedTags
2023-06-13 12:21:26 +02:00
instance . Configuration . Accounts . MaxProfileFields = instanceAccountsMaxProfileFields
2023-02-02 14:08:13 +01:00
instance . Configuration . Emojis . EmojiSizeLimit = int ( config . GetMediaEmojiLocalMaxSize ( ) )
// URLs
instance . URLs . StreamingAPI = "wss://" + i . Domain
// statistics
stats := make ( map [ string ] int , 3 )
userCount , err := c . db . CountInstanceUsers ( ctx , i . Domain )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting counting instance users: %w" , err )
}
stats [ "user_count" ] = userCount
statusCount , err := c . db . CountInstanceStatuses ( ctx , i . Domain )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting counting instance statuses: %w" , err )
}
stats [ "status_count" ] = statusCount
domainCount , err := c . db . CountInstanceDomains ( ctx , i . Domain )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting counting instance domains: %w" , err )
}
stats [ "domain_count" ] = domainCount
instance . Stats = stats
// thumbnail
iAccount , err := c . db . GetInstanceAccount ( ctx , "" )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting instance account: %w" , err )
}
if iAccount . AvatarMediaAttachmentID != "" {
if iAccount . AvatarMediaAttachment == nil {
avi , err := c . db . GetAttachmentByID ( ctx , iAccount . AvatarMediaAttachmentID )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIInstance: error getting instance avatar attachment with id %s: %w" , iAccount . AvatarMediaAttachmentID , err )
2022-06-26 12:33:11 +02:00
}
2023-02-02 14:08:13 +01:00
iAccount . AvatarMediaAttachment = avi
2022-06-26 12:33:11 +02:00
}
2023-02-02 14:08:13 +01:00
instance . Thumbnail = iAccount . AvatarMediaAttachment . URL
instance . ThumbnailType = iAccount . AvatarMediaAttachment . File . ContentType
instance . ThumbnailDescription = iAccount . AvatarMediaAttachment . Description
} else {
instance . Thumbnail = config . GetProtocol ( ) + "://" + i . Domain + "/assets/logo.png" // default thumb
}
2021-06-23 16:35:57 +02:00
2023-02-02 14:08:13 +01:00
// contact account
if i . ContactAccountID != "" {
if i . ContactAccount == nil {
contactAccount , err := c . db . GetAccountByID ( ctx , i . ContactAccountID )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting instance contact account %s: %w" , i . ContactAccountID , err )
}
i . ContactAccount = contactAccount
2021-06-23 16:35:57 +02:00
}
2023-02-02 14:08:13 +01:00
account , err := c . AccountToAPIAccountPublic ( ctx , i . ContactAccount )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: error converting instance contact account %s: %w" , i . ContactAccountID , err )
2021-06-23 16:35:57 +02:00
}
2023-02-02 14:08:13 +01:00
instance . ContactAccount = account
}
2021-06-23 16:35:57 +02:00
2023-02-02 14:08:13 +01:00
return instance , nil
}
func ( c * converter ) InstanceToAPIV2Instance ( ctx context . Context , i * gtsmodel . Instance ) ( * apimodel . InstanceV2 , error ) {
instance := & apimodel . InstanceV2 {
Domain : i . Domain ,
AccountDomain : config . GetAccountDomain ( ) ,
Title : i . Title ,
Version : config . GetSoftwareVersion ( ) ,
SourceURL : instanceSourceURL ,
Description : i . Description ,
Usage : apimodel . InstanceV2Usage { } , // todo: not implemented
Languages : [ ] string { } , // todo: not implemented
Rules : [ ] interface { } { } , // todo: not implemented
}
2023-07-21 19:49:13 +02:00
if config . GetInstanceInjectMastodonVersion ( ) {
instance . Version = toMastodonVersion ( instance . Version )
}
2023-02-02 14:08:13 +01:00
// thumbnail
thumbnail := apimodel . InstanceV2Thumbnail { }
iAccount , err := c . db . GetInstanceAccount ( ctx , "" )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV2Instance: db error getting instance account: %w" , err )
2021-05-09 14:06:06 +02:00
}
2023-02-02 14:08:13 +01:00
if iAccount . AvatarMediaAttachmentID != "" {
if iAccount . AvatarMediaAttachment == nil {
avi , err := c . db . GetAttachmentByID ( ctx , iAccount . AvatarMediaAttachmentID )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV2Instance: error getting instance avatar attachment with id %s: %w" , iAccount . AvatarMediaAttachmentID , err )
}
iAccount . AvatarMediaAttachment = avi
}
thumbnail . URL = iAccount . AvatarMediaAttachment . URL
thumbnail . Type = iAccount . AvatarMediaAttachment . File . ContentType
thumbnail . Description = iAccount . AvatarMediaAttachment . Description
thumbnail . Blurhash = iAccount . AvatarMediaAttachment . Blurhash
} else {
thumbnail . URL = config . GetProtocol ( ) + "://" + i . Domain + "/assets/logo.png" // default thumb
}
instance . Thumbnail = thumbnail
// configuration
instance . Configuration . URLs . Streaming = "wss://" + i . Domain
instance . Configuration . Statuses . MaxCharacters = config . GetStatusesMaxChars ( )
instance . Configuration . Statuses . MaxMediaAttachments = config . GetStatusesMediaMaxFiles ( )
instance . Configuration . Statuses . CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
2023-03-02 12:06:40 +01:00
instance . Configuration . Statuses . SupportedMimeTypes = instanceStatusesSupportedMimeTypes
2023-02-02 14:08:13 +01:00
instance . Configuration . MediaAttachments . SupportedMimeTypes = media . SupportedMIMETypes
instance . Configuration . MediaAttachments . ImageSizeLimit = int ( config . GetMediaImageMaxSize ( ) )
instance . Configuration . MediaAttachments . ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit
instance . Configuration . MediaAttachments . VideoSizeLimit = int ( config . GetMediaVideoMaxSize ( ) )
instance . Configuration . MediaAttachments . VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit
instance . Configuration . MediaAttachments . VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit
instance . Configuration . Polls . MaxOptions = config . GetStatusesPollMaxOptions ( )
instance . Configuration . Polls . MaxCharactersPerOption = config . GetStatusesPollOptionMaxChars ( )
instance . Configuration . Polls . MinExpiration = instancePollsMinExpiration
instance . Configuration . Polls . MaxExpiration = instancePollsMaxExpiration
instance . Configuration . Accounts . AllowCustomCSS = config . GetAccountsAllowCustomCSS ( )
instance . Configuration . Accounts . MaxFeaturedTags = instanceAccountsMaxFeaturedTags
2023-06-13 12:21:26 +02:00
instance . Configuration . Accounts . MaxProfileFields = instanceAccountsMaxProfileFields
2023-02-02 14:08:13 +01:00
instance . Configuration . Emojis . EmojiSizeLimit = int ( config . GetMediaEmojiLocalMaxSize ( ) )
// registrations
instance . Registrations . Enabled = config . GetAccountsRegistrationOpen ( )
instance . Registrations . ApprovalRequired = config . GetAccountsApprovalRequired ( )
instance . Registrations . Message = nil // todo: not implemented
// contact
instance . Contact . Email = i . ContactEmail
2021-05-09 14:06:06 +02:00
if i . ContactAccountID != "" {
2021-08-25 15:34:33 +02:00
if i . ContactAccount == nil {
contactAccount , err := c . db . GetAccountByID ( ctx , i . ContactAccountID )
2023-02-02 14:08:13 +01:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV2Instance: db error getting instance contact account %s: %w" , i . ContactAccountID , err )
2021-05-09 14:06:06 +02:00
}
2023-02-02 14:08:13 +01:00
i . ContactAccount = contactAccount
2021-05-09 14:06:06 +02:00
}
2023-02-02 14:08:13 +01:00
account , err := c . AccountToAPIAccountPublic ( ctx , i . ContactAccount )
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV2Instance: error converting instance contact account %s: %w" , i . ContactAccountID , err )
2021-08-25 15:34:33 +02:00
}
2023-02-02 14:08:13 +01:00
instance . Contact . Account = account
2021-05-09 14:06:06 +02:00
}
2023-02-02 14:08:13 +01:00
return instance , nil
2021-05-09 14:06:06 +02:00
}
2021-05-21 15:48:26 +02:00
2023-01-02 13:10:50 +01:00
func ( c * converter ) RelationshipToAPIRelationship ( ctx context . Context , r * gtsmodel . Relationship ) ( * apimodel . Relationship , error ) {
return & apimodel . Relationship {
2021-05-21 23:04:59 +02:00
ID : r . ID ,
Following : r . Following ,
ShowingReblogs : r . ShowingReblogs ,
Notifying : r . Notifying ,
FollowedBy : r . FollowedBy ,
Blocking : r . Blocking ,
BlockedBy : r . BlockedBy ,
Muting : r . Muting ,
2021-05-21 15:48:26 +02:00
MutingNotifications : r . MutingNotifications ,
2021-05-21 23:04:59 +02:00
Requested : r . Requested ,
DomainBlocking : r . DomainBlocking ,
Endorsed : r . Endorsed ,
Note : r . Note ,
2021-05-21 15:48:26 +02:00
} , nil
}
2021-05-27 16:06:24 +02:00
2023-01-02 13:10:50 +01:00
func ( c * converter ) NotificationToAPINotification ( ctx context . Context , n * gtsmodel . Notification ) ( * apimodel . Notification , error ) {
2021-08-20 12:26:56 +02:00
if n . TargetAccount == nil {
2021-08-25 15:34:33 +02:00
tAccount , err := c . db . GetAccountByID ( ctx , n . TargetAccountID )
2021-08-20 12:26:56 +02:00
if err != nil {
2021-10-04 15:24:19 +02:00
return nil , fmt . Errorf ( "NotificationToapi: error getting target account with id %s from the db: %s" , n . TargetAccountID , err )
2021-05-27 16:06:24 +02:00
}
2021-08-20 12:26:56 +02:00
n . TargetAccount = tAccount
2021-05-27 16:06:24 +02:00
}
2021-08-20 12:26:56 +02:00
if n . OriginAccount == nil {
2021-08-25 15:34:33 +02:00
ogAccount , err := c . db . GetAccountByID ( ctx , n . OriginAccountID )
2021-08-20 12:26:56 +02:00
if err != nil {
2021-10-04 15:24:19 +02:00
return nil , fmt . Errorf ( "NotificationToapi: error getting origin account with id %s from the db: %s" , n . OriginAccountID , err )
2021-05-27 16:06:24 +02:00
}
2021-08-20 12:26:56 +02:00
n . OriginAccount = ogAccount
2021-05-27 16:06:24 +02:00
}
2021-08-20 12:26:56 +02:00
2021-10-04 15:24:19 +02:00
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , n . OriginAccount )
2021-05-27 16:06:24 +02:00
if err != nil {
2021-10-04 15:24:19 +02:00
return nil , fmt . Errorf ( "NotificationToapi: error converting account to api: %s" , err )
2021-05-27 16:06:24 +02:00
}
2023-01-02 13:10:50 +01:00
var apiStatus * apimodel . Status
2021-05-27 16:06:24 +02:00
if n . StatusID != "" {
2021-08-20 12:26:56 +02:00
if n . Status == nil {
2021-08-25 15:34:33 +02:00
status , err := c . db . GetStatusByID ( ctx , n . StatusID )
2021-08-20 12:26:56 +02:00
if err != nil {
2021-10-04 15:24:19 +02:00
return nil , fmt . Errorf ( "NotificationToapi: error getting status with id %s from the db: %s" , n . StatusID , err )
2021-05-27 16:06:24 +02:00
}
2021-08-20 12:26:56 +02:00
n . Status = status
2021-05-27 16:06:24 +02:00
}
2021-08-20 12:26:56 +02:00
if n . Status . Account == nil {
if n . Status . AccountID == n . TargetAccount . ID {
n . Status . Account = n . TargetAccount
} else if n . Status . AccountID == n . OriginAccount . ID {
n . Status . Account = n . OriginAccount
2021-05-27 16:06:24 +02:00
}
}
var err error
2022-08-30 11:42:52 +02:00
apiStatus , err = c . StatusToAPIStatus ( ctx , n . Status , n . TargetAccount )
2021-05-27 16:06:24 +02:00
if err != nil {
2021-10-04 15:24:19 +02:00
return nil , fmt . Errorf ( "NotificationToapi: error converting status to api: %s" , err )
2021-05-27 16:06:24 +02:00
}
}
2022-08-29 11:06:37 +02:00
if apiStatus != nil && apiStatus . Reblog != nil {
// use the actual reblog status for the notifications endpoint
apiStatus = apiStatus . Reblog . Status
}
2023-01-02 13:10:50 +01:00
return & apimodel . Notification {
2021-05-27 16:06:24 +02:00
ID : n . ID ,
Type : string ( n . NotificationType ) ,
2022-05-24 18:21:27 +02:00
CreatedAt : util . FormatISO8601 ( n . CreatedAt ) ,
2021-10-04 15:24:19 +02:00
Account : apiAccount ,
Status : apiStatus ,
2021-05-27 16:06:24 +02:00
} , nil
}
2021-07-05 13:23:03 +02:00
2023-01-02 13:10:50 +01:00
func ( c * converter ) DomainBlockToAPIDomainBlock ( ctx context . Context , b * gtsmodel . DomainBlock , export bool ) ( * apimodel . DomainBlock , error ) {
2023-05-07 19:53:21 +02:00
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( b . Domain )
if err != nil {
return nil , fmt . Errorf ( "DomainBlockToAPIDomainBlock: error de-punifying domain %s: %w" , b . Domain , err )
}
2023-01-02 13:10:50 +01:00
domainBlock := & apimodel . DomainBlock {
Domain : apimodel . Domain {
2023-05-07 19:53:21 +02:00
Domain : d ,
2022-06-23 16:54:54 +02:00
PublicComment : b . PublicComment ,
} ,
2021-07-05 13:23:03 +02:00
}
// if we're exporting a domain block, return it with minimal information attached
if ! export {
domainBlock . ID = b . ID
2022-08-15 12:35:05 +02:00
domainBlock . Obfuscate = * b . Obfuscate
2021-07-05 13:23:03 +02:00
domainBlock . PrivateComment = b . PrivateComment
domainBlock . SubscriptionID = b . SubscriptionID
domainBlock . CreatedBy = b . CreatedByAccountID
2022-05-24 18:21:27 +02:00
domainBlock . CreatedAt = util . FormatISO8601 ( b . CreatedAt )
2021-07-05 13:23:03 +02:00
}
return domainBlock , nil
}
2022-11-29 18:59:59 +01:00
2023-01-23 13:14:21 +01:00
func ( c * converter ) ReportToAPIReport ( ctx context . Context , r * gtsmodel . Report ) ( * apimodel . Report , error ) {
report := & apimodel . Report {
ID : r . ID ,
CreatedAt : util . FormatISO8601 ( r . CreatedAt ) ,
ActionTaken : ! r . ActionTakenAt . IsZero ( ) ,
Category : "other" , // todo: only support default 'other' category right now
Comment : r . Comment ,
Forwarded : * r . Forwarded ,
StatusIDs : r . StatusIDs ,
RuleIDs : [ ] int { } , // todo: not supported yet
}
if ! r . ActionTakenAt . IsZero ( ) {
actionTakenAt := util . FormatISO8601 ( r . ActionTakenAt )
report . ActionTakenAt = & actionTakenAt
}
if actionComment := r . ActionTaken ; actionComment != "" {
2023-01-25 11:12:17 +01:00
report . ActionTakenComment = & actionComment
2023-01-23 13:14:21 +01:00
}
if r . TargetAccount == nil {
tAccount , err := c . db . GetAccountByID ( ctx , r . TargetAccountID )
if err != nil {
return nil , fmt . Errorf ( "ReportToAPIReport: error getting target account with id %s from the db: %s" , r . TargetAccountID , err )
}
r . TargetAccount = tAccount
}
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , r . TargetAccount )
if err != nil {
return nil , fmt . Errorf ( "ReportToAPIReport: error converting target account to api: %s" , err )
}
report . TargetAccount = apiAccount
return report , nil
}
2023-01-25 11:12:17 +01:00
func ( c * converter ) ReportToAdminAPIReport ( ctx context . Context , r * gtsmodel . Report , requestingAccount * gtsmodel . Account ) ( * apimodel . AdminReport , error ) {
var (
err error
actionTakenAt * string
actionTakenComment * string
actionTakenByAccount * apimodel . AdminAccountInfo
)
if ! r . ActionTakenAt . IsZero ( ) {
ata := util . FormatISO8601 ( r . ActionTakenAt )
actionTakenAt = & ata
}
if r . Account == nil {
r . Account , err = c . db . GetAccountByID ( ctx , r . AccountID )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error getting account with id %s from the db: %w" , r . AccountID , err )
}
}
account , err := c . AccountToAdminAPIAccount ( ctx , r . Account )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error converting account with id %s to adminAPIAccount: %w" , r . AccountID , err )
}
if r . TargetAccount == nil {
r . TargetAccount , err = c . db . GetAccountByID ( ctx , r . TargetAccountID )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error getting target account with id %s from the db: %w" , r . TargetAccountID , err )
}
}
targetAccount , err := c . AccountToAdminAPIAccount ( ctx , r . TargetAccount )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error converting target account with id %s to adminAPIAccount: %w" , r . TargetAccountID , err )
}
if r . ActionTakenByAccountID != "" {
if r . ActionTakenByAccount == nil {
r . ActionTakenByAccount , err = c . db . GetAccountByID ( ctx , r . ActionTakenByAccountID )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error getting action taken by account with id %s from the db: %w" , r . ActionTakenByAccountID , err )
}
}
actionTakenByAccount , err = c . AccountToAdminAPIAccount ( ctx , r . ActionTakenByAccount )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error converting action taken by account with id %s to adminAPIAccount: %w" , r . ActionTakenByAccountID , err )
}
}
statuses := make ( [ ] * apimodel . Status , 0 , len ( r . StatusIDs ) )
if len ( r . StatusIDs ) != 0 && len ( r . Statuses ) == 0 {
r . Statuses , err = c . db . GetStatuses ( ctx , r . StatusIDs )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error getting statuses from the db: %w" , err )
}
}
for _ , s := range r . Statuses {
status , err := c . StatusToAPIStatus ( ctx , s , requestingAccount )
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error converting status with id %s to api status: %w" , s . ID , err )
}
statuses = append ( statuses , status )
}
if ac := r . ActionTaken ; ac != "" {
actionTakenComment = & ac
}
return & apimodel . AdminReport {
ID : r . ID ,
ActionTaken : ! r . ActionTakenAt . IsZero ( ) ,
ActionTakenAt : actionTakenAt ,
Category : "other" , // todo: only support default 'other' category right now
Comment : r . Comment ,
Forwarded : * r . Forwarded ,
CreatedAt : util . FormatISO8601 ( r . CreatedAt ) ,
UpdatedAt : util . FormatISO8601 ( r . UpdatedAt ) ,
Account : account ,
TargetAccount : targetAccount ,
AssignedAccount : actionTakenByAccount ,
ActionTakenByAccount : actionTakenByAccount ,
ActionTakenComment : actionTakenComment ,
Statuses : statuses ,
Rules : [ ] interface { } { } , // not implemented
} , nil
}
2023-05-25 10:37:38 +02:00
func ( c * converter ) ListToAPIList ( ctx context . Context , l * gtsmodel . List ) ( * apimodel . List , error ) {
return & apimodel . List {
ID : l . ID ,
Title : l . Title ,
RepliesPolicy : string ( l . RepliesPolicy ) ,
} , nil
}
2022-11-29 18:59:59 +01:00
// convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied.
2023-01-02 13:10:50 +01:00
func ( c * converter ) convertAttachmentsToAPIAttachments ( ctx context . Context , attachments [ ] * gtsmodel . MediaAttachment , attachmentIDs [ ] string ) ( [ ] apimodel . Attachment , error ) {
2022-12-22 11:48:28 +01:00
var errs gtserror . MultiError
2022-11-29 18:59:59 +01:00
if len ( attachments ) == 0 {
// GTS model attachments were not populated
// Preallocate expected GTS slice
attachments = make ( [ ] * gtsmodel . MediaAttachment , 0 , len ( attachmentIDs ) )
// Fetch GTS models for attachment IDs
for _ , id := range attachmentIDs {
attachment , err := c . db . GetAttachmentByID ( ctx , id )
if err != nil {
errs . Appendf ( "error fetching attachment %s from database: %v" , id , err )
continue
}
attachments = append ( attachments , attachment )
}
}
// Preallocate expected frontend slice
2023-01-02 13:10:50 +01:00
apiAttachments := make ( [ ] apimodel . Attachment , 0 , len ( attachments ) )
2022-11-29 18:59:59 +01:00
// Convert GTS models to frontend models
for _ , attachment := range attachments {
apiAttachment , err := c . AttachmentToAPIAttachment ( ctx , attachment )
if err != nil {
errs . Appendf ( "error converting attchment %s to api attachment: %v" , attachment . ID , err )
continue
}
apiAttachments = append ( apiAttachments , apiAttachment )
}
return apiAttachments , errs . Combine ( )
}
// convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
2023-01-02 13:10:50 +01:00
func ( c * converter ) convertEmojisToAPIEmojis ( ctx context . Context , emojis [ ] * gtsmodel . Emoji , emojiIDs [ ] string ) ( [ ] apimodel . Emoji , error ) {
2022-12-22 11:48:28 +01:00
var errs gtserror . MultiError
2022-11-29 18:59:59 +01:00
if len ( emojis ) == 0 {
// GTS model attachments were not populated
// Preallocate expected GTS slice
emojis = make ( [ ] * gtsmodel . Emoji , 0 , len ( emojiIDs ) )
// Fetch GTS models for emoji IDs
for _ , id := range emojiIDs {
emoji , err := c . db . GetEmojiByID ( ctx , id )
if err != nil {
errs . Appendf ( "error fetching emoji %s from database: %v" , id , err )
continue
}
emojis = append ( emojis , emoji )
}
}
// Preallocate expected frontend slice
2023-01-02 13:10:50 +01:00
apiEmojis := make ( [ ] apimodel . Emoji , 0 , len ( emojis ) )
2022-11-29 18:59:59 +01:00
// Convert GTS models to frontend models
for _ , emoji := range emojis {
apiEmoji , err := c . EmojiToAPIEmoji ( ctx , emoji )
if err != nil {
errs . Appendf ( "error converting emoji %s to api emoji: %v" , emoji . ID , err )
continue
}
apiEmojis = append ( apiEmojis , apiEmoji )
}
return apiEmojis , errs . Combine ( )
}
// convertMentionsToAPIMentions will convert a slice of GTS model mentions to frontend API model mentions, falling back to IDs if no GTS models supplied.
2023-01-02 13:10:50 +01:00
func ( c * converter ) convertMentionsToAPIMentions ( ctx context . Context , mentions [ ] * gtsmodel . Mention , mentionIDs [ ] string ) ( [ ] apimodel . Mention , error ) {
2022-12-22 11:48:28 +01:00
var errs gtserror . MultiError
2022-11-29 18:59:59 +01:00
if len ( mentions ) == 0 {
var err error
// GTS model mentions were not populated
//
// Fetch GTS models for mention IDs
mentions , err = c . db . GetMentions ( ctx , mentionIDs )
if err != nil {
errs . Appendf ( "error fetching mentions from database: %v" , err )
}
}
// Preallocate expected frontend slice
2023-01-02 13:10:50 +01:00
apiMentions := make ( [ ] apimodel . Mention , 0 , len ( mentions ) )
2022-11-29 18:59:59 +01:00
// Convert GTS models to frontend models
for _ , mention := range mentions {
apiMention , err := c . MentionToAPIMention ( ctx , mention )
if err != nil {
errs . Appendf ( "error converting mention %s to api mention: %v" , mention . ID , err )
continue
}
apiMentions = append ( apiMentions , apiMention )
}
return apiMentions , errs . Combine ( )
}
// convertTagsToAPITags will convert a slice of GTS model tags to frontend API model tags, falling back to IDs if no GTS models supplied.
2023-01-02 13:10:50 +01:00
func ( c * converter ) convertTagsToAPITags ( ctx context . Context , tags [ ] * gtsmodel . Tag , tagIDs [ ] string ) ( [ ] apimodel . Tag , error ) {
2022-12-22 11:48:28 +01:00
var errs gtserror . MultiError
2022-11-29 18:59:59 +01:00
if len ( tags ) == 0 {
// GTS model tags were not populated
// Preallocate expected GTS slice
tags = make ( [ ] * gtsmodel . Tag , 0 , len ( tagIDs ) )
// Fetch GTS models for tag IDs
for _ , id := range tagIDs {
tag := new ( gtsmodel . Tag )
if err := c . db . GetByID ( ctx , id , tag ) ; err != nil {
errs . Appendf ( "error fetching tag %s from database: %v" , id , err )
continue
}
tags = append ( tags , tag )
}
}
// Preallocate expected frontend slice
2023-01-02 13:10:50 +01:00
apiTags := make ( [ ] apimodel . Tag , 0 , len ( tags ) )
2022-11-29 18:59:59 +01:00
// Convert GTS models to frontend models
for _ , tag := range tags {
apiTag , err := c . TagToAPITag ( ctx , tag )
if err != nil {
errs . Appendf ( "error converting tag %s to api tag: %v" , tag . ID , err )
continue
}
apiTags = append ( apiTags , apiTag )
}
return apiTags , errs . Combine ( )
}