From 993aae5e48a5a3b47a7c7bb3cb66e2d8abda17b2 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 25 Jan 2023 11:12:27 +0100
Subject: [PATCH] [feature] Accept incoming federated Flag activity (#1382)
* start working on handling incoming Flag activity
* interim commit
* federate Flag in successfully
---
internal/ap/extract.go | 17 ++
internal/ap/interfaces.go | 10 +
internal/federation/federatingdb/create.go | 38 +++
.../federation/federatingdb/create_test.go | 46 +++
internal/processing/fromfederator.go | 10 +
internal/regexes/regexes.go | 4 +
internal/typeutils/astointernal.go | 125 ++++++++
internal/typeutils/astointernal_test.go | 268 +++++++++++++++---
internal/typeutils/converter.go | 2 +
internal/typeutils/util.go | 43 ++-
internal/typeutils/util_test.go | 47 +++
11 files changed, 560 insertions(+), 50 deletions(-)
create mode 100644 internal/typeutils/util_test.go
diff --git a/internal/ap/extract.go b/internal/ap/extract.go
index c09f07c4..20420e02 100644
--- a/internal/ap/extract.go
+++ b/internal/ap/extract.go
@@ -635,6 +635,23 @@ func ExtractObject(i WithObject) (*url.URL, error) {
return nil, errors.New("no iri found for object prop")
}
+// ExtractObjects extracts a slice of URL objects from a WithObject interface.
+func ExtractObjects(i WithObject) ([]*url.URL, error) {
+ objectProp := i.GetActivityStreamsObject()
+ if objectProp == nil {
+ return nil, errors.New("object property was nil")
+ }
+
+ urls := make([]*url.URL, 0, objectProp.Len())
+ for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
+ if iter.IsIRI() && iter.GetIRI() != nil {
+ urls = append(urls, iter.GetIRI())
+ }
+ }
+
+ return urls, nil
+}
+
// ExtractVisibility extracts the gtsmodel.Visibility of a given addressable with a To and CC property.
//
// ActorFollowersURI is needed to check whether the visibility is FollowersOnly or not. The passed-in value
diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go
index 91960eed..a538e4c2 100644
--- a/internal/ap/interfaces.go
+++ b/internal/ap/interfaces.go
@@ -157,6 +157,16 @@ type CollectionPageable interface {
WithItems
}
+// Flaggable represents the minimum interface for an activitystreams 'Flag' activity.
+type Flaggable interface {
+ WithJSONLDId
+ WithTypeName
+
+ WithActor
+ WithContent
+ WithObject
+}
+
// WithJSONLDId represents an activity with JSONLDIdProperty
type WithJSONLDId interface {
GetJSONLDId() vocab.JSONLDIdProperty
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index 05321bd6..fbe3d3ad 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -78,6 +78,9 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
case ap.ActivityLike:
// LIKE SOMETHING
return f.activityLike(ctx, asType, receivingAccount, requestingAccount)
+ case ap.ActivityFlag:
+ // FLAG / REPORT SOMETHING
+ return f.activityFlag(ctx, asType, receivingAccount, requestingAccount)
}
return nil
}
@@ -314,3 +317,38 @@ func (f *federatingDB) activityLike(ctx context.Context, asType vocab.Type, rece
return nil
}
+
+/*
+ FLAG HANDLERS
+*/
+
+func (f *federatingDB) activityFlag(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
+ flag, ok := asType.(vocab.ActivityStreamsFlag)
+ if !ok {
+ return errors.New("activityFlag: could not convert type to flag")
+ }
+
+ report, err := f.typeConverter.ASFlagToReport(ctx, flag)
+ if err != nil {
+ return fmt.Errorf("activityFlag: could not convert Flag to report: %w", err)
+ }
+
+ newID, err := id.NewULID()
+ if err != nil {
+ return err
+ }
+ report.ID = newID
+
+ if err := f.db.PutReport(ctx, report); err != nil {
+ return fmt.Errorf("activityFlag: database error inserting report: %w", err)
+ }
+
+ f.fedWorker.Queue(messages.FromFederator{
+ APObjectType: ap.ActivityFlag,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: report,
+ ReceivingAccount: receivingAccount,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/create_test.go b/internal/federation/federatingdb/create_test.go
index 6bab4288..c56dcd86 100644
--- a/internal/federation/federatingdb/create_test.go
+++ b/internal/federation/federatingdb/create_test.go
@@ -20,9 +20,11 @@ package federatingdb_test
import (
"context"
+ "encoding/json"
"testing"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
@@ -83,6 +85,50 @@ func (suite *CreateTestSuite) TestCreateNoteForward() {
suite.Equal("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1", msg.APIri.String())
}
+func (suite *CreateTestSuite) TestCreateFlag1() {
+ reportedAccount := suite.testAccounts["local_account_1"]
+ reportingAccount := suite.testAccounts["remote_account_1"]
+ reportedStatus := suite.testStatuses["local_account_1_status_1"]
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "Note: ` + reportedStatus.URL + `\n-----\nban this sick filth ⛔",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": "` + reportedAccount.URI + `",
+ "type": "Flag"
+}`
+
+ m := make(map[string]interface{})
+ if err := json.Unmarshal([]byte(raw), &m); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ t, err := streams.ToType(context.Background(), m)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ ctx := createTestContext(reportedAccount, reportingAccount)
+ if err := suite.federatingDB.Create(ctx, t); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // should be a message heading to the processor now, which we can intercept here
+ msg := <-suite.fromFederator
+ suite.Equal(ap.ActivityFlag, msg.APObjectType)
+ suite.Equal(ap.ActivityCreate, msg.APActivityType)
+
+ // shiny new report should be defined on the message
+ suite.NotNil(msg.GTSModel)
+ report := msg.GTSModel.(*gtsmodel.Report)
+
+ // report should be in the database
+ if _, err := suite.db.GetReportByID(context.Background(), report.ID); err != nil {
+ suite.FailNow(err.Error())
+ }
+}
+
func TestCreateTestSuite(t *testing.T) {
suite.Run(t, &CreateTestSuite{})
}
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
index 51e464f3..c30fc269 100644
--- a/internal/processing/fromfederator.go
+++ b/internal/processing/fromfederator.go
@@ -82,6 +82,9 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa
case ap.ActivityBlock:
// CREATE A BLOCK
return p.processCreateBlockFromFederator(ctx, federatorMsg)
+ case ap.ActivityFlag:
+ // CREATE A FLAG / REPORT
+ return p.processCreateFlagFromFederator(ctx, federatorMsg)
}
case ap.ActivityUpdate:
// UPDATE SOMETHING
@@ -357,6 +360,13 @@ func (p *processor) processCreateBlockFromFederator(ctx context.Context, federat
return nil
}
+func (p *processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
+ // TODO: handle side effects of flag creation:
+ // - send email to admins
+ // - notify admins
+ return nil
+}
+
// processUpdateAccountFromFederator handles Activity Update and Object Profile
func (p *processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
diff --git a/internal/regexes/regexes.go b/internal/regexes/regexes.go
index 4c9d48da..06fb92b4 100644
--- a/internal/regexes/regexes.go
+++ b/internal/regexes/regexes.go
@@ -150,6 +150,10 @@ var (
// It captures the account id, media type, media size, file name, and file extension, eg
// `01F8MH1H7YV1Z7D2C8K2730QBF`, `attachment`, `small`, `01F8MH8RMYQ6MSNY3JM2XT1CQ5`, `jpeg`.
FilePath = regexp.MustCompile(filePath)
+
+ // MisskeyReportNotes captures a list of Note URIs from report content created by Misskey.
+ // https://regex101.com/r/EnTOBV/1
+ MisskeyReportNotes = regexp.MustCompile(`(?m)(?:^Note: ((?:http|https):\/\/.*)$)`)
)
// bufpool is a memory pool of byte buffers for use in our regex utility functions.
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index cd1b0c77..bebf6298 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -22,12 +22,15 @@ import (
"context"
"errors"
"fmt"
+ "net/url"
"github.com/miekg/dns"
"github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable ap.Accountable, accountDomain string, update bool) (*gtsmodel.Account, error) {
@@ -574,3 +577,125 @@ func (c *converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno
// the rest of the fields will be taken from the target status, but it's not our job to do the dereferencing here
return status, isNew, nil
}
+
+func (c *converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) (*gtsmodel.Report, error) {
+ // Extract flag uri.
+ idProp := flaggable.GetJSONLDId()
+ if idProp == nil || !idProp.IsIRI() {
+ return nil, errors.New("ASFlagToReport: no id property set on flaggable, or was not an iri")
+ }
+ uri := idProp.GetIRI().String()
+
+ // Extract account that created the flag / report.
+ // This will usually be an instance actor.
+ actor, err := ap.ExtractActor(flaggable)
+ if err != nil {
+ return nil, fmt.Errorf("ASFlagToReport: error extracting actor: %w", err)
+ }
+ account, err := c.db.GetAccountByURI(ctx, actor.String())
+ if err != nil {
+ return nil, fmt.Errorf("ASFlagToReport: error in db fetching account with uri %s: %w", actor.String(), err)
+ }
+
+ // Get the content of the report.
+ // For Mastodon, this will just be a string, or nothing.
+ // In Misskey's case, it may also contain the URLs of
+ // one or more reported statuses, so extract these too.
+ content := ap.ExtractContent(flaggable)
+ statusURIs := []*url.URL{}
+ inlineURLs := misskeyReportInlineURLs(content)
+ statusURIs = append(statusURIs, inlineURLs...)
+
+ // Extract account and statuses targeted by the flag / report.
+ //
+ // Incoming flags from mastodon usually have a target account uri as
+ // first entry in objects, followed by URIs of one or more statuses.
+ // Misskey on the other hand will just contain the target account uri.
+ // We shouldn't assume the order of the objects will correspond to this,
+ // but we can check that he objects slice contains just one account, and
+ // maybe some statuses.
+ //
+ // Throw away anything that's not relevant to us.
+ objects, err := ap.ExtractObjects(flaggable)
+ if err != nil {
+ return nil, fmt.Errorf("ASFlagToReport: error extracting objects: %w", err)
+ }
+ if len(objects) == 0 {
+ return nil, errors.New("ASFlagToReport: flaggable objects empty, can't create report")
+ }
+
+ var targetAccountURI *url.URL
+ for _, object := range objects {
+ switch {
+ case object.Host != config.GetHost():
+ // object doesn't belong to us, just ignore it
+ continue
+ case uris.IsUserPath(object):
+ if targetAccountURI != nil {
+ return nil, errors.New("ASFlagToReport: flaggable objects contained more than one target account uri")
+ }
+ targetAccountURI = object
+ case uris.IsStatusesPath(object):
+ statusURIs = append(statusURIs, object)
+ }
+ }
+
+ // Make sure we actually have a target account now.
+ if targetAccountURI == nil {
+ return nil, errors.New("ASFlagToReport: flaggable objects contained no recognizable target account uri")
+ }
+ targetAccount, err := c.db.GetAccountByURI(ctx, targetAccountURI.String())
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ return nil, fmt.Errorf("ASFlagToReport: account with uri %s could not be found in the db", targetAccountURI.String())
+ }
+ return nil, fmt.Errorf("ASFlagToReport: db error getting account with uri %s: %w", targetAccountURI.String(), err)
+ }
+
+ // If we got some status URIs, try to get them from the db now
+ var (
+ statusIDs = make([]string, 0, len(statusURIs))
+ statuses = make([]*gtsmodel.Status, 0, len(statusURIs))
+ )
+ for _, statusURI := range statusURIs {
+ statusURIString := statusURI.String()
+
+ // try getting this status by URI first, then URL
+ status, err := c.db.GetStatusByURI(ctx, statusURIString)
+ if err != nil {
+ if !errors.Is(err, db.ErrNoEntries) {
+ return nil, fmt.Errorf("ASFlagToReport: db error getting status with uri %s: %w", statusURIString, err)
+ }
+
+ status, err = c.db.GetStatusByURL(ctx, statusURIString)
+ if err != nil {
+ if !errors.Is(err, db.ErrNoEntries) {
+ return nil, fmt.Errorf("ASFlagToReport: db error getting status with url %s: %w", statusURIString, err)
+ }
+
+ log.Warnf("ASFlagToReport: reported status %s could not be found in the db, skipping it", statusURIString)
+ continue
+ }
+ }
+
+ if status.AccountID != targetAccount.ID {
+ // status doesn't belong to this account, ignore it
+ continue
+ }
+
+ statusIDs = append(statusIDs, status.ID)
+ statuses = append(statuses, status)
+ }
+
+ // id etc should be handled the caller, so just return what we got
+ return >smodel.Report{
+ URI: uri,
+ AccountID: account.ID,
+ Account: account,
+ TargetAccountID: targetAccount.ID,
+ TargetAccount: targetAccount,
+ Comment: content,
+ StatusIDs: statusIDs,
+ Statuses: statuses,
+ }, nil
+}
diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go
index 1731a8f3..eb7ed12f 100644
--- a/internal/typeutils/astointernal_test.go
+++ b/internal/typeutils/astointernal_test.go
@@ -35,6 +35,20 @@ type ASToInternalTestSuite struct {
TypeUtilsTestSuite
}
+func (suite *ASToInternalTestSuite) jsonToType(in string) vocab.Type {
+ m := make(map[string]interface{})
+ if err := json.Unmarshal([]byte(in), &m); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ t, err := streams.ToType(context.Background(), m)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ return t
+}
+
func (suite *ASToInternalTestSuite) TestParsePerson() {
testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"]
@@ -80,15 +94,11 @@ func (suite *ASToInternalTestSuite) TestParsePersonWithSharedInbox() {
}
func (suite *ASToInternalTestSuite) TestParsePublicStatus() {
- m := make(map[string]interface{})
- err := json.Unmarshal([]byte(publicStatusActivityJson), &m)
- suite.NoError(err)
-
- t, err := streams.ToType(context.Background(), m)
- suite.NoError(err)
-
+ t := suite.jsonToType(publicStatusActivityJson)
rep, ok := t.(ap.Statusable)
- suite.True(ok)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
status, err := suite.typeconverter.ASStatusToStatus(context.Background(), rep)
suite.NoError(err)
@@ -98,15 +108,11 @@ func (suite *ASToInternalTestSuite) TestParsePublicStatus() {
}
func (suite *ASToInternalTestSuite) TestParsePublicStatusNoURL() {
- m := make(map[string]interface{})
- err := json.Unmarshal([]byte(publicStatusActivityJsonNoURL), &m)
- suite.NoError(err)
-
- t, err := streams.ToType(context.Background(), m)
- suite.NoError(err)
-
+ t := suite.jsonToType(publicStatusActivityJsonNoURL)
rep, ok := t.(ap.Statusable)
- suite.True(ok)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
status, err := suite.typeconverter.ASStatusToStatus(context.Background(), rep)
suite.NoError(err)
@@ -119,35 +125,23 @@ func (suite *ASToInternalTestSuite) TestParsePublicStatusNoURL() {
}
func (suite *ASToInternalTestSuite) TestParseGargron() {
- m := make(map[string]interface{})
- err := json.Unmarshal([]byte(gargronAsActivityJson), &m)
- suite.NoError(err)
-
- t, err := streams.ToType(context.Background(), m)
- suite.NoError(err)
-
+ t := suite.jsonToType(gargronAsActivityJson)
rep, ok := t.(ap.Accountable)
- suite.True(ok)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", false)
suite.NoError(err)
-
suite.Equal("https://mastodon.social/inbox", *acct.SharedInboxURI)
-
- fmt.Printf("%+v", acct)
- // TODO: write assertions here, rn we're just eyeballing the output
}
func (suite *ASToInternalTestSuite) TestParseReplyWithMention() {
- m := make(map[string]interface{})
- err := json.Unmarshal([]byte(statusWithMentionsActivityJson), &m)
- suite.NoError(err)
-
- t, err := streams.ToType(context.Background(), m)
- suite.NoError(err)
-
+ t := suite.jsonToType(statusWithMentionsActivityJson)
create, ok := t.(vocab.ActivityStreamsCreate)
- suite.True(ok)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
object := create.GetActivityStreamsObject()
var status *gtsmodel.Status
@@ -183,15 +177,11 @@ func (suite *ASToInternalTestSuite) TestParseReplyWithMention() {
}
func (suite *ASToInternalTestSuite) TestParseOwncastService() {
- m := make(map[string]interface{})
- err := json.Unmarshal([]byte(owncastService), &m)
- suite.NoError(err)
-
- t, err := streams.ToType(context.Background(), m)
- suite.NoError(err)
-
+ t := suite.jsonToType(owncastService)
rep, ok := t.(ap.Accountable)
- suite.True(ok)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", false)
suite.NoError(err)
@@ -225,6 +215,196 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
fmt.Printf("\n\n\n%s\n\n\n", string(b))
}
+func (suite *ASToInternalTestSuite) TestParseFlag1() {
+ reportedAccount := suite.testAccounts["local_account_1"]
+ reportingAccount := suite.testAccounts["remote_account_1"]
+ reportedStatus := suite.testStatuses["local_account_1_status_1"]
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "Note: ` + reportedStatus.URL + `\n-----\nban this sick filth ⛔",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": "` + reportedAccount.URI + `",
+ "type": "Flag"
+}`
+
+ t := suite.jsonToType(raw)
+ asFlag, ok := t.(ap.Flaggable)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
+
+ report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(report.AccountID, reportingAccount.ID)
+ suite.Equal(report.TargetAccountID, reportedAccount.ID)
+ suite.Len(report.StatusIDs, 1)
+ suite.Len(report.Statuses, 1)
+ suite.Equal(report.Statuses[0].ID, reportedStatus.ID)
+ suite.Equal(report.Comment, "Note: "+reportedStatus.URL+"\n-----\nban this sick filth ⛔")
+}
+
+func (suite *ASToInternalTestSuite) TestParseFlag2() {
+ reportedAccount := suite.testAccounts["local_account_1"]
+ reportingAccount := suite.testAccounts["remote_account_1"]
+ // report a status that doesn't exist
+ reportedStatusURL := "http://localhost:8080/@the_mighty_zork/01GQHR6MCQSTCP85ZG4A0VR316"
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "Note: ` + reportedStatusURL + `\n-----\nban this sick filth ⛔",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": "` + reportedAccount.URI + `",
+ "type": "Flag"
+}`
+
+ t := suite.jsonToType(raw)
+ asFlag, ok := t.(ap.Flaggable)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
+
+ report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(report.AccountID, reportingAccount.ID)
+ suite.Equal(report.TargetAccountID, reportedAccount.ID)
+
+ // nonexistent status should just be skipped, it'll still be in the content though
+ suite.Len(report.StatusIDs, 0)
+ suite.Len(report.Statuses, 0)
+ suite.Equal(report.Comment, "Note: "+reportedStatusURL+"\n-----\nban this sick filth ⛔")
+}
+
+func (suite *ASToInternalTestSuite) TestParseFlag3() {
+ // flag an account that doesn't exist
+ reportedAccountURI := "http://localhost:8080/users/mr_e_man"
+ reportingAccount := suite.testAccounts["remote_account_1"]
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "ban this sick filth ⛔",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": "` + reportedAccountURI + `",
+ "type": "Flag"
+}`
+
+ t := suite.jsonToType(raw)
+ asFlag, ok := t.(ap.Flaggable)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
+
+ report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag)
+ suite.Nil(report)
+ suite.EqualError(err, "ASFlagToReport: account with uri http://localhost:8080/users/mr_e_man could not be found in the db")
+}
+
+func (suite *ASToInternalTestSuite) TestParseFlag4() {
+ // flag an account from another instance
+ reportingAccount := suite.testAccounts["remote_account_1"]
+ reportedAccountURI := suite.testAccounts["remote_account_2"].URI
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "ban this sick filth ⛔",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": "` + reportedAccountURI + `",
+ "type": "Flag"
+}`
+
+ t := suite.jsonToType(raw)
+ asFlag, ok := t.(ap.Flaggable)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
+
+ report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag)
+ suite.Nil(report)
+ suite.EqualError(err, "ASFlagToReport: flaggable objects contained no recognizable target account uri")
+}
+
+func (suite *ASToInternalTestSuite) TestParseFlag5() {
+ reportedAccount := suite.testAccounts["local_account_1"]
+ reportingAccount := suite.testAccounts["remote_account_1"]
+ reportedStatus := suite.testStatuses["local_account_1_status_1"]
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "misinformation",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": [
+ "` + reportedAccount.URI + `",
+ "` + reportedStatus.URI + `"
+ ],
+ "type": "Flag"
+ }`
+
+ t := suite.jsonToType(raw)
+ asFlag, ok := t.(ap.Flaggable)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
+
+ report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(report.AccountID, reportingAccount.ID)
+ suite.Equal(report.TargetAccountID, reportedAccount.ID)
+ suite.Len(report.StatusIDs, 1)
+ suite.Len(report.Statuses, 1)
+ suite.Equal(report.Statuses[0].ID, reportedStatus.ID)
+ suite.Equal(report.Comment, "misinformation")
+}
+
+func (suite *ASToInternalTestSuite) TestParseFlag6() {
+ reportedAccount := suite.testAccounts["local_account_1"]
+ reportingAccount := suite.testAccounts["remote_account_1"]
+ // flag a status that belongs to another account
+ reportedStatus := suite.testStatuses["local_account_2_status_1"]
+
+ raw := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "` + reportingAccount.URI + `",
+ "content": "misinformation",
+ "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d",
+ "object": [
+ "` + reportedAccount.URI + `",
+ "` + reportedStatus.URI + `"
+ ],
+ "type": "Flag"
+ }`
+
+ t := suite.jsonToType(raw)
+ asFlag, ok := t.(ap.Flaggable)
+ if !ok {
+ suite.FailNow("type not coercible")
+ }
+
+ report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(report.AccountID, reportingAccount.ID)
+ suite.Equal(report.TargetAccountID, reportedAccount.ID)
+ suite.Len(report.StatusIDs, 0)
+ suite.Len(report.Statuses, 0)
+ suite.Equal(report.Comment, "misinformation")
+}
+
func TestASToInternalTestSuite(t *testing.T) {
suite.Run(t, new(ASToInternalTestSuite))
}
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index c7fd3147..33ed617e 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -141,6 +141,8 @@ type TypeConverter interface {
//
// NOTE -- this is different from one status being boosted multiple times! In this case, new boosts should indeed be created.
ASAnnounceToStatus(ctx context.Context, announceable ap.Announceable) (status *gtsmodel.Status, new bool, err error)
+ // ASFlagToReport converts a remote activitystreams 'flag' representation into a gts model report.
+ ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) (report *gtsmodel.Report, err error)
/*
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go
index 1d1903af..40001e91 100644
--- a/internal/typeutils/util.go
+++ b/internal/typeutils/util.go
@@ -1,12 +1,39 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ 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 typeutils
import (
"context"
"fmt"
+ "net/url"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/regexes"
)
+type statusInteractions struct {
+ Faved bool
+ Muted bool
+ Bookmarked bool
+ Reblogged bool
+}
+
func (c *converter) interactionsWithStatusForAccount(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*statusInteractions, error) {
si := &statusInteractions{}
@@ -38,10 +65,14 @@ func (c *converter) interactionsWithStatusForAccount(ctx context.Context, s *gts
return si, nil
}
-// StatusInteractions denotes interactions with a status on behalf of an account.
-type statusInteractions struct {
- Faved bool
- Muted bool
- Bookmarked bool
- Reblogged bool
+func misskeyReportInlineURLs(content string) []*url.URL {
+ m := regexes.MisskeyReportNotes.FindAllStringSubmatch(content, -1)
+ urls := make([]*url.URL, 0, len(m))
+ for _, sm := range m {
+ url, err := url.Parse(sm[1])
+ if err == nil && url != nil {
+ urls = append(urls, url)
+ }
+ }
+ return urls
}
diff --git a/internal/typeutils/util_test.go b/internal/typeutils/util_test.go
new file mode 100644
index 00000000..79f93bdd
--- /dev/null
+++ b/internal/typeutils/util_test.go
@@ -0,0 +1,47 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ 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 typeutils
+
+import (
+ "testing"
+)
+
+func TestMisskeyReportContentURLs1(t *testing.T) {
+ content := `Note: https://bad.instance/@tobi/statuses/01GPB56GPJ37JTK9HW308HQKBQ
+Note: https://bad.instance/@tobi/statuses/01GPB56GPJ37JTK9HW308HQKBQ
+Note: https://bad.instance/@tobi/statuses/01GPB56GPJ37JTK9HW308HQKBQ
+-----
+Test report from Calckey`
+
+ urls := misskeyReportInlineURLs(content)
+ if l := len(urls); l != 3 {
+ t.Fatalf("wanted 3 urls, got %d", l)
+ }
+}
+
+func TestMisskeyReportContentURLs2(t *testing.T) {
+ content := `This is a report
+with just a normal url in it: https://example.org, and is not
+misskey-formatted`
+
+ urls := misskeyReportInlineURLs(content)
+ if l := len(urls); l != 0 {
+ t.Fatalf("wanted 0 urls, got %d", l)
+ }
+}