Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
xmpp: Request an upload slot from the HTTP upload component
  • Loading branch information
selfhoster1312 committed Mar 1, 2026
commit 1030bc60c262e8750645e0887bdd52a705625ac9
16 changes: 14 additions & 2 deletions bridge/xmpp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/matterbridge-org/matterbridge/bridge/config"
"github.com/matterbridge-org/matterbridge/bridge/helper"
"github.com/rs/xid"
"github.com/xmppo/go-xmpp"
)

Expand Down Expand Up @@ -76,8 +77,19 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) {
// In this case, no need to reupload the file.
b.announceUploadedFile(msg.Channel+"@"+b.GetString("Muc"), msg.Username+fileInfo.Comment, fileInfo.Comment, fileInfo.URL)
} else {
// TODO
b.Log.Warn("OOB file upload unimplemented yet")
// The file received from other bridges is just a bunch of bytes in fileInfo.Data
// We need to upload it to the XMPP server's HTTP upload component.
// This is defined in XEP-0363: https://xmpp.org/extensions/xep-0363.html
//
// The steps are performed asynchronously:
//
// 1. Find the server's HTTP upload component (upon login, in HTTP_UPLOAD_DISCO steps)
// 2. Request an "upload slot" from the upload component (we are here)
// 3. Send a PUT request with the data to the remote HTTP "upload slot" (when receiving the slot)
//
// Steps 2 and 3 are commented as HTTP_UPLOAD_SLOT
fileId := xid.New().String()
go b.requestUploadSlot(fileId, &fileInfo)
}
}
}
Expand Down
58 changes: 58 additions & 0 deletions bridge/xmpp/helpers.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package bxmpp

import (
"fmt"
"mime"
"path"
"regexp"
"strconv"
"time"

"github.com/matterbridge-org/matterbridge/bridge/config"
"github.com/xmppo/go-xmpp"
Expand Down Expand Up @@ -98,3 +102,57 @@ func (b *Bxmpp) extractMaxSizeFromXFieldValue(value string) int64 {

return maxFileSize
}

// HTTP_UPLOAD_SLOT step 1
//
// Request an upload slot from the HTTP upload component, saving the file
// in the internal upload buffer for later processing.
//
// Will stall until the compoennt is advertised by the server, or until a timeout has been reached.
// This method must therefore be called from a background thread.
func (b *Bxmpp) requestUploadSlot(fileId string, fileInfo *config.FileInfo) {
retry := 0

httpUploadComponent := ""
for httpUploadComponent == "" {
retry += 1
if retry > 6 {
// No need to keep trying, the XMPP server apparently has no HTTP upload
// component configured.
b.Log.Warn("Abandoning file upload because XMPP server still hasn't advertised an HTTP upload component.")
break
}

b.Lock()
httpUploadComponent = b.httpUploadComponent
b.Unlock()

// Wait 5 seconds before next attempt
time.Sleep(5 * time.Second)
}

reg := regexp.MustCompile(`[^a-zA-Z0-9\+\-\_\.]+`)
fileNameEscaped := reg.ReplaceAllString(fileInfo.Name, "_")

// Guess the mime-type
mimeType := mime.TypeByExtension(path.Ext(fileInfo.Name))
if mimeType == "" {
mimeType = "application/octet-stream"
}

b.Log.Debugf("Requesting upload slot ID %s for %s (escaped) with mime-type %s", fileId, fileNameEscaped, mimeType)

request := fmt.Sprintf("<request xmlns='urn:xmpp:http:upload:0' filename='%s' size='%d' content-type='%s' />", fileNameEscaped, fileInfo.Size, mimeType)

_, err := b.xc.RawInformation(b.xc.JID(), httpUploadComponent, fileId, "get", request)
if err != nil {
b.Log.WithError(err).Warn("Failed to request upload slot")
return
}

// Save the FileInfo in the buffer to actually upload it later
// when we receive the upload slot.
b.Lock()
b.httpUploadBuffer[fileId] = fileInfo
b.Unlock()
}
27 changes: 27 additions & 0 deletions bridge/xmpp/xmpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ type Bxmpp struct {
httpUploadComponent string
// The max attachment size is discovered in the last step of HTTP_UPLOAD_DISCO.
httpUploadMaxSize int64
// Files are stored in this buffer so we can perform the uploads asynchronously
// without blocking the main thread:
//
// - request an upload slot and store the file in the buffer (HTTP_UPLOAD_SLOT step 1)
// - (matterbridge processes other messages)
// - receive the upload slot and perform the HTTP upload (HTTP_UPLOAD_SLOT step 2)
// - (matterbridge processes other messages)
// - receive upload confirmation and post the OOB URL (HTTP_UPLOAD_SLOT step 3)
//
// Note that in most cases, remote bridges will provide an attachment URL, no file
// will actually be uploaded on XMPP side, and this buffer will be untouched.
httpUploadBuffer map[string]*config.FileInfo
}

func New(cfg *bridge.Config) bridge.Bridger {
Expand All @@ -40,6 +52,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
xmppMap: make(map[string]string),
avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string),
httpUploadBuffer: make(map[string]*config.FileInfo),
}
}

Expand Down Expand Up @@ -353,6 +366,20 @@ func (b *Bxmpp) handleXMPP() error {
b.httpUploadMaxSize = foundSize
b.Unlock()
}
case xmpp.Slot:
// HTTP_UPLOAD_SLOT step 2
b.Log.Debugf("Received upload slot ID %s", v.ID)
b.Lock()
entry, ok := b.httpUploadBuffer[v.ID]
b.Unlock()

if !ok {
b.Log.Warnf("Received upload slot ID %s doesn't match a known file", v.ID)
continue
}

b.Log.Debugf("Preparing to upload file %s to %s", entry.Name, v.Put.Url)
// TODO: upload file to the upload slot, then share it in the chat
}
}
}
Expand Down