package event import ( "encoding/json" "fmt" "time" "github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/matrix/signing" ) // Builder assembles a new event for this server to send. // // The caller fills in what the event says; Build fills in what the room // version says — where the redaction target goes, whether the create event // carries a room ID — then hashes, signs and validates the result. The // roomserver (PLAN.md phase 3) is the only thing that should build events, // because it is the only thing that knows the right prev_events, depth and // auth_events. type Builder struct { // RoomID is the room the event belongs to. It must be empty for an // m.room.create event in a room version where the room ID is derived // from that event; use RoomIDOfCreateEvent on the built event to find // out what the room ID turned out to be. RoomID string // Type is the event type, such as "m.room.message". Type string // StateKey makes this a state event when it is non-nil. The empty // string is a perfectly good state key, which is why this is a pointer. StateKey *string // Sender is the user ID sending the event. Its server name is the one // the event is signed as, since an event must be signed by its // sender's server. Sender string // Content is the event content. An empty Content becomes {}. Content json.RawMessage // PrevEvents and AuthEvents are the event IDs this event refers to. PrevEvents []string AuthEvents []string // Depth is one more than the greatest depth of the prev_events. Depth int64 // Redacts is the event an m.room.redaction event targets. Build puts // it wherever this room version keeps it, so callers do not have to // know which versions moved it into the content. Redacts string // Unsigned is attached to the event but covered by neither its hashes // nor its signatures, so anything here can be changed in transit by // any server that passes the event on. Unsigned json.RawMessage } // Build hashes, signs and validates the event. // // now becomes origin_server_ts. It is passed in rather than read from the // clock so that tests are deterministic and so that a caller sending a // batch can stamp them all alike. func (b *Builder) Build(now time.Time, key *signing.KeyPair, rules roomversion.Rules) (*Event, error) { serverName, err := id.ServerNameOf(b.Sender) if err != nil { return nil, fmt.Errorf("build event: sender: %w", err) } if b.Type == "" { return nil, fmt.Errorf("build event: type is required") } if b.Depth < 0 { return nil, fmt.Errorf("build event: depth %d is negative", b.Depth) } obj := map[string]json.RawMessage{} set := func(k string, v any) error { raw, err := json.Marshal(v) if err != nil { return fmt.Errorf("build event: %s: %w", k, err) } obj[k] = raw return nil } content := b.Content if len(content) == 0 { content = json.RawMessage("{}") } if b.Redacts != "" { if content, err = b.placeRedacts(content, obj, rules); err != nil { return nil, err } } if err := b.setRoomID(obj, rules); err != nil { return nil, err } for k, v := range map[string]any{ "type": b.Type, "sender": b.Sender, "depth": b.Depth, "origin_server_ts": now.UnixMilli(), // prev_events and auth_events are required, and must be [] rather // than absent when there are none — which is the case for the // create event and for nothing else. "prev_events": orEmpty(b.PrevEvents), "auth_events": orEmpty(b.AuthEvents), } { if err := set(k, v); err != nil { return nil, err } } obj["content"] = content if b.StateKey != nil { if err := set("state_key", *b.StateKey); err != nil { return nil, err } } if len(b.Unsigned) > 0 { obj["unsigned"] = b.Unsigned } raw, err := canonicaljson.Marshal(obj) if err != nil { return nil, fmt.Errorf("build event: %w", err) } signed, err := hashAndSign(raw, rules, serverName, key) if err != nil { return nil, fmt.Errorf("build event: %w", err) } // Parse is the one gate every event goes through, built here or // received over federation, so a bug in the builder surfaces as a // failure to build rather than as an invalid event in a room. return Parse(signed, rules) } // setRoomID writes the room_id property, or leaves it out for a create // event in a room version that derives the room ID from it. func (b *Builder) setRoomID(obj map[string]json.RawMessage, rules roomversion.Rules) error { if rules.RoomIDIsCreateEventID && b.Type == TypeCreate { if b.RoomID != "" { return fmt.Errorf("build event: an m.room.create event in a version %s room must not have a room_id; it becomes the room ID", rules.Version) } return nil } if err := id.ValidateRoomID(b.RoomID); err != nil { return fmt.Errorf("build event: room_id: %w", err) } raw, err := json.Marshal(b.RoomID) if err != nil { return fmt.Errorf("build event: room_id: %w", err) } obj["room_id"] = raw return nil } // placeRedacts puts Redacts where this room version keeps it: in the // content from room version 11, at the top level before that. func (b *Builder) placeRedacts(content json.RawMessage, obj map[string]json.RawMessage, rules roomversion.Rules) (json.RawMessage, error) { if err := id.ValidateEventID(b.Redacts); err != nil { return nil, fmt.Errorf("build event: redacts: %w", err) } if !rules.RedactsInContent { raw, err := json.Marshal(b.Redacts) if err != nil { return nil, fmt.Errorf("build event: redacts: %w", err) } obj["redacts"] = raw return content, nil } var parsed map[string]json.RawMessage if err := json.Unmarshal(content, &parsed); err != nil { return nil, fmt.Errorf("build event: content: %w", err) } if parsed == nil { parsed = map[string]json.RawMessage{} } raw, err := json.Marshal(b.Redacts) if err != nil { return nil, fmt.Errorf("build event: redacts: %w", err) } parsed["redacts"] = raw out, err := json.Marshal(parsed) if err != nil { return nil, fmt.Errorf("build event: content: %w", err) } return out, nil } // orEmpty turns a nil slice into an empty one, so that it encodes as [] // rather than null. func orEmpty(s []string) []string { if s == nil { return []string{} } return s }