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