mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-27 09:36:31 +00:00
23fc70f4e6
* add support for extracting Updated field from Statusable implementers * add support for status edits in the database, and update status dereferencer to handle them * remove unused AdditionalInfo{}.CreatedAt * remove unused AdditionalEmojiInfo{}.CreatedAt * update new mention creation to use status.UpdatedAt * remove mention.UpdatedAt, fixes related to NewULIDFromTime() change * add migration to remove Mention{}.UpdatedAt field * add migration to add the StatusEdit{} table * start adding tests, add delete function for status edits * add more of status edit migrations, fill in more of the necessary edit delete functionality * remove unused function * allow generating gotosocial compatible ulid via CLI with `go run ./cmd/gen-ulid` * add StatusEdit{} test models * fix new statusedits sql * use model instead of table name * actually remove the Mention.UpdatedAt field... * fix tests now new models are added, add more status edit DB tests * fix panic wording * add test for deleting status edits * don't automatically set `updated_at` field on updated statuses * flesh out more of the dereferencer status edit tests, ensure updated at field set on outgoing AS statuses * remove media_attachments.updated_at column * fix up more tests, further complete the dereferencer status edit tests * update more status serialization tests not expecting 'updated' AS property * gah!! json serialization tests!! * undo some gtscontext wrapping changes * more serialization test fixing 🥲 * more test fixing, ensure the edit.status_id field is actually set 🤦 * fix status edit test * grrr linter * add edited_at field to apimodel status * remove the choice of paging on the timeline public filtered test (otherwise it needs updating every time you add statuses ...) * ensure that status.updated_at always fits chronologically * fix more serialization tests ... * add more code comments * fix envparsing * update swagger file * properly handle media description changes during status edits * slight formatting tweak * code comment
2131 lines
66 KiB
Go
2131 lines
66 KiB
Go
// 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/>.
|
|
|
|
package typeutils
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/superseriousbusiness/activity/pub"
|
|
"github.com/superseriousbusiness/activity/streams"
|
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
|
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
|
)
|
|
|
|
// 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) {
|
|
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)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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.
|
|
manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
|
|
manuallyApprovesFollowersProp.Set(*a.Locked)
|
|
person.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp)
|
|
|
|
// discoverable
|
|
// Will be shown in the profile directory.
|
|
discoverableProp := streams.NewTootDiscoverableProperty()
|
|
discoverableProp.Set(*a.Discoverable)
|
|
person.SetTootDiscoverable(discoverableProp)
|
|
|
|
// devices
|
|
// NOT IMPLEMENTED, probably won't implement
|
|
|
|
// alsoKnownAs
|
|
// Required for Move activity.
|
|
if l := len(a.AlsoKnownAsURIs); l != 0 {
|
|
alsoKnownAsURIs := make([]*url.URL, l)
|
|
for i, rawURL := range a.AlsoKnownAsURIs {
|
|
uri, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alsoKnownAsURIs[i] = uri
|
|
}
|
|
|
|
ap.SetAlsoKnownAs(person, alsoKnownAsURIs)
|
|
}
|
|
|
|
// movedTo
|
|
// Required for Move activity.
|
|
if a.MovedToURI != "" {
|
|
movedTo, err := url.Parse(a.MovedToURI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ap.SetMovedTo(person, movedTo)
|
|
}
|
|
|
|
// 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)
|
|
|
|
// tags
|
|
tagProp := streams.NewActivityStreamsTagProperty()
|
|
|
|
// tag -- emojis
|
|
emojis := a.Emojis
|
|
if len(a.EmojiIDs) > len(emojis) {
|
|
emojis = []*gtsmodel.Emoji{}
|
|
for _, emojiID := range a.EmojiIDs {
|
|
emoji, err := c.state.DB.GetEmojiByID(ctx, emojiID)
|
|
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)
|
|
|
|
// attachment
|
|
// Used for profile fields.
|
|
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)
|
|
}
|
|
|
|
// endpoints
|
|
// NOT IMPLEMENTED -- this is for shared inbox which we don't use
|
|
|
|
// icon
|
|
// Used as profile avatar.
|
|
if a.AvatarMediaAttachmentID != "" {
|
|
if a.AvatarMediaAttachment == nil {
|
|
avatar, err := c.state.DB.GetAttachmentByID(ctx, a.AvatarMediaAttachmentID)
|
|
if err == nil {
|
|
a.AvatarMediaAttachment = avatar
|
|
} else {
|
|
log.Errorf(ctx, "error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err)
|
|
}
|
|
}
|
|
|
|
if a.AvatarMediaAttachment != nil {
|
|
iconProperty := streams.NewActivityStreamsIconProperty()
|
|
|
|
iconImage := streams.NewActivityStreamsImage()
|
|
|
|
mediaType := streams.NewActivityStreamsMediaTypeProperty()
|
|
mediaType.Set(a.AvatarMediaAttachment.File.ContentType)
|
|
iconImage.SetActivityStreamsMediaType(mediaType)
|
|
|
|
avatarURLProperty := streams.NewActivityStreamsUrlProperty()
|
|
avatarURL, err := url.Parse(a.AvatarMediaAttachment.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
avatarURLProperty.AppendIRI(avatarURL)
|
|
iconImage.SetActivityStreamsUrl(avatarURLProperty)
|
|
|
|
iconProperty.AppendActivityStreamsImage(iconImage)
|
|
person.SetActivityStreamsIcon(iconProperty)
|
|
}
|
|
}
|
|
|
|
// image
|
|
// Used as profile header.
|
|
if a.HeaderMediaAttachmentID != "" {
|
|
if a.HeaderMediaAttachment == nil {
|
|
header, err := c.state.DB.GetAttachmentByID(ctx, a.HeaderMediaAttachmentID)
|
|
if err == nil {
|
|
a.HeaderMediaAttachment = header
|
|
} else {
|
|
log.Errorf(ctx, "error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err)
|
|
}
|
|
}
|
|
|
|
if a.HeaderMediaAttachment != nil {
|
|
headerProperty := streams.NewActivityStreamsImageProperty()
|
|
|
|
headerImage := streams.NewActivityStreamsImage()
|
|
|
|
mediaType := streams.NewActivityStreamsMediaTypeProperty()
|
|
mediaType.Set(a.HeaderMediaAttachment.File.ContentType)
|
|
headerImage.SetActivityStreamsMediaType(mediaType)
|
|
|
|
headerURLProperty := streams.NewActivityStreamsUrlProperty()
|
|
headerURL, err := url.Parse(a.HeaderMediaAttachment.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headerURLProperty.AppendIRI(headerURL)
|
|
headerImage.SetActivityStreamsUrl(headerURLProperty)
|
|
|
|
headerProperty.AppendActivityStreamsImage(headerImage)
|
|
person.SetActivityStreamsImage(headerProperty)
|
|
}
|
|
}
|
|
|
|
return person, nil
|
|
}
|
|
|
|
// AccountToASMinimal converts a gts model account into an activity streams person, suitable for federation.
|
|
//
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
var status ap.Statusable
|
|
|
|
if s.Poll != nil {
|
|
// If status has poll available, we convert
|
|
// it as an AS Question (similar to a Note).
|
|
poll := streams.NewActivityStreamsQuestion()
|
|
|
|
// Add required status poll data to AS Question.
|
|
if err := c.addPollToAS(s.Poll, poll); err != nil {
|
|
return nil, gtserror.Newf("error converting poll: %w", err)
|
|
}
|
|
|
|
// Set poll as status.
|
|
status = poll
|
|
} else {
|
|
// Else we converter it as an AS Note.
|
|
status = streams.NewActivityStreamsNote()
|
|
}
|
|
|
|
// id
|
|
statusURI, err := url.Parse(s.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing url %s: %w", s.URI, err)
|
|
}
|
|
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
|
|
if s.InReplyToURI != "" {
|
|
rURI, err := url.Parse(s.InReplyToURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing url %s: %w", s.InReplyToURI, err)
|
|
}
|
|
|
|
inReplyToProp := streams.NewActivityStreamsInReplyToProperty()
|
|
inReplyToProp.AppendIRI(rURI)
|
|
status.SetActivityStreamsInReplyTo(inReplyToProp)
|
|
}
|
|
|
|
// Set created / updated at properties.
|
|
ap.SetPublished(status, s.CreatedAt)
|
|
ap.SetUpdated(status, s.UpdatedAt)
|
|
|
|
// url
|
|
if s.URL != "" {
|
|
sURL, err := url.Parse(s.URL)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing url %s: %w", s.URL, err)
|
|
}
|
|
|
|
urlProp := streams.NewActivityStreamsUrlProperty()
|
|
urlProp.AppendIRI(sURL)
|
|
status.SetActivityStreamsUrl(urlProp)
|
|
}
|
|
|
|
// attributedTo
|
|
authorAccountURI, err := url.Parse(s.Account.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing url %s: %w", s.Account.URI, err)
|
|
}
|
|
attributedToProp := streams.NewActivityStreamsAttributedToProperty()
|
|
attributedToProp.AppendIRI(authorAccountURI)
|
|
status.SetActivityStreamsAttributedTo(attributedToProp)
|
|
|
|
// tags
|
|
tagProp := streams.NewActivityStreamsTagProperty()
|
|
|
|
// tag -- mentions
|
|
mentions := s.Mentions
|
|
if len(s.MentionIDs) != len(mentions) {
|
|
mentions, err = c.state.DB.GetMentions(ctx, s.MentionIDs)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error getting mentions: %w", err)
|
|
}
|
|
}
|
|
for _, m := range mentions {
|
|
asMention, err := c.MentionToAS(ctx, m)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error converting mention to AS mention: %w", err)
|
|
}
|
|
tagProp.AppendActivityStreamsMention(asMention)
|
|
}
|
|
|
|
// tag -- emojis
|
|
emojis := s.Emojis
|
|
if len(s.EmojiIDs) != len(emojis) {
|
|
emojis, err = c.state.DB.GetEmojisByIDs(ctx, s.EmojiIDs)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error getting emojis from database: %w", err)
|
|
}
|
|
}
|
|
for _, emoji := range emojis {
|
|
asEmoji, err := c.EmojiToAS(ctx, emoji)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error converting emoji to AS emoji: %w", err)
|
|
}
|
|
tagProp.AppendTootEmoji(asEmoji)
|
|
}
|
|
|
|
// tag -- hashtags
|
|
hashtags := s.Tags
|
|
if len(s.TagIDs) != len(hashtags) {
|
|
hashtags, err = c.state.DB.GetTags(ctx, s.TagIDs)
|
|
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)
|
|
}
|
|
status.SetActivityStreamsTag(tagProp)
|
|
|
|
// parse out some URIs we need here
|
|
authorFollowersURI, err := url.Parse(s.Account.FollowersURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing url %s: %w", s.Account.FollowersURI, err)
|
|
}
|
|
|
|
publicURI, err := url.Parse(pub.PublicActivityPubIRI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing url %s: %w", pub.PublicActivityPubIRI, err)
|
|
}
|
|
|
|
// 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
|
|
for _, m := range mentions {
|
|
iri, err := url.Parse(m.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing uri %s: %w", m.TargetAccount.URI, err)
|
|
}
|
|
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)
|
|
for _, m := range mentions {
|
|
iri, err := url.Parse(m.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing uri %s: %w", m.TargetAccount.URI, err)
|
|
}
|
|
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)
|
|
for _, m := range mentions {
|
|
iri, err := url.Parse(m.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing uri %s: %w", m.TargetAccount.URI, err)
|
|
}
|
|
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)
|
|
for _, m := range mentions {
|
|
iri, err := url.Parse(m.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error parsing uri %s: %w", m.TargetAccount.URI, err)
|
|
}
|
|
ccProp.AppendIRI(iri)
|
|
}
|
|
}
|
|
status.SetActivityStreamsTo(toProp)
|
|
status.SetActivityStreamsCc(ccProp)
|
|
|
|
// conversation
|
|
// TODO
|
|
|
|
// content -- the actual post
|
|
// itself, plus the language
|
|
contentProp := streams.NewActivityStreamsContentProperty()
|
|
contentProp.AppendXMLSchemaString(s.Content)
|
|
|
|
if s.Language != "" {
|
|
contentProp.AppendRDFLangString(map[string]string{
|
|
s.Language: s.Content,
|
|
})
|
|
}
|
|
|
|
status.SetActivityStreamsContent(contentProp)
|
|
|
|
// attachments
|
|
attachmentProp := streams.NewActivityStreamsAttachmentProperty()
|
|
attachments := s.Attachments
|
|
if len(s.AttachmentIDs) != len(attachments) {
|
|
attachments, err = c.state.DB.GetAttachmentsByIDs(ctx, s.AttachmentIDs)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error getting attachments from database: %w", err)
|
|
}
|
|
}
|
|
for _, a := range attachments {
|
|
doc, err := c.AttachmentToAS(ctx, a)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("error converting attachment: %w", err)
|
|
}
|
|
attachmentProp.AppendActivityStreamsDocument(doc)
|
|
}
|
|
status.SetActivityStreamsAttachment(attachmentProp)
|
|
|
|
// replies
|
|
repliesCollection, err := c.StatusToASRepliesCollection(ctx, s, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating repliesCollection: %w", err)
|
|
}
|
|
|
|
repliesProp := streams.NewActivityStreamsRepliesProperty()
|
|
repliesProp.SetActivityStreamsCollection(repliesCollection)
|
|
status.SetActivityStreamsReplies(repliesProp)
|
|
|
|
// sensitive
|
|
sensitiveProp := streams.NewActivityStreamsSensitiveProperty()
|
|
sensitiveProp.AppendXMLSchemaBoolean(*s.Sensitive)
|
|
status.SetActivityStreamsSensitive(sensitiveProp)
|
|
|
|
// interactionPolicy
|
|
var p *gtsmodel.InteractionPolicy
|
|
if s.InteractionPolicy != nil {
|
|
// Use InteractionPolicy
|
|
// set on the status.
|
|
p = s.InteractionPolicy
|
|
} else {
|
|
// Fall back to default policy
|
|
// for the status's visibility.
|
|
p = gtsmodel.DefaultInteractionPolicyFor(s.Visibility)
|
|
}
|
|
policy, err := c.InteractionPolicyToASInteractionPolicy(ctx, p, s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating interactionPolicy: %w", err)
|
|
}
|
|
|
|
policyProp := streams.NewGoToSocialInteractionPolicyProperty()
|
|
policyProp.AppendGoToSocialInteractionPolicy(policy)
|
|
status.SetGoToSocialInteractionPolicy(policyProp)
|
|
|
|
// Parse + set approvedBy.
|
|
if s.ApprovedByURI != "" {
|
|
approvedBy, err := url.Parse(s.ApprovedByURI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing approvedBy: %w", err)
|
|
}
|
|
|
|
approvedByProp := streams.NewGoToSocialApprovedByProperty()
|
|
approvedByProp.Set(approvedBy)
|
|
status.SetGoToSocialApprovedBy(approvedByProp)
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
func (c *Converter) addPollToAS(poll *gtsmodel.Poll, dst ap.Pollable) error {
|
|
var optionsProp interface {
|
|
// the minimum interface for appending AS Notes
|
|
// to an AS type options property of some kind.
|
|
AppendActivityStreamsNote(vocab.ActivityStreamsNote)
|
|
}
|
|
|
|
if len(poll.Options) != len(poll.Votes) {
|
|
return gtserror.Newf("invalid poll %s", poll.ID)
|
|
}
|
|
|
|
if !*poll.HideCounts {
|
|
// Set total no. voting accounts.
|
|
ap.SetVotersCount(dst, *poll.Voters)
|
|
}
|
|
|
|
if *poll.Multiple {
|
|
// Create new multiple-choice (AnyOf) property for poll.
|
|
anyOfProp := streams.NewActivityStreamsAnyOfProperty()
|
|
dst.SetActivityStreamsAnyOf(anyOfProp)
|
|
optionsProp = anyOfProp
|
|
} else {
|
|
// Create new single-choice (OneOf) property for poll.
|
|
oneOfProp := streams.NewActivityStreamsOneOfProperty()
|
|
dst.SetActivityStreamsOneOf(oneOfProp)
|
|
optionsProp = oneOfProp
|
|
}
|
|
|
|
for i, name := range poll.Options {
|
|
// Create new Note object to represent option.
|
|
note := streams.NewActivityStreamsNote()
|
|
|
|
// Create new name property and set the option name.
|
|
nameProp := streams.NewActivityStreamsNameProperty()
|
|
nameProp.AppendXMLSchemaString(name)
|
|
note.SetActivityStreamsName(nameProp)
|
|
|
|
if !*poll.HideCounts {
|
|
// Create new total items property to hold the vote count.
|
|
totalItemsProp := streams.NewActivityStreamsTotalItemsProperty()
|
|
totalItemsProp.Set(poll.Votes[i])
|
|
|
|
// Create new replies property with collection to encompass count.
|
|
repliesProp := streams.NewActivityStreamsRepliesProperty()
|
|
collection := streams.NewActivityStreamsCollection()
|
|
collection.SetActivityStreamsTotalItems(totalItemsProp)
|
|
repliesProp.SetActivityStreamsCollection(collection)
|
|
|
|
// Attach the replies to Note object.
|
|
note.SetActivityStreamsReplies(repliesProp)
|
|
}
|
|
|
|
// Append the note to options property.
|
|
optionsProp.AppendActivityStreamsNote(note)
|
|
}
|
|
|
|
if !poll.ExpiresAt.IsZero() {
|
|
// Set poll endTime property.
|
|
ap.SetEndTime(dst, poll.ExpiresAt)
|
|
}
|
|
|
|
if !poll.ClosedAt.IsZero() {
|
|
// Poll is closed, set closed property.
|
|
ap.AppendClosed(dst, poll.ClosedAt)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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) {
|
|
// Parse / fetch some information
|
|
// we need to create the Delete.
|
|
|
|
if s.Account == nil {
|
|
var err error
|
|
s.Account, err = c.state.DB.GetAccountByID(ctx, s.AccountID)
|
|
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) {
|
|
mentions, err = c.state.DB.GetMentions(ctx, s.MentionIDs)
|
|
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.
|
|
var f func(*url.URL)
|
|
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 {
|
|
s.InReplyToAccount, err = c.state.DB.GetAccountByID(ctx, s.InReplyToAccountID)
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
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)
|
|
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)
|
|
targetAccountURI, err := url.Parse(f.TargetAccount.URI)
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
if m.TargetAccount == nil {
|
|
a, err := c.state.DB.GetAccountByID(ctx, m.TargetAccountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err)
|
|
}
|
|
m.TargetAccount = a
|
|
}
|
|
|
|
// create the mention
|
|
mention := streams.NewActivityStreamsMention()
|
|
|
|
// href -- this should be the URI of the mentioned user
|
|
hrefProp := streams.NewActivityStreamsHrefProperty()
|
|
hrefURI, err := url.Parse(m.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
|
|
}
|
|
hrefProp.SetIRI(hrefURI)
|
|
mention.SetActivityStreamsHref(hrefProp)
|
|
|
|
// name -- this should be the namestring of the mentioned user, something like @whatever@example.org
|
|
var domain string
|
|
if m.TargetAccount.Domain == "" {
|
|
accountDomain := config.GetAccountDomain()
|
|
if accountDomain == "" {
|
|
accountDomain = config.GetHost()
|
|
}
|
|
domain = accountDomain
|
|
} else {
|
|
domain = m.TargetAccount.Domain
|
|
}
|
|
username := m.TargetAccount.Username
|
|
nameString := fmt.Sprintf("@%s@%s", username, domain)
|
|
nameProp := streams.NewActivityStreamsNameProperty()
|
|
nameProp.AppendXMLSchemaString(nameString)
|
|
mention.SetActivityStreamsName(nameProp)
|
|
|
|
return mention, nil
|
|
}
|
|
|
|
// 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) {
|
|
// This is probably already lowercase,
|
|
// but let's err on the safe side.
|
|
nameLower := strings.ToLower(t.Name)
|
|
tagURLString := uris.URIForTag(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
|
|
}
|
|
|
|
// 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) {
|
|
// 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)
|
|
|
|
updatedProp := streams.NewActivityStreamsUpdatedProperty()
|
|
updatedProp.Set(e.UpdatedAt)
|
|
emoji.SetActivityStreamsUpdated(updatedProp)
|
|
|
|
return emoji, nil
|
|
}
|
|
|
|
// 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) {
|
|
// 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
|
|
}
|
|
|
|
// 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) {
|
|
// check if targetStatus is already pinned to this fave, and fetch it if not
|
|
if f.Status == nil {
|
|
s, err := c.state.DB.GetStatusByID(ctx, f.StatusID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err)
|
|
}
|
|
f.Status = s
|
|
}
|
|
|
|
// check if the targetAccount is already pinned to this fave, and fetch it if not
|
|
if f.TargetAccount == nil {
|
|
a, err := c.state.DB.GetAccountByID(ctx, f.TargetAccountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err)
|
|
}
|
|
f.TargetAccount = a
|
|
}
|
|
|
|
// check if the faving account is already pinned to this fave, and fetch it if not
|
|
if f.Account == nil {
|
|
a, err := c.state.DB.GetAccountByID(ctx, f.AccountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err)
|
|
}
|
|
f.Account = a
|
|
}
|
|
|
|
// create the like
|
|
like := streams.NewActivityStreamsLike()
|
|
|
|
// set the actor property to the fave-ing account's URI
|
|
actorProp := streams.NewActivityStreamsActorProperty()
|
|
actorIRI, err := url.Parse(f.Account.URI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Account.URI, err)
|
|
}
|
|
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()
|
|
statusIRI, err := url.Parse(f.Status.URI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Status.URI, err)
|
|
}
|
|
objectProp.AppendIRI(statusIRI)
|
|
like.SetActivityStreamsObject(objectProp)
|
|
|
|
// set the TO property to the target account's IRI
|
|
toProp := streams.NewActivityStreamsToProperty()
|
|
toIRI, err := url.Parse(f.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.TargetAccount.URI, err)
|
|
}
|
|
toProp.AppendIRI(toIRI)
|
|
like.SetActivityStreamsTo(toProp)
|
|
|
|
// Parse + set approvedBy.
|
|
if f.ApprovedByURI != "" {
|
|
approvedBy, err := url.Parse(f.ApprovedByURI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing approvedBy: %w", err)
|
|
}
|
|
|
|
approvedByProp := streams.NewGoToSocialApprovedByProperty()
|
|
approvedByProp.Set(approvedBy)
|
|
like.SetGoToSocialApprovedBy(approvedByProp)
|
|
}
|
|
|
|
return like, nil
|
|
}
|
|
|
|
// 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) {
|
|
// the boosted status is probably pinned to the boostWrapperStatus but double check to make sure
|
|
if boostWrapperStatus.BoostOf == nil {
|
|
b, err := c.state.DB.GetStatusByID(ctx, boostWrapperStatus.BoostOfID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err)
|
|
}
|
|
boostWrapperStatus.BoostOf = b
|
|
}
|
|
|
|
// 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
|
|
boostedStatusURI, err := url.Parse(boostWrapperStatus.BoostOf.URI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.BoostOf.URI, err)
|
|
}
|
|
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
|
|
ccProp := streams.NewActivityStreamsCcProperty()
|
|
boostedAccountURI, err := url.Parse(boostedAccount.URI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostedAccount.URI, err)
|
|
}
|
|
ccProp.AppendIRI(boostedAccountURI)
|
|
|
|
// 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)
|
|
}
|
|
|
|
announce.SetActivityStreamsCc(ccProp)
|
|
|
|
// Parse + set approvedBy.
|
|
if boostWrapperStatus.ApprovedByURI != "" {
|
|
approvedBy, err := url.Parse(boostWrapperStatus.ApprovedByURI)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing approvedBy: %w", err)
|
|
}
|
|
|
|
approvedByProp := streams.NewGoToSocialApprovedByProperty()
|
|
approvedByProp.Set(approvedBy)
|
|
announce.SetGoToSocialApprovedBy(approvedByProp)
|
|
}
|
|
|
|
return announce, nil
|
|
}
|
|
|
|
// 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) {
|
|
if b.Account == nil {
|
|
a, err := c.state.DB.GetAccountByID(ctx, b.AccountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("BlockToAS: error getting block owner account from database: %s", err)
|
|
}
|
|
b.Account = a
|
|
}
|
|
|
|
if b.TargetAccount == nil {
|
|
a, err := c.state.DB.GetAccountByID(ctx, b.TargetAccountID)
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
activity := WrapStatusableInCreate(note, true)
|
|
itemsProp.AppendActivityStreamsCreate(activity)
|
|
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
instanceAccount, err := c.state.DB.GetInstanceAccount(ctx, "")
|
|
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
|
|
}
|
|
|
|
// PollVoteToASCreate converts a vote on a poll into a Create
|
|
// activity, suitable for federation, with each choice in the
|
|
// vote appended as a Note to the Create's Object field.
|
|
//
|
|
// TODO: as soon as other AP server implementations support
|
|
// the use of multiple objects in a single create, update this
|
|
// to return just the one create event again.
|
|
func (c *Converter) PollVoteToASCreates(
|
|
ctx context.Context,
|
|
vote *gtsmodel.PollVote,
|
|
) ([]vocab.ActivityStreamsCreate, error) {
|
|
if len(vote.Choices) == 0 {
|
|
panic("no vote.Choices")
|
|
}
|
|
|
|
// Ensure the vote is fully populated (this fetches author).
|
|
if err := c.state.DB.PopulatePollVote(ctx, vote); err != nil {
|
|
return nil, gtserror.Newf("error populating vote from db: %w", err)
|
|
}
|
|
|
|
// Get the vote author.
|
|
author := vote.Account
|
|
|
|
// Get the JSONLD ID IRI for vote author.
|
|
authorIRI, err := url.Parse(author.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid author uri: %w", err)
|
|
}
|
|
|
|
// Get the vote poll.
|
|
poll := vote.Poll
|
|
|
|
// Ensure the poll is fully populated with status.
|
|
if err := c.state.DB.PopulatePoll(ctx, poll); err != nil {
|
|
return nil, gtserror.Newf("error populating poll from db: %w", err)
|
|
}
|
|
|
|
// Get the JSONLD ID IRI for poll's source status.
|
|
statusIRI, err := url.Parse(poll.Status.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid status uri: %w", err)
|
|
}
|
|
|
|
// Get the JSONLD ID IRI for poll's author account.
|
|
pollAuthorIRI, err := url.Parse(poll.Status.AccountURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid account uri: %w", err)
|
|
}
|
|
|
|
// Parse each choice to a Note and add it to the list of Creates.
|
|
creates := make([]vocab.ActivityStreamsCreate, len(vote.Choices))
|
|
for i, choice := range vote.Choices {
|
|
|
|
// Allocate Create activity and address 'To' poll author.
|
|
create := streams.NewActivityStreamsCreate()
|
|
ap.AppendTo(create, pollAuthorIRI)
|
|
|
|
// Create ID formatted as: {$voterIRI}/activity#vote{$index}/{$statusIRI}.
|
|
createID := fmt.Sprintf("%s/activity#vote%d/%s", author.URI, i, poll.Status.URI)
|
|
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), createID)
|
|
|
|
// Set Create actor appropriately.
|
|
ap.AppendActorIRIs(create, authorIRI)
|
|
|
|
// Set publish time for activity.
|
|
ap.SetPublished(create, vote.CreatedAt)
|
|
|
|
// Allocate new note to hold the vote.
|
|
note := streams.NewActivityStreamsNote()
|
|
|
|
// For AP IRI generate from author URI + poll ID + vote choice.
|
|
id := fmt.Sprintf("%s#%s/votes/%d", author.URI, poll.ID, choice)
|
|
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(note), id)
|
|
|
|
// Attach new name property to note with vote choice.
|
|
nameProp := streams.NewActivityStreamsNameProperty()
|
|
nameProp.AppendXMLSchemaString(poll.Options[choice])
|
|
note.SetActivityStreamsName(nameProp)
|
|
|
|
// Set 'to', 'attribTo', 'inReplyTo' fields.
|
|
ap.AppendAttributedTo(note, authorIRI)
|
|
ap.AppendInReplyTo(note, statusIRI)
|
|
ap.AppendTo(note, pollAuthorIRI)
|
|
|
|
// Append this note to the Create Object.
|
|
appendStatusableToActivity(create, note, false)
|
|
|
|
// Set create in slice.
|
|
creates[i] = create
|
|
}
|
|
|
|
return creates, nil
|
|
}
|
|
|
|
// populateValuesForProp appends the given PolicyValues
|
|
// to the given property, for the given status.
|
|
func populateValuesForProp[T ap.WithIRI](
|
|
prop ap.Property[T],
|
|
status *gtsmodel.Status,
|
|
urns gtsmodel.PolicyValues,
|
|
) error {
|
|
iriStrs := make([]string, 0)
|
|
|
|
for _, urn := range urns {
|
|
switch urn {
|
|
|
|
case gtsmodel.PolicyValueAuthor:
|
|
iriStrs = append(iriStrs, status.Account.URI)
|
|
|
|
case gtsmodel.PolicyValueMentioned:
|
|
for _, m := range status.Mentions {
|
|
iriStrs = append(iriStrs, m.TargetAccount.URI)
|
|
}
|
|
|
|
case gtsmodel.PolicyValueFollowing:
|
|
iriStrs = append(iriStrs, status.Account.FollowingURI)
|
|
|
|
case gtsmodel.PolicyValueFollowers:
|
|
iriStrs = append(iriStrs, status.Account.FollowersURI)
|
|
|
|
case gtsmodel.PolicyValuePublic:
|
|
iriStrs = append(iriStrs, pub.PublicActivityPubIRI)
|
|
|
|
default:
|
|
iriStrs = append(iriStrs, string(urn))
|
|
}
|
|
}
|
|
|
|
// Deduplicate the iri strings to
|
|
// make sure we're not parsing + adding
|
|
// the same string multiple times.
|
|
iriStrs = xslices.Deduplicate(iriStrs)
|
|
|
|
// Append them to the property.
|
|
for _, iriStr := range iriStrs {
|
|
iri, err := url.Parse(iriStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
prop.AppendIRI(iri)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InteractionPolicyToASInteractionPolicy returns a
|
|
// GoToSocial interaction policy suitable for federation.
|
|
func (c *Converter) InteractionPolicyToASInteractionPolicy(
|
|
ctx context.Context,
|
|
interactionPolicy *gtsmodel.InteractionPolicy,
|
|
status *gtsmodel.Status,
|
|
) (vocab.GoToSocialInteractionPolicy, error) {
|
|
policy := streams.NewGoToSocialInteractionPolicy()
|
|
|
|
/*
|
|
CAN LIKE
|
|
*/
|
|
|
|
// Build canLike
|
|
canLike := streams.NewGoToSocialCanLike()
|
|
|
|
// Build canLike.always
|
|
canLikeAlwaysProp := streams.NewGoToSocialAlwaysProperty()
|
|
if err := populateValuesForProp(
|
|
canLikeAlwaysProp,
|
|
status,
|
|
interactionPolicy.CanLike.Always,
|
|
); err != nil {
|
|
return nil, gtserror.Newf("error setting canLike.always: %w", err)
|
|
}
|
|
|
|
// Set canLike.always
|
|
canLike.SetGoToSocialAlways(canLikeAlwaysProp)
|
|
|
|
// Build canLike.approvalRequired
|
|
canLikeApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
|
|
if err := populateValuesForProp(
|
|
canLikeApprovalRequiredProp,
|
|
status,
|
|
interactionPolicy.CanLike.WithApproval,
|
|
); err != nil {
|
|
return nil, gtserror.Newf("error setting canLike.approvalRequired: %w", err)
|
|
}
|
|
|
|
// Set canLike.approvalRequired.
|
|
canLike.SetGoToSocialApprovalRequired(canLikeApprovalRequiredProp)
|
|
|
|
// Set canLike on the policy.
|
|
canLikeProp := streams.NewGoToSocialCanLikeProperty()
|
|
canLikeProp.AppendGoToSocialCanLike(canLike)
|
|
policy.SetGoToSocialCanLike(canLikeProp)
|
|
|
|
/*
|
|
CAN REPLY
|
|
*/
|
|
|
|
// Build canReply
|
|
canReply := streams.NewGoToSocialCanReply()
|
|
|
|
// Build canReply.always
|
|
canReplyAlwaysProp := streams.NewGoToSocialAlwaysProperty()
|
|
if err := populateValuesForProp(
|
|
canReplyAlwaysProp,
|
|
status,
|
|
interactionPolicy.CanReply.Always,
|
|
); err != nil {
|
|
return nil, gtserror.Newf("error setting canReply.always: %w", err)
|
|
}
|
|
|
|
// Set canReply.always
|
|
canReply.SetGoToSocialAlways(canReplyAlwaysProp)
|
|
|
|
// Build canReply.approvalRequired
|
|
canReplyApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
|
|
if err := populateValuesForProp(
|
|
canReplyApprovalRequiredProp,
|
|
status,
|
|
interactionPolicy.CanReply.WithApproval,
|
|
); err != nil {
|
|
return nil, gtserror.Newf("error setting canReply.approvalRequired: %w", err)
|
|
}
|
|
|
|
// Set canReply.approvalRequired.
|
|
canReply.SetGoToSocialApprovalRequired(canReplyApprovalRequiredProp)
|
|
|
|
// Set canReply on the policy.
|
|
canReplyProp := streams.NewGoToSocialCanReplyProperty()
|
|
canReplyProp.AppendGoToSocialCanReply(canReply)
|
|
policy.SetGoToSocialCanReply(canReplyProp)
|
|
|
|
/*
|
|
CAN ANNOUNCE
|
|
*/
|
|
|
|
// Build canAnnounce
|
|
canAnnounce := streams.NewGoToSocialCanAnnounce()
|
|
|
|
// Build canAnnounce.always
|
|
canAnnounceAlwaysProp := streams.NewGoToSocialAlwaysProperty()
|
|
if err := populateValuesForProp(
|
|
canAnnounceAlwaysProp,
|
|
status,
|
|
interactionPolicy.CanAnnounce.Always,
|
|
); err != nil {
|
|
return nil, gtserror.Newf("error setting canAnnounce.always: %w", err)
|
|
}
|
|
|
|
// Set canAnnounce.always
|
|
canAnnounce.SetGoToSocialAlways(canAnnounceAlwaysProp)
|
|
|
|
// Build canAnnounce.approvalRequired
|
|
canAnnounceApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
|
|
if err := populateValuesForProp(
|
|
canAnnounceApprovalRequiredProp,
|
|
status,
|
|
interactionPolicy.CanAnnounce.WithApproval,
|
|
); err != nil {
|
|
return nil, gtserror.Newf("error setting canAnnounce.approvalRequired: %w", err)
|
|
}
|
|
|
|
// Set canAnnounce.approvalRequired.
|
|
canAnnounce.SetGoToSocialApprovalRequired(canAnnounceApprovalRequiredProp)
|
|
|
|
// Set canAnnounce on the policy.
|
|
canAnnounceProp := streams.NewGoToSocialCanAnnounceProperty()
|
|
canAnnounceProp.AppendGoToSocialCanAnnounce(canAnnounce)
|
|
policy.SetGoToSocialCanAnnounce(canAnnounceProp)
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
// InteractionReqToASAccept converts a *gtsmodel.InteractionRequest
|
|
// to an ActivityStreams Accept, addressed to the interacting account.
|
|
func (c *Converter) InteractionReqToASAccept(
|
|
ctx context.Context,
|
|
req *gtsmodel.InteractionRequest,
|
|
) (vocab.ActivityStreamsAccept, error) {
|
|
accept := streams.NewActivityStreamsAccept()
|
|
|
|
acceptID, err := url.Parse(req.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid accept uri: %w", err)
|
|
}
|
|
|
|
actorIRI, err := url.Parse(req.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid account uri: %w", err)
|
|
}
|
|
|
|
objectIRI, err := url.Parse(req.InteractionURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid target uri: %w", err)
|
|
}
|
|
|
|
toIRI, err := url.Parse(req.InteractingAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
|
}
|
|
|
|
// Set id to the URI of
|
|
// interaction request.
|
|
ap.SetJSONLDId(accept, acceptID)
|
|
|
|
// Actor is the account that
|
|
// owns the approval / accept.
|
|
ap.AppendActorIRIs(accept, actorIRI)
|
|
|
|
// Object is the interaction URI.
|
|
ap.AppendObjectIRIs(accept, objectIRI)
|
|
|
|
// Address to the owner
|
|
// of interaction URI.
|
|
ap.AppendTo(accept, toIRI)
|
|
|
|
// Whether or not we cc this Accept to
|
|
// followers and public depends on the
|
|
// type of interaction it Accepts.
|
|
|
|
var cc bool
|
|
switch req.InteractionType {
|
|
|
|
case gtsmodel.InteractionLike:
|
|
// Accept of Like doesn't get cc'd
|
|
// because it's not that important.
|
|
|
|
case gtsmodel.InteractionReply:
|
|
// Accept of reply gets cc'd.
|
|
cc = true
|
|
|
|
case gtsmodel.InteractionAnnounce:
|
|
// Accept of announce gets cc'd.
|
|
cc = true
|
|
}
|
|
|
|
if cc {
|
|
publicIRI, err := url.Parse(pub.PublicActivityPubIRI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid public uri: %w", err)
|
|
}
|
|
|
|
followersIRI, err := url.Parse(req.TargetAccount.FollowersURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid followers uri: %w", err)
|
|
}
|
|
|
|
ap.AppendCc(accept, publicIRI, followersIRI)
|
|
}
|
|
|
|
return accept, nil
|
|
}
|
|
|
|
// InteractionReqToASReject converts a *gtsmodel.InteractionRequest
|
|
// to an ActivityStreams Reject, addressed to the interacting account.
|
|
func (c *Converter) InteractionReqToASReject(
|
|
ctx context.Context,
|
|
req *gtsmodel.InteractionRequest,
|
|
) (vocab.ActivityStreamsReject, error) {
|
|
reject := streams.NewActivityStreamsReject()
|
|
|
|
rejectID, err := url.Parse(req.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid reject uri: %w", err)
|
|
}
|
|
|
|
actorIRI, err := url.Parse(req.TargetAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid account uri: %w", err)
|
|
}
|
|
|
|
objectIRI, err := url.Parse(req.InteractionURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid target uri: %w", err)
|
|
}
|
|
|
|
toIRI, err := url.Parse(req.InteractingAccount.URI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
|
}
|
|
|
|
// Set id to the URI of
|
|
// interaction request.
|
|
ap.SetJSONLDId(reject, rejectID)
|
|
|
|
// Actor is the account that
|
|
// owns the approval / reject.
|
|
ap.AppendActorIRIs(reject, actorIRI)
|
|
|
|
// Object is the interaction URI.
|
|
ap.AppendObjectIRIs(reject, objectIRI)
|
|
|
|
// Address to the owner
|
|
// of interaction URI.
|
|
ap.AppendTo(reject, toIRI)
|
|
|
|
// Whether or not we cc this Reject to
|
|
// followers and public depends on the
|
|
// type of interaction it Rejects.
|
|
|
|
var cc bool
|
|
switch req.InteractionType {
|
|
|
|
case gtsmodel.InteractionLike:
|
|
// Reject of Like doesn't get cc'd
|
|
// because it's not that important.
|
|
|
|
case gtsmodel.InteractionReply:
|
|
// Reject of reply gets cc'd.
|
|
cc = true
|
|
|
|
case gtsmodel.InteractionAnnounce:
|
|
// Reject of announce gets cc'd.
|
|
cc = true
|
|
}
|
|
|
|
if cc {
|
|
publicIRI, err := url.Parse(pub.PublicActivityPubIRI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid public uri: %w", err)
|
|
}
|
|
|
|
followersIRI, err := url.Parse(req.TargetAccount.FollowersURI)
|
|
if err != nil {
|
|
return nil, gtserror.Newf("invalid followers uri: %w", err)
|
|
}
|
|
|
|
ap.AppendCc(reject, publicIRI, followersIRI)
|
|
}
|
|
|
|
return reject, nil
|
|
}
|