astointernal.go (25283B)
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 "net/url" 25 26 "github.com/miekg/dns" 27 "github.com/superseriousbusiness/gotosocial/internal/ap" 28 "github.com/superseriousbusiness/gotosocial/internal/config" 29 "github.com/superseriousbusiness/gotosocial/internal/db" 30 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 31 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 32 "github.com/superseriousbusiness/gotosocial/internal/log" 33 "github.com/superseriousbusiness/gotosocial/internal/uris" 34 ) 35 36 func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable ap.Accountable, accountDomain string) (*gtsmodel.Account, error) { 37 // first check if we actually already know this account 38 uriProp := accountable.GetJSONLDId() 39 if uriProp == nil || !uriProp.IsIRI() { 40 return nil, errors.New("no id property found on person, or id was not an iri") 41 } 42 uri := uriProp.GetIRI() 43 44 // we don't know the account, or we're being told to update it, so we need to generate it from the person -- at least we already have the URI! 45 acct := >smodel.Account{} 46 acct.URI = uri.String() 47 48 // Username aka preferredUsername 49 // We need this one so bail if it's not set. 50 username, err := ap.ExtractPreferredUsername(accountable) 51 if err != nil { 52 return nil, fmt.Errorf("couldn't extract username: %s", err) 53 } 54 acct.Username = username 55 56 // Domain 57 if accountDomain != "" { 58 acct.Domain = accountDomain 59 } else { 60 acct.Domain = uri.Host 61 } 62 63 // avatar aka icon 64 // if this one isn't extractable in a format we recognise we'll just skip it 65 if avatarURL, err := ap.ExtractIconURI(accountable); err == nil { 66 acct.AvatarRemoteURL = avatarURL.String() 67 } 68 69 // header aka image 70 // if this one isn't extractable in a format we recognise we'll just skip it 71 if headerURL, err := ap.ExtractImageURI(accountable); err == nil { 72 acct.HeaderRemoteURL = headerURL.String() 73 } 74 75 // display name aka name 76 // we default to the username, but take the more nuanced name property if it exists 77 if displayName := ap.ExtractName(accountable); displayName != "" { 78 acct.DisplayName = displayName 79 } else { 80 acct.DisplayName = username 81 } 82 83 // account emojis (used in bio, display name, fields) 84 if emojis, err := ap.ExtractEmojis(accountable); err != nil { 85 log.Infof(nil, "error extracting account emojis: %s", err) 86 } else { 87 acct.Emojis = emojis 88 } 89 90 // fields aka attachment array 91 acct.Fields = ap.ExtractFields(accountable) 92 93 // note aka summary 94 acct.Note = ap.ExtractSummary(accountable) 95 96 // check for bot and actor type 97 switch accountable.GetTypeName() { 98 case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization: 99 // people, groups, and organizations aren't bots 100 bot := false 101 acct.Bot = &bot 102 // apps and services are 103 case ap.ActorApplication, ap.ActorService: 104 bot := true 105 acct.Bot = &bot 106 default: 107 // we don't know what this is! 108 return nil, fmt.Errorf("type name %s not recognised or not convertible to ap.ActivityStreamsActor", accountable.GetTypeName()) 109 } 110 acct.ActorType = accountable.GetTypeName() 111 112 // assume not memorial (todo) 113 memorial := false 114 acct.Memorial = &memorial 115 116 // assume not sensitive (todo) 117 sensitive := false 118 acct.Sensitive = &sensitive 119 120 // assume not hide collections (todo) 121 hideCollections := false 122 acct.HideCollections = &hideCollections 123 124 // locked aka manuallyApprovesFollowers 125 locked := true 126 acct.Locked = &locked // assume locked by default 127 maf := accountable.GetActivityStreamsManuallyApprovesFollowers() 128 if maf != nil && maf.IsXMLSchemaBoolean() { 129 locked = maf.Get() 130 acct.Locked = &locked 131 } 132 133 // discoverable 134 // default to false -- take custom value if it's set though 135 discoverable := false 136 acct.Discoverable = &discoverable 137 d, err := ap.ExtractDiscoverable(accountable) 138 if err == nil { 139 acct.Discoverable = &d 140 } 141 142 // assume not rss feed 143 enableRSS := false 144 acct.EnableRSS = &enableRSS 145 146 // url property 147 url, err := ap.ExtractURL(accountable) 148 if err == nil { 149 // take the URL if we can find it 150 acct.URL = url.String() 151 } else { 152 // otherwise just take the account URI as the URL 153 acct.URL = uri.String() 154 } 155 156 // InboxURI 157 if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil { 158 acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String() 159 } 160 161 // SharedInboxURI: 162 // only trust shared inbox if it has at least two domains, 163 // from the right, in common with the domain of the account 164 if sharedInboxURI := ap.ExtractSharedInbox(accountable); // nocollapse 165 sharedInboxURI != nil && dns.CompareDomainName(acct.Domain, sharedInboxURI.Host) >= 2 { 166 sharedInbox := sharedInboxURI.String() 167 acct.SharedInboxURI = &sharedInbox 168 } 169 170 // OutboxURI 171 if accountable.GetActivityStreamsOutbox() != nil && accountable.GetActivityStreamsOutbox().GetIRI() != nil { 172 acct.OutboxURI = accountable.GetActivityStreamsOutbox().GetIRI().String() 173 } 174 175 // FollowingURI 176 if accountable.GetActivityStreamsFollowing() != nil && accountable.GetActivityStreamsFollowing().GetIRI() != nil { 177 acct.FollowingURI = accountable.GetActivityStreamsFollowing().GetIRI().String() 178 } 179 180 // FollowersURI 181 if accountable.GetActivityStreamsFollowers() != nil && accountable.GetActivityStreamsFollowers().GetIRI() != nil { 182 acct.FollowersURI = accountable.GetActivityStreamsFollowers().GetIRI().String() 183 } 184 185 // FeaturedURI aka pinned collection: 186 // Only trust featured URI if it has at least two domains, 187 // from the right, in common with the domain of the account 188 if featured := accountable.GetTootFeatured(); featured != nil && featured.IsIRI() { 189 if featuredURI := featured.GetIRI(); // nocollapse 190 featuredURI != nil && dns.CompareDomainName(acct.Domain, featuredURI.Host) >= 2 { 191 acct.FeaturedCollectionURI = featuredURI.String() 192 } 193 } 194 195 // TODO: FeaturedTagsURI 196 197 // TODO: alsoKnownAs 198 199 // publicKey 200 pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPublicKey(accountable) 201 if err != nil { 202 return nil, fmt.Errorf("couldn't get public key for person %s: %s", uri.String(), err) 203 } 204 205 if pkeyOwnerID.String() != acct.URI { 206 return nil, fmt.Errorf("public key %s was owned by %s and not by %s", pkeyURL, pkeyOwnerID, acct.URI) 207 } 208 209 acct.PublicKey = pkey 210 acct.PublicKeyURI = pkeyURL.String() 211 212 return acct, nil 213 } 214 215 func (c *converter) extractAttachments(i ap.WithAttachment) []*gtsmodel.MediaAttachment { 216 attachmentProp := i.GetActivityStreamsAttachment() 217 if attachmentProp == nil { 218 return nil 219 } 220 221 attachments := make([]*gtsmodel.MediaAttachment, 0, attachmentProp.Len()) 222 223 for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() { 224 t := iter.GetType() 225 if t == nil { 226 continue 227 } 228 229 attachmentable, ok := t.(ap.Attachmentable) 230 if !ok { 231 log.Error(nil, "ap attachment was not attachmentable") 232 continue 233 } 234 235 attachment, err := ap.ExtractAttachment(attachmentable) 236 if err != nil { 237 log.Errorf(nil, "error extracting attachment: %s", err) 238 continue 239 } 240 241 attachments = append(attachments, attachment) 242 } 243 244 return attachments 245 } 246 247 func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusable) (*gtsmodel.Status, error) { 248 status := >smodel.Status{} 249 250 // uri at which this status is reachable 251 uriProp := statusable.GetJSONLDId() 252 if uriProp == nil || !uriProp.IsIRI() { 253 return nil, errors.New("no id property found, or id was not an iri") 254 } 255 status.URI = uriProp.GetIRI().String() 256 257 l := log.WithContext(ctx). 258 WithField("statusURI", status.URI) 259 260 // web url for viewing this status 261 if statusURL, err := ap.ExtractURL(statusable); err == nil { 262 status.URL = statusURL.String() 263 } else { 264 // if no URL was set, just take the URI 265 status.URL = status.URI 266 } 267 268 // the html-formatted content of this status 269 status.Content = ap.ExtractContent(statusable) 270 271 // attachments to dereference and fetch later on (we don't do that here) 272 status.Attachments = c.extractAttachments(statusable) 273 274 // hashtags to dereference later on 275 if hashtags, err := ap.ExtractHashtags(statusable); err != nil { 276 l.Infof("ASStatusToStatus: error extracting status hashtags: %s", err) 277 } else { 278 status.Tags = hashtags 279 } 280 281 // emojis to dereference and fetch later on 282 if emojis, err := ap.ExtractEmojis(statusable); err != nil { 283 l.Infof("ASStatusToStatus: error extracting status emojis: %s", err) 284 } else { 285 status.Emojis = emojis 286 } 287 288 // mentions to dereference later on 289 if mentions, err := ap.ExtractMentions(statusable); err != nil { 290 l.Infof("ASStatusToStatus: error extracting status mentions: %s", err) 291 } else { 292 status.Mentions = mentions 293 } 294 295 // cw string for this status 296 // prefer Summary, fall back to Name 297 if summary := ap.ExtractSummary(statusable); summary != "" { 298 status.ContentWarning = summary 299 } else { 300 status.ContentWarning = ap.ExtractName(statusable) 301 } 302 303 // when was this status created? 304 published, err := ap.ExtractPublished(statusable) 305 if err != nil { 306 l.Infof("ASStatusToStatus: error extracting status published: %s", err) 307 } else { 308 status.CreatedAt = published 309 status.UpdatedAt = published 310 } 311 312 // which account posted this status? 313 // if we don't know the account yet we can dereference it later 314 attributedTo, err := ap.ExtractAttributedToURI(statusable) 315 if err != nil { 316 return nil, errors.New("ASStatusToStatus: attributedTo was empty") 317 } 318 status.AccountURI = attributedTo.String() 319 320 statusOwner, err := c.db.GetAccountByURI(ctx, attributedTo.String()) 321 if err != nil { 322 return nil, fmt.Errorf("ASStatusToStatus: couldn't get status owner from db: %s", err) 323 } 324 status.AccountID = statusOwner.ID 325 status.AccountURI = statusOwner.URI 326 status.Account = statusOwner 327 328 // check if there's a post that this is a reply to 329 inReplyToURI := ap.ExtractInReplyToURI(statusable) 330 if inReplyToURI != nil { 331 // something is set so we can at least set this field on the 332 // status and dereference using this later if we need to 333 status.InReplyToURI = inReplyToURI.String() 334 335 // now we can check if we have the replied-to status in our db already 336 if inReplyToStatus, err := c.db.GetStatusByURI(ctx, inReplyToURI.String()); err == nil { 337 // we have the status in our database already 338 // so we can set these fields here and now... 339 status.InReplyToID = inReplyToStatus.ID 340 status.InReplyToAccountID = inReplyToStatus.AccountID 341 status.InReplyTo = inReplyToStatus 342 if status.InReplyToAccount == nil { 343 if inReplyToAccount, err := c.db.GetAccountByID(ctx, inReplyToStatus.AccountID); err == nil { 344 status.InReplyToAccount = inReplyToAccount 345 } 346 } 347 } 348 } 349 350 // visibility entry for this status 351 visibility, err := ap.ExtractVisibility(statusable, status.Account.FollowersURI) 352 if err != nil { 353 return nil, fmt.Errorf("ASStatusToStatus: error extracting visibility: %s", err) 354 } 355 status.Visibility = visibility 356 357 // advanced visibility for this status 358 // TODO: a lot of work to be done here -- a new type needs to be created for this in go-fed/activity using ASTOOL 359 // for now we just set everything to true 360 federated := true 361 boostable := true 362 replyable := true 363 likeable := true 364 365 status.Federated = &federated 366 status.Boostable = &boostable 367 status.Replyable = &replyable 368 status.Likeable = &likeable 369 370 // sensitive 371 sensitive := ap.ExtractSensitive(statusable) 372 status.Sensitive = &sensitive 373 374 // language 375 // we might be able to extract this from the contentMap field 376 377 // ActivityStreamsType 378 status.ActivityStreamsType = statusable.GetTypeName() 379 380 return status, nil 381 } 382 383 func (c *converter) ASFollowToFollowRequest(ctx context.Context, followable ap.Followable) (*gtsmodel.FollowRequest, error) { 384 idProp := followable.GetJSONLDId() 385 if idProp == nil || !idProp.IsIRI() { 386 return nil, errors.New("no id property set on follow, or was not an iri") 387 } 388 uri := idProp.GetIRI().String() 389 390 origin, err := ap.ExtractActorURI(followable) 391 if err != nil { 392 return nil, errors.New("error extracting actor property from follow") 393 } 394 originAccount, err := c.db.GetAccountByURI(ctx, origin.String()) 395 if err != nil { 396 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 397 } 398 399 target, err := ap.ExtractObjectURI(followable) 400 if err != nil { 401 return nil, errors.New("error extracting object property from follow") 402 } 403 targetAccount, err := c.db.GetAccountByURI(ctx, target.String()) 404 if err != nil { 405 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 406 } 407 408 followRequest := >smodel.FollowRequest{ 409 URI: uri, 410 AccountID: originAccount.ID, 411 TargetAccountID: targetAccount.ID, 412 } 413 414 return followRequest, nil 415 } 416 417 func (c *converter) ASFollowToFollow(ctx context.Context, followable ap.Followable) (*gtsmodel.Follow, error) { 418 idProp := followable.GetJSONLDId() 419 if idProp == nil || !idProp.IsIRI() { 420 return nil, errors.New("no id property set on follow, or was not an iri") 421 } 422 uri := idProp.GetIRI().String() 423 424 origin, err := ap.ExtractActorURI(followable) 425 if err != nil { 426 return nil, errors.New("error extracting actor property from follow") 427 } 428 originAccount, err := c.db.GetAccountByURI(ctx, origin.String()) 429 if err != nil { 430 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 431 } 432 433 target, err := ap.ExtractObjectURI(followable) 434 if err != nil { 435 return nil, errors.New("error extracting object property from follow") 436 } 437 targetAccount, err := c.db.GetAccountByURI(ctx, target.String()) 438 if err != nil { 439 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 440 } 441 442 follow := >smodel.Follow{ 443 URI: uri, 444 AccountID: originAccount.ID, 445 TargetAccountID: targetAccount.ID, 446 } 447 448 return follow, nil 449 } 450 451 func (c *converter) ASLikeToFave(ctx context.Context, likeable ap.Likeable) (*gtsmodel.StatusFave, error) { 452 idProp := likeable.GetJSONLDId() 453 if idProp == nil || !idProp.IsIRI() { 454 return nil, errors.New("no id property set on like, or was not an iri") 455 } 456 uri := idProp.GetIRI().String() 457 458 origin, err := ap.ExtractActorURI(likeable) 459 if err != nil { 460 return nil, errors.New("error extracting actor property from like") 461 } 462 originAccount, err := c.db.GetAccountByURI(ctx, origin.String()) 463 if err != nil { 464 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 465 } 466 467 target, err := ap.ExtractObjectURI(likeable) 468 if err != nil { 469 return nil, errors.New("error extracting object property from like") 470 } 471 472 targetStatus, err := c.db.GetStatusByURI(ctx, target.String()) 473 if err != nil { 474 return nil, fmt.Errorf("error extracting status with uri %s from the database: %s", target.String(), err) 475 } 476 477 var targetAccount *gtsmodel.Account 478 if targetStatus.Account != nil { 479 targetAccount = targetStatus.Account 480 } else { 481 a, err := c.db.GetAccountByID(ctx, targetStatus.AccountID) 482 if err != nil { 483 return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) 484 } 485 targetAccount = a 486 } 487 488 return >smodel.StatusFave{ 489 AccountID: originAccount.ID, 490 Account: originAccount, 491 TargetAccountID: targetAccount.ID, 492 TargetAccount: targetAccount, 493 StatusID: targetStatus.ID, 494 Status: targetStatus, 495 URI: uri, 496 }, nil 497 } 498 499 func (c *converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) (*gtsmodel.Block, error) { 500 idProp := blockable.GetJSONLDId() 501 if idProp == nil || !idProp.IsIRI() { 502 return nil, errors.New("ASBlockToBlock: no id property set on block, or was not an iri") 503 } 504 uri := idProp.GetIRI().String() 505 506 origin, err := ap.ExtractActorURI(blockable) 507 if err != nil { 508 return nil, errors.New("ASBlockToBlock: error extracting actor property from block") 509 } 510 originAccount, err := c.db.GetAccountByURI(ctx, origin.String()) 511 if err != nil { 512 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 513 } 514 515 target, err := ap.ExtractObjectURI(blockable) 516 if err != nil { 517 return nil, errors.New("ASBlockToBlock: error extracting object property from block") 518 } 519 520 targetAccount, err := c.db.GetAccountByURI(ctx, target.String()) 521 if err != nil { 522 return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) 523 } 524 525 return >smodel.Block{ 526 AccountID: originAccount.ID, 527 Account: originAccount, 528 TargetAccountID: targetAccount.ID, 529 TargetAccount: targetAccount, 530 URI: uri, 531 }, nil 532 } 533 534 // Implementation note: this function creates and returns a boost WRAPPER 535 // status which references the boosted status in its BoostOf field. No 536 // dereferencing is done on the boosted status by this function. Callers 537 // should look at `status.BoostOf` to see the status being boosted, and do 538 // dereferencing on it as appropriate. 539 // 540 // The returned boolean indicates whether or not the boost has already been 541 // seen before by this instance. If it was, then status.BoostOf should be a 542 // fully filled-out status. If not, then only status.BoostOf.URI will be set. 543 func (c *converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Announceable) (*gtsmodel.Status, bool, error) { 544 // Ensure item has an ID URI set. 545 _, statusURIStr, err := getURI(announceable) 546 if err != nil { 547 err = gtserror.Newf("error extracting URI: %w", err) 548 return nil, false, err 549 } 550 551 var ( 552 status *gtsmodel.Status 553 isNew bool 554 ) 555 556 // Check if we already have this boost in the database. 557 status, err = c.db.GetStatusByURI(ctx, statusURIStr) 558 if err != nil && !errors.Is(err, db.ErrNoEntries) { 559 // Real database error. 560 err = gtserror.Newf("db error trying to get status with uri %s: %w", statusURIStr, err) 561 return nil, isNew, err 562 } 563 564 if status != nil { 565 // We already have this status, 566 // no need to proceed further. 567 return status, isNew, nil 568 } 569 570 // If we reach here, we're dealing 571 // with a boost we haven't seen before. 572 isNew = true 573 574 // Start assembling the new status 575 // (we already know the URI). 576 status = new(gtsmodel.Status) 577 status.URI = statusURIStr 578 579 // Get the URI of the boosted status. 580 boostOfURI, err := ap.ExtractObjectURI(announceable) 581 if err != nil { 582 err = gtserror.Newf("error extracting Object: %w", err) 583 return nil, isNew, err 584 } 585 586 // Set the URI of the boosted status on 587 // the new status, for later dereferencing. 588 boostOf := >smodel.Status{ 589 URI: boostOfURI.String(), 590 } 591 status.BoostOf = boostOf 592 593 // Extract published time for the boost. 594 published, err := ap.ExtractPublished(announceable) 595 if err != nil { 596 err = gtserror.Newf("error extracting published: %w", err) 597 return nil, isNew, err 598 } 599 status.CreatedAt = published 600 status.UpdatedAt = published 601 602 // Extract URI of the boosting account. 603 accountURI, err := ap.ExtractActorURI(announceable) 604 if err != nil { 605 err = gtserror.Newf("error extracting Actor: %w", err) 606 return nil, isNew, err 607 } 608 accountURIStr := accountURI.String() 609 610 // Try to get the boosting account based on the URI. 611 // This should have been dereferenced already before 612 // we hit this point so we can confidently error out 613 // if we don't have it. 614 account, err := c.db.GetAccountByURI(ctx, accountURIStr) 615 if err != nil { 616 err = gtserror.Newf("db error trying to get account with uri %s: %w", accountURIStr, err) 617 return nil, isNew, err 618 } 619 status.AccountID = account.ID 620 status.AccountURI = account.URI 621 status.Account = account 622 623 // Calculate intended visibility of the boost. 624 visibility, err := ap.ExtractVisibility(announceable, account.FollowersURI) 625 if err != nil { 626 err = gtserror.Newf("error extracting visibility: %w", err) 627 return nil, isNew, err 628 } 629 status.Visibility = visibility 630 631 // Below IDs will all be included in the 632 // boosted status, so set them empty here. 633 status.AttachmentIDs = make([]string, 0) 634 status.TagIDs = make([]string, 0) 635 status.MentionIDs = make([]string, 0) 636 status.EmojiIDs = make([]string, 0) 637 638 // Remaining fields on the boost status will be taken 639 // from the boosted status; it's not our job to do all 640 // that dereferencing here. 641 return status, isNew, nil 642 } 643 644 func (c *converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) (*gtsmodel.Report, error) { 645 // Extract flag uri. 646 idProp := flaggable.GetJSONLDId() 647 if idProp == nil || !idProp.IsIRI() { 648 return nil, errors.New("ASFlagToReport: no id property set on flaggable, or was not an iri") 649 } 650 uri := idProp.GetIRI().String() 651 652 // Extract account that created the flag / report. 653 // This will usually be an instance actor. 654 actor, err := ap.ExtractActorURI(flaggable) 655 if err != nil { 656 return nil, fmt.Errorf("ASFlagToReport: error extracting actor: %w", err) 657 } 658 account, err := c.db.GetAccountByURI(ctx, actor.String()) 659 if err != nil { 660 return nil, fmt.Errorf("ASFlagToReport: error in db fetching account with uri %s: %w", actor.String(), err) 661 } 662 663 // Get the content of the report. 664 // For Mastodon, this will just be a string, or nothing. 665 // In Misskey's case, it may also contain the URLs of 666 // one or more reported statuses, so extract these too. 667 content := ap.ExtractContent(flaggable) 668 statusURIs := []*url.URL{} 669 inlineURLs := misskeyReportInlineURLs(content) 670 statusURIs = append(statusURIs, inlineURLs...) 671 672 // Extract account and statuses targeted by the flag / report. 673 // 674 // Incoming flags from mastodon usually have a target account uri as 675 // first entry in objects, followed by URIs of one or more statuses. 676 // Misskey on the other hand will just contain the target account uri. 677 // We shouldn't assume the order of the objects will correspond to this, 678 // but we can check that he objects slice contains just one account, and 679 // maybe some statuses. 680 // 681 // Throw away anything that's not relevant to us. 682 objects, err := ap.ExtractObjectURIs(flaggable) 683 if err != nil { 684 return nil, fmt.Errorf("ASFlagToReport: error extracting objects: %w", err) 685 } 686 if len(objects) == 0 { 687 return nil, errors.New("ASFlagToReport: flaggable objects empty, can't create report") 688 } 689 690 var targetAccountURI *url.URL 691 for _, object := range objects { 692 switch { 693 case object.Host != config.GetHost(): 694 // object doesn't belong to us, just ignore it 695 continue 696 case uris.IsUserPath(object): 697 if targetAccountURI != nil { 698 return nil, errors.New("ASFlagToReport: flaggable objects contained more than one target account uri") 699 } 700 targetAccountURI = object 701 case uris.IsStatusesPath(object): 702 statusURIs = append(statusURIs, object) 703 } 704 } 705 706 // Make sure we actually have a target account now. 707 if targetAccountURI == nil { 708 return nil, errors.New("ASFlagToReport: flaggable objects contained no recognizable target account uri") 709 } 710 targetAccount, err := c.db.GetAccountByURI(ctx, targetAccountURI.String()) 711 if err != nil { 712 if errors.Is(err, db.ErrNoEntries) { 713 return nil, fmt.Errorf("ASFlagToReport: account with uri %s could not be found in the db", targetAccountURI.String()) 714 } 715 return nil, fmt.Errorf("ASFlagToReport: db error getting account with uri %s: %w", targetAccountURI.String(), err) 716 } 717 718 // If we got some status URIs, try to get them from the db now 719 var ( 720 statusIDs = make([]string, 0, len(statusURIs)) 721 statuses = make([]*gtsmodel.Status, 0, len(statusURIs)) 722 ) 723 for _, statusURI := range statusURIs { 724 statusURIString := statusURI.String() 725 726 // try getting this status by URI first, then URL 727 status, err := c.db.GetStatusByURI(ctx, statusURIString) 728 if err != nil { 729 if !errors.Is(err, db.ErrNoEntries) { 730 return nil, fmt.Errorf("ASFlagToReport: db error getting status with uri %s: %w", statusURIString, err) 731 } 732 733 status, err = c.db.GetStatusByURL(ctx, statusURIString) 734 if err != nil { 735 if !errors.Is(err, db.ErrNoEntries) { 736 return nil, fmt.Errorf("ASFlagToReport: db error getting status with url %s: %w", statusURIString, err) 737 } 738 739 log.Warnf(nil, "reported status %s could not be found in the db, skipping it", statusURIString) 740 continue 741 } 742 } 743 744 if status.AccountID != targetAccount.ID { 745 // status doesn't belong to this account, ignore it 746 continue 747 } 748 749 statusIDs = append(statusIDs, status.ID) 750 statuses = append(statuses, status) 751 } 752 753 // id etc should be handled the caller, so just return what we got 754 return >smodel.Report{ 755 URI: uri, 756 AccountID: account.ID, 757 Account: account, 758 TargetAccountID: targetAccount.ID, 759 TargetAccount: targetAccount, 760 Comment: content, 761 StatusIDs: statusIDs, 762 Statuses: statuses, 763 }, nil 764 }