Follow accept works and messags are pushed as expected
Some checks failed
/ docker (push) Failing after 2m50s
Some checks failed
/ docker (push) Failing after 2m50s
This commit is contained in:
parent
9a3a330b1d
commit
ff6a730e58
10 changed files with 482 additions and 19 deletions
|
@ -50,6 +50,7 @@ func main() {
|
||||||
g.ApplyInterface(func(models.IUser) {}, models.User{})
|
g.ApplyInterface(func(models.IUser) {}, models.User{})
|
||||||
g.ApplyInterface(func(models.IAccessToken) {}, models.AccessToken{})
|
g.ApplyInterface(func(models.IAccessToken) {}, models.AccessToken{})
|
||||||
g.ApplyInterface(func(models.INote) {}, models.Note{})
|
g.ApplyInterface(func(models.INote) {}, models.Note{})
|
||||||
|
g.ApplyInterface(func(models.IUserToUserRelation) {}, models.UserToUserRelation{})
|
||||||
|
|
||||||
log.Info().Msg("Extra features applied, starting generation")
|
log.Info().Msg("Extra features applied, starting generation")
|
||||||
g.Execute()
|
g.Execute()
|
||||||
|
|
|
@ -7,6 +7,7 @@ package dbgen
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
@ -587,6 +588,30 @@ type IUserToUserRelationDo interface {
|
||||||
Returning(value interface{}, columns ...string) IUserToUserRelationDo
|
Returning(value interface{}, columns ...string) IUserToUserRelationDo
|
||||||
UnderlyingDB() *gorm.DB
|
UnderlyingDB() *gorm.DB
|
||||||
schema.Tabler
|
schema.Tabler
|
||||||
|
|
||||||
|
GetFollowersForId(id string) (result []string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT u.inbox_link
|
||||||
|
// FROM user_to_user_relations r
|
||||||
|
// LEFT JOIN user_remote_links u
|
||||||
|
// ON r.user_id = u.user_id
|
||||||
|
// WHERE
|
||||||
|
//
|
||||||
|
// r.target_user_id = @id AND
|
||||||
|
// r.relation = 'follow'
|
||||||
|
func (u userToUserRelationDo) GetFollowersForId(id string) (result []string, err error) {
|
||||||
|
var params []interface{}
|
||||||
|
|
||||||
|
var generateSQL strings.Builder
|
||||||
|
params = append(params, id)
|
||||||
|
generateSQL.WriteString("SELECT u.inbox_link FROM user_to_user_relations r LEFT JOIN user_remote_links u ON r.user_id = u.user_id WHERE r.target_user_id = ? AND r.relation = 'follow' ")
|
||||||
|
|
||||||
|
var executeSQL *gorm.DB
|
||||||
|
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Find(&result) // ignore_security_alert
|
||||||
|
err = executeSQL.Error
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u userToUserRelationDo) Debug() IUserToUserRelationDo {
|
func (u userToUserRelationDo) Debug() IUserToUserRelationDo {
|
||||||
|
|
|
@ -11,3 +11,14 @@ type UserToUserRelation struct {
|
||||||
TargetUserId string
|
TargetUserId string
|
||||||
Relation string // `gorm:"type:relation_type"`
|
Relation string // `gorm:"type:relation_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IUserToUserRelation interface {
|
||||||
|
// SELECT u.inbox_link
|
||||||
|
// FROM user_to_user_relations r
|
||||||
|
// LEFT JOIN user_remote_links u
|
||||||
|
// ON r.user_id = u.user_id
|
||||||
|
// WHERE
|
||||||
|
// r.target_user_id = @id AND
|
||||||
|
// r.relation = 'follow'
|
||||||
|
GetFollowersForId(id string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[general]
|
[general]
|
||||||
protocol = "https"
|
protocol = "https"
|
||||||
domain = "serveo.net"
|
domain = "lhr.life"
|
||||||
subdomain = "b2f4e7c5596220d4c4957b24f6954220"
|
subdomain = "d0deb4c7b3ee95"
|
||||||
private_port = 8080
|
private_port = 8080
|
||||||
public_port = 443
|
public_port = 443
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
|
@ -11,10 +12,12 @@ import (
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"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"
|
||||||
|
webap "git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
|
||||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,7 +69,8 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
||||||
AccessLevel: models.NOTE_TARGET_PUBLIC,
|
AccessLevel: models.NOTE_TARGET_PUBLIC,
|
||||||
OriginId: 1,
|
OriginId: 1,
|
||||||
}
|
}
|
||||||
err = n.Select(
|
tx := dbgen.Q.Begin()
|
||||||
|
err = tx.Note.Select(
|
||||||
n.ID,
|
n.ID,
|
||||||
n.CreatorId,
|
n.CreatorId,
|
||||||
n.RawContent,
|
n.RawContent,
|
||||||
|
@ -78,6 +82,7 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
||||||
n.OriginId,
|
n.OriginId,
|
||||||
).Create(¬e)
|
).Create(¬e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("username", data.Username).
|
Str("username", data.Username).
|
||||||
|
@ -92,11 +97,51 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
||||||
ObjectId: note.ID,
|
ObjectId: note.ID,
|
||||||
ObjectType: uint32(models.ActivitystreamsActivityTargetNote),
|
ObjectType: uint32(models.ActivitystreamsActivityTargetNote),
|
||||||
}
|
}
|
||||||
a := dbgen.Activity
|
err = tx.Activity.Create(&activity)
|
||||||
err = a.Create(&activity)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
log.Error().Err(err).Msg("Failed to create activity for new note")
|
log.Error().Err(err).Msg("Failed to create activity for new note")
|
||||||
}
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to commit note creation")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u2u := dbgen.UserToUserRelation
|
||||||
|
links, err := u2u.GetFollowersForId(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get follower inbox links for user")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug().Strs("links", links).Send()
|
||||||
|
act, err := webap.CreateFromStorage(r.Context(), activity.Id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to fetch and format new note")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
act.Context = activitypub.BaseLdContext
|
||||||
|
outData, err := json.Marshal(act)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to marshal new note")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
log.Debug().Str("target-inbox", link).Msg("Sending message to")
|
||||||
|
go func() {
|
||||||
|
res, err := webshared.RequestSignedCavage("POST", link, outData, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Str("link", link).Msg("Failed to send create to target inbox")
|
||||||
|
}
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
log.Warn().Int("status-code", res.StatusCode).Bytes("body", body).Msg("Bad reply")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func notesFrom(w http.ResponseWriter, r *http.Request) {
|
func notesFrom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -1,5 +1,75 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
func activityAccept(w http.ResponseWriter, r *http.Request) {}
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityAcceptOut struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Actor string `json:"actor"`
|
||||||
|
Object any `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityAccept(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := hlog.FromRequest(r)
|
||||||
|
id := r.PathValue("id")
|
||||||
|
activity, err := CreateFromStorage(r.Context(), id)
|
||||||
|
switch err {
|
||||||
|
case gorm.ErrRecordNotFound:
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
case nil:
|
||||||
|
activity.Context = activitypub.BaseLdContext
|
||||||
|
data, err := json.Marshal(activity)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Any("activity", activity).Msg("Failed to marshal create activity")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Type", "application/activity+json")
|
||||||
|
fmt.Fprint(w, string(data))
|
||||||
|
default:
|
||||||
|
if storage.HandleReconnectError(err) {
|
||||||
|
log.Error().Err(err).Msg("Connection failed, restart attempt started")
|
||||||
|
} else {
|
||||||
|
log.Error().Err(err).Msg("Failed to get create activity from db")
|
||||||
|
}
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AcceptFromStorage(ctx context.Context, id string) (*ActivityAcceptOut, error) {
|
||||||
|
a := dbgen.Activity
|
||||||
|
activity, err := a.Where(a.Id.Eq(id), a.Type.Eq(string(models.ActivityAccept))).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// switch activity.ObjectType {
|
||||||
|
// case models.ActivitystreamsActivityTargetFollow:
|
||||||
|
// default:
|
||||||
|
// return nil, errors.New("unknown activity target type")
|
||||||
|
// }
|
||||||
|
follow, err := FollowFromStorage(ctx, activity.ObjectId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ActivityAcceptOut{
|
||||||
|
Id: id,
|
||||||
|
Actor: follow.Object.(string),
|
||||||
|
Type: "Accept",
|
||||||
|
Object: follow,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type activityCreateOut struct {
|
type ActivityCreateOut struct {
|
||||||
Context any `json:"@context,omitempty"`
|
Context any `json:"@context,omitempty"`
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
@ -28,7 +28,7 @@ type activityCreateOut struct {
|
||||||
func activityCreate(w http.ResponseWriter, r *http.Request) {
|
func activityCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
activity, err := createFromStorage(r.Context(), id)
|
activity, err := CreateFromStorage(r.Context(), id)
|
||||||
switch err {
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
@ -56,7 +56,7 @@ func activityCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
// Does not set the context for the activity, in case the activity is embedded
|
// Does not set the context for the activity, in case the activity is embedded
|
||||||
// in another activity or object. That's the responsibility of the handler
|
// in another activity or object. That's the responsibility of the handler
|
||||||
// getting the final result
|
// getting the final result
|
||||||
func createFromStorage(ctx context.Context, id string) (*activityCreateOut, error) {
|
func CreateFromStorage(ctx context.Context, id string) (*ActivityCreateOut, error) {
|
||||||
// log := log.Ctx(ctx)
|
// log := log.Ctx(ctx)
|
||||||
a := dbgen.Activity
|
a := dbgen.Activity
|
||||||
activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))).
|
activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))).
|
||||||
|
@ -67,11 +67,11 @@ func createFromStorage(ctx context.Context, id string) (*activityCreateOut, erro
|
||||||
}
|
}
|
||||||
switch models.ActivitystreamsActivityTargetType(activity.ObjectType) {
|
switch models.ActivitystreamsActivityTargetType(activity.ObjectType) {
|
||||||
case models.ActivitystreamsActivityTargetNote:
|
case models.ActivitystreamsActivityTargetNote:
|
||||||
note, err := noteFromStorage(ctx, activity.ObjectId)
|
note, err := NoteFromStorage(ctx, activity.ObjectId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out := activityCreateOut{
|
out := ActivityCreateOut{
|
||||||
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id,
|
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id,
|
||||||
Type: "Create",
|
Type: "Create",
|
||||||
Actor: note.AttributedTo,
|
Actor: note.AttributedTo,
|
||||||
|
|
|
@ -1 +1,95 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityFollowOut struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Actor string `json:"actor"`
|
||||||
|
Object any `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := hlog.FromRequest(r)
|
||||||
|
id := r.PathValue("id")
|
||||||
|
activity, err := FollowFromStorage(r.Context(), id)
|
||||||
|
switch err {
|
||||||
|
case gorm.ErrRecordNotFound:
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
case nil:
|
||||||
|
activity.Context = activitypub.BaseLdContext
|
||||||
|
data, err := json.Marshal(activity)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Any("activity", activity).Msg("Failed to marshal create activity")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Type", "application/activity+json")
|
||||||
|
fmt.Fprint(w, string(data))
|
||||||
|
default:
|
||||||
|
if storage.HandleReconnectError(err) {
|
||||||
|
log.Error().Err(err).Msg("Connection failed, restart attempt started")
|
||||||
|
} else {
|
||||||
|
log.Error().Err(err).Msg("Failed to get create activity from db")
|
||||||
|
}
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FollowFromStorage(ctx context.Context, id string) (*ActivityFollowOut, error) {
|
||||||
|
ac := dbgen.Activity
|
||||||
|
u2u := dbgen.UserToUserRelation
|
||||||
|
u := dbgen.User
|
||||||
|
// log := log.Ctx(ctx)
|
||||||
|
activity, err := ac.Where(ac.Id.Eq(id), ac.Type.Eq(string(models.ActivityFollow))).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
followId, err := strconv.ParseUint(activity.ObjectId, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
relation, err := u2u.Where(u2u.ID.Eq(followId)).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
follower, err := u.Where(u.ID.Eq(relation.UserId)).Preload(u.RemoteInfo).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
followed, err := u.Where(u.ID.Eq(relation.TargetUserId)).Preload(u.RemoteInfo).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := ActivityFollowOut{
|
||||||
|
Id: id,
|
||||||
|
Type: "Follow",
|
||||||
|
}
|
||||||
|
if follower.RemoteInfo != nil {
|
||||||
|
out.Actor = follower.RemoteInfo.ApLink
|
||||||
|
} else {
|
||||||
|
out.Actor = userIdToApUrl(follower.ID)
|
||||||
|
}
|
||||||
|
if followed.RemoteInfo != nil {
|
||||||
|
out.Object = followed.RemoteInfo.ApLink
|
||||||
|
} else {
|
||||||
|
out.Object = userIdToApUrl(followed.ID)
|
||||||
|
}
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
"git.mstar.dev/mstar/goutils/other"
|
"git.mstar.dev/mstar/goutils/other"
|
||||||
|
@ -15,8 +16,10 @@ 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"
|
||||||
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
var objectIdRegex = regexp.MustCompile(
|
var objectIdRegex = regexp.MustCompile(
|
||||||
|
@ -316,8 +319,54 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to commit follow activity transaction")
|
log.Error().Err(err).Msg("Failed to commit follow activity transaction")
|
||||||
}
|
}
|
||||||
|
if !followed.RestrictedFollow {
|
||||||
|
// FIXME: Handle errors
|
||||||
|
tx = dbgen.Q.Begin()
|
||||||
|
_, err = u2u.Where(u2u.ID.Eq(req.ID)).UpdateColumn(u2u.Relation, models.RelationFollow)
|
||||||
|
acceptActivity := models.Activity{
|
||||||
|
Id: shared.NewId(),
|
||||||
|
Type: string(models.ActivityAccept),
|
||||||
|
ObjectId: activity.Id,
|
||||||
|
ObjectType: uint32(models.ActivitystreamsActivityTargetActivity),
|
||||||
|
}
|
||||||
|
err = tx.Activity.Create(&acceptActivity)
|
||||||
|
tx.Commit()
|
||||||
|
go func() {
|
||||||
|
// FIXME: Clean this entire mess up
|
||||||
|
time.Sleep(time.Millisecond * 20)
|
||||||
|
webAccept, err := AcceptFromStorage(r.Context(), acceptActivity.Id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get accept from db")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
webAccept.Context = activitypub.BaseLdContext
|
||||||
|
body, err := json.Marshal(webAccept)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to marshal accept")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := webshared.RequestSignedCavage(
|
||||||
|
"POST",
|
||||||
|
follower.RemoteInfo.InboxLink,
|
||||||
|
body,
|
||||||
|
followed,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to send accept")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
body, _ = io.ReadAll(res.Body)
|
||||||
|
log.Error().
|
||||||
|
Int("status-code", res.StatusCode).
|
||||||
|
Bytes("body", body).
|
||||||
|
Msg("Post of accept failed")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WARN: Untested as can't send follow activities yet
|
||||||
func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) {
|
func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
rawTarget, ok := object["object"]
|
rawTarget, ok := object["object"]
|
||||||
|
@ -404,9 +453,124 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
}
|
}
|
||||||
relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64))
|
relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64))
|
||||||
dbrel := dbgen.UserToUserRelation
|
dbrel := dbgen.UserToUserRelation
|
||||||
_, err = dbrel.Where(dbrel.ID.Eq(relationId)).
|
tx := dbgen.Q.Begin()
|
||||||
|
_, err = tx.UserToUserRelation.Where(dbrel.ID.Eq(relationId)).
|
||||||
UpdateColumn(dbrel.Relation, models.RelationFollow)
|
UpdateColumn(dbrel.Relation, models.RelationFollow)
|
||||||
switch err {
|
switch err {
|
||||||
|
case gorm.ErrRecordNotFound:
|
||||||
|
// No need to rollback, nothing was done
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
_ = tx.Rollback()
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("target-id", internalId).
|
||||||
|
Msg("Failed to update follow status to confirmed follow")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity := models.Activity{
|
||||||
|
Id: object["id"].(string),
|
||||||
|
Type: string(models.ActivityAccept),
|
||||||
|
ObjectType: uint32(models.ActivitystreamsActivityTargetActivity),
|
||||||
|
ObjectId: followActivity.Id,
|
||||||
|
}
|
||||||
|
err = tx.Activity.Create(&activity)
|
||||||
|
if err != nil {
|
||||||
|
err = tx.Rollback()
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("target-id", internalId).
|
||||||
|
Msg("Failed to store accept activity in db")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("target-id", internalId).
|
||||||
|
Msg("Failed to commit accept transaction")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARN: Untested as can't send follow activities yet
|
||||||
|
func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) {
|
||||||
|
log := hlog.FromRequest(r)
|
||||||
|
rawTarget, ok := object["object"]
|
||||||
|
if !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Request data needs to contain a field "object"`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// FIXME: Also handle other undo cases, such as follows
|
||||||
|
var targetObjectId string
|
||||||
|
// I *think* the spec says that this must be an object. Not sure though
|
||||||
|
switch target := rawTarget.(type) {
|
||||||
|
case string:
|
||||||
|
targetObjectId = target
|
||||||
|
case map[string]any:
|
||||||
|
objType, ok := target["type"].(string)
|
||||||
|
// TODO: Ensure accept is only used for follows
|
||||||
|
if !ok || objType != "Follow" {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Target object type must be a string with value "Follow"`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetObjectId, ok = target["id"].(string)
|
||||||
|
if !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Missing id in undone object`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
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 or object`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalIdMatch := objectIdRegex.FindStringSubmatch(targetObjectId)
|
||||||
|
if len(internalIdMatch) != 2 {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Request data target object is not internal id`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalId := internalIdMatch[1]
|
||||||
|
followActivity, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(internalId)).First()
|
||||||
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -419,6 +583,59 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64))
|
||||||
|
dbrel := dbgen.UserToUserRelation
|
||||||
|
tx := dbgen.Q.Begin()
|
||||||
|
_, err = tx.UserToUserRelation.Where(dbrel.ID.Eq(relationId)).Delete()
|
||||||
|
switch err {
|
||||||
|
case gorm.ErrRecordNotFound:
|
||||||
|
// No need to rollback, nothing was done
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
_ = tx.Rollback()
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("target-id", internalId).
|
||||||
|
Msg("Failed to delete follow status")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = tx.Activity.Where(
|
||||||
|
dbgen.Activity.ObjectId.Eq(followActivity.Id),
|
||||||
|
dbgen.Activity.Type.Eq("Accept"),
|
||||||
|
).
|
||||||
|
Delete()
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
log.Error().Err(err).Msg("Failed to delete accept for later rejected follow")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity := models.Activity{
|
||||||
|
Id: object["id"].(string),
|
||||||
|
Type: string(models.ActivityAccept),
|
||||||
|
ObjectType: uint32(models.ActivitystreamsActivityTargetActivity),
|
||||||
|
ObjectId: followActivity.Id,
|
||||||
|
}
|
||||||
|
err = tx.Activity.Create(&activity)
|
||||||
|
if err != nil {
|
||||||
|
err = tx.Rollback()
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("target-id", internalId).
|
||||||
|
Msg("Failed to store accept activity in db")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("target-id", internalId).
|
||||||
|
Msg("Failed to commit accept transaction")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) {}
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
)
|
)
|
||||||
|
|
||||||
type objectNoteOut struct {
|
type ObjectNoteOut struct {
|
||||||
// Context should be set, if needed, by the endpoint handler
|
// Context should be set, if needed, by the endpoint handler
|
||||||
Context any `json:"@context,omitempty"`
|
Context any `json:"@context,omitempty"`
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ type objectNoteOut struct {
|
||||||
func objectNote(w http.ResponseWriter, r *http.Request) {
|
func objectNote(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
note, err := noteFromStorage(r.Context(), id)
|
note, err := NoteFromStorage(r.Context(), id)
|
||||||
switch err {
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
@ -73,12 +73,12 @@ func objectNote(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func noteFromStorage(ctx context.Context, id string) (*objectNoteOut, error) {
|
func NoteFromStorage(ctx context.Context, id string) (*ObjectNoteOut, error) {
|
||||||
note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)).Preload(dbgen.Note.Creator).First()
|
note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)).Preload(dbgen.Note.Creator).First()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data := &objectNoteOut{
|
data := &ObjectNoteOut{
|
||||||
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/note/" + id,
|
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/note/" + id,
|
||||||
Type: "Note",
|
Type: "Note",
|
||||||
Published: note.CreatedAt,
|
Published: note.CreatedAt,
|
||||||
|
|
Loading…
Reference in a new issue