- Not tested yet - Undo needs to be extended to also handle undo of follow
This commit is contained in:
parent
7e60188fb5
commit
d84a693b22
6 changed files with 130 additions and 17 deletions
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue