Monorepo for Tangled tangled.org

appview/state: handle profile pic uploads

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

anirudh.fi 8c961e1e 316b720a

verified
Changed files
+133
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,
+126
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 + profile, err := db.GetProfile(s.db, user.Did) 806 + if err != nil { 807 + l.Warn("getting profile data from DB", "err", err) 808 + profile = &models.Profile{Did: user.Did} 809 + } 810 + 811 + profile.Avatar = uploadBlobResp.Blob.Ref.String() 812 + 813 + profileRecord := &tangled.ActorProfile{ 814 + Avatar: uploadBlobResp.Blob, 815 + Bluesky: profile.IncludeBluesky, 816 + } 817 + 818 + if profile.Description != "" { 819 + profileRecord.Description = &profile.Description 820 + } 821 + if profile.Location != "" { 822 + profileRecord.Location = &profile.Location 823 + } 824 + if profile.Pronouns != "" { 825 + profileRecord.Pronouns = &profile.Pronouns 826 + } 827 + 828 + for _, link := range profile.Links { 829 + if link != "" { 830 + profileRecord.Links = append(profileRecord.Links, link) 831 + } 832 + } 833 + 834 + for _, stat := range profile.Stats { 835 + if stat.Kind != "" { 836 + profileRecord.Stats = append(profileRecord.Stats, string(stat.Kind)) 837 + } 838 + } 839 + 840 + for _, pin := range profile.PinnedRepos { 841 + if pin != "" { 842 + profileRecord.PinnedRepositories = append(profileRecord.PinnedRepositories, string(pin)) 843 + } 844 + } 845 + 846 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 847 + Collection: tangled.ActorProfileNSID, 848 + Repo: user.Did, 849 + Rkey: "self", 850 + Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, 851 + SwapRecord: getRecordResp.Cid, 852 + }) 853 + 854 + if err != nil { 855 + l.Error("failed to update profile record", "err", err) 856 + w.WriteHeader(http.StatusInternalServerError) 857 + fmt.Fprintf(w, "Failed to update profile on your PDS") 858 + return 859 + } 860 + 861 + l.Info("successfully updated profile with avatar") 862 + 863 + s.pages.HxRefresh(w) 864 + w.WriteHeader(http.StatusOK) 865 + }
+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())