package roomserver import ( "context" "encoding/json" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/auth" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/storage" ) // SendEventRequest sends one event into a room on a user's behalf. type SendEventRequest struct { RoomID string // Sender is the full user ID sending the event. Sender string Type string // StateKey makes this a state event when it is non-nil. StateKey *string Content json.RawMessage // Transaction, when set, makes the send idempotent: a retry with the // same transaction ID returns the event the first attempt created. // State events have no transaction ID in the Client-Server API, which // is why this is optional. Transaction *storage.TransactionRef } // SendEvent sends a message or state event into a room. // // It does no authorisation of its own, deliberately: whether a user may // send a given type into a given room is a question about the room's power // levels and membership, and the one place that is answered correctly is // the authorisation rules inside the pipeline. A handler-level check here // would be a second, worse copy of them. // // The one thing it does refuse up front is m.room.create. The rules would // refuse it too — a second create event in a room is rejected — but the // error a client gets for "you cannot re-create a room" is more use than // the one for "rule 2 said no". func (r *API) SendEvent(ctx context.Context, req SendEventRequest) (string, error) { if req.Type == event.TypeCreate { return "", fmt.Errorf("%w: a room's m.room.create event is sent by createRoom and cannot be replaced", ErrForbidden) } if req.Type == event.TypeMember { // A membership change through the state endpoint is legitimate and // the rules handle it, but the membership value still has to be one // the rules know: content that is not a membership at all would // otherwise become an event nobody can interpret. if err := checkMembershipContent(req.Content); err != nil { return "", err } } sent, err := r.send(ctx, sendRequest{ RoomID: req.RoomID, Type: req.Type, StateKey: req.StateKey, Sender: req.Sender, Content: req.Content, Transaction: req.Transaction, }) if err != nil { return "", err } return sent.EventID(), nil } // checkMembershipContent rejects an m.room.member content whose membership // is missing or not one of the five the spec defines. func checkMembershipContent(content json.RawMessage) error { var body struct { Membership string `json:"membership"` } if err := json.Unmarshal(content, &body); err != nil { return fmt.Errorf("%w: an m.room.member content needs a membership", ErrInvalidContent) } switch body.Membership { case event.MembershipJoin, event.MembershipLeave, event.MembershipInvite, event.MembershipBan, event.MembershipKnock: return nil case "": return fmt.Errorf("%w: an m.room.member content needs a membership", ErrInvalidContent) default: return fmt.Errorf("%w: %q is not a membership", ErrInvalidContent, body.Membership) } } // RedactRequest asks to redact an event. type RedactRequest struct { RoomID string // Sender is the full user ID sending the redaction. Sender string // EventID is the event to redact. EventID string Reason string Transaction *storage.TransactionRef } // Redact redacts an event. // // This is two decisions, not one, and the spec keeps them apart. The first // is whether the m.room.redaction event may be in the room at all, which // the authorisation rules answer and which is permissive: almost any member // may send one, because a server receiving a redaction for an event it has // never seen still has to be able to store it. The second is whether the // redaction takes effect on its target, which needs both events in hand and // is auth.ShouldApplyRedaction's job — either the sender has the redact // level, or the redaction comes from the same server as the event, because // a server may always take back its own users' events. // // Phase 3 only ever has both events, since every event in a room is local. // A redaction whose target has not arrived is a federation case. func (r *API) Redact(ctx context.Context, req RedactRequest) (string, error) { // The room is read first so that a room this server does not have is a // 404 before anything is built, and so that a room whose version is no // longer supported says so. room, err := r.store.Room(ctx, req.RoomID) if err != nil { return "", err } if _, err := r.rules(room); err != nil { return "", err } // The target is read before the redaction is built so that redacting // an event that does not exist is a 404 rather than an event in the // room that refers to nothing. if _, err := r.store.Event(ctx, req.RoomID, req.EventID); err != nil { return "", err } content := map[string]any{} if req.Reason != "" { content["reason"] = req.Reason } redaction, err := r.send(ctx, sendRequest{ RoomID: req.RoomID, Type: event.TypeRedaction, Sender: req.Sender, Content: mustMarshal(content), Redacts: req.EventID, Transaction: req.Transaction, }) if err != nil { return "", err } return redaction.EventID(), nil } // prepareRedaction decides whether a redaction takes effect and, if it // does, puts the target's redacted form into the write. // // It runs inside persist, within the same transaction as the redaction // event itself, so that a client which gets an event ID back can always // read the stripped event. func (r *API) prepareRedaction(ctx context.Context, write *storage.RoomWrite, req persistRequest) error { storedTarget, err := r.store.Event(ctx, req.Room.ID, req.RedactsEventID) if isNotFound(err) { // Over federation a redaction can arrive before its target, and the // right answer is to keep it and apply it later. Locally the target // was read before the redaction was built, so this means it was // purged in between. r.log.Info("redaction target is not here", "room_id", req.Room.ID, "redacts", req.RedactsEventID) return nil } else if err != nil { return err } if storedTarget.RedactedBy != "" { // Already redacted. The event stays as it is: a second redaction // has nothing left to remove. return nil } target, err := parseEvent(storedTarget, req.Rules) if err != nil { return err } applies, err := r.redactionApplies(ctx, req.Room, req.Rules, req.Event, target) if err != nil { return err } if !applies { // The event is in the room and has no effect, which is what the // spec asks for. It is worth a log line: from the client's side a // redaction that returns an event ID and changes nothing looks // exactly like one that worked. r.log.Info("accepted a redaction that does not apply", "room_id", req.Room.ID, "event_id", req.Event.EventID(), "redacts", req.RedactsEventID, "sender", req.Event.Sender()) return nil } redacted, err := target.Redact() if err != nil { return fmt.Errorf("redacting %s: %w", req.RedactsEventID, err) } write.Redacts = req.RedactsEventID write.RedactedJSON = redacted.JSON() return nil } // redactionApplies runs the spec's "should this redaction be applied" test. func (r *API) redactionApplies(ctx context.Context, room storage.Room, rules roomversion.Rules, redaction, target *event.Event) (bool, error) { st, err := r.currentState(ctx, room, rules) if err != nil { return false, err } create := st.Create() if create == nil { return false, fmt.Errorf("%w: %s has no create event in its state", ErrRejected, room.ID) } powerLevels, err := auth.ResolvePowerLevels(st.PowerLevels(), create, rules) if err != nil { return false, fmt.Errorf("%w: %v", ErrRejected, err) } return auth.ShouldApplyRedaction(redaction, target, powerLevels), nil }