diff --git a/go-socket b/go-socket index 84467a3..c1ed7a7 100755 Binary files a/go-socket and b/go-socket differ diff --git a/go.mod b/go.mod index e4c6aa1..7f83720 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) 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/main.go b/main.go index b8eefc7..caffed1 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,9 @@ import ( "context" "log" "net/http" + "strconv" + "go-socket/packages/config" "go-socket/packages/httpRequest" "go-socket/packages/minio" "go-socket/packages/postgresql" @@ -21,6 +23,7 @@ func withCORS(h http.HandlerFunc) http.HandlerFunc { func main() { ctx := context.Background() + config.LoadConfFile() postgresql.Init(ctx) minio.Init(ctx) @@ -57,5 +60,5 @@ func main() { http.HandleFunc("GET /ws", wsServer.ServeWsConnection) log.Println("beep boop; server server started") - log.Fatal(http.ListenAndServe(":8080", nil)) + log.Fatal(http.ListenAndServe(":"+strconv.Itoa(int(config.Port)), nil)) } diff --git a/packages/config/config.go b/packages/config/config.go new file mode 100644 index 0000000..413d60e --- /dev/null +++ b/packages/config/config.go @@ -0,0 +1,65 @@ +package config + +import ( + "log/slog" + "os" + "path/filepath" + "time" + + "github.com/BurntSushi/toml" +) + +// Array-size constants — must be compile-time values; cannot be overridden via config file. +const ( + MaxDirectMsgCache uint32 = 32 + MaxHubMsgCache uint32 = 32 +) + +var ( + Port uint32 = 8080 + FileStorageBucketName string = "communicator" + MaxRequestBytes uint32 = 4 << 10 + MaxRequestWithFileBytes uint32 = 1 << 30 + MaxRequestWithAvatarBytes uint = 1 << 20 + MaxRequestWithProfileBgBytes uint = 4 << 20 + FileProcessingPartBytes uint64 = 12 << 20 + FileProcessingThreads uint = 3 + FileDownloadLinkTtl time.Duration = 24 * time.Hour +) + +type configFile struct { + Port uint32 `toml:"port"` + MaxDirectMsgCache uint32 `toml:"max_direct_messages_cache"` + MaxHubMsgCache uint32 `toml:"max_hub_msg_cache"` + FileStorageBucketName string `toml:"file_storage_bucket_name"` + MaxRequestBytes uint32 `toml:"max_request_bytes"` + MaxRequestWithFileBytes uint32 `toml:"max_request_with_file_bytes"` + MaxRequestWithAvatarBytes uint `toml:"max_request_with_avatar_bytes"` + MaxRequestWithProfileBgBytes uint `toml:"max_request_with_profile_bg_bytes"` + FileProcessingPartBytes uint64 `toml:"file_processing_part_bytes"` + FileProcessingThreads uint `toml:"file_processing_threads"` + FileDownloadLinkTtl time.Duration `toml:"file_download_link_ttl"` +} + +func LoadConfFile() { + path, err := os.Executable() + if err != nil { + panic(err) + } + + var cfg configFile + if _, err := toml.DecodeFile("config.toml", &cfg); err != nil { + slog.Warn("Failed to load config.toml. Default values will be used", "path", filepath.Dir(path)+"/config.toml") + return + } + + Port = cfg.Port + FileStorageBucketName = cfg.FileStorageBucketName + MaxRequestBytes = cfg.MaxRequestBytes + MaxRequestWithFileBytes = cfg.MaxRequestWithFileBytes + MaxRequestWithAvatarBytes = cfg.MaxRequestWithAvatarBytes + MaxRequestWithProfileBgBytes = cfg.MaxRequestWithProfileBgBytes + FileProcessingPartBytes = cfg.FileProcessingPartBytes + FileProcessingThreads = cfg.FileProcessingThreads + FileDownloadLinkTtl = cfg.FileDownloadLinkTtl +} diff --git a/packages/convertions/convertions.go b/packages/convertions/convertions.go index 9311159..f476527 100644 --- a/packages/convertions/convertions.go +++ b/packages/convertions/convertions.go @@ -17,28 +17,28 @@ func StringToUint32(s string) (uint32, error) { return uint32(v), err } -func StringToRgba(str string) (*types.Rgba, error) { +func StringToRgba(str string) (types.Rgba, error) { parts := strings.SplitN(str, ",", 5) if len(parts) != 4 { - return nil, fmt.Errorf("invalid rgba") + return types.Rgba{}, fmt.Errorf("invalid rgba") } - rgba := &types.Rgba{} + rgba := types.Rgba{} for i, p := range parts { n, err := strconv.ParseUint(strings.TrimSpace(p), 10, 8) if err != nil { - return nil, fmt.Errorf("invalid component %d: %w", i, err) + return types.Rgba{}, fmt.Errorf("invalid component %d: %w", i, err) } rgba[i] = uint8(n) } return rgba, nil } -func RgbaToUint32(r *types.Rgba) uint32 { +func RgbaToUint32(r types.Rgba) uint32 { return uint32(r[0])<<24 | uint32(r[1])<<16 | uint32(r[2])<<8 | uint32(r[3]) } -func Uint32ToRgba(v uint32) *types.Rgba { - return &types.Rgba{uint8(v >> 24), uint8(v >> 16), uint8(v >> 8), uint8(v)} +func Uint32ToRgba(v uint32) types.Rgba { + return types.Rgba{uint8(v >> 24), uint8(v >> 16), uint8(v >> 8), uint8(v)} } func StringToUuid(str string) (uuid.UUID, error) { diff --git a/packages/globals/globals.go b/packages/globals/globals.go deleted file mode 100644 index e516b33..0000000 --- a/packages/globals/globals.go +++ /dev/null @@ -1,16 +0,0 @@ -package globals - -import "time" - -const ( - MaxDirectMsgCache uint32 = 32 - MaxHubMsgCache - FileStorageBucketName string = "communicator" - MaxRequestBytes uint32 = 4 << 10 - MaxRequestWithFileBytes uint32 = 1 << 30 - MaxRequestWithAvatarBytes uint = 1 << 20 - MaxRequestWithProfileBgBytes uint = 4 << 20 - FileProcessingPartBytes uint64 = 12 << 20 - FileProcessingThreads uint = 3 - FileDownloadLinkTtl time.Duration = 24 * time.Hour -) diff --git a/packages/httpRequest/connectionsAndDms.go b/packages/httpRequest/connectionsAndDms.go index fb89119..7d3b57b 100644 --- a/packages/httpRequest/connectionsAndDms.go +++ b/packages/httpRequest/connectionsAndDms.go @@ -11,8 +11,8 @@ import ( "go-socket/packages/Enums/ConnectionState" "go-socket/packages/Enums/WsEventType" "go-socket/packages/cache" + "go-socket/packages/config" "go-socket/packages/convertions" - "go-socket/packages/globals" "go-socket/packages/postgresql" "go-socket/packages/types" "go-socket/packages/wsServer" @@ -158,7 +158,7 @@ func HandleUserGetConnectionMessages(response http.ResponseWriter, request *http messagesCap, err := convertions.StringToUint32(request.URL.Query().Get("messages")) if err != nil { - messagesCap = globals.MaxDirectMsgCache + messagesCap = config.MaxDirectMsgCache } buffer, bufferSize := conn.GetSortedMessagesBuff() diff --git a/packages/httpRequest/files.go b/packages/httpRequest/files.go index c9194c6..e304e53 100644 --- a/packages/httpRequest/files.go +++ b/packages/httpRequest/files.go @@ -5,8 +5,8 @@ import ( "net/http" "strings" + "go-socket/packages/config" "go-socket/packages/convertions" - "go-socket/packages/globals" "go-socket/packages/minio" ) @@ -22,9 +22,9 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ return } - request.Body = http.MaxBytesReader(response, request.Body, int64(globals.MaxRequestWithFileBytes)) + request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithFileBytes)) - if err = request.ParseMultipartForm(int64(globals.MaxRequestBytes)); err != nil { + if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil { http.Error(response, "invalid multipart form", http.StatusBadRequest) return } diff --git a/packages/httpRequest/helper.go b/packages/httpRequest/helper.go index 1ae998e..8cf209b 100644 --- a/packages/httpRequest/helper.go +++ b/packages/httpRequest/helper.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "go-socket/packages/globals" + "go-socket/packages/config" ) type postType uint8 @@ -21,13 +21,13 @@ func validCheckWithResponseOnFail(response *http.ResponseWriter, request *http.R var maxSize int64 switch pt { case file: - maxSize = int64(globals.MaxRequestWithFileBytes) + maxSize = int64(config.MaxRequestWithFileBytes) case avatar: - maxSize = int64(globals.MaxRequestWithAvatarBytes) + maxSize = int64(config.MaxRequestWithAvatarBytes) case profileBg: - maxSize = int64(globals.MaxRequestWithProfileBgBytes) + maxSize = int64(config.MaxRequestWithProfileBgBytes) default: - maxSize = int64(globals.MaxRequestBytes) + maxSize = int64(config.MaxRequestBytes) } if request.ContentLength > maxSize { diff --git a/packages/httpRequest/user.go b/packages/httpRequest/user.go index 1407c1d..ae373fe 100644 --- a/packages/httpRequest/user.go +++ b/packages/httpRequest/user.go @@ -5,8 +5,8 @@ import ( "net/http" "time" + "go-socket/packages/config" "go-socket/packages/convertions" - "go-socket/packages/globals" "go-socket/packages/minio" "go-socket/packages/cache" @@ -202,9 +202,9 @@ func HandleUserModAvatar(response http.ResponseWriter, request *http.Request) { return } - request.Body = http.MaxBytesReader(response, request.Body, int64(globals.MaxRequestWithAvatarBytes)) + request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithAvatarBytes)) - if err = request.ParseMultipartForm(int64(globals.MaxRequestBytes)); err != nil { + if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil { http.Error(response, "invalid multipart form", http.StatusBadRequest) return } @@ -265,9 +265,9 @@ func HandleUserModProfileBg(response http.ResponseWriter, request *http.Request) return } - request.Body = http.MaxBytesReader(response, request.Body, int64(globals.MaxRequestWithProfileBgBytes)) + request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithProfileBgBytes)) - if err = request.ParseMultipartForm(int64(globals.MaxRequestBytes)); err != nil { + if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil { http.Error(response, "invalid multipart form", http.StatusBadRequest) return } diff --git a/packages/minio/minio.go b/packages/minio/minio.go index 4418cb0..7208937 100644 --- a/packages/minio/minio.go +++ b/packages/minio/minio.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - "go-socket/packages/globals" + "go-socket/packages/config" "github.com/google/uuid" "github.com/minio/minio-go/v7" @@ -68,15 +68,15 @@ func Init(ctx context.Context) { panic(err) } - exists, err := minClient.BucketExists(ctx, globals.FileStorageBucketName) + exists, err := minClient.BucketExists(ctx, config.FileStorageBucketName) if err != nil { panic(err) } if !exists { - err = minClient.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{}) + err = minClient.MakeBucket(ctx, config.FileStorageBucketName, minio.MakeBucketOptions{}) if err != nil { - exists, checkErr := minClient.BucketExists(ctx, globals.FileStorageBucketName) + exists, checkErr := minClient.BucketExists(ctx, config.FileStorageBucketName) if checkErr != nil || !exists { panic(err) } @@ -87,22 +87,22 @@ func Init(ctx context.Context) { func Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string, metadata map[string]string) error { opt := minio.PutObjectOptions{ ContentType: contentType, - PartSize: globals.FileProcessingPartBytes, - NumThreads: globals.FileProcessingThreads, + PartSize: config.FileProcessingPartBytes, + NumThreads: config.FileProcessingThreads, UserMetadata: metadata, } opt.SetMatchETagExcept("*") - _, err := minClient.PutObject(ctx, globals.FileStorageBucketName, key, body, size, opt) + _, err := minClient.PutObject(ctx, config.FileStorageBucketName, key, body, size, opt) return err } func GetDownloadUrlAndMetadata(ctx context.Context, key string) (*url.URL, map[string]string, error) { - info, err := minClient.StatObject(ctx, globals.FileStorageBucketName, key, minio.StatObjectOptions{}) + info, err := minClient.StatObject(ctx, config.FileStorageBucketName, key, minio.StatObjectOptions{}) if err != nil { return nil, nil, err } - u, err := minClient.PresignedGetObject(ctx, globals.FileStorageBucketName, key, globals.FileDownloadLinkTtl, nil) + u, err := minClient.PresignedGetObject(ctx, config.FileStorageBucketName, key, config.FileDownloadLinkTtl, nil) if err != nil { return nil, nil, err } @@ -110,11 +110,11 @@ func GetDownloadUrlAndMetadata(ctx context.Context, key string) (*url.URL, map[s } func DoesExist(ctx context.Context, key string) bool { - _, err := minClient.StatObject(ctx, globals.FileStorageBucketName, key, minio.StatObjectOptions{}) + _, err := minClient.StatObject(ctx, config.FileStorageBucketName, key, minio.StatObjectOptions{}) return err == nil } func Delete(ctx context.Context, key string) error { - err := minClient.RemoveObject(ctx, globals.FileStorageBucketName, key, minio.RemoveObjectOptions{}) + err := minClient.RemoveObject(ctx, config.FileStorageBucketName, key, minio.RemoveObjectOptions{}) return err } diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go index 6d99dd6..d3603e2 100644 --- a/packages/postgresql/postgresql.go +++ b/packages/postgresql/postgresql.go @@ -76,6 +76,9 @@ func Init(ctx context.Context) { CREATE TABLE IF NOT EXISTS hubs () id UUID PRIMARY KEY DEFAULT gen_random_uuid(), creator_id UUID NOT NULL REFERENCES users(id), + name TEXT NOT NULL, + allow_user_color BOOLEAN DEFAULT TRUE, + rgba BIGINT NOT NULL DEFAULT 0 CHECK (rgba BETWEEN 0 AND 4294967295), created_at TIMESTAMP NOT NULL DEFAULT NOW() `) if err != nil { @@ -118,7 +121,7 @@ func Init(ctx context.Context) { _, err = dbConn.Exec(ctx, ` CREATE TABLE IF NOT EXISTS hub_user_roles () user_id UUID PRIMARY KEY NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_id SMALLINT NOT NULL REFERENCES hub_roles(role_id) + role_id SMALLINT NOT NULL REFERENCES hub_roles(role_id) ON DELETE CASCADE `) if err != nil { panic(err) @@ -129,6 +132,7 @@ func Init(ctx context.Context) { hub_id UUID PRIMARY KEY NOT NULL REFERENCES hubs(id) ON DELETE CASCADE, role_id SMALLINT NOT NULL DEFAULT 0, name TEXT NOT NULL, + permissions INTEGER NOT NULL DEFAULT 0, rgba BIGINT NOT NULL DEFAULT 0 CHECK (rgba BETWEEN 0 AND 4294967295), `) if err != nil { @@ -316,3 +320,10 @@ func ConnectionGetMessagesBefore(ctx context.Context, before time.Time, connecti } return messages, rows.Err() } + +func HubCreate(ctx context.Context, hub *types.Hub, creator *types.User) error { + _, err := dbConn.Exec(ctx, ` + INSERT INTO hubs (id, creator_id, name, allow_user_color, rgba, created_at) VALUES ($1, $2, $3, $4, $5) + `, hub.Id, creator.Id, hub.Name, hub.AllowUserColor, convertions.RgbaToUint32(hub.Color), hub.CreatedAt) + return err +} diff --git a/packages/types/types.go b/packages/types/types.go index 33e6723..b69507b 100644 --- a/packages/types/types.go +++ b/packages/types/types.go @@ -8,7 +8,7 @@ import ( "go-socket/packages/Enums/ConnectionState" "go-socket/packages/Enums/WsEventType" - "go-socket/packages/globals" + "go-socket/packages/config" "github.com/coder/websocket" "github.com/google/uuid" @@ -17,11 +17,11 @@ import ( type Rgba [4]uint8 type Sha256Hash [sha256.Size]byte -func (r Rgba) GetRandom() *Rgba { +func (r Rgba) GetRandom() Rgba { for i := range r { r[i] = uint8(rand.IntN(256)) } - return &r + return r } type User struct { @@ -36,7 +36,7 @@ type User struct { WsConn *websocket.Conn `json:"-"` Id uuid.UUID `json:"-"` Connections map[uuid.UUID]*Connection `json:"-"` - Color *Rgba `json:"color"` + Color Rgba `json:"color"` } type UserProfileUpdateList struct { @@ -48,31 +48,31 @@ type UserProfileUpdateList struct { } type Connection struct { - Mu sync.RWMutex `json:"-"` - Id uuid.UUID `json:"id"` - CreatedAt time.Time `json:"createdAt"` - MessagesBuff [globals.MaxDirectMsgCache]*Message `json:"-"` - NextBuffIdx uint32 `json:"-"` - RequestorId uuid.UUID `json:"requestorId"` - RecipientId uuid.UUID `json:"recipientId"` - UserWantingToElevate uuid.UUID `json:"userWantingToElevate"` // TODO add to database - HaveOverflowed bool `json:"-"` - State ConnectionState.ConnectionState `json:"state"` + Mu sync.RWMutex `json:"-"` + Id uuid.UUID `json:"id"` + CreatedAt time.Time `json:"createdAt"` + MessagesBuff [config.MaxDirectMsgCache]*Message `json:"-"` + NextBuffIdx uint32 `json:"-"` + RequestorId uuid.UUID `json:"requestorId"` + RecipientId uuid.UUID `json:"recipientId"` + UserWantingToElevate uuid.UUID `json:"userWantingToElevate"` // TODO add to database + HaveOverflowed bool `json:"-"` + State ConnectionState.ConnectionState `json:"state"` } func (conn *Connection) AddMessageToBuff(message *Message) { conn.Mu.Lock() defer conn.Mu.Unlock() - conn.MessagesBuff[conn.NextBuffIdx%globals.MaxDirectMsgCache] = message + conn.MessagesBuff[conn.NextBuffIdx%config.MaxDirectMsgCache] = message conn.NextBuffIdx++ - if conn.NextBuffIdx >= globals.MaxDirectMsgCache { + if conn.NextBuffIdx >= config.MaxDirectMsgCache { conn.HaveOverflowed = true } } // GetSortedMessagesBuff returns arr, length -func (conn *Connection) GetSortedMessagesBuff() (*[globals.MaxDirectMsgCache]*Message, uint32) { +func (conn *Connection) GetSortedMessagesBuff() (*[config.MaxDirectMsgCache]*Message, uint32) { conn.Mu.RLock() defer conn.Mu.RUnlock() @@ -80,11 +80,11 @@ func (conn *Connection) GetSortedMessagesBuff() (*[globals.MaxDirectMsgCache]*Me return &conn.MessagesBuff, conn.NextBuffIdx } - sorted := new([globals.MaxDirectMsgCache]*Message) - for i := uint32(0); i < globals.MaxDirectMsgCache; i++ { - sorted[i] = conn.MessagesBuff[(conn.NextBuffIdx+i)%globals.MaxDirectMsgCache] + sorted := new([config.MaxDirectMsgCache]*Message) + for i := uint32(0); i < config.MaxDirectMsgCache; i++ { + sorted[i] = conn.MessagesBuff[(conn.NextBuffIdx+i)%config.MaxDirectMsgCache] } - return sorted, globals.MaxDirectMsgCache + return sorted, config.MaxDirectMsgCache } type ConnectionStatusSetData struct { @@ -138,6 +138,7 @@ const ( PermissionChangeRoleName PermissionChangeRoleColor PermissionChangeRolePermissions + PermissionOnlySelfRoleRemove // Channel group permissions PermissionAddChannelGroup @@ -156,21 +157,24 @@ const ( ) type Hub struct { - Mu sync.RWMutex `json:"-"` - Id uuid.UUID `json:"id"` - CreatedAt time.Time `json:"createdAt"` - MessagesBuff [globals.MaxHubMsgCache]*Message `json:"-"` - NextBuffIdx uint32 `json:"-"` - HaveOverflowed bool `json:"-"` - Users map[uuid.UUID]*HubUser `json:"-"` - Roles map[uint8]*HubRole `json:"-"` - Creator uuid.UUID `json:"creator"` + Mu sync.RWMutex `json:"-"` + Id uuid.UUID `json:"id"` + CreatedAt time.Time `json:"createdAt"` + MessagesBuff [config.MaxHubMsgCache]*Message `json:"-"` + NextBuffIdx uint32 `json:"-"` + HaveOverflowed bool `json:"-"` + Users map[uuid.UUID]*HubUser `json:"-"` + Roles map[uint8]*HubRole `json:"-"` + Creator uuid.UUID `json:"creator"` + Name string `json:"name"` + Color Rgba `json:"color"` + AllowUserColor bool `json:"allowUserColor"` } type HubChannelGroup struct { Id uuid.UUID `json:"id"` Name string `json:"name"` - Color string `json:"color"` + Color Rgba `json:"color"` } type HubChannel struct { @@ -190,6 +194,7 @@ type HubRole struct { Name string `json:"role"` Id uint8 `json:"id"` RolePermission RolePermission `json:"rolePermission"` + Color Rgba `json:"color"` } func (h HubRole) GrantPermission(r RolePermission) {