user no stays in cache forever, fix some hubs bugs and add new enpoints

This commit is contained in:
2026-05-04 14:03:02 +02:00
parent 22e2d18810
commit 015c79bf09
14 changed files with 260 additions and 34 deletions
BIN
View File
Binary file not shown.
+5 -1
View File
@@ -69,6 +69,10 @@ func main() {
http.HandleFunc("PUT /hub/join", withCORS(httpRequest.HandleHubJoin))
http.HandleFunc("PATCH /hub/name", withCORS(httpRequest.HandleHubSetName))
http.HandleFunc("PATCH /hub/color", withCORS(httpRequest.HandleHubSetColor))
http.HandleFunc("PATCH /hub/icon", withCORS(httpRequest.HandleHubSetIcon))
http.HandleFunc("GET /hub/icon", withCORS(httpRequest.HandleGetHubIcon))
http.HandleFunc("PATCH /hub/bg", withCORS(httpRequest.HandleHubSetBg))
http.HandleFunc("GET /hub/bg", withCORS(httpRequest.HandleGetHubBg))
http.HandleFunc("DELETE /hub", withCORS(httpRequest.HandleHubRemove))
http.HandleFunc("PATCH /hub/usercolorallowed", withCORS(httpRequest.HandleHubToggleUserColorAllowed))
http.HandleFunc("DELETE /hub/user", withCORS(httpRequest.HandleHubUserRemove))
@@ -93,7 +97,7 @@ func main() {
http.HandleFunc("POST /connection/message", withCORS(httpRequest.HandleDm))
http.HandleFunc("GET /ws", wsServer.ServeWsConnection)
http.Handle("GET /client/", http.StripPrefix("/client/", http.FileServer(http.Dir("machine-client"))))
http.Handle("GET /client/", http.StripPrefix("/client/", http.FileServer(http.Dir("test-client"))))
log.Println("beep boop; server server started")
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(int(config.Port)), nil))
+40 -12
View File
@@ -29,18 +29,12 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ
return
}
request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithFileBytes))
if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil {
http.Error(response, "invalid multipart form", http.StatusBadRequest)
return
}
conn, ok := getConnectionWithResponseOnFail(response, request.FormValue("connectionid"), user)
if !ok {
return
}
target := request.FormValue("target_id")
file, header, err := request.FormFile("file")
if err != nil {
http.Error(response, "missing file", http.StatusBadRequest)
@@ -49,11 +43,31 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ
defer file.Close()
contentType := header.Header.Get("Content-Type")
key := minio.GetKey(&minio.GetKeyOptions{
var key string
if conn, ok := getConnection(ctx, target, user); ok {
key = minio.GetKey(&minio.GetKeyOptions{
ConnectionId: conn.Id,
MimeType: contentType,
UploadType: minio.ConnectionFile,
})
} else if channel, ok := getChannelFromUser(user, target); ok {
channel.Mu.RLock()
perms := channel.UsersCachedPermissions[user.Id]
channel.Mu.RUnlock()
if !perms.CanMessage() {
http.Error(response, "forbidden", http.StatusForbidden)
return
}
key = minio.GetKey(&minio.GetKeyOptions{
ChannelId: channel.Id,
MimeType: contentType,
UploadType: minio.HubChannelFile,
})
} else {
http.Error(response, "cannot find target", http.StatusBadRequest)
return
}
if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{
"originalName": header.Filename,
@@ -322,13 +336,27 @@ func HandleAttachmentFileDownload(response http.ResponseWriter, request *http.Re
return
}
conn, ok := getConnectionWithResponseOnFail(response, request.FormValue("connectionid"), user)
if !ok {
target := request.URL.Query().Get("target_id")
key := request.URL.Query().Get("key")
var validPrefix string
if conn, ok := getConnection(ctx, target, user); ok {
validPrefix = string(minio.ConnectionFilePrefix) + conn.Id.String() + "/"
} else if channel, ok := getChannelFromUser(user, target); ok {
channel.Mu.RLock()
perms := channel.UsersCachedPermissions[user.Id]
channel.Mu.RUnlock()
if !perms.CanReadHistory() {
http.Error(response, "forbidden", http.StatusForbidden)
return
}
validPrefix = string(minio.HubChannelFilePrefix) + channel.Id.String() + "/"
} else {
http.Error(response, "cannot find target", http.StatusBadRequest)
return
}
key := request.URL.Query().Get("key")
if !strings.HasPrefix(key, string(minio.ConnectionFilePrefix)+conn.Id.String()+"/") {
if !strings.HasPrefix(key, validPrefix) {
http.Error(response, "no such file", http.StatusUnauthorized)
return
}
+39
View File
@@ -36,6 +36,45 @@ func getUserByToken(ctx context.Context, token string) (*types.User, error) {
return getUserById(ctx, userId)
}
func getConnection(ctx context.Context, connectionIdStr string, user *types.User) (*types.Connection, bool) {
connectionId, err := convertions.StringToUuid(connectionIdStr)
if err != nil {
return nil, false
}
if conn, ok := cache.GetConnection(user, connectionId); ok {
return conn, true
}
conn, err := postgresql.ConnectionGetById(ctx, connectionId)
if err != nil {
return nil, false
}
if conn.RequestorId != user.Id && conn.RecipientId != user.Id {
return nil, false
}
user.Mu.Lock()
user.Connections[conn.Id] = conn
user.Mu.Unlock()
return conn, true
}
func getChannelFromUser(user *types.User, channelIdStr string) (*types.HubChannel, bool) {
channelId, err := convertions.StringToUuid(channelIdStr)
if err != nil {
return nil, false
}
user.Mu.RLock()
defer user.Mu.RUnlock()
for _, hub := range user.Hubs {
hub.Mu.RLock()
ch, ok := hub.Channels[channelId]
hub.Mu.RUnlock()
if ok {
return ch, true
}
}
return nil, false
}
func getConnectionWithResponseOnFail(response http.ResponseWriter, connectionIdStr string, user *types.User) (*types.Connection, bool) {
connectionId, err := convertions.StringToUuid(connectionIdStr)
if err != nil {
+2
View File
@@ -41,6 +41,8 @@ func validCheckWithResponseOnFail(response http.ResponseWriter, request *http.Re
maxSize = int64(config.MaxRequestBytes)
}
request.Body = http.MaxBytesReader(response, request.Body, maxSize)
if request.ContentLength > maxSize {
io.Copy(io.Discard, request.Body)
http.Error(response, "Request too large", http.StatusRequestEntityTooLarge)
+69 -1
View File
@@ -551,6 +551,74 @@ func HandleHubSetBg(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(http.StatusCreated)
}
func HandleGetHubIcon(response http.ResponseWriter, request *http.Request) {
if !validCheckWithResponseOnFail(response, request, normal) {
return
}
ctx := request.Context()
_, _, hub, err := getHubUserIfValidWithResponseOnFail(ctx, response, request)
if err != nil {
return
}
if hub.IconUrl == "" {
http.Error(response, "hub has no icon", http.StatusNoContent)
return
}
url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, hub.IconUrl)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
iconData, err := json.Marshal(map[string]any{
"url": url.String(),
"metadata": meta,
})
if err != nil {
http.Error(response, "json error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(iconData)
}
func HandleGetHubBg(response http.ResponseWriter, request *http.Request) {
if !validCheckWithResponseOnFail(response, request, normal) {
return
}
ctx := request.Context()
_, _, hub, err := getHubUserIfValidWithResponseOnFail(ctx, response, request)
if err != nil {
return
}
if hub.BgUrl == "" {
http.Error(response, "hub has no background", http.StatusNoContent)
return
}
url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, hub.BgUrl)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
bgData, err := json.Marshal(map[string]any{
"url": url.String(),
"metadata": meta,
})
if err != nil {
http.Error(response, "json error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(bgData)
}
func HandleHubRemove(response http.ResponseWriter, request *http.Request) {
_, hub, _, _, ok := hubPermissionContext(response, request, normal, types.PermissionRemoveHub)
if !ok {
@@ -852,7 +920,7 @@ func HandleRoleSetColor(response http.ResponseWriter, request *http.Request) {
}
color, err := convertions.StringToRgba(request.FormValue("new_color"))
if err != nil {
http.Error(response, "invalid newcolor", http.StatusBadRequest)
http.Error(response, "invalid new_color", http.StatusBadRequest)
return
}
+3 -1
View File
@@ -26,17 +26,19 @@ const (
UserProfileBg
HubIcon
HubBackground
ChannelIcon
)
type DataTypePrefix string
const (
ConnectionFilePrefix DataTypePrefix = "connection/"
HubChannelFilePrefix DataTypePrefix = "hub/"
HubChannelFilePrefix DataTypePrefix = "hubChannel/"
UserAvatarPrefix DataTypePrefix = "userAvatar/"
UserProfileBgPrefix DataTypePrefix = "userProfileBg/"
HubIconPrefix DataTypePrefix = "hubIcon/"
HubBackgroundPrefix DataTypePrefix = "hubBackground/"
ChannelIconPrefix DataTypePrefix = "channelIcon/"
)
type GetKeyOptions struct {
+13
View File
@@ -181,6 +181,19 @@ func ConnectionDelete(ctx context.Context, conn *types.Connection) error {
return err
}
func ConnectionGetById(ctx context.Context, id uuid.UUID) (*types.Connection, error) {
conn := types.NewConn()
err := dbConn.QueryRow(ctx, `
SELECT id, requestor_id, recipient_id, state, created_at
FROM user_connections
WHERE id = $1
`, id).Scan(&conn.Id, &conn.RequestorId, &conn.RecipientId, &conn.State, &conn.CreatedAt)
if err != nil {
return nil, err
}
return conn, nil
}
func ConnectionsGetBelongingToUser(ctx context.Context, user *types.User) error {
rows, err := dbConn.Query(ctx, `
SELECT id, requestor_id, recipient_id, state, created_at
+2 -3
View File
@@ -38,7 +38,7 @@ type User struct {
WsConn *websocket.Conn `json:"-"`
Id uuid.UUID `json:"-"`
Connections map[uuid.UUID]*Connection `json:"-"`
Hubs map[uuid.UUID]*Hub `json:"-"`
Hubs map[uuid.UUID]*Hub `json:"hubs-to-delete"`
Color Rgba `json:"color"`
}
@@ -249,7 +249,7 @@ const (
CachedUserCanMessage
)
const CachedUserPermissionsAll = CachedUserCanMessage | CachedUserCanReadHistory | CachedUserCanReadHistory
const CachedUserPermissionsAll = CachedUserCanView | CachedUserCanReadHistory | CachedUserCanMessage
func (p *CachedUserPermissions) SetCanView() { *p |= CachedUserCanView }
func (p *CachedUserPermissions) ClearCanView() { *p &^= CachedUserCanView }
@@ -292,7 +292,6 @@ type HubRole struct {
Permissions Permissions `json:"permissions"`
Color Rgba `json:"color"`
Id uint8 `json:"id"`
BoundedGroup uint8 `json:"boundedGroup"` // BoundedGroup 0 for global
}
func (h *HubRole) GrantPermission(r Permissions) {
+1 -1
View File
@@ -32,7 +32,7 @@ func ServeWsConnection(responseWriter http.ResponseWriter, request *http.Request
var user = types.User{WsConn: connection}
var isAuthenticated bool
var ignoreCache bool
var ignoreCache bool = true
defer func() { closeConnection(&user, ignoreCache) }()
for {
@@ -53,6 +53,10 @@
<button data-form="get-users" onclick="showForm('get-users')">GET /users</button>
<button data-form="hub-set-name" onclick="showForm('hub-set-name')">PATCH /hub/name</button>
<button data-form="hub-set-color" onclick="showForm('hub-set-color')">PATCH /hub/color</button>
<button data-form="hub-set-icon" onclick="showForm('hub-set-icon')">PATCH /hub/icon</button>
<button data-form="get-hub-icon" onclick="showForm('get-hub-icon')">GET /hub/icon</button>
<button data-form="hub-set-bg" onclick="showForm('hub-set-bg')">PATCH /hub/bg</button>
<button data-form="get-hub-bg" onclick="showForm('get-hub-bg')">GET /hub/bg</button>
<button data-form="hub-toggle-color" onclick="showForm('hub-toggle-color')">PATCH /hub/usercolorallowed</button>
<button data-form="hub-user-rename" onclick="showForm('hub-user-rename')">PATCH /hub/user/name</button>
<button data-form="hub-self-rename" onclick="showForm('hub-self-rename')">PATCH /hub/self/name</button>
@@ -178,7 +182,7 @@
<!-- GET /file -->
<div class="form-content" id="fc-file-download">
<div class="field"><label>token</label><input id="fd-token" placeholder=""></div>
<div class="field"><label>connectionid</label><input id="fd-connectionid" placeholder="UUID"></div>
<div class="field"><label>target_id</label><input id="fd-targetid" placeholder="connection or hub UUID"></div>
<div class="field"><label>key</label><input id="fd-key" placeholder="returned by upload"></div>
<div class="form-actions"><button class="send" onclick="submitFileDownload()">Send</button></div>
</div>
@@ -286,6 +290,36 @@
<div class="form-actions"><button class="send" onclick="submit('hub-set-color')">Send</button></div>
</div>
<!-- PATCH /hub/icon -->
<div class="form-content" id="fc-hub-set-icon">
<div class="field"><label>token</label><input id="hsi-token" placeholder=""></div>
<div class="field"><label>hubid</label><input id="hsi-hubid" placeholder="UUID"></div>
<div class="field"><label>file</label><input id="hsi-file" type="file"></div>
<div class="form-actions"><button class="send" onclick="submitHubIconUpload()">Send</button></div>
</div>
<!-- GET /hub/icon -->
<div class="form-content" id="fc-get-hub-icon">
<div class="field"><label>token</label><input id="ghi-token" placeholder=""></div>
<div class="field"><label>hubid</label><input id="ghi-hubid" placeholder="UUID"></div>
<div class="form-actions"><button class="send" onclick="submit('get-hub-icon')">Send</button></div>
</div>
<!-- PATCH /hub/bg -->
<div class="form-content" id="fc-hub-set-bg">
<div class="field"><label>token</label><input id="hsb-token" placeholder=""></div>
<div class="field"><label>hubid</label><input id="hsb-hubid" placeholder="UUID"></div>
<div class="field"><label>file</label><input id="hsb-file" type="file"></div>
<div class="form-actions"><button class="send" onclick="submitHubBgUpload()">Send</button></div>
</div>
<!-- GET /hub/bg -->
<div class="form-content" id="fc-get-hub-bg">
<div class="field"><label>token</label><input id="ghbg-token" placeholder=""></div>
<div class="field"><label>hubid</label><input id="ghbg-hubid" placeholder="UUID"></div>
<div class="form-actions"><button class="send" onclick="submit('get-hub-bg')">Send</button></div>
</div>
<!-- DELETE /hub -->
<div class="form-content" id="fc-hub-remove">
<div class="field"><label>token</label><input id="hr-token" placeholder=""></div>
@@ -541,6 +575,10 @@
'get-users': { method:'GET', path:'/users', title:'GET /users — get multiple users by IDs', fields:[{id:'gus-token',dest:'header',name:'token'},{id:'gus-targetids',dest:'query',name:'target_ids'}] },
'hub-set-name': { method:'PATCH', path:'/hub/name', title:'PATCH /hub/name — set hub name', fields:[{id:'hsn-token',dest:'header',name:'token'},{id:'hsn-hubid',dest:'header',name:'hub_id'},{id:'hsn-newname',dest:'body',name:'new_name'}] },
'hub-set-color': { method:'PATCH', path:'/hub/color', title:'PATCH /hub/color — set hub color (R,G,B,A)', fields:[{id:'hsc-token',dest:'header',name:'token'},{id:'hsc-hubid',dest:'header',name:'hub_id'},{id:'hsc-newcolor',dest:'body',name:'new_color'}] },
'hub-set-icon': { title:'PATCH /hub/icon — set hub icon image' },
'get-hub-icon': { method:'GET', path:'/hub/icon', title:'GET /hub/icon — get hub icon URL', fields:[{id:'ghi-token',dest:'header',name:'token'},{id:'ghi-hubid',dest:'header',name:'hub_id'}] },
'hub-set-bg': { title:'PATCH /hub/bg — set hub background image' },
'get-hub-bg': { method:'GET', path:'/hub/bg', title:'GET /hub/bg — get hub background URL', fields:[{id:'ghbg-token',dest:'header',name:'token'},{id:'ghbg-hubid',dest:'header',name:'hub_id'}] },
'hub-remove': { method:'DELETE', path:'/hub', title:'DELETE /hub — remove hub', fields:[{id:'hr-token',dest:'header',name:'token'},{id:'hr-hubid',dest:'header',name:'hub_id'}] },
'hub-toggle-color': { method:'PATCH', path:'/hub/usercolorallowed', title:'PATCH /hub/usercolorallowed — toggle user color allowed', fields:[{id:'htc-token',dest:'header',name:'token'},{id:'htc-hubid',dest:'header',name:'hub_id'}] },
'hub-user-remove': { method:'DELETE', path:'/hub/user', title:'DELETE /hub/user — remove user from hub', fields:[{id:'hur-token',dest:'header',name:'token'},{id:'hur-hubid',dest:'header',name:'hub_id'},{id:'hur-targetid',dest:'query',name:'target_id'}] },
@@ -778,9 +816,9 @@
async function submitFileDownload() {
const token = document.getElementById('fd-token').value;
const connectionid = document.getElementById('fd-connectionid').value;
const targetId = document.getElementById('fd-targetid').value;
const key = document.getElementById('fd-key').value;
const query = new URLSearchParams({ connection_id: connectionid, key });
const query = new URLSearchParams({ target_id: targetId, key });
log('GET /file', query.toString(), 'log-info');
try {
const resp = await fetch(baseUrl() + '/file?' + query.toString(), { method: 'GET', headers: { token } });
@@ -845,6 +883,36 @@
} catch(e) { log('HTTP ERR', e.message, 'log-err'); }
}
async function submitHubIconUpload() {
const token = document.getElementById('hsi-token').value;
const hubid = document.getElementById('hsi-hubid').value;
const fileInput = document.getElementById('hsi-file');
if (!fileInput.files.length) { log('HTTP ERR', 'no file selected', 'log-err'); return; }
const form = new FormData();
form.append('file', fileInput.files[0]);
log('PATCH /hub/icon', 'file=' + fileInput.files[0].name, 'log-info');
try {
const resp = await fetch(baseUrl() + '/hub/icon', { method: 'PATCH', headers: { token, hub_id: hubid }, body: form });
const text = await resp.text();
log('HTTP ' + resp.status, text, resp.ok ? 'log-http' : 'log-err');
} catch(e) { log('HTTP ERR', e.message, 'log-err'); }
}
async function submitHubBgUpload() {
const token = document.getElementById('hsb-token').value;
const hubid = document.getElementById('hsb-hubid').value;
const fileInput = document.getElementById('hsb-file');
if (!fileInput.files.length) { log('HTTP ERR', 'no file selected', 'log-err'); return; }
const form = new FormData();
form.append('file', fileInput.files[0]);
log('PATCH /hub/bg', 'file=' + fileInput.files[0].name, 'log-info');
try {
const resp = await fetch(baseUrl() + '/hub/bg', { method: 'PATCH', headers: { token, hub_id: hubid }, body: form });
const text = await resp.text();
log('HTTP ' + resp.status, text, resp.ok ? 'log-http' : 'log-err');
} catch(e) { log('HTTP ERR', e.message, 'log-err'); }
}
async function submitGetUserProfileBg() {
const token = document.getElementById('gpb-token').value;
const userid = document.getElementById('gpb-userid').value;
+6 -3
View File
@@ -1,8 +1,11 @@
check hubs attachment working
add option to change join role
add channel icons
make hub persistent
when user not ws connected collect count of unread messages for each conn (add db table in future)
setting avatar and profilebg
check when mutex needed
change api endpoints and body/url/header to follow same case
fix cache
user banners