mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 09:15:50 +03:00
[feature] Accept incoming federated Flag activity (#1382)
* start working on handling incoming Flag activity * interim commit * federate Flag in successfully
This commit is contained in:
parent
faeb7ded3b
commit
993aae5e48
11 changed files with 560 additions and 50 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
|
47
internal/typeutils/util_test.go
Normal file
47
internal/typeutils/util_test.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue