From 8c961e1e5a078a965ad57cb8794863d2c0660efc 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 | 126 +++++++++++++++++++++++++++++++++++++++ appview/state/router.go | 1 + 3 files changed, 133 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..2de48801 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -737,3 +737,129 @@ 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 + } + + 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() + + profileRecord := &tangled.ActorProfile{ + Avatar: uploadBlobResp.Blob, + Bluesky: profile.IncludeBluesky, + } + + if profile.Description != "" { + profileRecord.Description = &profile.Description + } + if profile.Location != "" { + profileRecord.Location = &profile.Location + } + if profile.Pronouns != "" { + profileRecord.Pronouns = &profile.Pronouns + } + + for _, link := range profile.Links { + if link != "" { + profileRecord.Links = append(profileRecord.Links, link) + } + } + + for _, stat := range profile.Stats { + if stat.Kind != "" { + profileRecord.Stats = append(profileRecord.Stats, string(stat.Kind)) + } + } + + for _, pin := range profile.PinnedRepos { + if pin != "" { + profileRecord.PinnedRepositories = append(profileRecord.PinnedRepositories, string(pin)) + } + } + + _, 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") + + s.pages.HxRefresh(w) + 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