2023-03-12 15:00:57 +00:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-05-08 12:25:55 +00:00
package typeutils
import (
2021-08-25 13:34:33 +00:00
"context"
2021-05-08 12:25:55 +00:00
"crypto/x509"
"encoding/pem"
2023-03-03 19:56:34 +00:00
"errors"
2021-05-21 13:48:26 +00:00
"fmt"
2021-05-08 12:25:55 +00:00
"net/url"
2023-07-31 13:47:35 +00:00
"strings"
2021-05-08 12:25:55 +00:00
2021-11-13 16:29:43 +00:00
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
2023-10-04 12:09:42 +00:00
"github.com/superseriousbusiness/gotosocial/internal/ap"
2021-12-07 12:31:39 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2023-03-03 19:56:34 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2021-08-10 11:32:39 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2022-07-19 08:47:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2023-07-31 13:47:35 +00:00
"github.com/superseriousbusiness/gotosocial/internal/uris"
2021-05-08 12:25:55 +00:00
)
2023-09-23 16:44:11 +00:00
// AccountToAS converts a gts model account into an activity streams person, suitable for federation
func ( c * Converter ) AccountToAS ( ctx context . Context , a * gtsmodel . Account ) ( vocab . ActivityStreamsPerson , error ) {
2021-05-08 12:25:55 +00:00
person := streams . NewActivityStreamsPerson ( )
// id should be the activitypub URI of this user
// something like https://example.org/users/example_user
profileIDURI , err := url . Parse ( a . URI )
if err != nil {
return nil , err
}
idProp := streams . NewJSONLDIdProperty ( )
idProp . SetIRI ( profileIDURI )
person . SetJSONLDId ( idProp )
// following
// The URI for retrieving a list of accounts this user is following
followingURI , err := url . Parse ( a . FollowingURI )
if err != nil {
return nil , err
}
followingProp := streams . NewActivityStreamsFollowingProperty ( )
followingProp . SetIRI ( followingURI )
person . SetActivityStreamsFollowing ( followingProp )
// followers
// The URI for retrieving a list of this user's followers
followersURI , err := url . Parse ( a . FollowersURI )
if err != nil {
return nil , err
}
followersProp := streams . NewActivityStreamsFollowersProperty ( )
followersProp . SetIRI ( followersURI )
person . SetActivityStreamsFollowers ( followersProp )
// inbox
// the activitypub inbox of this user for accepting messages
inboxURI , err := url . Parse ( a . InboxURI )
if err != nil {
return nil , err
}
inboxProp := streams . NewActivityStreamsInboxProperty ( )
inboxProp . SetIRI ( inboxURI )
person . SetActivityStreamsInbox ( inboxProp )
2022-09-23 19:27:35 +00:00
// shared inbox -- only add this if we know for sure it has one
if a . SharedInboxURI != nil && * a . SharedInboxURI != "" {
sharedInboxURI , err := url . Parse ( * a . SharedInboxURI )
if err != nil {
return nil , err
}
endpointsProp := streams . NewActivityStreamsEndpointsProperty ( )
endpoints := streams . NewActivityStreamsEndpoints ( )
sharedInboxProp := streams . NewActivityStreamsSharedInboxProperty ( )
sharedInboxProp . SetIRI ( sharedInboxURI )
endpoints . SetActivityStreamsSharedInbox ( sharedInboxProp )
endpointsProp . AppendActivityStreamsEndpoints ( endpoints )
person . SetActivityStreamsEndpoints ( endpointsProp )
}
2021-05-08 12:25:55 +00:00
// outbox
// the activitypub outbox of this user for serving messages
outboxURI , err := url . Parse ( a . OutboxURI )
if err != nil {
return nil , err
}
outboxProp := streams . NewActivityStreamsOutboxProperty ( )
outboxProp . SetIRI ( outboxURI )
person . SetActivityStreamsOutbox ( outboxProp )
// featured posts
// Pinned posts.
featuredURI , err := url . Parse ( a . FeaturedCollectionURI )
if err != nil {
return nil , err
}
featuredProp := streams . NewTootFeaturedProperty ( )
featuredProp . SetIRI ( featuredURI )
person . SetTootFeatured ( featuredProp )
// featuredTags
// NOT IMPLEMENTED
// preferredUsername
// Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
preferredUsernameProp := streams . NewActivityStreamsPreferredUsernameProperty ( )
preferredUsernameProp . SetXMLSchemaString ( a . Username )
person . SetActivityStreamsPreferredUsername ( preferredUsernameProp )
// name
// Used as profile display name.
nameProp := streams . NewActivityStreamsNameProperty ( )
if a . Username != "" {
nameProp . AppendXMLSchemaString ( a . DisplayName )
} else {
nameProp . AppendXMLSchemaString ( a . Username )
}
person . SetActivityStreamsName ( nameProp )
// summary
// Used as profile bio.
if a . Note != "" {
summaryProp := streams . NewActivityStreamsSummaryProperty ( )
summaryProp . AppendXMLSchemaString ( a . Note )
person . SetActivityStreamsSummary ( summaryProp )
}
// url
// Used as profile link.
profileURL , err := url . Parse ( a . URL )
if err != nil {
return nil , err
}
urlProp := streams . NewActivityStreamsUrlProperty ( )
urlProp . AppendIRI ( profileURL )
person . SetActivityStreamsUrl ( urlProp )
// manuallyApprovesFollowers
// Will be shown as a locked account.
2021-08-23 10:46:05 +00:00
manuallyApprovesFollowersProp := streams . NewActivityStreamsManuallyApprovesFollowersProperty ( )
2022-08-15 10:35:05 +00:00
manuallyApprovesFollowersProp . Set ( * a . Locked )
2021-08-23 10:46:05 +00:00
person . SetActivityStreamsManuallyApprovesFollowers ( manuallyApprovesFollowersProp )
2021-05-08 12:25:55 +00:00
// discoverable
// Will be shown in the profile directory.
discoverableProp := streams . NewTootDiscoverableProperty ( )
2022-08-15 10:35:05 +00:00
discoverableProp . Set ( * a . Discoverable )
2021-05-08 12:25:55 +00:00
person . SetTootDiscoverable ( discoverableProp )
// devices
// NOT IMPLEMENTED, probably won't implement
// alsoKnownAs
// Required for Move activity.
// 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
// publicKey
// Required for signatures.
publicKeyProp := streams . NewW3IDSecurityV1PublicKeyProperty ( )
// create the public key
publicKey := streams . NewW3IDSecurityV1PublicKey ( )
// set ID for the public key
publicKeyIDProp := streams . NewJSONLDIdProperty ( )
publicKeyURI , err := url . Parse ( a . PublicKeyURI )
if err != nil {
return nil , err
}
publicKeyIDProp . SetIRI ( publicKeyURI )
publicKey . SetJSONLDId ( publicKeyIDProp )
// set owner for the public key
publicKeyOwnerProp := streams . NewW3IDSecurityV1OwnerProperty ( )
publicKeyOwnerProp . SetIRI ( profileIDURI )
publicKey . SetW3IDSecurityV1Owner ( publicKeyOwnerProp )
// set the pem key itself
encodedPublicKey , err := x509 . MarshalPKIXPublicKey ( a . PublicKey )
if err != nil {
return nil , err
}
publicKeyBytes := pem . EncodeToMemory ( & pem . Block {
Type : "PUBLIC KEY" ,
Bytes : encodedPublicKey ,
} )
publicKeyPEMProp := streams . NewW3IDSecurityV1PublicKeyPemProperty ( )
publicKeyPEMProp . Set ( string ( publicKeyBytes ) )
publicKey . SetW3IDSecurityV1PublicKeyPem ( publicKeyPEMProp )
// append the public key to the public key property
publicKeyProp . AppendW3IDSecurityV1PublicKey ( publicKey )
// set the public key property on the Person
person . SetW3IDSecurityV1PublicKey ( publicKeyProp )
2022-09-26 09:56:01 +00:00
// tags
tagProp := streams . NewActivityStreamsTagProperty ( )
// tag -- emojis
emojis := a . Emojis
if len ( a . EmojiIDs ) > len ( emojis ) {
emojis = [ ] * gtsmodel . Emoji { }
for _ , emojiID := range a . EmojiIDs {
2023-09-23 16:44:11 +00:00
emoji , err := c . state . DB . GetEmojiByID ( ctx , emojiID )
2022-09-26 09:56:01 +00:00
if err != nil {
return nil , fmt . Errorf ( "AccountToAS: error getting emoji %s from database: %s" , emojiID , err )
}
emojis = append ( emojis , emoji )
}
}
for _ , emoji := range emojis {
asEmoji , err := c . EmojiToAS ( ctx , emoji )
if err != nil {
return nil , fmt . Errorf ( "AccountToAS: error converting emoji to AS emoji: %s" , err )
}
tagProp . AppendTootEmoji ( asEmoji )
}
// tag -- hashtags
// TODO
person . SetActivityStreamsTag ( tagProp )
2021-05-08 12:25:55 +00:00
// attachment
// Used for profile fields.
2023-05-09 10:16:10 +00:00
if len ( a . Fields ) != 0 {
attachmentProp := streams . NewActivityStreamsAttachmentProperty ( )
for _ , field := range a . Fields {
propertyValue := streams . NewSchemaPropertyValue ( )
nameProp := streams . NewActivityStreamsNameProperty ( )
nameProp . AppendXMLSchemaString ( field . Name )
propertyValue . SetActivityStreamsName ( nameProp )
valueProp := streams . NewSchemaValueProperty ( )
valueProp . Set ( field . Value )
propertyValue . SetSchemaValue ( valueProp )
attachmentProp . AppendSchemaPropertyValue ( propertyValue )
}
person . SetActivityStreamsAttachment ( attachmentProp )
}
2021-05-08 12:25:55 +00:00
// endpoints
// NOT IMPLEMENTED -- this is for shared inbox which we don't use
// icon
// Used as profile avatar.
if a . AvatarMediaAttachmentID != "" {
2021-08-25 13:34:33 +00:00
if a . AvatarMediaAttachment == nil {
2023-09-23 16:44:11 +00:00
avatar , err := c . state . DB . GetAttachmentByID ( ctx , a . AvatarMediaAttachmentID )
2022-01-25 11:03:25 +00:00
if err == nil {
a . AvatarMediaAttachment = avatar
2022-02-08 11:37:58 +00:00
} else {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error getting Avatar with id %s: %s" , a . AvatarMediaAttachmentID , err )
2021-08-25 13:34:33 +00:00
}
2021-05-08 12:25:55 +00:00
}
2022-01-25 11:03:25 +00:00
if a . AvatarMediaAttachment != nil {
iconProperty := streams . NewActivityStreamsIconProperty ( )
2021-05-15 09:58:11 +00:00
2022-01-25 11:03:25 +00:00
iconImage := streams . NewActivityStreamsImage ( )
2021-05-15 09:58:11 +00:00
2022-01-25 11:03:25 +00:00
mediaType := streams . NewActivityStreamsMediaTypeProperty ( )
mediaType . Set ( a . AvatarMediaAttachment . File . ContentType )
iconImage . SetActivityStreamsMediaType ( mediaType )
2021-05-08 12:25:55 +00:00
2022-01-25 11:03:25 +00:00
avatarURLProperty := streams . NewActivityStreamsUrlProperty ( )
avatarURL , err := url . Parse ( a . AvatarMediaAttachment . URL )
if err != nil {
return nil , err
}
avatarURLProperty . AppendIRI ( avatarURL )
iconImage . SetActivityStreamsUrl ( avatarURLProperty )
2021-05-08 12:25:55 +00:00
2022-01-25 11:03:25 +00:00
iconProperty . AppendActivityStreamsImage ( iconImage )
person . SetActivityStreamsIcon ( iconProperty )
}
2021-05-08 12:25:55 +00:00
}
// image
// Used as profile header.
if a . HeaderMediaAttachmentID != "" {
2021-08-25 13:34:33 +00:00
if a . HeaderMediaAttachment == nil {
2023-09-23 16:44:11 +00:00
header , err := c . state . DB . GetAttachmentByID ( ctx , a . HeaderMediaAttachmentID )
2022-01-25 11:03:25 +00:00
if err == nil {
a . HeaderMediaAttachment = header
2022-02-08 11:37:58 +00:00
} else {
2023-02-17 11:02:29 +00:00
log . Errorf ( ctx , "error getting Header with id %s: %s" , a . HeaderMediaAttachmentID , err )
2021-08-25 13:34:33 +00:00
}
2021-05-08 12:25:55 +00:00
}
2022-01-25 11:03:25 +00:00
if a . HeaderMediaAttachment != nil {
headerProperty := streams . NewActivityStreamsImageProperty ( )
2021-05-15 09:58:11 +00:00
2022-01-25 11:03:25 +00:00
headerImage := streams . NewActivityStreamsImage ( )
2021-05-15 09:58:11 +00:00
2022-01-25 11:03:25 +00:00
mediaType := streams . NewActivityStreamsMediaTypeProperty ( )
mediaType . Set ( a . HeaderMediaAttachment . File . ContentType )
headerImage . SetActivityStreamsMediaType ( mediaType )
2021-05-08 12:25:55 +00:00
2022-01-25 11:03:25 +00:00
headerURLProperty := streams . NewActivityStreamsUrlProperty ( )
headerURL , err := url . Parse ( a . HeaderMediaAttachment . URL )
if err != nil {
return nil , err
}
headerURLProperty . AppendIRI ( headerURL )
headerImage . SetActivityStreamsUrl ( headerURLProperty )
2021-05-08 12:25:55 +00:00
2022-01-25 11:03:25 +00:00
headerProperty . AppendActivityStreamsImage ( headerImage )
person . SetActivityStreamsImage ( headerProperty )
}
2021-05-08 12:25:55 +00:00
}
return person , nil
}
2023-09-23 16:44:11 +00:00
// AccountToASMinimal converts a gts model account into an activity streams person, suitable for federation.
2021-06-26 14:21:40 +00:00
//
2023-09-23 16:44:11 +00:00
// The returned account will just have the Type, Username, PublicKey, and ID properties set. This is
// suitable for serving to requesters to whom we want to give as little information as possible because
// we don't trust them (yet).
func ( c * Converter ) AccountToASMinimal ( ctx context . Context , a * gtsmodel . Account ) ( vocab . ActivityStreamsPerson , error ) {
2021-06-26 14:21:40 +00:00
person := streams . NewActivityStreamsPerson ( )
// id should be the activitypub URI of this user
// something like https://example.org/users/example_user
profileIDURI , err := url . Parse ( a . URI )
if err != nil {
return nil , err
}
idProp := streams . NewJSONLDIdProperty ( )
idProp . SetIRI ( profileIDURI )
person . SetJSONLDId ( idProp )
// preferredUsername
// Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
preferredUsernameProp := streams . NewActivityStreamsPreferredUsernameProperty ( )
preferredUsernameProp . SetXMLSchemaString ( a . Username )
person . SetActivityStreamsPreferredUsername ( preferredUsernameProp )
// publicKey
// Required for signatures.
publicKeyProp := streams . NewW3IDSecurityV1PublicKeyProperty ( )
// create the public key
publicKey := streams . NewW3IDSecurityV1PublicKey ( )
// set ID for the public key
publicKeyIDProp := streams . NewJSONLDIdProperty ( )
publicKeyURI , err := url . Parse ( a . PublicKeyURI )
if err != nil {
return nil , err
}
publicKeyIDProp . SetIRI ( publicKeyURI )
publicKey . SetJSONLDId ( publicKeyIDProp )
// set owner for the public key
publicKeyOwnerProp := streams . NewW3IDSecurityV1OwnerProperty ( )
publicKeyOwnerProp . SetIRI ( profileIDURI )
publicKey . SetW3IDSecurityV1Owner ( publicKeyOwnerProp )
// set the pem key itself
encodedPublicKey , err := x509 . MarshalPKIXPublicKey ( a . PublicKey )
if err != nil {
return nil , err
}
publicKeyBytes := pem . EncodeToMemory ( & pem . Block {
Type : "PUBLIC KEY" ,
Bytes : encodedPublicKey ,
} )
publicKeyPEMProp := streams . NewW3IDSecurityV1PublicKeyPemProperty ( )
publicKeyPEMProp . Set ( string ( publicKeyBytes ) )
publicKey . SetW3IDSecurityV1PublicKeyPem ( publicKeyPEMProp )
// append the public key to the public key property
publicKeyProp . AppendW3IDSecurityV1PublicKey ( publicKey )
// set the public key property on the Person
person . SetW3IDSecurityV1PublicKey ( publicKeyProp )
return person , nil
}
2023-10-04 12:09:42 +00:00
// StatusToAS converts a gts model status into an ActivityStreams Statusable implementation, suitable for federation
func ( c * Converter ) StatusToAS ( ctx context . Context , s * gtsmodel . Status ) ( ap . Statusable , error ) {
// Ensure the status model is fully populated.
// The status and poll models are REQUIRED so nothing to do if this fails.
if err := c . state . DB . PopulateStatus ( ctx , s ) ; err != nil {
return nil , gtserror . Newf ( "error populating status: %w" , err )
2021-05-21 13:48:26 +00:00
}
2023-10-04 12:09:42 +00:00
// We convert it as an AS Note.
2021-05-21 13:48:26 +00:00
status := streams . NewActivityStreamsNote ( )
// id
statusURI , err := url . Parse ( s . URI )
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing url %s: %w" , s . URI , err )
2021-05-21 13:48:26 +00:00
}
statusIDProp := streams . NewJSONLDIdProperty ( )
statusIDProp . SetIRI ( statusURI )
status . SetJSONLDId ( statusIDProp )
// type
// will be set automatically by go-fed
// summary aka cw
statusSummaryProp := streams . NewActivityStreamsSummaryProperty ( )
statusSummaryProp . AppendXMLSchemaString ( s . ContentWarning )
status . SetActivityStreamsSummary ( statusSummaryProp )
// inReplyTo
2023-03-03 19:56:34 +00:00
if s . InReplyToURI != "" {
rURI , err := url . Parse ( s . InReplyToURI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing url %s: %w" , s . InReplyToURI , err )
2021-05-21 13:48:26 +00:00
}
inReplyToProp := streams . NewActivityStreamsInReplyToProperty ( )
inReplyToProp . AppendIRI ( rURI )
status . SetActivityStreamsInReplyTo ( inReplyToProp )
}
// published
publishedProp := streams . NewActivityStreamsPublishedProperty ( )
publishedProp . Set ( s . CreatedAt )
status . SetActivityStreamsPublished ( publishedProp )
// url
if s . URL != "" {
sURL , err := url . Parse ( s . URL )
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing url %s: %w" , s . URL , err )
2021-05-21 13:48:26 +00:00
}
urlProp := streams . NewActivityStreamsUrlProperty ( )
urlProp . AppendIRI ( sURL )
status . SetActivityStreamsUrl ( urlProp )
}
// attributedTo
2021-08-20 10:26:56 +00:00
authorAccountURI , err := url . Parse ( s . Account . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing url %s: %w" , s . Account . URI , err )
2021-05-21 13:48:26 +00:00
}
attributedToProp := streams . NewActivityStreamsAttributedToProperty ( )
attributedToProp . AppendIRI ( authorAccountURI )
status . SetActivityStreamsAttributedTo ( attributedToProp )
// tags
tagProp := streams . NewActivityStreamsTagProperty ( )
// tag -- mentions
2022-09-15 09:29:05 +00:00
mentions := s . Mentions
if len ( s . MentionIDs ) > len ( mentions ) {
2023-09-23 16:44:11 +00:00
mentions , err = c . state . DB . GetMentions ( ctx , s . MentionIDs )
2023-03-03 19:56:34 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error getting mentions: %w" , err )
2022-09-15 09:29:05 +00:00
}
}
for _ , m := range mentions {
2021-08-25 13:34:33 +00:00
asMention , err := c . MentionToAS ( ctx , m )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error converting mention to AS mention: %w" , err )
2021-05-21 13:48:26 +00:00
}
tagProp . AppendActivityStreamsMention ( asMention )
}
// tag -- emojis
2022-09-15 09:29:05 +00:00
emojis := s . Emojis
if len ( s . EmojiIDs ) > len ( emojis ) {
emojis = [ ] * gtsmodel . Emoji { }
for _ , emojiID := range s . EmojiIDs {
2023-09-23 16:44:11 +00:00
emoji , err := c . state . DB . GetEmojiByID ( ctx , emojiID )
2022-09-15 09:29:05 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error getting emoji %s from database: %w" , emojiID , err )
2022-09-15 09:29:05 +00:00
}
emojis = append ( emojis , emoji )
}
}
for _ , emoji := range emojis {
2022-09-26 09:56:01 +00:00
asEmoji , err := c . EmojiToAS ( ctx , emoji )
2022-09-02 10:11:43 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error converting emoji to AS emoji: %w" , err )
2022-09-02 10:11:43 +00:00
}
2022-09-26 09:56:01 +00:00
tagProp . AppendTootEmoji ( asEmoji )
2022-09-02 10:11:43 +00:00
}
2021-05-21 13:48:26 +00:00
// tag -- hashtags
2023-07-31 13:47:35 +00:00
hashtags := s . Tags
if len ( s . TagIDs ) > len ( hashtags ) {
2023-09-23 16:44:11 +00:00
hashtags , err = c . state . DB . GetTags ( ctx , s . TagIDs )
2023-07-31 13:47:35 +00:00
if err != nil {
return nil , gtserror . Newf ( "error getting tags: %w" , err )
}
}
for _ , ht := range hashtags {
asHashtag , err := c . TagToAS ( ctx , ht )
if err != nil {
return nil , gtserror . Newf ( "error converting tag to AS tag: %w" , err )
}
tagProp . AppendTootHashtag ( asHashtag )
}
2021-05-21 13:48:26 +00:00
status . SetActivityStreamsTag ( tagProp )
// parse out some URIs we need here
2021-08-20 10:26:56 +00:00
authorFollowersURI , err := url . Parse ( s . Account . FollowersURI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing url %s: %w" , s . Account . FollowersURI , err )
2021-05-21 13:48:26 +00:00
}
2021-10-06 16:18:02 +00:00
publicURI , err := url . Parse ( pub . PublicActivityPubIRI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing url %s: %w" , pub . PublicActivityPubIRI , err )
2021-05-21 13:48:26 +00:00
}
// to and cc
toProp := streams . NewActivityStreamsToProperty ( )
ccProp := streams . NewActivityStreamsCcProperty ( )
switch s . Visibility {
case gtsmodel . VisibilityDirect :
// if DIRECT, then only mentioned users should be added to TO, and nothing to CC
2023-03-03 19:56:34 +00:00
for _ , m := range mentions {
2021-11-22 11:48:46 +00:00
iri , err := url . Parse ( m . TargetAccount . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing uri %s: %w" , m . TargetAccount . URI , err )
2021-05-21 13:48:26 +00:00
}
toProp . AppendIRI ( iri )
}
case gtsmodel . VisibilityMutualsOnly :
// TODO
case gtsmodel . VisibilityFollowersOnly :
// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC
toProp . AppendIRI ( authorFollowersURI )
2023-03-03 19:56:34 +00:00
for _ , m := range mentions {
2021-11-22 11:48:46 +00:00
iri , err := url . Parse ( m . TargetAccount . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing uri %s: %w" , m . TargetAccount . URI , err )
2021-05-21 13:48:26 +00:00
}
ccProp . AppendIRI ( iri )
}
case gtsmodel . VisibilityUnlocked :
// if UNLOCKED, we want to add followers to TO, and public and mentions to CC
toProp . AppendIRI ( authorFollowersURI )
ccProp . AppendIRI ( publicURI )
2023-03-03 19:56:34 +00:00
for _ , m := range mentions {
2021-11-22 11:48:46 +00:00
iri , err := url . Parse ( m . TargetAccount . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing uri %s: %w" , m . TargetAccount . URI , err )
2021-05-21 13:48:26 +00:00
}
ccProp . AppendIRI ( iri )
}
case gtsmodel . VisibilityPublic :
// if PUBLIC, we want to add public to TO, and followers and mentions to CC
toProp . AppendIRI ( publicURI )
ccProp . AppendIRI ( authorFollowersURI )
2023-03-03 19:56:34 +00:00
for _ , m := range mentions {
2021-11-22 11:48:46 +00:00
iri , err := url . Parse ( m . TargetAccount . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error parsing uri %s: %w" , m . TargetAccount . URI , err )
2021-05-21 13:48:26 +00:00
}
ccProp . AppendIRI ( iri )
}
}
status . SetActivityStreamsTo ( toProp )
status . SetActivityStreamsCc ( ccProp )
// conversation
// TODO
// content -- the actual post itself
contentProp := streams . NewActivityStreamsContentProperty ( )
contentProp . AppendXMLSchemaString ( s . Content )
status . SetActivityStreamsContent ( contentProp )
2022-09-15 09:29:05 +00:00
// attachments
2021-05-21 13:48:26 +00:00
attachmentProp := streams . NewActivityStreamsAttachmentProperty ( )
2022-09-15 09:29:05 +00:00
attachments := s . Attachments
if len ( s . AttachmentIDs ) > len ( attachments ) {
attachments = [ ] * gtsmodel . MediaAttachment { }
for _ , attachmentID := range s . AttachmentIDs {
2023-09-23 16:44:11 +00:00
attachment , err := c . state . DB . GetAttachmentByID ( ctx , attachmentID )
2022-09-15 09:29:05 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error getting attachment %s from database: %w" , attachmentID , err )
2022-09-15 09:29:05 +00:00
}
attachments = append ( attachments , attachment )
}
}
for _ , a := range attachments {
2021-08-25 13:34:33 +00:00
doc , err := c . AttachmentToAS ( ctx , a )
2021-05-21 13:48:26 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , gtserror . Newf ( "error converting attachment: %w" , err )
2021-05-21 13:48:26 +00:00
}
attachmentProp . AppendActivityStreamsDocument ( doc )
}
status . SetActivityStreamsAttachment ( attachmentProp )
// replies
2021-08-25 13:34:33 +00:00
repliesCollection , err := c . StatusToASRepliesCollection ( ctx , s , false )
2021-08-10 11:32:39 +00:00
if err != nil {
2023-07-31 13:47:35 +00:00
return nil , fmt . Errorf ( "error creating repliesCollection: %w" , err )
2021-08-10 11:32:39 +00:00
}
repliesProp := streams . NewActivityStreamsRepliesProperty ( )
repliesProp . SetActivityStreamsCollection ( repliesCollection )
status . SetActivityStreamsReplies ( repliesProp )
2021-05-21 21:04:59 +00:00
2021-11-13 16:29:43 +00:00
// sensitive
sensitiveProp := streams . NewActivityStreamsSensitiveProperty ( )
2022-08-15 10:35:05 +00:00
sensitiveProp . AppendXMLSchemaBoolean ( * s . Sensitive )
2021-11-13 16:29:43 +00:00
status . SetActivityStreamsSensitive ( sensitiveProp )
2021-05-21 13:48:26 +00:00
return status , nil
}
2023-09-23 16:44:11 +00:00
// StatusToASDelete converts a gts model status into a Delete of that status, using just the
// URI of the status as object, and addressing the Delete appropriately.
func ( c * Converter ) StatusToASDelete ( ctx context . Context , s * gtsmodel . Status ) ( vocab . ActivityStreamsDelete , error ) {
2023-03-03 19:56:34 +00:00
// Parse / fetch some information
// we need to create the Delete.
if s . Account == nil {
var err error
2023-09-23 16:44:11 +00:00
s . Account , err = c . state . DB . GetAccountByID ( ctx , s . AccountID )
2023-03-03 19:56:34 +00:00
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error retrieving author account from db: %w" , err )
}
}
actorIRI , err := url . Parse ( s . AccountURI )
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error parsing actorIRI %s: %w" , s . AccountURI , err )
}
statusIRI , err := url . Parse ( s . URI )
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error parsing statusIRI %s: %w" , s . URI , err )
}
// Create a Delete.
delete := streams . NewActivityStreamsDelete ( )
// Set appropriate actor for the Delete.
deleteActor := streams . NewActivityStreamsActorProperty ( )
deleteActor . AppendIRI ( actorIRI )
delete . SetActivityStreamsActor ( deleteActor )
// Set the status IRI as the 'object' property.
// We should avoid serializing the whole status
// when doing a delete because it's wasteful and
// could accidentally leak the now-deleted status.
deleteObject := streams . NewActivityStreamsObjectProperty ( )
deleteObject . AppendIRI ( statusIRI )
delete . SetActivityStreamsObject ( deleteObject )
// Address the Delete appropriately.
toProp := streams . NewActivityStreamsToProperty ( )
ccProp := streams . NewActivityStreamsCcProperty ( )
// Unless the status was a direct message, we can
// address the Delete To the ActivityPub Public URI.
// This ensures that the Delete will have as wide an
// audience as possible.
//
// Because we're using just the status URI, not the
// whole status, it won't leak any sensitive info.
// At worst, a remote instance becomes aware of the
// URI for a status which is now deleted anyway.
if s . Visibility != gtsmodel . VisibilityDirect {
publicURI , err := url . Parse ( pub . PublicActivityPubIRI )
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error parsing url %s: %w" , pub . PublicActivityPubIRI , err )
}
toProp . AppendIRI ( publicURI )
actorFollowersURI , err := url . Parse ( s . Account . FollowersURI )
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error parsing url %s: %w" , s . Account . FollowersURI , err )
}
ccProp . AppendIRI ( actorFollowersURI )
}
// Always include the replied-to account and any
// mentioned accounts as addressees as well.
//
// Worst case scenario here is that a replied account
// who wasn't mentioned (and perhaps didn't see the
// message), sees that someone has now deleted a status
// in which they were replied to but not mentioned. In
// other words, they *might* see that someone subtooted
// about them, but they won't know what was said.
// Ensure mentions are populated.
mentions := s . Mentions
if len ( s . MentionIDs ) > len ( mentions ) {
2023-09-23 16:44:11 +00:00
mentions , err = c . state . DB . GetMentions ( ctx , s . MentionIDs )
2023-03-03 19:56:34 +00:00
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error getting mentions: %w" , err )
}
}
// Remember which accounts were mentioned
// here to avoid duplicating them later.
mentionedAccountIDs := make ( map [ string ] interface { } , len ( mentions ) )
// For direct messages, add URI
// to To, else just add to CC.
2023-09-20 15:49:46 +00:00
var f func ( * url . URL )
2023-03-03 19:56:34 +00:00
if s . Visibility == gtsmodel . VisibilityDirect {
f = toProp . AppendIRI
} else {
f = ccProp . AppendIRI
}
for _ , m := range mentions {
mentionedAccountIDs [ m . TargetAccountID ] = nil // Remember this ID.
iri , err := url . Parse ( m . TargetAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "StatusToAS: error parsing uri %s: %s" , m . TargetAccount . URI , err )
}
f ( iri )
}
if s . InReplyToAccountID != "" {
if _ , ok := mentionedAccountIDs [ s . InReplyToAccountID ] ; ! ok {
// Only address to this account if it
// wasn't already included as a mention.
if s . InReplyToAccount == nil {
2023-09-23 16:44:11 +00:00
s . InReplyToAccount , err = c . state . DB . GetAccountByID ( ctx , s . InReplyToAccountID )
2023-03-03 19:56:34 +00:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , fmt . Errorf ( "StatusToASDelete: db error getting account %s: %w" , s . InReplyToAccountID , err )
}
}
if s . InReplyToAccount != nil {
inReplyToAccountURI , err := url . Parse ( s . InReplyToAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "StatusToASDelete: error parsing url %s: %w" , s . InReplyToAccount . URI , err )
}
ccProp . AppendIRI ( inReplyToAccountURI )
}
}
}
delete . SetActivityStreamsTo ( toProp )
delete . SetActivityStreamsCc ( ccProp )
return delete , nil
}
2023-09-23 16:44:11 +00:00
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
func ( c * Converter ) FollowToAS ( ctx context . Context , f * gtsmodel . Follow ) ( vocab . ActivityStreamsFollow , error ) {
if err := c . state . DB . PopulateFollow ( ctx , f ) ; err != nil {
2023-08-09 17:14:33 +00:00
return nil , gtserror . Newf ( "error populating follow: %w" , err )
}
// Parse out the various URIs we need for this
// origin account (who's doing the follow).
originAccountURI , err := url . Parse ( f . Account . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
return nil , fmt . Errorf ( "followtoasfollow: error parsing origin account uri: %s" , err )
}
originActor := streams . NewActivityStreamsActorProperty ( )
originActor . AppendIRI ( originAccountURI )
// target account (who's being followed)
2023-08-09 17:14:33 +00:00
targetAccountURI , err := url . Parse ( f . TargetAccount . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
return nil , fmt . Errorf ( "followtoasfollow: error parsing target account uri: %s" , err )
}
// uri of the follow activity itself
followURI , err := url . Parse ( f . URI )
if err != nil {
return nil , fmt . Errorf ( "followtoasfollow: error parsing follow uri: %s" , err )
}
// start preparing the follow activity
follow := streams . NewActivityStreamsFollow ( )
// set the actor
follow . SetActivityStreamsActor ( originActor )
// set the id
followIDProp := streams . NewJSONLDIdProperty ( )
followIDProp . SetIRI ( followURI )
follow . SetJSONLDId ( followIDProp )
// set the object
followObjectProp := streams . NewActivityStreamsObjectProperty ( )
followObjectProp . AppendIRI ( targetAccountURI )
follow . SetActivityStreamsObject ( followObjectProp )
// set the To property
followToProp := streams . NewActivityStreamsToProperty ( )
followToProp . AppendIRI ( targetAccountURI )
follow . SetActivityStreamsTo ( followToProp )
return follow , nil
}
2023-09-23 16:44:11 +00:00
// MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation
func ( c * Converter ) MentionToAS ( ctx context . Context , m * gtsmodel . Mention ) ( vocab . ActivityStreamsMention , error ) {
2021-08-29 10:03:08 +00:00
if m . TargetAccount == nil {
2023-09-23 16:44:11 +00:00
a , err := c . state . DB . GetAccountByID ( ctx , m . TargetAccountID )
2021-08-29 10:03:08 +00:00
if err != nil {
2021-05-21 13:48:26 +00:00
return nil , fmt . Errorf ( "MentionToAS: error getting target account from db: %s" , err )
}
2021-08-29 10:03:08 +00:00
m . TargetAccount = a
2021-05-21 13:48:26 +00:00
}
// create the mention
mention := streams . NewActivityStreamsMention ( )
// href -- this should be the URI of the mentioned user
hrefProp := streams . NewActivityStreamsHrefProperty ( )
2021-08-29 10:03:08 +00:00
hrefURI , err := url . Parse ( m . TargetAccount . URI )
2021-05-21 13:48:26 +00:00
if err != nil {
2021-08-29 10:03:08 +00:00
return nil , fmt . Errorf ( "MentionToAS: error parsing uri %s: %s" , m . TargetAccount . URI , err )
2021-05-21 13:48:26 +00:00
}
hrefProp . SetIRI ( hrefURI )
mention . SetActivityStreamsHref ( hrefProp )
// name -- this should be the namestring of the mentioned user, something like @whatever@example.org
var domain string
2021-08-29 10:03:08 +00:00
if m . TargetAccount . Domain == "" {
2022-05-30 12:41:24 +00:00
accountDomain := config . GetAccountDomain ( )
2022-02-06 13:07:38 +00:00
if accountDomain == "" {
2022-05-30 12:41:24 +00:00
accountDomain = config . GetHost ( )
2022-02-06 13:07:38 +00:00
}
2021-12-07 12:31:39 +00:00
domain = accountDomain
2021-05-21 13:48:26 +00:00
} else {
2021-08-29 10:03:08 +00:00
domain = m . TargetAccount . Domain
2021-05-21 13:48:26 +00:00
}
2021-08-29 10:03:08 +00:00
username := m . TargetAccount . Username
2021-05-21 13:48:26 +00:00
nameString := fmt . Sprintf ( "@%s@%s" , username , domain )
nameProp := streams . NewActivityStreamsNameProperty ( )
nameProp . AppendXMLSchemaString ( nameString )
mention . SetActivityStreamsName ( nameProp )
return mention , nil
}
2023-09-23 16:44:11 +00:00
// TagToAS converts a gts model tag into a toot Hashtag, suitable for federation.
func ( c * Converter ) TagToAS ( ctx context . Context , t * gtsmodel . Tag ) ( vocab . TootHashtag , error ) {
2023-07-31 13:47:35 +00:00
// This is probably already lowercase,
// but let's err on the safe side.
nameLower := strings . ToLower ( t . Name )
tagURLString := uris . GenerateURIForTag ( nameLower )
// Create the tag.
tag := streams . NewTootHashtag ( )
// `href` should be the URL of the tag.
hrefProp := streams . NewActivityStreamsHrefProperty ( )
tagURL , err := url . Parse ( tagURLString )
if err != nil {
return nil , gtserror . Newf ( "error parsing url %s: %w" , tagURLString , err )
}
hrefProp . SetIRI ( tagURL )
tag . SetActivityStreamsHref ( hrefProp )
// `name` should be the name of the tag with the # prefix.
nameProp := streams . NewActivityStreamsNameProperty ( )
nameProp . AppendXMLSchemaString ( "#" + nameLower )
tag . SetActivityStreamsName ( nameProp )
return tag , nil
}
2023-09-23 16:44:11 +00:00
// EmojiToAS converts a gts emoji into a mastodon ns Emoji, suitable for federation.
// we're making something like this:
//
// {
// "id": "https://example.com/emoji/123",
// "type": "Emoji",
// "name": ":kappa:",
// "icon": {
// "type": "Image",
// "mediaType": "image/png",
// "url": "https://example.com/files/kappa.png"
// }
// }
func ( c * Converter ) EmojiToAS ( ctx context . Context , e * gtsmodel . Emoji ) ( vocab . TootEmoji , error ) {
2022-09-02 10:11:43 +00:00
// create the emoji
emoji := streams . NewTootEmoji ( )
// set the ID property to the blocks's URI
idProp := streams . NewJSONLDIdProperty ( )
idIRI , err := url . Parse ( e . URI )
if err != nil {
return nil , fmt . Errorf ( "EmojiToAS: error parsing uri %s: %s" , e . URI , err )
}
idProp . Set ( idIRI )
emoji . SetJSONLDId ( idProp )
nameProp := streams . NewActivityStreamsNameProperty ( )
nameString := fmt . Sprintf ( ":%s:" , e . Shortcode )
nameProp . AppendXMLSchemaString ( nameString )
emoji . SetActivityStreamsName ( nameProp )
iconProperty := streams . NewActivityStreamsIconProperty ( )
iconImage := streams . NewActivityStreamsImage ( )
mediaType := streams . NewActivityStreamsMediaTypeProperty ( )
mediaType . Set ( e . ImageContentType )
iconImage . SetActivityStreamsMediaType ( mediaType )
emojiURLProperty := streams . NewActivityStreamsUrlProperty ( )
emojiURL , err := url . Parse ( e . ImageURL )
if err != nil {
return nil , fmt . Errorf ( "EmojiToAS: error parsing url %s: %s" , e . ImageURL , err )
}
emojiURLProperty . AppendIRI ( emojiURL )
iconImage . SetActivityStreamsUrl ( emojiURLProperty )
iconProperty . AppendActivityStreamsImage ( iconImage )
emoji . SetActivityStreamsIcon ( iconProperty )
2022-10-13 13:16:24 +00:00
updatedProp := streams . NewActivityStreamsUpdatedProperty ( )
updatedProp . Set ( e . ImageUpdatedAt )
emoji . SetActivityStreamsUpdated ( updatedProp )
2022-09-02 10:11:43 +00:00
return emoji , nil
}
2023-09-23 16:44:11 +00:00
// AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation
func ( c * Converter ) AttachmentToAS ( ctx context . Context , a * gtsmodel . MediaAttachment ) ( vocab . ActivityStreamsDocument , error ) {
2021-05-21 13:48:26 +00:00
// type -- Document
doc := streams . NewActivityStreamsDocument ( )
// mediaType aka mime content type
mediaTypeProp := streams . NewActivityStreamsMediaTypeProperty ( )
mediaTypeProp . Set ( a . File . ContentType )
doc . SetActivityStreamsMediaType ( mediaTypeProp )
// url -- for the original image not the thumbnail
urlProp := streams . NewActivityStreamsUrlProperty ( )
imageURL , err := url . Parse ( a . URL )
if err != nil {
return nil , fmt . Errorf ( "AttachmentToAS: error parsing uri %s: %s" , a . URL , err )
}
urlProp . AppendIRI ( imageURL )
doc . SetActivityStreamsUrl ( urlProp )
// name -- aka image description
nameProp := streams . NewActivityStreamsNameProperty ( )
nameProp . AppendXMLSchemaString ( a . Description )
doc . SetActivityStreamsName ( nameProp )
// blurhash
blurProp := streams . NewTootBlurhashProperty ( )
blurProp . Set ( a . Blurhash )
doc . SetTootBlurhash ( blurProp )
// focalpoint
// TODO
return doc , nil
2021-05-08 12:25:55 +00:00
}
2021-05-24 16:49:48 +00:00
2023-09-23 16:44:11 +00:00
// FaveToAS converts a gts model status fave into an activityStreams LIKE, suitable for federation.
// We want to end up with something like this:
//
// {
// "@context": "https://www.w3.org/ns/activitystreams",
// "actor": "https://ondergrond.org/users/dumpsterqueer",
// "id": "https://ondergrond.org/users/dumpsterqueer#likes/44584",
// "object": "https://testingtesting123.xyz/users/gotosocial_test_account/statuses/771aea80-a33d-4d6d-8dfd-57d4d2bfcbd4",
// "type": "Like"
// }
func ( c * Converter ) FaveToAS ( ctx context . Context , f * gtsmodel . StatusFave ) ( vocab . ActivityStreamsLike , error ) {
2021-05-24 16:49:48 +00:00
// check if targetStatus is already pinned to this fave, and fetch it if not
2021-08-20 10:26:56 +00:00
if f . Status == nil {
2023-09-23 16:44:11 +00:00
s , err := c . state . DB . GetStatusByID ( ctx , f . StatusID )
2021-08-29 10:03:08 +00:00
if err != nil {
2021-05-24 16:49:48 +00:00
return nil , fmt . Errorf ( "FaveToAS: error fetching target status from database: %s" , err )
}
2021-08-20 10:26:56 +00:00
f . Status = s
2021-05-24 16:49:48 +00:00
}
// check if the targetAccount is already pinned to this fave, and fetch it if not
2021-08-20 10:26:56 +00:00
if f . TargetAccount == nil {
2023-09-23 16:44:11 +00:00
a , err := c . state . DB . GetAccountByID ( ctx , f . TargetAccountID )
2021-08-29 10:03:08 +00:00
if err != nil {
2021-05-24 16:49:48 +00:00
return nil , fmt . Errorf ( "FaveToAS: error fetching target account from database: %s" , err )
}
2021-08-20 10:26:56 +00:00
f . TargetAccount = a
2021-05-24 16:49:48 +00:00
}
// check if the faving account is already pinned to this fave, and fetch it if not
2021-08-20 10:26:56 +00:00
if f . Account == nil {
2023-09-23 16:44:11 +00:00
a , err := c . state . DB . GetAccountByID ( ctx , f . AccountID )
2021-08-29 10:03:08 +00:00
if err != nil {
2021-05-24 16:49:48 +00:00
return nil , fmt . Errorf ( "FaveToAS: error fetching faving account from database: %s" , err )
}
2021-08-20 10:26:56 +00:00
f . Account = a
2021-05-24 16:49:48 +00:00
}
// create the like
like := streams . NewActivityStreamsLike ( )
// set the actor property to the fave-ing account's URI
actorProp := streams . NewActivityStreamsActorProperty ( )
2021-08-20 10:26:56 +00:00
actorIRI , err := url . Parse ( f . Account . URI )
2021-05-24 16:49:48 +00:00
if err != nil {
2021-08-20 10:26:56 +00:00
return nil , fmt . Errorf ( "FaveToAS: error parsing uri %s: %s" , f . Account . URI , err )
2021-05-24 16:49:48 +00:00
}
actorProp . AppendIRI ( actorIRI )
like . SetActivityStreamsActor ( actorProp )
// set the ID property to the fave's URI
idProp := streams . NewJSONLDIdProperty ( )
idIRI , err := url . Parse ( f . URI )
if err != nil {
return nil , fmt . Errorf ( "FaveToAS: error parsing uri %s: %s" , f . URI , err )
}
idProp . Set ( idIRI )
like . SetJSONLDId ( idProp )
// set the object property to the target status's URI
objectProp := streams . NewActivityStreamsObjectProperty ( )
2021-08-20 10:26:56 +00:00
statusIRI , err := url . Parse ( f . Status . URI )
2021-05-24 16:49:48 +00:00
if err != nil {
2021-08-20 10:26:56 +00:00
return nil , fmt . Errorf ( "FaveToAS: error parsing uri %s: %s" , f . Status . URI , err )
2021-05-24 16:49:48 +00:00
}
objectProp . AppendIRI ( statusIRI )
like . SetActivityStreamsObject ( objectProp )
// set the TO property to the target account's IRI
toProp := streams . NewActivityStreamsToProperty ( )
2021-08-20 10:26:56 +00:00
toIRI , err := url . Parse ( f . TargetAccount . URI )
2021-05-24 16:49:48 +00:00
if err != nil {
2021-08-20 10:26:56 +00:00
return nil , fmt . Errorf ( "FaveToAS: error parsing uri %s: %s" , f . TargetAccount . URI , err )
2021-05-24 16:49:48 +00:00
}
toProp . AppendIRI ( toIRI )
like . SetActivityStreamsTo ( toProp )
return like , nil
}
2021-05-28 17:57:04 +00:00
2023-09-23 16:44:11 +00:00
// BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation
func ( c * Converter ) BoostToAS ( ctx context . Context , boostWrapperStatus * gtsmodel . Status , boostingAccount * gtsmodel . Account , boostedAccount * gtsmodel . Account ) ( vocab . ActivityStreamsAnnounce , error ) {
2021-05-28 17:57:04 +00:00
// the boosted status is probably pinned to the boostWrapperStatus but double check to make sure
2021-08-20 10:26:56 +00:00
if boostWrapperStatus . BoostOf == nil {
2023-09-23 16:44:11 +00:00
b , err := c . state . DB . GetStatusByID ( ctx , boostWrapperStatus . BoostOfID )
2021-08-29 10:03:08 +00:00
if err != nil {
2021-05-28 17:57:04 +00:00
return nil , fmt . Errorf ( "BoostToAS: error getting status with ID %s from the db: %s" , boostWrapperStatus . BoostOfID , err )
}
2021-08-20 10:26:56 +00:00
boostWrapperStatus . BoostOf = b
2021-05-28 17:57:04 +00:00
}
// create the announce
announce := streams . NewActivityStreamsAnnounce ( )
// set the actor
boosterURI , err := url . Parse ( boostingAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "BoostToAS: error parsing uri %s: %s" , boostingAccount . URI , err )
}
actorProp := streams . NewActivityStreamsActorProperty ( )
actorProp . AppendIRI ( boosterURI )
announce . SetActivityStreamsActor ( actorProp )
// set the ID
boostIDURI , err := url . Parse ( boostWrapperStatus . URI )
if err != nil {
return nil , fmt . Errorf ( "BoostToAS: error parsing uri %s: %s" , boostWrapperStatus . URI , err )
}
idProp := streams . NewJSONLDIdProperty ( )
idProp . SetIRI ( boostIDURI )
announce . SetJSONLDId ( idProp )
// set the object
2021-08-20 10:26:56 +00:00
boostedStatusURI , err := url . Parse ( boostWrapperStatus . BoostOf . URI )
2021-05-28 17:57:04 +00:00
if err != nil {
2021-08-20 10:26:56 +00:00
return nil , fmt . Errorf ( "BoostToAS: error parsing uri %s: %s" , boostWrapperStatus . BoostOf . URI , err )
2021-05-28 17:57:04 +00:00
}
objectProp := streams . NewActivityStreamsObjectProperty ( )
objectProp . AppendIRI ( boostedStatusURI )
announce . SetActivityStreamsObject ( objectProp )
// set the published time
publishedProp := streams . NewActivityStreamsPublishedProperty ( )
publishedProp . Set ( boostWrapperStatus . CreatedAt )
announce . SetActivityStreamsPublished ( publishedProp )
// set the to
followersURI , err := url . Parse ( boostingAccount . FollowersURI )
if err != nil {
return nil , fmt . Errorf ( "BoostToAS: error parsing uri %s: %s" , boostingAccount . FollowersURI , err )
}
toProp := streams . NewActivityStreamsToProperty ( )
toProp . AppendIRI ( followersURI )
announce . SetActivityStreamsTo ( toProp )
// set the cc
2022-07-04 13:41:20 +00:00
ccProp := streams . NewActivityStreamsCcProperty ( )
boostedAccountURI , err := url . Parse ( boostedAccount . URI )
2021-05-28 17:57:04 +00:00
if err != nil {
return nil , fmt . Errorf ( "BoostToAS: error parsing uri %s: %s" , boostedAccount . URI , err )
}
2022-07-04 13:41:20 +00:00
ccProp . AppendIRI ( boostedAccountURI )
2021-05-28 17:57:04 +00:00
2022-07-04 13:41:20 +00:00
// maybe CC it to public depending on the boosted status visibility
switch boostWrapperStatus . BoostOf . Visibility {
case gtsmodel . VisibilityPublic , gtsmodel . VisibilityUnlocked :
publicURI , err := url . Parse ( pub . PublicActivityPubIRI )
if err != nil {
return nil , fmt . Errorf ( "BoostToAS: error parsing uri %s: %s" , pub . PublicActivityPubIRI , err )
}
ccProp . AppendIRI ( publicURI )
2021-05-28 17:57:04 +00:00
}
announce . SetActivityStreamsCc ( ccProp )
return announce , nil
}
2021-07-11 14:22:21 +00:00
2023-09-23 16:44:11 +00:00
// BlockToAS converts a gts model block into an activityStreams BLOCK, suitable for federation.
// we want to end up with something like this:
//
// {
// "@context": "https://www.w3.org/ns/activitystreams",
// "actor": "https://example.org/users/some_user",
// "id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK",
// "object":"https://some_other.instance/users/some_other_user",
// "type":"Block"
// }
func ( c * Converter ) BlockToAS ( ctx context . Context , b * gtsmodel . Block ) ( vocab . ActivityStreamsBlock , error ) {
2021-07-11 14:22:21 +00:00
if b . Account == nil {
2023-09-23 16:44:11 +00:00
a , err := c . state . DB . GetAccountByID ( ctx , b . AccountID )
2021-08-29 10:03:08 +00:00
if err != nil {
return nil , fmt . Errorf ( "BlockToAS: error getting block owner account from database: %s" , err )
2021-07-11 14:22:21 +00:00
}
b . Account = a
}
if b . TargetAccount == nil {
2023-09-23 16:44:11 +00:00
a , err := c . state . DB . GetAccountByID ( ctx , b . TargetAccountID )
2021-08-29 10:03:08 +00:00
if err != nil {
2021-07-11 14:22:21 +00:00
return nil , fmt . Errorf ( "BlockToAS: error getting block target account from database: %s" , err )
}
b . TargetAccount = a
}
// create the block
block := streams . NewActivityStreamsBlock ( )
// set the actor property to the block-ing account's URI
actorProp := streams . NewActivityStreamsActorProperty ( )
actorIRI , err := url . Parse ( b . Account . URI )
if err != nil {
return nil , fmt . Errorf ( "BlockToAS: error parsing uri %s: %s" , b . Account . URI , err )
}
actorProp . AppendIRI ( actorIRI )
block . SetActivityStreamsActor ( actorProp )
// set the ID property to the blocks's URI
idProp := streams . NewJSONLDIdProperty ( )
idIRI , err := url . Parse ( b . URI )
if err != nil {
return nil , fmt . Errorf ( "BlockToAS: error parsing uri %s: %s" , b . URI , err )
}
idProp . Set ( idIRI )
block . SetJSONLDId ( idProp )
// set the object property to the target account's URI
objectProp := streams . NewActivityStreamsObjectProperty ( )
targetIRI , err := url . Parse ( b . TargetAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "BlockToAS: error parsing uri %s: %s" , b . TargetAccount . URI , err )
}
objectProp . AppendIRI ( targetIRI )
block . SetActivityStreamsObject ( objectProp )
// set the TO property to the target account's IRI
toProp := streams . NewActivityStreamsToProperty ( )
toIRI , err := url . Parse ( b . TargetAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "BlockToAS: error parsing uri %s: %s" , b . TargetAccount . URI , err )
}
toProp . AppendIRI ( toIRI )
block . SetActivityStreamsTo ( toProp )
return block , nil
}
2021-08-10 11:32:39 +00:00
2023-09-23 16:44:11 +00:00
// StatusToASRepliesCollection converts a gts model status into an activityStreams REPLIES collection.
// the goal is to end up with something like this:
//
// {
// "@context": "https://www.w3.org/ns/activitystreams",
// "id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
// "type": "Collection",
// "first": {
// "id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?page=true",
// "type": "CollectionPage",
// "next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true",
// "partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
// "items": []
// }
// }
func ( c * Converter ) StatusToASRepliesCollection ( ctx context . Context , status * gtsmodel . Status , onlyOtherAccounts bool ) ( vocab . ActivityStreamsCollection , error ) {
2021-08-10 11:32:39 +00:00
collectionID := fmt . Sprintf ( "%s/replies" , status . URI )
collectionIDURI , err := url . Parse ( collectionID )
if err != nil {
return nil , err
}
collection := streams . NewActivityStreamsCollection ( )
// collection.id
collectionIDProp := streams . NewJSONLDIdProperty ( )
collectionIDProp . SetIRI ( collectionIDURI )
collection . SetJSONLDId ( collectionIDProp )
// first
first := streams . NewActivityStreamsFirstProperty ( )
firstPage := streams . NewActivityStreamsCollectionPage ( )
// first.id
firstPageIDProp := streams . NewJSONLDIdProperty ( )
firstPageID , err := url . Parse ( fmt . Sprintf ( "%s?page=true" , collectionID ) )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
firstPageIDProp . SetIRI ( firstPageID )
firstPage . SetJSONLDId ( firstPageIDProp )
// first.next
nextProp := streams . NewActivityStreamsNextProperty ( )
nextPropID , err := url . Parse ( fmt . Sprintf ( "%s?only_other_accounts=%t&page=true" , collectionID , onlyOtherAccounts ) )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
nextProp . SetIRI ( nextPropID )
firstPage . SetActivityStreamsNext ( nextProp )
// first.partOf
partOfProp := streams . NewActivityStreamsPartOfProperty ( )
partOfProp . SetIRI ( collectionIDURI )
firstPage . SetActivityStreamsPartOf ( partOfProp )
first . SetActivityStreamsCollectionPage ( firstPage )
// collection.first
collection . SetActivityStreamsFirst ( first )
return collection , nil
}
2023-09-23 16:44:11 +00:00
// StatusURIsToASRepliesPage returns a collection page with appropriate next/part of pagination.
// the goal is to end up with something like this:
//
// {
// "@context": "https://www.w3.org/ns/activitystreams",
// "id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true",
// "type": "CollectionPage",
// "next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?min_id=106720870266901180&only_other_accounts=true&page=true",
// "partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
// "items": [
// "https://example.com/users/someone/statuses/106720752853216226",
// "https://somewhere.online/users/eeeeeeeeeep/statuses/106720870163727231"
// ]
// }
func ( c * Converter ) StatusURIsToASRepliesPage ( ctx context . Context , status * gtsmodel . Status , onlyOtherAccounts bool , minID string , replies map [ string ] * url . URL ) ( vocab . ActivityStreamsCollectionPage , error ) {
2021-08-10 11:32:39 +00:00
collectionID := fmt . Sprintf ( "%s/replies" , status . URI )
page := streams . NewActivityStreamsCollectionPage ( )
// .id
pageIDProp := streams . NewJSONLDIdProperty ( )
pageIDString := fmt . Sprintf ( "%s?page=true&only_other_accounts=%t" , collectionID , onlyOtherAccounts )
if minID != "" {
pageIDString = fmt . Sprintf ( "%s&min_id=%s" , pageIDString , minID )
}
pageID , err := url . Parse ( pageIDString )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
pageIDProp . SetIRI ( pageID )
page . SetJSONLDId ( pageIDProp )
// .partOf
collectionIDURI , err := url . Parse ( collectionID )
if err != nil {
return nil , err
}
partOfProp := streams . NewActivityStreamsPartOfProperty ( )
partOfProp . SetIRI ( collectionIDURI )
page . SetActivityStreamsPartOf ( partOfProp )
// .items
items := streams . NewActivityStreamsItemsProperty ( )
var highestID string
for k , v := range replies {
items . AppendIRI ( v )
if k > highestID {
highestID = k
}
}
page . SetActivityStreamsItems ( items )
// .next
nextProp := streams . NewActivityStreamsNextProperty ( )
nextPropIDString := fmt . Sprintf ( "%s?only_other_accounts=%t&page=true" , collectionID , onlyOtherAccounts )
if highestID != "" {
nextPropIDString = fmt . Sprintf ( "%s&min_id=%s" , nextPropIDString , highestID )
}
nextPropID , err := url . Parse ( nextPropIDString )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
nextProp . SetIRI ( nextPropID )
page . SetActivityStreamsNext ( nextProp )
return page , nil
}
2021-10-24 09:57:39 +00:00
2023-09-23 16:44:11 +00:00
// StatusesToASOutboxPage returns an ordered collection page using the given statuses and parameters as contents.
//
// The maxID and minID should be the parameters that were passed to the database to obtain the given statuses.
// These will be used to create the 'id' field of the collection.
//
// OutboxID is used to create the 'partOf' field in the collection.
//
// Appropriate 'next' and 'prev' fields will be created based on the highest and lowest IDs present in the statuses slice.
// the goal is to end up with something like this:
//
// {
// "id": "https://example.org/users/whatever/outbox?page=true",
// "type": "OrderedCollectionPage",
// "next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
// "prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
// "partOf": "https://example.org/users/whatever/outbox",
// "orderedItems": [
// "id": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7/activity",
// "type": "Create",
// "actor": "https://example.org/users/whatever",
// "published": "2021-10-18T20:06:18Z",
// "to": [
// "https://www.w3.org/ns/activitystreams#Public"
// ],
// "cc": [
// "https://example.org/users/whatever/followers"
// ],
// "object": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7"
// ]
// }
func ( c * Converter ) StatusesToASOutboxPage ( ctx context . Context , outboxID string , maxID string , minID string , statuses [ ] * gtsmodel . Status ) ( vocab . ActivityStreamsOrderedCollectionPage , error ) {
2021-10-24 09:57:39 +00:00
page := streams . NewActivityStreamsOrderedCollectionPage ( )
// .id
pageIDProp := streams . NewJSONLDIdProperty ( )
pageID := fmt . Sprintf ( "%s?page=true" , outboxID )
if minID != "" {
pageID = fmt . Sprintf ( "%s&minID=%s" , pageID , minID )
}
if maxID != "" {
pageID = fmt . Sprintf ( "%s&maxID=%s" , pageID , maxID )
}
pageIDURI , err := url . Parse ( pageID )
if err != nil {
return nil , err
}
pageIDProp . SetIRI ( pageIDURI )
page . SetJSONLDId ( pageIDProp )
// .partOf
collectionIDURI , err := url . Parse ( outboxID )
if err != nil {
return nil , err
}
partOfProp := streams . NewActivityStreamsPartOfProperty ( )
partOfProp . SetIRI ( collectionIDURI )
page . SetActivityStreamsPartOf ( partOfProp )
// .orderedItems
itemsProp := streams . NewActivityStreamsOrderedItemsProperty ( )
var highest string
var lowest string
for _ , s := range statuses {
note , err := c . StatusToAS ( ctx , s )
if err != nil {
return nil , err
}
2023-10-04 12:09:42 +00:00
create , err := c . WrapStatusableInCreate ( note , true )
2021-10-24 09:57:39 +00:00
if err != nil {
return nil , err
}
itemsProp . AppendActivityStreamsCreate ( create )
if highest == "" || s . ID > highest {
highest = s . ID
}
if lowest == "" || s . ID < lowest {
lowest = s . ID
}
}
page . SetActivityStreamsOrderedItems ( itemsProp )
// .next
if lowest != "" {
nextProp := streams . NewActivityStreamsNextProperty ( )
nextPropIDString := fmt . Sprintf ( "%s?page=true&max_id=%s" , outboxID , lowest )
nextPropIDURI , err := url . Parse ( nextPropIDString )
if err != nil {
return nil , err
}
nextProp . SetIRI ( nextPropIDURI )
page . SetActivityStreamsNext ( nextProp )
}
// .prev
if highest != "" {
prevProp := streams . NewActivityStreamsPrevProperty ( )
prevPropIDString := fmt . Sprintf ( "%s?page=true&min_id=%s" , outboxID , highest )
prevPropIDURI , err := url . Parse ( prevPropIDString )
if err != nil {
return nil , err
}
prevProp . SetIRI ( prevPropIDURI )
page . SetActivityStreamsPrev ( prevProp )
}
return page , nil
}
2023-09-23 16:44:11 +00:00
// OutboxToASCollection returns an ordered collection with appropriate id, next, and last fields.
// The returned collection won't have any actual entries; just links to where entries can be obtained.
// we want something that looks like this:
//
// {
// "@context": "https://www.w3.org/ns/activitystreams",
// "id": "https://example.org/users/whatever/outbox",
// "type": "OrderedCollection",
// "first": "https://example.org/users/whatever/outbox?page=true"
// }
func ( c * Converter ) OutboxToASCollection ( ctx context . Context , outboxID string ) ( vocab . ActivityStreamsOrderedCollection , error ) {
2021-10-24 09:57:39 +00:00
collection := streams . NewActivityStreamsOrderedCollection ( )
collectionIDProp := streams . NewJSONLDIdProperty ( )
outboxIDURI , err := url . Parse ( outboxID )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s" , outboxID )
}
collectionIDProp . SetIRI ( outboxIDURI )
collection . SetJSONLDId ( collectionIDProp )
collectionFirstProp := streams . NewActivityStreamsFirstProperty ( )
collectionFirstPropID := fmt . Sprintf ( "%s?page=true" , outboxID )
collectionFirstPropIDURI , err := url . Parse ( collectionFirstPropID )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s" , collectionFirstPropID )
}
collectionFirstProp . SetIRI ( collectionFirstPropIDURI )
collection . SetActivityStreamsFirst ( collectionFirstProp )
return collection , nil
}
2023-01-27 13:48:11 +00:00
2023-09-23 16:44:11 +00:00
// StatusesToASFeaturedCollection converts a slice of statuses into an ordered collection
// of URIs, suitable for serializing and serving via the activitypub API.
func ( c * Converter ) StatusesToASFeaturedCollection ( ctx context . Context , featuredCollectionID string , statuses [ ] * gtsmodel . Status ) ( vocab . ActivityStreamsOrderedCollection , error ) {
2023-03-01 17:52:44 +00:00
collection := streams . NewActivityStreamsOrderedCollection ( )
collectionIDProp := streams . NewJSONLDIdProperty ( )
featuredCollectionIDURI , err := url . Parse ( featuredCollectionID )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s" , featuredCollectionID )
}
collectionIDProp . SetIRI ( featuredCollectionIDURI )
collection . SetJSONLDId ( collectionIDProp )
itemsProp := streams . NewActivityStreamsOrderedItemsProperty ( )
for _ , s := range statuses {
uri , err := url . Parse ( s . URI )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s" , s . URI )
}
itemsProp . AppendIRI ( uri )
}
collection . SetActivityStreamsOrderedItems ( itemsProp )
totalItemsProp := streams . NewActivityStreamsTotalItemsProperty ( )
totalItemsProp . Set ( len ( statuses ) )
collection . SetActivityStreamsTotalItems ( totalItemsProp )
return collection , nil
}
2023-09-23 16:44:11 +00:00
// ReportToASFlag converts a gts model report into an activitystreams FLAG, suitable for federation.
func ( c * Converter ) ReportToASFlag ( ctx context . Context , r * gtsmodel . Report ) ( vocab . ActivityStreamsFlag , error ) {
2023-01-27 13:48:11 +00:00
flag := streams . NewActivityStreamsFlag ( )
flagIDProp := streams . NewJSONLDIdProperty ( )
idURI , err := url . Parse ( r . URI )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s: %w" , r . URI , err )
}
flagIDProp . SetIRI ( idURI )
flag . SetJSONLDId ( flagIDProp )
// for privacy, set the actor as the INSTANCE ACTOR,
// not as the actor who created the report
2023-09-23 16:44:11 +00:00
instanceAccount , err := c . state . DB . GetInstanceAccount ( ctx , "" )
2023-01-27 13:48:11 +00:00
if err != nil {
return nil , fmt . Errorf ( "error getting instance account: %w" , err )
}
instanceAccountIRI , err := url . Parse ( instanceAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s: %w" , instanceAccount . URI , err )
}
flagActorProp := streams . NewActivityStreamsActorProperty ( )
flagActorProp . AppendIRI ( instanceAccountIRI )
flag . SetActivityStreamsActor ( flagActorProp )
// content should be the comment submitted when the report was created
contentProp := streams . NewActivityStreamsContentProperty ( )
contentProp . AppendXMLSchemaString ( r . Comment )
flag . SetActivityStreamsContent ( contentProp )
// set at least the target account uri as the object of the flag
objectProp := streams . NewActivityStreamsObjectProperty ( )
targetAccountURI , err := url . Parse ( r . TargetAccount . URI )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s: %w" , r . TargetAccount . URI , err )
}
objectProp . AppendIRI ( targetAccountURI )
// also set status URIs if they were provided with the report
for _ , s := range r . Statuses {
statusURI , err := url . Parse ( s . URI )
if err != nil {
return nil , fmt . Errorf ( "error parsing url %s: %w" , s . URI , err )
}
objectProp . AppendIRI ( statusURI )
}
flag . SetActivityStreamsObject ( objectProp )
return flag , nil
}