diff --git a/main.go b/main.go index 0042875..e0443a0 100644 --- a/main.go +++ b/main.go @@ -31,14 +31,14 @@ func main() { http.HandleFunc("/new/user", withCORS(httpRequest.HandleUserNew)) http.HandleFunc("/new/connection", withCORS(httpRequest.HandleUserNewConnection)) http.HandleFunc("/new/token", withCORS(httpRequest.HandleUserNewToken)) - http.HandleFunc("/new/file", withCORS(httpRequest.HandleFileUpload)) + http.HandleFunc("/new/file", withCORS(httpRequest.HandleAttachmentFileUpload)) http.HandleFunc("/mod/user/appearence", withCORS(httpRequest.HandleUserModifyAppearance)) - http.HandleFunc("/mod/user/about", withCORS(httpRequest.HandleUserModifyAbout)) + http.HandleFunc("/mod/user/about", withCORS(httpRequest.HandleUserModProfile)) http.HandleFunc("/mod/connection/accept", withCORS(httpRequest.HandleUserElevateConnection)) http.HandleFunc("/get/connections", withCORS(httpRequest.HandleUserGetConnections)) http.HandleFunc("/get/connection/messages", withCORS(httpRequest.HandleUserGetConnectionMessages)) - http.HandleFunc("/get/file", withCORS(httpRequest.HandleFileDownload)) + http.HandleFunc("/get/file", withCORS(httpRequest.HandleAttachmentFileDownload)) http.HandleFunc("/del/user", withCORS(httpRequest.HandleUserDelete)) http.HandleFunc("/del/connection", withCORS(httpRequest.HandleUserDeleteConnection)) diff --git a/packages/Enums/ConnectionState/ConnectionState.go b/packages/Enums/ConnectionState/ConnectionState.go index f0bee24..1902579 100644 --- a/packages/Enums/ConnectionState/ConnectionState.go +++ b/packages/Enums/ConnectionState/ConnectionState.go @@ -4,7 +4,5 @@ type ConnectionState uint8 const ( Stranger ConnectionState = iota - GroupFellow Friend - GroupFriend ) diff --git a/packages/httpRequest/file.go b/packages/httpRequest/attachmentFile.go similarity index 64% rename from packages/httpRequest/file.go rename to packages/httpRequest/attachmentFile.go index 48bd979..769344a 100644 --- a/packages/httpRequest/file.go +++ b/packages/httpRequest/attachmentFile.go @@ -1,15 +1,15 @@ package httpRequest import ( + json2 "encoding/json" "net/http" "strings" - "go-socket/packages/convertions" "go-socket/packages/globals" "go-socket/packages/minio" ) -func HandleFileUpload(response http.ResponseWriter, request *http.Request) { +func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Request) { if !postValidCheckWithResponseOnFail(&response, request, true) { return } @@ -28,14 +28,8 @@ func HandleFileUpload(response http.ResponseWriter, request *http.Request) { return } - connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) - if err != nil { - http.Error(response, "invalid connectionid", http.StatusBadRequest) - return - } - _, ok := user.Connections[connectionId] + conn, ok := getConnectionWithResponseOnFail(&response, request, user) if !ok { - http.Error(response, "no such connection", http.StatusUnauthorized) return } @@ -47,7 +41,7 @@ func HandleFileUpload(response http.ResponseWriter, request *http.Request) { defer file.Close() contentType := header.Header.Get("Content-Type") - key := minio.GetKey(connectionId, contentType) + key := minio.GetKey(conn.Id, contentType, minio.File) if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ "originalName": header.Filename, @@ -61,7 +55,7 @@ func HandleFileUpload(response http.ResponseWriter, request *http.Request) { response.Write([]byte(key)) } -func HandleFileDownload(response http.ResponseWriter, request *http.Request) { +func HandleAttachmentFileDownload(response http.ResponseWriter, request *http.Request) { if !postValidCheckWithResponseOnFail(&response, request, false) { return } @@ -73,28 +67,32 @@ func HandleFileDownload(response http.ResponseWriter, request *http.Request) { return } - connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) - if err != nil { - http.Error(response, "invalid connectionid", http.StatusBadRequest) - return - } - if _, ok := user.Connections[connectionId]; !ok { - http.Error(response, "no such connection", http.StatusUnauthorized) + conn, ok := getConnectionWithResponseOnFail(&response, request, user) + if !ok { return } key := request.FormValue("key") - if !strings.HasPrefix(key, connectionId.String()+"/") { + if !strings.HasPrefix(key, conn.Id.String()+"/") { http.Error(response, "no such file", http.StatusUnauthorized) return } - url, err := minio.GetDownloadUrl(ctx, key) + url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, key) if err != nil { http.Error(response, "no such file", http.StatusUnauthorized) return } + json, err := json2.Marshal(map[string]string{ + "url": url.String(), + "originalName": meta["originalName"], + }) + if err != nil { + http.Error(response, "metadata error", http.StatusInternalServerError) + return + } + response.WriteHeader(http.StatusOK) - response.Write([]byte(url.String())) + response.Write(json) } diff --git a/packages/httpRequest/connectionsAndDms.go b/packages/httpRequest/connectionsAndDms.go index b53f81d..641c7a2 100644 --- a/packages/httpRequest/connectionsAndDms.go +++ b/packages/httpRequest/connectionsAndDms.go @@ -32,14 +32,8 @@ func HandleDm(response http.ResponseWriter, request *http.Request) { return } - targetConnection, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) - if err != nil { - http.Error(response, "invalid connectionid", http.StatusBadRequest) - return - } - conn, ok := cache.CacheGetConnection(user, targetConnection) + conn, ok := getConnectionWithResponseOnFail(&response, request, user) if !ok { - http.Error(response, "invalid connectionid", http.StatusBadRequest) return } @@ -51,7 +45,7 @@ func HandleDm(response http.ResponseWriter, request *http.Request) { return } - if attachedFile != "" && !strings.HasPrefix(attachedFile, targetConnection.String()+"/") { + if attachedFile != "" && !strings.HasPrefix(attachedFile, conn.Id.String()+"/") { http.Error(response, "invalid attachedFile", http.StatusBadRequest) return } @@ -105,9 +99,8 @@ func HandleUserGetConnectionMessages(response http.ResponseWriter, request *http return } - connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) - if err != nil { - http.Error(response, "invalid connectionid", http.StatusBadRequest) + conn, ok := getConnectionWithResponseOnFail(&response, request, user) + if !ok { return } @@ -121,12 +114,6 @@ func HandleUserGetConnectionMessages(response http.ResponseWriter, request *http messagesCap = globals.MaxDirectMsgCache } - conn, ok := cache.CacheGetConnection(user, connectionId) - if !ok { - http.Error(response, "invalid connectionid", http.StatusBadRequest) - return - } - buffer, bufferSize := conn.GetSortedMessagesBuff() var validBufCount uint32 @@ -148,7 +135,7 @@ func HandleUserGetConnectionMessages(response http.ResponseWriter, request *http if validBufCount > 0 { cutoff = buffer[0].CreatedAt } - dbMessages, err := postgresql.ConnectionGetMessagesBefore(ctx, cutoff, connectionId, remaining) + dbMessages, err := postgresql.ConnectionGetMessagesBefore(ctx, cutoff, conn.Id, remaining) if err != nil { http.Error(response, "internal server error", http.StatusInternalServerError) return @@ -241,15 +228,8 @@ func HandleUserDeleteConnection(response http.ResponseWriter, request *http.Requ return } - connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) - if err != nil { - http.Error(response, "invalid connectionid", http.StatusBadRequest) - return - } - - conn, ok := cache.CacheGetConnection(user, connectionId) + conn, ok := getConnectionWithResponseOnFail(&response, request, user) if !ok { - http.Error(response, "invalid connectionid", http.StatusBadRequest) return } @@ -279,10 +259,10 @@ func HandleUserDeleteConnection(response http.ResponseWriter, request *http.Requ return } - cache.CacheDeleteConnection(user, user2, connectionId) + cache.CacheDeleteConnection(user, user2, conn.Id) wsServer.WsSendMessageCloseIfTimeout(user2, types.WsEventMessage{ Type: WsEventType.ConnectionDeleted, - Event: connectionId, + Event: conn.Id, }) response.WriteHeader(http.StatusAccepted) @@ -298,14 +278,8 @@ func HandleUserElevateConnection(response http.ResponseWriter, request *http.Req http.Error(response, "invalid token", http.StatusUnauthorized) return } - connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) - if err != nil { - http.Error(response, "invalid connectionid", http.StatusBadRequest) - return - } - conn, ok := cache.CacheGetConnection(user, connectionId) + conn, ok := getConnectionWithResponseOnFail(&response, request, user) if !ok { - http.Error(response, "invalid connectionid", http.StatusBadRequest) return } @@ -316,9 +290,6 @@ func HandleUserElevateConnection(response http.ResponseWriter, request *http.Req case ConnectionState.Stranger: conn.State = ConnectionState.Friend break - case ConnectionState.GroupFellow: - conn.State = ConnectionState.Stranger - break default: http.Error(response, "cannot elevate further", http.StatusBadRequest) return @@ -345,7 +316,7 @@ func HandleUserElevateConnection(response http.ResponseWriter, request *http.Req wsServer.WsSendMessageCloseIfTimeout(user2, types.WsEventMessage{ Type: WsEventType.ConnectionElevated, Event: types.ConnectionElevationData{ - Id: connectionId, + Id: conn.Id, NewState: conn.State, }, }) diff --git a/packages/httpRequest/get.go b/packages/httpRequest/get.go index 8dbf81c..7f5f6ac 100644 --- a/packages/httpRequest/get.go +++ b/packages/httpRequest/get.go @@ -2,6 +2,8 @@ package httpRequest import ( "context" + "go-socket/packages/convertions" + "net/http" "go-socket/packages/cache" "go-socket/packages/postgresql" @@ -31,3 +33,16 @@ func getUserByToken(ctx context.Context, token string) (*types.User, error) { } return getUserById(ctx, userId) } +func getConnectionWithResponseOnFail(response *http.ResponseWriter, request *http.Request, user *types.User) (*types.Connection, bool) { + connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) + if err != nil { + http.Error(*response, "invalid connectionid", http.StatusBadRequest) + return nil, false + } + conn, ok := cache.CacheGetConnection(user, connectionId) + if !ok { + http.Error(*response, "invalid connectionid", http.StatusBadRequest) + return nil, false + } + return conn, true +} diff --git a/packages/httpRequest/user.go b/packages/httpRequest/user.go index 0f5e8d9..914be8d 100644 --- a/packages/httpRequest/user.go +++ b/packages/httpRequest/user.go @@ -2,11 +2,13 @@ package httpRequest import ( json2 "encoding/json" + "go-socket/packages/convertions" + "go-socket/packages/globals" + "go-socket/packages/minio" "net/http" "time" "go-socket/packages/cache" - "go-socket/packages/convertions" "go-socket/packages/passwords" "go-socket/packages/postgresql" "go-socket/packages/tokens" @@ -137,8 +139,7 @@ func HandleUserDelete(response http.ResponseWriter, request *http.Request) { response.WriteHeader(http.StatusAccepted) } -// HandleUserModifyAppearance currently just color -func HandleUserModifyAppearance(response http.ResponseWriter, request *http.Request) { +func HandleUserModProfile(response http.ResponseWriter, request *http.Request) { if !postValidCheckWithResponseOnFail(&response, request, false) { return } @@ -150,13 +151,37 @@ func HandleUserModifyAppearance(response http.ResponseWriter, request *http.Requ return } - color, err := convertions.StringToRgba(request.FormValue("color")) - if err != nil { - http.Error(response, "invalid color", http.StatusBadRequest) - return + var updateList types.UserProfileUpdateList + + if pronouns := request.FormValue("pronouns"); pronouns != "" { + if len(pronouns) > 32 { + http.Error(response, "pronouns too long", http.StatusBadRequest) + return + } + user.Pronouns = pronouns + updateList.Pronouns = true } - user.Color = color - err = postgresql.UserSetColor(ctx, user) + + if description := request.FormValue("description"); description != "" { + if len(description) > 256 { + http.Error(response, "description too long", http.StatusBadRequest) + return + } + user.Description = description + updateList.Description = true + } + + if colorStr := request.FormValue("color"); colorStr != "" { + color, err := convertions.StringToRgba(colorStr) + if err != nil { + http.Error(response, "invalid color", http.StatusBadRequest) + return + } + user.Color = color + updateList.Color = true + } + + err = postgresql.UserUpdateProfile(ctx, user, updateList) if err != nil { http.Error(response, "internal server error", http.StatusInternalServerError) return @@ -164,30 +189,46 @@ func HandleUserModifyAppearance(response http.ResponseWriter, request *http.Requ response.WriteHeader(http.StatusAccepted) } -// HandleUserModifyAbout currently just pronouns -func HandleUserModifyAbout(response http.ResponseWriter, request *http.Request) { - if !postValidCheckWithResponseOnFail(&response, request, false) { +func HandleUserModAvatar(response http.ResponseWriter, request *http.Request) { + + if !postValidCheckWithResponseOnFail(&response, request, true) { return } - ctx := request.Context() - user, err := getUserByToken(ctx, request.FormValue("token")) + + user, err := getUserByToken(ctx, request.Header.Get("token")) if err != nil { http.Error(response, "invalid token", http.StatusUnauthorized) return } - pronouns := request.FormValue("pronouns") - if len(pronouns) > 25 || len(pronouns) < 2 { - http.Error(response, "invalid pronouns", http.StatusBadRequest) + request.Body = http.MaxBytesReader(response, request.Body, int64(globals.MaxPostWithFileBytes)) + + if err = request.ParseMultipartForm(int64(globals.MaxPostBytes)); err != nil { + http.Error(response, "invalid multipart form", http.StatusBadRequest) return } - user.Pronouns = pronouns - err = postgresql.UserSetPronouns(ctx, user) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) + conn, ok := getConnectionWithResponseOnFail(&response, request, user) + if !ok { + return + } + + file, header, err := request.FormFile("file") + if err != nil { + http.Error(response, "missing file", http.StatusBadRequest) + return + } + defer file.Close() + + contentType := header.Header.Get("Content-Type") + key := minio.GetKey(conn.Id, contentType, minio.File) + + 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 } - response.WriteHeader(http.StatusAccepted) } diff --git a/packages/minio/minio.go b/packages/minio/minio.go index 1b454a1..86440c7 100644 --- a/packages/minio/minio.go +++ b/packages/minio/minio.go @@ -17,12 +17,28 @@ import ( var minClient *minio.Client -func GetKey(connectionId uuid.UUID, mimeType string) string { +type DataType uint8 + +const ( + File DataType = iota + UserAvatar + UserProfileBg +) + +func GetKey(connectionId uuid.UUID, mimeType string, uploadType DataType) string { extensions, err := mime.ExtensionsByType(mimeType) if err != nil || len(extensions) == 0 { extensions = []string{".unknown"} } - return connectionId.String() + "/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + extensions[0] + + key := connectionId.String() + "/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + extensions[0] + + if uploadType == UserAvatar { + return "userAvatar/" + key + } else if uploadType == UserProfileBg { + return "userProfileBg/" + key + } + return "upload/" + key } func Init(ctx context.Context) { @@ -30,7 +46,7 @@ func Init(ctx context.Context) { minClient, err = minio.New("localhost:9000", &minio.Options{ Creds: credentials.NewStaticV4("root", "change_to_env", ""), Secure: false, - }) // TODO change in production + }) // TODO change for production if err != nil { panic(err) } @@ -49,7 +65,6 @@ func Init(ctx context.Context) { } } } - } func Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string, metadata map[string]string) error { @@ -65,9 +80,16 @@ func Upload(ctx context.Context, key string, body io.Reader, size int64, content return err } -func GetDownloadUrl(ctx context.Context, key string) (*url.URL, error) { +func GetDownloadUrlAndMetadata(ctx context.Context, key string) (*url.URL, map[string]string, error) { + info, err := minClient.StatObject(ctx, globals.FileStorageBucketName, key, minio.StatObjectOptions{}) + if err != nil { + return nil, nil, err + } u, err := minClient.PresignedGetObject(ctx, globals.FileStorageBucketName, key, globals.FileDownloadLinkTtl, nil) - return u, err + if err != nil { + return nil, nil, err + } + return u, info.UserMetadata, nil } func DoesExist(ctx context.Context, key string) bool { diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go index 3aa3d5e..d9f0d36 100644 --- a/packages/postgresql/postgresql.go +++ b/packages/postgresql/postgresql.go @@ -3,6 +3,7 @@ package postgresql import ( "context" "fmt" + "strings" "time" "go-socket/packages/cache" @@ -33,6 +34,9 @@ func Init(ctx context.Context) { name TEXT UNIQUE NOT NULL, pass_hash TEXT NOT NULL, pronouns TEXT DEFAULT NULL, + description TEXT DEFAULT NULL, + avatar TEXT DEFAULT NULL, + profileBg TEXT DEFAULT NULL, rgba BIGINT NOT NULL DEFAULT 0 CHECK (rgba BETWEEN 0 AND 4294967295), created_at TIMESTAMP NOT NULL DEFAULT NOW() ) @@ -108,17 +112,35 @@ func UserGetById(ctx context.Context, user *types.User) error { return err } -func UserSetColor(ctx context.Context, user *types.User) error { - _, err := dbConn.Exec(ctx, ` - UPDATE users SET rgba = $1 WHERE id = $2 - `, convertions.RgbaToUint32(user.Color), user.Id) - return err -} +func UserUpdateProfile(ctx context.Context, user *types.User, updateList types.UserProfileUpdateList) error { + setClauses := make([]string, 0, 3) + args := make([]any, 0, 4) + argIdx := 1 -func UserSetPronouns(ctx context.Context, user *types.User) error { - _, err := dbConn.Exec(ctx, ` - UPDATE users SET pronouns = $1 WHERE id = $2 - `, user.Pronouns, user.Id) + if updateList.Pronouns { + setClauses = append(setClauses, fmt.Sprintf("pronouns = $%d", argIdx)) + args = append(args, user.Pronouns) + argIdx++ + } + if updateList.Description { + setClauses = append(setClauses, fmt.Sprintf("description = $%d", argIdx)) + args = append(args, user.Description) + argIdx++ + } + if updateList.Color { + setClauses = append(setClauses, fmt.Sprintf("rgba = $%d", argIdx)) + args = append(args, convertions.RgbaToUint32(user.Color)) + argIdx++ + } + + if len(setClauses) == 0 { + return nil + } + + query := "UPDATE users SET " + strings.Join(setClauses, ", ") + fmt.Sprintf(" WHERE id = $%d", argIdx) + args = append(args, user.Id) + + _, err := dbConn.Exec(ctx, query, args...) return err } diff --git a/packages/types/types.go b/packages/types/types.go index 04177a2..0b6b913 100644 --- a/packages/types/types.go +++ b/packages/types/types.go @@ -28,6 +28,9 @@ type User struct { Mu sync.RWMutex Name string Pronouns string + Description string + Avatar string + ProfileBg string PasswordHash string CreatedAt time.Time WsConn *websocket.Conn @@ -36,6 +39,12 @@ type User struct { Color *Rgba } +type UserProfileUpdateList struct { + Pronouns bool + Description bool + Color bool +} + type Connection struct { Mu sync.RWMutex `json:"-"` Id uuid.UUID `json:"id"` diff --git a/todo.txt b/todo.txt index 2fe52f4..ba832f9 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,4 @@ more user customization (avatar, banners, desc) -add hubs \ No newline at end of file +add hubs +Alan's sun \ No newline at end of file