add configs from file, continue develpoment on hubs
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+11
-11
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+36
-31
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user