summary refs log tree commit diff
path: root/vendor/maunium.net/go/mautrix/event
diff options
context:
space:
mode:
authorEmile <git@emile.space>2024-10-25 15:55:50 +0200
committerEmile <git@emile.space>2024-10-25 15:55:50 +0200
commitc90f36e3dd179d2de96f4f5fe38d8dc9a9de6dfe (patch)
tree89e9afb41c5bf76f48cfb09305a2d3db8d302b06 /vendor/maunium.net/go/mautrix/event
parent98bbb0f559a8883bc47bae80607dbe326a448e61 (diff)
vendor HEAD main
Diffstat (limited to 'vendor/maunium.net/go/mautrix/event')
-rw-r--r--vendor/maunium.net/go/mautrix/event/accountdata.go107
-rw-r--r--vendor/maunium.net/go/mautrix/event/audio.go21
-rw-r--r--vendor/maunium.net/go/mautrix/event/beeper.go106
-rw-r--r--vendor/maunium.net/go/mautrix/event/content.go609
-rw-r--r--vendor/maunium.net/go/mautrix/event/encryption.go202
-rw-r--r--vendor/maunium.net/go/mautrix/event/ephemeral.go140
-rw-r--r--vendor/maunium.net/go/mautrix/event/events.go156
-rw-r--r--vendor/maunium.net/go/mautrix/event/eventsource.go72
-rw-r--r--vendor/maunium.net/go/mautrix/event/member.go53
-rw-r--r--vendor/maunium.net/go/mautrix/event/message.go356
-rw-r--r--vendor/maunium.net/go/mautrix/event/poll.go67
-rw-r--r--vendor/maunium.net/go/mautrix/event/powerlevels.go199
-rw-r--r--vendor/maunium.net/go/mautrix/event/relations.go234
-rw-r--r--vendor/maunium.net/go/mautrix/event/reply.go98
-rw-r--r--vendor/maunium.net/go/mautrix/event/state.go212
-rw-r--r--vendor/maunium.net/go/mautrix/event/type.go290
-rw-r--r--vendor/maunium.net/go/mautrix/event/verification.go308
-rw-r--r--vendor/maunium.net/go/mautrix/event/voip.go116
18 files changed, 3346 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/event/accountdata.go b/vendor/maunium.net/go/mautrix/event/accountdata.go
new file mode 100644
index 0000000..30ca35a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/accountdata.go
@@ -0,0 +1,107 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"strings"
+	"time"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// TagEventContent represents the content of a m.tag room account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mtag
+type TagEventContent struct {
+	Tags Tags `json:"tags"`
+}
+
+type Tags map[RoomTag]TagMetadata
+
+type RoomTag string
+
+const (
+	RoomTagFavourite    RoomTag = "m.favourite"
+	RoomTagLowPriority  RoomTag = "m.lowpriority"
+	RoomTagServerNotice RoomTag = "m.server_notice"
+)
+
+func (rt RoomTag) IsUserDefined() bool {
+	return strings.HasPrefix(string(rt), "u.")
+}
+
+func (rt RoomTag) String() string {
+	return string(rt)
+}
+
+func (rt RoomTag) Name() string {
+	if rt.IsUserDefined() {
+		return string(rt[2:])
+	}
+	switch rt {
+	case RoomTagFavourite:
+		return "Favourite"
+	case RoomTagLowPriority:
+		return "Low priority"
+	case RoomTagServerNotice:
+		return "Server notice"
+	default:
+		return ""
+	}
+}
+
+// Deprecated: type alias
+type Tag = TagMetadata
+
+type TagMetadata struct {
+	Order json.Number `json:"order,omitempty"`
+
+	MauDoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"`
+}
+
+// DirectChatsEventContent represents the content of a m.direct account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mdirect
+type DirectChatsEventContent map[id.UserID][]id.RoomID
+
+// FullyReadEventContent represents the content of a m.fully_read account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mfully_read
+type FullyReadEventContent struct {
+	EventID id.EventID `json:"event_id"`
+}
+
+// IgnoredUserListEventContent represents the content of a m.ignored_user_list account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mignored_user_list
+type IgnoredUserListEventContent struct {
+	IgnoredUsers map[id.UserID]IgnoredUser `json:"ignored_users"`
+}
+
+type IgnoredUser struct {
+	// This is an empty object
+}
+
+type MarkedUnreadEventContent struct {
+	Unread bool `json:"unread"`
+}
+
+type BeeperMuteEventContent struct {
+	MutedUntil int64 `json:"muted_until,omitempty"`
+}
+
+func (bmec *BeeperMuteEventContent) IsMuted() bool {
+	return bmec.MutedUntil < 0 || (bmec.MutedUntil > 0 && bmec.GetMutedUntilTime().After(time.Now()))
+}
+
+var MutedForever = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC)
+
+func (bmec *BeeperMuteEventContent) GetMutedUntilTime() time.Time {
+	if bmec.MutedUntil < 0 {
+		return MutedForever
+	} else if bmec.MutedUntil > 0 {
+		return time.UnixMilli(bmec.MutedUntil)
+	}
+	return time.Time{}
+}
diff --git a/vendor/maunium.net/go/mautrix/event/audio.go b/vendor/maunium.net/go/mautrix/event/audio.go
new file mode 100644
index 0000000..9eeb8ed
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/audio.go
@@ -0,0 +1,21 @@
+package event
+
+import (
+	"encoding/json"
+)
+
+type MSC1767Audio struct {
+	Duration int   `json:"duration"`
+	Waveform []int `json:"waveform"`
+}
+
+type serializableMSC1767Audio MSC1767Audio
+
+func (ma *MSC1767Audio) MarshalJSON() ([]byte, error) {
+	if ma.Waveform == nil {
+		ma.Waveform = []int{}
+	}
+	return json.Marshal((*serializableMSC1767Audio)(ma))
+}
+
+type MSC3245Voice struct{}
diff --git a/vendor/maunium.net/go/mautrix/event/beeper.go b/vendor/maunium.net/go/mautrix/event/beeper.go
new file mode 100644
index 0000000..911bdfe
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/beeper.go
@@ -0,0 +1,106 @@
+// Copyright (c) 2022 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"maunium.net/go/mautrix/id"
+)
+
+type MessageStatusReason string
+
+const (
+	MessageStatusGenericError      MessageStatusReason = "m.event_not_handled"
+	MessageStatusUnsupported       MessageStatusReason = "com.beeper.unsupported_event"
+	MessageStatusUndecryptable     MessageStatusReason = "com.beeper.undecryptable_event"
+	MessageStatusTooOld            MessageStatusReason = "m.event_too_old"
+	MessageStatusNetworkError      MessageStatusReason = "m.foreign_network_error"
+	MessageStatusNoPermission      MessageStatusReason = "m.no_permission"
+	MessageStatusBridgeUnavailable MessageStatusReason = "m.bridge_unavailable"
+)
+
+type MessageStatus string
+
+const (
+	MessageStatusSuccess   MessageStatus = "SUCCESS"
+	MessageStatusPending   MessageStatus = "PENDING"
+	MessageStatusRetriable MessageStatus = "FAIL_RETRIABLE"
+	MessageStatusFail      MessageStatus = "FAIL_PERMANENT"
+)
+
+type BeeperMessageStatusEventContent struct {
+	Network   string              `json:"network,omitempty"`
+	RelatesTo RelatesTo           `json:"m.relates_to"`
+	Status    MessageStatus       `json:"status"`
+	Reason    MessageStatusReason `json:"reason,omitempty"`
+	// Deprecated: clients were showing this to users even though they aren't supposed to.
+	// Use InternalError for error messages that should be included in bug reports, but not shown in the UI.
+	Error         string `json:"error,omitempty"`
+	InternalError string `json:"internal_error,omitempty"`
+	Message       string `json:"message,omitempty"`
+
+	LastRetry id.EventID `json:"last_retry,omitempty"`
+
+	MutateEventKey string `json:"mutate_event_key,omitempty"`
+
+	// Indicates the set of users to whom the event was delivered. If nil, then
+	// the client should not expect delivered status at any later point. If not
+	// nil (even if empty), this field indicates which users the event was
+	// delivered to.
+	DeliveredToUsers *[]id.UserID `json:"delivered_to_users,omitempty"`
+}
+
+type BeeperRetryMetadata struct {
+	OriginalEventID id.EventID `json:"original_event_id"`
+	RetryCount      int        `json:"retry_count"`
+	// last_retry is also present, but not used by bridges
+}
+
+type BeeperRoomKeyAckEventContent struct {
+	RoomID            id.RoomID    `json:"room_id"`
+	SessionID         id.SessionID `json:"session_id"`
+	FirstMessageIndex int          `json:"first_message_index"`
+}
+
+type LinkPreview struct {
+	CanonicalURL string `json:"og:url,omitempty"`
+	Title        string `json:"og:title,omitempty"`
+	Type         string `json:"og:type,omitempty"`
+	Description  string `json:"og:description,omitempty"`
+
+	ImageURL id.ContentURIString `json:"og:image,omitempty"`
+
+	ImageSize   int    `json:"matrix:image:size,omitempty"`
+	ImageWidth  int    `json:"og:image:width,omitempty"`
+	ImageHeight int    `json:"og:image:height,omitempty"`
+	ImageType   string `json:"og:image:type,omitempty"`
+}
+
+// BeeperLinkPreview contains the data for a bundled URL preview as specified in MSC4095
+//
+// https://github.com/matrix-org/matrix-spec-proposals/pull/4095
+type BeeperLinkPreview struct {
+	LinkPreview
+
+	MatchedURL      string             `json:"matched_url,omitempty"`
+	ImageEncryption *EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
+}
+
+type BeeperProfileExtra struct {
+	RemoteID     string   `json:"com.beeper.bridge.remote_id,omitempty"`
+	Identifiers  []string `json:"com.beeper.bridge.identifiers,omitempty"`
+	Service      string   `json:"com.beeper.bridge.service,omitempty"`
+	Network      string   `json:"com.beeper.bridge.network,omitempty"`
+	IsBridgeBot  bool     `json:"com.beeper.bridge.is_bridge_bot,omitempty"`
+	IsNetworkBot bool     `json:"com.beeper.bridge.is_network_bot,omitempty"`
+}
+
+type BeeperPerMessageProfile struct {
+	ID          string               `json:"id"`
+	Displayname string               `json:"displayname,omitempty"`
+	AvatarURL   *id.ContentURIString `json:"avatar_url,omitempty"`
+	AvatarFile  *EncryptedFileInfo   `json:"avatar_file,omitempty"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/content.go b/vendor/maunium.net/go/mautrix/event/content.go
new file mode 100644
index 0000000..882d336
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/content.go
@@ -0,0 +1,609 @@
+// Copyright (c) 2021 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/gob"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+)
+
+// TypeMap is a mapping from event type to the content struct type.
+// This is used by Content.ParseRaw() for creating the correct type of struct.
+var TypeMap = map[Type]reflect.Type{
+	StateMember:            reflect.TypeOf(MemberEventContent{}),
+	StatePowerLevels:       reflect.TypeOf(PowerLevelsEventContent{}),
+	StateCanonicalAlias:    reflect.TypeOf(CanonicalAliasEventContent{}),
+	StateRoomName:          reflect.TypeOf(RoomNameEventContent{}),
+	StateRoomAvatar:        reflect.TypeOf(RoomAvatarEventContent{}),
+	StateServerACL:         reflect.TypeOf(ServerACLEventContent{}),
+	StateTopic:             reflect.TypeOf(TopicEventContent{}),
+	StateTombstone:         reflect.TypeOf(TombstoneEventContent{}),
+	StateCreate:            reflect.TypeOf(CreateEventContent{}),
+	StateJoinRules:         reflect.TypeOf(JoinRulesEventContent{}),
+	StateHistoryVisibility: reflect.TypeOf(HistoryVisibilityEventContent{}),
+	StateGuestAccess:       reflect.TypeOf(GuestAccessEventContent{}),
+	StatePinnedEvents:      reflect.TypeOf(PinnedEventsEventContent{}),
+	StatePolicyRoom:        reflect.TypeOf(ModPolicyContent{}),
+	StatePolicyServer:      reflect.TypeOf(ModPolicyContent{}),
+	StatePolicyUser:        reflect.TypeOf(ModPolicyContent{}),
+	StateEncryption:        reflect.TypeOf(EncryptionEventContent{}),
+	StateBridge:            reflect.TypeOf(BridgeEventContent{}),
+	StateHalfShotBridge:    reflect.TypeOf(BridgeEventContent{}),
+	StateSpaceParent:       reflect.TypeOf(SpaceParentEventContent{}),
+	StateSpaceChild:        reflect.TypeOf(SpaceChildEventContent{}),
+	StateInsertionMarker:   reflect.TypeOf(InsertionMarkerContent{}),
+
+	StateLegacyPolicyRoom:     reflect.TypeOf(ModPolicyContent{}),
+	StateLegacyPolicyServer:   reflect.TypeOf(ModPolicyContent{}),
+	StateLegacyPolicyUser:     reflect.TypeOf(ModPolicyContent{}),
+	StateUnstablePolicyRoom:   reflect.TypeOf(ModPolicyContent{}),
+	StateUnstablePolicyServer: reflect.TypeOf(ModPolicyContent{}),
+	StateUnstablePolicyUser:   reflect.TypeOf(ModPolicyContent{}),
+
+	StateElementFunctionalMembers: reflect.TypeOf(ElementFunctionalMembersContent{}),
+
+	EventMessage:   reflect.TypeOf(MessageEventContent{}),
+	EventSticker:   reflect.TypeOf(MessageEventContent{}),
+	EventEncrypted: reflect.TypeOf(EncryptedEventContent{}),
+	EventRedaction: reflect.TypeOf(RedactionEventContent{}),
+	EventReaction:  reflect.TypeOf(ReactionEventContent{}),
+
+	EventUnstablePollStart:    reflect.TypeOf(PollStartEventContent{}),
+	EventUnstablePollResponse: reflect.TypeOf(PollResponseEventContent{}),
+
+	BeeperMessageStatus: reflect.TypeOf(BeeperMessageStatusEventContent{}),
+
+	AccountDataRoomTags:        reflect.TypeOf(TagEventContent{}),
+	AccountDataDirectChats:     reflect.TypeOf(DirectChatsEventContent{}),
+	AccountDataFullyRead:       reflect.TypeOf(FullyReadEventContent{}),
+	AccountDataIgnoredUserList: reflect.TypeOf(IgnoredUserListEventContent{}),
+	AccountDataMarkedUnread:    reflect.TypeOf(MarkedUnreadEventContent{}),
+	AccountDataBeeperMute:      reflect.TypeOf(BeeperMuteEventContent{}),
+
+	EphemeralEventTyping:   reflect.TypeOf(TypingEventContent{}),
+	EphemeralEventReceipt:  reflect.TypeOf(ReceiptEventContent{}),
+	EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}),
+
+	InRoomVerificationReady:  reflect.TypeOf(VerificationReadyEventContent{}),
+	InRoomVerificationStart:  reflect.TypeOf(VerificationStartEventContent{}),
+	InRoomVerificationDone:   reflect.TypeOf(VerificationDoneEventContent{}),
+	InRoomVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}),
+
+	InRoomVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
+	InRoomVerificationKey:    reflect.TypeOf(VerificationKeyEventContent{}),
+	InRoomVerificationMAC:    reflect.TypeOf(VerificationMACEventContent{}),
+
+	ToDeviceRoomKey:          reflect.TypeOf(RoomKeyEventContent{}),
+	ToDeviceForwardedRoomKey: reflect.TypeOf(ForwardedRoomKeyEventContent{}),
+	ToDeviceRoomKeyRequest:   reflect.TypeOf(RoomKeyRequestEventContent{}),
+	ToDeviceEncrypted:        reflect.TypeOf(EncryptedEventContent{}),
+	ToDeviceRoomKeyWithheld:  reflect.TypeOf(RoomKeyWithheldEventContent{}),
+	ToDeviceSecretRequest:    reflect.TypeOf(SecretRequestEventContent{}),
+	ToDeviceSecretSend:       reflect.TypeOf(SecretSendEventContent{}),
+	ToDeviceDummy:            reflect.TypeOf(DummyEventContent{}),
+
+	ToDeviceVerificationRequest: reflect.TypeOf(VerificationRequestEventContent{}),
+	ToDeviceVerificationReady:   reflect.TypeOf(VerificationReadyEventContent{}),
+	ToDeviceVerificationStart:   reflect.TypeOf(VerificationStartEventContent{}),
+	ToDeviceVerificationDone:    reflect.TypeOf(VerificationDoneEventContent{}),
+	ToDeviceVerificationCancel:  reflect.TypeOf(VerificationCancelEventContent{}),
+
+	ToDeviceVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
+	ToDeviceVerificationKey:    reflect.TypeOf(VerificationKeyEventContent{}),
+	ToDeviceVerificationMAC:    reflect.TypeOf(VerificationMACEventContent{}),
+
+	ToDeviceOrgMatrixRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}),
+
+	ToDeviceBeeperRoomKeyAck: reflect.TypeOf(BeeperRoomKeyAckEventContent{}),
+
+	CallInvite:       reflect.TypeOf(CallInviteEventContent{}),
+	CallCandidates:   reflect.TypeOf(CallCandidatesEventContent{}),
+	CallAnswer:       reflect.TypeOf(CallAnswerEventContent{}),
+	CallReject:       reflect.TypeOf(CallRejectEventContent{}),
+	CallSelectAnswer: reflect.TypeOf(CallSelectAnswerEventContent{}),
+	CallNegotiate:    reflect.TypeOf(CallNegotiateEventContent{}),
+	CallHangup:       reflect.TypeOf(CallHangupEventContent{}),
+}
+
+// Content stores the content of a Matrix event.
+//
+// By default, the raw JSON bytes are stored in VeryRaw and parsed into a map[string]interface{} in the Raw field.
+// Additionally, you can call ParseRaw with the correct event type to parse the (VeryRaw) content into a nicer struct,
+// which you can then access from Parsed or via the helper functions.
+//
+// When being marshaled into JSON, the data in Parsed will be marshaled first and then recursively merged
+// with the data in Raw. Values in Raw are preferred, but nested objects will be recursed into before merging,
+// rather than overriding the whole object with the one in Raw).
+// If one of them is nil, the only the other is used. If both (Parsed and Raw) are nil, VeryRaw is used instead.
+type Content struct {
+	VeryRaw json.RawMessage
+	Raw     map[string]interface{}
+	Parsed  interface{}
+}
+
+type Relatable interface {
+	GetRelatesTo() *RelatesTo
+	OptionalGetRelatesTo() *RelatesTo
+	SetRelatesTo(rel *RelatesTo)
+}
+
+func (content *Content) UnmarshalJSON(data []byte) error {
+	content.VeryRaw = data
+	err := json.Unmarshal(data, &content.Raw)
+	return err
+}
+
+func (content *Content) MarshalJSON() ([]byte, error) {
+	if content.Raw == nil {
+		if content.Parsed == nil {
+			if content.VeryRaw == nil {
+				return []byte("{}"), nil
+			}
+			return content.VeryRaw, nil
+		}
+		return json.Marshal(content.Parsed)
+	} else if content.Parsed != nil {
+		// TODO this whole thing is incredibly hacky
+		// It needs to produce JSON, where:
+		// * content.Parsed is applied after content.Raw
+		// * MarshalJSON() is respected inside content.Parsed
+		// * Custom field inside nested objects of content.Raw are preserved,
+		//   even if content.Parsed contains the higher-level objects.
+		// * content.Raw is not modified
+
+		unparsed, err := json.Marshal(content.Parsed)
+		if err != nil {
+			return nil, err
+		}
+
+		var rawParsed map[string]interface{}
+		err = json.Unmarshal(unparsed, &rawParsed)
+		if err != nil {
+			return nil, err
+		}
+
+		output := make(map[string]interface{})
+		for key, value := range content.Raw {
+			output[key] = value
+		}
+
+		mergeMaps(output, rawParsed)
+		return json.Marshal(output)
+	}
+	return json.Marshal(content.Raw)
+}
+
+// Deprecated: use errors.Is directly
+func IsUnsupportedContentType(err error) bool {
+	return errors.Is(err, ErrUnsupportedContentType)
+}
+
+var ErrContentAlreadyParsed = errors.New("content is already parsed")
+var ErrUnsupportedContentType = errors.New("unsupported event type")
+
+func (content *Content) ParseRaw(evtType Type) error {
+	if content.Parsed != nil {
+		return ErrContentAlreadyParsed
+	}
+	structType, ok := TypeMap[evtType]
+	if !ok {
+		return fmt.Errorf("%w %s", ErrUnsupportedContentType, evtType.Repr())
+	}
+	content.Parsed = reflect.New(structType).Interface()
+	return json.Unmarshal(content.VeryRaw, &content.Parsed)
+}
+
+func mergeMaps(into, from map[string]interface{}) {
+	for key, newValue := range from {
+		existingValue, ok := into[key]
+		if !ok {
+			into[key] = newValue
+			continue
+		}
+		existingValueMap, okEx := existingValue.(map[string]interface{})
+		newValueMap, okNew := newValue.(map[string]interface{})
+		if okEx && okNew {
+			mergeMaps(existingValueMap, newValueMap)
+		} else {
+			into[key] = newValue
+		}
+	}
+}
+
+func init() {
+	gob.Register(&MemberEventContent{})
+	gob.Register(&PowerLevelsEventContent{})
+	gob.Register(&CanonicalAliasEventContent{})
+	gob.Register(&EncryptionEventContent{})
+	gob.Register(&BridgeEventContent{})
+	gob.Register(&SpaceChildEventContent{})
+	gob.Register(&SpaceParentEventContent{})
+	gob.Register(&ElementFunctionalMembersContent{})
+	gob.Register(&RoomNameEventContent{})
+	gob.Register(&RoomAvatarEventContent{})
+	gob.Register(&TopicEventContent{})
+	gob.Register(&TombstoneEventContent{})
+	gob.Register(&CreateEventContent{})
+	gob.Register(&JoinRulesEventContent{})
+	gob.Register(&HistoryVisibilityEventContent{})
+	gob.Register(&GuestAccessEventContent{})
+	gob.Register(&PinnedEventsEventContent{})
+	gob.Register(&MessageEventContent{})
+	gob.Register(&MessageEventContent{})
+	gob.Register(&EncryptedEventContent{})
+	gob.Register(&RedactionEventContent{})
+	gob.Register(&ReactionEventContent{})
+	gob.Register(&TagEventContent{})
+	gob.Register(&DirectChatsEventContent{})
+	gob.Register(&FullyReadEventContent{})
+	gob.Register(&IgnoredUserListEventContent{})
+	gob.Register(&TypingEventContent{})
+	gob.Register(&ReceiptEventContent{})
+	gob.Register(&PresenceEventContent{})
+	gob.Register(&RoomKeyEventContent{})
+	gob.Register(&ForwardedRoomKeyEventContent{})
+	gob.Register(&RoomKeyRequestEventContent{})
+	gob.Register(&RoomKeyWithheldEventContent{})
+}
+
+func CastOrDefault[T any](content *Content) *T {
+	casted, ok := content.Parsed.(*T)
+	if ok {
+		return casted
+	}
+	casted2, _ := content.Parsed.(T)
+	return &casted2
+}
+
+// Helper cast functions below
+
+func (content *Content) AsMember() *MemberEventContent {
+	casted, ok := content.Parsed.(*MemberEventContent)
+	if !ok {
+		return &MemberEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsPowerLevels() *PowerLevelsEventContent {
+	casted, ok := content.Parsed.(*PowerLevelsEventContent)
+	if !ok {
+		return &PowerLevelsEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCanonicalAlias() *CanonicalAliasEventContent {
+	casted, ok := content.Parsed.(*CanonicalAliasEventContent)
+	if !ok {
+		return &CanonicalAliasEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomName() *RoomNameEventContent {
+	casted, ok := content.Parsed.(*RoomNameEventContent)
+	if !ok {
+		return &RoomNameEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomAvatar() *RoomAvatarEventContent {
+	casted, ok := content.Parsed.(*RoomAvatarEventContent)
+	if !ok {
+		return &RoomAvatarEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTopic() *TopicEventContent {
+	casted, ok := content.Parsed.(*TopicEventContent)
+	if !ok {
+		return &TopicEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTombstone() *TombstoneEventContent {
+	casted, ok := content.Parsed.(*TombstoneEventContent)
+	if !ok {
+		return &TombstoneEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCreate() *CreateEventContent {
+	casted, ok := content.Parsed.(*CreateEventContent)
+	if !ok {
+		return &CreateEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsJoinRules() *JoinRulesEventContent {
+	casted, ok := content.Parsed.(*JoinRulesEventContent)
+	if !ok {
+		return &JoinRulesEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsHistoryVisibility() *HistoryVisibilityEventContent {
+	casted, ok := content.Parsed.(*HistoryVisibilityEventContent)
+	if !ok {
+		return &HistoryVisibilityEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsGuestAccess() *GuestAccessEventContent {
+	casted, ok := content.Parsed.(*GuestAccessEventContent)
+	if !ok {
+		return &GuestAccessEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsPinnedEvents() *PinnedEventsEventContent {
+	casted, ok := content.Parsed.(*PinnedEventsEventContent)
+	if !ok {
+		return &PinnedEventsEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsEncryption() *EncryptionEventContent {
+	casted, ok := content.Parsed.(*EncryptionEventContent)
+	if !ok {
+		return &EncryptionEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsBridge() *BridgeEventContent {
+	casted, ok := content.Parsed.(*BridgeEventContent)
+	if !ok {
+		return &BridgeEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsSpaceChild() *SpaceChildEventContent {
+	casted, ok := content.Parsed.(*SpaceChildEventContent)
+	if !ok {
+		return &SpaceChildEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsSpaceParent() *SpaceParentEventContent {
+	casted, ok := content.Parsed.(*SpaceParentEventContent)
+	if !ok {
+		return &SpaceParentEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsElementFunctionalMembers() *ElementFunctionalMembersContent {
+	casted, ok := content.Parsed.(*ElementFunctionalMembersContent)
+	if !ok {
+		return &ElementFunctionalMembersContent{}
+	}
+	return casted
+}
+func (content *Content) AsMessage() *MessageEventContent {
+	casted, ok := content.Parsed.(*MessageEventContent)
+	if !ok {
+		return &MessageEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsEncrypted() *EncryptedEventContent {
+	casted, ok := content.Parsed.(*EncryptedEventContent)
+	if !ok {
+		return &EncryptedEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRedaction() *RedactionEventContent {
+	casted, ok := content.Parsed.(*RedactionEventContent)
+	if !ok {
+		return &RedactionEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsReaction() *ReactionEventContent {
+	casted, ok := content.Parsed.(*ReactionEventContent)
+	if !ok {
+		return &ReactionEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTag() *TagEventContent {
+	casted, ok := content.Parsed.(*TagEventContent)
+	if !ok {
+		return &TagEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsDirectChats() *DirectChatsEventContent {
+	casted, ok := content.Parsed.(*DirectChatsEventContent)
+	if !ok {
+		return &DirectChatsEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsFullyRead() *FullyReadEventContent {
+	casted, ok := content.Parsed.(*FullyReadEventContent)
+	if !ok {
+		return &FullyReadEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsIgnoredUserList() *IgnoredUserListEventContent {
+	casted, ok := content.Parsed.(*IgnoredUserListEventContent)
+	if !ok {
+		return &IgnoredUserListEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsMarkedUnread() *MarkedUnreadEventContent {
+	casted, ok := content.Parsed.(*MarkedUnreadEventContent)
+	if !ok {
+		return &MarkedUnreadEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTyping() *TypingEventContent {
+	casted, ok := content.Parsed.(*TypingEventContent)
+	if !ok {
+		return &TypingEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsReceipt() *ReceiptEventContent {
+	casted, ok := content.Parsed.(*ReceiptEventContent)
+	if !ok {
+		return &ReceiptEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsPresence() *PresenceEventContent {
+	casted, ok := content.Parsed.(*PresenceEventContent)
+	if !ok {
+		return &PresenceEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomKey() *RoomKeyEventContent {
+	casted, ok := content.Parsed.(*RoomKeyEventContent)
+	if !ok {
+		return &RoomKeyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsForwardedRoomKey() *ForwardedRoomKeyEventContent {
+	casted, ok := content.Parsed.(*ForwardedRoomKeyEventContent)
+	if !ok {
+		return &ForwardedRoomKeyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomKeyRequest() *RoomKeyRequestEventContent {
+	casted, ok := content.Parsed.(*RoomKeyRequestEventContent)
+	if !ok {
+		return &RoomKeyRequestEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomKeyWithheld() *RoomKeyWithheldEventContent {
+	casted, ok := content.Parsed.(*RoomKeyWithheldEventContent)
+	if !ok {
+		return &RoomKeyWithheldEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallInvite() *CallInviteEventContent {
+	casted, ok := content.Parsed.(*CallInviteEventContent)
+	if !ok {
+		return &CallInviteEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallCandidates() *CallCandidatesEventContent {
+	casted, ok := content.Parsed.(*CallCandidatesEventContent)
+	if !ok {
+		return &CallCandidatesEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallAnswer() *CallAnswerEventContent {
+	casted, ok := content.Parsed.(*CallAnswerEventContent)
+	if !ok {
+		return &CallAnswerEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallReject() *CallRejectEventContent {
+	casted, ok := content.Parsed.(*CallRejectEventContent)
+	if !ok {
+		return &CallRejectEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallSelectAnswer() *CallSelectAnswerEventContent {
+	casted, ok := content.Parsed.(*CallSelectAnswerEventContent)
+	if !ok {
+		return &CallSelectAnswerEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallNegotiate() *CallNegotiateEventContent {
+	casted, ok := content.Parsed.(*CallNegotiateEventContent)
+	if !ok {
+		return &CallNegotiateEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallHangup() *CallHangupEventContent {
+	casted, ok := content.Parsed.(*CallHangupEventContent)
+	if !ok {
+		return &CallHangupEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsModPolicy() *ModPolicyContent {
+	casted, ok := content.Parsed.(*ModPolicyContent)
+	if !ok {
+		return &ModPolicyContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationRequest() *VerificationRequestEventContent {
+	casted, ok := content.Parsed.(*VerificationRequestEventContent)
+	if !ok {
+		return &VerificationRequestEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationReady() *VerificationReadyEventContent {
+	casted, ok := content.Parsed.(*VerificationReadyEventContent)
+	if !ok {
+		return &VerificationReadyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationStart() *VerificationStartEventContent {
+	casted, ok := content.Parsed.(*VerificationStartEventContent)
+	if !ok {
+		return &VerificationStartEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationDone() *VerificationDoneEventContent {
+	casted, ok := content.Parsed.(*VerificationDoneEventContent)
+	if !ok {
+		return &VerificationDoneEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationCancel() *VerificationCancelEventContent {
+	casted, ok := content.Parsed.(*VerificationCancelEventContent)
+	if !ok {
+		return &VerificationCancelEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationAccept() *VerificationAcceptEventContent {
+	casted, ok := content.Parsed.(*VerificationAcceptEventContent)
+	if !ok {
+		return &VerificationAcceptEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationKey() *VerificationKeyEventContent {
+	casted, ok := content.Parsed.(*VerificationKeyEventContent)
+	if !ok {
+		return &VerificationKeyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationMAC() *VerificationMACEventContent {
+	casted, ok := content.Parsed.(*VerificationMACEventContent)
+	if !ok {
+		return &VerificationMACEventContent{}
+	}
+	return casted
+}
diff --git a/vendor/maunium.net/go/mautrix/event/encryption.go b/vendor/maunium.net/go/mautrix/event/encryption.go
new file mode 100644
index 0000000..cf9c281
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/encryption.go
@@ -0,0 +1,202 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// EncryptionEventContent represents the content of a m.room.encryption state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomencryption
+type EncryptionEventContent struct {
+	// The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
+	Algorithm id.Algorithm `json:"algorithm"`
+	// How long the session should be used before changing it. 604800000 (a week) is the recommended default.
+	RotationPeriodMillis int64 `json:"rotation_period_ms,omitempty"`
+	// How many messages should be sent before changing the session. 100 is the recommended default.
+	RotationPeriodMessages int `json:"rotation_period_msgs,omitempty"`
+}
+
+// EncryptedEventContent represents the content of a m.room.encrypted message event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomencrypted
+//
+// Note that sender_key and device_id are deprecated in Megolm events as of https://github.com/matrix-org/matrix-spec-proposals/pull/3700
+type EncryptedEventContent struct {
+	Algorithm id.Algorithm `json:"algorithm"`
+	SenderKey id.SenderKey `json:"sender_key,omitempty"`
+	// Deprecated: Matrix v1.3
+	DeviceID id.DeviceID `json:"device_id,omitempty"`
+	// Only present for Megolm events
+	SessionID id.SessionID `json:"session_id,omitempty"`
+
+	Ciphertext json.RawMessage `json:"ciphertext"`
+
+	MegolmCiphertext []byte         `json:"-"`
+	OlmCiphertext    OlmCiphertexts `json:"-"`
+
+	RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
+	Mentions  *Mentions  `json:"m.mentions,omitempty"`
+}
+
+type OlmCiphertexts map[id.Curve25519]struct {
+	Body string        `json:"body"`
+	Type id.OlmMsgType `json:"type"`
+}
+
+type serializableEncryptedEventContent EncryptedEventContent
+
+func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, (*serializableEncryptedEventContent)(content))
+	if err != nil {
+		return err
+	}
+	switch content.Algorithm {
+	case id.AlgorithmOlmV1:
+		content.OlmCiphertext = make(OlmCiphertexts)
+		return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext)
+	case id.AlgorithmMegolmV1:
+		if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' {
+			return id.InputNotJSONString
+		}
+		content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1]
+	}
+	return nil
+}
+
+func (content *EncryptedEventContent) MarshalJSON() ([]byte, error) {
+	var err error
+	switch content.Algorithm {
+	case id.AlgorithmOlmV1:
+		content.Ciphertext, err = json.Marshal(content.OlmCiphertext)
+	case id.AlgorithmMegolmV1:
+		content.Ciphertext = make([]byte, len(content.MegolmCiphertext)+2)
+		content.Ciphertext[0] = '"'
+		content.Ciphertext[len(content.Ciphertext)-1] = '"'
+		copy(content.Ciphertext[1:len(content.Ciphertext)-1], content.MegolmCiphertext)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal((*serializableEncryptedEventContent)(content))
+}
+
+// RoomKeyEventContent represents the content of a m.room_key to_device event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroom_key
+type RoomKeyEventContent struct {
+	Algorithm  id.Algorithm `json:"algorithm"`
+	RoomID     id.RoomID    `json:"room_id"`
+	SessionID  id.SessionID `json:"session_id"`
+	SessionKey string       `json:"session_key"`
+
+	MaxAge      int64 `json:"com.beeper.max_age_ms"`
+	MaxMessages int   `json:"com.beeper.max_messages"`
+	IsScheduled bool  `json:"com.beeper.is_scheduled"`
+}
+
+// ForwardedRoomKeyEventContent represents the content of a m.forwarded_room_key to_device event.
+// https://spec.matrix.org/v1.2/client-server-api/#mforwarded_room_key
+type ForwardedRoomKeyEventContent struct {
+	RoomKeyEventContent
+	SenderKey          id.SenderKey `json:"sender_key"`
+	SenderClaimedKey   id.Ed25519   `json:"sender_claimed_ed25519_key"`
+	ForwardingKeyChain []string     `json:"forwarding_curve25519_key_chain"`
+
+	MaxAge      int64 `json:"com.beeper.max_age_ms"`
+	MaxMessages int   `json:"com.beeper.max_messages"`
+	IsScheduled bool  `json:"com.beeper.is_scheduled"`
+}
+
+type KeyRequestAction string
+
+const (
+	KeyRequestActionRequest = "request"
+	KeyRequestActionCancel  = "request_cancellation"
+)
+
+// RoomKeyRequestEventContent represents the content of a m.room_key_request to_device event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroom_key_request
+type RoomKeyRequestEventContent struct {
+	Body               RequestedKeyInfo `json:"body"`
+	Action             KeyRequestAction `json:"action"`
+	RequestingDeviceID id.DeviceID      `json:"requesting_device_id"`
+	RequestID          string           `json:"request_id"`
+}
+
+type RequestedKeyInfo struct {
+	Algorithm id.Algorithm `json:"algorithm"`
+	RoomID    id.RoomID    `json:"room_id"`
+	SenderKey id.SenderKey `json:"sender_key"`
+	SessionID id.SessionID `json:"session_id"`
+}
+
+type RoomKeyWithheldCode string
+
+const (
+	RoomKeyWithheldBlacklisted  RoomKeyWithheldCode = "m.blacklisted"
+	RoomKeyWithheldUnverified   RoomKeyWithheldCode = "m.unverified"
+	RoomKeyWithheldUnauthorized RoomKeyWithheldCode = "m.unauthorised"
+	RoomKeyWithheldUnavailable  RoomKeyWithheldCode = "m.unavailable"
+	RoomKeyWithheldNoOlmSession RoomKeyWithheldCode = "m.no_olm"
+
+	RoomKeyWithheldBeeperRedacted RoomKeyWithheldCode = "com.beeper.redacted"
+)
+
+type RoomKeyWithheldEventContent struct {
+	RoomID    id.RoomID           `json:"room_id,omitempty"`
+	Algorithm id.Algorithm        `json:"algorithm"`
+	SessionID id.SessionID        `json:"session_id,omitempty"`
+	SenderKey id.SenderKey        `json:"sender_key"`
+	Code      RoomKeyWithheldCode `json:"code"`
+	Reason    string              `json:"reason,omitempty"`
+}
+
+const groupSessionWithheldMsg = "group session has been withheld: %s"
+
+func (withheld *RoomKeyWithheldEventContent) Error() string {
+	switch withheld.Code {
+	case RoomKeyWithheldBlacklisted, RoomKeyWithheldUnverified, RoomKeyWithheldUnauthorized, RoomKeyWithheldUnavailable, RoomKeyWithheldNoOlmSession:
+		return fmt.Sprintf(groupSessionWithheldMsg, withheld.Code)
+	default:
+		return fmt.Sprintf(groupSessionWithheldMsg+" (%s)", withheld.Code, withheld.Reason)
+	}
+}
+
+func (withheld *RoomKeyWithheldEventContent) Is(other error) bool {
+	otherWithheld, ok := other.(*RoomKeyWithheldEventContent)
+	if !ok {
+		return false
+	}
+	return withheld.Code == "" || otherWithheld.Code == "" || withheld.Code == otherWithheld.Code
+}
+
+type SecretRequestAction string
+
+func (a SecretRequestAction) String() string {
+	return string(a)
+}
+
+const (
+	SecretRequestRequest      = "request"
+	SecretRequestCancellation = "request_cancellation"
+)
+
+type SecretRequestEventContent struct {
+	Name               id.Secret           `json:"name,omitempty"`
+	Action             SecretRequestAction `json:"action"`
+	RequestingDeviceID id.DeviceID         `json:"requesting_device_id"`
+	RequestID          string              `json:"request_id"`
+}
+
+type SecretSendEventContent struct {
+	RequestID string `json:"request_id"`
+	Secret    string `json:"secret"`
+}
+
+type DummyEventContent struct{}
diff --git a/vendor/maunium.net/go/mautrix/event/ephemeral.go b/vendor/maunium.net/go/mautrix/event/ephemeral.go
new file mode 100644
index 0000000..f447404
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/ephemeral.go
@@ -0,0 +1,140 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"time"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// TypingEventContent represents the content of a m.typing ephemeral event.
+// https://spec.matrix.org/v1.2/client-server-api/#mtyping
+type TypingEventContent struct {
+	UserIDs []id.UserID `json:"user_ids"`
+}
+
+// ReceiptEventContent represents the content of a m.receipt ephemeral event.
+// https://spec.matrix.org/v1.2/client-server-api/#mreceipt
+type ReceiptEventContent map[id.EventID]Receipts
+
+func (rec ReceiptEventContent) Set(evtID id.EventID, receiptType ReceiptType, userID id.UserID, receipt ReadReceipt) {
+	rec.GetOrCreate(evtID).GetOrCreate(receiptType).Set(userID, receipt)
+}
+
+func (rec ReceiptEventContent) GetOrCreate(evt id.EventID) Receipts {
+	receipts, ok := rec[evt]
+	if !ok {
+		receipts = make(Receipts)
+		rec[evt] = receipts
+	}
+	return receipts
+}
+
+type ReceiptType string
+
+const (
+	ReceiptTypeRead        ReceiptType = "m.read"
+	ReceiptTypeReadPrivate ReceiptType = "m.read.private"
+)
+
+type Receipts map[ReceiptType]UserReceipts
+
+func (rps Receipts) GetOrCreate(receiptType ReceiptType) UserReceipts {
+	read, ok := rps[receiptType]
+	if !ok {
+		read = make(UserReceipts)
+		rps[receiptType] = read
+	}
+	return read
+}
+
+type UserReceipts map[id.UserID]ReadReceipt
+
+func (ur UserReceipts) Set(userID id.UserID, receipt ReadReceipt) {
+	ur[userID] = receipt
+}
+
+type ThreadID = id.EventID
+
+const ReadReceiptThreadMain ThreadID = "main"
+
+type ReadReceipt struct {
+	Timestamp time.Time
+
+	// Thread ID for thread-specific read receipts from MSC3771
+	ThreadID ThreadID
+
+	// Extra contains any unknown fields in the read receipt event.
+	// Most servers don't allow clients to set them, so this will be empty in most cases.
+	Extra map[string]interface{}
+}
+
+func (rr *ReadReceipt) UnmarshalJSON(data []byte) error {
+	// Hacky compatibility hack against crappy clients that send double-encoded read receipts.
+	// TODO is this actually needed? clients can't currently set custom content in receipts 🤔
+	if data[0] == '"' && data[len(data)-1] == '"' {
+		var strData string
+		err := json.Unmarshal(data, &strData)
+		if err != nil {
+			return err
+		}
+		data = []byte(strData)
+	}
+
+	var parsed map[string]interface{}
+	err := json.Unmarshal(data, &parsed)
+	if err != nil {
+		return err
+	}
+	threadID, _ := parsed["thread_id"].(string)
+	ts, tsOK := parsed["ts"].(float64)
+	delete(parsed, "thread_id")
+	delete(parsed, "ts")
+	*rr = ReadReceipt{
+		ThreadID: ThreadID(threadID),
+		Extra:    parsed,
+	}
+	if tsOK {
+		rr.Timestamp = time.UnixMilli(int64(ts))
+	}
+	return nil
+}
+
+func (rr ReadReceipt) MarshalJSON() ([]byte, error) {
+	data := rr.Extra
+	if data == nil {
+		data = make(map[string]interface{})
+	}
+	if rr.ThreadID != "" {
+		data["thread_id"] = rr.ThreadID
+	}
+	if !rr.Timestamp.IsZero() {
+		data["ts"] = rr.Timestamp.UnixMilli()
+	}
+	return json.Marshal(data)
+}
+
+type Presence string
+
+const (
+	PresenceOnline      Presence = "online"
+	PresenceOffline     Presence = "offline"
+	PresenceUnavailable Presence = "unavailable"
+)
+
+// PresenceEventContent represents the content of a m.presence ephemeral event.
+// https://spec.matrix.org/v1.2/client-server-api/#mpresence
+type PresenceEventContent struct {
+	Presence        Presence            `json:"presence"`
+	Displayname     string              `json:"displayname,omitempty"`
+	AvatarURL       id.ContentURIString `json:"avatar_url,omitempty"`
+	LastActiveAgo   int64               `json:"last_active_ago,omitempty"`
+	CurrentlyActive bool                `json:"currently_active,omitempty"`
+	StatusMessage   string              `json:"status_msg,omitempty"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/events.go b/vendor/maunium.net/go/mautrix/event/events.go
new file mode 100644
index 0000000..23769ae
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/events.go
@@ -0,0 +1,156 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"time"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// Event represents a single Matrix event.
+type Event struct {
+	StateKey  *string    `json:"state_key,omitempty"`        // The state key for the event. Only present on State Events.
+	Sender    id.UserID  `json:"sender,omitempty"`           // The user ID of the sender of the event
+	Type      Type       `json:"type"`                       // The event type
+	Timestamp int64      `json:"origin_server_ts,omitempty"` // The unix timestamp when this message was sent by the origin server
+	ID        id.EventID `json:"event_id,omitempty"`         // The unique ID of this event
+	RoomID    id.RoomID  `json:"room_id,omitempty"`          // The room the event was sent to. May be nil (e.g. for presence)
+	Content   Content    `json:"content"`                    // The JSON content of the event.
+	Redacts   id.EventID `json:"redacts,omitempty"`          // The event ID that was redacted if a m.room.redaction event
+	Unsigned  Unsigned   `json:"unsigned,omitempty"`         // Unsigned content set by own homeserver.
+
+	Mautrix MautrixInfo `json:"-"`
+
+	ToUserID   id.UserID   `json:"to_user_id,omitempty"`   // The user ID that the to-device event was sent to. Only present in MSC2409 appservice transactions.
+	ToDeviceID id.DeviceID `json:"to_device_id,omitempty"` // The device ID that the to-device event was sent to. Only present in MSC2409 appservice transactions.
+}
+
+type eventForMarshaling struct {
+	StateKey  *string    `json:"state_key,omitempty"`
+	Sender    id.UserID  `json:"sender,omitempty"`
+	Type      Type       `json:"type"`
+	Timestamp int64      `json:"origin_server_ts,omitempty"`
+	ID        id.EventID `json:"event_id,omitempty"`
+	RoomID    id.RoomID  `json:"room_id,omitempty"`
+	Content   Content    `json:"content"`
+	Redacts   id.EventID `json:"redacts,omitempty"`
+	Unsigned  *Unsigned  `json:"unsigned,omitempty"`
+
+	PrevContent   *Content    `json:"prev_content,omitempty"`
+	ReplacesState *id.EventID `json:"replaces_state,omitempty"`
+
+	ToUserID   id.UserID   `json:"to_user_id,omitempty"`
+	ToDeviceID id.DeviceID `json:"to_device_id,omitempty"`
+}
+
+// UnmarshalJSON unmarshals the event, including moving prev_content from the top level to inside unsigned.
+func (evt *Event) UnmarshalJSON(data []byte) error {
+	var efm eventForMarshaling
+	err := json.Unmarshal(data, &efm)
+	if err != nil {
+		return err
+	}
+	evt.StateKey = efm.StateKey
+	evt.Sender = efm.Sender
+	evt.Type = efm.Type
+	evt.Timestamp = efm.Timestamp
+	evt.ID = efm.ID
+	evt.RoomID = efm.RoomID
+	evt.Content = efm.Content
+	evt.Redacts = efm.Redacts
+	if efm.Unsigned != nil {
+		evt.Unsigned = *efm.Unsigned
+	}
+	if efm.PrevContent != nil && evt.Unsigned.PrevContent == nil {
+		evt.Unsigned.PrevContent = efm.PrevContent
+	}
+	if efm.ReplacesState != nil && *efm.ReplacesState != "" && evt.Unsigned.ReplacesState == "" {
+		evt.Unsigned.ReplacesState = *efm.ReplacesState
+	}
+	evt.ToUserID = efm.ToUserID
+	evt.ToDeviceID = efm.ToDeviceID
+	return nil
+}
+
+// MarshalJSON marshals the event, including omitting the unsigned field if it's empty.
+//
+// This is necessary because Unsigned is not a pointer (for convenience reasons),
+// and encoding/json doesn't know how to check if a non-pointer struct is empty.
+//
+// TODO(tulir): maybe it makes more sense to make Unsigned a pointer and make an easy and safe way to access it?
+func (evt *Event) MarshalJSON() ([]byte, error) {
+	unsigned := &evt.Unsigned
+	if unsigned.IsEmpty() {
+		unsigned = nil
+	}
+	return json.Marshal(&eventForMarshaling{
+		StateKey:   evt.StateKey,
+		Sender:     evt.Sender,
+		Type:       evt.Type,
+		Timestamp:  evt.Timestamp,
+		ID:         evt.ID,
+		RoomID:     evt.RoomID,
+		Content:    evt.Content,
+		Redacts:    evt.Redacts,
+		Unsigned:   unsigned,
+		ToUserID:   evt.ToUserID,
+		ToDeviceID: evt.ToDeviceID,
+	})
+}
+
+type MautrixInfo struct {
+	EventSource Source
+
+	TrustState    id.TrustState
+	ForwardedKeys bool
+	WasEncrypted  bool
+	TrustSource   *id.Device
+
+	ReceivedAt         time.Time
+	EditedAt           time.Time
+	LastEditID         id.EventID
+	DecryptionDuration time.Duration
+
+	CheckpointSent bool
+}
+
+func (evt *Event) GetStateKey() string {
+	if evt.StateKey != nil {
+		return *evt.StateKey
+	}
+	return ""
+}
+
+type StrippedState struct {
+	Content  Content   `json:"content"`
+	Type     Type      `json:"type"`
+	StateKey string    `json:"state_key"`
+	Sender   id.UserID `json:"sender"`
+}
+
+type Unsigned struct {
+	PrevContent     *Content        `json:"prev_content,omitempty"`
+	PrevSender      id.UserID       `json:"prev_sender,omitempty"`
+	ReplacesState   id.EventID      `json:"replaces_state,omitempty"`
+	Age             int64           `json:"age,omitempty"`
+	TransactionID   string          `json:"transaction_id,omitempty"`
+	Relations       *Relations      `json:"m.relations,omitempty"`
+	RedactedBecause *Event          `json:"redacted_because,omitempty"`
+	InviteRoomState []StrippedState `json:"invite_room_state,omitempty"`
+
+	BeeperHSOrder    int64 `json:"com.beeper.hs.order,omitempty"`
+	BeeperHSSuborder int64 `json:"com.beeper.hs.suborder,omitempty"`
+	BeeperFromBackup bool  `json:"com.beeper.from_backup,omitempty"`
+}
+
+func (us *Unsigned) IsEmpty() bool {
+	return us.PrevContent == nil && us.PrevSender == "" && us.ReplacesState == "" && us.Age == 0 &&
+		us.TransactionID == "" && us.RedactedBecause == nil && us.InviteRoomState == nil && us.Relations == nil &&
+		us.BeeperHSOrder == 0 && us.BeeperHSSuborder == 0
+}
diff --git a/vendor/maunium.net/go/mautrix/event/eventsource.go b/vendor/maunium.net/go/mautrix/event/eventsource.go
new file mode 100644
index 0000000..86c1ceb
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/eventsource.go
@@ -0,0 +1,72 @@
+// Copyright (c) 2024 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"fmt"
+)
+
+// Source represents the part of the sync response that an event came from.
+type Source int
+
+const (
+	SourcePresence Source = 1 << iota
+	SourceJoin
+	SourceInvite
+	SourceLeave
+	SourceAccountData
+	SourceTimeline
+	SourceState
+	SourceEphemeral
+	SourceToDevice
+	SourceDecrypted
+)
+
+const primaryTypes = SourcePresence | SourceAccountData | SourceToDevice | SourceTimeline | SourceState
+const roomSections = SourceJoin | SourceInvite | SourceLeave
+const roomableTypes = SourceAccountData | SourceTimeline | SourceState
+const encryptableTypes = roomableTypes | SourceToDevice
+
+func (es Source) String() string {
+	var typeName string
+	switch es & primaryTypes {
+	case SourcePresence:
+		typeName = "presence"
+	case SourceAccountData:
+		typeName = "account data"
+	case SourceToDevice:
+		typeName = "to-device"
+	case SourceTimeline:
+		typeName = "timeline"
+	case SourceState:
+		typeName = "state"
+	default:
+		return fmt.Sprintf("unknown (%d)", es)
+	}
+	if es&roomableTypes != 0 {
+		switch es & roomSections {
+		case SourceJoin:
+			typeName = "joined room " + typeName
+		case SourceInvite:
+			typeName = "invited room " + typeName
+		case SourceLeave:
+			typeName = "left room " + typeName
+		default:
+			return fmt.Sprintf("unknown (%s+%d)", typeName, es)
+		}
+		es &^= roomSections
+	}
+	if es&encryptableTypes != 0 && es&SourceDecrypted != 0 {
+		typeName += " (decrypted)"
+		es &^= SourceDecrypted
+	}
+	es &^= primaryTypes
+	if es != 0 {
+		return fmt.Sprintf("unknown (%s+%d)", typeName, es)
+	}
+	return typeName
+}
diff --git a/vendor/maunium.net/go/mautrix/event/member.go b/vendor/maunium.net/go/mautrix/event/member.go
new file mode 100644
index 0000000..ebafdcb
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/member.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// Membership is an enum specifying the membership state of a room member.
+type Membership string
+
+func (ms Membership) IsInviteOrJoin() bool {
+	return ms == MembershipJoin || ms == MembershipInvite
+}
+
+func (ms Membership) IsLeaveOrBan() bool {
+	return ms == MembershipLeave || ms == MembershipBan
+}
+
+// The allowed membership states as specified in spec section 10.5.5.
+const (
+	MembershipJoin   Membership = "join"
+	MembershipLeave  Membership = "leave"
+	MembershipInvite Membership = "invite"
+	MembershipBan    Membership = "ban"
+	MembershipKnock  Membership = "knock"
+)
+
+// MemberEventContent represents the content of a m.room.member state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroommember
+type MemberEventContent struct {
+	Membership       Membership          `json:"membership"`
+	AvatarURL        id.ContentURIString `json:"avatar_url,omitempty"`
+	Displayname      string              `json:"displayname,omitempty"`
+	IsDirect         bool                `json:"is_direct,omitempty"`
+	ThirdPartyInvite *ThirdPartyInvite   `json:"third_party_invite,omitempty"`
+	Reason           string              `json:"reason,omitempty"`
+}
+
+type ThirdPartyInvite struct {
+	DisplayName string `json:"display_name"`
+	Signed      struct {
+		Token      string          `json:"token"`
+		Signatures json.RawMessage `json:"signatures"`
+		MXID       string          `json:"mxid"`
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/event/message.go b/vendor/maunium.net/go/mautrix/event/message.go
new file mode 100644
index 0000000..9badd9a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/message.go
@@ -0,0 +1,356 @@
+// Copyright (c) 2023 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"slices"
+	"strconv"
+	"strings"
+
+	"golang.org/x/net/html"
+
+	"maunium.net/go/mautrix/crypto/attachment"
+	"maunium.net/go/mautrix/id"
+)
+
+// MessageType is the sub-type of a m.room.message event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes
+type MessageType string
+
+func (mt MessageType) IsText() bool {
+	switch mt {
+	case MsgText, MsgNotice, MsgEmote:
+		return true
+	default:
+		return false
+	}
+}
+
+func (mt MessageType) IsMedia() bool {
+	switch mt {
+	case MsgImage, MsgVideo, MsgAudio, MsgFile, MessageType(EventSticker.Type):
+		return true
+	default:
+		return false
+	}
+}
+
+// Msgtypes
+const (
+	MsgText     MessageType = "m.text"
+	MsgEmote    MessageType = "m.emote"
+	MsgNotice   MessageType = "m.notice"
+	MsgImage    MessageType = "m.image"
+	MsgLocation MessageType = "m.location"
+	MsgVideo    MessageType = "m.video"
+	MsgAudio    MessageType = "m.audio"
+	MsgFile     MessageType = "m.file"
+
+	MsgVerificationRequest MessageType = "m.key.verification.request"
+
+	MsgBeeperGallery MessageType = "com.beeper.gallery"
+)
+
+// Format specifies the format of the formatted_body in m.room.message events.
+// https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes
+type Format string
+
+// Message formats
+const (
+	FormatHTML Format = "org.matrix.custom.html"
+)
+
+// RedactionEventContent represents the content of a m.room.redaction message event.
+//
+// https://spec.matrix.org/v1.8/client-server-api/#mroomredaction
+type RedactionEventContent struct {
+	Reason string `json:"reason,omitempty"`
+
+	// The event ID is here as of room v11. In old servers it may only be at the top level.
+	Redacts id.EventID `json:"redacts,omitempty"`
+}
+
+// ReactionEventContent represents the content of a m.reaction message event.
+// This is not yet in a spec release, see https://github.com/matrix-org/matrix-doc/pull/1849
+type ReactionEventContent struct {
+	RelatesTo RelatesTo `json:"m.relates_to"`
+}
+
+func (content *ReactionEventContent) GetRelatesTo() *RelatesTo {
+	return &content.RelatesTo
+}
+
+func (content *ReactionEventContent) OptionalGetRelatesTo() *RelatesTo {
+	return &content.RelatesTo
+}
+
+func (content *ReactionEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = *rel
+}
+
+// MessageEventContent represents the content of a m.room.message event.
+//
+// It is also used to represent m.sticker events, as they are equivalent to m.room.message
+// with the exception of the msgtype field.
+//
+// https://spec.matrix.org/v1.2/client-server-api/#mroommessage
+type MessageEventContent struct {
+	// Base m.room.message fields
+	MsgType MessageType `json:"msgtype,omitempty"`
+	Body    string      `json:"body"`
+
+	// Extra fields for text types
+	Format        Format `json:"format,omitempty"`
+	FormattedBody string `json:"formatted_body,omitempty"`
+
+	// Extra field for m.location
+	GeoURI string `json:"geo_uri,omitempty"`
+
+	// Extra fields for media types
+	URL  id.ContentURIString `json:"url,omitempty"`
+	Info *FileInfo           `json:"info,omitempty"`
+	File *EncryptedFileInfo  `json:"file,omitempty"`
+
+	FileName string `json:"filename,omitempty"`
+
+	Mentions *Mentions `json:"m.mentions,omitempty"`
+
+	// Edits and relations
+	NewContent *MessageEventContent `json:"m.new_content,omitempty"`
+	RelatesTo  *RelatesTo           `json:"m.relates_to,omitempty"`
+
+	// In-room verification
+	To         id.UserID            `json:"to,omitempty"`
+	FromDevice id.DeviceID          `json:"from_device,omitempty"`
+	Methods    []VerificationMethod `json:"methods,omitempty"`
+
+	replyFallbackRemoved bool
+
+	MessageSendRetry         *BeeperRetryMetadata     `json:"com.beeper.message_send_retry,omitempty"`
+	BeeperGalleryImages      []*MessageEventContent   `json:"com.beeper.gallery.images,omitempty"`
+	BeeperGalleryCaption     string                   `json:"com.beeper.gallery.caption,omitempty"`
+	BeeperGalleryCaptionHTML string                   `json:"com.beeper.gallery.caption_html,omitempty"`
+	BeeperPerMessageProfile  *BeeperPerMessageProfile `json:"com.beeper.per_message_profile,omitempty"`
+
+	BeeperLinkPreviews []*BeeperLinkPreview `json:"com.beeper.linkpreviews,omitempty"`
+
+	MSC1767Audio *MSC1767Audio `json:"org.matrix.msc1767.audio,omitempty"`
+	MSC3245Voice *MSC3245Voice `json:"org.matrix.msc3245.voice,omitempty"`
+}
+
+func (content *MessageEventContent) GetFileName() string {
+	if content.FileName != "" {
+		return content.FileName
+	}
+	return content.Body
+}
+
+func (content *MessageEventContent) GetCaption() string {
+	if content.FileName != "" && content.Body != "" && content.Body != content.FileName {
+		return content.Body
+	}
+	return ""
+}
+
+func (content *MessageEventContent) GetFormattedCaption() string {
+	if content.Format == FormatHTML && content.FormattedBody != "" {
+		return content.FormattedBody
+	}
+	return ""
+}
+
+func (content *MessageEventContent) GetRelatesTo() *RelatesTo {
+	if content.RelatesTo == nil {
+		content.RelatesTo = &RelatesTo{}
+	}
+	return content.RelatesTo
+}
+
+func (content *MessageEventContent) OptionalGetRelatesTo() *RelatesTo {
+	return content.RelatesTo
+}
+
+func (content *MessageEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = rel
+}
+
+func (content *MessageEventContent) SetEdit(original id.EventID) {
+	newContent := *content
+	content.NewContent = &newContent
+	content.RelatesTo = (&RelatesTo{}).SetReplace(original)
+	if content.MsgType == MsgText || content.MsgType == MsgNotice {
+		content.Body = "* " + content.Body
+		if content.Format == FormatHTML && len(content.FormattedBody) > 0 {
+			content.FormattedBody = "* " + content.FormattedBody
+		}
+		// If the message is long, remove most of the useless edit fallback to avoid event size issues.
+		if len(content.Body) > 10000 {
+			content.FormattedBody = ""
+			content.Format = ""
+			content.Body = content.Body[:50] + "[edit fallback cut…]"
+		}
+	}
+}
+
+// TextToHTML converts the given text to a HTML-safe representation by escaping HTML characters
+// and replacing newlines with <br/> tags.
+func TextToHTML(text string) string {
+	return strings.ReplaceAll(html.EscapeString(text), "\n", "<br/>")
+}
+
+// ReverseTextToHTML reverses the modifications made by TextToHTML, i.e. replaces <br/> tags with newlines
+// and unescapes HTML escape codes. For actually parsing HTML, use the format package instead.
+func ReverseTextToHTML(input string) string {
+	return html.UnescapeString(strings.ReplaceAll(input, "<br/>", "\n"))
+}
+
+func (content *MessageEventContent) EnsureHasHTML() {
+	if len(content.FormattedBody) == 0 || content.Format != FormatHTML {
+		content.FormattedBody = TextToHTML(content.Body)
+		content.Format = FormatHTML
+	}
+}
+
+func (content *MessageEventContent) GetFile() *EncryptedFileInfo {
+	if content.File == nil {
+		content.File = &EncryptedFileInfo{}
+	}
+	return content.File
+}
+
+func (content *MessageEventContent) GetInfo() *FileInfo {
+	if content.Info == nil {
+		content.Info = &FileInfo{}
+	}
+	return content.Info
+}
+
+type Mentions struct {
+	UserIDs []id.UserID `json:"user_ids,omitempty"`
+	Room    bool        `json:"room,omitempty"`
+}
+
+func (m *Mentions) Add(userID id.UserID) {
+	if userID != "" && !slices.Contains(m.UserIDs, userID) {
+		m.UserIDs = append(m.UserIDs, userID)
+	}
+}
+
+func (m *Mentions) Has(userID id.UserID) bool {
+	return m != nil && slices.Contains(m.UserIDs, userID)
+}
+
+type EncryptedFileInfo struct {
+	attachment.EncryptedFile
+	URL id.ContentURIString `json:"url"`
+}
+
+type FileInfo struct {
+	MimeType      string              `json:"mimetype,omitempty"`
+	ThumbnailInfo *FileInfo           `json:"thumbnail_info,omitempty"`
+	ThumbnailURL  id.ContentURIString `json:"thumbnail_url,omitempty"`
+	ThumbnailFile *EncryptedFileInfo  `json:"thumbnail_file,omitempty"`
+
+	Blurhash     string `json:"blurhash,omitempty"`
+	AnoaBlurhash string `json:"xyz.amorgan.blurhash,omitempty"`
+
+	Width    int `json:"-"`
+	Height   int `json:"-"`
+	Duration int `json:"-"`
+	Size     int `json:"-"`
+}
+
+type serializableFileInfo struct {
+	MimeType      string                `json:"mimetype,omitempty"`
+	ThumbnailInfo *serializableFileInfo `json:"thumbnail_info,omitempty"`
+	ThumbnailURL  id.ContentURIString   `json:"thumbnail_url,omitempty"`
+	ThumbnailFile *EncryptedFileInfo    `json:"thumbnail_file,omitempty"`
+
+	Blurhash     string `json:"blurhash,omitempty"`
+	AnoaBlurhash string `json:"xyz.amorgan.blurhash,omitempty"`
+
+	Width    json.Number `json:"w,omitempty"`
+	Height   json.Number `json:"h,omitempty"`
+	Duration json.Number `json:"duration,omitempty"`
+	Size     json.Number `json:"size,omitempty"`
+}
+
+func (sfi *serializableFileInfo) CopyFrom(fileInfo *FileInfo) *serializableFileInfo {
+	if fileInfo == nil {
+		return nil
+	}
+	*sfi = serializableFileInfo{
+		MimeType:      fileInfo.MimeType,
+		ThumbnailURL:  fileInfo.ThumbnailURL,
+		ThumbnailInfo: (&serializableFileInfo{}).CopyFrom(fileInfo.ThumbnailInfo),
+		ThumbnailFile: fileInfo.ThumbnailFile,
+
+		Blurhash:     fileInfo.Blurhash,
+		AnoaBlurhash: fileInfo.AnoaBlurhash,
+	}
+	if fileInfo.Width > 0 {
+		sfi.Width = json.Number(strconv.Itoa(fileInfo.Width))
+	}
+	if fileInfo.Height > 0 {
+		sfi.Height = json.Number(strconv.Itoa(fileInfo.Height))
+	}
+	if fileInfo.Size > 0 {
+		sfi.Size = json.Number(strconv.Itoa(fileInfo.Size))
+
+	}
+	if fileInfo.Duration > 0 {
+		sfi.Duration = json.Number(strconv.Itoa(int(fileInfo.Duration)))
+	}
+	return sfi
+}
+
+func (sfi *serializableFileInfo) CopyTo(fileInfo *FileInfo) {
+	*fileInfo = FileInfo{
+		Width:         numberToInt(sfi.Width),
+		Height:        numberToInt(sfi.Height),
+		Size:          numberToInt(sfi.Size),
+		Duration:      numberToInt(sfi.Duration),
+		MimeType:      sfi.MimeType,
+		ThumbnailURL:  sfi.ThumbnailURL,
+		ThumbnailFile: sfi.ThumbnailFile,
+		Blurhash:      sfi.Blurhash,
+		AnoaBlurhash:  sfi.AnoaBlurhash,
+	}
+	if sfi.ThumbnailInfo != nil {
+		fileInfo.ThumbnailInfo = &FileInfo{}
+		sfi.ThumbnailInfo.CopyTo(fileInfo.ThumbnailInfo)
+	}
+}
+
+func (fileInfo *FileInfo) UnmarshalJSON(data []byte) error {
+	sfi := &serializableFileInfo{}
+	if err := json.Unmarshal(data, sfi); err != nil {
+		return err
+	}
+	sfi.CopyTo(fileInfo)
+	return nil
+}
+
+func (fileInfo *FileInfo) MarshalJSON() ([]byte, error) {
+	return json.Marshal((&serializableFileInfo{}).CopyFrom(fileInfo))
+}
+
+func numberToInt(val json.Number) int {
+	f64, _ := val.Float64()
+	if f64 > 0 {
+		return int(f64)
+	}
+	return 0
+}
+
+func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo {
+	if fileInfo.ThumbnailInfo == nil {
+		fileInfo.ThumbnailInfo = &FileInfo{}
+	}
+	return fileInfo.ThumbnailInfo
+}
diff --git a/vendor/maunium.net/go/mautrix/event/poll.go b/vendor/maunium.net/go/mautrix/event/poll.go
new file mode 100644
index 0000000..3733301
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/poll.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2024 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+type PollResponseEventContent struct {
+	RelatesTo RelatesTo `json:"m.relates_to"`
+	Response  struct {
+		Answers []string `json:"answers"`
+	} `json:"org.matrix.msc3381.poll.response"`
+}
+
+func (content *PollResponseEventContent) GetRelatesTo() *RelatesTo {
+	return &content.RelatesTo
+}
+
+func (content *PollResponseEventContent) OptionalGetRelatesTo() *RelatesTo {
+	if content.RelatesTo.Type == "" {
+		return nil
+	}
+	return &content.RelatesTo
+}
+
+func (content *PollResponseEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = *rel
+}
+
+type MSC1767Message struct {
+	Text    string `json:"org.matrix.msc1767.text,omitempty"`
+	HTML    string `json:"org.matrix.msc1767.html,omitempty"`
+	Message []struct {
+		MimeType string `json:"mimetype"`
+		Body     string `json:"body"`
+	} `json:"org.matrix.msc1767.message,omitempty"`
+}
+
+type PollStartEventContent struct {
+	RelatesTo *RelatesTo `json:"m.relates_to"`
+	Mentions  *Mentions  `json:"m.mentions,omitempty"`
+	PollStart struct {
+		Kind          string         `json:"kind"`
+		MaxSelections int            `json:"max_selections"`
+		Question      MSC1767Message `json:"question"`
+		Answers       []struct {
+			ID string `json:"id"`
+			MSC1767Message
+		} `json:"answers"`
+	} `json:"org.matrix.msc3381.poll.start"`
+}
+
+func (content *PollStartEventContent) GetRelatesTo() *RelatesTo {
+	if content.RelatesTo == nil {
+		content.RelatesTo = &RelatesTo{}
+	}
+	return content.RelatesTo
+}
+
+func (content *PollStartEventContent) OptionalGetRelatesTo() *RelatesTo {
+	return content.RelatesTo
+}
+
+func (content *PollStartEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = rel
+}
diff --git a/vendor/maunium.net/go/mautrix/event/powerlevels.go b/vendor/maunium.net/go/mautrix/event/powerlevels.go
new file mode 100644
index 0000000..2f4d457
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/powerlevels.go
@@ -0,0 +1,199 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"sync"
+
+	"go.mau.fi/util/ptr"
+	"golang.org/x/exp/maps"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// PowerLevelsEventContent represents the content of a m.room.power_levels state event content.
+// https://spec.matrix.org/v1.5/client-server-api/#mroompower_levels
+type PowerLevelsEventContent struct {
+	usersLock    sync.RWMutex
+	Users        map[id.UserID]int `json:"users,omitempty"`
+	UsersDefault int               `json:"users_default,omitempty"`
+
+	eventsLock    sync.RWMutex
+	Events        map[string]int `json:"events,omitempty"`
+	EventsDefault int            `json:"events_default,omitempty"`
+
+	Notifications *NotificationPowerLevels `json:"notifications,omitempty"`
+
+	StateDefaultPtr *int `json:"state_default,omitempty"`
+
+	InvitePtr *int `json:"invite,omitempty"`
+	KickPtr   *int `json:"kick,omitempty"`
+	BanPtr    *int `json:"ban,omitempty"`
+	RedactPtr *int `json:"redact,omitempty"`
+}
+
+func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent {
+	if pl == nil {
+		return nil
+	}
+	return &PowerLevelsEventContent{
+		Users:           maps.Clone(pl.Users),
+		UsersDefault:    pl.UsersDefault,
+		Events:          maps.Clone(pl.Events),
+		EventsDefault:   pl.EventsDefault,
+		StateDefaultPtr: ptr.Clone(pl.StateDefaultPtr),
+
+		Notifications: pl.Notifications.Clone(),
+
+		InvitePtr: ptr.Clone(pl.InvitePtr),
+		KickPtr:   ptr.Clone(pl.KickPtr),
+		BanPtr:    ptr.Clone(pl.BanPtr),
+		RedactPtr: ptr.Clone(pl.RedactPtr),
+	}
+}
+
+type NotificationPowerLevels struct {
+	RoomPtr *int `json:"room,omitempty"`
+}
+
+func (npl *NotificationPowerLevels) Clone() *NotificationPowerLevels {
+	if npl == nil {
+		return nil
+	}
+	return &NotificationPowerLevels{
+		RoomPtr: ptr.Clone(npl.RoomPtr),
+	}
+}
+
+func (npl *NotificationPowerLevels) Room() int {
+	if npl != nil && npl.RoomPtr != nil {
+		return *npl.RoomPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) Invite() int {
+	if pl.InvitePtr != nil {
+		return *pl.InvitePtr
+	}
+	return 0
+}
+
+func (pl *PowerLevelsEventContent) Kick() int {
+	if pl.KickPtr != nil {
+		return *pl.KickPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) Ban() int {
+	if pl.BanPtr != nil {
+		return *pl.BanPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) Redact() int {
+	if pl.RedactPtr != nil {
+		return *pl.RedactPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) StateDefault() int {
+	if pl.StateDefaultPtr != nil {
+		return *pl.StateDefaultPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int {
+	pl.usersLock.RLock()
+	defer pl.usersLock.RUnlock()
+	level, ok := pl.Users[userID]
+	if !ok {
+		return pl.UsersDefault
+	}
+	return level
+}
+
+func (pl *PowerLevelsEventContent) SetUserLevel(userID id.UserID, level int) {
+	pl.usersLock.Lock()
+	defer pl.usersLock.Unlock()
+	if level == pl.UsersDefault {
+		delete(pl.Users, userID)
+	} else {
+		if pl.Users == nil {
+			pl.Users = make(map[id.UserID]int)
+		}
+		pl.Users[userID] = level
+	}
+}
+
+func (pl *PowerLevelsEventContent) EnsureUserLevel(target id.UserID, level int) bool {
+	return pl.EnsureUserLevelAs("", target, level)
+}
+
+func (pl *PowerLevelsEventContent) EnsureUserLevelAs(actor, target id.UserID, level int) bool {
+	existingLevel := pl.GetUserLevel(target)
+	if actor != "" {
+		actorLevel := pl.GetUserLevel(actor)
+		if actorLevel <= existingLevel || actorLevel < level {
+			return false
+		}
+	}
+	if existingLevel != level {
+		pl.SetUserLevel(target, level)
+		return true
+	}
+	return false
+}
+
+func (pl *PowerLevelsEventContent) GetEventLevel(eventType Type) int {
+	pl.eventsLock.RLock()
+	defer pl.eventsLock.RUnlock()
+	level, ok := pl.Events[eventType.String()]
+	if !ok {
+		if eventType.IsState() {
+			return pl.StateDefault()
+		}
+		return pl.EventsDefault
+	}
+	return level
+}
+
+func (pl *PowerLevelsEventContent) SetEventLevel(eventType Type, level int) {
+	pl.eventsLock.Lock()
+	defer pl.eventsLock.Unlock()
+	if (eventType.IsState() && level == pl.StateDefault()) || (!eventType.IsState() && level == pl.EventsDefault) {
+		delete(pl.Events, eventType.String())
+	} else {
+		if pl.Events == nil {
+			pl.Events = make(map[string]int)
+		}
+		pl.Events[eventType.String()] = level
+	}
+}
+
+func (pl *PowerLevelsEventContent) EnsureEventLevel(eventType Type, level int) bool {
+	return pl.EnsureEventLevelAs("", eventType, level)
+}
+
+func (pl *PowerLevelsEventContent) EnsureEventLevelAs(actor id.UserID, eventType Type, level int) bool {
+	existingLevel := pl.GetEventLevel(eventType)
+	if actor != "" {
+		actorLevel := pl.GetUserLevel(actor)
+		if existingLevel > actorLevel || level > actorLevel {
+			return false
+		}
+	}
+	if existingLevel != level {
+		pl.SetEventLevel(eventType, level)
+		return true
+	}
+	return false
+}
diff --git a/vendor/maunium.net/go/mautrix/event/relations.go b/vendor/maunium.net/go/mautrix/event/relations.go
new file mode 100644
index 0000000..ea40cc0
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/relations.go
@@ -0,0 +1,234 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type RelationType string
+
+const (
+	RelReplace    RelationType = "m.replace"
+	RelReference  RelationType = "m.reference"
+	RelAnnotation RelationType = "m.annotation"
+	RelThread     RelationType = "m.thread"
+)
+
+type RelatesTo struct {
+	Type    RelationType `json:"rel_type,omitempty"`
+	EventID id.EventID   `json:"event_id,omitempty"`
+	Key     string       `json:"key,omitempty"`
+
+	InReplyTo     *InReplyTo `json:"m.in_reply_to,omitempty"`
+	IsFallingBack bool       `json:"is_falling_back,omitempty"`
+}
+
+type InReplyTo struct {
+	EventID id.EventID `json:"event_id,omitempty"`
+
+	UnstableRoomID id.RoomID `json:"room_id,omitempty"`
+}
+
+func (rel *RelatesTo) Copy() *RelatesTo {
+	if rel == nil {
+		return nil
+	}
+	cp := *rel
+	return &cp
+}
+
+func (rel *RelatesTo) GetReplaceID() id.EventID {
+	if rel != nil && rel.Type == RelReplace {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetReferenceID() id.EventID {
+	if rel != nil && rel.Type == RelReference {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetThreadParent() id.EventID {
+	if rel != nil && rel.Type == RelThread {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetReplyTo() id.EventID {
+	if rel != nil && rel.InReplyTo != nil {
+		return rel.InReplyTo.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetNonFallbackReplyTo() id.EventID {
+	if rel != nil && rel.InReplyTo != nil && (rel.Type != RelThread || !rel.IsFallingBack) {
+		return rel.InReplyTo.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetAnnotationID() id.EventID {
+	if rel != nil && rel.Type == RelAnnotation {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetAnnotationKey() string {
+	if rel != nil && rel.Type == RelAnnotation {
+		return rel.Key
+	}
+	return ""
+}
+
+func (rel *RelatesTo) SetReplace(mxid id.EventID) *RelatesTo {
+	rel.Type = RelReplace
+	rel.EventID = mxid
+	return rel
+}
+
+func (rel *RelatesTo) SetReplyTo(mxid id.EventID) *RelatesTo {
+	rel.InReplyTo = &InReplyTo{EventID: mxid}
+	rel.IsFallingBack = false
+	return rel
+}
+
+func (rel *RelatesTo) SetThread(mxid, fallback id.EventID) *RelatesTo {
+	rel.Type = RelThread
+	rel.EventID = mxid
+	if fallback != "" && rel.GetReplyTo() == "" {
+		rel.SetReplyTo(fallback)
+		rel.IsFallingBack = true
+	}
+	return rel
+}
+
+func (rel *RelatesTo) SetAnnotation(mxid id.EventID, key string) *RelatesTo {
+	rel.Type = RelAnnotation
+	rel.EventID = mxid
+	rel.Key = key
+	return rel
+}
+
+type RelationChunkItem struct {
+	Type    RelationType `json:"type"`
+	EventID string       `json:"event_id,omitempty"`
+	Key     string       `json:"key,omitempty"`
+	Count   int          `json:"count,omitempty"`
+}
+
+type RelationChunk struct {
+	Chunk []RelationChunkItem `json:"chunk"`
+
+	Limited bool `json:"limited"`
+	Count   int  `json:"count"`
+}
+
+type AnnotationChunk struct {
+	RelationChunk
+	Map map[string]int `json:"-"`
+}
+
+type serializableAnnotationChunk AnnotationChunk
+
+func (ac *AnnotationChunk) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, (*serializableAnnotationChunk)(ac)); err != nil {
+		return err
+	}
+	ac.Map = make(map[string]int)
+	for _, item := range ac.Chunk {
+		if item.Key != "" {
+			ac.Map[item.Key] += item.Count
+		}
+	}
+	return nil
+}
+
+func (ac *AnnotationChunk) Serialize() RelationChunk {
+	ac.Chunk = make([]RelationChunkItem, len(ac.Map))
+	i := 0
+	for key, count := range ac.Map {
+		ac.Chunk[i] = RelationChunkItem{
+			Type:  RelAnnotation,
+			Key:   key,
+			Count: count,
+		}
+		i++
+	}
+	return ac.RelationChunk
+}
+
+type EventIDChunk struct {
+	RelationChunk
+	List []string `json:"-"`
+}
+
+type serializableEventIDChunk EventIDChunk
+
+func (ec *EventIDChunk) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, (*serializableEventIDChunk)(ec)); err != nil {
+		return err
+	}
+	for _, item := range ec.Chunk {
+		ec.List = append(ec.List, item.EventID)
+	}
+	return nil
+}
+
+func (ec *EventIDChunk) Serialize(typ RelationType) RelationChunk {
+	ec.Chunk = make([]RelationChunkItem, len(ec.List))
+	for i, eventID := range ec.List {
+		ec.Chunk[i] = RelationChunkItem{
+			Type:    typ,
+			EventID: eventID,
+		}
+	}
+	return ec.RelationChunk
+}
+
+type Relations struct {
+	Raw map[RelationType]RelationChunk `json:"-"`
+
+	Annotations AnnotationChunk `json:"m.annotation,omitempty"`
+	References  EventIDChunk    `json:"m.reference,omitempty"`
+	Replaces    EventIDChunk    `json:"m.replace,omitempty"`
+}
+
+type serializableRelations Relations
+
+func (relations *Relations) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, &relations.Raw); err != nil {
+		return err
+	}
+	return json.Unmarshal(data, (*serializableRelations)(relations))
+}
+
+func (relations *Relations) MarshalJSON() ([]byte, error) {
+	if relations.Raw == nil {
+		relations.Raw = make(map[RelationType]RelationChunk)
+	}
+	relations.Raw[RelAnnotation] = relations.Annotations.Serialize()
+	relations.Raw[RelReference] = relations.References.Serialize(RelReference)
+	relations.Raw[RelReplace] = relations.Replaces.Serialize(RelReplace)
+	for key, item := range relations.Raw {
+		if !item.Limited {
+			item.Count = len(item.Chunk)
+		}
+		if item.Count == 0 {
+			delete(relations.Raw, key)
+		}
+	}
+	return json.Marshal(relations.Raw)
+}
diff --git a/vendor/maunium.net/go/mautrix/event/reply.go b/vendor/maunium.net/go/mautrix/event/reply.go
new file mode 100644
index 0000000..73f8cfc
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/reply.go
@@ -0,0 +1,98 @@
+// Copyright (c) 2020 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"maunium.net/go/mautrix/id"
+)
+
+var HTMLReplyFallbackRegex = regexp.MustCompile(`^<mx-reply>[\s\S]+?</mx-reply>`)
+
+func TrimReplyFallbackHTML(html string) string {
+	return HTMLReplyFallbackRegex.ReplaceAllString(html, "")
+}
+
+func TrimReplyFallbackText(text string) string {
+	if (!strings.HasPrefix(text, "> <") && !strings.HasPrefix(text, "> * <")) || !strings.Contains(text, "\n") {
+		return text
+	}
+
+	lines := strings.Split(text, "\n")
+	for len(lines) > 0 && strings.HasPrefix(lines[0], "> ") {
+		lines = lines[1:]
+	}
+	return strings.TrimSpace(strings.Join(lines, "\n"))
+}
+
+func (content *MessageEventContent) RemoveReplyFallback() {
+	if len(content.RelatesTo.GetReplyTo()) > 0 && !content.replyFallbackRemoved {
+		if content.Format == FormatHTML {
+			content.FormattedBody = TrimReplyFallbackHTML(content.FormattedBody)
+		}
+		content.Body = TrimReplyFallbackText(content.Body)
+		content.replyFallbackRemoved = true
+	}
+}
+
+// Deprecated: RelatesTo methods are nil-safe, so RelatesTo.GetReplyTo can be used directly
+func (content *MessageEventContent) GetReplyTo() id.EventID {
+	return content.RelatesTo.GetReplyTo()
+}
+
+const ReplyFormat = `<mx-reply><blockquote><a href="https://matrix.to/#/%s/%s">In reply to</a> <a href="https://matrix.to/#/%s">%s</a><br>%s</blockquote></mx-reply>`
+
+func (evt *Event) GenerateReplyFallbackHTML() string {
+	parsedContent, ok := evt.Content.Parsed.(*MessageEventContent)
+	if !ok {
+		return ""
+	}
+	parsedContent.RemoveReplyFallback()
+	body := parsedContent.FormattedBody
+	if len(body) == 0 {
+		body = TextToHTML(parsedContent.Body)
+	}
+
+	senderDisplayName := evt.Sender
+
+	return fmt.Sprintf(ReplyFormat, evt.RoomID, evt.ID, evt.Sender, senderDisplayName, body)
+}
+
+func (evt *Event) GenerateReplyFallbackText() string {
+	parsedContent, ok := evt.Content.Parsed.(*MessageEventContent)
+	if !ok {
+		return ""
+	}
+	parsedContent.RemoveReplyFallback()
+	body := parsedContent.Body
+	lines := strings.Split(strings.TrimSpace(body), "\n")
+	firstLine, lines := lines[0], lines[1:]
+
+	senderDisplayName := evt.Sender
+
+	var fallbackText strings.Builder
+	_, _ = fmt.Fprintf(&fallbackText, "> <%s> %s", senderDisplayName, firstLine)
+	for _, line := range lines {
+		_, _ = fmt.Fprintf(&fallbackText, "\n> %s", line)
+	}
+	fallbackText.WriteString("\n\n")
+	return fallbackText.String()
+}
+
+func (content *MessageEventContent) SetReply(inReplyTo *Event) {
+	content.RelatesTo = (&RelatesTo{}).SetReplyTo(inReplyTo.ID)
+
+	if content.MsgType == MsgText || content.MsgType == MsgNotice {
+		content.EnsureHasHTML()
+		content.FormattedBody = inReplyTo.GenerateReplyFallbackHTML() + content.FormattedBody
+		content.Body = inReplyTo.GenerateReplyFallbackText() + content.Body
+		content.replyFallbackRemoved = false
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/event/state.go b/vendor/maunium.net/go/mautrix/event/state.go
new file mode 100644
index 0000000..1597289
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/state.go
@@ -0,0 +1,212 @@
+// Copyright (c) 2021 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"maunium.net/go/mautrix/id"
+)
+
+// CanonicalAliasEventContent represents the content of a m.room.canonical_alias state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomcanonical_alias
+type CanonicalAliasEventContent struct {
+	Alias      id.RoomAlias   `json:"alias"`
+	AltAliases []id.RoomAlias `json:"alt_aliases,omitempty"`
+}
+
+// RoomNameEventContent represents the content of a m.room.name state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomname
+type RoomNameEventContent struct {
+	Name string `json:"name"`
+}
+
+// RoomAvatarEventContent represents the content of a m.room.avatar state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomavatar
+type RoomAvatarEventContent struct {
+	URL         id.ContentURIString `json:"url,omitempty"`
+	Info        *FileInfo           `json:"info,omitempty"`
+	MSC3414File *EncryptedFileInfo  `json:"org.matrix.msc3414.file,omitempty"`
+}
+
+// ServerACLEventContent represents the content of a m.room.server_acl state event.
+// https://spec.matrix.org/v1.2/client-server-api/#server-access-control-lists-acls-for-rooms
+type ServerACLEventContent struct {
+	Allow           []string `json:"allow,omitempty"`
+	AllowIPLiterals bool     `json:"allow_ip_literals"`
+	Deny            []string `json:"deny,omitempty"`
+}
+
+// TopicEventContent represents the content of a m.room.topic state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomtopic
+type TopicEventContent struct {
+	Topic string `json:"topic"`
+}
+
+// TombstoneEventContent represents the content of a m.room.tombstone state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomtombstone
+type TombstoneEventContent struct {
+	Body            string    `json:"body"`
+	ReplacementRoom id.RoomID `json:"replacement_room"`
+}
+
+type Predecessor struct {
+	RoomID  id.RoomID  `json:"room_id"`
+	EventID id.EventID `json:"event_id"`
+}
+
+type RoomVersion string
+
+const (
+	RoomV1  RoomVersion = "1"
+	RoomV2  RoomVersion = "2"
+	RoomV3  RoomVersion = "3"
+	RoomV4  RoomVersion = "4"
+	RoomV5  RoomVersion = "5"
+	RoomV6  RoomVersion = "6"
+	RoomV7  RoomVersion = "7"
+	RoomV8  RoomVersion = "8"
+	RoomV9  RoomVersion = "9"
+	RoomV10 RoomVersion = "10"
+	RoomV11 RoomVersion = "11"
+)
+
+// CreateEventContent represents the content of a m.room.create state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomcreate
+type CreateEventContent struct {
+	Type        RoomType     `json:"type,omitempty"`
+	Creator     id.UserID    `json:"creator,omitempty"`
+	Federate    bool         `json:"m.federate,omitempty"`
+	RoomVersion RoomVersion  `json:"room_version,omitempty"`
+	Predecessor *Predecessor `json:"predecessor,omitempty"`
+}
+
+// JoinRule specifies how open a room is to new members.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules
+type JoinRule string
+
+const (
+	JoinRulePublic          JoinRule = "public"
+	JoinRuleKnock           JoinRule = "knock"
+	JoinRuleInvite          JoinRule = "invite"
+	JoinRuleRestricted      JoinRule = "restricted"
+	JoinRuleKnockRestricted JoinRule = "knock_restricted"
+	JoinRulePrivate         JoinRule = "private"
+)
+
+// JoinRulesEventContent represents the content of a m.room.join_rules state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules
+type JoinRulesEventContent struct {
+	JoinRule JoinRule        `json:"join_rule"`
+	Allow    []JoinRuleAllow `json:"allow,omitempty"`
+}
+
+type JoinRuleAllowType string
+
+const (
+	JoinRuleAllowRoomMembership JoinRuleAllowType = "m.room_membership"
+)
+
+type JoinRuleAllow struct {
+	RoomID id.RoomID         `json:"room_id"`
+	Type   JoinRuleAllowType `json:"type"`
+}
+
+// PinnedEventsEventContent represents the content of a m.room.pinned_events state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroompinned_events
+type PinnedEventsEventContent struct {
+	Pinned []id.EventID `json:"pinned"`
+}
+
+// HistoryVisibility specifies who can see new messages.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility
+type HistoryVisibility string
+
+const (
+	HistoryVisibilityInvited       HistoryVisibility = "invited"
+	HistoryVisibilityJoined        HistoryVisibility = "joined"
+	HistoryVisibilityShared        HistoryVisibility = "shared"
+	HistoryVisibilityWorldReadable HistoryVisibility = "world_readable"
+)
+
+// HistoryVisibilityEventContent represents the content of a m.room.history_visibility state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility
+type HistoryVisibilityEventContent struct {
+	HistoryVisibility HistoryVisibility `json:"history_visibility"`
+}
+
+// GuestAccess specifies whether or not guest accounts can join.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access
+type GuestAccess string
+
+const (
+	GuestAccessCanJoin   GuestAccess = "can_join"
+	GuestAccessForbidden GuestAccess = "forbidden"
+)
+
+// GuestAccessEventContent represents the content of a m.room.guest_access state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access
+type GuestAccessEventContent struct {
+	GuestAccess GuestAccess `json:"guest_access"`
+}
+
+type BridgeInfoSection struct {
+	ID          string              `json:"id"`
+	DisplayName string              `json:"displayname,omitempty"`
+	AvatarURL   id.ContentURIString `json:"avatar_url,omitempty"`
+	ExternalURL string              `json:"external_url,omitempty"`
+
+	Receiver string `json:"fi.mau.receiver,omitempty"`
+}
+
+// BridgeEventContent represents the content of a m.bridge state event.
+// https://github.com/matrix-org/matrix-doc/pull/2346
+type BridgeEventContent struct {
+	BridgeBot id.UserID          `json:"bridgebot"`
+	Creator   id.UserID          `json:"creator,omitempty"`
+	Protocol  BridgeInfoSection  `json:"protocol"`
+	Network   *BridgeInfoSection `json:"network,omitempty"`
+	Channel   BridgeInfoSection  `json:"channel"`
+
+	BeeperRoomType   string `json:"com.beeper.room_type,omitempty"`
+	BeeperRoomTypeV2 string `json:"com.beeper.room_type.v2,omitempty"`
+}
+
+type SpaceChildEventContent struct {
+	Via       []string `json:"via,omitempty"`
+	Order     string   `json:"order,omitempty"`
+	Suggested bool     `json:"suggested,omitempty"`
+}
+
+type SpaceParentEventContent struct {
+	Via       []string `json:"via,omitempty"`
+	Canonical bool     `json:"canonical,omitempty"`
+}
+
+type PolicyRecommendation string
+
+const (
+	PolicyRecommendationBan         PolicyRecommendation = "m.ban"
+	PolicyRecommendationUnstableBan PolicyRecommendation = "org.matrix.mjolnir.ban"
+	PolicyRecommendationUnban       PolicyRecommendation = "fi.mau.meowlnir.unban"
+)
+
+// ModPolicyContent represents the content of a m.room.rule.user, m.room.rule.room, and m.room.rule.server state event.
+// https://spec.matrix.org/v1.2/client-server-api/#moderation-policy-lists
+type ModPolicyContent struct {
+	Entity         string               `json:"entity"`
+	Reason         string               `json:"reason"`
+	Recommendation PolicyRecommendation `json:"recommendation"`
+}
+
+// Deprecated: MSC2716 has been abandoned
+type InsertionMarkerContent struct {
+	InsertionID id.EventID `json:"org.matrix.msc2716.marker.insertion"`
+	Timestamp   int64      `json:"com.beeper.timestamp,omitempty"`
+}
+
+type ElementFunctionalMembersContent struct {
+	ServiceMembers []id.UserID `json:"service_members"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/type.go b/vendor/maunium.net/go/mautrix/event/type.go
new file mode 100644
index 0000000..4396c9c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/type.go
@@ -0,0 +1,290 @@
+// Copyright (c) 2021 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type RoomType string
+
+const (
+	RoomTypeDefault RoomType = ""
+	RoomTypeSpace   RoomType = "m.space"
+)
+
+type TypeClass int
+
+func (tc TypeClass) Name() string {
+	switch tc {
+	case MessageEventType:
+		return "message"
+	case StateEventType:
+		return "state"
+	case EphemeralEventType:
+		return "ephemeral"
+	case AccountDataEventType:
+		return "account data"
+	case ToDeviceEventType:
+		return "to-device"
+	default:
+		return "unknown"
+	}
+}
+
+const (
+	// Unknown events
+	UnknownEventType TypeClass = iota
+	// Normal message events
+	MessageEventType
+	// State events
+	StateEventType
+	// Ephemeral events
+	EphemeralEventType
+	// Account data events
+	AccountDataEventType
+	// Device-to-device events
+	ToDeviceEventType
+)
+
+type Type struct {
+	Type  string
+	Class TypeClass
+}
+
+func NewEventType(name string) Type {
+	evtType := Type{Type: name}
+	evtType.Class = evtType.GuessClass()
+	return evtType
+}
+
+func (et *Type) IsState() bool {
+	return et.Class == StateEventType
+}
+
+func (et *Type) IsEphemeral() bool {
+	return et.Class == EphemeralEventType
+}
+
+func (et *Type) IsAccountData() bool {
+	return et.Class == AccountDataEventType
+}
+
+func (et *Type) IsToDevice() bool {
+	return et.Class == ToDeviceEventType
+}
+
+func (et *Type) IsInRoomVerification() bool {
+	switch et.Type {
+	case InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type,
+		InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type:
+		return true
+	default:
+		return false
+	}
+}
+
+func (et *Type) IsCall() bool {
+	switch et.Type {
+	case CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type,
+		CallNegotiate.Type, CallHangup.Type:
+		return true
+	default:
+		return false
+	}
+}
+
+func (et *Type) IsCustom() bool {
+	return !strings.HasPrefix(et.Type, "m.")
+}
+
+func (et *Type) GuessClass() TypeClass {
+	switch et.Type {
+	case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type,
+		StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateServerACL.Type, StateTopic.Type,
+		StatePinnedEvents.Type, StateTombstone.Type, StateEncryption.Type, StateBridge.Type, StateHalfShotBridge.Type,
+		StateSpaceParent.Type, StateSpaceChild.Type, StatePolicyRoom.Type, StatePolicyServer.Type, StatePolicyUser.Type,
+		StateInsertionMarker.Type, StateElementFunctionalMembers.Type:
+		return StateEventType
+	case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type:
+		return EphemeralEventType
+	case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type,
+		AccountDataFullyRead.Type, AccountDataIgnoredUserList.Type, AccountDataMarkedUnread.Type,
+		AccountDataSecretStorageKey.Type, AccountDataSecretStorageDefaultKey.Type,
+		AccountDataCrossSigningMaster.Type, AccountDataCrossSigningSelf.Type, AccountDataCrossSigningUser.Type,
+		AccountDataFullyRead.Type, AccountDataMegolmBackupKey.Type:
+		return AccountDataEventType
+	case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type,
+		InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type,
+		InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type,
+		CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type,
+		CallNegotiate.Type, CallHangup.Type, BeeperMessageStatus.Type, EventUnstablePollStart.Type, EventUnstablePollResponse.Type:
+		return MessageEventType
+	case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type, ToDeviceRoomKeyWithheld.Type,
+		ToDeviceBeeperRoomKeyAck.Type:
+		return ToDeviceEventType
+	default:
+		return UnknownEventType
+	}
+}
+
+func (et *Type) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &et.Type)
+	if err != nil {
+		return err
+	}
+	et.Class = et.GuessClass()
+	return nil
+}
+
+func (et *Type) MarshalJSON() ([]byte, error) {
+	return json.Marshal(&et.Type)
+}
+
+func (et Type) UnmarshalText(data []byte) error {
+	et.Type = string(data)
+	et.Class = et.GuessClass()
+	return nil
+}
+
+func (et Type) MarshalText() ([]byte, error) {
+	return []byte(et.Type), nil
+}
+
+func (et *Type) String() string {
+	return et.Type
+}
+
+func (et *Type) Repr() string {
+	return fmt.Sprintf("%s (%s)", et.Type, et.Class.Name())
+}
+
+// State events
+var (
+	StateAliases           = Type{"m.room.aliases", StateEventType}
+	StateCanonicalAlias    = Type{"m.room.canonical_alias", StateEventType}
+	StateCreate            = Type{"m.room.create", StateEventType}
+	StateJoinRules         = Type{"m.room.join_rules", StateEventType}
+	StateHistoryVisibility = Type{"m.room.history_visibility", StateEventType}
+	StateGuestAccess       = Type{"m.room.guest_access", StateEventType}
+	StateMember            = Type{"m.room.member", StateEventType}
+	StatePowerLevels       = Type{"m.room.power_levels", StateEventType}
+	StateRoomName          = Type{"m.room.name", StateEventType}
+	StateTopic             = Type{"m.room.topic", StateEventType}
+	StateRoomAvatar        = Type{"m.room.avatar", StateEventType}
+	StatePinnedEvents      = Type{"m.room.pinned_events", StateEventType}
+	StateServerACL         = Type{"m.room.server_acl", StateEventType}
+	StateTombstone         = Type{"m.room.tombstone", StateEventType}
+	StatePolicyRoom        = Type{"m.policy.rule.room", StateEventType}
+	StatePolicyServer      = Type{"m.policy.rule.server", StateEventType}
+	StatePolicyUser        = Type{"m.policy.rule.user", StateEventType}
+	StateEncryption        = Type{"m.room.encryption", StateEventType}
+	StateBridge            = Type{"m.bridge", StateEventType}
+	StateHalfShotBridge    = Type{"uk.half-shot.bridge", StateEventType}
+	StateSpaceChild        = Type{"m.space.child", StateEventType}
+	StateSpaceParent       = Type{"m.space.parent", StateEventType}
+
+	StateLegacyPolicyRoom     = Type{"m.room.rule.room", StateEventType}
+	StateLegacyPolicyServer   = Type{"m.room.rule.server", StateEventType}
+	StateLegacyPolicyUser     = Type{"m.room.rule.user", StateEventType}
+	StateUnstablePolicyRoom   = Type{"org.matrix.mjolnir.rule.room", StateEventType}
+	StateUnstablePolicyServer = Type{"org.matrix.mjolnir.rule.server", StateEventType}
+	StateUnstablePolicyUser   = Type{"org.matrix.mjolnir.rule.user", StateEventType}
+
+	// Deprecated: MSC2716 has been abandoned
+	StateInsertionMarker = Type{"org.matrix.msc2716.marker", StateEventType}
+
+	StateElementFunctionalMembers = Type{"io.element.functional_members", StateEventType}
+)
+
+// Message events
+var (
+	EventRedaction = Type{"m.room.redaction", MessageEventType}
+	EventMessage   = Type{"m.room.message", MessageEventType}
+	EventEncrypted = Type{"m.room.encrypted", MessageEventType}
+	EventReaction  = Type{"m.reaction", MessageEventType}
+	EventSticker   = Type{"m.sticker", MessageEventType}
+
+	InRoomVerificationReady  = Type{"m.key.verification.ready", MessageEventType}
+	InRoomVerificationStart  = Type{"m.key.verification.start", MessageEventType}
+	InRoomVerificationDone   = Type{"m.key.verification.done", MessageEventType}
+	InRoomVerificationCancel = Type{"m.key.verification.cancel", MessageEventType}
+
+	// SAS Verification Events
+	InRoomVerificationAccept = Type{"m.key.verification.accept", MessageEventType}
+	InRoomVerificationKey    = Type{"m.key.verification.key", MessageEventType}
+	InRoomVerificationMAC    = Type{"m.key.verification.mac", MessageEventType}
+
+	CallInvite       = Type{"m.call.invite", MessageEventType}
+	CallCandidates   = Type{"m.call.candidates", MessageEventType}
+	CallAnswer       = Type{"m.call.answer", MessageEventType}
+	CallReject       = Type{"m.call.reject", MessageEventType}
+	CallSelectAnswer = Type{"m.call.select_answer", MessageEventType}
+	CallNegotiate    = Type{"m.call.negotiate", MessageEventType}
+	CallHangup       = Type{"m.call.hangup", MessageEventType}
+
+	BeeperMessageStatus = Type{"com.beeper.message_send_status", MessageEventType}
+
+	EventUnstablePollStart    = Type{Type: "org.matrix.msc3381.poll.start", Class: MessageEventType}
+	EventUnstablePollResponse = Type{Type: "org.matrix.msc3381.poll.response", Class: MessageEventType}
+)
+
+// Ephemeral events
+var (
+	EphemeralEventReceipt  = Type{"m.receipt", EphemeralEventType}
+	EphemeralEventTyping   = Type{"m.typing", EphemeralEventType}
+	EphemeralEventPresence = Type{"m.presence", EphemeralEventType}
+)
+
+// Account data events
+var (
+	AccountDataDirectChats     = Type{"m.direct", AccountDataEventType}
+	AccountDataPushRules       = Type{"m.push_rules", AccountDataEventType}
+	AccountDataRoomTags        = Type{"m.tag", AccountDataEventType}
+	AccountDataFullyRead       = Type{"m.fully_read", AccountDataEventType}
+	AccountDataIgnoredUserList = Type{"m.ignored_user_list", AccountDataEventType}
+	AccountDataMarkedUnread    = Type{"m.marked_unread", AccountDataEventType}
+	AccountDataBeeperMute      = Type{"com.beeper.mute", AccountDataEventType}
+
+	AccountDataSecretStorageDefaultKey = Type{"m.secret_storage.default_key", AccountDataEventType}
+	AccountDataSecretStorageKey        = Type{"m.secret_storage.key", AccountDataEventType}
+	AccountDataCrossSigningMaster      = Type{string(id.SecretXSMaster), AccountDataEventType}
+	AccountDataCrossSigningUser        = Type{string(id.SecretXSUserSigning), AccountDataEventType}
+	AccountDataCrossSigningSelf        = Type{string(id.SecretXSSelfSigning), AccountDataEventType}
+	AccountDataMegolmBackupKey         = Type{string(id.SecretMegolmBackupV1), AccountDataEventType}
+)
+
+// Device-to-device events
+var (
+	ToDeviceRoomKey          = Type{"m.room_key", ToDeviceEventType}
+	ToDeviceRoomKeyRequest   = Type{"m.room_key_request", ToDeviceEventType}
+	ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType}
+	ToDeviceEncrypted        = Type{"m.room.encrypted", ToDeviceEventType}
+	ToDeviceRoomKeyWithheld  = Type{"m.room_key.withheld", ToDeviceEventType}
+	ToDeviceSecretRequest    = Type{"m.secret.request", ToDeviceEventType}
+	ToDeviceSecretSend       = Type{"m.secret.send", ToDeviceEventType}
+	ToDeviceDummy            = Type{"m.dummy", ToDeviceEventType}
+
+	ToDeviceVerificationRequest = Type{"m.key.verification.request", ToDeviceEventType}
+	ToDeviceVerificationReady   = Type{"m.key.verification.ready", ToDeviceEventType}
+	ToDeviceVerificationStart   = Type{"m.key.verification.start", ToDeviceEventType}
+	ToDeviceVerificationDone    = Type{"m.key.verification.done", ToDeviceEventType}
+	ToDeviceVerificationCancel  = Type{"m.key.verification.cancel", ToDeviceEventType}
+
+	// SAS Verification Events
+	ToDeviceVerificationAccept = Type{"m.key.verification.accept", ToDeviceEventType}
+	ToDeviceVerificationKey    = Type{"m.key.verification.key", ToDeviceEventType}
+	ToDeviceVerificationMAC    = Type{"m.key.verification.mac", ToDeviceEventType}
+
+	ToDeviceOrgMatrixRoomKeyWithheld = Type{"org.matrix.room_key.withheld", ToDeviceEventType}
+
+	ToDeviceBeeperRoomKeyAck = Type{"com.beeper.room_key.ack", ToDeviceEventType}
+)
diff --git a/vendor/maunium.net/go/mautrix/event/verification.go b/vendor/maunium.net/go/mautrix/event/verification.go
new file mode 100644
index 0000000..6101896
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/verification.go
@@ -0,0 +1,308 @@
+// Copyright (c) 2020 Nikos Filippakis
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"go.mau.fi/util/jsonbytes"
+	"go.mau.fi/util/jsontime"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type VerificationMethod string
+
+const (
+	VerificationMethodSAS VerificationMethod = "m.sas.v1"
+
+	VerificationMethodReciprocate VerificationMethod = "m.reciprocate.v1"
+	VerificationMethodQRCodeShow  VerificationMethod = "m.qr_code.show.v1"
+	VerificationMethodQRCodeScan  VerificationMethod = "m.qr_code.scan.v1"
+)
+
+type VerificationTransactionable interface {
+	GetTransactionID() id.VerificationTransactionID
+	SetTransactionID(id.VerificationTransactionID)
+}
+
+// ToDeviceVerificationEvent contains the fields common to all to-device
+// verification events.
+type ToDeviceVerificationEvent struct {
+	// TransactionID is an opaque identifier for the verification request. Must
+	// be unique with respect to the devices involved.
+	TransactionID id.VerificationTransactionID `json:"transaction_id,omitempty"`
+}
+
+var _ VerificationTransactionable = (*ToDeviceVerificationEvent)(nil)
+
+func (ve *ToDeviceVerificationEvent) GetTransactionID() id.VerificationTransactionID {
+	return ve.TransactionID
+}
+
+func (ve *ToDeviceVerificationEvent) SetTransactionID(id id.VerificationTransactionID) {
+	ve.TransactionID = id
+}
+
+// InRoomVerificationEvent contains the fields common to all in-room
+// verification events.
+type InRoomVerificationEvent struct {
+	// RelatesTo indicates the m.key.verification.request that this message is
+	// related to. Note that for encrypted messages, this property should be in
+	// the unencrypted portion of the event.
+	RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
+}
+
+var _ Relatable = (*InRoomVerificationEvent)(nil)
+
+func (ve *InRoomVerificationEvent) GetRelatesTo() *RelatesTo {
+	if ve.RelatesTo == nil {
+		ve.RelatesTo = &RelatesTo{}
+	}
+	return ve.RelatesTo
+}
+
+func (ve *InRoomVerificationEvent) OptionalGetRelatesTo() *RelatesTo {
+	return ve.RelatesTo
+}
+
+func (ve *InRoomVerificationEvent) SetRelatesTo(rel *RelatesTo) {
+	ve.RelatesTo = rel
+}
+
+// VerificationRequestEventContent represents the content of an
+// [m.key.verification.request] to-device event as described in [Section
+// 11.12.2.1] of the Spec.
+//
+// For the in-room version, use a standard [MessageEventContent] struct.
+//
+// [m.key.verification.request]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationrequest
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
+type VerificationRequestEventContent struct {
+	ToDeviceVerificationEvent
+	// FromDevice is the device ID which is initiating the request.
+	FromDevice id.DeviceID `json:"from_device"`
+	// Methods is a list of the verification methods supported by the sender.
+	Methods []VerificationMethod `json:"methods"`
+	// Timestamp is the time at which the request was made.
+	Timestamp jsontime.UnixMilli `json:"timestamp,omitempty"`
+}
+
+// VerificationRequestEventContentFromMessage converts an in-room verification
+// request message event to a [VerificationRequestEventContent].
+func VerificationRequestEventContentFromMessage(evt *Event) *VerificationRequestEventContent {
+	content := evt.Content.AsMessage()
+	return &VerificationRequestEventContent{
+		ToDeviceVerificationEvent: ToDeviceVerificationEvent{
+			TransactionID: id.VerificationTransactionID(evt.ID),
+		},
+		Timestamp:  jsontime.UMInt(evt.Timestamp),
+		FromDevice: content.FromDevice,
+		Methods:    content.Methods,
+	}
+}
+
+// VerificationReadyEventContent represents the content of an
+// [m.key.verification.ready] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.1] of the Spec.
+//
+// [m.key.verification.ready]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationready
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
+type VerificationReadyEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// FromDevice is the device ID which is initiating the request.
+	FromDevice id.DeviceID `json:"from_device"`
+	// Methods is a list of the verification methods supported by the sender.
+	Methods []VerificationMethod `json:"methods"`
+}
+
+type KeyAgreementProtocol string
+
+const (
+	KeyAgreementProtocolCurve25519           KeyAgreementProtocol = "curve25519"
+	KeyAgreementProtocolCurve25519HKDFSHA256 KeyAgreementProtocol = "curve25519-hkdf-sha256"
+)
+
+type VerificationHashMethod string
+
+const VerificationHashMethodSHA256 VerificationHashMethod = "sha256"
+
+type MACMethod string
+
+const (
+	MACMethodHKDFHMACSHA256   MACMethod = "hkdf-hmac-sha256"
+	MACMethodHKDFHMACSHA256V2 MACMethod = "hkdf-hmac-sha256.v2"
+)
+
+type SASMethod string
+
+const (
+	SASMethodDecimal SASMethod = "decimal"
+	SASMethodEmoji   SASMethod = "emoji"
+)
+
+// VerificationStartEventContent represents the content of an
+// [m.key.verification.start] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.1] of the Spec.
+//
+// This struct also contains the fields for an [m.key.verification.start] event
+// using the [VerificationMethodSAS] method as described in [Section
+// 11.12.2.2.2] and an [m.key.verification.start] using
+// [VerificationMethodReciprocate] as described in [Section 11.12.2.4.2].
+//
+// [m.key.verification.start]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationstart
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+// [Section 11.12.2.4.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-qr-codes
+type VerificationStartEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// FromDevice is the device ID which is initiating the request.
+	FromDevice id.DeviceID `json:"from_device"`
+	// Method is the verification method to use.
+	Method VerificationMethod `json:"method"`
+	// NextMethod is an optional method to use to verify the other user's key.
+	// Applicable when the method chosen only verifies one user’s key. This
+	// field will never be present if the method verifies keys both ways.
+	NextMethod VerificationMethod `json:"next_method,omitempty"`
+
+	// Hashes are the hash methods the sending device understands. This field
+	// is only applicable when the method is m.sas.v1.
+	Hashes []VerificationHashMethod `json:"hashes,omitempty"`
+	// KeyAgreementProtocols is the list of key agreement protocols the sending
+	// device understands. This field is only applicable when the method is
+	// m.sas.v1.
+	KeyAgreementProtocols []KeyAgreementProtocol `json:"key_agreement_protocols,omitempty"`
+	// MessageAuthenticationCodes is a list of the MAC methods that the sending
+	// device understands. This field is only applicable when the method is
+	// m.sas.v1.
+	MessageAuthenticationCodes []MACMethod `json:"message_authentication_codes"`
+	// ShortAuthenticationString is a list of SAS methods the sending device
+	// (and the sending device's user) understands. This field is only
+	// applicable when the method is m.sas.v1.
+	ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
+
+	// Secret is the shared secret from the QR code. This field is only
+	// applicable when the method is m.reciprocate.v1.
+	Secret jsonbytes.UnpaddedBytes `json:"secret,omitempty"`
+}
+
+// VerificationDoneEventContent represents the content of an
+// [m.key.verification.done] event (both the to-device and the in-room version)
+// as described in [Section 11.12.2.1] of the Spec.
+//
+// This type is an alias for [VerificationRelatable] since there are no
+// additional fields defined by the spec.
+//
+// [m.key.verification.done]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
+type VerificationDoneEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+}
+
+type VerificationCancelCode string
+
+const (
+	VerificationCancelCodeUser               VerificationCancelCode = "m.user"
+	VerificationCancelCodeTimeout            VerificationCancelCode = "m.timeout"
+	VerificationCancelCodeUnknownTransaction VerificationCancelCode = "m.unknown_transaction"
+	VerificationCancelCodeUnknownMethod      VerificationCancelCode = "m.unknown_method"
+	VerificationCancelCodeUnexpectedMessage  VerificationCancelCode = "m.unexpected_message"
+	VerificationCancelCodeKeyMismatch        VerificationCancelCode = "m.key_mismatch"
+	VerificationCancelCodeUserMismatch       VerificationCancelCode = "m.user_mismatch"
+	VerificationCancelCodeInvalidMessage     VerificationCancelCode = "m.invalid_message"
+	VerificationCancelCodeAccepted           VerificationCancelCode = "m.accepted"
+	VerificationCancelCodeSASMismatch        VerificationCancelCode = "m.mismatched_sas"
+	VerificationCancelCodeCommitmentMismatch VerificationCancelCode = "m.mismatched_commitment"
+
+	// Non-spec codes
+	VerificationCancelCodeInternalError       VerificationCancelCode = "com.beeper.internal_error"
+	VerificationCancelCodeMasterKeyNotTrusted VerificationCancelCode = "com.beeper.master_key_not_trusted" // the master key is not trusted by this device, but the QR code that was scanned was from a device that doesn't trust the master key
+)
+
+// VerificationCancelEventContent represents the content of an
+// [m.key.verification.cancel] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.1] of the Spec.
+//
+// [m.key.verification.cancel]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationcancel
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
+type VerificationCancelEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Code is the error code for why the process/request was cancelled by the
+	// user.
+	Code VerificationCancelCode `json:"code"`
+	// Reason is a human readable description of the code. The client should
+	// only rely on this string if it does not understand the code.
+	Reason string `json:"reason"`
+}
+
+// VerificationAcceptEventContent represents the content of an
+// [m.key.verification.accept] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.2.2] of the Spec.
+//
+// [m.key.verification.accept]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationaccept
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+type VerificationAcceptEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Commitment is the hash of the concatenation of the device's ephemeral
+	// public key (encoded as unpadded base64) and the canonical JSON
+	// representation of the m.key.verification.start message.
+	Commitment jsonbytes.UnpaddedBytes `json:"commitment"`
+	// Hash is the hash method the device is choosing to use, out of the
+	// options in the m.key.verification.start message.
+	Hash VerificationHashMethod `json:"hash"`
+	// KeyAgreementProtocol is the key agreement protocol the device is
+	// choosing to use, out of the options in the m.key.verification.start
+	// message.
+	KeyAgreementProtocol KeyAgreementProtocol `json:"key_agreement_protocol"`
+	// MessageAuthenticationCode is the message authentication code the device
+	// is choosing to use, out of the options in the m.key.verification.start
+	// message.
+	MessageAuthenticationCode MACMethod `json:"message_authentication_code"`
+	// ShortAuthenticationString is a list of SAS methods both devices involved
+	// in the verification process understand. Must be a subset of the options
+	// in the m.key.verification.start message.
+	ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
+}
+
+// VerificationKeyEventContent represents the content of an
+// [m.key.verification.key] event (both the to-device and the in-room version)
+// as described in [Section 11.12.2.2.2] of the Spec.
+//
+// [m.key.verification.key]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationkey
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+type VerificationKeyEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Key is the device’s ephemeral public key.
+	Key jsonbytes.UnpaddedBytes `json:"key"`
+}
+
+// VerificationMACEventContent represents the content of an
+// [m.key.verification.mac] event (both the to-device and the in-room version)
+// as described in [Section 11.12.2.2.2] of the Spec.
+//
+// [m.key.verification.mac]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationmac
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+type VerificationMACEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Keys is the MAC of the comma-separated, sorted, list of key IDs given in
+	// the MAC property.
+	Keys jsonbytes.UnpaddedBytes `json:"keys"`
+	// MAC is a map of the key ID to the MAC of the key, using the algorithm in
+	// the verification process.
+	MAC map[id.KeyID]jsonbytes.UnpaddedBytes `json:"mac"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/voip.go b/vendor/maunium.net/go/mautrix/event/voip.go
new file mode 100644
index 0000000..28f56c9
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/voip.go
@@ -0,0 +1,116 @@
+// Copyright (c) 2021 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package event
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+)
+
+type CallHangupReason string
+
+const (
+	CallHangupICEFailed       CallHangupReason = "ice_failed"
+	CallHangupInviteTimeout   CallHangupReason = "invite_timeout"
+	CallHangupUserHangup      CallHangupReason = "user_hangup"
+	CallHangupUserMediaFailed CallHangupReason = "user_media_failed"
+	CallHangupUnknownError    CallHangupReason = "unknown_error"
+)
+
+type CallDataType string
+
+const (
+	CallDataTypeOffer  CallDataType = "offer"
+	CallDataTypeAnswer CallDataType = "answer"
+)
+
+type CallData struct {
+	SDP  string       `json:"sdp"`
+	Type CallDataType `json:"type"`
+}
+
+type CallCandidate struct {
+	Candidate     string `json:"candidate"`
+	SDPMLineIndex int    `json:"sdpMLineIndex"`
+	SDPMID        string `json:"sdpMid"`
+}
+
+type CallVersion string
+
+func (cv *CallVersion) UnmarshalJSON(raw []byte) error {
+	var numberVersion int
+	err := json.Unmarshal(raw, &numberVersion)
+	if err != nil {
+		var stringVersion string
+		err = json.Unmarshal(raw, &stringVersion)
+		if err != nil {
+			return fmt.Errorf("failed to parse CallVersion: %w", err)
+		}
+		*cv = CallVersion(stringVersion)
+	} else {
+		*cv = CallVersion(strconv.Itoa(numberVersion))
+	}
+	return nil
+}
+
+func (cv *CallVersion) MarshalJSON() ([]byte, error) {
+	for _, char := range *cv {
+		if char < '0' || char > '9' {
+			// The version contains weird characters, return as string.
+			return json.Marshal(string(*cv))
+		}
+	}
+	// The version consists of only ASCII digits, return as an integer.
+	return []byte(*cv), nil
+}
+
+func (cv *CallVersion) Int() (int, error) {
+	return strconv.Atoi(string(*cv))
+}
+
+type BaseCallEventContent struct {
+	CallID  string      `json:"call_id"`
+	PartyID string      `json:"party_id"`
+	Version CallVersion `json:"version"`
+}
+
+type CallInviteEventContent struct {
+	BaseCallEventContent
+	Lifetime int      `json:"lifetime"`
+	Offer    CallData `json:"offer"`
+}
+
+type CallCandidatesEventContent struct {
+	BaseCallEventContent
+	Candidates []CallCandidate `json:"candidates"`
+}
+
+type CallRejectEventContent struct {
+	BaseCallEventContent
+}
+
+type CallAnswerEventContent struct {
+	BaseCallEventContent
+	Answer CallData `json:"answer"`
+}
+
+type CallSelectAnswerEventContent struct {
+	BaseCallEventContent
+	SelectedPartyID string `json:"selected_party_id"`
+}
+
+type CallNegotiateEventContent struct {
+	BaseCallEventContent
+	Lifetime    int      `json:"lifetime"`
+	Description CallData `json:"description"`
+}
+
+type CallHangupEventContent struct {
+	BaseCallEventContent
+	Reason CallHangupReason `json:"reason"`
+}