get remote follows/accepts working

This commit is contained in:
tsmethurst 2021-05-19 22:07:27 +02:00
parent c7b4f847d8
commit d69786ef17
16 changed files with 459 additions and 104 deletions

View File

@ -59,6 +59,8 @@ const (
GetFollowersPath = BasePathWithID + "/followers"
// GetRelationshipsPath is for showing an account's relationship with other accounts
GetRelationshipsPath = BasePath + "/relationships"
// FollowPath is for POSTing new follows to, and updating existing follows
PostFollowPath = BasePathWithID + "/follow"
)
// Module implements the ClientAPIModule interface for account-related actions
@ -85,6 +87,7 @@ func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler)
return nil
}

View File

@ -0,0 +1,56 @@
/*
GoToSocial
Copyright (C) 2021 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 account
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// AccountFollowPOSTHandler is the endpoint for creating a new follow request to the target account
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
return
}
form := &model.AccountFollowRequest{}
if err := c.ShouldBind(form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
form.TargetAccountID = targetAcctID
relationship, errWithCode := m.processor.AccountFollowCreate(authed, form)
if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
c.JSON(http.StatusOK, relationship)
}

View File

@ -21,9 +21,14 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
targetAccountIDs := c.QueryArray("id[]")
if len(targetAccountIDs) == 0 {
l.Debug("no account id specified in query")
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
return
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
id := c.Query("id")
if id == "" {
l.Debug("no account id specified in query")
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
return
}
targetAccountIDs = append(targetAccountIDs, id)
}
relationships := []model.Relationship{}

View File

@ -130,7 +130,18 @@ type UpdateSource struct {
// By default, max 4 fields and 255 characters per property/value.
type UpdateField struct {
// Name of the field
Name *string `form:"name"`
Name *string `form:"name" json:"name" xml:"name"`
// Value of the field
Value *string `form:"value"`
Value *string `form:"value" json:"value" xml:"value"`
}
// AccountFollowRequest is for parsing requests at /api/v1/accounts/:id/follow
type AccountFollowRequest struct {
// ID of the account to follow request
// This should be a URL parameter not a form field
TargetAccountID string `form:"-"`
// Show reblogs for this account?
Reblogs *bool `form:"reblogs" json:"reblogs" xml:"reblogs"`
// Notify when this account posts?
Notify *bool `form:"notify" json:"notify" xml:"notify"`
}

View File

@ -32,11 +32,17 @@ const (
// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
type ErrNoEntries struct{}
func (e ErrNoEntries) Error() string {
return "no entries"
}
// ErrAlreadyExists is to be returned from the DB interface when an entry already exists for a given query or its constraints.
type ErrAlreadyExists struct{}
func (e ErrAlreadyExists) Error() string {
return "already exists"
}
// DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres).
// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated
// by whatever is returned from the database.
@ -226,6 +232,9 @@ type DB interface {
// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
// FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out.
FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out.
Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error)

View File

@ -242,6 +242,9 @@ func (ps *postgresService) GetAll(i interface{}) error {
func (ps *postgresService) Put(i interface{}) error {
_, err := ps.conn.Model(i).Insert(i)
if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return db.ErrAlreadyExists{}
}
return err
}
@ -900,6 +903,10 @@ func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccoun
return ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
}
func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
return ps.conn.Model(&gtsmodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
}
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
// make sure account 1 follows account 2
f1, err := ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()

View File

@ -40,7 +40,8 @@ import (
type FederatingDB interface {
pub.Database
Undo(c context.Context, asType vocab.Type) error
Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
}
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
@ -352,6 +353,7 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er
if err := f.db.GetWhere("uri", id.String(), acct); err != nil {
return nil, err
}
l.Debug("is user path! returning account")
return f.typeConverter.AccountToAS(acct)
}
@ -426,6 +428,9 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
return fmt.Errorf("error converting note to status: %s", err)
}
if err := f.db.Put(status); err != nil {
if _, ok := err.(db.ErrAlreadyExists); ok {
return nil
}
return fmt.Errorf("database error inserting status: %s", err)
}
@ -461,98 +466,6 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
return nil
}
func (f *federatingDB) Undo(ctx context.Context, asType vocab.Type) error {
l := f.log.WithFields(
logrus.Fields{
"func": "Undo",
"asType": asType.GetTypeName(),
},
)
m, err := streams.Serialize(asType)
if err != nil {
return err
}
b, err := json.Marshal(m)
if err != nil {
return err
}
l.Debugf("received UNDO asType %s", string(b))
targetAcctI := ctx.Value(util.APAccount)
if targetAcctI == nil {
l.Error("UNDO: target account wasn't set on context")
return nil
}
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
if !ok {
l.Error("UNDO: target account was set on context but couldn't be parsed")
return nil
}
// fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
// if fromFederatorChanI == nil {
// l.Error("from federator channel wasn't set on context")
// return nil
// }
// fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
// if !ok {
// l.Error("from federator channel was set on context but couldn't be parsed")
// return nil
// }
switch asType.GetTypeName() {
// UNDO
case gtsmodel.ActivityStreamsUndo:
undo, ok := asType.(vocab.ActivityStreamsUndo)
if !ok {
return errors.New("UNDO: couldn't parse UNDO into vocab.ActivityStreamsUndo")
}
undoObject := undo.GetActivityStreamsObject()
if undoObject == nil {
return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
}
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
switch iter.GetType().GetTypeName() {
case string(gtsmodel.ActivityStreamsFollow):
// UNDO FOLLOW
ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
if !ok {
return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow")
}
// make sure the actor owns the follow
if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) {
return errors.New("UNDO: follow actor and activity actor not the same")
}
// convert the follow to something we can understand
gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow)
if err != nil {
return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err)
}
// make sure the addressee of the original follow is the same as whatever inbox this landed in
if gtsFollow.TargetAccountID != targetAcct.ID {
return errors.New("UNDO: follow object account and inbox account were not the same")
}
// delete any existing FOLLOW
if err := f.db.DeleteWhere("uri", gtsFollow.URI, &gtsmodel.Follow{}); err != nil {
return fmt.Errorf("UNDO: db error removing follow: %s", err)
}
// delete any existing FOLLOW REQUEST
if err := f.db.DeleteWhere("uri", gtsFollow.URI, &gtsmodel.FollowRequest{}); err != nil {
return fmt.Errorf("UNDO: db error removing follow request: %s", err)
}
l.Debug("follow undone")
return nil
case string(gtsmodel.ActivityStreamsLike):
// UNDO LIKE
case string(gtsmodel.ActivityStreamsAnnounce):
// UNDO BOOST/REBLOG/ANNOUNCE
}
}
}
return nil
}
// Update sets an existing entry to the database based on the value's
// id.
//
@ -715,9 +628,38 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err
if err != nil {
return nil, err
}
l.Debugf("received NEWID request for asType %s", string(b))
switch t.GetTypeName() {
case gtsmodel.ActivityStreamsFollow:
// FOLLOW
// ID might already be set on a follow we've created, so check it here and return it if it is
follow, ok := t.(vocab.ActivityStreamsFollow)
if !ok {
return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsFollow")
}
idProp := follow.GetJSONLDId()
if idProp != nil {
if idProp.IsIRI() {
return idProp.GetIRI(), nil
}
}
// it's not set so create one based on the actor set on the follow (ie., the followER not the followEE)
actorProp := follow.GetActivityStreamsActor()
if actorProp != nil {
for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
// take the IRI of the first actor we can find (there should only be one)
if iter.IsIRI() {
actorAccount := &gtsmodel.Account{}
if err := f.db.GetWhere("uri", iter.GetIRI().String(), actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here
return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host))
}
}
}
}
}
// fallback default behavior: just return a random UUID after our protocol and host
return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString()))
}
@ -821,3 +763,139 @@ func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab.
l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String())
return nil, nil
}
/*
CUSTOM FUNCTIONALITY FOR GTS
*/
func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
l := f.log.WithFields(
logrus.Fields{
"func": "Undo",
"asType": undo.GetTypeName(),
},
)
m, err := streams.Serialize(undo)
if err != nil {
return err
}
b, err := json.Marshal(m)
if err != nil {
return err
}
l.Debugf("received UNDO asType %s", string(b))
targetAcctI := ctx.Value(util.APAccount)
if targetAcctI == nil {
l.Error("UNDO: target account wasn't set on context")
return nil
}
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
if !ok {
l.Error("UNDO: target account was set on context but couldn't be parsed")
return nil
}
undoObject := undo.GetActivityStreamsObject()
if undoObject == nil {
return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
}
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
switch iter.GetType().GetTypeName() {
case string(gtsmodel.ActivityStreamsFollow):
// UNDO FOLLOW
ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
if !ok {
return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow")
}
// make sure the actor owns the follow
if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) {
return errors.New("UNDO: follow actor and activity actor not the same")
}
// convert the follow to something we can understand
gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow)
if err != nil {
return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err)
}
// make sure the addressee of the original follow is the same as whatever inbox this landed in
if gtsFollow.TargetAccountID != targetAcct.ID {
return errors.New("UNDO: follow object account and inbox account were not the same")
}
// delete any existing FOLLOW
if err := f.db.DeleteWhere("uri", gtsFollow.URI, &gtsmodel.Follow{}); err != nil {
return fmt.Errorf("UNDO: db error removing follow: %s", err)
}
// delete any existing FOLLOW REQUEST
if err := f.db.DeleteWhere("uri", gtsFollow.URI, &gtsmodel.FollowRequest{}); err != nil {
return fmt.Errorf("UNDO: db error removing follow request: %s", err)
}
l.Debug("follow undone")
return nil
case string(gtsmodel.ActivityStreamsLike):
// UNDO LIKE
case string(gtsmodel.ActivityStreamsAnnounce):
// UNDO BOOST/REBLOG/ANNOUNCE
}
}
return nil
}
func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
l := f.log.WithFields(
logrus.Fields{
"func": "Accept",
"asType": accept.GetTypeName(),
},
)
m, err := streams.Serialize(accept)
if err != nil {
return err
}
b, err := json.Marshal(m)
if err != nil {
return err
}
l.Debugf("received ACCEPT asType %s", string(b))
inboxAcctI := ctx.Value(util.APAccount)
if inboxAcctI == nil {
l.Error("ACCEPT: inbox account wasn't set on context")
return nil
}
inboxAcct, ok := inboxAcctI.(*gtsmodel.Account)
if !ok {
l.Error("ACCEPT: inbox account was set on context but couldn't be parsed")
return nil
}
acceptObject := accept.GetActivityStreamsObject()
if acceptObject == nil {
return errors.New("ACCEPT: no object set on vocab.ActivityStreamsUndo")
}
for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
switch iter.GetType().GetTypeName() {
case string(gtsmodel.ActivityStreamsFollow):
// ACCEPT FOLLOW
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
if !ok {
return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow")
}
// convert the follow to something we can understand
gtsFollow, err := f.typeConverter.ASFollowToFollow(asFollow)
if err != nil {
return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
}
// make sure the addressee of the original follow is the same as whatever inbox this landed in
if gtsFollow.AccountID != inboxAcct.ID {
return errors.New("ACCEPT: follow object account and inbox account were not the same")
}
_, err = f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID)
return err
}
}
return nil
}

