[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:
tobi 2023-01-25 11:12:27 +01:00 committed by GitHub
parent faeb7ded3b
commit 993aae5e48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 560 additions and 50 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -20,9 +20,11 @@
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{})
}

View File

@ -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)

View File

@ -150,6 +150,10 @@
// 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.

View File

@ -22,12 +22,15 @@
"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 &gtsmodel.Report{
URI: uri,
AccountID: account.ID,
Account: account,
TargetAccountID: targetAccount.ID,
TargetAccount: targetAccount,
Comment: content,
StatusIDs: statusIDs,
Statuses: statuses,
}, nil
}

View File

@ -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))
}

View File

@ -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

View File

@ -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
}

View 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)
}
}