mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-03 01:22:26 -06:00
get remote follows/accepts working
This commit is contained in:
parent
c7b4f847d8
commit
d69786ef17
16 changed files with 459 additions and 104 deletions
|
|
@ -59,6 +59,8 @@ const (
|
||||||
GetFollowersPath = BasePathWithID + "/followers"
|
GetFollowersPath = BasePathWithID + "/followers"
|
||||||
// GetRelationshipsPath is for showing an account's relationship with other accounts
|
// GetRelationshipsPath is for showing an account's relationship with other accounts
|
||||||
GetRelationshipsPath = BasePath + "/relationships"
|
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
|
// 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, GetStatusesPath, m.AccountStatusesGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
|
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
|
r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
56
internal/api/client/account/follow.go
Normal file
56
internal/api/client/account/follow.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -21,9 +21,14 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
targetAccountIDs := c.QueryArray("id[]")
|
targetAccountIDs := c.QueryArray("id[]")
|
||||||
if len(targetAccountIDs) == 0 {
|
if len(targetAccountIDs) == 0 {
|
||||||
l.Debug("no account id specified in query")
|
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
id := c.Query("id")
|
||||||
return
|
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{}
|
relationships := []model.Relationship{}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,18 @@ type UpdateSource struct {
|
||||||
// By default, max 4 fields and 255 characters per property/value.
|
// By default, max 4 fields and 255 characters per property/value.
|
||||||
type UpdateField struct {
|
type UpdateField struct {
|
||||||
// Name of the field
|
// Name of the field
|
||||||
Name *string `form:"name"`
|
Name *string `form:"name" json:"name" xml:"name"`
|
||||||
// Value of the field
|
// 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"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,17 @@ const (
|
||||||
|
|
||||||
// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
|
// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
|
||||||
type ErrNoEntries struct{}
|
type ErrNoEntries struct{}
|
||||||
|
|
||||||
func (e ErrNoEntries) Error() string {
|
func (e ErrNoEntries) Error() string {
|
||||||
return "no entries"
|
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).
|
// 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
|
// 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.
|
// 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 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)
|
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 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)
|
Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,9 @@ func (ps *postgresService) GetAll(i interface{}) error {
|
||||||
|
|
||||||
func (ps *postgresService) Put(i interface{}) error {
|
func (ps *postgresService) Put(i interface{}) error {
|
||||||
_, err := ps.conn.Model(i).Insert(i)
|
_, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -900,6 +903,10 @@ func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccoun
|
||||||
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
return ps.conn.Model(>smodel.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(>smodel.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) {
|
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||||
// make sure account 1 follows account 2
|
// make sure account 1 follows account 2
|
||||||
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ import (
|
||||||
|
|
||||||
type FederatingDB interface {
|
type FederatingDB interface {
|
||||||
pub.Database
|
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.
|
// 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 {
|
if err := f.db.GetWhere("uri", id.String(), acct); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
l.Debug("is user path! returning account")
|
||||||
return f.typeConverter.AccountToAS(acct)
|
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)
|
return fmt.Errorf("error converting note to status: %s", err)
|
||||||
}
|
}
|
||||||
if err := f.db.Put(status); err != nil {
|
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)
|
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
|
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, >smodel.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, >smodel.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
|
// Update sets an existing entry to the database based on the value's
|
||||||
// id.
|
// id.
|
||||||
//
|
//
|
||||||
|
|
@ -715,9 +628,38 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debugf("received NEWID request for asType %s", string(b))
|
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 := >smodel.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()))
|
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())
|
l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String())
|
||||||
return nil, nil
|
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, >smodel.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, >smodel.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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,11 +266,15 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
|
||||||
OnFollow: onFollow,
|
OnFollow: onFollow,
|
||||||
}
|
}
|
||||||
|
|
||||||
// override default undo behavior
|
|
||||||
other = []interface{}{
|
other = []interface{}{
|
||||||
|
// override default undo behavior
|
||||||
func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
|
func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
|
||||||
return f.FederatingDB().Undo(ctx, undo)
|
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
|
return
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ type FromClientAPI struct {
|
||||||
APObjectType string
|
APObjectType string
|
||||||
APActivityType string
|
APActivityType string
|
||||||
GTSModel interface{}
|
GTSModel interface{}
|
||||||
|
OriginAccount *Account
|
||||||
|
TargetAccount *Account
|
||||||
}
|
}
|
||||||
|
|
||||||
// // ToFederator wraps a message that travels from the processor into the federator
|
// // ToFederator wraps a message that travels from the processor into the federator
|
||||||
|
|
|
||||||
|
|
@ -326,3 +326,87 @@ func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID s
|
||||||
|
|
||||||
return r, nil
|
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 := >smodel.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 := >smodel.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: >smodel.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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
return p.federateStatus(status)
|
return p.federateStatus(status)
|
||||||
}
|
}
|
||||||
return nil
|
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:
|
case gtsmodel.ActivityStreamsUpdate:
|
||||||
// UPDATE
|
// UPDATE
|
||||||
|
|
@ -90,8 +102,26 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
||||||
return nil
|
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 {
|
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 := >smodel.Account{}
|
followAccepter := >smodel.Account{}
|
||||||
if err := p.db.GetByID(follow.TargetAccountID, followAccepter); err != nil {
|
if err := p.db.GetByID(follow.TargetAccountID, followAccepter); err != nil {
|
||||||
return fmt.Errorf("error federating follow accept: %s", err)
|
return fmt.Errorf("error federating follow accept: %s", err)
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,7 @@ import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) notifyFollow(follow *gtsmodel.Follow) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
||||||
// the account given in authed.
|
// the account given in authed.
|
||||||
AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
|
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)
|
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)
|
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 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)
|
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ type TypeConverter interface {
|
||||||
ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)
|
ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)
|
||||||
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow.
|
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow.
|
||||||
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
|
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
|
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 converts a gts model status into an activity streams note, suitable for federation
|
||||||
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
|
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 {
|
type converter struct {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ package typeutils
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"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) {
|
func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
|
||||||
return nil, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -47,6 +49,8 @@ const (
|
||||||
FeaturedPath = "featured"
|
FeaturedPath = "featured"
|
||||||
// PublicKeyPath is for serving an account's public key
|
// PublicKeyPath is for serving an account's public key
|
||||||
PublicKeyPath = "main-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
|
// 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
|
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.
|
// 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 {
|
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
|
||||||
// The below URLs are used for serving web requests
|
// The below URLs are used for serving web requests
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue