2023-03-12 15:00:57 +00: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 17:42:19 +00:00
2021-05-08 12:25:55 +00:00
package typeutils
2021-04-19 17:42:19 +00:00
import (
2024-11-04 14:00:10 +00:00
"cmp"
2021-08-25 13:34:33 +00:00
"context"
2023-05-07 17:53:21 +00:00
"errors"
2021-04-19 17:42:19 +00:00
"fmt"
2024-11-04 14:00:10 +00:00
"math"
2024-09-16 12:00:23 +00:00
"slices"
"strconv"
2021-05-17 17:06:58 +00:00
"strings"
2024-05-06 11:49:08 +00:00
"time"
2021-04-19 17:42:19 +00:00
2024-06-03 09:20:53 +00:00
"codeberg.org/gruf/go-debug"
2023-01-02 12:10:50 +00:00
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
2021-12-07 12:31:39 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2023-05-07 17:53:21 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2024-05-06 11:49:08 +00:00
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
2024-06-06 16:38:02 +00:00
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
2022-12-22 10:48:28 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2024-12-14 14:01:58 +00:00
"github.com/superseriousbusiness/gotosocial/internal/id"
2023-11-17 10:35:28 +00:00
"github.com/superseriousbusiness/gotosocial/internal/language"
2022-07-19 08:47:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2022-06-26 08:58:45 +00:00
"github.com/superseriousbusiness/gotosocial/internal/media"
2023-07-31 13:47:35 +00:00
"github.com/superseriousbusiness/gotosocial/internal/uris"
2022-05-24 16:21:27 +00:00
"github.com/superseriousbusiness/gotosocial/internal/util"
2021-04-19 17:42:19 +00:00
)
2022-09-08 10:36:42 +00:00
const (
2024-11-04 14:00:10 +00:00
instanceStatusesCharactersReservedPerURL = 25
instancePollsMinExpiration = 300 // seconds
instancePollsMaxExpiration = 2629746 // seconds
instanceAccountsMaxFeaturedTags = 10
instanceAccountsMaxProfileFields = 6 // FIXME: https://github.com/superseriousbusiness/gotosocial/issues/1876
instanceSourceURL = "https://github.com/superseriousbusiness/gotosocial"
instanceMastodonVersion = "3.5.3"
2022-09-08 10:36:42 +00:00
)
2023-03-02 11:06:40 +00:00
var instanceStatusesSupportedMimeTypes = [ ] string {
string ( apimodel . StatusContentTypePlain ) ,
string ( apimodel . StatusContentTypeMarkdown ) ,
}
2023-07-21 17:49:13 +00:00
func toMastodonVersion ( in string ) string {
return instanceMastodonVersion + "+" + strings . ReplaceAll ( in , " " , "-" )
}
2024-06-06 13:43:25 +00:00
// UserToAPIUser converts a *gtsmodel.User to an API
// representation suitable for serving to that user.
//
// Contains sensitive info so should only
// ever be served to the user themself.
func ( c * Converter ) UserToAPIUser ( ctx context . Context , u * gtsmodel . User ) * apimodel . User {
user := & apimodel . User {
ID : u . ID ,
CreatedAt : util . FormatISO8601 ( u . CreatedAt ) ,
Email : u . Email ,
UnconfirmedEmail : u . UnconfirmedEmail ,
Reason : u . Reason ,
Moderator : * u . Moderator ,
Admin : * u . Admin ,
Disabled : * u . Disabled ,
Approved : * u . Approved ,
}
// Zero-able dates.
if ! u . LastEmailedAt . IsZero ( ) {
user . LastEmailedAt = util . FormatISO8601 ( u . LastEmailedAt )
}
if ! u . ConfirmedAt . IsZero ( ) {
user . ConfirmedAt = util . FormatISO8601 ( u . ConfirmedAt )
}
if ! u . ConfirmationSentAt . IsZero ( ) {
user . ConfirmationSentAt = util . FormatISO8601 ( u . ConfirmationSentAt )
}
if ! u . ResetPasswordSentAt . IsZero ( ) {
user . ResetPasswordSentAt = util . FormatISO8601 ( u . ResetPasswordSentAt )
}
return user
}
2024-07-31 16:26:09 +00:00
// AccountToAPIAccountSensitive takes a db model application as a param, and returns a populated apitype application, or an error
2023-09-23 16:44:11 +00:00
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
func ( c * Converter ) AccountToAPIAccountSensitive ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2024-04-16 11:10:13 +00:00
// We can build this sensitive account model
// by first getting the public account, and
2024-07-31 16:26:09 +00:00
// then adding the Source object and role permissions bitmap to it.
2021-10-04 13:24:19 +00:00
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , a )
2021-04-19 17:42:19 +00:00
if err != nil {
return nil , err
}
2024-04-16 11:10:13 +00:00
// Ensure account stats populated.
2024-08-02 12:15:11 +00:00
if err := c . state . DB . PopulateAccountStats ( ctx , a ) ; err != nil {
return nil , gtserror . Newf (
"error getting stats for account %s: %w" ,
a . ID , err ,
)
2021-04-19 17:42:19 +00:00
}
2024-07-31 16:26:09 +00:00
// Populate the account's role permissions bitmap and highlightedness from its public role.
if len ( apiAccount . Roles ) > 0 {
apiAccount . Role = c . APIAccountDisplayRoleToAPIAccountRoleSensitive ( & apiAccount . Roles [ 0 ] )
} else {
apiAccount . Role = c . APIAccountDisplayRoleToAPIAccountRoleSensitive ( nil )
}
2023-03-02 11:06:40 +00:00
statusContentType := string ( apimodel . StatusContentTypeDefault )
2024-03-22 13:03:46 +00:00
if a . Settings . StatusContentType != "" {
statusContentType = a . Settings . StatusContentType
2022-08-06 10:09:21 +00:00
}
2023-01-02 12:10:50 +00:00
apiAccount . Source = & apimodel . Source {
2024-03-22 13:03:46 +00:00
Privacy : c . VisToAPIVis ( ctx , a . Settings . Privacy ) ,
2024-09-09 16:07:25 +00:00
WebVisibility : c . VisToAPIVis ( ctx , a . Settings . WebVisibility ) ,
2024-03-22 13:03:46 +00:00
Sensitive : * a . Settings . Sensitive ,
Language : a . Settings . Language ,
2023-03-02 11:06:40 +00:00
StatusContentType : statusContentType ,
2022-05-07 15:55:27 +00:00
Note : a . NoteRaw ,
2023-05-09 10:16:10 +00:00
Fields : c . fieldsToAPIFields ( a . FieldsRaw ) ,
2024-04-16 11:10:13 +00:00
FollowRequestsCount : * a . Stats . FollowRequestsCount ,
2024-01-16 16:22:44 +00:00
AlsoKnownAsURIs : a . AlsoKnownAsURIs ,
2021-04-19 17:42:19 +00:00
}
2021-10-04 13:24:19 +00:00
return apiAccount , nil
2021-04-19 17:42:19 +00:00
}
2023-09-23 16:44:11 +00:00
// AccountToAPIAccountPublic takes a db model account as a param, and returns a populated apitype account, or an error
// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.
// In other words, this is the public record that the server has of an account.
func ( c * Converter ) AccountToAPIAccountPublic ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2024-06-10 18:42:41 +00:00
account , err := c . accountToAPIAccountPublic ( ctx , a )
if err != nil {
return nil , err
}
if a . MovedTo != nil {
account . Moved , err = c . accountToAPIAccountPublic ( ctx , a . MovedTo )
if err != nil {
log . Errorf ( ctx , "error converting account movedTo: %v" , err )
}
}
return account , nil
}
2024-07-08 13:47:03 +00:00
// AccountToWebAccount converts a gts model account into an
// api representation suitable for serving into a web template.
//
// Should only be used when preparing to template an account,
// callers looking to serialize an account into a model for
// serving over the client API should always use one of the
// AccountToAPIAccount functions instead.
func ( c * Converter ) AccountToWebAccount (
ctx context . Context ,
a * gtsmodel . Account ,
2024-07-21 12:22:08 +00:00
) ( * apimodel . WebAccount , error ) {
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , a )
2024-07-08 13:47:03 +00:00
if err != nil {
return nil , err
}
2024-07-21 12:22:08 +00:00
webAccount := & apimodel . WebAccount {
Account : apiAccount ,
}
2024-07-08 13:47:03 +00:00
// Set additional avatar information for
2024-07-21 12:22:08 +00:00
// serving the avatar in a nice <picture>.
if ogAvi := a . AvatarMediaAttachment ; ogAvi != nil {
avatarAttachment , err := c . AttachmentToAPIAttachment ( ctx , ogAvi )
2024-07-08 13:47:03 +00:00
if err != nil {
// This is just extra data so just
// log but don't return any error.
log . Errorf ( ctx , "error converting account avatar attachment: %v" , err )
} else {
2024-07-21 12:22:08 +00:00
webAccount . AvatarAttachment = & apimodel . WebAttachment {
Attachment : & avatarAttachment ,
MIMEType : ogAvi . File . ContentType ,
PreviewMIMEType : ogAvi . Thumbnail . ContentType ,
}
}
}
// Set additional header information for
// serving the header in a nice <picture>.
if ogHeader := a . HeaderMediaAttachment ; ogHeader != nil {
headerAttachment , err := c . AttachmentToAPIAttachment ( ctx , ogHeader )
if err != nil {
// This is just extra data so just
// log but don't return any error.
log . Errorf ( ctx , "error converting account header attachment: %v" , err )
} else {
webAccount . HeaderAttachment = & apimodel . WebAttachment {
Attachment : & headerAttachment ,
MIMEType : ogHeader . File . ContentType ,
PreviewMIMEType : ogHeader . Thumbnail . ContentType ,
}
2024-07-08 13:47:03 +00:00
}
}
return webAccount , nil
}
2024-06-10 18:42:41 +00:00
// accountToAPIAccountPublic provides all the logic for AccountToAPIAccount, MINUS fetching moved account, to prevent possible recursion.
func ( c * Converter ) accountToAPIAccountPublic ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2024-04-16 11:10:13 +00:00
// Populate account struct fields.
err := c . state . DB . PopulateAccount ( ctx , a )
switch {
case err == nil :
// No problem.
2024-06-10 18:42:41 +00:00
case a . Stats != nil :
2024-04-16 11:10:13 +00:00
// We have stats so that's
// *maybe* OK, try to continue.
2023-05-07 17:53:21 +00:00
log . Errorf ( ctx , "error(s) populating account, will continue: %s" , err )
2024-04-16 11:10:13 +00:00
default :
// There was an error and we don't
// have stats, we can't continue.
return nil , gtserror . Newf ( "account stats not populated, could not continue: %w" , err )
2023-05-07 17:53:21 +00:00
}
// Basic account stats:
// - Followers count
// - Following count
// - Statuses count
// - Last status time
2024-04-16 11:10:13 +00:00
var (
followersCount = * a . Stats . FollowersCount
followingCount = * a . Stats . FollowingCount
statusesCount = * a . Stats . StatusesCount
lastStatusAt = func ( ) * string {
if a . Stats . LastStatusAt . IsZero ( ) {
return nil
}
2024-10-12 08:02:26 +00:00
return util . Ptr ( util . FormatISO8601Date ( a . Stats . LastStatusAt ) )
2024-04-16 11:10:13 +00:00
} ( )
)
2021-04-19 17:42:19 +00:00
2023-05-07 17:53:21 +00:00
// Profile media + nice extras:
// - Avatar
// - Header
// - Fields
// - Emojis
var (
2024-10-21 12:04:50 +00:00
aviID string
2023-05-07 17:53:21 +00:00
aviURL string
aviURLStatic string
2024-07-08 13:47:03 +00:00
aviDesc string
2024-10-21 12:04:50 +00:00
headerID string
2023-05-07 17:53:21 +00:00
headerURL string
headerURLStatic string
2024-07-08 13:47:03 +00:00
headerDesc string
2023-05-07 17:53:21 +00:00
)
if a . AvatarMediaAttachment != nil {
2024-10-21 12:04:50 +00:00
aviID = a . AvatarMediaAttachmentID
2023-05-07 17:53:21 +00:00
aviURL = a . AvatarMediaAttachment . URL
aviURLStatic = a . AvatarMediaAttachment . Thumbnail . URL
2024-07-08 13:47:03 +00:00
aviDesc = a . AvatarMediaAttachment . Description
2021-04-19 17:42:19 +00:00
}
2023-05-07 17:53:21 +00:00
if a . HeaderMediaAttachment != nil {
2024-10-21 12:04:50 +00:00
headerID = a . HeaderMediaAttachmentID
2023-05-07 17:53:21 +00:00
headerURL = a . HeaderMediaAttachment . URL
headerURLStatic = a . HeaderMediaAttachment . Thumbnail . URL
2024-07-08 13:47:03 +00:00
headerDesc = a . HeaderMediaAttachment . Description
2023-05-07 17:53:21 +00:00
}
2022-11-29 17:59:59 +00:00
2023-05-09 10:16:10 +00:00
// convert account gts model fields to front api model fields
fields := c . fieldsToAPIFields ( a . Fields )
2021-04-19 17:42:19 +00:00
2023-05-07 17:53:21 +00:00
// GTS model emojis -> frontend.
2022-11-29 17:59:59 +00:00
apiEmojis , err := c . convertEmojisToAPIEmojis ( ctx , a . Emojis , a . EmojiIDs )
if err != nil {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error converting account emojis: %v" , err )
2022-09-26 09:56:01 +00:00
}
2021-05-27 14:06:24 +00:00
2023-05-07 17:53:21 +00:00
// Bits that vary between remote + local accounts:
// - Account (acct) string.
// - Role.
2024-04-02 09:42:24 +00:00
// - Settings things (enableRSS, theme, customCSS, hideCollections).
2023-05-07 17:53:21 +00:00
var (
2024-04-02 09:42:24 +00:00
acct string
2024-07-31 16:26:09 +00:00
roles [ ] apimodel . AccountDisplayRole
2024-04-02 09:42:24 +00:00
enableRSS bool
theme string
customCSS string
hideCollections bool
2023-05-07 17:53:21 +00:00
)
if a . IsRemote ( ) {
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( a . Domain )
if err != nil {
2024-01-16 16:22:44 +00:00
return nil , gtserror . Newf ( "error de-punifying domain %s for account id %s: %w" , a . Domain , a . ID , err )
2023-05-07 17:53:21 +00:00
}
2022-11-15 09:19:32 +00:00
2023-05-07 17:53:21 +00:00
acct = a . Username + "@" + d
2021-04-19 17:42:19 +00:00
} else {
2023-05-09 15:05:35 +00:00
// This is a local account, try to
// fetch more info. Skip for instance
// accounts since they have no user.
if ! a . IsInstance ( ) {
2023-09-23 16:44:11 +00:00
user , err := c . state . DB . GetUserByAccountID ( ctx , a . ID )
2023-05-09 15:05:35 +00:00
if err != nil {
2024-01-16 16:22:44 +00:00
return nil , gtserror . Newf ( "error getting user from database for account id %s: %w" , a . ID , err )
2023-05-09 15:05:35 +00:00
}
2024-07-31 16:26:09 +00:00
if role := c . UserToAPIAccountDisplayRole ( user ) ; role != nil {
roles = append ( roles , * role )
2023-05-09 15:05:35 +00:00
}
2024-03-22 13:03:46 +00:00
enableRSS = * a . Settings . EnableRSS
2024-03-25 17:32:24 +00:00
theme = a . Settings . Theme
2024-03-22 13:03:46 +00:00
customCSS = a . Settings . CustomCSS
2024-04-02 09:42:24 +00:00
hideCollections = * a . Settings . HideCollections
2022-11-15 09:19:32 +00:00
}
2023-05-09 15:05:35 +00:00
acct = a . Username // omit domain
2021-04-19 17:42:19 +00:00
}
2024-01-19 13:02:04 +00:00
var (
2024-07-17 15:26:33 +00:00
locked = util . PtrOrValue ( a . Locked , true )
discoverable = util . PtrOrValue ( a . Discoverable , false )
bot = util . PtrOrValue ( a . Bot , false )
2024-01-19 13:02:04 +00:00
)
2023-05-07 17:53:21 +00:00
// Remaining properties are simple and
// can be populated directly below.
2021-07-11 14:22:21 +00:00
2023-01-02 12:10:50 +00:00
accountFrontend := & apimodel . Account {
2024-07-08 13:47:03 +00:00
ID : a . ID ,
Username : a . Username ,
Acct : acct ,
DisplayName : a . DisplayName ,
Locked : locked ,
Discoverable : discoverable ,
Bot : bot ,
CreatedAt : util . FormatISO8601 ( a . CreatedAt ) ,
Note : a . Note ,
URL : a . URL ,
Avatar : aviURL ,
AvatarStatic : aviURLStatic ,
AvatarDescription : aviDesc ,
2024-10-21 12:04:50 +00:00
AvatarMediaID : aviID ,
2024-07-08 13:47:03 +00:00
Header : headerURL ,
HeaderStatic : headerURLStatic ,
HeaderDescription : headerDesc ,
2024-10-21 12:04:50 +00:00
HeaderMediaID : headerID ,
2024-07-08 13:47:03 +00:00
FollowersCount : followersCount ,
FollowingCount : followingCount ,
StatusesCount : statusesCount ,
LastStatusAt : lastStatusAt ,
Emojis : apiEmojis ,
Fields : fields ,
Suspended : ! a . SuspendedAt . IsZero ( ) ,
Theme : theme ,
CustomCSS : customCSS ,
EnableRSS : enableRSS ,
HideCollections : hideCollections ,
2024-07-31 16:26:09 +00:00
Roles : roles ,
2021-08-20 10:26:56 +00:00
}
2023-05-07 17:53:21 +00:00
// Bodge default avatar + header in,
// if we didn't have one already.
2022-09-04 12:41:42 +00:00
c . ensureAvatar ( accountFrontend )
c . ensureHeader ( accountFrontend )
2021-08-20 10:26:56 +00:00
return accountFrontend , nil
2021-07-11 14:22:21 +00:00
}
2024-07-31 16:26:09 +00:00
// UserToAPIAccountDisplayRole returns the API representation of a user's display role.
// This will accept a nil user but does not always return a value:
// the default "user" role is considered uninteresting and not returned.
func ( c * Converter ) UserToAPIAccountDisplayRole ( user * gtsmodel . User ) * apimodel . AccountDisplayRole {
switch {
case user == nil :
return nil
case * user . Admin :
return & apimodel . AccountDisplayRole {
ID : string ( apimodel . AccountRoleAdmin ) ,
Name : apimodel . AccountRoleAdmin ,
}
case * user . Moderator :
return & apimodel . AccountDisplayRole {
ID : string ( apimodel . AccountRoleModerator ) ,
Name : apimodel . AccountRoleModerator ,
}
default :
return nil
}
}
// APIAccountDisplayRoleToAPIAccountRoleSensitive returns the API representation of a user's role,
// with permission bitmap. This will accept a nil display role and always returns a value.
func ( c * Converter ) APIAccountDisplayRoleToAPIAccountRoleSensitive ( display * apimodel . AccountDisplayRole ) * apimodel . AccountRole {
// Default to user role.
role := & apimodel . AccountRole {
AccountDisplayRole : apimodel . AccountDisplayRole {
ID : string ( apimodel . AccountRoleUser ) ,
Name : apimodel . AccountRoleUser ,
} ,
Permissions : apimodel . AccountRolePermissionsNone ,
Highlighted : false ,
}
// If there's a display role, use that instead.
if display != nil {
role . AccountDisplayRole = * display
role . Highlighted = true
switch display . Name {
case apimodel . AccountRoleAdmin :
role . Permissions = apimodel . AccountRolePermissionsForAdminRole
case apimodel . AccountRoleModerator :
role . Permissions = apimodel . AccountRolePermissionsForModeratorRole
}
}
return role
}
2023-09-23 16:44:11 +00:00
func ( c * Converter ) fieldsToAPIFields ( f [ ] * gtsmodel . Field ) [ ] apimodel . Field {
2023-05-09 10:16:10 +00:00
fields := make ( [ ] apimodel . Field , len ( f ) )
for i , field := range f {
mField := apimodel . Field {
Name : field . Name ,
Value : field . Value ,
}
if ! field . VerifiedAt . IsZero ( ) {
2024-06-10 18:42:41 +00:00
verified := util . FormatISO8601 ( field . VerifiedAt )
mField . VerifiedAt = util . Ptr ( verified )
2023-05-09 10:16:10 +00:00
}
fields [ i ] = mField
}
return fields
}
2023-09-23 16:44:11 +00:00
// AccountToAPIAccountBlocked takes a db model account as a param, and returns a apitype account, or an error if
// something goes wrong. The returned account will be a bare minimum representation of the account. This function should be used
// when someone wants to view an account they've blocked.
func ( c * Converter ) AccountToAPIAccountBlocked ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . Account , error ) {
2023-05-07 17:53:21 +00:00
var (
2024-07-31 16:26:09 +00:00
acct string
roles [ ] apimodel . AccountDisplayRole
2023-05-07 17:53:21 +00:00
)
if a . IsRemote ( ) {
// Domain may be in Punycode,
// de-punify it just in case.
d , err := util . DePunify ( a . Domain )
if err != nil {
2023-10-30 18:01:00 +00:00
return nil , gtserror . Newf ( "error de-punifying domain %s for account id %s: %w" , a . Domain , a . ID , err )
2023-05-07 17:53:21 +00:00
}
acct = a . Username + "@" + d
2021-07-11 14:22:21 +00:00
} else {
2023-05-09 15:05:35 +00:00
// This is a local account, try to
// fetch more info. Skip for instance
// accounts since they have no user.
if ! a . IsInstance ( ) {
2023-09-23 16:44:11 +00:00
user , err := c . state . DB . GetUserByAccountID ( ctx , a . ID )
2023-05-09 15:05:35 +00:00
if err != nil {
2023-10-30 18:01:00 +00:00
return nil , gtserror . Newf ( "error getting user from database for account id %s: %w" , a . ID , err )
2023-05-09 15:05:35 +00:00
}
2024-07-31 16:26:09 +00:00
if role := c . UserToAPIAccountDisplayRole ( user ) ; role != nil {
roles = append ( roles , * role )
2023-05-09 15:05:35 +00:00
}
2023-05-07 17:53:21 +00:00
}
2023-05-09 15:05:35 +00:00
acct = a . Username // omit domain
2021-07-11 14:22:21 +00:00
}
2023-10-30 18:01:00 +00:00
account := & apimodel . Account {
ID : a . ID ,
Username : a . Username ,
Acct : acct ,
Bot : * a . Bot ,
CreatedAt : util . FormatISO8601 ( a . CreatedAt ) ,
URL : a . URL ,
2024-06-06 10:22:16 +00:00
// Empty array (not nillable).
Emojis : make ( [ ] apimodel . Emoji , 0 ) ,
// Empty array (not nillable).
Fields : make ( [ ] apimodel . Field , 0 ) ,
2023-10-30 18:01:00 +00:00
Suspended : ! a . SuspendedAt . IsZero ( ) ,
2024-07-31 16:26:09 +00:00
Roles : roles ,
2023-10-30 18:01:00 +00:00
}
// Don't show the account's actual
// avatar+header since it may be
// upsetting to the blocker. Just
// show generic avatar+header instead.
c . ensureAvatar ( account )
c . ensureHeader ( account )
return account , nil
2021-04-19 17:42:19 +00:00
}
2023-09-23 16:44:11 +00:00
func ( c * Converter ) AccountToAdminAPIAccount ( ctx context . Context , a * gtsmodel . Account ) ( * apimodel . AdminAccountInfo , error ) {
2023-01-25 10:12:17 +00:00
var (
email string
ip * string
domain * string
locale string
confirmed bool
inviteRequest * string
approved bool
disabled bool
2024-07-31 16:26:09 +00:00
role = * c . APIAccountDisplayRoleToAPIAccountRoleSensitive ( nil )
2023-01-25 10:12:17 +00:00
createdByApplicationID string
)
2024-03-22 13:03:46 +00:00
if err := c . state . DB . PopulateAccount ( ctx , a ) ; err != nil {
log . Errorf ( ctx , "error(s) populating account, will continue: %s" , err )
}
2023-03-31 13:01:29 +00:00
if a . IsRemote ( ) {
2023-05-07 17:53:21 +00: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 15:05:35 +00:00
} else if ! a . IsInstance ( ) {
// This is a local, non-instance
// acct; we can fetch more info.
2023-09-23 16:44:11 +00:00
user , err := c . state . DB . GetUserByAccountID ( ctx , a . ID )
2023-01-25 10:12:17 +00:00
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
}
2024-04-11 09:45:53 +00:00
if i := user . SignUpIP . String ( ) ; i != "<nil>" {
2023-01-25 10:12:17 +00:00
ip = & i
}
locale = user . Locale
2024-04-11 09:45:53 +00:00
if user . Reason != "" {
inviteRequest = & user . Reason
2023-03-31 13:01:29 +00:00
}
2023-05-09 15:05:35 +00:00
2024-07-31 16:26:09 +00:00
role = * c . APIAccountDisplayRoleToAPIAccountRoleSensitive (
c . UserToAPIAccountDisplayRole ( user ) ,
)
2023-05-09 15:05:35 +00:00
2023-01-25 10:12:17 +00: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 16:00:44 +00:00
Role : role ,
2023-01-25 10:12:17 +00:00
Confirmed : confirmed ,
Approved : approved ,
Disabled : disabled ,
2023-05-07 17:53:21 +00:00
Silenced : ! a . SilencedAt . IsZero ( ) ,
Suspended : ! a . SuspendedAt . IsZero ( ) ,
2023-01-25 10:12:17 +00:00
Account : apiAccount ,
CreatedByApplicationID : createdByApplicationID ,
InvitedByAccountID : "" , // not implemented (yet)
} , nil
}
2023-09-23 16:44:11 +00:00
func ( c * Converter ) AppToAPIAppSensitive ( ctx context . Context , a * gtsmodel . Application ) ( * apimodel . Application , error ) {
2023-01-02 12:10:50 +00:00
return & apimodel . Application {
2021-04-19 17:42:19 +00:00
ID : a . ID ,
Name : a . Name ,
Website : a . Website ,
RedirectURI : a . RedirectURI ,
ClientID : a . ClientID ,
ClientSecret : a . ClientSecret ,
} , nil
}
2023-09-23 16:44:11 +00:00
// AppToAPIAppPublic takes a db model application as a param, and returns a populated apitype application, or an error
// if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive
// fields sanitized so that it can be served to non-authorized accounts without revealing any private information.
func ( c * Converter ) AppToAPIAppPublic ( ctx context . Context , a * gtsmodel . Application ) ( * apimodel . Application , error ) {
2023-01-02 12:10:50 +00:00
return & apimodel . Application {
2021-04-19 17:42:19 +00:00
Name : a . Name ,
Website : a . Website ,
} , nil
}
2023-09-23 16:44:11 +00:00
// AttachmentToAPIAttachment converts a gts model media attacahment into its api representation for serialization on the API.
2024-07-17 15:26:33 +00:00
func ( c * Converter ) AttachmentToAPIAttachment ( ctx context . Context , media * gtsmodel . MediaAttachment ) ( apimodel . Attachment , error ) {
var api apimodel . Attachment
api . Type = media . Type . String ( )
api . ID = media . ID
// Only add file details if
// we have stored locally.
if media . File . Path != "" {
api . Meta = new ( apimodel . MediaMeta )
api . Meta . Original = apimodel . MediaDimensions {
Width : media . FileMeta . Original . Width ,
Height : media . FileMeta . Original . Height ,
Aspect : media . FileMeta . Original . Aspect ,
Size : toAPISize ( media . FileMeta . Original . Width , media . FileMeta . Original . Height ) ,
FrameRate : toAPIFrameRate ( media . FileMeta . Original . Framerate ) ,
Duration : util . PtrOrZero ( media . FileMeta . Original . Duration ) ,
2024-10-16 12:13:58 +00:00
Bitrate : util . PtrOrZero ( media . FileMeta . Original . Bitrate ) ,
2024-07-17 15:26:33 +00:00
}
// Copy over local file URL.
api . URL = util . Ptr ( media . URL )
api . TextURL = util . Ptr ( media . URL )
// Set file focus details.
// (this doesn't make much sense if media
// has no image, but the API doesn't yet
// distinguish between zero values vs. none).
api . Meta . Focus = new ( apimodel . MediaFocus )
api . Meta . Focus . X = media . FileMeta . Focus . X
api . Meta . Focus . Y = media . FileMeta . Focus . Y
// Only add thumbnail details if
// we have thumbnail stored locally.
if media . Thumbnail . Path != "" {
api . Meta . Small = apimodel . MediaDimensions {
Width : media . FileMeta . Small . Width ,
Height : media . FileMeta . Small . Height ,
Aspect : media . FileMeta . Small . Aspect ,
Size : toAPISize ( media . FileMeta . Small . Width , media . FileMeta . Small . Height ) ,
}
2023-11-10 18:29:26 +00:00
2024-07-17 15:26:33 +00:00
// Copy over local thumbnail file URL.
api . PreviewURL = util . Ptr ( media . Thumbnail . URL )
2023-11-10 18:29:26 +00:00
}
}
2024-07-17 15:26:33 +00:00
// Set remaining API attachment fields.
api . Blurhash = util . PtrIf ( media . Blurhash )
api . RemoteURL = util . PtrIf ( media . RemoteURL )
api . PreviewRemoteURL = util . PtrIf ( media . Thumbnail . RemoteURL )
api . Description = util . PtrIf ( media . Description )
2022-12-22 10:48:28 +00:00
2024-07-17 15:26:33 +00:00
return api , nil
2021-04-19 17:42:19 +00:00
}
2023-09-23 16:44:11 +00:00
// MentionToAPIMention converts a gts model mention into its api (frontend) representation for serialization on the API.
func ( c * Converter ) MentionToAPIMention ( ctx context . Context , m * gtsmodel . Mention ) ( apimodel . Mention , error ) {
2021-08-25 13:34:33 +00:00
if m . TargetAccount == nil {
2023-09-23 16:44:11 +00:00
targetAccount , err := c . state . DB . GetAccountByID ( ctx , m . TargetAccountID )
2021-08-25 13:34:33 +00:00
if err != nil {
2023-01-02 12:10:50 +00:00
return apimodel . Mention { } , err
2021-08-25 13:34:33 +00:00
}
m . TargetAccount = targetAccount
2021-04-19 17:42:19 +00:00
}
var acct string
2023-05-07 17:53:21 +00:00
if m . TargetAccount . IsLocal ( ) {
2021-08-25 13:34:33 +00:00
acct = m . TargetAccount . Username
2021-04-19 17:42:19 +00:00
} else {
2023-05-07 17:53:21 +00: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 17:42:19 +00:00
}
2023-01-02 12:10:50 +00:00
return apimodel . Mention {
2021-08-25 13:34:33 +00:00
ID : m . TargetAccount . ID ,
Username : m . TargetAccount . Username ,
URL : m . TargetAccount . URL ,
2021-04-19 17:42:19 +00:00
Acct : acct ,
} , nil
}
2023-09-23 16:44:11 +00:00
// EmojiToAPIEmoji converts a gts model emoji into its api (frontend) representation for serialization on the API.
func ( c * Converter ) EmojiToAPIEmoji ( ctx context . Context , e * gtsmodel . Emoji ) ( apimodel . Emoji , error ) {
2022-11-14 22:47:27 +00:00
var category string
2024-07-17 15:26:33 +00:00
2022-11-14 22:47:27 +00:00
if e . CategoryID != "" {
if e . Category == nil {
var err error
2023-09-23 16:44:11 +00:00
e . Category , err = c . state . DB . GetEmojiCategory ( ctx , e . CategoryID )
2022-11-14 22:47:27 +00:00
if err != nil {
2023-01-02 12:10:50 +00:00
return apimodel . Emoji { } , err
2022-11-14 22:47:27 +00:00
}
}
category = e . Category . Name
}
2023-01-02 12:10:50 +00:00
return apimodel . Emoji {
2021-04-19 17:42:19 +00:00
Shortcode : e . Shortcode ,
URL : e . ImageURL ,
StaticURL : e . ImageStaticURL ,
2022-08-15 10:35:05 +00:00
VisibleInPicker : * e . VisibleInPicker ,
2022-11-14 22:47:27 +00:00
Category : category ,
2021-04-19 17:42:19 +00:00
} , nil
}
2023-09-23 16:44:11 +00:00
// EmojiToAdminAPIEmoji converts a gts model emoji into an API representation with extra admin information.
func ( c * Converter ) EmojiToAdminAPIEmoji ( ctx context . Context , e * gtsmodel . Emoji ) ( * apimodel . AdminEmoji , error ) {
2022-10-12 13:01:42 +00:00
emoji , err := c . EmojiToAPIEmoji ( ctx , e )
if err != nil {
return nil , err
}
2024-01-29 14:57:22 +00:00
if ! e . IsLocal ( ) {
2023-05-07 17:53:21 +00:00
// 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 12:10:50 +00:00
return & apimodel . AdminEmoji {
2022-10-12 13:01:42 +00: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-09-23 16:44:11 +00:00
// EmojiCategoryToAPIEmojiCategory converts a gts model emoji category into its api (frontend) representation.
func ( c * Converter ) EmojiCategoryToAPIEmojiCategory ( ctx context . Context , category * gtsmodel . EmojiCategory ) ( * apimodel . EmojiCategory , error ) {
2023-01-02 12:10:50 +00:00
return & apimodel . EmojiCategory {
2022-11-14 22:47:27 +00:00
ID : category . ID ,
Name : category . Name ,
} , nil
}
2023-09-23 16:44:11 +00:00
// TagToAPITag converts a gts model tag into its api (frontend) representation for serialization on the API.
// If stubHistory is set to 'true', then the 'history' field of the tag will be populated with a pointer to an empty slice, for API compatibility reasons.
2024-07-29 18:26:31 +00:00
// following is an optional flag marking whether the currently authenticated user (if there is one) is following the tag.
func ( c * Converter ) TagToAPITag ( ctx context . Context , t * gtsmodel . Tag , stubHistory bool , following * bool ) ( apimodel . Tag , error ) {
2023-01-02 12:10:50 +00:00
return apimodel . Tag {
2023-07-31 13:47:35 +00:00
Name : strings . ToLower ( t . Name ) ,
2023-11-10 18:29:26 +00:00
URL : uris . URIForTag ( t . Name ) ,
2023-07-31 13:47:35 +00:00
History : func ( ) * [ ] any {
if ! stubHistory {
return nil
}
h := make ( [ ] any , 0 )
return & h
} ( ) ,
2024-07-29 18:26:31 +00:00
Following : following ,
2021-04-19 17:42:19 +00:00
} , nil
}
2024-09-23 12:42:19 +00:00
// StatusToAPIStatus converts a gts model
// status into its api (frontend) representation
// for serialization on the API.
2023-11-10 18:29:26 +00:00
//
// Requesting account can be nil.
2024-05-06 11:49:08 +00:00
//
2024-09-23 12:42:19 +00:00
// filterContext can be the empty string
// if these statuses are not being filtered.
2024-05-06 11:49:08 +00:00
//
2024-09-23 12:42:19 +00:00
// If there is a matching "hide" filter, the returned
// status will be nil with a ErrHideStatus error; callers
// need to handle that case by excluding it from results.
2023-11-10 18:29:26 +00:00
func ( c * Converter ) StatusToAPIStatus (
ctx context . Context ,
2024-09-23 12:42:19 +00:00
status * gtsmodel . Status ,
requestingAccount * gtsmodel . Account ,
filterContext statusfilter . FilterContext ,
filters [ ] * gtsmodel . Filter ,
mutes * usermute . CompiledUserMuteList ,
) ( * apimodel . Status , error ) {
return c . statusToAPIStatus (
ctx ,
status ,
requestingAccount ,
filterContext ,
filters ,
mutes ,
true ,
true ,
)
}
// statusToAPIStatus is the package-internal implementation
// of StatusToAPIStatus that lets the caller customize whether
// to placehold unknown attachment types, and/or add a note
// about the status being pending and requiring approval.
func ( c * Converter ) statusToAPIStatus (
ctx context . Context ,
status * gtsmodel . Status ,
2023-11-10 18:29:26 +00:00
requestingAccount * gtsmodel . Account ,
2024-05-06 11:49:08 +00:00
filterContext statusfilter . FilterContext ,
filters [ ] * gtsmodel . Filter ,
2024-06-06 16:38:02 +00:00
mutes * usermute . CompiledUserMuteList ,
2024-09-23 12:42:19 +00:00
placeholdAttachments bool ,
addPendingNote bool ,
2023-11-10 18:29:26 +00:00
) ( * apimodel . Status , error ) {
2024-07-21 12:22:08 +00:00
apiStatus , err := c . statusToFrontend (
ctx ,
2024-09-23 12:42:19 +00:00
status ,
2024-07-21 12:22:08 +00:00
requestingAccount , // Can be nil.
filterContext , // Can be empty.
filters ,
mutes ,
)
2023-11-10 18:29:26 +00:00
if err != nil {
return nil , err
}
2024-07-21 12:22:08 +00:00
// Convert author to API model.
2024-09-23 12:42:19 +00:00
acct , err := c . AccountToAPIAccountPublic ( ctx , status . Account )
2024-07-21 12:22:08 +00:00
if err != nil {
return nil , gtserror . Newf ( "error converting status acct: %w" , err )
}
apiStatus . Account = acct
// Convert author of boosted
// status (if set) to API model.
if apiStatus . Reblog != nil {
2024-09-23 12:42:19 +00:00
boostAcct , err := c . AccountToAPIAccountPublic ( ctx , status . BoostOfAccount )
2024-07-21 12:22:08 +00:00
if err != nil {
return nil , gtserror . Newf ( "error converting boost acct: %w" , err )
}
apiStatus . Reblog . Account = boostAcct
}
2024-09-23 12:42:19 +00:00
if placeholdAttachments {
// Normalize status for API by pruning attachments
// that were not able to be locally stored, and replacing
// them with a helpful message + links to remote.
var attachNote string
attachNote , apiStatus . MediaAttachments = placeholderAttachments ( apiStatus . MediaAttachments )
apiStatus . Content += attachNote
// Do the same for the reblogged status.
if apiStatus . Reblog != nil {
attachNote , apiStatus . Reblog . MediaAttachments = placeholderAttachments ( apiStatus . Reblog . MediaAttachments )
apiStatus . Reblog . Content += attachNote
}
}
if addPendingNote {
// If this status is pending approval and
// replies to the requester, add a note
// about how to approve or reject the reply.
pendingApproval := util . PtrOrValue ( status . PendingApproval , false )
if pendingApproval &&
requestingAccount != nil &&
requestingAccount . ID == status . InReplyToAccountID {
pendingNote , err := c . pendingReplyNote ( ctx , status )
if err != nil {
return nil , gtserror . Newf ( "error deriving 'pending reply' note: %w" , err )
}
apiStatus . Content += pendingNote
}
2024-06-10 18:42:41 +00:00
}
2023-11-10 18:29:26 +00:00
return apiStatus , nil
}
2024-06-06 16:38:02 +00:00
// statusToAPIFilterResults applies filters and mutes to a status and returns an API filter result object.
2024-05-06 11:49:08 +00:00
// The result may be nil if no filters matched.
// If the status should not be returned at all, it returns the ErrHideStatus error.
func ( c * Converter ) statusToAPIFilterResults (
ctx context . Context ,
s * gtsmodel . Status ,
requestingAccount * gtsmodel . Account ,
filterContext statusfilter . FilterContext ,
filters [ ] * gtsmodel . Filter ,
2024-06-06 16:38:02 +00:00
mutes * usermute . CompiledUserMuteList ,
2024-05-06 11:49:08 +00:00
) ( [ ] apimodel . FilterResult , error ) {
2024-06-06 16:38:02 +00:00
// If there are no filters or mutes, we're done.
// We never hide statuses authored by the requesting account,
// since not being able to see your own posts is confusing.
if filterContext == "" || ( len ( filters ) == 0 && mutes . Len ( ) == 0 ) || s . AccountID == requestingAccount . ID {
2024-05-06 11:49:08 +00:00
return nil , nil
}
2024-06-06 16:38:02 +00:00
// Both mutes and filters can expire.
2024-05-06 11:49:08 +00:00
now := time . Now ( )
2024-06-06 16:38:02 +00:00
// If the requesting account mutes the account that created this status, hide the status.
if mutes . Matches ( s . AccountID , filterContext , now ) {
return nil , statusfilter . ErrHideStatus
}
2024-07-23 19:51:07 +00:00
2024-06-06 16:38:02 +00:00
// If this status is part of a multi-account discussion,
// and all of the accounts replied to or mentioned are invisible to the requesting account
// (due to blocks, domain blocks, moderation, etc.),
// or are muted, hide the status.
// First, collect the accounts we have to check.
otherAccounts := make ( [ ] * gtsmodel . Account , 0 , 1 + len ( s . Mentions ) )
if s . InReplyToAccount != nil {
otherAccounts = append ( otherAccounts , s . InReplyToAccount )
}
for _ , mention := range s . Mentions {
otherAccounts = append ( otherAccounts , mention . TargetAccount )
}
2024-07-30 09:29:32 +00:00
2024-06-06 16:38:02 +00:00
// If there are no other accounts, skip this check.
if len ( otherAccounts ) > 0 {
// Start by assuming that they're all invisible or muted.
allOtherAccountsInvisibleOrMuted := true
for _ , account := range otherAccounts {
// Is this account visible?
2024-07-24 11:27:42 +00:00
visible , err := c . visFilter . AccountVisible ( ctx , requestingAccount , account )
2024-06-06 16:38:02 +00:00
if err != nil {
return nil , err
}
if ! visible {
// It's invisible. Check the next account.
continue
}
// If visible, is it muted?
if mutes . Matches ( account . ID , filterContext , now ) {
// It's muted. Check the next account.
continue
}
// If we get here, the account is visible and not muted.
// We should show this status, and don't have to check any more accounts.
allOtherAccountsInvisibleOrMuted = false
break
}
// If we didn't find any visible non-muted accounts, hide the status.
if allOtherAccountsInvisibleOrMuted {
return nil , statusfilter . ErrHideStatus
}
}
// At this point, the status isn't muted, but might still be filtered.
2024-09-15 08:42:04 +00:00
if len ( filters ) == 0 {
// If it can't be filtered because there are no filters, we're done.
return nil , nil
}
2024-09-16 12:00:23 +00:00
// Key this status based on ID + last updated time,
// to ensure we always filter on latest version.
statusKey := s . ID + strconv . FormatInt ( s . UpdatedAt . Unix ( ) , 10 )
// Check if we have filterable fields cached for this status.
cache := c . state . Caches . StatusesFilterableFields
fields , stored := cache . Get ( statusKey )
if ! stored {
// We don't have filterable fields
// cached, calculate + cache now.
fields = filterableFields ( s )
cache . Set ( statusKey , fields )
}
2024-09-15 08:42:04 +00:00
2024-06-06 16:38:02 +00:00
// Record all matching warn filters and the reasons they matched.
filterResults := make ( [ ] apimodel . FilterResult , 0 , len ( filters ) )
2024-05-06 11:49:08 +00:00
for _ , filter := range filters {
if ! filterAppliesInContext ( filter , filterContext ) {
2024-09-16 12:00:23 +00:00
// Filter doesn't apply
// to this context.
2024-05-06 11:49:08 +00:00
continue
}
2024-09-16 12:00:23 +00:00
2024-06-06 18:16:20 +00:00
if filter . Expired ( now ) {
2024-09-16 12:00:23 +00:00
// Filter doesn't
// apply anymore.
2024-05-06 11:49:08 +00:00
continue
}
2024-09-16 12:00:23 +00:00
// Assemble matching keywords (if any) from this filter.
2024-05-06 11:49:08 +00:00
keywordMatches := make ( [ ] string , 0 , len ( filter . Keywords ) )
2024-09-16 12:00:23 +00:00
for _ , keyword := range filter . Keywords {
// Check if at least one filterable field
// in the status matches on this filter.
if slices . ContainsFunc (
fields ,
func ( field string ) bool {
return keyword . Regexp . MatchString ( field )
} ,
) {
// At least one field matched on this filter.
keywordMatches = append ( keywordMatches , keyword . Keyword )
2024-05-06 11:49:08 +00:00
}
}
// A status has only one ID. Not clear why this is a list in the Mastodon API.
statusMatches := make ( [ ] string , 0 , 1 )
for _ , filterStatus := range filter . Statuses {
if s . ID == filterStatus . StatusID {
statusMatches = append ( statusMatches , filterStatus . StatusID )
break
}
}
if len ( keywordMatches ) > 0 || len ( statusMatches ) > 0 {
switch filter . Action {
case gtsmodel . FilterActionWarn :
// Record what matched.
apiFilter , err := c . FilterToAPIFilterV2 ( ctx , filter )
if err != nil {
return nil , err
}
filterResults = append ( filterResults , apimodel . FilterResult {
Filter : * apiFilter ,
KeywordMatches : keywordMatches ,
StatusMatches : statusMatches ,
} )
case gtsmodel . FilterActionHide :
// Don't show this status. Immediate return.
return nil , statusfilter . ErrHideStatus
}
}
}
return filterResults , nil
}
// filterAppliesInContext returns whether a given filter applies in a given context.
func filterAppliesInContext ( filter * gtsmodel . Filter , filterContext statusfilter . FilterContext ) bool {
switch filterContext {
case statusfilter . FilterContextHome :
2024-07-17 15:26:33 +00:00
return util . PtrOrValue ( filter . ContextHome , false )
2024-05-06 11:49:08 +00:00
case statusfilter . FilterContextNotifications :
2024-07-17 15:26:33 +00:00
return util . PtrOrValue ( filter . ContextNotifications , false )
2024-05-06 11:49:08 +00:00
case statusfilter . FilterContextPublic :
2024-07-17 15:26:33 +00:00
return util . PtrOrValue ( filter . ContextPublic , false )
2024-05-06 11:49:08 +00:00
case statusfilter . FilterContextThread :
2024-07-17 15:26:33 +00:00
return util . PtrOrValue ( filter . ContextThread , false )
2024-05-06 11:49:08 +00:00
case statusfilter . FilterContextAccount :
2024-07-17 15:26:33 +00:00
return util . PtrOrValue ( filter . ContextAccount , false )
2024-05-06 11:49:08 +00:00
}
return false
}
2023-11-10 18:29:26 +00:00
// StatusToWebStatus converts a gts model status into an
// api representation suitable for serving into a web template.
//
// Requesting account can be nil.
func ( c * Converter ) StatusToWebStatus (
ctx context . Context ,
s * gtsmodel . Status ,
2024-07-12 18:36:03 +00:00
) ( * apimodel . WebStatus , error ) {
2024-07-21 12:22:08 +00:00
apiStatus , err := c . statusToFrontend ( ctx , s ,
nil , // No authed requester.
statusfilter . FilterContextNone , // No filters.
nil , // No filters.
nil , // No mutes.
2024-07-12 18:36:03 +00:00
)
2023-11-17 10:35:28 +00:00
if err != nil {
return nil , err
}
2024-07-21 12:22:08 +00:00
// Convert status author to web model.
acct , err := c . AccountToWebAccount ( ctx , s . Account )
if err != nil {
return nil , err
}
2024-07-12 18:36:03 +00:00
webStatus := & apimodel . WebStatus {
2024-07-21 12:22:08 +00:00
Status : apiStatus ,
Account : acct ,
2024-07-12 18:36:03 +00:00
}
2023-12-27 10:23:52 +00:00
// Whack a newline before and after each "pre" to make it easier to outdent it.
webStatus . Content = strings . ReplaceAll ( webStatus . Content , "<pre>" , "\n<pre>" )
webStatus . Content = strings . ReplaceAll ( webStatus . Content , "</pre>" , "</pre>\n" )
2023-11-17 10:35:28 +00:00
// Add additional information for template.
// Assume empty langs, hope for not empty language.
webStatus . LanguageTag = new ( language . Language )
if lang := webStatus . Language ; lang != nil {
langTag , err := language . Parse ( * lang )
if err != nil {
log . Warnf (
ctx ,
"error parsing %s as language tag: %v" ,
* lang , err ,
)
} else {
webStatus . LanguageTag = langTag
}
}
2023-11-22 11:17:42 +00:00
if poll := webStatus . Poll ; poll != nil {
// Calculate vote share of each poll option and
// format them for easier template consumption.
totalVotes := poll . VotesCount
2024-07-12 18:36:03 +00:00
PollOptions := make ( [ ] apimodel . WebPollOption , len ( poll . Options ) )
2023-11-22 11:17:42 +00:00
for i , option := range poll . Options {
var voteShare float32
2023-12-12 13:47:07 +00:00
if totalVotes != 0 && option . VotesCount != nil {
voteShare = float32 ( * option . VotesCount ) / float32 ( totalVotes ) * 100
2023-11-22 11:17:42 +00:00
}
// Format to two decimal points and ditch any
// trailing zeroes.
//
// We want to be precise enough that eg., "1.54%"
// is distinct from "1.68%" in polls with loads
// of votes.
//
// However, if we've got eg., a two-option poll
// in which each option has half the votes, then
// "50%" looks better than "50.00%".
//
// By the same token, it's pointless to show
// "0.00%" or "100.00%".
voteShareStr := fmt . Sprintf ( "%.2f" , voteShare )
voteShareStr = strings . TrimSuffix ( voteShareStr , ".00" )
webPollOption := apimodel . WebPollOption {
PollOption : option ,
2023-11-22 15:27:32 +00:00
PollID : poll . ID ,
2023-11-22 11:17:42 +00:00
Emojis : webStatus . Emojis ,
LanguageTag : webStatus . LanguageTag ,
VoteShare : voteShare ,
VoteShareStr : voteShareStr ,
}
2024-07-12 18:36:03 +00:00
PollOptions [ i ] = webPollOption
2023-11-22 11:17:42 +00:00
}
2024-07-12 18:36:03 +00:00
webStatus . PollOptions = PollOptions
2023-11-22 11:17:42 +00:00
}
2024-07-15 09:47:57 +00:00
// Mark local.
webStatus . Local = * s . Local
2023-12-05 11:43:07 +00:00
// Set additional templating
// variables on media attachments.
2024-07-15 09:47:57 +00:00
// Get gtsmodel attachments
// into a convenient map.
ogAttachments := make (
map [ string ] * gtsmodel . MediaAttachment ,
len ( s . Attachments ) ,
)
for _ , a := range s . Attachments {
ogAttachments [ a . ID ] = a
2023-12-05 11:43:07 +00:00
}
2024-07-15 09:47:57 +00:00
// Convert each API attachment
// into a web attachment.
webStatus . MediaAttachments = make (
[ ] * apimodel . WebAttachment ,
len ( apiStatus . MediaAttachments ) ,
)
for i , apiAttachment := range apiStatus . MediaAttachments {
ogAttachment := ogAttachments [ apiAttachment . ID ]
webStatus . MediaAttachments [ i ] = & apimodel . WebAttachment {
2024-07-21 12:22:08 +00:00
Attachment : apiAttachment ,
Sensitive : apiStatus . Sensitive ,
MIMEType : ogAttachment . File . ContentType ,
PreviewMIMEType : ogAttachment . Thumbnail . ContentType ,
2024-07-15 09:47:57 +00:00
}
}
2023-12-27 10:23:52 +00:00
2023-11-17 10:35:28 +00:00
return webStatus , nil
2023-11-10 18:29:26 +00:00
}
2024-04-17 12:49:20 +00:00
// StatusToAPIStatusSource returns the *apimodel.StatusSource of the given status.
// Callers should check beforehand whether a requester has permission to view the
// source of the status, and ensure they're passing only a local status into this function.
func ( c * Converter ) StatusToAPIStatusSource ( ctx context . Context , s * gtsmodel . Status ) ( * apimodel . StatusSource , error ) {
2024-04-18 11:22:55 +00:00
// TODO: remove this when edit support is added.
text := "**STATUS EDITS ARE NOT CURRENTLY SUPPORTED IN GOTOSOCIAL (coming in 2024)**\n" +
"You can review the original text of your status below, but you will not be able to submit this edit.\n\n---\n\n" + s . Text
2024-04-17 12:49:20 +00:00
return & apimodel . StatusSource {
ID : s . ID ,
2024-04-18 11:22:55 +00:00
Text : text ,
2024-04-17 12:49:20 +00:00
SpoilerText : s . ContentWarning ,
} , nil
}
2023-11-10 18:29:26 +00:00
// statusToFrontend is a package internal function for
// parsing a status into its initial frontend representation.
2023-09-23 16:44:11 +00:00
//
// Requesting account can be nil.
2024-07-21 12:22:08 +00:00
//
// This function also doesn't handle converting the
// account to api/web model -- the caller must do that.
2023-11-10 18:29:26 +00:00
func ( c * Converter ) statusToFrontend (
2024-06-10 18:42:41 +00:00
ctx context . Context ,
status * gtsmodel . Status ,
requestingAccount * gtsmodel . Account ,
filterContext statusfilter . FilterContext ,
filters [ ] * gtsmodel . Filter ,
mutes * usermute . CompiledUserMuteList ,
) (
* apimodel . Status ,
error ,
) {
apiStatus , err := c . baseStatusToFrontend ( ctx ,
status ,
requestingAccount ,
filterContext ,
filters ,
mutes ,
)
if err != nil {
return nil , err
}
if status . BoostOf != nil {
reblog , err := c . baseStatusToFrontend ( ctx ,
status . BoostOf ,
requestingAccount ,
filterContext ,
filters ,
mutes ,
)
if errors . Is ( err , statusfilter . ErrHideStatus ) {
// If we'd hide the original status, hide the boost.
return nil , err
} else if err != nil {
return nil , gtserror . Newf ( "error converting boosted status: %w" , err )
}
2024-07-23 19:51:07 +00:00
// Set boosted status and set interactions and filter results from original.
2024-06-10 18:42:41 +00:00
apiStatus . Reblog = & apimodel . StatusReblogged { reblog }
apiStatus . Favourited = apiStatus . Reblog . Favourited
apiStatus . Bookmarked = apiStatus . Reblog . Bookmarked
apiStatus . Muted = apiStatus . Reblog . Muted
apiStatus . Reblogged = apiStatus . Reblog . Reblogged
apiStatus . Pinned = apiStatus . Reblog . Pinned
2024-07-23 19:51:07 +00:00
apiStatus . Filtered = apiStatus . Reblog . Filtered
2024-06-10 18:42:41 +00:00
}
return apiStatus , nil
}
// baseStatusToFrontend performs the main logic
// of statusToFrontend() without handling of boost
// logic, to prevent *possible* recursion issues.
2024-07-21 12:22:08 +00:00
//
// This function also doesn't handle converting the
// account to api/web model -- the caller must do that.
2024-06-10 18:42:41 +00:00
func ( c * Converter ) baseStatusToFrontend (
2023-11-10 18:29:26 +00:00
ctx context . Context ,
s * gtsmodel . Status ,
requestingAccount * gtsmodel . Account ,
2024-05-06 11:49:08 +00:00
filterContext statusfilter . FilterContext ,
filters [ ] * gtsmodel . Filter ,
2024-06-06 16:38:02 +00:00
mutes * usermute . CompiledUserMuteList ,
2024-06-10 18:42:41 +00:00
) (
* apimodel . Status ,
error ,
) {
2023-12-01 14:27:15 +00:00
// Try to populate status struct pointer fields.
// We can continue in many cases of partial failure,
// but there are some fields we actually need.
2023-09-23 16:44:11 +00:00
if err := c . state . DB . PopulateStatus ( ctx , s ) ; err != nil {
2024-06-10 18:42:41 +00:00
switch {
case s . Account == nil :
return nil , gtserror . Newf ( "error(s) populating status, required account not set: %w" , err )
2023-12-01 14:27:15 +00:00
2024-06-10 18:42:41 +00:00
case s . BoostOfID != "" && s . BoostOf == nil :
return nil , gtserror . Newf ( "error(s) populating status, required boost not set: %w" , err )
2021-04-19 17:42:19 +00:00
2024-06-10 18:42:41 +00:00
default :
log . Errorf ( ctx , "error(s) populating status, will continue: %v" , err )
}
2021-04-19 17:42:19 +00:00
}
2023-09-23 16:44:11 +00:00
repliesCount , err := c . state . DB . CountStatusReplies ( ctx , s . ID )
2023-05-09 11:25:48 +00:00
if err != nil {
2023-11-10 18:29:26 +00:00
return nil , gtserror . Newf ( "error counting replies: %w" , err )
2021-05-08 13:16:24 +00:00
}
2021-04-19 17:42:19 +00:00
2023-09-23 16:44:11 +00:00
reblogsCount , err := c . state . DB . CountStatusBoosts ( ctx , s . ID )
2023-05-09 11:25:48 +00:00
if err != nil {
2023-11-10 18:29:26 +00:00
return nil , gtserror . Newf ( "error counting reblogs: %w" , err )
2021-04-19 17:42:19 +00:00
}
2023-09-23 16:44:11 +00:00
favesCount , err := c . state . DB . CountStatusFaves ( ctx , s . ID )
2023-05-09 11:25:48 +00:00
if err != nil {
2023-11-10 18:29:26 +00:00
return nil , gtserror . Newf ( "error counting faves: %w" , err )
2021-06-17 16:02:33 +00:00
}
2022-11-29 17:59:59 +00:00
apiAttachments , err := c . convertAttachmentsToAPIAttachments ( ctx , s . Attachments , s . AttachmentIDs )
if err != nil {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error converting status attachments: %v" , err )
2021-04-19 17:42:19 +00:00
}
2022-11-29 17:59:59 +00:00
apiMentions , err := c . convertMentionsToAPIMentions ( ctx , s . Mentions , s . MentionIDs )
if err != nil {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error converting status mentions: %v" , err )
2021-04-19 17:42:19 +00:00
}
2022-11-29 17:59:59 +00:00
apiTags , err := c . convertTagsToAPITags ( ctx , s . Tags , s . TagIDs )
if err != nil {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error converting status tags: %v" , err )
2021-04-19 17:42:19 +00:00
}
2022-11-29 17:59:59 +00:00
apiEmojis , err := c . convertEmojisToAPIEmojis ( ctx , s . Emojis , s . EmojiIDs )
if err != nil {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error converting status emojis: %v" , err )
2021-04-19 17:42:19 +00:00
}
2024-07-17 14:46:52 +00:00
// Take status's interaction policy, or
// fall back to default for its visibility.
var p * gtsmodel . InteractionPolicy
if s . InteractionPolicy != nil {
p = s . InteractionPolicy
} else {
p = gtsmodel . DefaultInteractionPolicyFor ( s . Visibility )
}
apiInteractionPolicy , err := c . InteractionPolicyToAPIInteractionPolicy ( ctx , p , s , requestingAccount )
if err != nil {
return nil , gtserror . Newf ( "error converting interaction policy: %w" , err )
}
2023-01-02 12:10:50 +00:00
apiStatus := & apimodel . Status {
2021-04-19 17:42:19 +00:00
ID : s . ID ,
2022-05-24 16:21:27 +00:00
CreatedAt : util . FormatISO8601 ( s . CreatedAt ) ,
2023-12-01 14:27:15 +00:00
InReplyToID : nil , // Set below.
InReplyToAccountID : nil , // Set below.
2022-08-15 10:35:05 +00:00
Sensitive : * s . Sensitive ,
2021-04-19 17:42:19 +00:00
SpoilerText : s . ContentWarning ,
2021-10-04 13:24:19 +00:00
Visibility : c . VisToAPIVis ( ctx , s . Visibility ) ,
2024-08-22 17:47:10 +00:00
LocalOnly : s . IsLocalOnly ( ) ,
2023-12-01 14:27:15 +00:00
Language : nil , // Set below.
2021-04-19 17:42:19 +00:00
URI : s . URI ,
URL : s . URL ,
RepliesCount : repliesCount ,
ReblogsCount : reblogsCount ,
FavouritesCount : favesCount ,
Content : s . Content ,
2023-12-01 14:27:15 +00:00
Reblog : nil , // Set below.
Application : nil , // Set below.
2024-07-21 12:22:08 +00:00
Account : nil , // Caller must do this.
2021-10-04 13:24:19 +00:00
MediaAttachments : apiAttachments ,
Mentions : apiMentions ,
Tags : apiTags ,
Emojis : apiEmojis ,
2022-09-02 15:00:11 +00:00
Card : nil , // TODO: implement cards
2021-04-19 17:42:19 +00:00
Text : s . Text ,
2024-07-17 14:46:52 +00:00
InteractionPolicy : * apiInteractionPolicy ,
2021-08-02 17:06:44 +00:00
}
2023-05-09 11:25:48 +00:00
// Nullable fields.
2024-12-05 13:35:07 +00:00
if ! s . UpdatedAt . Equal ( s . CreatedAt ) {
timestamp := util . FormatISO8601 ( s . UpdatedAt )
apiStatus . EditedAt = util . Ptr ( timestamp )
2023-05-09 11:25:48 +00:00
}
2024-12-05 13:35:07 +00:00
apiStatus . InReplyToID = util . PtrIf ( s . InReplyToID )
apiStatus . InReplyToAccountID = util . PtrIf ( s . InReplyToAccountID )
apiStatus . Language = util . PtrIf ( s . Language )
2023-05-09 11:25:48 +00:00
2023-12-01 14:27:15 +00:00
if app := s . CreatedWithApplication ; app != nil {
apiStatus . Application , err = c . AppToAPIAppPublic ( ctx , app )
2023-05-09 11:25:48 +00:00
if err != nil {
2023-12-01 14:27:15 +00:00
return nil , gtserror . Newf (
"error converting application %s: %w" ,
s . CreatedWithApplicationID , err ,
)
2023-05-09 11:25:48 +00:00
}
}
2023-11-08 14:32:17 +00:00
if s . Poll != nil {
// Set originating
// status on the poll.
poll := s . Poll
poll . Status = s
apiStatus . Poll , err = c . PollToAPIPoll ( ctx , requestingAccount , poll )
if err != nil {
return nil , fmt . Errorf ( "error converting poll: %w" , err )
}
}
2024-04-17 10:41:40 +00:00
// Status interactions.
//
2024-06-10 18:42:41 +00:00
if s . BoostOf != nil { //nolint
// populated *outside* this
// function to prevent recursion.
2024-04-17 10:41:40 +00:00
} else {
interacts , err := c . interactionsWithStatusForAccount ( ctx , s , requestingAccount )
if err != nil {
log . Errorf ( ctx ,
"error getting interactions for status %s for account %s: %v" ,
s . ID , requestingAccount . ID , err ,
)
// Ensure non-nil object.
interacts = new ( statusInteractions )
}
apiStatus . Favourited = interacts . Favourited
apiStatus . Bookmarked = interacts . Bookmarked
apiStatus . Muted = interacts . Muted
apiStatus . Reblogged = interacts . Reblogged
apiStatus . Pinned = interacts . Pinned
}
2023-11-10 18:29:26 +00:00
// If web URL is empty for whatever
// reason, provide AP URI as fallback.
2023-05-09 11:25:48 +00:00
if s . URL == "" {
s . URL = s . URI
2021-08-02 17:06:44 +00:00
}
2024-05-06 11:49:08 +00:00
// Apply filters.
2024-06-06 16:38:02 +00:00
filterResults , err := c . statusToAPIFilterResults ( ctx , s , requestingAccount , filterContext , filters , mutes )
2024-05-06 11:49:08 +00:00
if err != nil {
2024-06-06 16:38:02 +00:00
if errors . Is ( err , statusfilter . ErrHideStatus ) {
return nil , err
}
2024-05-06 11:49:08 +00:00
return nil , fmt . Errorf ( "error applying filters: %w" , err )
}
2024-06-10 18:42:41 +00:00
2024-05-06 11:49:08 +00:00
apiStatus . Filtered = filterResults
2021-08-02 17:06:44 +00:00
return apiStatus , nil
2021-04-19 17:42:19 +00:00
}
2021-05-08 12:25:55 +00:00
2023-09-23 16:44:11 +00:00
// VisToAPIVis converts a gts visibility into its api equivalent
func ( c * Converter ) VisToAPIVis ( ctx context . Context , m gtsmodel . Visibility ) apimodel . Visibility {
2021-05-08 12:25:55 +00:00
switch m {
case gtsmodel . VisibilityPublic :
2023-01-02 12:10:50 +00:00
return apimodel . VisibilityPublic
2021-05-08 12:25:55 +00:00
case gtsmodel . VisibilityUnlocked :
2023-01-02 12:10:50 +00:00
return apimodel . VisibilityUnlisted
2021-05-08 12:25:55 +00:00
case gtsmodel . VisibilityFollowersOnly , gtsmodel . VisibilityMutualsOnly :
2023-01-02 12:10:50 +00:00
return apimodel . VisibilityPrivate
2021-05-08 12:25:55 +00:00
case gtsmodel . VisibilityDirect :
2023-01-02 12:10:50 +00:00
return apimodel . VisibilityDirect
2021-05-08 12:25:55 +00:00
}
return ""
}
2021-05-09 12:06:06 +00:00
2023-09-23 16:44:11 +00:00
// InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id
func ( c * Converter ) InstanceRuleToAPIRule ( r gtsmodel . Rule ) apimodel . InstanceRule {
2023-08-19 12:33:15 +00:00
return apimodel . InstanceRule {
ID : r . ID ,
Text : r . Text ,
}
}
2023-09-23 16:44:11 +00:00
// InstanceRulesToAPIRules converts all local instance rules into their api equivalent for serving at /api/v1/instance/rules
func ( c * Converter ) InstanceRulesToAPIRules ( r [ ] gtsmodel . Rule ) [ ] apimodel . InstanceRule {
2023-08-19 12:33:15 +00:00
rules := make ( [ ] apimodel . InstanceRule , len ( r ) )
for i , v := range r {
rules [ i ] = c . InstanceRuleToAPIRule ( v )
}
return rules
}
2023-09-23 16:44:11 +00:00
// InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id
func ( c * Converter ) InstanceRuleToAdminAPIRule ( r * gtsmodel . Rule ) * apimodel . AdminInstanceRule {
2023-08-19 12:33:15 +00:00
return & apimodel . AdminInstanceRule {
ID : r . ID ,
CreatedAt : util . FormatISO8601 ( r . CreatedAt ) ,
UpdatedAt : util . FormatISO8601 ( r . UpdatedAt ) ,
Text : r . Text ,
}
}
2023-09-23 16:44:11 +00:00
// InstanceToAPIV1Instance converts a gts instance into its api equivalent for serving at /api/v1/instance
func ( c * Converter ) InstanceToAPIV1Instance ( ctx context . Context , i * gtsmodel . Instance ) ( * apimodel . InstanceV1 , error ) {
2024-10-22 14:47:28 +00:00
domain := i . Domain
accDomain := config . GetAccountDomain ( )
if accDomain != "" {
domain = accDomain
}
2023-02-02 13:08:13 +00:00
instance := & apimodel . InstanceV1 {
2024-10-22 14:47:28 +00:00
URI : domain ,
AccountDomain : accDomain ,
2024-01-05 12:39:31 +00:00
Title : i . Title ,
Description : i . Description ,
DescriptionText : i . DescriptionText ,
2024-12-02 11:24:48 +00:00
CustomCSS : i . CustomCSS ,
2024-01-05 12:39:31 +00:00
ShortDescription : i . ShortDescription ,
ShortDescriptionText : i . ShortDescriptionText ,
Email : i . ContactEmail ,
Version : config . GetSoftwareVersion ( ) ,
Languages : config . GetInstanceLanguages ( ) . TagStrs ( ) ,
Registrations : config . GetAccountsRegistrationOpen ( ) ,
2024-10-16 12:13:58 +00:00
ApprovalRequired : true , // approval always required
InvitesEnabled : false , // todo: not supported yet
MaxTootChars : uint ( config . GetStatusesMaxChars ( ) ) , // #nosec G115 -- Already validated.
2024-01-05 12:39:31 +00:00
Rules : c . InstanceRulesToAPIRules ( i . Rules ) ,
Terms : i . Terms ,
TermsRaw : i . TermsText ,
2023-02-02 13:08:13 +00:00
}
2023-07-21 17:49:13 +00:00
if config . GetInstanceInjectMastodonVersion ( ) {
instance . Version = toMastodonVersion ( instance . Version )
}
2024-06-03 09:20:53 +00:00
if debug . DEBUG {
instance . Debug = util . Ptr ( true )
}
2023-02-02 13:08:13 +00:00
// configuration
instance . Configuration . Statuses . MaxCharacters = config . GetStatusesMaxChars ( )
instance . Configuration . Statuses . MaxMediaAttachments = config . GetStatusesMediaMaxFiles ( )
instance . Configuration . Statuses . CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
2023-03-02 11:06:40 +00:00
instance . Configuration . Statuses . SupportedMimeTypes = instanceStatusesSupportedMimeTypes
2023-02-02 13:08:13 +00:00
instance . Configuration . MediaAttachments . SupportedMimeTypes = media . SupportedMIMETypes
2024-11-04 14:00:10 +00:00
// NOTE: we use the local max sizes here
// as it hints to apps like Tusky for image
// compression of locally uploaded media.
//
// TODO: return local / remote depending
// on authorized endpoint user (if any)?
localMax := config . GetMediaLocalMaxSize ( )
imageSz := cmp . Or ( config . GetMediaImageSizeHint ( ) , localMax )
videoSz := cmp . Or ( config . GetMediaVideoSizeHint ( ) , localMax )
instance . Configuration . MediaAttachments . ImageSizeLimit = int ( imageSz ) // #nosec G115 -- Already validated.
instance . Configuration . MediaAttachments . VideoSizeLimit = int ( videoSz ) // #nosec G115 -- Already validated.
// we don't actually set any limits on these. set to max possible.
2024-11-05 22:16:06 +00:00
instance . Configuration . MediaAttachments . ImageMatrixLimit = math . MaxInt32
instance . Configuration . MediaAttachments . VideoFrameRateLimit = math . MaxInt32
instance . Configuration . MediaAttachments . VideoMatrixLimit = math . MaxInt32
2024-11-04 14:00:10 +00:00
2023-02-02 13:08:13 +00:00
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 10:21:26 +00:00
instance . Configuration . Accounts . MaxProfileFields = instanceAccountsMaxProfileFields
2024-10-16 12:13:58 +00:00
instance . Configuration . Emojis . EmojiSizeLimit = int ( config . GetMediaEmojiLocalMaxSize ( ) ) // #nosec G115 -- Already validated.
2024-06-07 14:21:57 +00:00
instance . Configuration . OIDCEnabled = config . GetOIDCEnabled ( )
2023-02-02 13:08:13 +00:00
// URLs
instance . URLs . StreamingAPI = "wss://" + i . Domain
// statistics
2024-02-19 12:17:14 +00:00
stats := make ( map [ string ] * int , 3 )
2023-09-23 16:44:11 +00:00
userCount , err := c . state . DB . CountInstanceUsers ( ctx , i . Domain )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting counting instance users: %w" , err )
}
2024-02-19 12:17:14 +00:00
stats [ "user_count" ] = util . Ptr ( userCount )
2023-02-02 13:08:13 +00:00
2023-09-23 16:44:11 +00:00
statusCount , err := c . state . DB . CountInstanceStatuses ( ctx , i . Domain )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting counting instance statuses: %w" , err )
}
2024-02-19 12:17:14 +00:00
stats [ "status_count" ] = util . Ptr ( statusCount )
2023-02-02 13:08:13 +00:00
2023-09-23 16:44:11 +00:00
domainCount , err := c . state . DB . CountInstanceDomains ( ctx , i . Domain )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting counting instance domains: %w" , err )
}
2024-02-19 12:17:14 +00:00
stats [ "domain_count" ] = util . Ptr ( domainCount )
2023-02-02 13:08:13 +00:00
instance . Stats = stats
// thumbnail
2023-09-23 16:44:11 +00:00
iAccount , err := c . state . DB . GetInstanceAccount ( ctx , "" )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV1Instance: db error getting instance account: %w" , err )
}
if iAccount . AvatarMediaAttachmentID != "" {
if iAccount . AvatarMediaAttachment == nil {
2023-09-23 16:44:11 +00:00
avi , err := c . state . DB . GetAttachmentByID ( ctx , iAccount . AvatarMediaAttachmentID )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIInstance: error getting instance avatar attachment with id %s: %w" , iAccount . AvatarMediaAttachmentID , err )
2022-06-26 10:33:11 +00:00
}
2023-02-02 13:08:13 +00:00
iAccount . AvatarMediaAttachment = avi
2022-06-26 10:33:11 +00:00
}
2023-02-02 13:08:13 +00:00
instance . Thumbnail = iAccount . AvatarMediaAttachment . URL
instance . ThumbnailType = iAccount . AvatarMediaAttachment . File . ContentType
2024-07-21 12:22:08 +00:00
instance . ThumbnailStatic = iAccount . AvatarMediaAttachment . Thumbnail . URL
instance . ThumbnailStaticType = iAccount . AvatarMediaAttachment . Thumbnail . ContentType
2023-02-02 13:08:13 +00:00
instance . ThumbnailDescription = iAccount . AvatarMediaAttachment . Description
} else {
2024-07-20 13:02:22 +00:00
instance . Thumbnail = config . GetProtocol ( ) + "://" + i . Domain + "/assets/logo.webp" // default thumb
2023-02-02 13:08:13 +00:00
}
2021-06-23 14:35:57 +00:00
2023-02-02 13:08:13 +00:00
// contact account
if i . ContactAccountID != "" {
if i . ContactAccount == nil {
2023-09-23 16:44:11 +00:00
contactAccount , err := c . state . DB . GetAccountByID ( ctx , i . ContactAccountID )
2023-02-02 13:08:13 +00:00
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 14:35:57 +00:00
}
2023-02-02 13:08:13 +00: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 14:35:57 +00:00
}
2023-02-02 13:08:13 +00:00
instance . ContactAccount = account
}
2021-06-23 14:35:57 +00:00
2023-02-02 13:08:13 +00:00
return instance , nil
}
2023-09-23 16:44:11 +00:00
// InstanceToAPIV2Instance converts a gts instance into its api equivalent for serving at /api/v2/instance
func ( c * Converter ) InstanceToAPIV2Instance ( ctx context . Context , i * gtsmodel . Instance ) ( * apimodel . InstanceV2 , error ) {
2024-10-22 14:47:28 +00:00
domain := i . Domain
accDomain := config . GetAccountDomain ( )
if accDomain != "" {
domain = accDomain
}
2023-02-02 13:08:13 +00:00
instance := & apimodel . InstanceV2 {
2024-10-22 14:47:28 +00:00
Domain : domain ,
AccountDomain : accDomain ,
2024-01-05 12:39:31 +00:00
Title : i . Title ,
Version : config . GetSoftwareVersion ( ) ,
SourceURL : instanceSourceURL ,
Description : i . Description ,
DescriptionText : i . DescriptionText ,
2024-12-02 11:24:48 +00:00
CustomCSS : i . CustomCSS ,
2024-01-05 12:39:31 +00:00
Usage : apimodel . InstanceV2Usage { } , // todo: not implemented
Languages : config . GetInstanceLanguages ( ) . TagStrs ( ) ,
Rules : c . InstanceRulesToAPIRules ( i . Rules ) ,
Terms : i . Terms ,
TermsText : i . TermsText ,
2023-02-02 13:08:13 +00:00
}
2023-07-21 17:49:13 +00:00
if config . GetInstanceInjectMastodonVersion ( ) {
instance . Version = toMastodonVersion ( instance . Version )
}
2024-06-03 09:20:53 +00:00
if debug . DEBUG {
instance . Debug = util . Ptr ( true )
}
2023-02-02 13:08:13 +00:00
// thumbnail
thumbnail := apimodel . InstanceV2Thumbnail { }
2023-09-23 16:44:11 +00:00
iAccount , err := c . state . DB . GetInstanceAccount ( ctx , "" )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV2Instance: db error getting instance account: %w" , err )
2021-05-09 12:06:06 +00:00
}
2023-02-02 13:08:13 +00:00
if iAccount . AvatarMediaAttachmentID != "" {
if iAccount . AvatarMediaAttachment == nil {
2023-09-23 16:44:11 +00:00
avi , err := c . state . DB . GetAttachmentByID ( ctx , iAccount . AvatarMediaAttachmentID )
2023-02-02 13:08:13 +00:00
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
2024-07-21 12:22:08 +00:00
thumbnail . StaticURL = iAccount . AvatarMediaAttachment . Thumbnail . URL
thumbnail . StaticType = iAccount . AvatarMediaAttachment . Thumbnail . ContentType
2023-02-02 13:08:13 +00:00
thumbnail . Description = iAccount . AvatarMediaAttachment . Description
thumbnail . Blurhash = iAccount . AvatarMediaAttachment . Blurhash
} else {
2024-07-20 13:02:22 +00:00
thumbnail . URL = config . GetProtocol ( ) + "://" + i . Domain + "/assets/logo.webp" // default thumb
2023-02-02 13:08:13 +00:00
}
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 11:06:40 +00:00
instance . Configuration . Statuses . SupportedMimeTypes = instanceStatusesSupportedMimeTypes
2023-02-02 13:08:13 +00:00
instance . Configuration . MediaAttachments . SupportedMimeTypes = media . SupportedMIMETypes
2024-11-04 14:00:10 +00:00
// NOTE: we use the local max sizes here
// as it hints to apps like Tusky for image
// compression of locally uploaded media.
//
// TODO: return local / remote depending
// on authorized endpoint user (if any)?
localMax := config . GetMediaLocalMaxSize ( )
imageSz := cmp . Or ( config . GetMediaImageSizeHint ( ) , localMax )
videoSz := cmp . Or ( config . GetMediaVideoSizeHint ( ) , localMax )
instance . Configuration . MediaAttachments . ImageSizeLimit = int ( imageSz ) // #nosec G115 -- Already validated.
instance . Configuration . MediaAttachments . VideoSizeLimit = int ( videoSz ) // #nosec G115 -- Already validated.
// we don't actually set any limits on these. set to max possible.
2024-11-05 22:16:06 +00:00
instance . Configuration . MediaAttachments . ImageMatrixLimit = math . MaxInt32
instance . Configuration . MediaAttachments . VideoFrameRateLimit = math . MaxInt32
instance . Configuration . MediaAttachments . VideoMatrixLimit = math . MaxInt32
2024-11-04 14:00:10 +00:00
2023-02-02 13:08:13 +00:00
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 10:21:26 +00:00
instance . Configuration . Accounts . MaxProfileFields = instanceAccountsMaxProfileFields
2024-10-16 12:13:58 +00:00
instance . Configuration . Emojis . EmojiSizeLimit = int ( config . GetMediaEmojiLocalMaxSize ( ) ) // #nosec G115 -- Already validated.
2024-06-07 14:21:57 +00:00
instance . Configuration . OIDCEnabled = config . GetOIDCEnabled ( )
2023-02-02 13:08:13 +00:00
// registrations
instance . Registrations . Enabled = config . GetAccountsRegistrationOpen ( )
2024-04-11 09:45:53 +00:00
instance . Registrations . ApprovalRequired = true // always required
instance . Registrations . Message = nil // todo: not implemented
2023-02-02 13:08:13 +00:00
// contact
instance . Contact . Email = i . ContactEmail
2021-05-09 12:06:06 +00:00
if i . ContactAccountID != "" {
2021-08-25 13:34:33 +00:00
if i . ContactAccount == nil {
2023-09-23 16:44:11 +00:00
contactAccount , err := c . state . DB . GetAccountByID ( ctx , i . ContactAccountID )
2023-02-02 13:08:13 +00:00
if err != nil {
return nil , fmt . Errorf ( "InstanceToAPIV2Instance: db error getting instance contact account %s: %w" , i . ContactAccountID , err )
2021-05-09 12:06:06 +00:00
}
2023-02-02 13:08:13 +00:00
i . ContactAccount = contactAccount
2021-05-09 12:06:06 +00:00
}
2023-02-02 13:08:13 +00: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 13:34:33 +00:00
}
2023-02-02 13:08:13 +00:00
instance . Contact . Account = account
2021-05-09 12:06:06 +00:00
}
2023-02-02 13:08:13 +00:00
return instance , nil
2021-05-09 12:06:06 +00:00
}
2021-05-21 13:48:26 +00:00
2023-09-23 16:44:11 +00:00
// RelationshipToAPIRelationship converts a gts relationship into its api equivalent for serving in various places
func ( c * Converter ) RelationshipToAPIRelationship ( ctx context . Context , r * gtsmodel . Relationship ) ( * apimodel . Relationship , error ) {
2023-01-02 12:10:50 +00:00
return & apimodel . Relationship {
2021-05-21 21:04:59 +00: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 13:48:26 +00:00
MutingNotifications : r . MutingNotifications ,
2021-05-21 21:04:59 +00:00
Requested : r . Requested ,
2024-02-20 17:50:54 +00:00
RequestedBy : r . RequestedBy ,
2021-05-21 21:04:59 +00:00
DomainBlocking : r . DomainBlocking ,
Endorsed : r . Endorsed ,
Note : r . Note ,
2021-05-21 13:48:26 +00:00
} , nil
}
2021-05-27 14:06:24 +00:00
2023-09-23 16:44:11 +00:00
// NotificationToAPINotification converts a gts notification into a api notification
2024-06-06 16:38:02 +00:00
func ( c * Converter ) NotificationToAPINotification (
ctx context . Context ,
n * gtsmodel . Notification ,
filters [ ] * gtsmodel . Filter ,
mutes * usermute . CompiledUserMuteList ,
) ( * apimodel . Notification , error ) {
2021-08-20 10:26:56 +00:00
if n . TargetAccount == nil {
2023-09-23 16:44:11 +00:00
tAccount , err := c . state . DB . GetAccountByID ( ctx , n . TargetAccountID )
2021-08-20 10:26:56 +00:00
if err != nil {
2021-10-04 13:24:19 +00:00
return nil , fmt . Errorf ( "NotificationToapi: error getting target account with id %s from the db: %s" , n . TargetAccountID , err )
2021-05-27 14:06:24 +00:00
}
2021-08-20 10:26:56 +00:00
n . TargetAccount = tAccount
2021-05-27 14:06:24 +00:00
}
2021-08-20 10:26:56 +00:00
if n . OriginAccount == nil {
2023-09-23 16:44:11 +00:00
ogAccount , err := c . state . DB . GetAccountByID ( ctx , n . OriginAccountID )
2021-08-20 10:26:56 +00:00
if err != nil {
2021-10-04 13:24:19 +00:00
return nil , fmt . Errorf ( "NotificationToapi: error getting origin account with id %s from the db: %s" , n . OriginAccountID , err )
2021-05-27 14:06:24 +00:00
}
2021-08-20 10:26:56 +00:00
n . OriginAccount = ogAccount
2021-05-27 14:06:24 +00:00
}
2021-08-20 10:26:56 +00:00
2021-10-04 13:24:19 +00:00
apiAccount , err := c . AccountToAPIAccountPublic ( ctx , n . OriginAccount )
2021-05-27 14:06:24 +00:00
if err != nil {
2021-10-04 13:24:19 +00:00
return nil , fmt . Errorf ( "NotificationToapi: error converting account to api: %s" , err )
2021-05-27 14:06:24 +00:00
}
2023-01-02 12:10:50 +00:00
var apiStatus * apimodel . Status
2021-05-27 14:06:24 +00:00
if n . StatusID != "" {
2021-08-20 10:26:56 +00:00
if n . Status == nil {
2023-09-23 16:44:11 +00:00
status , err := c . state . DB . GetStatusByID ( ctx , n . StatusID )
2021-08-20 10:26:56 +00:00
if err != nil {
2021-10-04 13:24:19 +00:00
return nil , fmt . Errorf ( "NotificationToapi: error getting status with id %s from the db: %s" , n . StatusID , err )
2021-05-27 14:06:24 +00:00
}
2021-08-20 10:26:56 +00:00
n . Status = status
2021-05-27 14:06:24 +00:00
}
2021-08-20 10:26:56 +00: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 14:06:24 +00:00
}
}
var err error
2024-06-06 16:38:02 +00:00
apiStatus , err = c . StatusToAPIStatus ( ctx , n . Status , n . TargetAccount , statusfilter . FilterContextNotifications , filters , mutes )
2021-05-27 14:06:24 +00:00
if err != nil {
2024-06-06 16:38:02 +00:00
if errors . Is ( err , statusfilter . ErrHideStatus ) {
return nil , err
}
2021-10-04 13:24:19 +00:00
return nil , fmt . Errorf ( "NotificationToapi: error converting status to api: %s" , err )
2021-05-27 14:06:24 +00:00
}
}
2022-08-29 09:06:37 +00:00
if apiStatus != nil && apiStatus . Reblog != nil {
// use the actual reblog status for the notifications endpoint
apiStatus = apiStatus . Reblog . Status
}
2023-01-02 12:10:50 +00:00
return & apimodel . Notification {
2021-05-27 14:06:24 +00:00
ID : n . ID ,
2024-11-25 13:48:59 +00:00
Type : n . NotificationType . String ( ) ,
2022-05-24 16:21:27 +00:00
CreatedAt : util . FormatISO8601 ( n . CreatedAt ) ,
2021-10-04 13:24:19 +00:00
Account : apiAccount ,
Status : apiStatus ,
2021-05-27 14:06:24 +00:00
} , nil
}
2021-07-05 11:23:03 +00:00
2024-07-23 19:44:31 +00:00
// ConversationToAPIConversation converts a conversation into its API representation.
// The conversation status will be filtered using the notification filter context,
// and may be nil if the status was hidden.
func ( c * Converter ) ConversationToAPIConversation (
ctx context . Context ,
conversation * gtsmodel . Conversation ,
2024-10-04 17:22:52 +00:00
requester * gtsmodel . Account ,
2024-07-23 19:44:31 +00:00
filters [ ] * gtsmodel . Filter ,
mutes * usermute . CompiledUserMuteList ,
) ( * apimodel . Conversation , error ) {
apiConversation := & apimodel . Conversation {
2024-10-04 17:22:52 +00:00
ID : conversation . ID ,
Unread : ! * conversation . Read ,
2024-07-23 19:44:31 +00:00
}
2024-10-04 17:22:52 +00:00
// Populate most recent status in convo;
// can be nil if this status is filtered.
2024-07-23 19:44:31 +00:00
if conversation . LastStatus != nil {
var err error
apiConversation . LastStatus , err = c . StatusToAPIStatus (
ctx ,
conversation . LastStatus ,
2024-10-04 17:22:52 +00:00
requester ,
2024-07-23 19:44:31 +00:00
statusfilter . FilterContextNotifications ,
filters ,
mutes ,
)
if err != nil && ! errors . Is ( err , statusfilter . ErrHideStatus ) {
return nil , gtserror . Newf (
"error converting status %s to API representation: %w" ,
conversation . LastStatus . ID ,
err ,
)
}
}
2024-10-04 17:22:52 +00:00
// If no other accounts are involved in this convo,
// just include the requesting account and return.
//
// See: https://github.com/superseriousbusiness/gotosocial/issues/3385#issuecomment-2394033477
otherAcctsLen := len ( conversation . OtherAccounts )
if otherAcctsLen == 0 {
apiAcct , err := c . AccountToAPIAccountPublic ( ctx , requester )
if err != nil {
err := gtserror . Newf (
"error converting account %s to API representation: %w" ,
requester . ID , err ,
)
return nil , err
}
apiConversation . Accounts = [ ] apimodel . Account { * apiAcct }
return apiConversation , nil
}
// Other accounts are involved in the
// convo. Convert each to API model.
apiConversation . Accounts = make ( [ ] apimodel . Account , otherAcctsLen )
for i , account := range conversation . OtherAccounts {
blocked , err := c . state . DB . IsEitherBlocked ( ctx ,
requester . ID , account . ID ,
)
if err != nil {
err := gtserror . Newf (
"db error checking blocks between accounts %s and %s: %w" ,
requester . ID , account . ID , err ,
)
return nil , err
}
// API account model varies depending
// on status of conversation participant.
var apiAcct * apimodel . Account
if blocked || account . IsSuspended ( ) {
apiAcct , err = c . AccountToAPIAccountBlocked ( ctx , account )
} else {
apiAcct , err = c . AccountToAPIAccountPublic ( ctx , account )
}
if err != nil {
err := gtserror . Newf (
"error converting account %s to API representation: %w" ,
account . ID , err ,
)
return nil , err
}
apiConversation . Accounts [ i ] = * apiAcct
}
2024-07-23 19:44:31 +00:00
return apiConversation , nil
}
2024-11-21 13:09:58 +00:00
// DomainPermToAPIDomainPerm converts a gtsmodel domain block,
// allow, draft, or ignore into an api domain permission.
2023-09-23 16:44:11 +00:00
func ( c * Converter ) DomainPermToAPIDomainPerm (
2023-09-21 10:12:04 +00:00
ctx context . Context ,
d gtsmodel . DomainPermission ,
export bool ,
) ( * apimodel . DomainPermission , error ) {
2023-05-07 17:53:21 +00:00
// Domain may be in Punycode,
// de-punify it just in case.
2023-09-21 10:12:04 +00:00
domain , err := util . DePunify ( d . GetDomain ( ) )
2023-05-07 17:53:21 +00:00
if err != nil {
2023-09-21 10:12:04 +00:00
return nil , gtserror . Newf ( "error de-punifying domain %s: %w" , d . GetDomain ( ) , err )
2023-05-07 17:53:21 +00:00
}
2023-09-21 10:12:04 +00:00
domainPerm := & apimodel . DomainPermission {
2023-01-02 12:10:50 +00:00
Domain : apimodel . Domain {
2023-09-21 10:12:04 +00:00
Domain : domain ,
PublicComment : d . GetPublicComment ( ) ,
2022-06-23 14:54:54 +00:00
} ,
2021-07-05 11:23:03 +00:00
}
2023-09-21 10:12:04 +00:00
// If we're exporting, provide
// only bare minimum detail.
if export {
return domainPerm , nil
2021-07-05 11:23:03 +00:00
}
2023-09-21 10:12:04 +00:00
domainPerm . ID = d . GetID ( )
domainPerm . Obfuscate = * d . GetObfuscate ( )
domainPerm . PrivateComment = d . GetPrivateComment ( )
domainPerm . SubscriptionID = d . GetSubscriptionID ( )
domainPerm . CreatedBy = d . GetCreatedByAccountID ( )
domainPerm . CreatedAt = util . FormatISO8601 ( d . GetCreatedAt ( ) )
2024-11-21 13:09:58 +00:00
// If this is a draft, also add the permission type.
if _ , ok := d . ( * gtsmodel . DomainPermissionDraft ) ; ok {
domainPerm . PermissionType = d . GetType ( ) . String ( )
}
2023-09-21 10:12:04 +00:00
return domainPerm , nil
2021-07-05 11:23:03 +00:00
}
2022-11-29 17:59:59 +00:00
2024-12-14 14:01:58 +00:00
func ( c * Converter ) DomainPermSubToAPIDomainPermSub (
ctx context . Context ,
d * gtsmodel . DomainPermissionSubscription ,
) ( * apimodel . DomainPermissionSubscription , error ) {
createdAt , err := id . TimeFromULID ( d . ID )
if err != nil {
return nil , gtserror . Newf ( "error converting id to time: %w" , err )
}
// URI may be in Punycode,
// de-punify it just in case.
uri , err := util . DePunify ( d . URI )
if err != nil {
return nil , gtserror . Newf ( "error de-punifying URI %s: %w" , d . URI , err )
}
var (
fetchedAt string
successfullyFetchedAt string
)
if ! d . FetchedAt . IsZero ( ) {
fetchedAt = util . FormatISO8601 ( d . FetchedAt )
}
if ! d . SuccessfullyFetchedAt . IsZero ( ) {
successfullyFetchedAt = util . FormatISO8601 ( d . SuccessfullyFetchedAt )
}
return & apimodel . DomainPermissionSubscription {
ID : d . ID ,
Priority : d . Priority ,
Title : d . Title ,
PermissionType : d . PermissionType . String ( ) ,
AsDraft : * d . AsDraft ,
AdoptOrphans : * d . AdoptOrphans ,
CreatedBy : d . CreatedByAccountID ,
CreatedAt : util . FormatISO8601 ( createdAt ) ,
URI : uri ,
ContentType : d . ContentType . String ( ) ,
FetchUsername : d . FetchUsername ,
FetchPassword : d . FetchPassword ,
FetchedAt : fetchedAt ,
SuccessfullyFetchedAt : successfullyFetchedAt ,
Error : d . Error ,
} , nil
}
2023-09-23 16:44:11 +00:00
// ReportToAPIReport converts a gts model report into an api model report, for serving at /api/v1/reports
func ( c * Converter ) ReportToAPIReport ( ctx context . Context , r * gtsmodel . Report ) ( * apimodel . Report , error ) {
2023-01-23 12:14:21 +00:00
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 ,
2023-08-19 12:33:15 +00:00
RuleIDs : r . RuleIDs ,
2023-01-23 12:14:21 +00:00
}
if ! r . ActionTakenAt . IsZero ( ) {
actionTakenAt := util . FormatISO8601 ( r . ActionTakenAt )
report . ActionTakenAt = & actionTakenAt
}
if actionComment := r . ActionTaken ; actionComment != "" {
2023-01-25 10:12:17 +00:00
report . ActionTakenComment = & actionComment
2023-01-23 12:14:21 +00:00
}
if r . TargetAccount == nil {
2023-09-23 16:44:11 +00:00
tAccount , err := c . state . DB . GetAccountByID ( ctx , r . TargetAccountID )
2023-01-23 12:14:21 +00:00
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-09-23 16:44:11 +00:00
// ReportToAdminAPIReport converts a gts model report into an admin view report, for serving at /api/v1/admin/reports
func ( c * Converter ) ReportToAdminAPIReport ( ctx context . Context , r * gtsmodel . Report , requestingAccount * gtsmodel . Account ) ( * apimodel . AdminReport , error ) {
2023-01-25 10:12:17 +00:00
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 {
2023-09-23 16:44:11 +00:00
r . Account , err = c . state . DB . GetAccountByID ( ctx , r . AccountID )
2023-01-25 10:12:17 +00:00
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 {
2023-09-23 16:44:11 +00:00
r . TargetAccount , err = c . state . DB . GetAccountByID ( ctx , r . TargetAccountID )
2023-01-25 10:12:17 +00:00
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 {
2023-09-23 16:44:11 +00:00
r . ActionTakenByAccount , err = c . state . DB . GetAccountByID ( ctx , r . ActionTakenByAccountID )
2023-01-25 10:12:17 +00:00
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 {
2023-09-23 16:44:11 +00:00
r . Statuses , err = c . state . DB . GetStatusesByIDs ( ctx , r . StatusIDs )
2023-01-25 10:12:17 +00:00
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error getting statuses from the db: %w" , err )
}
}
for _ , s := range r . Statuses {
2024-09-23 12:42:19 +00:00
status , err := c . statusToAPIStatus (
ctx ,
s ,
requestingAccount ,
statusfilter . FilterContextNone ,
nil , // No filters.
nil , // No mutes.
true , // Placehold unknown attachments.
// Don't add note about
// pending, it's not
// relevant here.
false ,
)
2023-01-25 10:12:17 +00:00
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 )
}
2023-08-19 12:33:15 +00:00
rules := make ( [ ] * apimodel . InstanceRule , 0 , len ( r . RuleIDs ) )
if len ( r . RuleIDs ) != 0 && len ( r . Rules ) == 0 {
2023-09-23 16:44:11 +00:00
r . Rules , err = c . state . DB . GetRulesByIDs ( ctx , r . RuleIDs )
2023-08-19 12:33:15 +00:00
if err != nil {
return nil , fmt . Errorf ( "ReportToAdminAPIReport: error getting rules from the db: %w" , err )
}
}
for _ , v := range r . Rules {
rules = append ( rules , & apimodel . InstanceRule {
ID : v . ID ,
Text : v . Text ,
} )
}
2023-01-25 10:12:17 +00:00
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 ,
2023-08-19 12:33:15 +00:00
Rules : rules ,
2023-01-25 10:12:17 +00:00
} , nil
}
2023-09-23 16:44:11 +00:00
// ListToAPIList converts one gts model list into an api model list, for serving at /api/v1/lists/{id}
func ( c * Converter ) ListToAPIList ( ctx context . Context , l * gtsmodel . List ) ( * apimodel . List , error ) {
2023-05-25 08:37:38 +00:00
return & apimodel . List {
ID : l . ID ,
Title : l . Title ,
RepliesPolicy : string ( l . RepliesPolicy ) ,
2024-09-09 22:56:58 +00:00
Exclusive : * l . Exclusive ,
2023-05-25 08:37:38 +00:00
} , nil
}
2023-09-23 16:44:11 +00:00
// MarkersToAPIMarker converts several gts model markers into an api marker, for serving at /api/v1/markers
func ( c * Converter ) MarkersToAPIMarker ( ctx context . Context , markers [ ] * gtsmodel . Marker ) ( * apimodel . Marker , error ) {
2023-07-29 10:49:14 +00:00
apiMarker := & apimodel . Marker { }
for _ , marker := range markers {
apiTimelineMarker := & apimodel . TimelineMarker {
LastReadID : marker . LastReadID ,
UpdatedAt : util . FormatISO8601 ( marker . UpdatedAt ) ,
Version : marker . Version ,
}
switch apimodel . MarkerName ( marker . Name ) {
case apimodel . MarkerNameHome :
apiMarker . Home = apiTimelineMarker
case apimodel . MarkerNameNotifications :
apiMarker . Notifications = apiTimelineMarker
default :
return nil , fmt . Errorf ( "unknown marker timeline name: %s" , marker . Name )
}
}
return apiMarker , nil
}
2023-11-08 14:32:17 +00:00
// PollToAPIPoll converts a database (gtsmodel) Poll into an API model representation appropriate for the given requesting account.
func ( c * Converter ) PollToAPIPoll ( ctx context . Context , requester * gtsmodel . Account , poll * gtsmodel . Poll ) ( * apimodel . Poll , error ) {
// Ensure the poll model is fully populated for src status.
if err := c . state . DB . PopulatePoll ( ctx , poll ) ; err != nil {
return nil , gtserror . Newf ( "error populating poll: %w" , err )
}
var (
2023-11-08 22:37:35 +00:00
options [ ] apimodel . PollOption
2023-11-08 14:32:17 +00:00
totalVotes int
2023-12-12 13:47:07 +00:00
totalVoters * int
hasVoted * bool
2023-11-09 12:06:37 +00:00
ownChoices * [ ] int
2023-11-08 14:32:17 +00:00
isAuthor bool
2023-12-12 13:47:07 +00:00
expiresAt * string
2023-11-09 12:06:37 +00:00
emojis [ ] apimodel . Emoji
2023-11-08 14:32:17 +00:00
)
2023-11-08 22:37:35 +00:00
// Preallocate a slice of frontend model poll choices.
options = make ( [ ] apimodel . PollOption , len ( poll . Options ) )
// Add the titles to all of the options.
for i , title := range poll . Options {
options [ i ] . Title = title
}
2023-11-08 14:32:17 +00:00
if requester != nil {
// Get vote by requester in poll (if any).
vote , err := c . state . DB . GetPollVoteBy ( ctx ,
poll . ID ,
requester . ID ,
)
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , gtserror . Newf ( "error getting vote for poll %s: %w" , poll . ID , err )
}
if vote != nil {
// Set choices by requester.
2023-11-09 12:06:37 +00:00
ownChoices = & vote . Choices
2023-11-08 14:32:17 +00:00
2023-12-12 13:47:07 +00:00
// Update default total in the
// case that counts are hidden
// (so we just show our own).
2023-11-08 14:32:17 +00:00
totalVotes = len ( vote . Choices )
2023-11-09 12:06:37 +00:00
} else {
2023-12-12 13:47:07 +00:00
// Requester hasn't yet voted, use
// empty slice to serialize as `[]`.
ownChoices = & [ ] int { }
2023-11-08 14:32:17 +00:00
}
// Check if requester is author of source status.
isAuthor = ( requester . ID == poll . Status . AccountID )
2023-11-09 12:06:37 +00:00
2023-12-12 13:47:07 +00:00
// Set whether requester has voted in poll (or = author).
hasVoted = util . Ptr ( ( isAuthor || len ( * ownChoices ) > 0 ) )
2023-11-08 14:32:17 +00:00
}
if isAuthor || ! * poll . HideCounts {
2023-12-12 13:47:07 +00:00
// Only in the case that hide counts is
// disabled, or the requester is the author
// do we actually populate the vote counts.
2023-12-16 19:12:25 +00:00
// If we voted in this poll, we'll have set totalVotes
// earlier. Reset here to avoid double counting.
totalVotes = 0
2023-12-12 13:47:07 +00:00
if * poll . Multiple {
// The total number of voters are only
// provided in the case of a multiple
// choice poll. All else leaves it nil.
totalVoters = poll . Voters
}
// Populate per-vote counts
// and overall total vote count.
2023-11-08 22:37:35 +00:00
for i , count := range poll . Votes {
2023-12-12 13:47:07 +00:00
if options [ i ] . VotesCount == nil {
options [ i ] . VotesCount = new ( int )
}
( * options [ i ] . VotesCount ) += count
2023-11-08 22:37:35 +00:00
totalVotes += count
2023-11-08 14:32:17 +00:00
}
}
2023-11-11 10:15:04 +00:00
if ! poll . ExpiresAt . IsZero ( ) {
// Calculate poll expiry string (if set).
2023-12-12 13:47:07 +00:00
str := util . FormatISO8601 ( poll . ExpiresAt )
expiresAt = & str
2023-11-11 10:15:04 +00:00
}
2023-12-12 13:47:07 +00:00
var err error
// Try to inherit emojis from parent status.
emojis , err = c . convertEmojisToAPIEmojis ( ctx ,
poll . Status . Emojis ,
poll . Status . EmojiIDs ,
)
if err != nil {
log . Errorf ( ctx , "error converting emojis from parent status: %v" , err )
emojis = [ ] apimodel . Emoji { } // fallback to empty slice.
2023-11-22 11:17:42 +00:00
}
2023-11-09 12:06:37 +00:00
2023-11-08 14:32:17 +00:00
return & apimodel . Poll {
ID : poll . ID ,
2023-11-11 10:15:04 +00:00
ExpiresAt : expiresAt ,
2023-11-08 14:32:17 +00:00
Expired : poll . Closed ( ) ,
2023-11-08 22:37:35 +00:00
Multiple : ( * poll . Multiple ) ,
2023-11-08 14:32:17 +00:00
VotesCount : totalVotes ,
VotersCount : totalVoters ,
2023-12-12 13:47:07 +00:00
Voted : hasVoted ,
2023-11-08 14:32:17 +00:00
OwnVotes : ownChoices ,
Options : options ,
2023-11-09 12:06:37 +00:00
Emojis : emojis ,
2023-11-08 14:32:17 +00:00
} , nil
}
2022-11-29 17:59:59 +00: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-12-09 15:54:38 +00:00
func ( c * Converter ) convertAttachmentsToAPIAttachments ( ctx context . Context , attachments [ ] * gtsmodel . MediaAttachment , attachmentIDs [ ] string ) ( [ ] * apimodel . Attachment , error ) {
2022-12-22 10:48:28 +00:00
var errs gtserror . MultiError
2022-11-29 17:59:59 +00:00
2024-01-19 12:57:29 +00:00
if len ( attachments ) == 0 && len ( attachmentIDs ) > 0 {
2022-11-29 17:59:59 +00:00
// GTS model attachments were not populated
2024-01-19 12:57:29 +00:00
var err error
2022-11-29 17:59:59 +00:00
// Fetch GTS models for attachment IDs
2024-01-19 12:57:29 +00:00
attachments , err = c . state . DB . GetAttachmentsByIDs ( ctx , attachmentIDs )
if err != nil {
errs . Appendf ( "error fetching attachments from database: %w" , err )
2022-11-29 17:59:59 +00:00
}
}
// Preallocate expected frontend slice
2023-12-09 15:54:38 +00:00
apiAttachments := make ( [ ] * apimodel . Attachment , 0 , len ( attachments ) )
2022-11-29 17:59:59 +00:00
// Convert GTS models to frontend models
for _ , attachment := range attachments {
apiAttachment , err := c . AttachmentToAPIAttachment ( ctx , attachment )
if err != nil {
2024-01-19 12:57:29 +00:00
errs . Appendf ( "error converting attchment %s to api attachment: %w" , attachment . ID , err )
2022-11-29 17:59:59 +00:00
continue
}
2023-12-09 15:54:38 +00:00
apiAttachments = append ( apiAttachments , & apiAttachment )
2022-11-29 17:59:59 +00:00
}
return apiAttachments , errs . Combine ( )
}
2024-03-06 10:15:58 +00:00
// FilterToAPIFiltersV1 converts one GTS model filter into an API v1 filter list
func ( c * Converter ) FilterToAPIFiltersV1 ( ctx context . Context , filter * gtsmodel . Filter ) ( [ ] * apimodel . FilterV1 , error ) {
apiFilters := make ( [ ] * apimodel . FilterV1 , 0 , len ( filter . Keywords ) )
for _ , filterKeyword := range filter . Keywords {
apiFilter , err := c . FilterKeywordToAPIFilterV1 ( ctx , filterKeyword )
if err != nil {
return nil , err
}
apiFilters = append ( apiFilters , apiFilter )
}
return apiFilters , nil
}
// FilterKeywordToAPIFilterV1 converts one GTS model filter and filter keyword into an API v1 filter
func ( c * Converter ) FilterKeywordToAPIFilterV1 ( ctx context . Context , filterKeyword * gtsmodel . FilterKeyword ) ( * apimodel . FilterV1 , error ) {
if filterKeyword . Filter == nil {
return nil , gtserror . New ( "FilterKeyword model's Filter field isn't populated, but needs to be" )
}
filter := filterKeyword . Filter
2024-05-06 11:49:08 +00:00
return & apimodel . FilterV1 {
// v1 filters have a single keyword each, so we use the filter keyword ID as the v1 filter ID.
ID : filterKeyword . ID ,
Phrase : filterKeyword . Keyword ,
Context : filterToAPIFilterContexts ( filter ) ,
2024-07-17 15:26:33 +00:00
WholeWord : util . PtrOrValue ( filterKeyword . WholeWord , false ) ,
2024-05-06 11:49:08 +00:00
ExpiresAt : filterExpiresAtToAPIFilterExpiresAt ( filter . ExpiresAt ) ,
Irreversible : filter . Action == gtsmodel . FilterActionHide ,
} , nil
}
// FilterToAPIFilterV2 converts one GTS model filter into an API v2 filter.
func ( c * Converter ) FilterToAPIFilterV2 ( ctx context . Context , filter * gtsmodel . Filter ) ( * apimodel . FilterV2 , error ) {
apiFilterKeywords := make ( [ ] apimodel . FilterKeyword , 0 , len ( filter . Keywords ) )
for _ , filterKeyword := range filter . Keywords {
2024-05-31 10:55:56 +00:00
apiFilterKeywords = append ( apiFilterKeywords , * c . FilterKeywordToAPIFilterKeyword ( ctx , filterKeyword ) )
2024-05-06 11:49:08 +00:00
}
apiFilterStatuses := make ( [ ] apimodel . FilterStatus , 0 , len ( filter . Keywords ) )
for _ , filterStatus := range filter . Statuses {
2024-05-31 10:55:56 +00:00
apiFilterStatuses = append ( apiFilterStatuses , * c . FilterStatusToAPIFilterStatus ( ctx , filterStatus ) )
2024-05-06 11:49:08 +00:00
}
return & apimodel . FilterV2 {
ID : filter . ID ,
Title : filter . Title ,
Context : filterToAPIFilterContexts ( filter ) ,
ExpiresAt : filterExpiresAtToAPIFilterExpiresAt ( filter . ExpiresAt ) ,
FilterAction : filterActionToAPIFilterAction ( filter . Action ) ,
Keywords : apiFilterKeywords ,
Statuses : apiFilterStatuses ,
} , nil
}
func filterExpiresAtToAPIFilterExpiresAt ( expiresAt time . Time ) * string {
if expiresAt . IsZero ( ) {
return nil
}
return util . Ptr ( util . FormatISO8601 ( expiresAt ) )
}
func filterToAPIFilterContexts ( filter * gtsmodel . Filter ) [ ] apimodel . FilterContext {
2024-03-06 10:15:58 +00:00
apiContexts := make ( [ ] apimodel . FilterContext , 0 , apimodel . FilterContextNumValues )
2024-07-17 15:26:33 +00:00
if util . PtrOrValue ( filter . ContextHome , false ) {
2024-03-06 10:15:58 +00:00
apiContexts = append ( apiContexts , apimodel . FilterContextHome )
}
2024-07-17 15:26:33 +00:00
if util . PtrOrValue ( filter . ContextNotifications , false ) {
2024-03-06 10:15:58 +00:00
apiContexts = append ( apiContexts , apimodel . FilterContextNotifications )
}
2024-07-17 15:26:33 +00:00
if util . PtrOrValue ( filter . ContextPublic , false ) {
2024-03-06 10:15:58 +00:00
apiContexts = append ( apiContexts , apimodel . FilterContextPublic )
}
2024-07-17 15:26:33 +00:00
if util . PtrOrValue ( filter . ContextThread , false ) {
2024-03-06 10:15:58 +00:00
apiContexts = append ( apiContexts , apimodel . FilterContextThread )
}
2024-07-17 15:26:33 +00:00
if util . PtrOrValue ( filter . ContextAccount , false ) {
2024-03-06 10:15:58 +00:00
apiContexts = append ( apiContexts , apimodel . FilterContextAccount )
}
2024-05-06 11:49:08 +00:00
return apiContexts
}
2024-03-06 10:15:58 +00:00
2024-05-06 11:49:08 +00:00
func filterActionToAPIFilterAction ( m gtsmodel . FilterAction ) apimodel . FilterAction {
switch m {
case gtsmodel . FilterActionWarn :
return apimodel . FilterActionWarn
case gtsmodel . FilterActionHide :
return apimodel . FilterActionHide
2024-03-06 10:15:58 +00:00
}
2024-05-06 11:49:08 +00:00
return apimodel . FilterActionNone
2024-03-06 10:15:58 +00:00
}
2024-05-31 10:55:56 +00:00
// FilterKeywordToAPIFilterKeyword converts a GTS model filter status into an API filter status.
func ( c * Converter ) FilterKeywordToAPIFilterKeyword ( ctx context . Context , filterKeyword * gtsmodel . FilterKeyword ) * apimodel . FilterKeyword {
return & apimodel . FilterKeyword {
ID : filterKeyword . ID ,
Keyword : filterKeyword . Keyword ,
2024-07-17 15:26:33 +00:00
WholeWord : util . PtrOrValue ( filterKeyword . WholeWord , false ) ,
2024-05-31 10:55:56 +00:00
}
}
// FilterStatusToAPIFilterStatus converts a GTS model filter status into an API filter status.
func ( c * Converter ) FilterStatusToAPIFilterStatus ( ctx context . Context , filterStatus * gtsmodel . FilterStatus ) * apimodel . FilterStatus {
return & apimodel . FilterStatus {
ID : filterStatus . ID ,
StatusID : filterStatus . StatusID ,
}
}
2022-11-29 17:59:59 +00:00
// convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
2023-09-23 16:44:11 +00:00
func ( c * Converter ) convertEmojisToAPIEmojis ( ctx context . Context , emojis [ ] * gtsmodel . Emoji , emojiIDs [ ] string ) ( [ ] apimodel . Emoji , error ) {
2022-12-22 10:48:28 +00:00
var errs gtserror . MultiError
2022-11-29 17:59:59 +00:00
2024-01-19 12:57:29 +00:00
if len ( emojis ) == 0 && len ( emojiIDs ) > 0 {
2022-11-29 17:59:59 +00:00
// GTS model attachments were not populated
2024-01-19 12:57:29 +00:00
var err error
2022-11-29 17:59:59 +00:00
// Fetch GTS models for emoji IDs
2024-01-19 12:57:29 +00:00
emojis , err = c . state . DB . GetEmojisByIDs ( ctx , emojiIDs )
if err != nil {
errs . Appendf ( "error fetching emojis from database: %w" , err )
2022-11-29 17:59:59 +00:00
}
}
// Preallocate expected frontend slice
2023-01-02 12:10:50 +00:00
apiEmojis := make ( [ ] apimodel . Emoji , 0 , len ( emojis ) )
2022-11-29 17:59:59 +00:00
// Convert GTS models to frontend models
for _ , emoji := range emojis {
apiEmoji , err := c . EmojiToAPIEmoji ( ctx , emoji )
if err != nil {
2024-01-19 12:57:29 +00:00
errs . Appendf ( "error converting emoji %s to api emoji: %w" , emoji . ID , err )
2022-11-29 17:59:59 +00:00
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-09-23 16:44:11 +00:00
func ( c * Converter ) convertMentionsToAPIMentions ( ctx context . Context , mentions [ ] * gtsmodel . Mention , mentionIDs [ ] string ) ( [ ] apimodel . Mention , error ) {
2022-12-22 10:48:28 +00:00
var errs gtserror . MultiError
2022-11-29 17:59:59 +00:00
2024-01-19 12:57:29 +00:00
if len ( mentions ) == 0 && len ( mentionIDs ) > 0 {
2022-11-29 17:59:59 +00:00
var err error
// GTS model mentions were not populated
//
// Fetch GTS models for mention IDs
2023-09-23 16:44:11 +00:00
mentions , err = c . state . DB . GetMentions ( ctx , mentionIDs )
2022-11-29 17:59:59 +00:00
if err != nil {
2024-01-19 12:57:29 +00:00
errs . Appendf ( "error fetching mentions from database: %w" , err )
2022-11-29 17:59:59 +00:00
}
}
// Preallocate expected frontend slice
2023-01-02 12:10:50 +00:00
apiMentions := make ( [ ] apimodel . Mention , 0 , len ( mentions ) )
2022-11-29 17:59:59 +00:00
// Convert GTS models to frontend models
for _ , mention := range mentions {
apiMention , err := c . MentionToAPIMention ( ctx , mention )
if err != nil {
2024-01-19 12:57:29 +00:00
errs . Appendf ( "error converting mention %s to api mention: %w" , mention . ID , err )
2022-11-29 17:59:59 +00:00
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-09-23 16:44:11 +00:00
func ( c * Converter ) convertTagsToAPITags ( ctx context . Context , tags [ ] * gtsmodel . Tag , tagIDs [ ] string ) ( [ ] apimodel . Tag , error ) {
2022-12-22 10:48:28 +00:00
var errs gtserror . MultiError
2022-11-29 17:59:59 +00:00
2024-01-19 12:57:29 +00:00
if len ( tags ) == 0 && len ( tagIDs ) > 0 {
2023-07-31 13:47:35 +00:00
var err error
2022-11-29 17:59:59 +00:00
2023-09-23 16:44:11 +00:00
tags , err = c . state . DB . GetTags ( ctx , tagIDs )
2023-07-31 13:47:35 +00:00
if err != nil {
2024-01-19 12:57:29 +00:00
errs . Appendf ( "error fetching tags from database: %w" , err )
2022-11-29 17:59:59 +00:00
}
}
// Preallocate expected frontend slice
2023-01-02 12:10:50 +00:00
apiTags := make ( [ ] apimodel . Tag , 0 , len ( tags ) )
2022-11-29 17:59:59 +00:00
// Convert GTS models to frontend models
for _ , tag := range tags {
2024-07-29 18:26:31 +00:00
apiTag , err := c . TagToAPITag ( ctx , tag , false , nil )
2022-11-29 17:59:59 +00:00
if err != nil {
2024-01-19 12:57:29 +00:00
errs . Appendf ( "error converting tag %s to api tag: %w" , tag . ID , err )
2022-11-29 17:59:59 +00:00
continue
}
apiTags = append ( apiTags , apiTag )
}
return apiTags , errs . Combine ( )
}
2024-03-25 17:32:24 +00:00
// ThemesToAPIThemes converts a slice of gtsmodel Themes into apimodel Themes.
func ( c * Converter ) ThemesToAPIThemes ( themes [ ] * gtsmodel . Theme ) [ ] apimodel . Theme {
apiThemes := make ( [ ] apimodel . Theme , len ( themes ) )
for i , theme := range themes {
apiThemes [ i ] = apimodel . Theme {
Title : theme . Title ,
Description : theme . Description ,
FileName : theme . FileName ,
}
}
return apiThemes
}
2024-07-17 14:46:52 +00:00
// Convert the given gtsmodel policy
// into an apimodel interaction policy.
//
// Provided status can be nil to convert a
// policy without a particular status in mind.
//
// RequestingAccount can also be nil for
// unauthorized requests (web, public api etc).
func ( c * Converter ) InteractionPolicyToAPIInteractionPolicy (
ctx context . Context ,
policy * gtsmodel . InteractionPolicy ,
2024-07-24 11:27:42 +00:00
status * gtsmodel . Status ,
requester * gtsmodel . Account ,
2024-07-17 14:46:52 +00:00
) ( * apimodel . InteractionPolicy , error ) {
apiPolicy := & apimodel . InteractionPolicy {
CanFavourite : apimodel . PolicyRules {
Always : policyValsToAPIPolicyVals ( policy . CanLike . Always ) ,
WithApproval : policyValsToAPIPolicyVals ( policy . CanLike . WithApproval ) ,
} ,
CanReply : apimodel . PolicyRules {
Always : policyValsToAPIPolicyVals ( policy . CanReply . Always ) ,
WithApproval : policyValsToAPIPolicyVals ( policy . CanReply . WithApproval ) ,
} ,
CanReblog : apimodel . PolicyRules {
Always : policyValsToAPIPolicyVals ( policy . CanAnnounce . Always ) ,
WithApproval : policyValsToAPIPolicyVals ( policy . CanAnnounce . WithApproval ) ,
} ,
}
2024-07-24 11:27:42 +00:00
if status == nil || requester == nil {
// We're done here!
return apiPolicy , nil
}
// Status and requester are both defined,
// so we can add the "me" Value to the policy
// for each interaction type, if applicable.
likeable , err := c . intFilter . StatusLikeable ( ctx , requester , status )
if err != nil {
err := gtserror . Newf ( "error checking status likeable by requester: %w" , err )
return nil , err
}
if likeable . Permission == gtsmodel . PolicyPermissionPermitted {
// We can do this!
apiPolicy . CanFavourite . Always = append (
apiPolicy . CanFavourite . Always ,
apimodel . PolicyValueMe ,
)
} else if likeable . Permission == gtsmodel . PolicyPermissionWithApproval {
// We can do this with approval.
apiPolicy . CanFavourite . WithApproval = append (
apiPolicy . CanFavourite . WithApproval ,
apimodel . PolicyValueMe ,
)
}
replyable , err := c . intFilter . StatusReplyable ( ctx , requester , status )
if err != nil {
err := gtserror . Newf ( "error checking status replyable by requester: %w" , err )
return nil , err
}
if replyable . Permission == gtsmodel . PolicyPermissionPermitted {
// We can do this!
apiPolicy . CanReply . Always = append (
apiPolicy . CanReply . Always ,
apimodel . PolicyValueMe ,
)
} else if replyable . Permission == gtsmodel . PolicyPermissionWithApproval {
// We can do this with approval.
apiPolicy . CanReply . WithApproval = append (
apiPolicy . CanReply . WithApproval ,
apimodel . PolicyValueMe ,
)
}
boostable , err := c . intFilter . StatusBoostable ( ctx , requester , status )
if err != nil {
err := gtserror . Newf ( "error checking status boostable by requester: %w" , err )
return nil , err
}
if boostable . Permission == gtsmodel . PolicyPermissionPermitted {
// We can do this!
apiPolicy . CanReblog . Always = append (
apiPolicy . CanReblog . Always ,
apimodel . PolicyValueMe ,
)
} else if boostable . Permission == gtsmodel . PolicyPermissionWithApproval {
// We can do this with approval.
apiPolicy . CanReblog . WithApproval = append (
apiPolicy . CanReblog . WithApproval ,
apimodel . PolicyValueMe ,
)
}
2024-07-17 14:46:52 +00:00
return apiPolicy , nil
}
func policyValsToAPIPolicyVals ( vals gtsmodel . PolicyValues ) [ ] apimodel . PolicyValue {
var (
valsLen = len ( vals )
// Use a map to deduplicate added vals as we go.
addedVals = make ( map [ apimodel . PolicyValue ] struct { } , valsLen )
// Vals we'll be returning.
apiVals = make ( [ ] apimodel . PolicyValue , 0 , valsLen )
)
for _ , policyVal := range vals {
switch policyVal {
case gtsmodel . PolicyValueAuthor :
// Author can do this.
newVal := apimodel . PolicyValueAuthor
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
case gtsmodel . PolicyValueMentioned :
// Mentioned can do this.
newVal := apimodel . PolicyValueMentioned
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
case gtsmodel . PolicyValueMutuals :
// Mutuals can do this.
newVal := apimodel . PolicyValueMutuals
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
case gtsmodel . PolicyValueFollowing :
// Following can do this.
newVal := apimodel . PolicyValueFollowing
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
case gtsmodel . PolicyValueFollowers :
// Followers can do this.
newVal := apimodel . PolicyValueFollowers
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
case gtsmodel . PolicyValuePublic :
// Public can do this.
newVal := apimodel . PolicyValuePublic
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
default :
// Specific URI of ActivityPub Actor.
newVal := apimodel . PolicyValue ( policyVal )
if _ , added := addedVals [ newVal ] ; ! added {
apiVals = append ( apiVals , newVal )
addedVals [ newVal ] = struct { } { }
}
}
}
return apiVals
}
2024-08-24 09:49:37 +00:00
// InteractionReqToAPIInteractionReq converts the given *gtsmodel.InteractionRequest
// to an *apimodel.InteractionRequest, from the perspective of requestingAcct.
func ( c * Converter ) InteractionReqToAPIInteractionReq (
ctx context . Context ,
req * gtsmodel . InteractionRequest ,
requestingAcct * gtsmodel . Account ,
) ( * apimodel . InteractionRequest , error ) {
// Ensure interaction request is populated.
if err := c . state . DB . PopulateInteractionRequest ( ctx , req ) ; err != nil {
err := gtserror . Newf ( "error populating: %w" , err )
return nil , err
}
interactingAcct , err := c . AccountToAPIAccountPublic ( ctx , req . InteractingAccount )
if err != nil {
err := gtserror . Newf ( "error converting interacting acct: %w" , err )
return nil , err
}
interactedStatus , err := c . StatusToAPIStatus (
ctx ,
req . Status ,
requestingAcct ,
statusfilter . FilterContextNone ,
2024-09-23 12:42:19 +00:00
nil , // No filters.
nil , // No mutes.
2024-08-24 09:49:37 +00:00
)
if err != nil {
err := gtserror . Newf ( "error converting interacted status: %w" , err )
return nil , err
}
var reply * apimodel . Status
2024-10-05 09:36:01 +00:00
if req . InteractionType == gtsmodel . InteractionReply && req . Reply != nil {
2024-09-23 12:42:19 +00:00
reply , err = c . statusToAPIStatus (
2024-08-24 09:49:37 +00:00
ctx ,
2024-09-24 17:28:46 +00:00
req . Reply ,
2024-08-24 09:49:37 +00:00
requestingAcct ,
statusfilter . FilterContextNone ,
2024-09-23 12:42:19 +00:00
nil , // No filters.
nil , // No mutes.
true , // Placehold unknown attachments.
// Don't add note about pending;
// requester already knows it's
// pending because they're looking
// at the request right now.
false ,
2024-08-24 09:49:37 +00:00
)
if err != nil {
err := gtserror . Newf ( "error converting reply: %w" , err )
return nil , err
}
}
var acceptedAt string
if req . IsAccepted ( ) {
acceptedAt = util . FormatISO8601 ( req . AcceptedAt )
}
var rejectedAt string
if req . IsRejected ( ) {
rejectedAt = util . FormatISO8601 ( req . RejectedAt )
}
return & apimodel . InteractionRequest {
ID : req . ID ,
Type : req . InteractionType . String ( ) ,
CreatedAt : util . FormatISO8601 ( req . CreatedAt ) ,
Account : interactingAcct ,
Status : interactedStatus ,
Reply : reply ,
AcceptedAt : acceptedAt ,
RejectedAt : rejectedAt ,
URI : req . URI ,
} , nil
}