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.NoteId = field.NewString(tableName, "note_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{
db: db.Session(&gorm.Session{}),
@ -430,7 +430,7 @@ type reaction struct {
DeletedAt field.Field
NoteId field.String
ReactorId field.String
EmoteId field.Uint
EmoteId field.Field
Note reactionBelongsToNote
Reactor reactionBelongsToReactor
@ -458,7 +458,7 @@ func (r *reaction) updateTableName(table string) *reaction {
r.DeletedAt = field.NewField(table, "deleted_at")
r.NoteId = field.NewString(table, "note_id")
r.ReactorId = field.NewString(table, "reactor_id")
r.EmoteId = field.NewUint(table, "emote_id")
r.EmoteId = field.NewField(table, "emote_id")
r.fillFieldMap()

View file

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

View file

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

View file

@ -7,6 +7,7 @@ type RelationType string
const (
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)
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)

View file

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

View file

@ -15,6 +15,7 @@ import (
"gorm.io/gorm"
"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/models"
)
@ -86,6 +87,8 @@ func userInbox(w http.ResponseWriter, r *http.Request) {
handleLike(w, r, data)
case "Undo":
handleUndo(w, r, data)
case "Follow":
handleFollow(w, r, data)
default:
webutils.ProblemDetailsStatusOnly(w, 500)
}
@ -222,27 +225,28 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
)
return
}
// TODO: I *think* undo is only actively used for likes, but could also be used for
// undoing create's and thus removing notes
var likeActivityId string
// FIXME: Also handle other undo cases, such as follows
var targetObjectId string
var targetObjectType string
// I *think* the spec says that this must be an object. Not sure though
switch target := rawTarget.(type) {
case string:
likeActivityId = target
targetObjectId = target
case map[string]any:
objType, ok := target["type"].(string)
if !ok || objType != "Like" {
if !ok {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-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,
)
return
}
likeActivityId, ok = target["id"].(string)
targetObjectType = objType
targetObjectId, ok = target["id"].(string)
if !ok {
webutils.ProblemDetails(
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
// 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()
switch err {
case gorm.ErrRecordNotFound:
@ -276,7 +280,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
default:
log.Error().
Err(err).
Str("activity-id", likeActivityId).
Str("activity-id", targetObjectId).
Msg("Error while looking for find activity")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
@ -290,7 +294,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
default:
log.Error().
Err(err).
Str("activity-id", likeActivityId).
Str("activity-id", targetObjectId).
Msg("Error while looking for find activity")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
@ -319,3 +323,109 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) {
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")
}
}