Add inbox follow request handling
Some checks failed
/ docker (push) Failing after 3m6s

- Not tested yet
- Undo needs to be extended to also handle undo of follow
This commit is contained in:
Melody Becker 2025-05-08 08:32:02 +02:00
parent 7e60188fb5
commit d84a693b22
Signed by: mstar
SSH key fingerprint: SHA256:vkXfS9FG2pVNVfvDrzd1VW9n8VJzqqdKQGljxxX8uK8
6 changed files with 130 additions and 17 deletions

View file

@ -33,7 +33,7 @@ func newReaction(db *gorm.DB, opts ...gen.DOOption) reaction {
_reaction.DeletedAt = field.NewField(tableName, "deleted_at") _reaction.DeletedAt = field.NewField(tableName, "deleted_at")
_reaction.NoteId = field.NewString(tableName, "note_id") _reaction.NoteId = field.NewString(tableName, "note_id")
_reaction.ReactorId = field.NewString(tableName, "reactor_id") _reaction.ReactorId = field.NewString(tableName, "reactor_id")
_reaction.EmoteId = field.NewUint(tableName, "emote_id") _reaction.EmoteId = field.NewField(tableName, "emote_id")
_reaction.Note = reactionBelongsToNote{ _reaction.Note = reactionBelongsToNote{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@ -430,7 +430,7 @@ type reaction struct {
DeletedAt field.Field DeletedAt field.Field
NoteId field.String NoteId field.String
ReactorId field.String ReactorId field.String
EmoteId field.Uint EmoteId field.Field
Note reactionBelongsToNote Note reactionBelongsToNote
Reactor reactionBelongsToReactor Reactor reactionBelongsToReactor
@ -458,7 +458,7 @@ func (r *reaction) updateTableName(table string) *reaction {
r.DeletedAt = field.NewField(table, "deleted_at") r.DeletedAt = field.NewField(table, "deleted_at")
r.NoteId = field.NewString(table, "note_id") r.NoteId = field.NewString(table, "note_id")
r.ReactorId = field.NewString(table, "reactor_id") r.ReactorId = field.NewString(table, "reactor_id")
r.EmoteId = field.NewUint(table, "emote_id") r.EmoteId = field.NewField(table, "emote_id")
r.fillFieldMap() r.fillFieldMap()

View file

@ -30,7 +30,7 @@ func newUserToUserRelation(db *gorm.DB, opts ...gen.DOOption) userToUserRelation
_userToUserRelation.ID = field.NewUint64(tableName, "id") _userToUserRelation.ID = field.NewUint64(tableName, "id")
_userToUserRelation.UserId = field.NewString(tableName, "user_id") _userToUserRelation.UserId = field.NewString(tableName, "user_id")
_userToUserRelation.TargetUserId = field.NewString(tableName, "target_user_id") _userToUserRelation.TargetUserId = field.NewString(tableName, "target_user_id")
_userToUserRelation.Relation = field.NewField(tableName, "relation") _userToUserRelation.Relation = field.NewString(tableName, "relation")
_userToUserRelation.User = userToUserRelationBelongsToUser{ _userToUserRelation.User = userToUserRelationBelongsToUser{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@ -222,7 +222,7 @@ type userToUserRelation struct {
ID field.Uint64 ID field.Uint64
UserId field.String UserId field.String
TargetUserId field.String TargetUserId field.String
Relation field.Field Relation field.String
User userToUserRelationBelongsToUser User userToUserRelationBelongsToUser
TargetUser userToUserRelationBelongsToTargetUser TargetUser userToUserRelationBelongsToTargetUser
@ -245,7 +245,7 @@ func (u *userToUserRelation) updateTableName(table string) *userToUserRelation {
u.ID = field.NewUint64(table, "id") u.ID = field.NewUint64(table, "id")
u.UserId = field.NewString(table, "user_id") u.UserId = field.NewString(table, "user_id")
u.TargetUserId = field.NewString(table, "target_user_id") u.TargetUserId = field.NewString(table, "target_user_id")
u.Relation = field.NewField(table, "relation") u.Relation = field.NewString(table, "relation")
u.fillFieldMap() u.fillFieldMap()

View file

@ -11,6 +11,7 @@ const (
ActivitystreamsActivityTargetUser ActivitystreamsActivityTargetUser
ActivitystreamsActivityTargetBoost ActivitystreamsActivityTargetBoost
ActivitystreamsActivityTargetReaction ActivitystreamsActivityTargetReaction
ActivitystreamsActivityTargetFollow
) )
func (n *ActivitystreamsActivityTargetType) Value() (driver.Value, error) { func (n *ActivitystreamsActivityTargetType) Value() (driver.Value, error) {
@ -29,4 +30,5 @@ var AllActivitystreamsActivityTargetTypes = []ActivitystreamsActivityTargetType{
ActivitystreamsActivityTargetUser, ActivitystreamsActivityTargetUser,
ActivitystreamsActivityTargetBoost, ActivitystreamsActivityTargetBoost,
ActivitystreamsActivityTargetReaction, ActivitystreamsActivityTargetReaction,
ActivitystreamsActivityTargetFollow,
} }

View file

@ -7,6 +7,7 @@ type RelationType string
const ( const (
RelationFollow RelationType = "follow" // X follows Y RelationFollow RelationType = "follow" // X follows Y
RelationFollowRequest RelationType = "follow-request" // X has requested to follow Y but is not yet following Y
RelationMute RelationType = "mute" // X has Y muted (X doesn't see Y, but Y still X) RelationMute RelationType = "mute" // X has Y muted (X doesn't see Y, but Y still X)
RelationNoBoosts RelationType = "no-boosts" // X has Ys boosts muted RelationNoBoosts RelationType = "no-boosts" // X has Ys boosts muted
RelationBlock RelationType = "block" // X has Y blocked (X doesn't see Y and Y doesn't see X) RelationBlock RelationType = "block" // X has Y blocked (X doesn't see Y and Y doesn't see X)

View file

@ -9,5 +9,5 @@ type UserToUserRelation struct {
UserId string UserId string
TargetUser User // The user Y described in [RelationType] TargetUser User // The user Y described in [RelationType]
TargetUserId string TargetUserId string
Relation RelationType `gorm:"type:relation_type"` Relation string `gorm:"type:relation_type"`
} }

View file

@ -15,6 +15,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/activitypub"
"git.mstar.dev/mstar/linstrom/shared"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models" "git.mstar.dev/mstar/linstrom/storage-new/models"
) )
@ -86,6 +87,8 @@ func userInbox(w http.ResponseWriter, r *http.Request) {
handleLike(w, r, data) handleLike(w, r, data)
case "Undo": case "Undo":
handleUndo(w, r, data) handleUndo(w, r, data)
case "Follow":
handleFollow(w, r, data)
default: default:
webutils.ProblemDetailsStatusOnly(w, 500) webutils.ProblemDetailsStatusOnly(w, 500)
} }
@ -222,27 +225,28 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
) )
return return
} }
// TODO: I *think* undo is only actively used for likes, but could also be used for // FIXME: Also handle other undo cases, such as follows
// undoing create's and thus removing notes var targetObjectId string
var likeActivityId string var targetObjectType string
// I *think* the spec says that this must be an object. Not sure though // I *think* the spec says that this must be an object. Not sure though
switch target := rawTarget.(type) { switch target := rawTarget.(type) {
case string: case string:
likeActivityId = target targetObjectId = target
case map[string]any: case map[string]any:
objType, ok := target["type"].(string) objType, ok := target["type"].(string)
if !ok || objType != "Like" { if !ok {
webutils.ProblemDetails( webutils.ProblemDetails(
w, w,
http.StatusBadRequest, http.StatusBadRequest,
"/errors/bad-request-data", "/errors/bad-request-data",
"Bad activity data", "Bad activity data",
other.IntoPointer(`Undone object is not of type like. If you receive this error for a valid undo for a different activity type,please contact the Linstrom developers with the activity type`), other.IntoPointer(`Target object does not have a type`),
nil, nil,
) )
return return
} }
likeActivityId, ok = target["id"].(string) targetObjectType = objType
targetObjectId, ok = target["id"].(string)
if !ok { if !ok {
webutils.ProblemDetails( webutils.ProblemDetails(
w, w,
@ -267,7 +271,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
} }
// TODO: For above todo, get target resource via likeActivityId and remove approprietly // TODO: For above todo, get target resource via likeActivityId and remove approprietly
// Don't just assume it was a like being undone // Don't just assume it was a like being undone
act, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(likeActivityId), dbgen.Activity.Type.Eq("like")). act, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(targetObjectId), dbgen.Activity.Type.Eq("like")).
First() First()
switch err { switch err {
case gorm.ErrRecordNotFound: case gorm.ErrRecordNotFound:
@ -276,7 +280,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
default: default:
log.Error(). log.Error().
Err(err). Err(err).
Str("activity-id", likeActivityId). Str("activity-id", targetObjectId).
Msg("Error while looking for find activity") Msg("Error while looking for find activity")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return return
@ -290,7 +294,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
default: default:
log.Error(). log.Error().
Err(err). Err(err).
Str("activity-id", likeActivityId). Str("activity-id", targetObjectId).
Msg("Error while looking for find activity") Msg("Error while looking for find activity")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return return
@ -319,3 +323,109 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
} }
} }
func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) {
log := hlog.FromRequest(r)
log.Debug().Msg("Received follow request")
_ = object["id"].(string)
actorApId, ok := object["actor"].(string)
if !ok {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer(`Request data needs to contain a field "actor" with a string value`),
nil,
)
return
}
targetUrl, ok := object["object"].(string)
if !ok {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer(`Request data needs to contain a field "object" of type string`),
nil,
)
return
}
followedMatch := objectIdRegex.FindStringSubmatch(targetUrl)
if len(followedMatch) != 2 {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer(`Object must be a link to a Linstrom AP user`),
nil,
)
return
}
followedId := followedMatch[1]
followed, err := dbgen.User.Where(dbgen.User.ID.Eq(followedId)).First()
switch err {
case gorm.ErrRecordNotFound:
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
case nil:
default:
log.Error().Err(err).Str("target-id", followedId).Msg("Failed to get account from db")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
follower, err := activitypub.ImportRemoteAccountByAPUrl(actorApId)
u2u := dbgen.UserToUserRelation
followRelations, err := u2u.Where(
u2u.UserId.Eq(follower.ID),
u2u.TargetUserId.Eq(followed.ID),
u2u.Or(
u2u.Relation.Eq(models.RelationFollow),
u2u.Relation.Eq(models.RelationFollowRequest),
),
).Count()
if err != nil {
log.Error().
Err(err).
Str("follower", follower.ID).
Str("followed", followedId).
Msg("Failed to count follow relations")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
if followRelations > 0 {
return
}
tx := dbgen.Q.Begin()
req := models.UserToUserRelation{
Relation: string(models.RelationFollowRequest),
TargetUserId: followed.ID,
UserId: follower.ID,
}
err = tx.UserToUserRelation.Create(&req)
if err != nil {
tx.Rollback()
log.Error().Err(err).Any("follow-request", req).Msg("Failed to store follow request")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
activity := models.Activity{
Id: shared.NewId(),
Type: string(models.ActivityFollow),
ObjectId: fmt.Sprint(req.ID),
ObjectType: uint32(models.ActivitystreamsActivityTargetFollow),
}
err = tx.Activity.Create(&activity)
if err != nil {
tx.Rollback()
log.Error().Err(err).Any("activity", activity).Msg("Failed to store follow activity")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
err = tx.Commit()
if err != nil {
log.Error().Err(err).Msg("Failed to commit follow activity transaction")
}
}