mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:22:25 -05: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") | 	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. | // 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 | // ActorFollowersURI is needed to check whether the visibility is FollowersOnly or not. The passed-in value | ||||||
|  |  | ||||||
|  | @ -157,6 +157,16 @@ type CollectionPageable interface { | ||||||
| 	WithItems | 	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 | // WithJSONLDId represents an activity with JSONLDIdProperty | ||||||
| type WithJSONLDId interface { | type WithJSONLDId interface { | ||||||
| 	GetJSONLDId() vocab.JSONLDIdProperty | 	GetJSONLDId() vocab.JSONLDIdProperty | ||||||
|  |  | ||||||
|  | @ -78,6 +78,9 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| 	case ap.ActivityLike: | 	case ap.ActivityLike: | ||||||
| 		// LIKE SOMETHING | 		// LIKE SOMETHING | ||||||
| 		return f.activityLike(ctx, asType, receivingAccount, requestingAccount) | 		return f.activityLike(ctx, asType, receivingAccount, requestingAccount) | ||||||
|  | 	case ap.ActivityFlag: | ||||||
|  | 		// FLAG / REPORT SOMETHING | ||||||
|  | 		return f.activityFlag(ctx, asType, receivingAccount, requestingAccount) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | @ -314,3 +317,38 @@ func (f *federatingDB) activityLike(ctx context.Context, asType vocab.Type, rece | ||||||
| 
 | 
 | ||||||
| 	return nil | 	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 ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/activity/streams" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"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()) | 	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) { | func TestCreateTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, &CreateTestSuite{}) | 	suite.Run(t, &CreateTestSuite{}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -82,6 +82,9 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa | ||||||
| 		case ap.ActivityBlock: | 		case ap.ActivityBlock: | ||||||
| 			// CREATE A BLOCK | 			// CREATE A BLOCK | ||||||
| 			return p.processCreateBlockFromFederator(ctx, federatorMsg) | 			return p.processCreateBlockFromFederator(ctx, federatorMsg) | ||||||
|  | 		case ap.ActivityFlag: | ||||||
|  | 			// CREATE A FLAG / REPORT | ||||||
|  | 			return p.processCreateFlagFromFederator(ctx, federatorMsg) | ||||||
| 		} | 		} | ||||||
| 	case ap.ActivityUpdate: | 	case ap.ActivityUpdate: | ||||||
| 		// UPDATE SOMETHING | 		// UPDATE SOMETHING | ||||||
|  | @ -357,6 +360,13 @@ func (p *processor) processCreateBlockFromFederator(ctx context.Context, federat | ||||||
| 	return nil | 	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 | // processUpdateAccountFromFederator handles Activity Update and Object Profile | ||||||
| func (p *processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { | func (p *processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { | ||||||
| 	incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) | 	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 | 	// It captures the account id, media type, media size, file name, and file extension, eg | ||||||
| 	// `01F8MH1H7YV1Z7D2C8K2730QBF`, `attachment`, `small`, `01F8MH8RMYQ6MSNY3JM2XT1CQ5`, `jpeg`. | 	// `01F8MH1H7YV1Z7D2C8K2730QBF`, `attachment`, `small`, `01F8MH8RMYQ6MSNY3JM2XT1CQ5`, `jpeg`. | ||||||
| 	FilePath = regexp.MustCompile(filePath) | 	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. | // bufpool is a memory pool of byte buffers for use in our regex utility functions. | ||||||
|  |  | ||||||
|  | @ -22,12 +22,15 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/miekg/dns" | 	"github.com/miekg/dns" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"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) { | 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 | 	// 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 | 	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 | 	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() { | func (suite *ASToInternalTestSuite) TestParsePerson() { | ||||||
| 	testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"] | 	testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"] | ||||||
| 
 | 
 | ||||||
|  | @ -80,15 +94,11 @@ func (suite *ASToInternalTestSuite) TestParsePersonWithSharedInbox() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *ASToInternalTestSuite) TestParsePublicStatus() { | func (suite *ASToInternalTestSuite) TestParsePublicStatus() { | ||||||
| 	m := make(map[string]interface{}) | 	t := suite.jsonToType(publicStatusActivityJson) | ||||||
| 	err := json.Unmarshal([]byte(publicStatusActivityJson), &m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	t, err := streams.ToType(context.Background(), m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	rep, ok := t.(ap.Statusable) | 	rep, ok := t.(ap.Statusable) | ||||||
| 	suite.True(ok) | 	if !ok { | ||||||
|  | 		suite.FailNow("type not coercible") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	status, err := suite.typeconverter.ASStatusToStatus(context.Background(), rep) | 	status, err := suite.typeconverter.ASStatusToStatus(context.Background(), rep) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
|  | @ -98,15 +108,11 @@ func (suite *ASToInternalTestSuite) TestParsePublicStatus() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *ASToInternalTestSuite) TestParsePublicStatusNoURL() { | func (suite *ASToInternalTestSuite) TestParsePublicStatusNoURL() { | ||||||
| 	m := make(map[string]interface{}) | 	t := suite.jsonToType(publicStatusActivityJsonNoURL) | ||||||
| 	err := json.Unmarshal([]byte(publicStatusActivityJsonNoURL), &m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	t, err := streams.ToType(context.Background(), m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	rep, ok := t.(ap.Statusable) | 	rep, ok := t.(ap.Statusable) | ||||||
| 	suite.True(ok) | 	if !ok { | ||||||
|  | 		suite.FailNow("type not coercible") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	status, err := suite.typeconverter.ASStatusToStatus(context.Background(), rep) | 	status, err := suite.typeconverter.ASStatusToStatus(context.Background(), rep) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
|  | @ -119,35 +125,23 @@ func (suite *ASToInternalTestSuite) TestParsePublicStatusNoURL() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *ASToInternalTestSuite) TestParseGargron() { | func (suite *ASToInternalTestSuite) TestParseGargron() { | ||||||
| 	m := make(map[string]interface{}) | 	t := suite.jsonToType(gargronAsActivityJson) | ||||||
| 	err := json.Unmarshal([]byte(gargronAsActivityJson), &m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	t, err := streams.ToType(context.Background(), m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	rep, ok := t.(ap.Accountable) | 	rep, ok := t.(ap.Accountable) | ||||||
| 	suite.True(ok) | 	if !ok { | ||||||
|  | 		suite.FailNow("type not coercible") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", false) | 	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", false) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 |  | ||||||
| 	suite.Equal("https://mastodon.social/inbox", *acct.SharedInboxURI) | 	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() { | func (suite *ASToInternalTestSuite) TestParseReplyWithMention() { | ||||||
| 	m := make(map[string]interface{}) | 	t := suite.jsonToType(statusWithMentionsActivityJson) | ||||||
| 	err := json.Unmarshal([]byte(statusWithMentionsActivityJson), &m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	t, err := streams.ToType(context.Background(), m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	create, ok := t.(vocab.ActivityStreamsCreate) | 	create, ok := t.(vocab.ActivityStreamsCreate) | ||||||
| 	suite.True(ok) | 	if !ok { | ||||||
|  | 		suite.FailNow("type not coercible") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	object := create.GetActivityStreamsObject() | 	object := create.GetActivityStreamsObject() | ||||||
| 	var status *gtsmodel.Status | 	var status *gtsmodel.Status | ||||||
|  | @ -183,15 +177,11 @@ func (suite *ASToInternalTestSuite) TestParseReplyWithMention() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *ASToInternalTestSuite) TestParseOwncastService() { | func (suite *ASToInternalTestSuite) TestParseOwncastService() { | ||||||
| 	m := make(map[string]interface{}) | 	t := suite.jsonToType(owncastService) | ||||||
| 	err := json.Unmarshal([]byte(owncastService), &m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	t, err := streams.ToType(context.Background(), m) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	rep, ok := t.(ap.Accountable) | 	rep, ok := t.(ap.Accountable) | ||||||
| 	suite.True(ok) | 	if !ok { | ||||||
|  | 		suite.FailNow("type not coercible") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", false) | 	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", false) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
|  | @ -225,6 +215,196 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() { | ||||||
| 	fmt.Printf("\n\n\n%s\n\n\n", string(b)) | 	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) { | func TestASToInternalTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(ASToInternalTestSuite)) | 	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. | 	// 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) | 	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 | 		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 | package typeutils | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"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) { | func (c *converter) interactionsWithStatusForAccount(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*statusInteractions, error) { | ||||||
| 	si := &statusInteractions{} | 	si := &statusInteractions{} | ||||||
| 
 | 
 | ||||||
|  | @ -38,10 +65,14 @@ func (c *converter) interactionsWithStatusForAccount(ctx context.Context, s *gts | ||||||
| 	return si, nil | 	return si, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // StatusInteractions denotes interactions with a status on behalf of an account. | func misskeyReportInlineURLs(content string) []*url.URL { | ||||||
| type statusInteractions struct { | 	m := regexes.MisskeyReportNotes.FindAllStringSubmatch(content, -1) | ||||||
| 	Faved      bool | 	urls := make([]*url.URL, 0, len(m)) | ||||||
| 	Muted      bool | 	for _, sm := range m { | ||||||
| 	Bookmarked bool | 		url, err := url.Parse(sm[1]) | ||||||
| 	Reblogged  bool | 		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…
	
	Add table
		Add a link
		
	
		Reference in a new issue