View File

@ -266,11 +266,15 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
OnFollow: onFollow,
}
// override default undo behavior
other = []interface{}{
// override default undo behavior
func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
return f.FederatingDB().Undo(ctx, undo)
},
// override default accept behavior
func(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
return f.FederatingDB().Accept(ctx, accept)
},
}
return

View File

@ -12,6 +12,8 @@ type FromClientAPI struct {
APObjectType string
APActivityType string
GTSModel interface{}
OriginAccount *Account
TargetAccount *Account
}
// // ToFederator wraps a message that travels from the processor into the federator

View File

@ -326,3 +326,87 @@ func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID s
return r, nil
}
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) {
// if there's a block between the accounts we shouldn't create the request ofc
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
if err != nil {
return nil, NewErrorInternalError(err)
}
if blocked {
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
}
// make sure the target account actually exists in our db
targetAcct := &gtsmodel.Account{}
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
}
}
// check if a follow exists already
follows, err := p.db.Follows(authed.Account, targetAcct)
if err != nil {
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
}
if follows {
// already follows so just return the relationship
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}
// check if a follow exists already
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
if err != nil {
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
}
if followRequested {
// already follow requested so just return the relationship
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}
// make the follow request
fr := &gtsmodel.FollowRequest{
AccountID: authed.Account.ID,
TargetAccountID: form.TargetAccountID,
ShowReblogs: true,
URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host),
Notify: false,
}
if form.Reblogs != nil {
fr.ShowReblogs = *form.Reblogs
}
if form.Notify != nil {
fr.Notify = *form.Notify
}
// whack it in the database
if err := p.db.Put(fr); err != nil {
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
}
// if it's a local account that's not locked we can just straight up accept the follow request
if !targetAcct.Locked && targetAcct.Domain == "" {
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
}
// return the new relationship
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsCreate,
GTSModel: &gtsmodel.Follow{
AccountID: authed.Account.ID,
TargetAccountID: form.TargetAccountID,
URI: fr.URI,
},
OriginAccount: authed.Account,
TargetAccount: targetAcct,
}
// return whatever relationship results from this
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}

View File

@ -49,6 +49,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
return p.federateStatus(status)
}
return nil
case gtsmodel.ActivityStreamsFollow:
// CREATE FOLLOW (request)
follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
if !ok {
return errors.New("follow was not parseable as *gtsmodel.Follow")
}
if err := p.notifyFollow(follow); err != nil {
return err
}
return p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
case gtsmodel.ActivityStreamsUpdate:
// UPDATE
@ -90,8 +102,26 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
return nil
}
func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
if err != nil {
return fmt.Errorf("federateFollow: error converting follow to as format: %s", err)
}
outboxIRI, err := url.Parse(originAccount.OutboxURI)
if err != nil {
return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
}
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFollow)
return err
}
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow) error {
// TODO: tidy up this whole function -- move most of the logic for the conversion to the type converter because this is just a mess! Shame on me!
followAccepter := &gtsmodel.Account{}
if err := p.db.GetByID(follow.TargetAccountID, followAccepter); err != nil {
return fmt.Errorf("error federating follow accept: %s", err)

View File

@ -23,3 +23,7 @@ import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
return nil
}
func (p *processor) notifyFollow(follow *gtsmodel.Follow) error {
return nil
}

View File

@ -71,10 +71,12 @@ type Processor interface {
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
// AccountFollowersGet
// AccountFollowersGet fetches a list of the target account's followers.
AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
// AccountRelationshipGet
// AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
// AccountFollowCreate handles a follow request to an account, either remote or local.
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode)
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)

View File

@ -99,7 +99,7 @@ type TypeConverter interface {
ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow.
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
/*
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
*/
@ -109,6 +109,9 @@ type TypeConverter interface {
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error)
}
type converter struct {

View File

@ -21,6 +21,7 @@ package typeutils
import (
"crypto/x509"
"encoding/pem"
"fmt"
"net/url"
"github.com/go-fed/activity/streams"
@ -258,3 +259,49 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso
func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
return nil, nil
}
func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
// parse out the various URIs we need for this
// origin account (who's doing the follow)
originAccountURI, err := url.Parse(originAccount.URI)
if err != nil {
return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
}
originActor := streams.NewActivityStreamsActorProperty()
originActor.AppendIRI(originAccountURI)
// target account (who's being followed)
targetAccountURI, err := url.Parse(targetAccount.URI)
if err != nil {
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
}
// uri of the folow activity itself
followURI, err := url.Parse(f.URI)
if err != nil {
return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err)
}
// start preparing the follow activity
follow := streams.NewActivityStreamsFollow()
// set the actor
follow.SetActivityStreamsActor(originActor)
// set the id
followIDProp := streams.NewJSONLDIdProperty()
followIDProp.SetIRI(followURI)
follow.SetJSONLDId(followIDProp)
// set the object
followObjectProp := streams.NewActivityStreamsObjectProperty()
followObjectProp.AppendIRI(targetAccountURI)
follow.SetActivityStreamsObject(followObjectProp)
// set the To property
followToProp := streams.NewActivityStreamsToProperty()
followToProp.AppendIRI(targetAccountURI)
follow.SetActivityStreamsTo(followToProp)
return follow, nil
}

View File

@ -22,6 +22,8 @@ import (
"fmt"
"net/url"
"strings"
"github.com/google/uuid"
)
const (
@ -47,6 +49,8 @@ const (
FeaturedPath = "featured"
// PublicKeyPath is for serving an account's public key
PublicKeyPath = "main-key"
// FollowPath used to generate the URI for an individual follow or follow request
FollowPath = "follow"
)
// APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains
@ -103,6 +107,12 @@ type UserURIs struct {
PublicKeyURI string
}
// GenerateURIForFollow returns the AP URI for a new follow -- something like:
// https://example.org/users/whatever_user/follow/41c7f33f-1060-48d9-84df-38dcb13cf0d8
func GenerateURIForFollow(username string, protocol string, host string) string {
return fmt.Sprintf("%s://%s/%s/%s/%s", protocol, host, UsersPath, FollowPath, uuid.NewString())
}
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
// The below URLs are used for serving web requests