From 8697cd2632cffbc95adc120f0fc7f501754099c9 Mon Sep 17 00:00:00 2001 From: gitGnome Date: Tue, 28 Apr 2026 11:16:37 +0200 Subject: [PATCH] add new user set avatar and profile background --- go.mod | 2 +- machine-client/index.html | 36 +++++++- main.go | 9 +- packages/config/config.go | 1 + packages/httpRequest/files.go | 147 ++++++++++++++++++++++++++++-- packages/httpRequest/get.go | 38 ++++---- packages/httpRequest/hubs.go | 61 ------------- packages/httpRequest/user.go | 133 +-------------------------- packages/minio/minio.go | 2 +- packages/postgresql/postgresql.go | 2 +- 10 files changed, 203 insertions(+), 228 deletions(-) diff --git a/go.mod b/go.mod index 7f83720..cae97cc 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module go-socket go 1.26 require ( + github.com/BurntSushi/toml v1.6.0 github.com/coder/websocket v1.8.14 github.com/jackc/pgx/v5 v5.8.0 golang.org/x/crypto v0.49.0 ) require ( - github.com/BurntSushi/toml v1.6.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/machine-client/index.html b/machine-client/index.html index 35558ae..4ffd848 100644 --- a/machine-client/index.html +++ b/machine-client/index.html @@ -83,7 +83,10 @@ - + + + + @@ -220,7 +223,7 @@
- +
@@ -229,6 +232,30 @@
+ +
+
+
+
+
+ + +
+
+
sent as header
+
+
+ + +
+
+
+
+
+
+
+
+
@@ -267,7 +294,10 @@ 'get-connection-messages': { method:'GET', path:'/connection/messages', title:'GET /connection/messages — message history', fields:[{id:'gcm-token',dest:'header',name:'token'},{id:'gcm-connectionid',dest:'query',name:'connectionid'},{id:'gcm-messages',dest:'query',name:'messages'},{id:'gcm-before',dest:'query',name:'before'}] }, 'del-user': { method:'DELETE', path:'/user', title:'DELETE /user — delete own account', fields:[{id:'du-token',dest:'header',name:'token'}] }, 'del-connection': { method:'DELETE', path:'/connection', title:'DELETE /connection — delete a connection', fields:[{id:'dc-token',dest:'header',name:'token'},{id:'dc-connectionid',dest:'query',name:'connectionid'}] }, - 'msg-user': { method:'POST', path:'/message', title:'POST /message — send direct message', fields:[{id:'mu-token',dest:'header',name:'token'},{id:'mu-connectionid',dest:'body',name:'connectionid'},{id:'mu-msgContent',dest:'body',name:'msgContent'},{id:'mu-attachedFile',dest:'body',name:'attachedFile'}] }, + 'msg-user': { method:'POST', path:'/connection/message', title:'POST /connection/message — send direct message', fields:[{id:'mu-token',dest:'header',name:'token'},{id:'mu-connectionid',dest:'body',name:'connectionid'},{id:'mu-msgContent',dest:'body',name:'msgContent'},{id:'mu-attachedFile',dest:'body',name:'attachedFile'}] }, + 'hub-create': { method:'POST', path:'/hub', title:'POST /hub — create a new hub', fields:[{id:'hc-token',dest:'header',name:'token'},{id:'hc-hubname',dest:'body',name:'hubname'}] }, + 'hub-join': { method:'PUT', path:'/hub/join', title:'PUT /hub/join — join hub (hubid as header)', fields:[{id:'hj-token',dest:'header',name:'token'},{id:'hj-hubid',dest:'header',name:'hubid'}] }, + 'channel-message': { method:'POST', path:'/channel/message', title:'POST /channel/message — send hub channel msg',fields:[{id:'cm-token',dest:'header',name:'token'},{id:'cm-hubid',dest:'body',name:'hubid'},{id:'cm-channelid',dest:'body',name:'channelid'},{id:'cm-msgContent',dest:'body',name:'msgContent'},{id:'cm-attachedFile',dest:'body',name:'attachedFile'}] }, 'mod-user-avatar': { title:'PATCH /user/avatar — set avatar image' }, 'mod-user-profilebg': { title:'PATCH /user/profilebg — set profile background' }, 'file-upload': { title:'POST /file — upload file (multipart)' }, diff --git a/main.go b/main.go index caffed1..4054f15 100644 --- a/main.go +++ b/main.go @@ -38,8 +38,8 @@ func main() { http.HandleFunc("DELETE /user", withCORS(httpRequest.HandleUserDelete)) http.HandleFunc("GET /user", withCORS(httpRequest.HandleUserGetUser)) http.HandleFunc("PATCH /user/profile", withCORS(httpRequest.HandleUserModProfile)) - http.HandleFunc("PATCH /user/avatar", withCORS(httpRequest.HandleUserModAvatar)) - http.HandleFunc("PATCH /user/profilebg", withCORS(httpRequest.HandleUserModProfileBg)) + http.HandleFunc("PATCH /user/avatar", withCORS(httpRequest.HandleSetUserAvatar)) + http.HandleFunc("PATCH /user/profilebg", withCORS(httpRequest.HandleSetUserProfileBg)) http.HandleFunc("GET /user/avatar", withCORS(httpRequest.HandleGetUserAvatar)) http.HandleFunc("GET /user/profilebg", withCORS(httpRequest.HandleGetUserProfileBg)) @@ -56,7 +56,10 @@ func main() { http.HandleFunc("POST /file", withCORS(httpRequest.HandleAttachmentFileUpload)) http.HandleFunc("GET /file", withCORS(httpRequest.HandleAttachmentFileDownload)) - http.HandleFunc("POST /message", withCORS(httpRequest.HandleDm)) + http.HandleFunc("POST /hub", withCORS(httpRequest.HandleHubCreate)) + http.HandleFunc("PUT /hub/join", withCORS(httpRequest.HandleHubJoin)) + + http.HandleFunc("POST /connection/message", withCORS(httpRequest.HandleDm)) http.HandleFunc("GET /ws", wsServer.ServeWsConnection) log.Println("beep boop; server server started") diff --git a/packages/config/config.go b/packages/config/config.go index 4a60011..ea45354 100644 --- a/packages/config/config.go +++ b/packages/config/config.go @@ -33,6 +33,7 @@ type configFile struct { MaxRequestWithProfileBgBytes uint32 `toml:"max_request_with_profile_bg_bytes"` FileProcessingPartBytes uint64 `toml:"file_processing_part_bytes"` FileProcessingThreads uint `toml:"file_processing_threads"` + FileStorageBucketName string `toml:"file_storage_bucket_name"` FileDownloadLinkTtl time.Duration `toml:"file_download_link_ttl"` } diff --git a/packages/httpRequest/files.go b/packages/httpRequest/files.go index 81ace0f..94e0027 100644 --- a/packages/httpRequest/files.go +++ b/packages/httpRequest/files.go @@ -2,6 +2,8 @@ package httpRequest import ( json2 "encoding/json" + "go-socket/packages/postgresql" + "go-socket/packages/types" "net/http" "strings" @@ -42,7 +44,7 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ defer file.Close() contentType := header.Header.Get("Content-Type") - key := minio.GetKey(minio.GetKeyOptions{ + key := minio.GetKey(&minio.GetKeyOptions{ ConnectionId: conn.Id, MimeType: contentType, UploadType: minio.ConnectionFile, @@ -60,6 +62,62 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ response.Write([]byte(key)) } +func HandleSetUserAvatar(response http.ResponseWriter, request *http.Request) { + if !validCheckWithResponseOnFail(&response, request, avatar) { + return + } + ctx := request.Context() + user, err := getUserByToken(ctx, request.Header.Get("token")) + if err != nil { + http.Error(response, "invalid token", http.StatusUnauthorized) + return + } + + file, header, err := request.FormFile("file") + if err != nil { + http.Error(response, "missing file", http.StatusBadRequest) + return + } + defer file.Close() + + isImg, contentType, err := isImage(file) + if err != nil || !isImg { + http.Error(response, "invalid file", http.StatusBadRequest) + return + } + + key := minio.GetKey(&minio.GetKeyOptions{ + MimeType: contentType, + UploadType: minio.UserAvatar, + UserId: user.Id, + }) + err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ + "originalName": header.Filename, + "uploaderId": user.Id.String(), + }) + if err != nil { + http.Error(response, "upload failed", http.StatusInternalServerError) + return + } + + if user.AvatarUrl != "" { + if err = minio.Delete(ctx, user.AvatarUrl); err != nil { + minio.Delete(ctx, key) + http.Error(response, "internal server error", http.StatusInternalServerError) + return + } + } + user.AvatarUrl = key + err = postgresql.UserUpdateProfile(ctx, user, &types.UserProfileUpdateList{Avatar: true}) + if err != nil { + http.Error(response, "failed to update user avatar", http.StatusInternalServerError) + minio.Delete(ctx, user.AvatarUrl) + return + } + + response.WriteHeader(http.StatusCreated) +} + func HandleGetUserAvatar(response http.ResponseWriter, request *http.Request) { if !validCheckWithResponseOnFail(&response, request, normal) { return @@ -85,18 +143,82 @@ func HandleGetUserAvatar(response http.ResponseWriter, request *http.Request) { } if target.AvatarUrl == "" { - http.Error(response, "no avatar", http.StatusNotFound) + http.Error(response, "user have no avatar", http.StatusNoContent) return } - url, _, err := minio.GetDownloadUrlAndMetadata(ctx, target.AvatarUrl) + url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, target.AvatarUrl) if err != nil { http.Error(response, "internal server error", http.StatusInternalServerError) return } + json, err := json2.Marshal(map[string]any{ + "url": url.String(), + "metadata": meta, + }) + if err != nil { + http.Error(response, "json error", http.StatusInternalServerError) + } + response.WriteHeader(http.StatusOK) - response.Write([]byte(url.String())) + response.Write(json) +} + +func HandleSetUserProfileBg(response http.ResponseWriter, request *http.Request) { + if !validCheckWithResponseOnFail(&response, request, profileBg) { + return + } + ctx := request.Context() + user, err := getUserByToken(ctx, request.Header.Get("token")) + if err != nil { + http.Error(response, "invalid token", http.StatusUnauthorized) + return + } + + file, header, err := request.FormFile("file") + if err != nil { + http.Error(response, "missing file", http.StatusBadRequest) + return + } + defer file.Close() + + isImg, contentType, err := isImage(file) + if err != nil || !isImg { + http.Error(response, "invalid file", http.StatusBadRequest) + return + } + + key := minio.GetKey(&minio.GetKeyOptions{ + MimeType: contentType, + UploadType: minio.UserProfileBg, + UserId: user.Id, + }) + err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ + "originalName": header.Filename, + "uploaderId": user.Id.String(), + }) + if err != nil { + http.Error(response, "upload failed", http.StatusInternalServerError) + return + } + + if user.ProfileBgUrl != "" { + if err = minio.Delete(ctx, user.ProfileBgUrl); err != nil { + minio.Delete(ctx, key) + http.Error(response, "internal server error", http.StatusInternalServerError) + return + } + } + user.ProfileBgUrl = key + err = postgresql.UserUpdateProfile(ctx, user, &types.UserProfileUpdateList{ProfileBg: true}) + if err != nil { + http.Error(response, "failed to update user profile background", http.StatusInternalServerError) + minio.Delete(ctx, user.ProfileBgUrl) + return + } + + response.WriteHeader(http.StatusCreated) } func HandleGetUserProfileBg(response http.ResponseWriter, request *http.Request) { @@ -105,7 +227,8 @@ func HandleGetUserProfileBg(response http.ResponseWriter, request *http.Request) } ctx := request.Context() - if _, err := getUserByToken(ctx, request.Header.Get("token")); err != nil { + _, err := getUserByToken(ctx, request.Header.Get("token")) + if err != nil { http.Error(response, "invalid token", http.StatusUnauthorized) return } @@ -123,18 +246,26 @@ func HandleGetUserProfileBg(response http.ResponseWriter, request *http.Request) } if target.ProfileBgUrl == "" { - http.Error(response, "no profile background", http.StatusNotFound) + http.Error(response, "user have no profile background", http.StatusNoContent) return } - url, _, err := minio.GetDownloadUrlAndMetadata(ctx, target.ProfileBgUrl) + url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, target.ProfileBgUrl) if err != nil { http.Error(response, "internal server error", http.StatusInternalServerError) return } + json, err := json2.Marshal(map[string]any{ + "url": url.String(), + "metadata": meta, + }) + if err != nil { + http.Error(response, "json error", http.StatusInternalServerError) + } + response.WriteHeader(http.StatusOK) - response.Write([]byte(url.String())) + response.Write(json) } func HandleAttachmentFileDownload(response http.ResponseWriter, request *http.Request) { diff --git a/packages/httpRequest/get.go b/packages/httpRequest/get.go index cc152d9..0745ee7 100644 --- a/packages/httpRequest/get.go +++ b/packages/httpRequest/get.go @@ -90,22 +90,22 @@ func getHubUserIfValidWithResponseOnFail(ctx context.Context, response http.Resp return user, hubUser, hub, nil } -func getHubChannelIfValidWithResponseOnFail(ctx context.Context, response http.ResponseWriter, hub *types.Hub, hubUser *types.HubUser, channelId string) ( - *types.HubChannel, error) { - channelUuid, err := convertions.StringToUuid(channelId) - if err != nil { - http.Error(response, "invalid channelid", http.StatusBadRequest) - return nil, errors.New("invalid channelid") - } - channel, ok := hub.Channels[channelUuid] - if !ok { - http.Error(response, "invalid channelid", http.StatusBadRequest) - return nil, errors.New("invalid channelid") - } - - if !haveHubUserPermissionsOnChannel(types.CachedUserCanView, hubUser, channel) { - return nil, errors.New("invalid channelid") - } - - return channel, nil -} +//func getHubChannelIfValidWithResponseOnFail(ctx context.Context, response http.ResponseWriter, hub *types.Hub, hubUser *types.HubUser, channelId string) ( +// *types.HubChannel, error) { +// channelUuid, err := convertions.StringToUuid(channelId) +// if err != nil { +// http.Error(response, "invalid channelid", http.StatusBadRequest) +// return nil, errors.New("invalid channelid") +// } +// channel, ok := hub.Channels[channelUuid] +// if !ok { +// http.Error(response, "invalid channelid", http.StatusBadRequest) +// return nil, errors.New("invalid channelid") +// } +// +// if !haveHubUserPermissionsOnChannel(types.CachedUserCanView, hubUser, channel) { +// return nil, errors.New("invalid channelid") +// } +// +// return channel, nil +//} diff --git a/packages/httpRequest/hubs.go b/packages/httpRequest/hubs.go index 3381788..cbb227d 100644 --- a/packages/httpRequest/hubs.go +++ b/packages/httpRequest/hubs.go @@ -4,10 +4,8 @@ import ( "net/http" "time" - "go-socket/packages/Enums/WsEventType" "go-socket/packages/cache" "go-socket/packages/types" - "go-socket/packages/wsServer" "github.com/google/uuid" ) @@ -134,65 +132,6 @@ func HandleHubCreate(response http.ResponseWriter, request *http.Request) { cache.SaveHub(hub) } -func HandleChannelSendMessage(response http.ResponseWriter, request *http.Request) { - if !validCheckWithResponseOnFail(&response, request, normal) { - return - } - ctx := request.Context() - user, hubUser, hub, err := getHubUserIfValidWithResponseOnFail(ctx, response, request.Header.Get("token"), request.FormValue("hubid")) - if err != nil { - return - } - - msgContent := request.FormValue("msgContent") - attachedFile := request.FormValue("attachedFile") - - if msgContent == "" && attachedFile == "" { - http.Error(response, "empty msgContent", http.StatusBadRequest) - return - } - - channel, err := getHubChannelIfValidWithResponseOnFail(ctx, response, hub, hubUser, request.FormValue("channelid")) - if err != nil { - return - } - - if !haveHubUserPermissionsOnChannel(types.CachedUserCanMessage, hubUser, channel) { - http.Error(response, "cannot send messages here", http.StatusUnauthorized) - return - } - - message := &types.Message{ - Id: uuid.New(), - AttachedFile: "", - Content: msgContent, - Sender: user.Id, - Receiver: channel.Id, - CreatedAt: time.Now(), - } - - channel.Mu.RLock() - recipients := make([]uuid.UUID, 0, len(channel.UsersCachedPermissions)) - for id, userCachedPerms := range channel.UsersCachedPermissions { - if userCachedPerms.CanReadHistory() && id != user.Id { - recipients = append(recipients, id) - } - } - channel.Mu.RUnlock() - - for _, id := range recipients { - targetUser, err := cache.GetUserById(id) - if err != nil { - // todo Add to postgres in future - continue - } - wsServer.WsSendMessageCloseIfTimeout(targetUser, types.WsEventMessage{ - Type: WsEventType.HubMessage, - Event: message, - }) - } -} - func HandleHubJoin(response http.ResponseWriter, request *http.Request) { if !validCheckWithResponseOnFail(&response, request, normal) { return diff --git a/packages/httpRequest/user.go b/packages/httpRequest/user.go index b2644a4..aef2487 100644 --- a/packages/httpRequest/user.go +++ b/packages/httpRequest/user.go @@ -5,11 +5,8 @@ import ( "net/http" "time" - "go-socket/packages/config" - "go-socket/packages/convertions" - "go-socket/packages/minio" - "go-socket/packages/cache" + "go-socket/packages/convertions" "go-socket/packages/passwords" "go-socket/packages/postgresql" "go-socket/packages/tokens" @@ -152,7 +149,7 @@ func HandleUserModProfile(response http.ResponseWriter, request *http.Request) { return } - var updateList types.UserProfileUpdateList + updateList := &types.UserProfileUpdateList{} if pronouns := request.FormValue("pronouns"); pronouns != "" { if len(pronouns) > 32 { @@ -190,132 +187,6 @@ func HandleUserModProfile(response http.ResponseWriter, request *http.Request) { response.WriteHeader(http.StatusAccepted) } -func HandleUserModAvatar(response http.ResponseWriter, request *http.Request) { - if !validCheckWithResponseOnFail(&response, request, avatar) { - return - } - ctx := request.Context() - - user, err := getUserByToken(ctx, request.Header.Get("token")) - if err != nil { - http.Error(response, "invalid token", http.StatusUnauthorized) - return - } - - request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithAvatarBytes)) - - if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil { - http.Error(response, "invalid multipart form", http.StatusBadRequest) - return - } - - file, header, err := request.FormFile("file") - if err != nil { - http.Error(response, "missing file", http.StatusBadRequest) - return - } - defer file.Close() - - isImg, contentType, err := isImage(file) - if err != nil || !isImg { - http.Error(response, "invalid file", http.StatusBadRequest) - return - } - - if user.AvatarUrl != "" { - err = minio.Delete(ctx, user.AvatarUrl) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - } - - key := minio.GetKey(minio.GetKeyOptions{ - UserId: user.Id, - MimeType: contentType, - UploadType: minio.UserAvatar, - }) - if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ - "originalName": header.Filename, - "uploaderId": user.Id.String(), - }); err != nil { - http.Error(response, "upload failed", http.StatusInternalServerError) - return - } - - user.AvatarUrl = key - err = postgresql.UserUpdateProfile(ctx, user, types.UserProfileUpdateList{Avatar: true}) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - response.WriteHeader(http.StatusAccepted) -} - -func HandleUserModProfileBg(response http.ResponseWriter, request *http.Request) { - if !validCheckWithResponseOnFail(&response, request, profileBg) { - return - } - ctx := request.Context() - - user, err := getUserByToken(ctx, request.Header.Get("token")) - if err != nil { - http.Error(response, "invalid token", http.StatusUnauthorized) - return - } - - request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithProfileBgBytes)) - - if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil { - http.Error(response, "invalid multipart form", http.StatusBadRequest) - return - } - - file, header, err := request.FormFile("file") - if err != nil { - http.Error(response, "missing file", http.StatusBadRequest) - return - } - defer file.Close() - - isImg, contentType, err := isImage(file) - if err != nil || !isImg { - http.Error(response, "invalid file", http.StatusBadRequest) - return - } - - if user.ProfileBgUrl != "" { - err = minio.Delete(ctx, user.ProfileBgUrl) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - } - - key := minio.GetKey(minio.GetKeyOptions{ - UserId: user.Id, - MimeType: contentType, - UploadType: minio.UserProfileBg, - }) - if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ - "originalName": header.Filename, - "uploaderId": user.Id.String(), - }); err != nil { - http.Error(response, "upload failed", http.StatusInternalServerError) - return - } - - user.ProfileBgUrl = key - err = postgresql.UserUpdateProfile(ctx, user, types.UserProfileUpdateList{ProfileBg: true}) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - response.WriteHeader(http.StatusAccepted) -} - func HandleUserGetUser(response http.ResponseWriter, request *http.Request) { if !validCheckWithResponseOnFail(&response, request, normal) { return diff --git a/packages/minio/minio.go b/packages/minio/minio.go index cb35440..8908323 100644 --- a/packages/minio/minio.go +++ b/packages/minio/minio.go @@ -43,7 +43,7 @@ type GetKeyOptions struct { UploadType DataType } -func GetKey(opts GetKeyOptions) string { +func GetKey(opts *GetKeyOptions) string { extensions, err := mime.ExtensionsByType(opts.MimeType) if err != nil || len(extensions) == 0 { extensions = []string{".unknown"} diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go index 129eed7..4220870 100644 --- a/packages/postgresql/postgresql.go +++ b/packages/postgresql/postgresql.go @@ -113,7 +113,7 @@ func UserGetById(ctx context.Context, user *types.User) error { return err } -func UserUpdateProfile(ctx context.Context, user *types.User, updateList types.UserProfileUpdateList) error { +func UserUpdateProfile(ctx context.Context, user *types.User, updateList *types.UserProfileUpdateList) error { setClauses := make([]string, 0, 3) args := make([]any, 0, 4) argIdx := 1