From 113616ff410776a206a58919ad2463080cf077e6 Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Sun, 12 Oct 2025 16:32:37 -0600 Subject: [PATCH] uploading a profile photo now works --- database/user.go | 1 + ui/images.go | 32 ++++++++++++++++++++++++---- ui/views/dialog.jet | 3 +-- userdata.go | 52 +++++++++++++++++++++++++++++++++++---------- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/database/user.go b/database/user.go index aa8af93..421bf90 100644 --- a/database/user.go +++ b/database/user.go @@ -641,6 +641,7 @@ func AmCreateNewUser(username string, password string, reminder string, dob *tim return user, nil } +// internalGetProp is a helper used by the property functions. func internalGetProp(uid int32, ndx int32) (*UserProperties, error) { var err error = nil key := fmt.Sprintf("%d:%d", uid, ndx) diff --git a/ui/images.go b/ui/images.go index d27117b..8c8e34b 100644 --- a/ui/images.go +++ b/ui/images.go @@ -13,7 +13,7 @@ package ui import ( "bytes" "embed" - "fmt" + "errors" "image" "image/gif" "image/jpeg" @@ -32,6 +32,15 @@ import ( //go:embed static_images/* var static_images embed.FS +// Constants for default photo sizes. +const ( + UserPhotoWidth = 100 + UserPhotoHeight = 100 + UserPhotoMaxBytes = 2097152 // 2 Mb + CommunityLogoWidth = 110 + CommunityLogoHeight = 60 +) + /* mimeTypeFromFilenane returns the MIME type of a file, given its filename. * Parameters: * filaname - The name of the file to be tested. @@ -76,11 +85,26 @@ func AmServeImage(ctxt AmContext) (string, any, error) { } } ctxt.SetRC(http.StatusNotFound) - // TODO: improve this error reporting - return "string", fmt.Sprintf("File not found: %s", ctxt.URLPath()), err + return ErrorPage(ctxt, err) } -func AmProcessUploadedImage(fileheader *multipart.FileHeader, width, height int) ([]byte, string, error) { +/* AmProcessUploadedImage takes an image and resizes it to a specified size, returning its data. + * Parameters: + * fileheader - The multipart file header from the uploaded file. + * width - New image width in pizels. + * height - New image height in pixels. + * maxbytes - The maximum size of the user photo. + * Returns: + * Image data as a byte array. + * The MIME type of the image data. + * Standard Go error status. + */ +func AmProcessUploadedImage(fileheader *multipart.FileHeader, width, height, maxbytes int) ([]byte, string, error) { + // test size + if fileheader.Size > int64(maxbytes) { + return nil, "", errors.New("file is too large; please try again") + } + // open the file file, err := fileheader.Open() if err != nil { diff --git a/ui/views/dialog.jet b/ui/views/dialog.jet index a50cc14..9ce8064 100644 --- a/ui/views/dialog.jet +++ b/ui/views/dialog.jet @@ -188,8 +188,7 @@ - Click to upload photo + Click to upload photo {{ else if .Type == "header" }} diff --git a/userdata.go b/userdata.go index bd972d6..38cc1cc 100644 --- a/userdata.go +++ b/userdata.go @@ -22,6 +22,14 @@ import ( log "github.com/sirupsen/logrus" ) +// userPhotoURL returns the photo URL from the contact info, or a default. +func userPhotoURL(ci *database.ContactInfo) string { + if ci.PhotoURL != nil && *ci.PhotoURL != "" { + return *ci.PhotoURL + } + return "/img/builtin/no-user.png" +} + /* EditProfileForm renders the Amsterdam profile editing form. * Parameters: * ctxt - The AmContext for the request. @@ -74,7 +82,7 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) { dlg.Field("url").SetVal(ci.URL) dlg.Field("dob").SetDate(u.DOB) dlg.Field("descr").SetVal(u.Description) - // TODO: do something for user photo + dlg.Field("photo").Value = userPhotoURL(ci) dlg.Field("pic_in_post").SetChecked(u.FlagValue(database.UserFlagPicturesInPosts)) dlg.Field("no_mass_mail").SetChecked(u.FlagValue(database.UserFlagMassMailOptOut)) dlg.Field("locale").Value = prefs.ReadLocale() @@ -86,6 +94,14 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } +/* EditProfile handles profile editing. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ func EditProfile(ctxt ui.AmContext) (string, any, error) { u := ctxt.CurrentUser() if u.IsAnon { @@ -201,15 +217,22 @@ func ProfilePhotoForm(ctxt ui.AmContext) (string, any, error) { } ci, err := u.ContactInfo() if err == nil { - _ = ci ctxt.VarMap().Set("target", target) - ctxt.VarMap().Set("photo_url", "/img/builtin/no-user.png") + ctxt.VarMap().Set("photo_url", userPhotoURL(ci)) ctxt.VarMap().Set("amsterdam_pageTitle", "Upload User Photo") return "framed_template", "photo_upload.jet", nil } return ui.ErrorPage(ctxt, err) } +/* ProfilePhoto handles processing the uploaded user photo.. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) { u := ctxt.CurrentUser() if u.IsAnon { @@ -229,7 +252,10 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) { if ctxt.FormFieldIsSet("upload") { file, err := ctxt.FormFile("thepic") if err == nil { - imageData, mimeType, err := ui.AmProcessUploadedImage(file, 200, 200) + var imageData []byte + var mimeType string + imageData, mimeType, err = ui.AmProcessUploadedImage(file, ui.UserPhotoWidth, ui.UserPhotoHeight, + ui.UserPhotoMaxBytes) if err == nil { var img *database.ImageStore img, err = database.AmStoreImage(database.ImageTypeUserPhoto, u.Uid, mimeType, imageData) @@ -245,12 +271,13 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) { } ctxt.VarMap().Set("errorMessage", err.Error()) ctxt.VarMap().Set("target", target) - ctxt.VarMap().Set("photo_url", "/img/builtin/no-user.png") + ctxt.VarMap().Set("photo_url", userPhotoURL(ci)) ctxt.VarMap().Set("amsterdam_pageTitle", "Upload User Photo") return "framed_template", "photo_upload.jet", nil } if ctxt.FormFieldIsSet("remove") { purl := ci.PhotoURL + happy := false if purl == nil || *purl == "" { // this is a no-op return "redirect", "/profile?tgt=" + url.QueryEscape(target), nil @@ -261,12 +288,14 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } defer func() { - go func() { - err := database.AmDeleteImage(int32(id)) - if err != nil { - log.Errorf("unable to delete image ID %d: %v", id, err) - } - }() + if happy { + go func() { + err := database.AmDeleteImage(int32(id)) + if err != nil { + log.Errorf("unable to delete image ID %d: %v", id, err) + } + }() + } }() } ci.PhotoURL = nil @@ -274,6 +303,7 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) { if err != nil { return ui.ErrorPage(ctxt, err) } + happy = true return "redirect", "/profile?tgt=" + url.QueryEscape(target), nil } return ui.ErrorPage(ctxt, errors.New("invalid button detected in photo upload"))