mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 08:32:25 -05:00 
			
		
		
		
	[feature] Federate reports to remote instance as Flag (if desired) (#1386)
* reports federate out, we did it lxds * fix optional line start (should be optional slash)
This commit is contained in:
		
					parent
					
						
							
								c59ec6f2a4
							
						
					
				
			
			
				commit
				
					
						3283900b0d
					
				
			
		
					 10 changed files with 207 additions and 69 deletions
				
			
		|  | @ -621,7 +621,7 @@ func ExtractActor(i WithActor) (*url.URL, error) { | ||||||
| 	return nil, errors.New("no iri found for actor prop") | 	return nil, errors.New("no iri found for actor prop") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ExtractObject extracts a URL object from a WithObject interface. | // ExtractObject extracts the first URL object from a WithObject interface. | ||||||
| func ExtractObject(i WithObject) (*url.URL, error) { | func ExtractObject(i WithObject) (*url.URL, error) { | ||||||
| 	objectProp := i.GetActivityStreamsObject() | 	objectProp := i.GetActivityStreamsObject() | ||||||
| 	if objectProp == nil { | 	if objectProp == nil { | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ package federatingdb | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"codeberg.org/gruf/go-kv" | 	"codeberg.org/gruf/go-kv" | ||||||
|  | @ -33,34 +33,27 @@ import ( | ||||||
| // | // | ||||||
| // The library makes this call only after acquiring a lock first. | // The library makes this call only after acquiring a lock first. | ||||||
| func (f *federatingDB) Get(ctx context.Context, id *url.URL) (value vocab.Type, err error) { | func (f *federatingDB) Get(ctx context.Context, id *url.URL) (value vocab.Type, err error) { | ||||||
| 	l := log.WithFields(kv.Fields{ | 	l := log.WithFields(kv.Fields{{"id", id}}...) | ||||||
| 		{"id", id}, |  | ||||||
| 	}...) |  | ||||||
| 	l.Debug("entering Get") | 	l.Debug("entering Get") | ||||||
| 
 | 
 | ||||||
| 	if uris.IsUserPath(id) { | 	switch { | ||||||
|  | 	case uris.IsUserPath(id): | ||||||
| 		acct, err := f.db.GetAccountByURI(ctx, id.String()) | 		acct, err := f.db.GetAccountByURI(ctx, id.String()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		return f.typeConverter.AccountToAS(ctx, acct) | 		return f.typeConverter.AccountToAS(ctx, acct) | ||||||
| 	} | 	case uris.IsStatusesPath(id): | ||||||
| 
 |  | ||||||
| 	if uris.IsStatusesPath(id) { |  | ||||||
| 		status, err := f.db.GetStatusByURI(ctx, id.String()) | 		status, err := f.db.GetStatusByURI(ctx, id.String()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		return f.typeConverter.StatusToAS(ctx, status) | 		return f.typeConverter.StatusToAS(ctx, status) | ||||||
| 	} | 	case uris.IsFollowersPath(id): | ||||||
| 
 |  | ||||||
| 	if uris.IsFollowersPath(id) { |  | ||||||
| 		return f.Followers(ctx, id) | 		return f.Followers(ctx, id) | ||||||
| 	} | 	case uris.IsFollowingPath(id): | ||||||
| 
 |  | ||||||
| 	if uris.IsFollowingPath(id) { |  | ||||||
| 		return f.Following(ctx, id) | 		return f.Following(ctx, id) | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("federatingDB: could not Get %s", id.String()) | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	return nil, errors.New("could not get") |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -229,61 +229,57 @@ func (f *federatingDB) ActorForInbox(ctx context.Context, inboxIRI *url.URL) (ac | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // getAccountForIRI returns the account that corresponds to or owns the given IRI. | // getAccountForIRI returns the account that corresponds to or owns the given IRI. | ||||||
| func (f *federatingDB) getAccountForIRI(ctx context.Context, iri *url.URL) (account *gtsmodel.Account, err error) { | func (f *federatingDB) getAccountForIRI(ctx context.Context, iri *url.URL) (*gtsmodel.Account, error) { | ||||||
| 	acct := >smodel.Account{} | 	var ( | ||||||
|  | 		acct = >smodel.Account{} | ||||||
|  | 		err  error | ||||||
|  | 	) | ||||||
| 
 | 
 | ||||||
| 	if uris.IsInboxPath(iri) { | 	switch { | ||||||
| 		if err := f.db.GetWhere(ctx, []db.Where{{Key: "inbox_uri", Value: iri.String()}}, acct); err != nil { | 	case uris.IsUserPath(iri): | ||||||
| 			if err == db.ErrNoEntries { | 		if acct, err = f.db.GetAccountByURI(ctx, iri.String()); err != nil { | ||||||
| 				return nil, fmt.Errorf("no actor found that corresponds to inbox %s", iri.String()) |  | ||||||
| 			} |  | ||||||
| 			return nil, fmt.Errorf("db error searching for actor with inbox %s", iri.String()) |  | ||||||
| 		} |  | ||||||
| 		return acct, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if uris.IsOutboxPath(iri) { |  | ||||||
| 		if err := f.db.GetWhere(ctx, []db.Where{{Key: "outbox_uri", Value: iri.String()}}, acct); err != nil { |  | ||||||
| 			if err == db.ErrNoEntries { |  | ||||||
| 				return nil, fmt.Errorf("no actor found that corresponds to outbox %s", iri.String()) |  | ||||||
| 			} |  | ||||||
| 			return nil, fmt.Errorf("db error searching for actor with outbox %s", iri.String()) |  | ||||||
| 		} |  | ||||||
| 		return acct, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if uris.IsUserPath(iri) { |  | ||||||
| 		if err := f.db.GetWhere(ctx, []db.Where{{Key: "uri", Value: iri.String()}}, acct); err != nil { |  | ||||||
| 			if err == db.ErrNoEntries { | 			if err == db.ErrNoEntries { | ||||||
| 				return nil, fmt.Errorf("no actor found that corresponds to uri %s", iri.String()) | 				return nil, fmt.Errorf("no actor found that corresponds to uri %s", iri.String()) | ||||||
| 			} | 			} | ||||||
| 			return nil, fmt.Errorf("db error searching for actor with uri %s", iri.String()) | 			return nil, fmt.Errorf("db error searching for actor with uri %s", iri.String()) | ||||||
| 		} | 		} | ||||||
| 		return acct, nil | 		return acct, nil | ||||||
|  | 	case uris.IsInboxPath(iri): | ||||||
|  | 		if err = f.db.GetWhere(ctx, []db.Where{{Key: "inbox_uri", Value: iri.String()}}, acct); err != nil { | ||||||
|  | 			if err == db.ErrNoEntries { | ||||||
|  | 				return nil, fmt.Errorf("no actor found that corresponds to inbox %s", iri.String()) | ||||||
| 			} | 			} | ||||||
| 
 | 			return nil, fmt.Errorf("db error searching for actor with inbox %s", iri.String()) | ||||||
| 	if uris.IsFollowersPath(iri) { | 		} | ||||||
| 		if err := f.db.GetWhere(ctx, []db.Where{{Key: "followers_uri", Value: iri.String()}}, acct); err != nil { | 		return acct, nil | ||||||
|  | 	case uris.IsOutboxPath(iri): | ||||||
|  | 		if err = f.db.GetWhere(ctx, []db.Where{{Key: "outbox_uri", Value: iri.String()}}, acct); err != nil { | ||||||
|  | 			if err == db.ErrNoEntries { | ||||||
|  | 				return nil, fmt.Errorf("no actor found that corresponds to outbox %s", iri.String()) | ||||||
|  | 			} | ||||||
|  | 			return nil, fmt.Errorf("db error searching for actor with outbox %s", iri.String()) | ||||||
|  | 		} | ||||||
|  | 		return acct, nil | ||||||
|  | 	case uris.IsFollowersPath(iri): | ||||||
|  | 		if err = f.db.GetWhere(ctx, []db.Where{{Key: "followers_uri", Value: iri.String()}}, acct); err != nil { | ||||||
| 			if err == db.ErrNoEntries { | 			if err == db.ErrNoEntries { | ||||||
| 				return nil, fmt.Errorf("no actor found that corresponds to followers_uri %s", iri.String()) | 				return nil, fmt.Errorf("no actor found that corresponds to followers_uri %s", iri.String()) | ||||||
| 			} | 			} | ||||||
| 			return nil, fmt.Errorf("db error searching for actor with followers_uri %s", iri.String()) | 			return nil, fmt.Errorf("db error searching for actor with followers_uri %s", iri.String()) | ||||||
| 		} | 		} | ||||||
| 		return acct, nil | 		return acct, nil | ||||||
| 	} | 	case uris.IsFollowingPath(iri): | ||||||
| 
 | 		if err = f.db.GetWhere(ctx, []db.Where{{Key: "following_uri", Value: iri.String()}}, acct); err != nil { | ||||||
| 	if uris.IsFollowingPath(iri) { |  | ||||||
| 		if err := f.db.GetWhere(ctx, []db.Where{{Key: "following_uri", Value: iri.String()}}, acct); err != nil { |  | ||||||
| 			if err == db.ErrNoEntries { | 			if err == db.ErrNoEntries { | ||||||
| 				return nil, fmt.Errorf("no actor found that corresponds to following_uri %s", iri.String()) | 				return nil, fmt.Errorf("no actor found that corresponds to following_uri %s", iri.String()) | ||||||
| 			} | 			} | ||||||
| 			return nil, fmt.Errorf("db error searching for actor with following_uri %s", iri.String()) | 			return nil, fmt.Errorf("db error searching for actor with following_uri %s", iri.String()) | ||||||
| 		} | 		} | ||||||
| 		return acct, nil | 		return acct, nil | ||||||
| 	} | 	default: | ||||||
| 
 |  | ||||||
| 		return nil, fmt.Errorf("getActorForIRI: iri %s not recognised", iri) | 		return nil, fmt.Errorf("getActorForIRI: iri %s not recognised", iri) | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // collectFollows takes a slice of iris and converts them into ActivityStreamsCollection of IRIs. | // collectFollows takes a slice of iris and converts them into ActivityStreamsCollection of IRIs. | ||||||
| func (f *federatingDB) collectIRIs(ctx context.Context, iris []*url.URL) (vocab.ActivityStreamsCollection, error) { | func (f *federatingDB) collectIRIs(ctx context.Context, iris []*url.URL) (vocab.ActivityStreamsCollection, error) { | ||||||
|  |  | ||||||
|  | @ -345,12 +345,21 @@ func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clien | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { | func (p *processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { | ||||||
| 	// TODO: in a separate PR, handle side effects of flag/report | 	report, ok := clientMsg.GTSModel.(*gtsmodel.Report) | ||||||
| 	// 1. email admin(s) | 	if !ok { | ||||||
| 	// 2. federate report if necessary | 		return errors.New("report was not parseable as *gtsmodel.Report") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO: in a separate PR, also email admin(s) | ||||||
|  | 
 | ||||||
|  | 	if !*report.Forwarded { | ||||||
|  | 		// nothing to do, don't federate the report | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	return p.federateReport(ctx, report) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // TODO: move all the below functions into federation.Federator | // TODO: move all the below functions into federation.Federator | ||||||
| 
 | 
 | ||||||
| func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error { | func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error { | ||||||
|  | @ -922,3 +931,51 @@ func (p *processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) | ||||||
| 	_, err = p.federator.FederatingActor().Send(ctx, outboxIRI, undo) | 	_, err = p.federator.FederatingActor().Send(ctx, outboxIRI, undo) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) federateReport(ctx context.Context, report *gtsmodel.Report) error { | ||||||
|  | 	if report.TargetAccount == nil { | ||||||
|  | 		reportTargetAccount, err := p.db.GetAccountByID(ctx, report.TargetAccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("federateReport: error getting report target account from database: %w", err) | ||||||
|  | 		} | ||||||
|  | 		report.TargetAccount = reportTargetAccount | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(report.StatusIDs) > 0 && len(report.Statuses) == 0 { | ||||||
|  | 		statuses, err := p.db.GetStatuses(ctx, report.StatusIDs) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("federateReport: error getting report statuses from database: %w", err) | ||||||
|  | 		} | ||||||
|  | 		report.Statuses = statuses | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	flag, err := p.tc.ReportToASFlag(ctx, report) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateReport: error converting report to AS flag: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// add bto so that our federating actor knows where to | ||||||
|  | 	// send the Flag; it'll still use a shared inbox if possible | ||||||
|  | 	reportTargetURI, err := url.Parse(report.TargetAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateReport: error parsing outboxURI %s: %w", report.TargetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 	bTo := streams.NewActivityStreamsBtoProperty() | ||||||
|  | 	bTo.AppendIRI(reportTargetURI) | ||||||
|  | 	flag.SetActivityStreamsBto(bTo) | ||||||
|  | 
 | ||||||
|  | 	// deliver the flag using the outbox of the | ||||||
|  | 	// instance account to anonymize the report | ||||||
|  | 	instanceAccount, err := p.db.GetInstanceAccount(ctx, "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateReport: error getting instance account: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	outboxIRI, err := url.Parse(instanceAccount.OutboxURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateReport: error parsing outboxURI %s: %w", instanceAccount.OutboxURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = p.federator.FederatingActor().Send(ctx, outboxIRI, flag) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -91,6 +91,7 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form | ||||||
| 		APActivityType: ap.ActivityFlag, | 		APActivityType: ap.ActivityFlag, | ||||||
| 		GTSModel:       report, | 		GTSModel:       report, | ||||||
| 		OriginAccount:  account, | 		OriginAccount:  account, | ||||||
|  | 		TargetAccount:  targetAccount, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	apiReport, err := p.tc.ReportToAPIReport(ctx, report) | 	apiReport, err := p.tc.ReportToAPIReport(ctx, report) | ||||||
|  |  | ||||||
|  | @ -77,40 +77,45 @@ var ( | ||||||
| 	// EmojiFinder extracts emoji strings from a piece of text. | 	// EmojiFinder extracts emoji strings from a piece of text. | ||||||
| 	EmojiFinder = regexp.MustCompile(emojiFinderString) | 	EmojiFinder = regexp.MustCompile(emojiFinderString) | ||||||
| 
 | 
 | ||||||
| 	// usernameString defines an acceptable username on this instance | 	// usernameString defines an acceptable username for a new account on this instance | ||||||
| 	usernameString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumUsernameLength) | 	usernameString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumUsernameLength) | ||||||
| 	// Username can be used to validate usernames of new signups | 	// Username can be used to validate usernames of new signups | ||||||
| 	Username = regexp.MustCompile(fmt.Sprintf(`^%s$`, usernameString)) | 	Username = regexp.MustCompile(fmt.Sprintf(`^%s$`, usernameString)) | ||||||
| 
 | 
 | ||||||
| 	userPathString = fmt.Sprintf(`^?/%s/(%s)$`, users, usernameString) | 	// usernameStringRelaxed is like usernameString, but also allows the '.' character, | ||||||
|  | 	// so it can also be used to match the instance account, which will have a username | ||||||
|  | 	// like 'example.org', and it has no upper length limit, so will work for long domains. | ||||||
|  | 	usernameStringRelaxed = `[a-z0-9_\.]{2,}` | ||||||
|  | 
 | ||||||
|  | 	userPathString = fmt.Sprintf(`^/?%s/(%s)$`, users, usernameStringRelaxed) | ||||||
| 	// UserPath parses a path that validates and captures the username part from eg /users/example_username | 	// UserPath parses a path that validates and captures the username part from eg /users/example_username | ||||||
| 	UserPath = regexp.MustCompile(userPathString) | 	UserPath = regexp.MustCompile(userPathString) | ||||||
| 
 | 
 | ||||||
| 	publicKeyPath = fmt.Sprintf(`^?/%s/(%s)/%s`, users, usernameString, publicKey) | 	publicKeyPath = fmt.Sprintf(`^/?%s/(%s)/%s`, users, usernameStringRelaxed, publicKey) | ||||||
| 	// PublicKeyPath parses a path that validates and captures the username part from eg /users/example_username/main-key | 	// PublicKeyPath parses a path that validates and captures the username part from eg /users/example_username/main-key | ||||||
| 	PublicKeyPath = regexp.MustCompile(publicKeyPath) | 	PublicKeyPath = regexp.MustCompile(publicKeyPath) | ||||||
| 
 | 
 | ||||||
| 	inboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, inbox) | 	inboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, inbox) | ||||||
| 	// InboxPath parses a path that validates and captures the username part from eg /users/example_username/inbox | 	// InboxPath parses a path that validates and captures the username part from eg /users/example_username/inbox | ||||||
| 	InboxPath = regexp.MustCompile(inboxPath) | 	InboxPath = regexp.MustCompile(inboxPath) | ||||||
| 
 | 
 | ||||||
| 	outboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, outbox) | 	outboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, outbox) | ||||||
| 	// OutboxPath parses a path that validates and captures the username part from eg /users/example_username/outbox | 	// OutboxPath parses a path that validates and captures the username part from eg /users/example_username/outbox | ||||||
| 	OutboxPath = regexp.MustCompile(outboxPath) | 	OutboxPath = regexp.MustCompile(outboxPath) | ||||||
| 
 | 
 | ||||||
| 	actorPath = fmt.Sprintf(`^?/%s/(%s)$`, actors, usernameString) | 	actorPath = fmt.Sprintf(`^/?%s/(%s)$`, actors, usernameStringRelaxed) | ||||||
| 	// ActorPath parses a path that validates and captures the username part from eg /actors/example_username | 	// ActorPath parses a path that validates and captures the username part from eg /actors/example_username | ||||||
| 	ActorPath = regexp.MustCompile(actorPath) | 	ActorPath = regexp.MustCompile(actorPath) | ||||||
| 
 | 
 | ||||||
| 	followersPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, followers) | 	followersPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, followers) | ||||||
| 	// FollowersPath parses a path that validates and captures the username part from eg /users/example_username/followers | 	// FollowersPath parses a path that validates and captures the username part from eg /users/example_username/followers | ||||||
| 	FollowersPath = regexp.MustCompile(followersPath) | 	FollowersPath = regexp.MustCompile(followersPath) | ||||||
| 
 | 
 | ||||||
| 	followingPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, following) | 	followingPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, following) | ||||||
| 	// FollowingPath parses a path that validates and captures the username part from eg /users/example_username/following | 	// FollowingPath parses a path that validates and captures the username part from eg /users/example_username/following | ||||||
| 	FollowingPath = regexp.MustCompile(followingPath) | 	FollowingPath = regexp.MustCompile(followingPath) | ||||||
| 
 | 
 | ||||||
| 	followPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, follow, ulid) | 	followPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, follow, ulid) | ||||||
| 	// FollowPath parses a path that validates and captures the username part and the ulid part | 	// FollowPath parses a path that validates and captures the username part and the ulid part | ||||||
| 	// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH | 	// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH | ||||||
| 	FollowPath = regexp.MustCompile(followPath) | 	FollowPath = regexp.MustCompile(followPath) | ||||||
|  | @ -119,22 +124,22 @@ var ( | ||||||
| 	// ULID parses and validate a ULID. | 	// ULID parses and validate a ULID. | ||||||
| 	ULID = regexp.MustCompile(fmt.Sprintf(`^%s$`, ulid)) | 	ULID = regexp.MustCompile(fmt.Sprintf(`^%s$`, ulid)) | ||||||
| 
 | 
 | ||||||
| 	likedPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, liked) | 	likedPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, liked) | ||||||
| 	// LikedPath parses a path that validates and captures the username part from eg /users/example_username/liked | 	// LikedPath parses a path that validates and captures the username part from eg /users/example_username/liked | ||||||
| 	LikedPath = regexp.MustCompile(likedPath) | 	LikedPath = regexp.MustCompile(likedPath) | ||||||
| 
 | 
 | ||||||
| 	likePath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, liked, ulid) | 	likePath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, liked, ulid) | ||||||
| 	// LikePath parses a path that validates and captures the username part and the ulid part | 	// LikePath parses a path that validates and captures the username part and the ulid part | ||||||
| 	// from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH | 	// from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH | ||||||
| 	LikePath = regexp.MustCompile(likePath) | 	LikePath = regexp.MustCompile(likePath) | ||||||
| 
 | 
 | ||||||
| 	statusesPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, statuses, ulid) | 	statusesPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, statuses, ulid) | ||||||
| 	// StatusesPath parses a path that validates and captures the username part and the ulid part | 	// StatusesPath parses a path that validates and captures the username part and the ulid part | ||||||
| 	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH | 	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH | ||||||
| 	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 | 	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 | ||||||
| 	StatusesPath = regexp.MustCompile(statusesPath) | 	StatusesPath = regexp.MustCompile(statusesPath) | ||||||
| 
 | 
 | ||||||
| 	blockPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, blocks, ulid) | 	blockPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, blocks, ulid) | ||||||
| 	// BlockPath parses a path that validates and captures the username part and the ulid part | 	// BlockPath parses a path that validates and captures the username part and the ulid part | ||||||
| 	// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH | 	// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH | ||||||
| 	BlockPath = regexp.MustCompile(blockPath) | 	BlockPath = regexp.MustCompile(blockPath) | ||||||
|  |  | ||||||
|  | @ -188,6 +188,8 @@ type TypeConverter interface { | ||||||
| 	// | 	// | ||||||
| 	// Appropriate 'next' and 'prev' fields will be created based on the highest and lowest IDs present in the statuses slice. | 	// Appropriate 'next' and 'prev' fields will be created based on the highest and lowest IDs present in the statuses slice. | ||||||
| 	StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error) | 	StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error) | ||||||
|  | 	// ReportToASFlag converts a gts model report into an activitystreams FLAG, suitable for federation. | ||||||
|  | 	ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (vocab.ActivityStreamsFlag, error) | ||||||
| 
 | 
 | ||||||
| 	/* | 	/* | ||||||
| 		INTERNAL (gts) MODEL TO INTERNAL MODEL | 		INTERNAL (gts) MODEL TO INTERNAL MODEL | ||||||
|  |  | ||||||
|  | @ -1295,3 +1295,53 @@ func (c *converter) OutboxToASCollection(ctx context.Context, outboxID string) ( | ||||||
| 
 | 
 | ||||||
| 	return collection, nil | 	return collection, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (vocab.ActivityStreamsFlag, error) { | ||||||
|  | 	flag := streams.NewActivityStreamsFlag() | ||||||
|  | 
 | ||||||
|  | 	flagIDProp := streams.NewJSONLDIdProperty() | ||||||
|  | 	idURI, err := url.Parse(r.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error parsing url %s: %w", r.URI, err) | ||||||
|  | 	} | ||||||
|  | 	flagIDProp.SetIRI(idURI) | ||||||
|  | 	flag.SetJSONLDId(flagIDProp) | ||||||
|  | 
 | ||||||
|  | 	// for privacy, set the actor as the INSTANCE ACTOR, | ||||||
|  | 	// not as the actor who created the report | ||||||
|  | 	instanceAccount, err := c.db.GetInstanceAccount(ctx, "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting instance account: %w", err) | ||||||
|  | 	} | ||||||
|  | 	instanceAccountIRI, err := url.Parse(instanceAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error parsing url %s: %w", instanceAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 	flagActorProp := streams.NewActivityStreamsActorProperty() | ||||||
|  | 	flagActorProp.AppendIRI(instanceAccountIRI) | ||||||
|  | 	flag.SetActivityStreamsActor(flagActorProp) | ||||||
|  | 
 | ||||||
|  | 	// content should be the comment submitted when the report was created | ||||||
|  | 	contentProp := streams.NewActivityStreamsContentProperty() | ||||||
|  | 	contentProp.AppendXMLSchemaString(r.Comment) | ||||||
|  | 	flag.SetActivityStreamsContent(contentProp) | ||||||
|  | 
 | ||||||
|  | 	// set at least the target account uri as the object of the flag | ||||||
|  | 	objectProp := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	targetAccountURI, err := url.Parse(r.TargetAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error parsing url %s: %w", r.TargetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 	objectProp.AppendIRI(targetAccountURI) | ||||||
|  | 	// also set status URIs if they were provided with the report | ||||||
|  | 	for _, s := range r.Statuses { | ||||||
|  | 		statusURI, err := url.Parse(s.URI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error parsing url %s: %w", s.URI, err) | ||||||
|  | 		} | ||||||
|  | 		objectProp.AppendIRI(statusURI) | ||||||
|  | 	} | ||||||
|  | 	flag.SetActivityStreamsObject(objectProp) | ||||||
|  | 
 | ||||||
|  | 	return flag, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -510,6 +510,40 @@ func (suite *InternalToASTestSuite) TestSelfBoostFollowersOnlyToAS() { | ||||||
| }`, string(bytes)) | }`, string(bytes)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *InternalToASTestSuite) TestReportToAS() { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 
 | ||||||
|  | 	testReport := suite.testReports["local_account_2_report_remote_account_1"] | ||||||
|  | 	account := suite.testAccounts["local_account_2"] | ||||||
|  | 	targetAccount := suite.testAccounts["remote_account_1"] | ||||||
|  | 	statuses := []*gtsmodel.Status{suite.testStatuses["remote_account_1_status_1"]} | ||||||
|  | 
 | ||||||
|  | 	testReport.Account = account | ||||||
|  | 	testReport.TargetAccount = targetAccount | ||||||
|  | 	testReport.Statuses = statuses | ||||||
|  | 
 | ||||||
|  | 	flag, err := suite.typeconverter.ReportToASFlag(ctx, testReport) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	ser, err := streams.Serialize(flag) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	bytes, err := json.MarshalIndent(ser, "", "  ") | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{ | ||||||
|  |   "@context": "https://www.w3.org/ns/activitystreams", | ||||||
|  |   "actor": "http://localhost:8080/users/localhost:8080", | ||||||
|  |   "content": "dark souls sucks, please yeet this nerd", | ||||||
|  |   "id": "http://localhost:8080/reports/01GP3AWY4CRDVRNZKW0TEAMB5R", | ||||||
|  |   "object": [ | ||||||
|  |     "http://fossbros-anonymous.io/users/foss_satan", | ||||||
|  |     "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M" | ||||||
|  |   ], | ||||||
|  |   "type": "Flag" | ||||||
|  | }`, string(bytes)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestInternalToASTestSuite(t *testing.T) { | func TestInternalToASTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(InternalToASTestSuite)) | 	suite.Run(t, new(InternalToASTestSuite)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1928,7 +1928,7 @@ func NewTestReports() map[string]*gtsmodel.Report { | ||||||
| 			ID:              "01GP3AWY4CRDVRNZKW0TEAMB5R", | 			ID:              "01GP3AWY4CRDVRNZKW0TEAMB5R", | ||||||
| 			CreatedAt:       TimeMustParse("2022-05-14T12:20:03+02:00"), | 			CreatedAt:       TimeMustParse("2022-05-14T12:20:03+02:00"), | ||||||
| 			UpdatedAt:       TimeMustParse("2022-05-14T12:20:03+02:00"), | 			UpdatedAt:       TimeMustParse("2022-05-14T12:20:03+02:00"), | ||||||
| 			URI:             "http://localhost:8080/01GP3AWY4CRDVRNZKW0TEAMB5R", | 			URI:             "http://localhost:8080/reports/01GP3AWY4CRDVRNZKW0TEAMB5R", | ||||||
| 			AccountID:       "01F8MH5NBDF2MV7CTC4Q5128HF", | 			AccountID:       "01F8MH5NBDF2MV7CTC4Q5128HF", | ||||||
| 			TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", | 			TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", | ||||||
| 			Comment:         "dark souls sucks, please yeet this nerd", | 			Comment:         "dark souls sucks, please yeet this nerd", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue