Files
Sneedchat-Matrix-Bridge-Go/matrix/appservice_server.go
2025-11-18 02:07:08 -05:00

172 lines
4.7 KiB
Go

package matrix
import (
"encoding/json"
"log"
"net/http"
"strings"
"local/sneedchatbridge/config"
)
// StartAppserviceServer starts the Appservice HTTP server on the configured
// listen address. Synapse will POST room events here as transactions, and
// GET user queries (ghost user checks).
func (b *Bridge) StartAppserviceServer(cfg *config.Config) error {
mux := http.NewServeMux()
// -----------------------------------------------------------
// /transactions/<txn_id> - Synapse pushes events here
// -----------------------------------------------------------
mux.HandleFunc("/_matrix/appservice/v1/transactions/", func(w http.ResponseWriter, r *http.Request) {
if !b.checkAppserviceAuth(r, cfg.MatrixAppserviceToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if r.Method != http.MethodPut && r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
b.handleTransaction(w, r)
})
// -----------------------------------------------------------
// /users/@user:domain - Synapse asks if ghost user is allowed
// -----------------------------------------------------------
mux.HandleFunc("/_matrix/appservice/v1/users/", func(w http.ResponseWriter, r *http.Request) {
if !b.checkAppserviceAuth(r, cfg.MatrixAppserviceToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
b.handleUserQuery(w, r, cfg)
})
addr := cfg.AppserviceListenAddr
log.Printf("🟢 Matrix Appservice HTTP server listening on %s", addr)
return http.ListenAndServe(addr, mux)
}
//
// AUTHENTICATION
//
// checkAppserviceAuth enforces authorization using the appservice token
// provided in registration.yaml as hs_token and in .env as MATRIX_APPSERVICE_TOKEN.
func (b *Bridge) checkAppserviceAuth(r *http.Request, token string) bool {
// Try ?access_token=...
if q := r.URL.Query().Get("access_token"); q != "" {
return q == token
}
// Try Authorization: Bearer <token>
if h := r.Header.Get("Authorization"); h != "" {
if strings.HasPrefix(h, "Bearer ") {
return strings.TrimPrefix(h, "Bearer ") == token
}
}
return false
}
//
// TRANSACTIONS
//
// handleTransaction receives a list of events from Synapse.
// Synapse POSTs a JSON body like:
// { "events": [ { ... }, { ... } ] }
func (b *Bridge) handleTransaction(w http.ResponseWriter, r *http.Request) {
var payload struct {
Events []map[string]interface{} `json:"events"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
log.Printf("❌ Appservice transaction decode error: %v", err)
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
for _, ev := range payload.Events {
b.routeEvent(ev)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("{}"))
}
// routeEvent takes a raw Matrix event (map[string]interface{})
// and converts it into a strongly-typed RoomEvent struct,
// then forwards it to the bridge core.
func (b *Bridge) routeEvent(ev map[string]interface{}) {
evType, _ := ev["type"].(string)
roomID, _ := ev["room_id"].(string)
sender, _ := ev["sender"].(string)
eventID, _ := ev["event_id"].(string)
// Extract content if present
content := map[string]interface{}{}
if c, ok := ev["content"].(map[string]interface{}); ok {
content = c
}
// Extract unsigned if present
unsigned := map[string]interface{}{}
if u, ok := ev["unsigned"].(map[string]interface{}); ok {
unsigned = u
}
// Redaction ID if present
redacts, _ := ev["redacts"].(string)
// Assemble our internal event struct
re := RoomEvent{
RoomID: roomID,
EventID: eventID,
Sender: sender,
Type: evType,
Content: content,
Unsigned: unsigned,
Redacts: redacts,
}
b.HandleMatrixEvent(re)
}
//
// USER QUERY ENDPOINT
//
// handleUserQuery answers the Appservice ghost-user existence query.
// Synapse queries:
//
// GET /_matrix/appservice/v1/users/@username:sneedchat.kiwifarms.net
//
// Returning `{}` approves ghost user creation.
// Returning 404 denies.
func (b *Bridge) handleUserQuery(w http.ResponseWriter, r *http.Request, cfg *config.Config) {
path := r.URL.Path
parts := strings.Split(path, "/")
if len(parts) == 0 {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
mxid := parts[len(parts)-1]
if mxid == "" {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// Allow only the configured ghost-user domain
if !strings.HasSuffix(mxid, ":"+cfg.MatrixGhostUserDomain) {
http.NotFound(w, r)
return
}
// Approve creation
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{}`))
}