Monorepo for Tangled

appview/state: handle profile pic uploads

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

authored by

Anirudh Oppiliappan and committed by tangled.org d2e9026e 332c39c6

+122
+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,
+115
appview/state/profile.go
··· 728 728 AllRepos: allRepos, 729 729 }) 730 730 } 731 + 732 + func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { 733 + l := s.logger.With("handler", "UploadProfileAvatar") 734 + user := s.oauth.GetUser(r) 735 + l = l.With("did", user.Did) 736 + 737 + // Parse multipart form (10MB max) 738 + if err := r.ParseMultipartForm(10 << 20); err != nil { 739 + l.Error("failed to parse form", "err", err) 740 + s.pages.Notice(w, "avatar-error", "Failed to parse form") 741 + return 742 + } 743 + 744 + file, handler, err := r.FormFile("avatar") 745 + if err != nil { 746 + l.Error("failed to read avatar file", "err", err) 747 + s.pages.Notice(w, "avatar-error", "Failed to read avatar file") 748 + return 749 + } 750 + defer file.Close() 751 + 752 + if handler.Size > 1000000 { 753 + l.Warn("avatar file too large", "size", handler.Size) 754 + s.pages.Notice(w, "avatar-error", "Avatar file too large (max 1MB)") 755 + return 756 + } 757 + 758 + contentType := handler.Header.Get("Content-Type") 759 + if contentType != "image/png" && contentType != "image/jpeg" { 760 + l.Warn("invalid image type", "contentType", contentType) 761 + s.pages.Notice(w, "avatar-error", "Invalid image type (only PNG and JPEG allowed)") 762 + return 763 + } 764 + 765 + client, err := s.oauth.AuthorizedClient(r) 766 + if err != nil { 767 + l.Error("failed to get PDS client", "err", err) 768 + s.pages.Notice(w, "avatar-error", "Failed to connect to your PDS") 769 + return 770 + } 771 + 772 + uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 773 + if err != nil { 774 + l.Error("failed to upload avatar blob", "err", err) 775 + s.pages.Notice(w, "avatar-error", "Failed to upload avatar to your PDS") 776 + return 777 + } 778 + 779 + l.Info("uploaded avatar blob", "cid", uploadBlobResp.Blob.Ref.String()) 780 + 781 + // get current profile record from PDS to get its CID for swap 782 + getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 783 + if err != nil { 784 + l.Error("failed to get current profile record", "err", err) 785 + s.pages.Notice(w, "avatar-error", "Failed to get current profile from your PDS") 786 + return 787 + } 788 + 789 + var profileRecord *tangled.ActorProfile 790 + if getRecordResp.Value != nil { 791 + if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok { 792 + profileRecord = val 793 + } else { 794 + l.Warn("profile record type assertion failed, creating new record") 795 + profileRecord = &tangled.ActorProfile{} 796 + } 797 + } else { 798 + l.Warn("no existing profile record, creating new record") 799 + profileRecord = &tangled.ActorProfile{} 800 + } 801 + 802 + profileRecord.Avatar = uploadBlobResp.Blob 803 + 804 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 805 + Collection: tangled.ActorProfileNSID, 806 + Repo: user.Did, 807 + Rkey: "self", 808 + Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, 809 + SwapRecord: getRecordResp.Cid, 810 + }) 811 + 812 + if err != nil { 813 + l.Error("failed to update profile record", "err", err) 814 + s.pages.Notice(w, "avatar-error", "Failed to update profile on your PDS") 815 + return 816 + } 817 + 818 + l.Info("successfully updated profile with avatar") 819 + 820 + profile, err := db.GetProfile(s.db, user.Did) 821 + if err != nil { 822 + l.Warn("getting profile data from DB", "err", err) 823 + profile = &models.Profile{Did: user.Did} 824 + } 825 + profile.Avatar = uploadBlobResp.Blob.Ref.String() 826 + 827 + tx, err := s.db.BeginTx(r.Context(), nil) 828 + if err != nil { 829 + l.Error("failed to start transaction", "err", err) 830 + s.pages.HxRefresh(w) 831 + w.WriteHeader(http.StatusOK) 832 + return 833 + } 834 + 835 + err = db.UpsertProfile(tx, profile) 836 + if err != nil { 837 + l.Error("failed to update profile in DB", "err", err) 838 + s.pages.HxRefresh(w) 839 + w.WriteHeader(http.StatusOK) 840 + return 841 + } 842 + 843 + w.Header().Set("HX-Redirect", r.Header.Get("Referer")) 844 + w.WriteHeader(http.StatusOK) 845 + }
+1
appview/state/router.go
··· 165 165 r.Get("/edit-pins", s.EditPinsFragment) 166 166 r.Post("/bio", s.UpdateProfileBio) 167 167 r.Post("/pins", s.UpdateProfilePins) 168 + r.Post("/avatar", s.UploadProfileAvatar) 168 169 }) 169 170 170 171 r.Mount("/settings", s.SettingsRouter())