Monorepo for Tangled tangled.org

appview/state: handle profile pic uploads

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

anirudh.fi 42a5675c 316b720a

verified
Changed files
+131
appview
+6
appview/ingester.go
··· 285 285 return err 286 286 } 287 287 288 + avatar := "" 289 + if record.Avatar != nil { 290 + avatar = record.Avatar.Ref.String() 291 + } 292 + 288 293 description := "" 289 294 if record.Description != nil { 290 295 description = *record.Description ··· 325 330 326 331 profile := models.Profile{ 327 332 Did: did, 333 + Avatar: avatar, 328 334 Description: description, 329 335 IncludeBluesky: includeBluesky, 330 336 Location: location,
+124
appview/state/profile.go
··· 737 737 AllRepos: allRepos, 738 738 }) 739 739 } 740 + 741 + func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { 742 + l := s.logger.With("handler", "UploadProfileAvatar") 743 + user := s.oauth.GetUser(r) 744 + l = l.With("did", user.Did) 745 + 746 + // Parse multipart form (10MB max) 747 + if err := r.ParseMultipartForm(10 << 20); err != nil { 748 + l.Error("failed to parse form", "err", err) 749 + w.WriteHeader(http.StatusBadRequest) 750 + fmt.Fprintf(w, "Failed to parse form") 751 + return 752 + } 753 + 754 + file, handler, err := r.FormFile("avatar") 755 + if err != nil { 756 + l.Error("failed to read avatar file", "err", err) 757 + w.WriteHeader(http.StatusBadRequest) 758 + fmt.Fprintf(w, "Failed to read avatar file") 759 + return 760 + } 761 + defer file.Close() 762 + 763 + if handler.Size > 1000000 { 764 + l.Warn("avatar file too large", "size", handler.Size) 765 + w.WriteHeader(http.StatusBadRequest) 766 + fmt.Fprintf(w, "Avatar file too large (max 1MB)") 767 + return 768 + } 769 + 770 + contentType := handler.Header.Get("Content-Type") 771 + if contentType != "image/png" && contentType != "image/jpeg" { 772 + l.Warn("invalid image type", "contentType", contentType) 773 + w.WriteHeader(http.StatusBadRequest) 774 + fmt.Fprintf(w, "Invalid image type (only PNG and JPEG allowed)") 775 + return 776 + } 777 + 778 + client, err := s.oauth.AuthorizedClient(r) 779 + if err != nil { 780 + l.Error("failed to get PDS client", "err", err) 781 + w.WriteHeader(http.StatusInternalServerError) 782 + fmt.Fprintf(w, "Failed to connect to your PDS") 783 + return 784 + } 785 + 786 + uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 787 + if err != nil { 788 + l.Error("failed to upload avatar blob", "err", err) 789 + w.WriteHeader(http.StatusInternalServerError) 790 + fmt.Fprintf(w, "Failed to upload avatar to your PDS") 791 + return 792 + } 793 + 794 + l.Info("uploaded avatar blob", "cid", uploadBlobResp.Blob.Ref.String()) 795 + 796 + // get current profile record from PDS to get its CID for swap 797 + getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 798 + if err != nil { 799 + l.Error("failed to get current profile record", "err", err) 800 + w.WriteHeader(http.StatusInternalServerError) 801 + fmt.Fprintf(w, "Failed to get current profile from your PDS") 802 + return 803 + } 804 + 805 + var profileRecord *tangled.ActorProfile 806 + if getRecordResp.Value != nil { 807 + if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok { 808 + profileRecord = val 809 + } else { 810 + l.Warn("profile record type assertion failed, creating new record") 811 + profileRecord = &tangled.ActorProfile{} 812 + } 813 + } else { 814 + l.Warn("no existing profile record, creating new record") 815 + profileRecord = &tangled.ActorProfile{} 816 + } 817 + 818 + profileRecord.Avatar = uploadBlobResp.Blob 819 + 820 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 821 + Collection: tangled.ActorProfileNSID, 822 + Repo: user.Did, 823 + Rkey: "self", 824 + Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, 825 + SwapRecord: getRecordResp.Cid, 826 + }) 827 + 828 + if err != nil { 829 + l.Error("failed to update profile record", "err", err) 830 + w.WriteHeader(http.StatusInternalServerError) 831 + fmt.Fprintf(w, "Failed to update profile on your PDS") 832 + return 833 + } 834 + 835 + l.Info("successfully updated profile with avatar") 836 + 837 + profile, err := db.GetProfile(s.db, user.Did) 838 + if err != nil { 839 + l.Warn("getting profile data from DB", "err", err) 840 + profile = &models.Profile{Did: user.Did} 841 + } 842 + profile.Avatar = uploadBlobResp.Blob.Ref.String() 843 + 844 + tx, err := s.db.BeginTx(r.Context(), nil) 845 + if err != nil { 846 + l.Error("failed to start transaction", "err", err) 847 + s.pages.HxRefresh(w) 848 + w.WriteHeader(http.StatusOK) 849 + return 850 + } 851 + 852 + err = db.UpsertProfile(tx, profile) 853 + if err != nil { 854 + l.Error("failed to update profile in DB", "err", err) 855 + tx.Rollback() 856 + s.pages.HxRefresh(w) 857 + w.WriteHeader(http.StatusOK) 858 + return 859 + } 860 + 861 + w.Header().Set("HX-Redirect", r.Header.Get("Referer")) 862 + w.WriteHeader(http.StatusOK) 863 + }
+1
appview/state/router.go
··· 162 162 r.Get("/edit-pins", s.EditPinsFragment) 163 163 r.Post("/bio", s.UpdateProfileBio) 164 164 r.Post("/pins", s.UpdateProfilePins) 165 + r.Post("/avatar", s.UploadProfileAvatar) 165 166 }) 166 167 167 168 r.Mount("/settings", s.SettingsRouter())