gtsocial-umbx

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

internaltoas.go (49202B)


      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 	"crypto/x509"
     23 	"encoding/pem"
     24 	"errors"
     25 	"fmt"
     26 	"net/url"
     27 
     28 	"github.com/superseriousbusiness/activity/pub"
     29 	"github.com/superseriousbusiness/activity/streams"
     30 	"github.com/superseriousbusiness/activity/streams/vocab"
     31 	"github.com/superseriousbusiness/gotosocial/internal/config"
     32 	"github.com/superseriousbusiness/gotosocial/internal/db"
     33 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     34 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     35 	"github.com/superseriousbusiness/gotosocial/internal/log"
     36 )
     37 
     38 // Converts a gts model account into an Activity Streams person type.
     39 func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {
     40 	person := streams.NewActivityStreamsPerson()
     41 
     42 	// id should be the activitypub URI of this user
     43 	// something like https://example.org/users/example_user
     44 	profileIDURI, err := url.Parse(a.URI)
     45 	if err != nil {
     46 		return nil, err
     47 	}
     48 	idProp := streams.NewJSONLDIdProperty()
     49 	idProp.SetIRI(profileIDURI)
     50 	person.SetJSONLDId(idProp)
     51 
     52 	// following
     53 	// The URI for retrieving a list of accounts this user is following
     54 	followingURI, err := url.Parse(a.FollowingURI)
     55 	if err != nil {
     56 		return nil, err
     57 	}
     58 	followingProp := streams.NewActivityStreamsFollowingProperty()
     59 	followingProp.SetIRI(followingURI)
     60 	person.SetActivityStreamsFollowing(followingProp)
     61 
     62 	// followers
     63 	// The URI for retrieving a list of this user's followers
     64 	followersURI, err := url.Parse(a.FollowersURI)
     65 	if err != nil {
     66 		return nil, err
     67 	}
     68 	followersProp := streams.NewActivityStreamsFollowersProperty()
     69 	followersProp.SetIRI(followersURI)
     70 	person.SetActivityStreamsFollowers(followersProp)
     71 
     72 	// inbox
     73 	// the activitypub inbox of this user for accepting messages
     74 	inboxURI, err := url.Parse(a.InboxURI)
     75 	if err != nil {
     76 		return nil, err
     77 	}
     78 	inboxProp := streams.NewActivityStreamsInboxProperty()
     79 	inboxProp.SetIRI(inboxURI)
     80 	person.SetActivityStreamsInbox(inboxProp)
     81 
     82 	// shared inbox -- only add this if we know for sure it has one
     83 	if a.SharedInboxURI != nil && *a.SharedInboxURI != "" {
     84 		sharedInboxURI, err := url.Parse(*a.SharedInboxURI)
     85 		if err != nil {
     86 			return nil, err
     87 		}
     88 		endpointsProp := streams.NewActivityStreamsEndpointsProperty()
     89 		endpoints := streams.NewActivityStreamsEndpoints()
     90 		sharedInboxProp := streams.NewActivityStreamsSharedInboxProperty()
     91 		sharedInboxProp.SetIRI(sharedInboxURI)
     92 		endpoints.SetActivityStreamsSharedInbox(sharedInboxProp)
     93 		endpointsProp.AppendActivityStreamsEndpoints(endpoints)
     94 		person.SetActivityStreamsEndpoints(endpointsProp)
     95 	}
     96 
     97 	// outbox
     98 	// the activitypub outbox of this user for serving messages
     99 	outboxURI, err := url.Parse(a.OutboxURI)
    100 	if err != nil {
    101 		return nil, err
    102 	}
    103 	outboxProp := streams.NewActivityStreamsOutboxProperty()
    104 	outboxProp.SetIRI(outboxURI)
    105 	person.SetActivityStreamsOutbox(outboxProp)
    106 
    107 	// featured posts
    108 	// Pinned posts.
    109 	featuredURI, err := url.Parse(a.FeaturedCollectionURI)
    110 	if err != nil {
    111 		return nil, err
    112 	}
    113 	featuredProp := streams.NewTootFeaturedProperty()
    114 	featuredProp.SetIRI(featuredURI)
    115 	person.SetTootFeatured(featuredProp)
    116 
    117 	// featuredTags
    118 	// NOT IMPLEMENTED
    119 
    120 	// preferredUsername
    121 	// Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
    122 	preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty()
    123 	preferredUsernameProp.SetXMLSchemaString(a.Username)
    124 	person.SetActivityStreamsPreferredUsername(preferredUsernameProp)
    125 
    126 	// name
    127 	// Used as profile display name.
    128 	nameProp := streams.NewActivityStreamsNameProperty()
    129 	if a.Username != "" {
    130 		nameProp.AppendXMLSchemaString(a.DisplayName)
    131 	} else {
    132 		nameProp.AppendXMLSchemaString(a.Username)
    133 	}
    134 	person.SetActivityStreamsName(nameProp)
    135 
    136 	// summary
    137 	// Used as profile bio.
    138 	if a.Note != "" {
    139 		summaryProp := streams.NewActivityStreamsSummaryProperty()
    140 		summaryProp.AppendXMLSchemaString(a.Note)
    141 		person.SetActivityStreamsSummary(summaryProp)
    142 	}
    143 
    144 	// url
    145 	// Used as profile link.
    146 	profileURL, err := url.Parse(a.URL)
    147 	if err != nil {
    148 		return nil, err
    149 	}
    150 	urlProp := streams.NewActivityStreamsUrlProperty()
    151 	urlProp.AppendIRI(profileURL)
    152 	person.SetActivityStreamsUrl(urlProp)
    153 
    154 	// manuallyApprovesFollowers
    155 	// Will be shown as a locked account.
    156 	manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
    157 	manuallyApprovesFollowersProp.Set(*a.Locked)
    158 	person.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp)
    159 
    160 	// discoverable
    161 	// Will be shown in the profile directory.
    162 	discoverableProp := streams.NewTootDiscoverableProperty()
    163 	discoverableProp.Set(*a.Discoverable)
    164 	person.SetTootDiscoverable(discoverableProp)
    165 
    166 	// devices
    167 	// NOT IMPLEMENTED, probably won't implement
    168 
    169 	// alsoKnownAs
    170 	// Required for Move activity.
    171 	// TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool
    172 
    173 	// publicKey
    174 	// Required for signatures.
    175 	publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
    176 
    177 	// create the public key
    178 	publicKey := streams.NewW3IDSecurityV1PublicKey()
    179 
    180 	// set ID for the public key
    181 	publicKeyIDProp := streams.NewJSONLDIdProperty()
    182 	publicKeyURI, err := url.Parse(a.PublicKeyURI)
    183 	if err != nil {
    184 		return nil, err
    185 	}
    186 	publicKeyIDProp.SetIRI(publicKeyURI)
    187 	publicKey.SetJSONLDId(publicKeyIDProp)
    188 
    189 	// set owner for the public key
    190 	publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty()
    191 	publicKeyOwnerProp.SetIRI(profileIDURI)
    192 	publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp)
    193 
    194 	// set the pem key itself
    195 	encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey)
    196 	if err != nil {
    197 		return nil, err
    198 	}
    199 	publicKeyBytes := pem.EncodeToMemory(&pem.Block{
    200 		Type:  "PUBLIC KEY",
    201 		Bytes: encodedPublicKey,
    202 	})
    203 	publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
    204 	publicKeyPEMProp.Set(string(publicKeyBytes))
    205 	publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp)
    206 
    207 	// append the public key to the public key property
    208 	publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey)
    209 
    210 	// set the public key property on the Person
    211 	person.SetW3IDSecurityV1PublicKey(publicKeyProp)
    212 
    213 	// tags
    214 	tagProp := streams.NewActivityStreamsTagProperty()
    215 
    216 	// tag -- emojis
    217 	emojis := a.Emojis
    218 	if len(a.EmojiIDs) > len(emojis) {
    219 		emojis = []*gtsmodel.Emoji{}
    220 		for _, emojiID := range a.EmojiIDs {
    221 			emoji, err := c.db.GetEmojiByID(ctx, emojiID)
    222 			if err != nil {
    223 				return nil, fmt.Errorf("AccountToAS: error getting emoji %s from database: %s", emojiID, err)
    224 			}
    225 			emojis = append(emojis, emoji)
    226 		}
    227 	}
    228 	for _, emoji := range emojis {
    229 		asEmoji, err := c.EmojiToAS(ctx, emoji)
    230 		if err != nil {
    231 			return nil, fmt.Errorf("AccountToAS: error converting emoji to AS emoji: %s", err)
    232 		}
    233 		tagProp.AppendTootEmoji(asEmoji)
    234 	}
    235 
    236 	// tag -- hashtags
    237 	// TODO
    238 
    239 	person.SetActivityStreamsTag(tagProp)
    240 
    241 	// attachment
    242 	// Used for profile fields.
    243 	if len(a.Fields) != 0 {
    244 		attachmentProp := streams.NewActivityStreamsAttachmentProperty()
    245 
    246 		for _, field := range a.Fields {
    247 			propertyValue := streams.NewSchemaPropertyValue()
    248 
    249 			nameProp := streams.NewActivityStreamsNameProperty()
    250 			nameProp.AppendXMLSchemaString(field.Name)
    251 			propertyValue.SetActivityStreamsName(nameProp)
    252 
    253 			valueProp := streams.NewSchemaValueProperty()
    254 			valueProp.Set(field.Value)
    255 			propertyValue.SetSchemaValue(valueProp)
    256 
    257 			attachmentProp.AppendSchemaPropertyValue(propertyValue)
    258 		}
    259 
    260 		person.SetActivityStreamsAttachment(attachmentProp)
    261 	}
    262 
    263 	// endpoints
    264 	// NOT IMPLEMENTED -- this is for shared inbox which we don't use
    265 
    266 	// icon
    267 	// Used as profile avatar.
    268 	if a.AvatarMediaAttachmentID != "" {
    269 		if a.AvatarMediaAttachment == nil {
    270 			avatar, err := c.db.GetAttachmentByID(ctx, a.AvatarMediaAttachmentID)
    271 			if err == nil {
    272 				a.AvatarMediaAttachment = avatar
    273 			} else {
    274 				log.Errorf(ctx, "error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err)
    275 			}
    276 		}
    277 
    278 		if a.AvatarMediaAttachment != nil {
    279 			iconProperty := streams.NewActivityStreamsIconProperty()
    280 
    281 			iconImage := streams.NewActivityStreamsImage()
    282 
    283 			mediaType := streams.NewActivityStreamsMediaTypeProperty()
    284 			mediaType.Set(a.AvatarMediaAttachment.File.ContentType)
    285 			iconImage.SetActivityStreamsMediaType(mediaType)
    286 
    287 			avatarURLProperty := streams.NewActivityStreamsUrlProperty()
    288 			avatarURL, err := url.Parse(a.AvatarMediaAttachment.URL)
    289 			if err != nil {
    290 				return nil, err
    291 			}
    292 			avatarURLProperty.AppendIRI(avatarURL)
    293 			iconImage.SetActivityStreamsUrl(avatarURLProperty)
    294 
    295 			iconProperty.AppendActivityStreamsImage(iconImage)
    296 			person.SetActivityStreamsIcon(iconProperty)
    297 		}
    298 	}
    299 
    300 	// image
    301 	// Used as profile header.
    302 	if a.HeaderMediaAttachmentID != "" {
    303 		if a.HeaderMediaAttachment == nil {
    304 			header, err := c.db.GetAttachmentByID(ctx, a.HeaderMediaAttachmentID)
    305 			if err == nil {
    306 				a.HeaderMediaAttachment = header
    307 			} else {
    308 				log.Errorf(ctx, "error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err)
    309 			}
    310 		}
    311 
    312 		if a.HeaderMediaAttachment != nil {
    313 			headerProperty := streams.NewActivityStreamsImageProperty()
    314 
    315 			headerImage := streams.NewActivityStreamsImage()
    316 
    317 			mediaType := streams.NewActivityStreamsMediaTypeProperty()
    318 			mediaType.Set(a.HeaderMediaAttachment.File.ContentType)
    319 			headerImage.SetActivityStreamsMediaType(mediaType)
    320 
    321 			headerURLProperty := streams.NewActivityStreamsUrlProperty()
    322 			headerURL, err := url.Parse(a.HeaderMediaAttachment.URL)
    323 			if err != nil {
    324 				return nil, err
    325 			}
    326 			headerURLProperty.AppendIRI(headerURL)
    327 			headerImage.SetActivityStreamsUrl(headerURLProperty)
    328 
    329 			headerProperty.AppendActivityStreamsImage(headerImage)
    330 			person.SetActivityStreamsImage(headerProperty)
    331 		}
    332 	}
    333 
    334 	return person, nil
    335 }
    336 
    337 // Converts a gts model account into a VERY MINIMAL Activity Streams person type.
    338 //
    339 // The returned account will just have the Type, Username, PublicKey, and ID properties set.
    340 func (c *converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {
    341 	person := streams.NewActivityStreamsPerson()
    342 
    343 	// id should be the activitypub URI of this user
    344 	// something like https://example.org/users/example_user
    345 	profileIDURI, err := url.Parse(a.URI)
    346 	if err != nil {
    347 		return nil, err
    348 	}
    349 	idProp := streams.NewJSONLDIdProperty()
    350 	idProp.SetIRI(profileIDURI)
    351 	person.SetJSONLDId(idProp)
    352 
    353 	// preferredUsername
    354 	// Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
    355 	preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty()
    356 	preferredUsernameProp.SetXMLSchemaString(a.Username)
    357 	person.SetActivityStreamsPreferredUsername(preferredUsernameProp)
    358 
    359 	// publicKey
    360 	// Required for signatures.
    361 	publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
    362 
    363 	// create the public key
    364 	publicKey := streams.NewW3IDSecurityV1PublicKey()
    365 
    366 	// set ID for the public key
    367 	publicKeyIDProp := streams.NewJSONLDIdProperty()
    368 	publicKeyURI, err := url.Parse(a.PublicKeyURI)
    369 	if err != nil {
    370 		return nil, err
    371 	}
    372 	publicKeyIDProp.SetIRI(publicKeyURI)
    373 	publicKey.SetJSONLDId(publicKeyIDProp)
    374 
    375 	// set owner for the public key
    376 	publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty()
    377 	publicKeyOwnerProp.SetIRI(profileIDURI)
    378 	publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp)
    379 
    380 	// set the pem key itself
    381 	encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey)
    382 	if err != nil {
    383 		return nil, err
    384 	}
    385 	publicKeyBytes := pem.EncodeToMemory(&pem.Block{
    386 		Type:  "PUBLIC KEY",
    387 		Bytes: encodedPublicKey,
    388 	})
    389 	publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
    390 	publicKeyPEMProp.Set(string(publicKeyBytes))
    391 	publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp)
    392 
    393 	// append the public key to the public key property
    394 	publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey)
    395 
    396 	// set the public key property on the Person
    397 	person.SetW3IDSecurityV1PublicKey(publicKeyProp)
    398 
    399 	return person, nil
    400 }
    401 
    402 func (c *converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
    403 	// ensure prerequisites here before we get stuck in
    404 
    405 	// check if author account is already attached to status and attach it if not
    406 	// if we can't retrieve this, bail here already because we can't attribute the status to anyone
    407 	if s.Account == nil {
    408 		a, err := c.db.GetAccountByID(ctx, s.AccountID)
    409 		if err != nil {
    410 			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err)
    411 		}
    412 		s.Account = a
    413 	}
    414 
    415 	// create the Note!
    416 	status := streams.NewActivityStreamsNote()
    417 
    418 	// id
    419 	statusURI, err := url.Parse(s.URI)
    420 	if err != nil {
    421 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URI, err)
    422 	}
    423 	statusIDProp := streams.NewJSONLDIdProperty()
    424 	statusIDProp.SetIRI(statusURI)
    425 	status.SetJSONLDId(statusIDProp)
    426 
    427 	// type
    428 	// will be set automatically by go-fed
    429 
    430 	// summary aka cw
    431 	statusSummaryProp := streams.NewActivityStreamsSummaryProperty()
    432 	statusSummaryProp.AppendXMLSchemaString(s.ContentWarning)
    433 	status.SetActivityStreamsSummary(statusSummaryProp)
    434 
    435 	// inReplyTo
    436 	if s.InReplyToURI != "" {
    437 		rURI, err := url.Parse(s.InReplyToURI)
    438 		if err != nil {
    439 			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.InReplyToURI, err)
    440 		}
    441 
    442 		inReplyToProp := streams.NewActivityStreamsInReplyToProperty()
    443 		inReplyToProp.AppendIRI(rURI)
    444 		status.SetActivityStreamsInReplyTo(inReplyToProp)
    445 	}
    446 
    447 	// published
    448 	publishedProp := streams.NewActivityStreamsPublishedProperty()
    449 	publishedProp.Set(s.CreatedAt)
    450 	status.SetActivityStreamsPublished(publishedProp)
    451 
    452 	// url
    453 	if s.URL != "" {
    454 		sURL, err := url.Parse(s.URL)
    455 		if err != nil {
    456 			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URL, err)
    457 		}
    458 
    459 		urlProp := streams.NewActivityStreamsUrlProperty()
    460 		urlProp.AppendIRI(sURL)
    461 		status.SetActivityStreamsUrl(urlProp)
    462 	}
    463 
    464 	// attributedTo
    465 	authorAccountURI, err := url.Parse(s.Account.URI)
    466 	if err != nil {
    467 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.URI, err)
    468 	}
    469 	attributedToProp := streams.NewActivityStreamsAttributedToProperty()
    470 	attributedToProp.AppendIRI(authorAccountURI)
    471 	status.SetActivityStreamsAttributedTo(attributedToProp)
    472 
    473 	// tags
    474 	tagProp := streams.NewActivityStreamsTagProperty()
    475 
    476 	// tag -- mentions
    477 	mentions := s.Mentions
    478 	if len(s.MentionIDs) > len(mentions) {
    479 		mentions, err = c.db.GetMentions(ctx, s.MentionIDs)
    480 		if err != nil {
    481 			return nil, fmt.Errorf("StatusToAS: error getting mentions: %w", err)
    482 		}
    483 	}
    484 	for _, m := range mentions {
    485 		asMention, err := c.MentionToAS(ctx, m)
    486 		if err != nil {
    487 			return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err)
    488 		}
    489 		tagProp.AppendActivityStreamsMention(asMention)
    490 	}
    491 
    492 	// tag -- emojis
    493 	emojis := s.Emojis
    494 	if len(s.EmojiIDs) > len(emojis) {
    495 		emojis = []*gtsmodel.Emoji{}
    496 		for _, emojiID := range s.EmojiIDs {
    497 			emoji, err := c.db.GetEmojiByID(ctx, emojiID)
    498 			if err != nil {
    499 				return nil, fmt.Errorf("StatusToAS: error getting emoji %s from database: %s", emojiID, err)
    500 			}
    501 			emojis = append(emojis, emoji)
    502 		}
    503 	}
    504 	for _, emoji := range emojis {
    505 		asEmoji, err := c.EmojiToAS(ctx, emoji)
    506 		if err != nil {
    507 			return nil, fmt.Errorf("StatusToAS: error converting emoji to AS emoji: %s", err)
    508 		}
    509 		tagProp.AppendTootEmoji(asEmoji)
    510 	}
    511 
    512 	// tag -- hashtags
    513 	// TODO
    514 
    515 	status.SetActivityStreamsTag(tagProp)
    516 
    517 	// parse out some URIs we need here
    518 	authorFollowersURI, err := url.Parse(s.Account.FollowersURI)
    519 	if err != nil {
    520 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.FollowersURI, err)
    521 	}
    522 
    523 	publicURI, err := url.Parse(pub.PublicActivityPubIRI)
    524 	if err != nil {
    525 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", pub.PublicActivityPubIRI, err)
    526 	}
    527 
    528 	// to and cc
    529 	toProp := streams.NewActivityStreamsToProperty()
    530 	ccProp := streams.NewActivityStreamsCcProperty()
    531 	switch s.Visibility {
    532 	case gtsmodel.VisibilityDirect:
    533 		// if DIRECT, then only mentioned users should be added to TO, and nothing to CC
    534 		for _, m := range mentions {
    535 			iri, err := url.Parse(m.TargetAccount.URI)
    536 			if err != nil {
    537 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
    538 			}
    539 			toProp.AppendIRI(iri)
    540 		}
    541 	case gtsmodel.VisibilityMutualsOnly:
    542 		// TODO
    543 	case gtsmodel.VisibilityFollowersOnly:
    544 		// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC
    545 		toProp.AppendIRI(authorFollowersURI)
    546 		for _, m := range mentions {
    547 			iri, err := url.Parse(m.TargetAccount.URI)
    548 			if err != nil {
    549 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
    550 			}
    551 			ccProp.AppendIRI(iri)
    552 		}
    553 	case gtsmodel.VisibilityUnlocked:
    554 		// if UNLOCKED, we want to add followers to TO, and public and mentions to CC
    555 		toProp.AppendIRI(authorFollowersURI)
    556 		ccProp.AppendIRI(publicURI)
    557 		for _, m := range mentions {
    558 			iri, err := url.Parse(m.TargetAccount.URI)
    559 			if err != nil {
    560 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
    561 			}
    562 			ccProp.AppendIRI(iri)
    563 		}
    564 	case gtsmodel.VisibilityPublic:
    565 		// if PUBLIC, we want to add public to TO, and followers and mentions to CC
    566 		toProp.AppendIRI(publicURI)
    567 		ccProp.AppendIRI(authorFollowersURI)
    568 		for _, m := range mentions {
    569 			iri, err := url.Parse(m.TargetAccount.URI)
    570 			if err != nil {
    571 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
    572 			}
    573 			ccProp.AppendIRI(iri)
    574 		}
    575 	}
    576 	status.SetActivityStreamsTo(toProp)
    577 	status.SetActivityStreamsCc(ccProp)
    578 
    579 	// conversation
    580 	// TODO
    581 
    582 	// content -- the actual post itself
    583 	contentProp := streams.NewActivityStreamsContentProperty()
    584 	contentProp.AppendXMLSchemaString(s.Content)
    585 	status.SetActivityStreamsContent(contentProp)
    586 
    587 	// attachments
    588 	attachmentProp := streams.NewActivityStreamsAttachmentProperty()
    589 	attachments := s.Attachments
    590 	if len(s.AttachmentIDs) > len(attachments) {
    591 		attachments = []*gtsmodel.MediaAttachment{}
    592 		for _, attachmentID := range s.AttachmentIDs {
    593 			attachment, err := c.db.GetAttachmentByID(ctx, attachmentID)
    594 			if err != nil {
    595 				return nil, fmt.Errorf("StatusToAS: error getting attachment %s from database: %s", attachmentID, err)
    596 			}
    597 			attachments = append(attachments, attachment)
    598 		}
    599 	}
    600 	for _, a := range attachments {
    601 		doc, err := c.AttachmentToAS(ctx, a)
    602 		if err != nil {
    603 			return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err)
    604 		}
    605 		attachmentProp.AppendActivityStreamsDocument(doc)
    606 	}
    607 	status.SetActivityStreamsAttachment(attachmentProp)
    608 
    609 	// replies
    610 	repliesCollection, err := c.StatusToASRepliesCollection(ctx, s, false)
    611 	if err != nil {
    612 		return nil, fmt.Errorf("error creating repliesCollection: %s", err)
    613 	}
    614 
    615 	repliesProp := streams.NewActivityStreamsRepliesProperty()
    616 	repliesProp.SetActivityStreamsCollection(repliesCollection)
    617 	status.SetActivityStreamsReplies(repliesProp)
    618 
    619 	// sensitive
    620 	sensitiveProp := streams.NewActivityStreamsSensitiveProperty()
    621 	sensitiveProp.AppendXMLSchemaBoolean(*s.Sensitive)
    622 	status.SetActivityStreamsSensitive(sensitiveProp)
    623 
    624 	return status, nil
    625 }
    626 
    627 func (c *converter) StatusToASDelete(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsDelete, error) {
    628 	// Parse / fetch some information
    629 	// we need to create the Delete.
    630 
    631 	if s.Account == nil {
    632 		var err error
    633 		s.Account, err = c.db.GetAccountByID(ctx, s.AccountID)
    634 		if err != nil {
    635 			return nil, fmt.Errorf("StatusToASDelete: error retrieving author account from db: %w", err)
    636 		}
    637 	}
    638 
    639 	actorIRI, err := url.Parse(s.AccountURI)
    640 	if err != nil {
    641 		return nil, fmt.Errorf("StatusToASDelete: error parsing actorIRI %s: %w", s.AccountURI, err)
    642 	}
    643 
    644 	statusIRI, err := url.Parse(s.URI)
    645 	if err != nil {
    646 		return nil, fmt.Errorf("StatusToASDelete: error parsing statusIRI %s: %w", s.URI, err)
    647 	}
    648 
    649 	// Create a Delete.
    650 	delete := streams.NewActivityStreamsDelete()
    651 
    652 	// Set appropriate actor for the Delete.
    653 	deleteActor := streams.NewActivityStreamsActorProperty()
    654 	deleteActor.AppendIRI(actorIRI)
    655 	delete.SetActivityStreamsActor(deleteActor)
    656 
    657 	// Set the status IRI as the 'object' property.
    658 	// We should avoid serializing the whole status
    659 	// when doing a delete because it's wasteful and
    660 	// could accidentally leak the now-deleted status.
    661 	deleteObject := streams.NewActivityStreamsObjectProperty()
    662 	deleteObject.AppendIRI(statusIRI)
    663 	delete.SetActivityStreamsObject(deleteObject)
    664 
    665 	// Address the Delete appropriately.
    666 	toProp := streams.NewActivityStreamsToProperty()
    667 	ccProp := streams.NewActivityStreamsCcProperty()
    668 
    669 	// Unless the status was a direct message, we can
    670 	// address the Delete To the ActivityPub Public URI.
    671 	// This ensures that the Delete will have as wide an
    672 	// audience as possible.
    673 	//
    674 	// Because we're using just the status URI, not the
    675 	// whole status, it won't leak any sensitive info.
    676 	// At worst, a remote instance becomes aware of the
    677 	// URI for a status which is now deleted anyway.
    678 	if s.Visibility != gtsmodel.VisibilityDirect {
    679 		publicURI, err := url.Parse(pub.PublicActivityPubIRI)
    680 		if err != nil {
    681 			return nil, fmt.Errorf("StatusToASDelete: error parsing url %s: %w", pub.PublicActivityPubIRI, err)
    682 		}
    683 		toProp.AppendIRI(publicURI)
    684 
    685 		actorFollowersURI, err := url.Parse(s.Account.FollowersURI)
    686 		if err != nil {
    687 			return nil, fmt.Errorf("StatusToASDelete: error parsing url %s: %w", s.Account.FollowersURI, err)
    688 		}
    689 		ccProp.AppendIRI(actorFollowersURI)
    690 	}
    691 
    692 	// Always include the replied-to account and any
    693 	// mentioned accounts as addressees as well.
    694 	//
    695 	// Worst case scenario here is that a replied account
    696 	// who wasn't mentioned (and perhaps didn't see the
    697 	// message), sees that someone has now deleted a status
    698 	// in which they were replied to but not mentioned. In
    699 	// other words, they *might* see that someone subtooted
    700 	// about them, but they won't know what was said.
    701 
    702 	// Ensure mentions are populated.
    703 	mentions := s.Mentions
    704 	if len(s.MentionIDs) > len(mentions) {
    705 		mentions, err = c.db.GetMentions(ctx, s.MentionIDs)
    706 		if err != nil {
    707 			return nil, fmt.Errorf("StatusToASDelete: error getting mentions: %w", err)
    708 		}
    709 	}
    710 
    711 	// Remember which accounts were mentioned
    712 	// here to avoid duplicating them later.
    713 	mentionedAccountIDs := make(map[string]interface{}, len(mentions))
    714 
    715 	// For direct messages, add URI
    716 	// to To, else just add to CC.
    717 	var f func(v *url.URL)
    718 	if s.Visibility == gtsmodel.VisibilityDirect {
    719 		f = toProp.AppendIRI
    720 	} else {
    721 		f = ccProp.AppendIRI
    722 	}
    723 
    724 	for _, m := range mentions {
    725 		mentionedAccountIDs[m.TargetAccountID] = nil // Remember this ID.
    726 
    727 		iri, err := url.Parse(m.TargetAccount.URI)
    728 		if err != nil {
    729 			return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
    730 		}
    731 
    732 		f(iri)
    733 	}
    734 
    735 	if s.InReplyToAccountID != "" {
    736 		if _, ok := mentionedAccountIDs[s.InReplyToAccountID]; !ok {
    737 			// Only address to this account if it
    738 			// wasn't already included as a mention.
    739 			if s.InReplyToAccount == nil {
    740 				s.InReplyToAccount, err = c.db.GetAccountByID(ctx, s.InReplyToAccountID)
    741 				if err != nil && !errors.Is(err, db.ErrNoEntries) {
    742 					return nil, fmt.Errorf("StatusToASDelete: db error getting account %s: %w", s.InReplyToAccountID, err)
    743 				}
    744 			}
    745 
    746 			if s.InReplyToAccount != nil {
    747 				inReplyToAccountURI, err := url.Parse(s.InReplyToAccount.URI)
    748 				if err != nil {
    749 					return nil, fmt.Errorf("StatusToASDelete: error parsing url %s: %w", s.InReplyToAccount.URI, err)
    750 				}
    751 				ccProp.AppendIRI(inReplyToAccountURI)
    752 			}
    753 		}
    754 	}
    755 
    756 	delete.SetActivityStreamsTo(toProp)
    757 	delete.SetActivityStreamsCc(ccProp)
    758 
    759 	return delete, nil
    760 }
    761 
    762 func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
    763 	// parse out the various URIs we need for this
    764 	// origin account (who's doing the follow)
    765 	originAccountURI, err := url.Parse(originAccount.URI)
    766 	if err != nil {
    767 		return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
    768 	}
    769 	originActor := streams.NewActivityStreamsActorProperty()
    770 	originActor.AppendIRI(originAccountURI)
    771 
    772 	// target account (who's being followed)
    773 	targetAccountURI, err := url.Parse(targetAccount.URI)
    774 	if err != nil {
    775 		return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
    776 	}
    777 
    778 	// uri of the follow activity itself
    779 	followURI, err := url.Parse(f.URI)
    780 	if err != nil {
    781 		return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err)
    782 	}
    783 
    784 	// start preparing the follow activity
    785 	follow := streams.NewActivityStreamsFollow()
    786 
    787 	// set the actor
    788 	follow.SetActivityStreamsActor(originActor)
    789 
    790 	// set the id
    791 	followIDProp := streams.NewJSONLDIdProperty()
    792 	followIDProp.SetIRI(followURI)
    793 	follow.SetJSONLDId(followIDProp)
    794 
    795 	// set the object
    796 	followObjectProp := streams.NewActivityStreamsObjectProperty()
    797 	followObjectProp.AppendIRI(targetAccountURI)
    798 	follow.SetActivityStreamsObject(followObjectProp)
    799 
    800 	// set the To property
    801 	followToProp := streams.NewActivityStreamsToProperty()
    802 	followToProp.AppendIRI(targetAccountURI)
    803 	follow.SetActivityStreamsTo(followToProp)
    804 
    805 	return follow, nil
    806 }
    807 
    808 func (c *converter) MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) {
    809 	if m.TargetAccount == nil {
    810 		a, err := c.db.GetAccountByID(ctx, m.TargetAccountID)
    811 		if err != nil {
    812 			return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err)
    813 		}
    814 		m.TargetAccount = a
    815 	}
    816 
    817 	// create the mention
    818 	mention := streams.NewActivityStreamsMention()
    819 
    820 	// href -- this should be the URI of the mentioned user
    821 	hrefProp := streams.NewActivityStreamsHrefProperty()
    822 	hrefURI, err := url.Parse(m.TargetAccount.URI)
    823 	if err != nil {
    824 		return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
    825 	}
    826 	hrefProp.SetIRI(hrefURI)
    827 	mention.SetActivityStreamsHref(hrefProp)
    828 
    829 	// name -- this should be the namestring of the mentioned user, something like @whatever@example.org
    830 	var domain string
    831 	if m.TargetAccount.Domain == "" {
    832 		accountDomain := config.GetAccountDomain()
    833 		if accountDomain == "" {
    834 			accountDomain = config.GetHost()
    835 		}
    836 		domain = accountDomain
    837 	} else {
    838 		domain = m.TargetAccount.Domain
    839 	}
    840 	username := m.TargetAccount.Username
    841 	nameString := fmt.Sprintf("@%s@%s", username, domain)
    842 	nameProp := streams.NewActivityStreamsNameProperty()
    843 	nameProp.AppendXMLSchemaString(nameString)
    844 	mention.SetActivityStreamsName(nameProp)
    845 
    846 	return mention, nil
    847 }
    848 
    849 /*
    850 	 we're making something like this:
    851 		{
    852 			"id": "https://example.com/emoji/123",
    853 			"type": "Emoji",
    854 			"name": ":kappa:",
    855 			"icon": {
    856 				"type": "Image",
    857 				"mediaType": "image/png",
    858 				"url": "https://example.com/files/kappa.png"
    859 			}
    860 		}
    861 */
    862 func (c *converter) EmojiToAS(ctx context.Context, e *gtsmodel.Emoji) (vocab.TootEmoji, error) {
    863 	// create the emoji
    864 	emoji := streams.NewTootEmoji()
    865 
    866 	// set the ID property to the blocks's URI
    867 	idProp := streams.NewJSONLDIdProperty()
    868 	idIRI, err := url.Parse(e.URI)
    869 	if err != nil {
    870 		return nil, fmt.Errorf("EmojiToAS: error parsing uri %s: %s", e.URI, err)
    871 	}
    872 	idProp.Set(idIRI)
    873 	emoji.SetJSONLDId(idProp)
    874 
    875 	nameProp := streams.NewActivityStreamsNameProperty()
    876 	nameString := fmt.Sprintf(":%s:", e.Shortcode)
    877 	nameProp.AppendXMLSchemaString(nameString)
    878 	emoji.SetActivityStreamsName(nameProp)
    879 
    880 	iconProperty := streams.NewActivityStreamsIconProperty()
    881 	iconImage := streams.NewActivityStreamsImage()
    882 
    883 	mediaType := streams.NewActivityStreamsMediaTypeProperty()
    884 	mediaType.Set(e.ImageContentType)
    885 	iconImage.SetActivityStreamsMediaType(mediaType)
    886 
    887 	emojiURLProperty := streams.NewActivityStreamsUrlProperty()
    888 	emojiURL, err := url.Parse(e.ImageURL)
    889 	if err != nil {
    890 		return nil, fmt.Errorf("EmojiToAS: error parsing url %s: %s", e.ImageURL, err)
    891 	}
    892 	emojiURLProperty.AppendIRI(emojiURL)
    893 	iconImage.SetActivityStreamsUrl(emojiURLProperty)
    894 
    895 	iconProperty.AppendActivityStreamsImage(iconImage)
    896 	emoji.SetActivityStreamsIcon(iconProperty)
    897 
    898 	updatedProp := streams.NewActivityStreamsUpdatedProperty()
    899 	updatedProp.Set(e.ImageUpdatedAt)
    900 	emoji.SetActivityStreamsUpdated(updatedProp)
    901 
    902 	return emoji, nil
    903 }
    904 
    905 func (c *converter) AttachmentToAS(ctx context.Context, a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) {
    906 	// type -- Document
    907 	doc := streams.NewActivityStreamsDocument()
    908 
    909 	// mediaType aka mime content type
    910 	mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty()
    911 	mediaTypeProp.Set(a.File.ContentType)
    912 	doc.SetActivityStreamsMediaType(mediaTypeProp)
    913 
    914 	// url -- for the original image not the thumbnail
    915 	urlProp := streams.NewActivityStreamsUrlProperty()
    916 	imageURL, err := url.Parse(a.URL)
    917 	if err != nil {
    918 		return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err)
    919 	}
    920 	urlProp.AppendIRI(imageURL)
    921 	doc.SetActivityStreamsUrl(urlProp)
    922 
    923 	// name -- aka image description
    924 	nameProp := streams.NewActivityStreamsNameProperty()
    925 	nameProp.AppendXMLSchemaString(a.Description)
    926 	doc.SetActivityStreamsName(nameProp)
    927 
    928 	// blurhash
    929 	blurProp := streams.NewTootBlurhashProperty()
    930 	blurProp.Set(a.Blurhash)
    931 	doc.SetTootBlurhash(blurProp)
    932 
    933 	// focalpoint
    934 	// TODO
    935 
    936 	return doc, nil
    937 }
    938 
    939 /*
    940 We want to end up with something like this:
    941 
    942 {
    943 "@context": "https://www.w3.org/ns/activitystreams",
    944 "actor": "https://ondergrond.org/users/dumpsterqueer",
    945 "id": "https://ondergrond.org/users/dumpsterqueer#likes/44584",
    946 "object": "https://testingtesting123.xyz/users/gotosocial_test_account/statuses/771aea80-a33d-4d6d-8dfd-57d4d2bfcbd4",
    947 "type": "Like"
    948 }
    949 */
    950 func (c *converter) FaveToAS(ctx context.Context, f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) {
    951 	// check if targetStatus is already pinned to this fave, and fetch it if not
    952 	if f.Status == nil {
    953 		s, err := c.db.GetStatusByID(ctx, f.StatusID)
    954 		if err != nil {
    955 			return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err)
    956 		}
    957 		f.Status = s
    958 	}
    959 
    960 	// check if the targetAccount is already pinned to this fave, and fetch it if not
    961 	if f.TargetAccount == nil {
    962 		a, err := c.db.GetAccountByID(ctx, f.TargetAccountID)
    963 		if err != nil {
    964 			return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err)
    965 		}
    966 		f.TargetAccount = a
    967 	}
    968 
    969 	// check if the faving account is already pinned to this fave, and fetch it if not
    970 	if f.Account == nil {
    971 		a, err := c.db.GetAccountByID(ctx, f.AccountID)
    972 		if err != nil {
    973 			return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err)
    974 		}
    975 		f.Account = a
    976 	}
    977 
    978 	// create the like
    979 	like := streams.NewActivityStreamsLike()
    980 
    981 	// set the actor property to the fave-ing account's URI
    982 	actorProp := streams.NewActivityStreamsActorProperty()
    983 	actorIRI, err := url.Parse(f.Account.URI)
    984 	if err != nil {
    985 		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Account.URI, err)
    986 	}
    987 	actorProp.AppendIRI(actorIRI)
    988 	like.SetActivityStreamsActor(actorProp)
    989 
    990 	// set the ID property to the fave's URI
    991 	idProp := streams.NewJSONLDIdProperty()
    992 	idIRI, err := url.Parse(f.URI)
    993 	if err != nil {
    994 		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.URI, err)
    995 	}
    996 	idProp.Set(idIRI)
    997 	like.SetJSONLDId(idProp)
    998 
    999 	// set the object property to the target status's URI
   1000 	objectProp := streams.NewActivityStreamsObjectProperty()
   1001 	statusIRI, err := url.Parse(f.Status.URI)
   1002 	if err != nil {
   1003 		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Status.URI, err)
   1004 	}
   1005 	objectProp.AppendIRI(statusIRI)
   1006 	like.SetActivityStreamsObject(objectProp)
   1007 
   1008 	// set the TO property to the target account's IRI
   1009 	toProp := streams.NewActivityStreamsToProperty()
   1010 	toIRI, err := url.Parse(f.TargetAccount.URI)
   1011 	if err != nil {
   1012 		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.TargetAccount.URI, err)
   1013 	}
   1014 	toProp.AppendIRI(toIRI)
   1015 	like.SetActivityStreamsTo(toProp)
   1016 
   1017 	return like, nil
   1018 }
   1019 
   1020 func (c *converter) BoostToAS(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) {
   1021 	// the boosted status is probably pinned to the boostWrapperStatus but double check to make sure
   1022 	if boostWrapperStatus.BoostOf == nil {
   1023 		b, err := c.db.GetStatusByID(ctx, boostWrapperStatus.BoostOfID)
   1024 		if err != nil {
   1025 			return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err)
   1026 		}
   1027 		boostWrapperStatus.BoostOf = b
   1028 	}
   1029 
   1030 	// create the announce
   1031 	announce := streams.NewActivityStreamsAnnounce()
   1032 
   1033 	// set the actor
   1034 	boosterURI, err := url.Parse(boostingAccount.URI)
   1035 	if err != nil {
   1036 		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostingAccount.URI, err)
   1037 	}
   1038 	actorProp := streams.NewActivityStreamsActorProperty()
   1039 	actorProp.AppendIRI(boosterURI)
   1040 	announce.SetActivityStreamsActor(actorProp)
   1041 
   1042 	// set the ID
   1043 	boostIDURI, err := url.Parse(boostWrapperStatus.URI)
   1044 	if err != nil {
   1045 		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.URI, err)
   1046 	}
   1047 	idProp := streams.NewJSONLDIdProperty()
   1048 	idProp.SetIRI(boostIDURI)
   1049 	announce.SetJSONLDId(idProp)
   1050 
   1051 	// set the object
   1052 	boostedStatusURI, err := url.Parse(boostWrapperStatus.BoostOf.URI)
   1053 	if err != nil {
   1054 		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.BoostOf.URI, err)
   1055 	}
   1056 	objectProp := streams.NewActivityStreamsObjectProperty()
   1057 	objectProp.AppendIRI(boostedStatusURI)
   1058 	announce.SetActivityStreamsObject(objectProp)
   1059 
   1060 	// set the published time
   1061 	publishedProp := streams.NewActivityStreamsPublishedProperty()
   1062 	publishedProp.Set(boostWrapperStatus.CreatedAt)
   1063 	announce.SetActivityStreamsPublished(publishedProp)
   1064 
   1065 	// set the to
   1066 	followersURI, err := url.Parse(boostingAccount.FollowersURI)
   1067 	if err != nil {
   1068 		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostingAccount.FollowersURI, err)
   1069 	}
   1070 	toProp := streams.NewActivityStreamsToProperty()
   1071 	toProp.AppendIRI(followersURI)
   1072 	announce.SetActivityStreamsTo(toProp)
   1073 
   1074 	// set the cc
   1075 	ccProp := streams.NewActivityStreamsCcProperty()
   1076 	boostedAccountURI, err := url.Parse(boostedAccount.URI)
   1077 	if err != nil {
   1078 		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostedAccount.URI, err)
   1079 	}
   1080 	ccProp.AppendIRI(boostedAccountURI)
   1081 
   1082 	// maybe CC it to public depending on the boosted status visibility
   1083 	switch boostWrapperStatus.BoostOf.Visibility {
   1084 	case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked:
   1085 		publicURI, err := url.Parse(pub.PublicActivityPubIRI)
   1086 		if err != nil {
   1087 			return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", pub.PublicActivityPubIRI, err)
   1088 		}
   1089 		ccProp.AppendIRI(publicURI)
   1090 	}
   1091 
   1092 	announce.SetActivityStreamsCc(ccProp)
   1093 
   1094 	return announce, nil
   1095 }
   1096 
   1097 /*
   1098 we want to end up with something like this:
   1099 
   1100 	{
   1101 		"@context": "https://www.w3.org/ns/activitystreams",
   1102 		"actor": "https://example.org/users/some_user",
   1103 		"id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK",
   1104 		"object":"https://some_other.instance/users/some_other_user",
   1105 		"type":"Block"
   1106 	}
   1107 */
   1108 func (c *converter) BlockToAS(ctx context.Context, b *gtsmodel.Block) (vocab.ActivityStreamsBlock, error) {
   1109 	if b.Account == nil {
   1110 		a, err := c.db.GetAccountByID(ctx, b.AccountID)
   1111 		if err != nil {
   1112 			return nil, fmt.Errorf("BlockToAS: error getting block owner account from database: %s", err)
   1113 		}
   1114 		b.Account = a
   1115 	}
   1116 
   1117 	if b.TargetAccount == nil {
   1118 		a, err := c.db.GetAccountByID(ctx, b.TargetAccountID)
   1119 		if err != nil {
   1120 			return nil, fmt.Errorf("BlockToAS: error getting block target account from database: %s", err)
   1121 		}
   1122 		b.TargetAccount = a
   1123 	}
   1124 
   1125 	// create the block
   1126 	block := streams.NewActivityStreamsBlock()
   1127 
   1128 	// set the actor property to the block-ing account's URI
   1129 	actorProp := streams.NewActivityStreamsActorProperty()
   1130 	actorIRI, err := url.Parse(b.Account.URI)
   1131 	if err != nil {
   1132 		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.Account.URI, err)
   1133 	}
   1134 	actorProp.AppendIRI(actorIRI)
   1135 	block.SetActivityStreamsActor(actorProp)
   1136 
   1137 	// set the ID property to the blocks's URI
   1138 	idProp := streams.NewJSONLDIdProperty()
   1139 	idIRI, err := url.Parse(b.URI)
   1140 	if err != nil {
   1141 		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.URI, err)
   1142 	}
   1143 	idProp.Set(idIRI)
   1144 	block.SetJSONLDId(idProp)
   1145 
   1146 	// set the object property to the target account's URI
   1147 	objectProp := streams.NewActivityStreamsObjectProperty()
   1148 	targetIRI, err := url.Parse(b.TargetAccount.URI)
   1149 	if err != nil {
   1150 		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err)
   1151 	}
   1152 	objectProp.AppendIRI(targetIRI)
   1153 	block.SetActivityStreamsObject(objectProp)
   1154 
   1155 	// set the TO property to the target account's IRI
   1156 	toProp := streams.NewActivityStreamsToProperty()
   1157 	toIRI, err := url.Parse(b.TargetAccount.URI)
   1158 	if err != nil {
   1159 		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err)
   1160 	}
   1161 	toProp.AppendIRI(toIRI)
   1162 	block.SetActivityStreamsTo(toProp)
   1163 
   1164 	return block, nil
   1165 }
   1166 
   1167 /*
   1168 the goal is to end up with something like this:
   1169 
   1170 	{
   1171 		"@context": "https://www.w3.org/ns/activitystreams",
   1172 		"id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
   1173 		"type": "Collection",
   1174 		"first": {
   1175 		"id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?page=true",
   1176 		"type": "CollectionPage",
   1177 		"next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true",
   1178 		"partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
   1179 		"items": []
   1180 		}
   1181 	}
   1182 */
   1183 func (c *converter) StatusToASRepliesCollection(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool) (vocab.ActivityStreamsCollection, error) {
   1184 	collectionID := fmt.Sprintf("%s/replies", status.URI)
   1185 	collectionIDURI, err := url.Parse(collectionID)
   1186 	if err != nil {
   1187 		return nil, err
   1188 	}
   1189 
   1190 	collection := streams.NewActivityStreamsCollection()
   1191 
   1192 	// collection.id
   1193 	collectionIDProp := streams.NewJSONLDIdProperty()
   1194 	collectionIDProp.SetIRI(collectionIDURI)
   1195 	collection.SetJSONLDId(collectionIDProp)
   1196 
   1197 	// first
   1198 	first := streams.NewActivityStreamsFirstProperty()
   1199 	firstPage := streams.NewActivityStreamsCollectionPage()
   1200 
   1201 	// first.id
   1202 	firstPageIDProp := streams.NewJSONLDIdProperty()
   1203 	firstPageID, err := url.Parse(fmt.Sprintf("%s?page=true", collectionID))
   1204 	if err != nil {
   1205 		return nil, gtserror.NewErrorInternalError(err)
   1206 	}
   1207 	firstPageIDProp.SetIRI(firstPageID)
   1208 	firstPage.SetJSONLDId(firstPageIDProp)
   1209 
   1210 	// first.next
   1211 	nextProp := streams.NewActivityStreamsNextProperty()
   1212 	nextPropID, err := url.Parse(fmt.Sprintf("%s?only_other_accounts=%t&page=true", collectionID, onlyOtherAccounts))
   1213 	if err != nil {
   1214 		return nil, gtserror.NewErrorInternalError(err)
   1215 	}
   1216 	nextProp.SetIRI(nextPropID)
   1217 	firstPage.SetActivityStreamsNext(nextProp)
   1218 
   1219 	// first.partOf
   1220 	partOfProp := streams.NewActivityStreamsPartOfProperty()
   1221 	partOfProp.SetIRI(collectionIDURI)
   1222 	firstPage.SetActivityStreamsPartOf(partOfProp)
   1223 
   1224 	first.SetActivityStreamsCollectionPage(firstPage)
   1225 
   1226 	// collection.first
   1227 	collection.SetActivityStreamsFirst(first)
   1228 
   1229 	return collection, nil
   1230 }
   1231 
   1232 /*
   1233 the goal is to end up with something like this:
   1234 
   1235 	{
   1236 		"@context": "https://www.w3.org/ns/activitystreams",
   1237 		"id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true",
   1238 		"type": "CollectionPage",
   1239 		"next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?min_id=106720870266901180&only_other_accounts=true&page=true",
   1240 		"partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
   1241 		"items": [
   1242 			"https://example.com/users/someone/statuses/106720752853216226",
   1243 			"https://somewhere.online/users/eeeeeeeeeep/statuses/106720870163727231"
   1244 		]
   1245 	}
   1246 */
   1247 func (c *converter) StatusURIsToASRepliesPage(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool, minID string, replies map[string]*url.URL) (vocab.ActivityStreamsCollectionPage, error) {
   1248 	collectionID := fmt.Sprintf("%s/replies", status.URI)
   1249 
   1250 	page := streams.NewActivityStreamsCollectionPage()
   1251 
   1252 	// .id
   1253 	pageIDProp := streams.NewJSONLDIdProperty()
   1254 	pageIDString := fmt.Sprintf("%s?page=true&only_other_accounts=%t", collectionID, onlyOtherAccounts)
   1255 	if minID != "" {
   1256 		pageIDString = fmt.Sprintf("%s&min_id=%s", pageIDString, minID)
   1257 	}
   1258 
   1259 	pageID, err := url.Parse(pageIDString)
   1260 	if err != nil {
   1261 		return nil, gtserror.NewErrorInternalError(err)
   1262 	}
   1263 	pageIDProp.SetIRI(pageID)
   1264 	page.SetJSONLDId(pageIDProp)
   1265 
   1266 	// .partOf
   1267 	collectionIDURI, err := url.Parse(collectionID)
   1268 	if err != nil {
   1269 		return nil, err
   1270 	}
   1271 	partOfProp := streams.NewActivityStreamsPartOfProperty()
   1272 	partOfProp.SetIRI(collectionIDURI)
   1273 	page.SetActivityStreamsPartOf(partOfProp)
   1274 
   1275 	// .items
   1276 	items := streams.NewActivityStreamsItemsProperty()
   1277 	var highestID string
   1278 	for k, v := range replies {
   1279 		items.AppendIRI(v)
   1280 		if k > highestID {
   1281 			highestID = k
   1282 		}
   1283 	}
   1284 	page.SetActivityStreamsItems(items)
   1285 
   1286 	// .next
   1287 	nextProp := streams.NewActivityStreamsNextProperty()
   1288 	nextPropIDString := fmt.Sprintf("%s?only_other_accounts=%t&page=true", collectionID, onlyOtherAccounts)
   1289 	if highestID != "" {
   1290 		nextPropIDString = fmt.Sprintf("%s&min_id=%s", nextPropIDString, highestID)
   1291 	}
   1292 
   1293 	nextPropID, err := url.Parse(nextPropIDString)
   1294 	if err != nil {
   1295 		return nil, gtserror.NewErrorInternalError(err)
   1296 	}
   1297 	nextProp.SetIRI(nextPropID)
   1298 	page.SetActivityStreamsNext(nextProp)
   1299 
   1300 	return page, nil
   1301 }
   1302 
   1303 /*
   1304 the goal is to end up with something like this:
   1305 
   1306 	{
   1307 		"id": "https://example.org/users/whatever/outbox?page=true",
   1308 		"type": "OrderedCollectionPage",
   1309 		"next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
   1310 		"prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
   1311 		"partOf": "https://example.org/users/whatever/outbox",
   1312 		"orderedItems": [
   1313 			"id": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7/activity",
   1314 			"type": "Create",
   1315 			"actor": "https://example.org/users/whatever",
   1316 			"published": "2021-10-18T20:06:18Z",
   1317 			"to": [
   1318 				"https://www.w3.org/ns/activitystreams#Public"
   1319 			],
   1320 			"cc": [
   1321 				"https://example.org/users/whatever/followers"
   1322 			],
   1323 			"object": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7"
   1324 		]
   1325 	}
   1326 */
   1327 func (c *converter) StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error) {
   1328 	page := streams.NewActivityStreamsOrderedCollectionPage()
   1329 
   1330 	// .id
   1331 	pageIDProp := streams.NewJSONLDIdProperty()
   1332 	pageID := fmt.Sprintf("%s?page=true", outboxID)
   1333 	if minID != "" {
   1334 		pageID = fmt.Sprintf("%s&minID=%s", pageID, minID)
   1335 	}
   1336 	if maxID != "" {
   1337 		pageID = fmt.Sprintf("%s&maxID=%s", pageID, maxID)
   1338 	}
   1339 	pageIDURI, err := url.Parse(pageID)
   1340 	if err != nil {
   1341 		return nil, err
   1342 	}
   1343 	pageIDProp.SetIRI(pageIDURI)
   1344 	page.SetJSONLDId(pageIDProp)
   1345 
   1346 	// .partOf
   1347 	collectionIDURI, err := url.Parse(outboxID)
   1348 	if err != nil {
   1349 		return nil, err
   1350 	}
   1351 	partOfProp := streams.NewActivityStreamsPartOfProperty()
   1352 	partOfProp.SetIRI(collectionIDURI)
   1353 	page.SetActivityStreamsPartOf(partOfProp)
   1354 
   1355 	// .orderedItems
   1356 	itemsProp := streams.NewActivityStreamsOrderedItemsProperty()
   1357 	var highest string
   1358 	var lowest string
   1359 	for _, s := range statuses {
   1360 		note, err := c.StatusToAS(ctx, s)
   1361 		if err != nil {
   1362 			return nil, err
   1363 		}
   1364 
   1365 		create, err := c.WrapNoteInCreate(note, true)
   1366 		if err != nil {
   1367 			return nil, err
   1368 		}
   1369 
   1370 		itemsProp.AppendActivityStreamsCreate(create)
   1371 
   1372 		if highest == "" || s.ID > highest {
   1373 			highest = s.ID
   1374 		}
   1375 		if lowest == "" || s.ID < lowest {
   1376 			lowest = s.ID
   1377 		}
   1378 	}
   1379 	page.SetActivityStreamsOrderedItems(itemsProp)
   1380 
   1381 	// .next
   1382 	if lowest != "" {
   1383 		nextProp := streams.NewActivityStreamsNextProperty()
   1384 		nextPropIDString := fmt.Sprintf("%s?page=true&max_id=%s", outboxID, lowest)
   1385 		nextPropIDURI, err := url.Parse(nextPropIDString)
   1386 		if err != nil {
   1387 			return nil, err
   1388 		}
   1389 		nextProp.SetIRI(nextPropIDURI)
   1390 		page.SetActivityStreamsNext(nextProp)
   1391 	}
   1392 
   1393 	// .prev
   1394 	if highest != "" {
   1395 		prevProp := streams.NewActivityStreamsPrevProperty()
   1396 		prevPropIDString := fmt.Sprintf("%s?page=true&min_id=%s", outboxID, highest)
   1397 		prevPropIDURI, err := url.Parse(prevPropIDString)
   1398 		if err != nil {
   1399 			return nil, err
   1400 		}
   1401 		prevProp.SetIRI(prevPropIDURI)
   1402 		page.SetActivityStreamsPrev(prevProp)
   1403 	}
   1404 
   1405 	return page, nil
   1406 }
   1407 
   1408 /*
   1409 we want something that looks like this:
   1410 
   1411 	{
   1412 		"@context": "https://www.w3.org/ns/activitystreams",
   1413 		"id": "https://example.org/users/whatever/outbox",
   1414 		"type": "OrderedCollection",
   1415 		"first": "https://example.org/users/whatever/outbox?page=true"
   1416 	}
   1417 */
   1418 func (c *converter) OutboxToASCollection(ctx context.Context, outboxID string) (vocab.ActivityStreamsOrderedCollection, error) {
   1419 	collection := streams.NewActivityStreamsOrderedCollection()
   1420 
   1421 	collectionIDProp := streams.NewJSONLDIdProperty()
   1422 	outboxIDURI, err := url.Parse(outboxID)
   1423 	if err != nil {
   1424 		return nil, fmt.Errorf("error parsing url %s", outboxID)
   1425 	}
   1426 	collectionIDProp.SetIRI(outboxIDURI)
   1427 	collection.SetJSONLDId(collectionIDProp)
   1428 
   1429 	collectionFirstProp := streams.NewActivityStreamsFirstProperty()
   1430 	collectionFirstPropID := fmt.Sprintf("%s?page=true", outboxID)
   1431 	collectionFirstPropIDURI, err := url.Parse(collectionFirstPropID)
   1432 	if err != nil {
   1433 		return nil, fmt.Errorf("error parsing url %s", collectionFirstPropID)
   1434 	}
   1435 	collectionFirstProp.SetIRI(collectionFirstPropIDURI)
   1436 	collection.SetActivityStreamsFirst(collectionFirstProp)
   1437 
   1438 	return collection, nil
   1439 }
   1440 
   1441 func (c *converter) StatusesToASFeaturedCollection(ctx context.Context, featuredCollectionID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollection, error) {
   1442 	collection := streams.NewActivityStreamsOrderedCollection()
   1443 
   1444 	collectionIDProp := streams.NewJSONLDIdProperty()
   1445 	featuredCollectionIDURI, err := url.Parse(featuredCollectionID)
   1446 	if err != nil {
   1447 		return nil, fmt.Errorf("error parsing url %s", featuredCollectionID)
   1448 	}
   1449 	collectionIDProp.SetIRI(featuredCollectionIDURI)
   1450 	collection.SetJSONLDId(collectionIDProp)
   1451 
   1452 	itemsProp := streams.NewActivityStreamsOrderedItemsProperty()
   1453 	for _, s := range statuses {
   1454 		uri, err := url.Parse(s.URI)
   1455 		if err != nil {
   1456 			return nil, fmt.Errorf("error parsing url %s", s.URI)
   1457 		}
   1458 		itemsProp.AppendIRI(uri)
   1459 	}
   1460 	collection.SetActivityStreamsOrderedItems(itemsProp)
   1461 
   1462 	totalItemsProp := streams.NewActivityStreamsTotalItemsProperty()
   1463 	totalItemsProp.Set(len(statuses))
   1464 	collection.SetActivityStreamsTotalItems(totalItemsProp)
   1465 
   1466 	return collection, nil
   1467 }
   1468 
   1469 func (c *converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (vocab.ActivityStreamsFlag, error) {
   1470 	flag := streams.NewActivityStreamsFlag()
   1471 
   1472 	flagIDProp := streams.NewJSONLDIdProperty()
   1473 	idURI, err := url.Parse(r.URI)
   1474 	if err != nil {
   1475 		return nil, fmt.Errorf("error parsing url %s: %w", r.URI, err)
   1476 	}
   1477 	flagIDProp.SetIRI(idURI)
   1478 	flag.SetJSONLDId(flagIDProp)
   1479 
   1480 	// for privacy, set the actor as the INSTANCE ACTOR,
   1481 	// not as the actor who created the report
   1482 	instanceAccount, err := c.db.GetInstanceAccount(ctx, "")
   1483 	if err != nil {
   1484 		return nil, fmt.Errorf("error getting instance account: %w", err)
   1485 	}
   1486 	instanceAccountIRI, err := url.Parse(instanceAccount.URI)
   1487 	if err != nil {
   1488 		return nil, fmt.Errorf("error parsing url %s: %w", instanceAccount.URI, err)
   1489 	}
   1490 	flagActorProp := streams.NewActivityStreamsActorProperty()
   1491 	flagActorProp.AppendIRI(instanceAccountIRI)
   1492 	flag.SetActivityStreamsActor(flagActorProp)
   1493 
   1494 	// content should be the comment submitted when the report was created
   1495 	contentProp := streams.NewActivityStreamsContentProperty()
   1496 	contentProp.AppendXMLSchemaString(r.Comment)
   1497 	flag.SetActivityStreamsContent(contentProp)
   1498 
   1499 	// set at least the target account uri as the object of the flag
   1500 	objectProp := streams.NewActivityStreamsObjectProperty()
   1501 	targetAccountURI, err := url.Parse(r.TargetAccount.URI)
   1502 	if err != nil {
   1503 		return nil, fmt.Errorf("error parsing url %s: %w", r.TargetAccount.URI, err)
   1504 	}
   1505 	objectProp.AppendIRI(targetAccountURI)
   1506 	// also set status URIs if they were provided with the report
   1507 	for _, s := range r.Statuses {
   1508 		statusURI, err := url.Parse(s.URI)
   1509 		if err != nil {
   1510 			return nil, fmt.Errorf("error parsing url %s: %w", s.URI, err)
   1511 		}
   1512 		objectProp.AppendIRI(statusURI)
   1513 	}
   1514 	flag.SetActivityStreamsObject(objectProp)
   1515 
   1516 	return flag, nil
   1517 }