// Copyright 2022 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package perform import ( "context" "encoding/json" "fmt" "time" "github.com/matrix-org/dendrite/internal/eventutil" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" "github.com/sirupsen/logrus" ) type Upgrader struct { Cfg *config.RoomServer URSAPI api.RoomserverInternalAPI } // fledglingEvent is a helper representation of an event used when creating many events in succession. type fledglingEvent struct { Type string `json:"type"` StateKey string `json:"state_key"` Content interface{} `json:"content"` } // PerformRoomUpgrade upgrades a room from one version to another func (r *Upgrader) PerformRoomUpgrade( ctx context.Context, req *api.PerformRoomUpgradeRequest, res *api.PerformRoomUpgradeResponse, ) { res.NewRoomID, res.Error = r.performRoomUpgrade(ctx, req) if res.Error != nil { res.NewRoomID = "" logrus.WithContext(ctx).WithError(res.Error).Error("Room upgrade failed") } } func (r *Upgrader) performRoomUpgrade( ctx context.Context, req *api.PerformRoomUpgradeRequest, ) (string, *api.PerformError) { roomID := req.RoomID userID := req.UserID evTime := time.Now() // Return an immediate error if the room does not exist if err := r.validateRoomExists(ctx, roomID); err != nil { return "", &api.PerformError{ Code: api.PerformErrorNoRoom, Msg: "Error validating that the room exists", } } // 1. Check if the user is authorized to actually perform the upgrade (can send m.room.tombstone) if !r.userIsAuthorized(ctx, userID, roomID) { return "", &api.PerformError{ Code: api.PerformErrorNotAllowed, Msg: "You don't have permission to upgrade the room, power level too low.", } } // TODO (#267): Check room ID doesn't clash with an existing one, and we // probably shouldn't be using pseudo-random strings, maybe GUIDs? newRoomID := fmt.Sprintf("!%s:%s", util.RandomString(16), r.Cfg.Matrix.ServerName) // Get the existing room state for the old room. oldRoomReq := &api.QueryLatestEventsAndStateRequest{ RoomID: roomID, } oldRoomRes := &api.QueryLatestEventsAndStateResponse{} if err := r.URSAPI.QueryLatestEventsAndState(ctx, oldRoomReq, oldRoomRes); err != nil { return "", &api.PerformError{ Msg: fmt.Sprintf("Failed to get latest state: %s", err), } } // Make the tombstone event tombstoneEvent, pErr := r.makeTombstoneEvent(ctx, evTime, userID, roomID, newRoomID) if pErr != nil { return "", pErr } // Generate the initial events we need to send into the new room. This includes copied state events and bans // as well as the power level events needed to set up the room eventsToMake, pErr := r.generateInitialEvents(ctx, oldRoomRes, userID, roomID, string(req.RoomVersion), tombstoneEvent) if pErr != nil { return "", pErr } // 5. Send the tombstone event to the old room (must do this before we set the new canonical_alias) if pErr = r.sendHeaderedEvent(ctx, tombstoneEvent); pErr != nil { return "", pErr } // Send the setup events to the new room if pErr = r.sendInitialEvents(ctx, evTime, userID, newRoomID, string(req.RoomVersion), eventsToMake); pErr != nil { return "", pErr } // If the old room was public, make sure the new one is too if pErr = r.publishIfOldRoomWasPublic(ctx, roomID, newRoomID); pErr != nil { return "", pErr } // If the old room had a canonical alias event, it should be deleted in the old room if pErr = r.clearOldCanonicalAliasEvent(ctx, oldRoomRes, evTime, userID, roomID); pErr != nil { return "", pErr } // 4. Move local aliases to the new room if pErr = moveLocalAliases(ctx, roomID, newRoomID, userID, r.URSAPI); pErr != nil { return "", pErr } // 6. Restrict power levels in the old room if pErr = r.restrictOldRoomPowerLevels(ctx, evTime, userID, roomID); pErr != nil { return "", pErr } return newRoomID, nil } func (r *Upgrader) getRoomPowerLevels(ctx context.Context, roomID string) (*gomatrixserverlib.PowerLevelContent, *api.PerformError) { oldPowerLevelsEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ EventType: gomatrixserverlib.MRoomPowerLevels, StateKey: "", }) powerLevelContent, err := oldPowerLevelsEvent.PowerLevels() if err != nil { util.GetLogger(ctx).WithError(err).Error() return nil, &api.PerformError{ Msg: "powerLevel event was not actually a power level event", } } return powerLevelContent, nil } func (r *Upgrader) restrictOldRoomPowerLevels(ctx context.Context, evTime time.Time, userID, roomID string) *api.PerformError { restrictedPowerLevelContent, pErr := r.getRoomPowerLevels(ctx, roomID) if pErr != nil { return pErr } // From: https://spec.matrix.org/v1.2/client-server-api/#server-behaviour-16 // If possible, the power levels in the old room should also be modified to // prevent sending of events and inviting new users. For example, setting // events_default and invite to the greater of 50 and users_default + 1. restrictedDefaultPowerLevel := int64(50) if restrictedPowerLevelContent.UsersDefault+1 > restrictedDefaultPowerLevel { restrictedDefaultPowerLevel = restrictedPowerLevelContent.UsersDefault + 1 } restrictedPowerLevelContent.EventsDefault = restrictedDefaultPowerLevel restrictedPowerLevelContent.Invite = restrictedDefaultPowerLevel restrictedPowerLevelsHeadered, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, fledglingEvent{ Type: gomatrixserverlib.MRoomPowerLevels, StateKey: "", Content: restrictedPowerLevelContent, }) if resErr != nil { if resErr.Code == api.PerformErrorNotAllowed { util.GetLogger(ctx).WithField(logrus.ErrorKey, resErr).Warn("UpgradeRoom: Could not restrict power levels in old room") } else { return resErr } } else { if resErr = r.sendHeaderedEvent(ctx, restrictedPowerLevelsHeadered); resErr != nil { return resErr } } return nil } func moveLocalAliases(ctx context.Context, roomID, newRoomID, userID string, URSAPI api.RoomserverInternalAPI) *api.PerformError { var err error aliasReq := api.GetAliasesForRoomIDRequest{RoomID: roomID} aliasRes := api.GetAliasesForRoomIDResponse{} if err = URSAPI.GetAliasesForRoomID(ctx, &aliasReq, &aliasRes); err != nil { return &api.PerformError{ Msg: "Could not get aliases for old room", } } for _, alias := range aliasRes.Aliases { removeAliasReq := api.RemoveRoomAliasRequest{UserID: userID, Alias: alias} removeAliasRes := api.RemoveRoomAliasResponse{} if err = URSAPI.RemoveRoomAlias(ctx, &removeAliasReq, &removeAliasRes); err != nil { return &api.PerformError{ Msg: "api.RemoveRoomAlias failed", } } setAliasReq := api.SetRoomAliasRequest{UserID: userID, Alias: alias, RoomID: newRoomID} setAliasRes := api.SetRoomAliasResponse{} if err = URSAPI.SetRoomAlias(ctx, &setAliasReq, &setAliasRes); err != nil { return &api.PerformError{ Msg: "api.SetRoomAlias failed", } } } return nil } func (r *Upgrader) clearOldCanonicalAliasEvent(ctx context.Context, oldRoom *api.QueryLatestEventsAndStateResponse, evTime time.Time, userID, roomID string) *api.PerformError { for _, event := range oldRoom.StateEvents { if event.Type() != gomatrixserverlib.MRoomCanonicalAlias || !event.StateKeyEquals("") { continue } var aliasContent struct { Alias string `json:"alias"` AltAliases []string `json:"alt_aliases"` } if err := json.Unmarshal(event.Content(), &aliasContent); err != nil { return &api.PerformError{ Msg: fmt.Sprintf("Failed to unmarshal canonical aliases: %s", err), } } if aliasContent.Alias == "" && len(aliasContent.AltAliases) == 0 { // There are no canonical aliases to clear, therefore do nothing. return nil } } emptyCanonicalAliasEvent, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, fledglingEvent{ Type: gomatrixserverlib.MRoomCanonicalAlias, Content: map[string]interface{}{}, }) if resErr != nil { if resErr.Code == api.PerformErrorNotAllowed { util.GetLogger(ctx).WithField(logrus.ErrorKey, resErr).Warn("UpgradeRoom: Could not set empty canonical alias event in old room") } else { return resErr } } else { if resErr = r.sendHeaderedEvent(ctx, emptyCanonicalAliasEvent); resErr != nil { return resErr } } return nil } func (r *Upgrader) publishIfOldRoomWasPublic(ctx context.Context, roomID, newRoomID string) *api.PerformError { // check if the old room was published var pubQueryRes api.QueryPublishedRoomsResponse err := r.URSAPI.QueryPublishedRooms(ctx, &api.QueryPublishedRoomsRequest{ RoomID: roomID, }, &pubQueryRes) if err != nil { return &api.PerformError{ Msg: "QueryPublishedRooms failed", } } // if the old room is published (was public), publish the new room if len(pubQueryRes.RoomIDs) == 1 { publishNewRoomAndUnpublishOldRoom(ctx, r.URSAPI, roomID, newRoomID) } return nil } func publishNewRoomAndUnpublishOldRoom( ctx context.Context, URSAPI api.RoomserverInternalAPI, oldRoomID, newRoomID string, ) { // expose this room in the published room list var pubNewRoomRes api.PerformPublishResponse URSAPI.PerformPublish(ctx, &api.PerformPublishRequest{ RoomID: newRoomID, Visibility: "public", }, &pubNewRoomRes) if pubNewRoomRes.Error != nil { // treat as non-fatal since the room is already made by this point util.GetLogger(ctx).WithError(pubNewRoomRes.Error).Error("failed to visibility:public") } var unpubOldRoomRes api.PerformPublishResponse // remove the old room from the published room list URSAPI.PerformPublish(ctx, &api.PerformPublishRequest{ RoomID: oldRoomID, Visibility: "private", }, &unpubOldRoomRes) if unpubOldRoomRes.Error != nil { // treat as non-fatal since the room is already made by this point util.GetLogger(ctx).WithError(unpubOldRoomRes.Error).Error("failed to visibility:private") } } func (r *Upgrader) validateRoomExists(ctx context.Context, roomID string) error { verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID} verRes := api.QueryRoomVersionForRoomResponse{} if err := r.URSAPI.QueryRoomVersionForRoom(ctx, &verReq, &verRes); err != nil { return &api.PerformError{ Code: api.PerformErrorNoRoom, Msg: "Room does not exist", } } return nil } func (r *Upgrader) userIsAuthorized(ctx context.Context, userID, roomID string, ) bool { plEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ EventType: gomatrixserverlib.MRoomPowerLevels, StateKey: "", }) if plEvent == nil { return false } pl, err := plEvent.PowerLevels() if err != nil { return false } // Check for power level required to send tombstone event (marks the current room as obsolete), // if not found, use the StateDefault power level return pl.UserLevel(userID) >= pl.EventLevel("m.room.tombstone", true) } // nolint:gocyclo func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.QueryLatestEventsAndStateResponse, userID, roomID, newVersion string, tombstoneEvent *gomatrixserverlib.HeaderedEvent) ([]fledglingEvent, *api.PerformError) { state := make(map[gomatrixserverlib.StateKeyTuple]*gomatrixserverlib.HeaderedEvent, len(oldRoom.StateEvents)) for _, event := range oldRoom.StateEvents { if event.StateKey() == nil { // This shouldn't ever happen, but better to be safe than sorry. continue } if event.Type() == gomatrixserverlib.MRoomMember && !event.StateKeyEquals(userID) { // With the exception of bans and invites which we do want to copy, we // should ignore membership events that aren't our own, as event auth will // prevent us from being able to create membership events on behalf of other // users anyway unless they are invites or bans. membership, err := event.Membership() if err != nil { continue } switch membership { case gomatrixserverlib.Ban: case gomatrixserverlib.Invite: default: continue } } state[gomatrixserverlib.StateKeyTuple{EventType: event.Type(), StateKey: *event.StateKey()}] = event } // The following events are ones that we are going to override manually // in the following section. override := map[gomatrixserverlib.StateKeyTuple]struct{}{ {EventType: gomatrixserverlib.MRoomCreate, StateKey: ""}: {}, {EventType: gomatrixserverlib.MRoomMember, StateKey: userID}: {}, {EventType: gomatrixserverlib.MRoomPowerLevels, StateKey: ""}: {}, {EventType: gomatrixserverlib.MRoomJoinRules, StateKey: ""}: {}, } // The overridden events are essential events that must be present in the // old room state. Check that they are there. for tuple := range override { if _, ok := state[tuple]; !ok { return nil, &api.PerformError{ Msg: fmt.Sprintf("Essential event of type %q state key %q is missing", tuple.EventType, tuple.StateKey), } } } oldCreateEvent := state[gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomCreate, StateKey: ""}] oldMembershipEvent := state[gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomMember, StateKey: userID}] oldPowerLevelsEvent := state[gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomPowerLevels, StateKey: ""}] oldJoinRulesEvent := state[gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomJoinRules, StateKey: ""}] // Create the new room create event. Using a map here instead of CreateContent // means that we preserve any other interesting fields that might be present // in the create event (such as for the room types MSC). newCreateContent := map[string]interface{}{} _ = json.Unmarshal(oldCreateEvent.Content(), &newCreateContent) newCreateContent["creator"] = userID newCreateContent["room_version"] = newVersion newCreateContent["predecessor"] = gomatrixserverlib.PreviousRoom{ EventID: tombstoneEvent.EventID(), RoomID: roomID, } newCreateEvent := fledglingEvent{ Type: gomatrixserverlib.MRoomCreate, StateKey: "", Content: newCreateContent, } // Now create the new membership event. Same rules apply as above, so // that we preserve fields we don't otherwise know about. We'll always // set the membership to join though, because that is necessary to auth // the events after it. newMembershipContent := map[string]interface{}{} _ = json.Unmarshal(oldMembershipEvent.Content(), &newMembershipContent) newMembershipContent["membership"] = gomatrixserverlib.Join newMembershipEvent := fledglingEvent{ Type: gomatrixserverlib.MRoomMember, StateKey: userID, Content: newMembershipContent, } // We might need to temporarily give ourselves a higher power level // than we had in the old room in order to be able to send all of // the relevant state events. This function will return whether we // had to override the power level events or not — if we did, we // need to send the original power levels again later on. powerLevelContent, err := oldPowerLevelsEvent.PowerLevels() if err != nil { util.GetLogger(ctx).WithError(err).Error() return nil, &api.PerformError{ Msg: "Power level event content was invalid", } } tempPowerLevelsEvent, powerLevelsOverridden := createTemporaryPowerLevels(powerLevelContent, userID) // Now do the join rules event, same as the create and membership // events. We'll set a sane default of "invite" so that if the // existing join rules contains garbage, the room can still be // upgraded. newJoinRulesContent := map[string]interface{}{ "join_rule": gomatrixserverlib.Invite, // sane default } _ = json.Unmarshal(oldJoinRulesEvent.Content(), &newJoinRulesContent) newJoinRulesEvent := fledglingEvent{ Type: gomatrixserverlib.MRoomJoinRules, StateKey: "", Content: newJoinRulesContent, } eventsToMake := make([]fledglingEvent, 0, len(state)) eventsToMake = append( eventsToMake, newCreateEvent, newMembershipEvent, tempPowerLevelsEvent, newJoinRulesEvent, ) // For some reason Sytest expects there to be a guest access event. // Create one if it doesn't exist. if _, ok := state[gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomGuestAccess, StateKey: ""}]; !ok { eventsToMake = append(eventsToMake, fledglingEvent{ Type: gomatrixserverlib.MRoomGuestAccess, Content: map[string]string{ "guest_access": "forbidden", }, }) } // Duplicate all of the old state events into the new room. for tuple, event := range state { if _, ok := override[tuple]; ok { // Don't duplicate events we have overridden already. They // are already in `eventsToMake`. continue } newEvent := fledglingEvent{ Type: tuple.EventType, StateKey: tuple.StateKey, } if err = json.Unmarshal(event.Content(), &newEvent.Content); err != nil { logrus.WithError(err).Error("Failed to unmarshal old event") continue } eventsToMake = append(eventsToMake, newEvent) } // If we sent a temporary power level event into the room before, // override that now by restoring the original power levels. if powerLevelsOverridden { eventsToMake = append(eventsToMake, fledglingEvent{ Type: gomatrixserverlib.MRoomPowerLevels, Content: powerLevelContent, }) } return eventsToMake, nil } func (r *Upgrader) sendInitialEvents(ctx context.Context, evTime time.Time, userID, newRoomID, newVersion string, eventsToMake []fledglingEvent) *api.PerformError { var err error var builtEvents []*gomatrixserverlib.HeaderedEvent authEvents := gomatrixserverlib.NewAuthEvents(nil) for i, e := range eventsToMake { depth := i + 1 // depth starts at 1 builder := gomatrixserverlib.EventBuilder{ Sender: userID, RoomID: newRoomID, Type: e.Type, StateKey: &e.StateKey, Depth: int64(depth), } err = builder.SetContent(e.Content) if err != nil { return &api.PerformError{ Msg: "builder.SetContent failed", } } if i > 0 { builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()} } var event *gomatrixserverlib.Event event, err = r.buildEvent(&builder, &authEvents, evTime, gomatrixserverlib.RoomVersion(newVersion)) if err != nil { return &api.PerformError{ Msg: "buildEvent failed", } } if err = gomatrixserverlib.Allowed(event, &authEvents); err != nil { return &api.PerformError{ Msg: "gomatrixserverlib.Allowed failed", } } // Add the event to the list of auth events builtEvents = append(builtEvents, event.Headered(gomatrixserverlib.RoomVersion(newVersion))) err = authEvents.AddEvent(event) if err != nil { return &api.PerformError{ Msg: "authEvents.AddEvent failed", } } } inputs := make([]api.InputRoomEvent, 0, len(builtEvents)) for _, event := range builtEvents { inputs = append(inputs, api.InputRoomEvent{ Kind: api.KindNew, Event: event, Origin: r.Cfg.Matrix.ServerName, SendAsServer: api.DoNotSendToOtherServers, }) } if err = api.SendInputRoomEvents(ctx, r.URSAPI, inputs, false); err != nil { return &api.PerformError{ Msg: "api.SendInputRoomEvents failed", } } return nil } func (r *Upgrader) makeTombstoneEvent( ctx context.Context, evTime time.Time, userID, roomID, newRoomID string, ) (*gomatrixserverlib.HeaderedEvent, *api.PerformError) { content := map[string]interface{}{ "body": "This room has been replaced", "replacement_room": newRoomID, } event := fledglingEvent{ Type: "m.room.tombstone", Content: content, } return r.makeHeaderedEvent(ctx, evTime, userID, roomID, event) } func (r *Upgrader) makeHeaderedEvent(ctx context.Context, evTime time.Time, userID, roomID string, event fledglingEvent) (*gomatrixserverlib.HeaderedEvent, *api.PerformError) { builder := gomatrixserverlib.EventBuilder{ Sender: userID, RoomID: roomID, Type: event.Type, StateKey: &event.StateKey, } err := builder.SetContent(event.Content) if err != nil { return nil, &api.PerformError{ Msg: "builder.SetContent failed", } } var queryRes api.QueryLatestEventsAndStateResponse headeredEvent, err := eventutil.QueryAndBuildEvent(ctx, &builder, r.Cfg.Matrix, evTime, r.URSAPI, &queryRes) if err == eventutil.ErrRoomNoExists { return nil, &api.PerformError{ Code: api.PerformErrorNoRoom, Msg: "Room does not exist", } } else if e, ok := err.(gomatrixserverlib.BadJSONError); ok { return nil, &api.PerformError{ Msg: e.Error(), } } else if e, ok := err.(gomatrixserverlib.EventValidationError); ok { if e.Code == gomatrixserverlib.EventValidationTooLarge { return nil, &api.PerformError{ Msg: e.Error(), } } return nil, &api.PerformError{ Msg: e.Error(), } } else if err != nil { return nil, &api.PerformError{ Msg: "eventutil.BuildEvent failed", } } // check to see if this user can perform this operation stateEvents := make([]*gomatrixserverlib.Event, len(queryRes.StateEvents)) for i := range queryRes.StateEvents { stateEvents[i] = queryRes.StateEvents[i].Event } provider := gomatrixserverlib.NewAuthEvents(stateEvents) if err = gomatrixserverlib.Allowed(headeredEvent.Event, &provider); err != nil { return nil, &api.PerformError{ Code: api.PerformErrorNotAllowed, Msg: err.Error(), // TODO: Is this error string comprehensible to the client? } } return headeredEvent, nil } func createTemporaryPowerLevels(powerLevelContent *gomatrixserverlib.PowerLevelContent, userID string) (fledglingEvent, bool) { // Work out what power level we need in order to be able to send events // of all types into the room. neededPowerLevel := powerLevelContent.StateDefault for _, powerLevel := range powerLevelContent.Events { if powerLevel > neededPowerLevel { neededPowerLevel = powerLevel } } // Make a copy of the existing power level content. tempPowerLevelContent := *powerLevelContent powerLevelsOverridden := false // At this point, the "Users", "Events" and "Notifications" keys are all // pointing to the map of the original PL content, so we will specifically // override the users map with a new one and duplicate the values deeply, // so that we can modify them without modifying the original. tempPowerLevelContent.Users = make(map[string]int64, len(powerLevelContent.Users)) for key, value := range powerLevelContent.Users { tempPowerLevelContent.Users[key] = value } // If the user who is upgrading the room doesn't already have sufficient // power, then elevate their power levels. if tempPowerLevelContent.UserLevel(userID) < neededPowerLevel { tempPowerLevelContent.Users[userID] = neededPowerLevel powerLevelsOverridden = true } // Then return the temporary power levels event. return fledglingEvent{ Type: gomatrixserverlib.MRoomPowerLevels, Content: tempPowerLevelContent, }, powerLevelsOverridden } func (r *Upgrader) sendHeaderedEvent( ctx context.Context, headeredEvent *gomatrixserverlib.HeaderedEvent, ) *api.PerformError { var inputs []api.InputRoomEvent inputs = append(inputs, api.InputRoomEvent{ Kind: api.KindNew, Event: headeredEvent, Origin: r.Cfg.Matrix.ServerName, SendAsServer: api.DoNotSendToOtherServers, }) if err := api.SendInputRoomEvents(ctx, r.URSAPI, inputs, false); err != nil { return &api.PerformError{ Msg: "api.SendInputRoomEvents failed", } } return nil } func (r *Upgrader) buildEvent( builder *gomatrixserverlib.EventBuilder, provider gomatrixserverlib.AuthEventProvider, evTime time.Time, roomVersion gomatrixserverlib.RoomVersion, ) (*gomatrixserverlib.Event, error) { eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) if err != nil { return nil, err } refs, err := eventsNeeded.AuthEventReferences(provider) if err != nil { return nil, err } builder.AuthEvents = refs event, err := builder.Build( evTime, r.Cfg.Matrix.ServerName, r.Cfg.Matrix.KeyID, r.Cfg.Matrix.PrivateKey, roomVersion, ) if err != nil { return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %w", builder.Type, err) } return event, nil }