mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] add support for polls + receiving federated status edits (#2330)
This commit is contained in:
@@ -261,8 +261,10 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
|
||||
// Attached poll information (the statusable will actually
|
||||
// be a Pollable, as a Question is a subset of our Status).
|
||||
if pollable, ok := ap.ToPollable(statusable); ok {
|
||||
// TODO: handle decoding poll data
|
||||
_ = pollable
|
||||
status.Poll, err = ap.ExtractPoll(pollable)
|
||||
if err != nil {
|
||||
l.Warnf("error(s) extracting poll: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// status.Hashtags
|
||||
|
@@ -412,8 +412,24 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat
|
||||
return nil, gtserror.Newf("error populating status: %w", err)
|
||||
}
|
||||
|
||||
// We convert it as an AS Note.
|
||||
status := streams.NewActivityStreamsNote()
|
||||
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(ctx, 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)
|
||||
@@ -636,6 +652,73 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (c *Converter) addPollToAS(ctx context.Context, 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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -1413,12 +1496,8 @@ func (c *Converter) StatusesToASOutboxPage(ctx context.Context, outboxID string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
create, err := c.WrapStatusableInCreate(note, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
itemsProp.AppendActivityStreamsCreate(create)
|
||||
activity := WrapStatusableInCreate(note, true)
|
||||
itemsProp.AppendActivityStreamsCreate(activity)
|
||||
|
||||
if highest == "" || s.ID > highest {
|
||||
highest = s.ID
|
||||
@@ -1569,3 +1648,66 @@ func (c *Converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (voc
|
||||
|
||||
return flag, nil
|
||||
}
|
||||
|
||||
func (c *Converter) PollVoteToASOptions(ctx context.Context, vote *gtsmodel.PollVote) ([]ap.PollOptionable, error) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Preallocate the return slice of notes.
|
||||
notes := make([]ap.PollOptionable, len(vote.Choices))
|
||||
|
||||
for i, choice := range vote.Choices {
|
||||
// Create new note to represent 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)
|
||||
|
||||
// Set note in return slice.
|
||||
notes[i] = note
|
||||
}
|
||||
|
||||
return notes, nil
|
||||
}
|
||||
|
@@ -680,7 +680,7 @@ func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() {
|
||||
{
|
||||
"actor": "http://localhost:8080/users/admin",
|
||||
"cc": "http://localhost:8080/users/admin/followers",
|
||||
"id": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37/activity",
|
||||
"id": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37/activity#Create",
|
||||
"object": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
|
||||
"published": "2021-10-20T12:36:45Z",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
@@ -689,7 +689,7 @@ func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() {
|
||||
{
|
||||
"actor": "http://localhost:8080/users/admin",
|
||||
"cc": "http://localhost:8080/users/admin/followers",
|
||||
"id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/activity",
|
||||
"id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/activity#Create",
|
||||
"object": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"published": "2021-10-20T11:36:45Z",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
|
@@ -729,9 +729,12 @@ func (c *Converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r
|
||||
}
|
||||
|
||||
if appID := s.CreatedWithApplicationID; appID != "" {
|
||||
app, err := c.state.DB.GetApplicationByID(ctx, appID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting application %s: %w", appID, err)
|
||||
app := s.CreatedWithApplication
|
||||
if app == nil {
|
||||
app, err = c.state.DB.GetApplicationByID(ctx, appID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting application %s: %w", appID, err)
|
||||
}
|
||||
}
|
||||
|
||||
apiApp, err := c.AppToAPIAppPublic(ctx, app)
|
||||
@@ -742,6 +745,18 @@ func (c *Converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r
|
||||
apiStatus.Application = apiApp
|
||||
}
|
||||
|
||||
if s.Poll != nil {
|
||||
// Set originating
|
||||
// status on the poll.
|
||||
poll := s.Poll
|
||||
poll.Status = s
|
||||
|
||||
apiStatus.Poll, err = c.PollToAPIPoll(ctx, requestingAccount, poll)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting poll: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalization.
|
||||
|
||||
if s.URL == "" {
|
||||
@@ -1287,6 +1302,86 @@ func (c *Converter) MarkersToAPIMarker(ctx context.Context, markers []*gtsmodel.
|
||||
return apiMarker, nil
|
||||
}
|
||||
|
||||
// PollToAPIPoll converts a database (gtsmodel) Poll into an API model representation appropriate for the given requesting account.
|
||||
func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Account, poll *gtsmodel.Poll) (*apimodel.Poll, error) {
|
||||
// Ensure the poll model is fully populated for src status.
|
||||
if err := c.state.DB.PopulatePoll(ctx, poll); err != nil {
|
||||
return nil, gtserror.Newf("error populating poll: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
totalVotes int
|
||||
totalVoters int
|
||||
voteCounts []int
|
||||
ownChoices []int
|
||||
isAuthor bool
|
||||
)
|
||||
|
||||
if requester != nil {
|
||||
// Get vote by requester in poll (if any).
|
||||
vote, err := c.state.DB.GetPollVoteBy(ctx,
|
||||
poll.ID,
|
||||
requester.ID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.Newf("error getting vote for poll %s: %w", poll.ID, err)
|
||||
}
|
||||
|
||||
if vote != nil {
|
||||
// Set choices by requester.
|
||||
ownChoices = vote.Choices
|
||||
|
||||
// Update default totals in the
|
||||
// case that counts are hidden.
|
||||
totalVotes = len(vote.Choices)
|
||||
totalVoters = 1
|
||||
}
|
||||
|
||||
// Check if requester is author of source status.
|
||||
isAuthor = (requester.ID == poll.Status.AccountID)
|
||||
}
|
||||
|
||||
// Preallocate a slice of frontend model poll choices.
|
||||
options := make([]apimodel.PollOption, len(poll.Options))
|
||||
|
||||
// Add the titles to all of the options.
|
||||
for i, title := range poll.Options {
|
||||
options[i].Title = title
|
||||
}
|
||||
|
||||
if isAuthor || !*poll.HideCounts {
|
||||
// A remote status,
|
||||
// the simple route!
|
||||
//
|
||||
// Pull cached remote values.
|
||||
totalVoters = *poll.Voters
|
||||
voteCounts = poll.Votes
|
||||
|
||||
// Accumulate total from all counts.
|
||||
for _, count := range poll.Votes {
|
||||
totalVotes += count
|
||||
}
|
||||
|
||||
// When this is status author, or hide counts
|
||||
// is disabled, set the counts known per vote.
|
||||
for i, count := range voteCounts {
|
||||
options[i].VotesCount = count
|
||||
}
|
||||
}
|
||||
|
||||
return &apimodel.Poll{
|
||||
ID: poll.ID,
|
||||
ExpiresAt: util.FormatISO8601(poll.ExpiresAt),
|
||||
Expired: poll.Closed(),
|
||||
Multiple: *poll.Multiple,
|
||||
VotesCount: totalVotes,
|
||||
VotersCount: totalVoters,
|
||||
Voted: (isAuthor || len(ownChoices) > 0),
|
||||
OwnVotes: ownChoices,
|
||||
Options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied.
|
||||
func (c *Converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]apimodel.Attachment, error) {
|
||||
var errs gtserror.MultiError
|
||||
|
@@ -58,8 +58,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 5,
|
||||
"last_status_at": "2022-05-20T11:37:55.000Z",
|
||||
"statuses_count": 6,
|
||||
"last_status_at": "2022-05-20T11:41:10.000Z",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
@@ -100,8 +100,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct()
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 5,
|
||||
"last_status_at": "2022-05-20T11:37:55.000Z",
|
||||
"statuses_count": 6,
|
||||
"last_status_at": "2022-05-20T11:41:10.000Z",
|
||||
"emojis": [
|
||||
{
|
||||
"shortcode": "rainbow",
|
||||
@@ -148,8 +148,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 5,
|
||||
"last_status_at": "2022-05-20T11:37:55.000Z",
|
||||
"statuses_count": 6,
|
||||
"last_status_at": "2022-05-20T11:41:10.000Z",
|
||||
"emojis": [
|
||||
{
|
||||
"shortcode": "rainbow",
|
||||
@@ -192,8 +192,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 5,
|
||||
"last_status_at": "2022-05-20T11:37:55.000Z",
|
||||
"statuses_count": 6,
|
||||
"last_status_at": "2022-05-20T11:41:10.000Z",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"source": {
|
||||
@@ -660,7 +660,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
|
||||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 16,
|
||||
"status_count": 18,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.png",
|
||||
@@ -910,8 +910,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend1() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2021-09-20T10:40:37.000Z",
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
@@ -953,8 +953,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 7,
|
||||
"last_status_at": "2021-10-20T10:40:37.000Z",
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
@@ -1027,8 +1027,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2021-09-20T10:40:37.000Z",
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
@@ -1068,8 +1068,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 7,
|
||||
"last_status_at": "2021-10-20T10:40:37.000Z",
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
@@ -1239,8 +1239,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 7,
|
||||
"last_status_at": "2021-10-20T10:40:37.000Z",
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
@@ -1295,8 +1295,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2021-09-20T10:40:37.000Z",
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
@@ -1342,8 +1342,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2021-09-20T10:40:37.000Z",
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
@@ -1473,8 +1473,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2021-09-20T10:40:37.000Z",
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ package typeutils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
@@ -84,132 +85,86 @@ func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
|
||||
return update, nil
|
||||
}
|
||||
|
||||
// WrapNoteInCreate wraps a Statusable with a Create activity.
|
||||
//
|
||||
// If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create,
|
||||
// but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference,
|
||||
// and still have control over whether or not they're allowed to actually see the contents.
|
||||
func (c *Converter) WrapStatusableInCreate(status ap.Statusable, objectIRIOnly bool) (vocab.ActivityStreamsCreate, error) {
|
||||
func WrapStatusableInCreate(status ap.Statusable, iriOnly bool) vocab.ActivityStreamsCreate {
|
||||
create := streams.NewActivityStreamsCreate()
|
||||
|
||||
// Object property
|
||||
objectProp := streams.NewActivityStreamsObjectProperty()
|
||||
if objectIRIOnly {
|
||||
// Only append the object IRI to objectProp.
|
||||
objectProp.AppendIRI(status.GetJSONLDId().GetIRI())
|
||||
} else {
|
||||
// Our statusable's are always note types.
|
||||
asNote := status.(vocab.ActivityStreamsNote)
|
||||
objectProp.AppendActivityStreamsNote(asNote)
|
||||
}
|
||||
create.SetActivityStreamsObject(objectProp)
|
||||
|
||||
// ID property
|
||||
idProp := streams.NewJSONLDIdProperty()
|
||||
createID := status.GetJSONLDId().GetIRI().String() + "/activity"
|
||||
createIDIRI, err := url.Parse(createID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idProp.SetIRI(createIDIRI)
|
||||
create.SetJSONLDId(idProp)
|
||||
|
||||
// Actor Property
|
||||
actorProp := streams.NewActivityStreamsActorProperty()
|
||||
actorIRI, err := ap.ExtractAttributedToURI(status)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err)
|
||||
}
|
||||
actorProp.AppendIRI(actorIRI)
|
||||
create.SetActivityStreamsActor(actorProp)
|
||||
|
||||
// Published Property
|
||||
publishedProp := streams.NewActivityStreamsPublishedProperty()
|
||||
published, err := ap.ExtractPublished(status)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("couldn't extract Published: %w", err)
|
||||
}
|
||||
publishedProp.Set(published)
|
||||
create.SetActivityStreamsPublished(publishedProp)
|
||||
|
||||
// To Property
|
||||
toProp := streams.NewActivityStreamsToProperty()
|
||||
if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 {
|
||||
for _, toURI := range toURIs {
|
||||
toProp.AppendIRI(toURI)
|
||||
}
|
||||
create.SetActivityStreamsTo(toProp)
|
||||
}
|
||||
|
||||
// Cc Property
|
||||
ccProp := streams.NewActivityStreamsCcProperty()
|
||||
if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
|
||||
for _, ccURI := range ccURIs {
|
||||
ccProp.AppendIRI(ccURI)
|
||||
}
|
||||
create.SetActivityStreamsCc(ccProp)
|
||||
}
|
||||
|
||||
return create, nil
|
||||
wrapStatusableInActivity(create, status, iriOnly)
|
||||
return create
|
||||
}
|
||||
|
||||
// WrapStatusableInUpdate wraps a Statusable with an Update activity.
|
||||
//
|
||||
// If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create,
|
||||
// but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference,
|
||||
// and still have control over whether or not they're allowed to actually see the contents.
|
||||
func (c *Converter) WrapStatusableInUpdate(status ap.Statusable, objectIRIOnly bool) (vocab.ActivityStreamsUpdate, error) {
|
||||
func WrapPollOptionablesInCreate(options ...ap.PollOptionable) vocab.ActivityStreamsCreate {
|
||||
if len(options) == 0 {
|
||||
panic("no options")
|
||||
}
|
||||
|
||||
// Extract attributedTo IRI from any option.
|
||||
attribTos := ap.GetAttributedTo(options[0])
|
||||
if len(attribTos) != 1 {
|
||||
panic("invalid attributedTo count")
|
||||
}
|
||||
|
||||
// Extract target status IRI from any option.
|
||||
replyTos := ap.GetInReplyTo(options[0])
|
||||
if len(replyTos) != 1 {
|
||||
panic("invalid inReplyTo count")
|
||||
}
|
||||
|
||||
// Allocate create activity and copy over 'To' property.
|
||||
create := streams.NewActivityStreamsCreate()
|
||||
ap.AppendTo(create, ap.GetTo(options[0])...)
|
||||
|
||||
// Activity ID formatted as: {$statusIRI}/activity#vote/{$voterIRI}.
|
||||
id := replyTos[0].String() + "/activity#vote/" + attribTos[0].String()
|
||||
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), id)
|
||||
|
||||
// Set a current publish time for activity.
|
||||
ap.SetPublished(create, time.Now())
|
||||
|
||||
// Append each poll option as object to activity.
|
||||
for _, option := range options {
|
||||
status, _ := ap.ToStatusable(option)
|
||||
appendStatusableToActivity(create, status, false)
|
||||
}
|
||||
|
||||
return create
|
||||
}
|
||||
|
||||
func WrapStatusableInUpdate(status ap.Statusable, iriOnly bool) vocab.ActivityStreamsUpdate {
|
||||
update := streams.NewActivityStreamsUpdate()
|
||||
|
||||
// Object property
|
||||
objectProp := streams.NewActivityStreamsObjectProperty()
|
||||
if objectIRIOnly {
|
||||
objectProp.AppendIRI(status.GetJSONLDId().GetIRI())
|
||||
} else if _, ok := status.(ap.Pollable); ok {
|
||||
asQuestion := status.(vocab.ActivityStreamsQuestion)
|
||||
objectProp.AppendActivityStreamsQuestion(asQuestion)
|
||||
} else {
|
||||
asNote := status.(vocab.ActivityStreamsNote)
|
||||
objectProp.AppendActivityStreamsNote(asNote)
|
||||
}
|
||||
update.SetActivityStreamsObject(objectProp)
|
||||
|
||||
// ID property
|
||||
idProp := streams.NewJSONLDIdProperty()
|
||||
createID := status.GetJSONLDId().GetIRI().String() + "/activity"
|
||||
createIDIRI, err := url.Parse(createID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idProp.SetIRI(createIDIRI)
|
||||
update.SetJSONLDId(idProp)
|
||||
|
||||
// Actor Property
|
||||
actorProp := streams.NewActivityStreamsActorProperty()
|
||||
actorIRI, err := ap.ExtractAttributedToURI(status)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err)
|
||||
}
|
||||
actorProp.AppendIRI(actorIRI)
|
||||
update.SetActivityStreamsActor(actorProp)
|
||||
|
||||
// To Property
|
||||
toProp := streams.NewActivityStreamsToProperty()
|
||||
if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 {
|
||||
for _, toURI := range toURIs {
|
||||
toProp.AppendIRI(toURI)
|
||||
}
|
||||
update.SetActivityStreamsTo(toProp)
|
||||
}
|
||||
|
||||
// Cc Property
|
||||
ccProp := streams.NewActivityStreamsCcProperty()
|
||||
if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
|
||||
for _, ccURI := range ccURIs {
|
||||
ccProp.AppendIRI(ccURI)
|
||||
}
|
||||
update.SetActivityStreamsCc(ccProp)
|
||||
}
|
||||
|
||||
return update, nil
|
||||
wrapStatusableInActivity(update, status, iriOnly)
|
||||
return update
|
||||
}
|
||||
|
||||
// wrapStatusableInActivity adds the required ap.Statusable data to the given ap.Activityable.
|
||||
func wrapStatusableInActivity(activity ap.Activityable, status ap.Statusable, iriOnly bool) {
|
||||
idIRI := ap.GetJSONLDId(status) // activity ID formatted as {$statusIRI}/activity#{$typeName}
|
||||
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(activity), idIRI.String()+"/activity#"+activity.GetTypeName())
|
||||
appendStatusableToActivity(activity, status, iriOnly)
|
||||
ap.AppendTo(activity, ap.GetTo(status)...)
|
||||
ap.AppendCc(activity, ap.GetCc(status)...)
|
||||
ap.AppendActor(activity, ap.GetAttributedTo(status)...)
|
||||
ap.SetPublished(activity, ap.GetPublished(status))
|
||||
}
|
||||
|
||||
// appendStatusableToActivity appends a Statusable type to an Activityable, handling case of Question, Note or just IRI type.
|
||||
func appendStatusableToActivity(activity ap.Activityable, status ap.Statusable, iriOnly bool) {
|
||||
// Get existing object property or allocate new.
|
||||
objProp := activity.GetActivityStreamsObject()
|
||||
if objProp == nil {
|
||||
objProp = streams.NewActivityStreamsObjectProperty()
|
||||
activity.SetActivityStreamsObject(objProp)
|
||||
}
|
||||
|
||||
if iriOnly {
|
||||
// Only append status IRI.
|
||||
idIRI := ap.GetJSONLDId(status)
|
||||
objProp.AppendIRI(idIRI)
|
||||
} else if poll, ok := ap.ToPollable(status); ok {
|
||||
// Our Pollable implementer is an AS Question type.
|
||||
question := poll.(vocab.ActivityStreamsQuestion)
|
||||
objProp.AppendActivityStreamsQuestion(question)
|
||||
} else {
|
||||
// All of our other Statusable types are AS Note.
|
||||
note := status.(vocab.ActivityStreamsNote)
|
||||
objProp.AppendActivityStreamsNote(note)
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
type WrapTestSuite struct {
|
||||
@@ -36,7 +37,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() {
|
||||
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
create, err := suite.typeconverter.WrapStatusableInCreate(note, true)
|
||||
create := typeutils.WrapStatusableInCreate(note, true)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(create)
|
||||
|
||||
@@ -50,7 +51,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create",
|
||||
"object": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"published": "2021-10-20T12:40:37+02:00",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
@@ -64,7 +65,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
|
||||
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
create, err := suite.typeconverter.WrapStatusableInCreate(note, false)
|
||||
create := typeutils.WrapStatusableInCreate(note, false)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(create)
|
||||
|
||||
@@ -78,7 +79,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create",
|
||||
"object": {
|
||||
"attachment": [],
|
||||
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||
|
Reference in New Issue
Block a user