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/ - 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 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(`{}`)) }