From fc8d3742c99fbb70e54e1890ed24a4ef56f2a64c Mon Sep 17 00:00:00 2001 From: kim Date: Wed, 13 Nov 2024 12:02:17 +0000 Subject: [PATCH] add support for extracting Updated field from Statusable implementers --- internal/ap/interfaces.go | 8 ++- internal/ap/interfaces_test.go | 93 ++++++++++++++++++++++++++++++ internal/ap/properties.go | 28 +++++++++ internal/typeutils/astointernal.go | 15 +++-- internal/typeutils/internal.go | 24 ++------ 5 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 internal/ap/interfaces_test.go diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index a721fa997..cbe95aa17 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -25,8 +25,11 @@ import ( // IsActivityable returns whether AS vocab type name is acceptable as Activityable. func IsActivityable(typeName string) bool { - return isActivity(typeName) || - isIntransitiveActivity(typeName) + return isActivity(typeName) + // See interfaces_test.go comment + // about intransitive activities: + // + // || isIntransitiveActivity(typeName) } // ToActivityable safely tries to cast vocab.Type as Activityable, also checking for expected AS type names. @@ -196,6 +199,7 @@ type Statusable interface { WithName WithInReplyTo WithPublished + WithUpdated WithURL WithAttributedTo WithTo diff --git a/internal/ap/interfaces_test.go b/internal/ap/interfaces_test.go new file mode 100644 index 000000000..d3248cb1d --- /dev/null +++ b/internal/ap/interfaces_test.go @@ -0,0 +1,93 @@ +// 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 . + +package ap_test + +import ( + "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/ap" +) + +var ( + // NOTE: the below aren't actually tests that are run, + // we just move them into an _test.go file to declutter + // the main interfaces.go file, which is already long. + + // Compile-time checks for Activityable interface methods. + _ ap.Activityable = (vocab.ActivityStreamsAccept)(nil) + _ ap.Activityable = (vocab.ActivityStreamsTentativeAccept)(nil) + _ ap.Activityable = (vocab.ActivityStreamsAdd)(nil) + _ ap.Activityable = (vocab.ActivityStreamsCreate)(nil) + _ ap.Activityable = (vocab.ActivityStreamsDelete)(nil) + _ ap.Activityable = (vocab.ActivityStreamsFollow)(nil) + _ ap.Activityable = (vocab.ActivityStreamsIgnore)(nil) + _ ap.Activityable = (vocab.ActivityStreamsJoin)(nil) + _ ap.Activityable = (vocab.ActivityStreamsLeave)(nil) + _ ap.Activityable = (vocab.ActivityStreamsLike)(nil) + _ ap.Activityable = (vocab.ActivityStreamsOffer)(nil) + _ ap.Activityable = (vocab.ActivityStreamsInvite)(nil) + _ ap.Activityable = (vocab.ActivityStreamsReject)(nil) + _ ap.Activityable = (vocab.ActivityStreamsTentativeReject)(nil) + _ ap.Activityable = (vocab.ActivityStreamsRemove)(nil) + _ ap.Activityable = (vocab.ActivityStreamsUndo)(nil) + _ ap.Activityable = (vocab.ActivityStreamsUpdate)(nil) + _ ap.Activityable = (vocab.ActivityStreamsView)(nil) + _ ap.Activityable = (vocab.ActivityStreamsListen)(nil) + _ ap.Activityable = (vocab.ActivityStreamsRead)(nil) + _ ap.Activityable = (vocab.ActivityStreamsMove)(nil) + _ ap.Activityable = (vocab.ActivityStreamsAnnounce)(nil) + _ ap.Activityable = (vocab.ActivityStreamsBlock)(nil) + _ ap.Activityable = (vocab.ActivityStreamsFlag)(nil) + _ ap.Activityable = (vocab.ActivityStreamsDislike)(nil) + + // the below intransitive activities don't fit the interface definition because they're + // missing an attached object (as the activity itself contains the details), but we don't + // actually end up using them so it's simpler to just comment them out and not have to do + // a WithObject{} interface check on every single incoming activity: + // + // _ Activityable = (vocab.ActivityStreamsArrive)(nil) + // _ Activityable = (vocab.ActivityStreamsTravel)(nil) + // _ Activityable = (vocab.ActivityStreamsQuestion)(nil) + + // Compile-time checks for Accountable interface methods. + _ ap.Accountable = (vocab.ActivityStreamsPerson)(nil) + _ ap.Accountable = (vocab.ActivityStreamsApplication)(nil) + _ ap.Accountable = (vocab.ActivityStreamsOrganization)(nil) + _ ap.Accountable = (vocab.ActivityStreamsService)(nil) + _ ap.Accountable = (vocab.ActivityStreamsGroup)(nil) + + // Compile-time checks for Statusable interface methods. + _ ap.Statusable = (vocab.ActivityStreamsArticle)(nil) + _ ap.Statusable = (vocab.ActivityStreamsDocument)(nil) + _ ap.Statusable = (vocab.ActivityStreamsImage)(nil) + _ ap.Statusable = (vocab.ActivityStreamsVideo)(nil) + _ ap.Statusable = (vocab.ActivityStreamsNote)(nil) + _ ap.Statusable = (vocab.ActivityStreamsPage)(nil) + _ ap.Statusable = (vocab.ActivityStreamsEvent)(nil) + _ ap.Statusable = (vocab.ActivityStreamsPlace)(nil) + _ ap.Statusable = (vocab.ActivityStreamsProfile)(nil) + _ ap.Statusable = (vocab.ActivityStreamsQuestion)(nil) + + // Compile-time checks for Pollable interface methods. + _ ap.Pollable = (vocab.ActivityStreamsQuestion)(nil) + + // Compile-time checks for PollOptionable interface methods. + _ ap.PollOptionable = (vocab.ActivityStreamsNote)(nil) + + // Compile-time checks for Acceptable interface methods. + _ ap.Acceptable = (vocab.ActivityStreamsAccept)(nil) +) diff --git a/internal/ap/properties.go b/internal/ap/properties.go index 38e58ebc0..a00e781e3 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -81,6 +81,15 @@ func SetJSONLDIdStr(with WithJSONLDId, id string) error { return nil } +// TryGet tries to get value from 'a' with 'get', only if necessary interface is implemented. +func TryGet[W any, T any](get func(W) T, a any) (T, bool) { + if with, ok := a.(W); ok { + return get(with), true + } + var zero T + return zero, false +} + // GetTo returns the IRIs contained in the To property of 'with'. Panics on entries with missing ID. func GetTo(with WithTo) []*url.URL { toProp := with.GetActivityStreamsTo() @@ -408,6 +417,25 @@ func SetPublished(with WithPublished, published time.Time) { publishProp.Set(published) } +// GetUpdated returns the time contained in the Updated property of 'with'. +func GetUpdated(with WithUpdated) time.Time { + updateProp := with.GetActivityStreamsUpdated() + if updateProp == nil || !updateProp.IsXMLSchemaDateTime() { + return time.Time{} + } + return updateProp.Get() +} + +// SetUpdated sets the given time on the Updated property of 'with'. +func SetUpdated(with WithUpdated, updated time.Time) { + updateProp := with.GetActivityStreamsUpdated() + if updateProp == nil { + updateProp = streams.NewActivityStreamsUpdatedProperty() + with.SetActivityStreamsUpdated(updateProp) + } + updateProp.Set(updated) +} + // GetEndTime returns the time contained in the EndTime property of 'with'. func GetEndTime(with WithEndTime) time.Time { endTimeProp := with.GetActivityStreamsEndTime() diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index cf0c0719a..08a48fab3 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -348,18 +348,25 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // zero-time will fall back to db defaults. if pub := ap.GetPublished(statusable); !pub.IsZero() { status.CreatedAt = pub - status.UpdatedAt = pub } else { log.Warnf(ctx, "unusable published property on %s", uri) } + // status.Updated + // + // Extract updated time for status, defaults to Published. + if upd := ap.GetUpdated(statusable); !upd.IsZero() { + status.UpdatedAt = upd + } else { + status.UpdatedAt = status.CreatedAt + } + // status.AccountURI // status.AccountID // status.Account // - // Account that created the status. Assume we have - // this in the db by the time this function is called, - // error if we don't. + // Account that created the status. Assume we have this + // in the db by the time this function is called, else error. status.Account, err = c.getASAttributedToAccount(ctx, status.URI, statusable, diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index ccde6a38f..573495e0a 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -104,14 +104,8 @@ func (c *Converter) StatusToBoost( return boost, nil } -func StatusToInteractionRequest( - ctx context.Context, - status *gtsmodel.Status, -) (*gtsmodel.InteractionRequest, error) { - reqID, err := id.NewULIDFromTime(status.CreatedAt) - if err != nil { - return nil, gtserror.Newf("error generating ID: %w", err) - } +func StatusToInteractionRequest(status *gtsmodel.Status) *gtsmodel.InteractionRequest { + reqID := id.NewULIDFromTime(status.CreatedAt) var ( targetID string @@ -154,17 +148,11 @@ func StatusToInteractionRequest( InteractionType: interactionType, Reply: reply, Announce: announce, - }, nil + } } -func StatusFaveToInteractionRequest( - ctx context.Context, - fave *gtsmodel.StatusFave, -) (*gtsmodel.InteractionRequest, error) { - reqID, err := id.NewULIDFromTime(fave.CreatedAt) - if err != nil { - return nil, gtserror.Newf("error generating ID: %w", err) - } +func StatusFaveToInteractionRequest(fave *gtsmodel.StatusFave) *gtsmodel.InteractionRequest { + reqID := id.NewULIDFromTime(fave.CreatedAt) return >smodel.InteractionRequest{ ID: reqID, @@ -178,7 +166,7 @@ func StatusFaveToInteractionRequest( InteractionURI: fave.URI, InteractionType: gtsmodel.InteractionLike, Like: fave, - }, nil + } } func (c *Converter) StatusToSinBinStatus(