From 03ca524c9922254fca2b3905aa8da5a952fc487e Mon Sep 17 00:00:00 2001 From: mStar Date: Tue, 6 May 2025 22:18:28 +0200 Subject: [PATCH] Cleanup, store likes - Clean up some logging - Properly store likes in db --- activitypub/import.go | 22 +- storage-new/models/ActivityToObject.go | 2 +- storage-new/models/Reaction.go | 10 +- temp.toml | 2 +- web/debug/posts.go | 2 + .../{0_activitypub.go => activitypub.go} | 4 - web/public/api/activitypub/inbox.go | 193 ++++++++++++++++++ .../api/activitypub/{0_user.go => user.go} | 13 -- .../api/activitypub/{0_util.go => util.go} | 0 web/public/middleware/authFetchCheck.go | 11 +- web/public/server.go | 5 - web/shared/clientRfc9421.go | 2 - 12 files changed, 222 insertions(+), 44 deletions(-) rename web/public/api/activitypub/{0_activitypub.go => activitypub.go} (83%) create mode 100644 web/public/api/activitypub/inbox.go rename web/public/api/activitypub/{0_user.go => user.go} (95%) rename web/public/api/activitypub/{0_util.go => util.go} (100%) diff --git a/activitypub/import.go b/activitypub/import.go index 5e5edc5..e4ab7b3 100644 --- a/activitypub/import.go +++ b/activitypub/import.go @@ -421,11 +421,6 @@ func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) { } defer response.Body.Close() body, _ := io.ReadAll(response.Body) - log.Debug(). - Int("status", response.StatusCode). - Bytes("body", body). - // Any("headers", response.Header). - Msg("Response information") if response.StatusCode != 200 { return nil, fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode) } @@ -452,9 +447,20 @@ func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) { Preload(dbgen.User.InfoFields). Preload(dbgen.User.BeingTypes). Preload(dbgen.User.Roles). - FirstOrCreate() - if err != nil { - return nil, other.Error("activitypub", "failed to find or create user in db", err) + First() + switch err { + case nil: + case gorm.ErrRecordNotFound: + user = &models.User{ + ID: shared.NewId(), + Username: data.PreferredUsername + "@" + targetUrl.Host, + ServerId: hostId, + } + if err = dbgen.User.Select(dbgen.User.ID, dbgen.User.Username, dbgen.User.ServerId).Create(user); err != nil { + return nil, other.Error("activitypub", "failed to create user", err) + } + default: + return nil, other.Error("activitypub", "failed to find user", err) } user.Verified = true user.FinishedRegistration = true diff --git a/storage-new/models/ActivityToObject.go b/storage-new/models/ActivityToObject.go index b36a26c..5ddb5b1 100644 --- a/storage-new/models/ActivityToObject.go +++ b/storage-new/models/ActivityToObject.go @@ -2,7 +2,7 @@ package models type Activity struct { Id string `gorm:"primarykey"` - Type string `gorm:"type:activitystreams_activity_type"` + Type string // `gorm:"type:activitystreams_activity_type"` ObjectId string ObjectType uint32 // Target type: ActivitystreamsActivityTargetType } diff --git a/storage-new/models/Reaction.go b/storage-new/models/Reaction.go index 64384f1..7f9bfd2 100644 --- a/storage-new/models/Reaction.go +++ b/storage-new/models/Reaction.go @@ -1,6 +1,10 @@ package models -import "gorm.io/gorm" +import ( + "database/sql" + + "gorm.io/gorm" +) // A Reaction is a user liking a note using an emote type Reaction struct { @@ -9,6 +13,6 @@ type Reaction struct { NoteId string Reactor User ReactorId string - Emote Emote - EmoteId uint + Emote *Emote // Emote is optional. If not set, use the default emote of the server + EmoteId sql.NullInt64 } diff --git a/temp.toml b/temp.toml index c7b9540..2f0fe77 100644 --- a/temp.toml +++ b/temp.toml @@ -1,7 +1,7 @@ [general] protocol = "https" domain = "serveo.net" - subdomain = "f14d150ea7c018b520cf19b0f98945c2" + subdomain = "b2f4e7c5596220d4c4957b24f6954220" private_port = 8080 public_port = 443 diff --git a/web/debug/posts.go b/web/debug/posts.go index 93c0b9d..8e4db0f 100644 --- a/web/debug/posts.go +++ b/web/debug/posts.go @@ -55,6 +55,7 @@ func postAs(w http.ResponseWriter, r *http.Request) { } n := dbgen.Note note := models.Note{ + ID: shared.NewId(), Creator: *user, CreatorId: user.ID, RawContent: data.Content, @@ -66,6 +67,7 @@ func postAs(w http.ResponseWriter, r *http.Request) { OriginId: 1, } err = n.Select( + n.ID, n.CreatorId, n.RawContent, n.Remote, diff --git a/web/public/api/activitypub/0_activitypub.go b/web/public/api/activitypub/activitypub.go similarity index 83% rename from web/public/api/activitypub/0_activitypub.go rename to web/public/api/activitypub/activitypub.go index f955fb7..abd878e 100644 --- a/web/public/api/activitypub/0_activitypub.go +++ b/web/public/api/activitypub/activitypub.go @@ -1,7 +1,6 @@ package activitypub import ( - "fmt" "net/http" ) @@ -15,8 +14,5 @@ func BuildActivitypubRouter() http.Handler { router.HandleFunc("/activity/reject/{id}", activityReject) router.HandleFunc("/activity/update/{id}", activityUpdate) router.HandleFunc("/note/{id}", objectNote) - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - _, _ = fmt.Fprint(w, "in ap") - }) return router } diff --git a/web/public/api/activitypub/inbox.go b/web/public/api/activitypub/inbox.go new file mode 100644 index 0000000..ddb5e89 --- /dev/null +++ b/web/public/api/activitypub/inbox.go @@ -0,0 +1,193 @@ +package activitypub + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + + webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/goutils/other" + "github.com/rs/zerolog/hlog" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +var objectIdRegex = regexp.MustCompile( + `https?://.+/api/activitypub/[a-z]+(?:/[a-z]+)?/([a-zA-Z0-9\-]+)`, +) + +func userInbox(w http.ResponseWriter, r *http.Request) { + log := hlog.FromRequest(r) + userId := r.PathValue("id") + body, err := io.ReadAll(r.Body) + log.Info(). + Err(err). + Str("userId", userId). + Bytes("body", body). + Any("headers", r.Header). + Msg("Inbox message") + data := map[string]any{} + err = json.Unmarshal(body, &data) + if err != nil { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer("Body to inbox needs to be json"), + nil, + ) + return + } + if _, ok := data["@context"]; !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer("Request data needs to contain context"), + nil, + ) + return + } + objectType, ok := data["type"].(string) + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Request data needs to contain a field "type" with a string value`), + nil, + ) + return + } + _, ok = data["id"].(string) + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Request data needs to contain a field "id" with a string value`), + nil, + ) + return + } + switch objectType { + case "Like": + handleLike(w, r, data) + default: + webutils.ProblemDetailsStatusOnly(w, 500) + } +} + +func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { + log := hlog.FromRequest(r) + activityId := object["id"].(string) + likerUrl, 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 + } + // TODO: Account for case where object is embedded in like + 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" with a string value`), + nil, + ) + return + } + targetIdMatches := objectIdRegex.FindStringSubmatch(targetUrl) + if len(targetIdMatches) != 2 { + log.Error(). + Strs("match-results", targetIdMatches). + Str("url", targetUrl). + Msg("Url didn't match id extractor regex") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + targetId := targetIdMatches[1] + // Assume likes can only happen on notes for now + // Thus check if a note with that Id exists at all + note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(targetId)).First() + switch err { + case nil: + case gorm.ErrRecordNotFound: + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer("There is no note with the target id"), + nil, + ) + return + default: + log.Error().Err(err).Str("note-id", targetId).Msg("Failed to get note from db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + // Import liker after verifying that target note is correct + liker, err := activitypub.ImportRemoteAccountByAPUrl(likerUrl) + if err != nil { + log.Error(). + Err(err). + Str("liker-url", likerUrl). + Msg("Failed to import liking remote account") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + reaction := models.Reaction{ + Note: *note, + NoteId: note.ID, + Reactor: *liker, + ReactorId: liker.ID, + Emote: nil, + EmoteId: sql.NullInt64{Valid: false}, + } + tx := dbgen.Q.Begin() + + err = tx.Reaction.Create(&reaction) + if err != nil { + _ = tx.Rollback() + log.Error().Err(err).Any("raw-reaction", reaction).Msg("Failed to store reaction in db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } + // TODO: Create corresponding activity too + activity := models.Activity{ + Id: activityId, + Type: string(models.ActivityLike), + ObjectId: fmt.Sprint(reaction.ID), + ObjectType: uint32(models.ActivitystreamsActivityTargetReaction), + } + err = tx.Activity.Create(&activity) + if err != nil { + _ = tx.Rollback() + log.Error().Err(err).Any("raw-reaction", reaction).Msg("Failed to store reaction in db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } + err = tx.Commit() + if err != nil { + log.Error().Err(err).Msg("Failed to commit reaction transaction to db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } +} diff --git a/web/public/api/activitypub/0_user.go b/web/public/api/activitypub/user.go similarity index 95% rename from web/public/api/activitypub/0_user.go rename to web/public/api/activitypub/user.go index 3ad6824..a683d61 100644 --- a/web/public/api/activitypub/0_user.go +++ b/web/public/api/activitypub/user.go @@ -3,7 +3,6 @@ package activitypub import ( "encoding/json" "fmt" - "io" "net/http" "time" @@ -140,18 +139,6 @@ func users(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(encoded)) } -func userInbox(w http.ResponseWriter, r *http.Request) { - log := hlog.FromRequest(r) - userId := r.PathValue("id") - data, err := io.ReadAll(r.Body) - log.Info(). - Err(err). - Str("userId", userId). - Bytes("body", data). - Any("headers", r.Header). - Msg("Inbox message") -} - /* Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then Fuck you. diff --git a/web/public/api/activitypub/0_util.go b/web/public/api/activitypub/util.go similarity index 100% rename from web/public/api/activitypub/0_util.go rename to web/public/api/activitypub/util.go diff --git a/web/public/middleware/authFetchCheck.go b/web/public/middleware/authFetchCheck.go index fb679c2..3f3c847 100644 --- a/web/public/middleware/authFetchCheck.go +++ b/web/public/middleware/authFetchCheck.go @@ -70,15 +70,12 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil } // Not an always open path, check methods if r.Method == "GET" && !forGet { - log.Debug().Msg("Get request to AP resources don't need signature") h.ServeHTTP(w, r) return } else if !forGet && !forNonGet { - log.Info().Msg("Requests to AP resources don't need signature") h.ServeHTTP(w, r) return } - log.Debug().Msg("Need signature for AP request") rawDate := r.Header.Get("Date") date, err := http.ParseTime(rawDate) if err != nil { @@ -105,7 +102,8 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil } signatureHeader := r.Header.Get("Signature") if signatureHeader == "" { - log.Info().Msg("Received AP request without signature header where one is required") + log.Debug(). + Msg("Received AP request without signature header where one is required") webutils.ProblemDetails( w, http.StatusUnauthorized, @@ -121,7 +119,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil Msg("Signature header of inbound AP request") match := signatureRegex.FindStringSubmatch(signatureHeader) if len(match) <= 1 { - log.Info(). + log.Debug(). Str("header", signatureHeader). Msg("Received signature with invalid pattern") webutils.ProblemDetails( @@ -167,7 +165,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil } _, err = url.Parse(rawKeyId) if err != nil { - log.Warn().Err(err).Msg("Key id is not an url") + log.Debug().Err(err).Msg("Key id is not an url") webutils.ProblemDetails( w, http.StatusUnauthorized, @@ -181,7 +179,6 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil _ = rawAlgorithm stringToCheck := buildStringToCheck(r, rawHeaders) - log.Warn().Str("string-to-check", stringToCheck).Send() requestingActor, err := getRequestingActor(rawKeyId) if err != nil { diff --git a/web/public/server.go b/web/public/server.go index 7dbd68f..dd5a782 100644 --- a/web/public/server.go +++ b/web/public/server.go @@ -28,7 +28,6 @@ package webpublic import ( "context" - "fmt" "io/fs" "net/http" @@ -44,10 +43,6 @@ type Server struct { func New(addr string, duckImg *string, duckFs fs.FS) *Server { handler := http.NewServeMux() - handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(500) - fmt.Fprint(w, "not implemented") - }) handler.Handle("/api/", http.StripPrefix("/api", api.BuildApiRouter())) handler.HandleFunc("GET /.well-known/webfinger", api.WellKnownWebfinger) handler.HandleFunc("GET /.well-known/nodeinfo", api.NodeInfoOverview) diff --git a/web/shared/clientRfc9421.go b/web/shared/clientRfc9421.go index 22510cb..45e4b08 100644 --- a/web/shared/clientRfc9421.go +++ b/web/shared/clientRfc9421.go @@ -10,7 +10,6 @@ import ( "slices" "time" - "github.com/rs/zerolog/log" "github.com/yaronf/httpsign" "git.mstar.dev/mstar/linstrom/config" @@ -108,7 +107,6 @@ func RequestSignedCavage( if err != nil { return nil, err } - log.Debug().Bytes("body", body).Any("headers", req.Header).Msg("Sending signed request") return RequestClient.Do(req) }