Initial Commit
This commit is contained in:
171
matrix/appservice_server.go
Normal file
171
matrix/appservice_server.go
Normal file
@@ -0,0 +1,171 @@
|
||||
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(`{}`))
|
||||
}
|
||||
Reference in New Issue
Block a user