From 42a5675c3ec75cbe539676fe20a0d8e377db5719 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Fri, 12 Dec 2025 11:28:28 +0200 Subject: [PATCH] appview/state: handle profile pic uploads Change-Id: pvlmunxrrooxwzsntzlwwlszznzpzrnr Signed-off-by: Anirudh Oppiliappan --- appview/ingester.go | 6 ++ appview/state/profile.go | 124 +++++++++++++++++++++++++++++++++++++++ appview/state/router.go | 1 + 3 files changed, 131 insertions(+) diff --git a/appview/ingester.go b/appview/ingester.go index af1300ae..338d40b0 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -285,6 +285,11 @@ func (i *Ingester) ingestProfile(e *jmodels.Event) error { return err } + avatar := "" + if record.Avatar != nil { + avatar = record.Avatar.Ref.String() + } + description := "" if record.Description != nil { description = *record.Description @@ -325,6 +330,7 @@ func (i *Ingester) ingestProfile(e *jmodels.Event) error { profile := models.Profile{ Did: did, + Avatar: avatar, Description: description, IncludeBluesky: includeBluesky, Location: location, diff --git a/appview/state/profile.go b/appview/state/profile.go index 400725ac..f7335edf 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -737,3 +737,127 @@ func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { AllRepos: allRepos, }) } + +func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { + l := s.logger.With("handler", "UploadProfileAvatar") + user := s.oauth.GetUser(r) + l = l.With("did", user.Did) + + // Parse multipart form (10MB max) + if err := r.ParseMultipartForm(10 << 20); err != nil { + l.Error("failed to parse form", "err", err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Failed to parse form") + return + } + + file, handler, err := r.FormFile("avatar") + if err != nil { + l.Error("failed to read avatar file", "err", err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Failed to read avatar file") + return + } + defer file.Close() + + if handler.Size > 1000000 { + l.Warn("avatar file too large", "size", handler.Size) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Avatar file too large (max 1MB)") + return + } + + contentType := handler.Header.Get("Content-Type") + if contentType != "image/png" && contentType != "image/jpeg" { + l.Warn("invalid image type", "contentType", contentType) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Invalid image type (only PNG and JPEG allowed)") + return + } + + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + l.Error("failed to get PDS client", "err", err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Failed to connect to your PDS") + return + } + + uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) + if err != nil { + l.Error("failed to upload avatar blob", "err", err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Failed to upload avatar to your PDS") + return + } + + l.Info("uploaded avatar blob", "cid", uploadBlobResp.Blob.Ref.String()) + + // get current profile record from PDS to get its CID for swap + getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") + if err != nil { + l.Error("failed to get current profile record", "err", err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Failed to get current profile from your PDS") + return + } + + var profileRecord *tangled.ActorProfile + if getRecordResp.Value != nil { + if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok { + profileRecord = val + } else { + l.Warn("profile record type assertion failed, creating new record") + profileRecord = &tangled.ActorProfile{} + } + } else { + l.Warn("no existing profile record, creating new record") + profileRecord = &tangled.ActorProfile{} + } + + profileRecord.Avatar = uploadBlobResp.Blob + + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: tangled.ActorProfileNSID, + Repo: user.Did, + Rkey: "self", + Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, + SwapRecord: getRecordResp.Cid, + }) + + if err != nil { + l.Error("failed to update profile record", "err", err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Failed to update profile on your PDS") + return + } + + l.Info("successfully updated profile with avatar") + + profile, err := db.GetProfile(s.db, user.Did) + if err != nil { + l.Warn("getting profile data from DB", "err", err) + profile = &models.Profile{Did: user.Did} + } + profile.Avatar = uploadBlobResp.Blob.Ref.String() + + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + l.Error("failed to start transaction", "err", err) + s.pages.HxRefresh(w) + w.WriteHeader(http.StatusOK) + return + } + + err = db.UpsertProfile(tx, profile) + if err != nil { + l.Error("failed to update profile in DB", "err", err) + tx.Rollback() + s.pages.HxRefresh(w) + w.WriteHeader(http.StatusOK) + return + } + + w.Header().Set("HX-Redirect", r.Header.Get("Referer")) + w.WriteHeader(http.StatusOK) +} diff --git a/appview/state/router.go b/appview/state/router.go index df7e258e..8617e6df 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -162,6 +162,7 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.Get("/edit-pins", s.EditPinsFragment) r.Post("/bio", s.UpdateProfileBio) r.Post("/pins", s.UpdateProfilePins) + r.Post("/avatar", s.UploadProfileAvatar) }) r.Mount("/settings", s.SettingsRouter()) -- 2.43.0