// 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 bridgev2 import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/rs/zerolog" "go.mau.fi/util/configupgrade" "go.mau.fi/util/ptr" "go.mau.fi/util/random" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "maunium.net/go/mautrix/mediaproxy" ) type ConvertedMessagePart struct { ID networkid.PartID Type event.Type Content *event.MessageEventContent Extra map[string]any DBMetadata any DontBridge bool } func (cmp *ConvertedMessagePart) ToEditPart(part *database.Message) *ConvertedEditPart { if cmp == nil { return nil } if cmp.DBMetadata != nil { merger, ok := part.Metadata.(database.MetaMerger) if ok { merger.CopyFrom(cmp.DBMetadata) } else { part.Metadata = cmp.DBMetadata } } return &ConvertedEditPart{ Part: part, Type: cmp.Type, Content: cmp.Content, Extra: cmp.Extra, DontBridge: cmp.DontBridge, } } // EventSender represents a specific user in a chat. type EventSender struct { // If IsFromMe is true, the UserLogin who the event was received through is used as the sender. // Double puppeting will be used if available. IsFromMe bool // SenderLogin is the ID of the UserLogin who sent the event. This may be different from the // login the event was received through. It is used to ensure double puppeting can still be // used even if the event is received through another login. SenderLogin networkid.UserLoginID // Sender is the remote user ID of the user who sent the event. // For new events, this will not be used for double puppeting. // // However, in the member list, [ChatMemberList.CheckAllLogins] can be specified to go through every login // and call [NetworkAPI.IsThisUser] to check if this ID belongs to that login. This method is not recommended, // it is better to fill the IsFromMe and SenderLogin fields appropriately. Sender networkid.UserID // ForceDMUser can be set if the event should be sent as the DM user even if the Sender is different. // This only applies in DM rooms where [database.Portal.OtherUserID] is set and is ignored if IsFromMe is true. // A warning will be logged if the sender is overridden due to this flag. ForceDMUser bool } func (es EventSender) MarshalZerologObject(evt *zerolog.Event) { evt.Str("user_id", string(es.Sender)) if string(es.SenderLogin) != string(es.Sender) { evt.Str("sender_login", string(es.SenderLogin)) } if es.IsFromMe { evt.Bool("is_from_me", true) } if es.ForceDMUser { evt.Bool("force_dm_user", true) } } type ConvertedMessage struct { ReplyTo *networkid.MessageOptionalPartID // Optional additional info about the reply. This is only used when backfilling messages // on Beeper, where replies may target messages that haven't been bridged yet. // Standard Matrix servers can't backwards backfill, so these are never used. ReplyToRoom networkid.PortalKey ReplyToUser networkid.UserID ReplyToLogin networkid.UserLoginID ThreadRoot *networkid.MessageID Parts []*ConvertedMessagePart Disappear database.DisappearingSetting } func MergeCaption(textPart, mediaPart *ConvertedMessagePart) *ConvertedMessagePart { if textPart == nil { return mediaPart } else if mediaPart == nil { return textPart } mediaPart = ptr.Clone(mediaPart) if mediaPart.Content.MsgType == event.MsgNotice || (mediaPart.Content.Body != "" && mediaPart.Content.FileName != "" && mediaPart.Content.Body != mediaPart.Content.FileName) { textPart = ptr.Clone(textPart) textPart.Content.EnsureHasHTML() mediaPart.Content.EnsureHasHTML() mediaPart.Content.Body += "\n\n" + textPart.Content.Body mediaPart.Content.FormattedBody += "

" + textPart.Content.FormattedBody mediaPart.Content.Mentions = mediaPart.Content.Mentions.Merge(textPart.Content.Mentions) mediaPart.Content.BeeperLinkPreviews = append(mediaPart.Content.BeeperLinkPreviews, textPart.Content.BeeperLinkPreviews...) } else { mediaPart.Content.FileName = mediaPart.Content.Body mediaPart.Content.Body = textPart.Content.Body mediaPart.Content.Format = textPart.Content.Format mediaPart.Content.FormattedBody = textPart.Content.FormattedBody mediaPart.Content.Mentions = textPart.Content.Mentions mediaPart.Content.BeeperLinkPreviews = textPart.Content.BeeperLinkPreviews } if metaMerger, ok := mediaPart.DBMetadata.(database.MetaMerger); ok { metaMerger.CopyFrom(textPart.DBMetadata) } mediaPart.ID = textPart.ID return mediaPart } func (cm *ConvertedMessage) MergeCaption() bool { if len(cm.Parts) != 2 { return false } textPart, mediaPart := cm.Parts[1], cm.Parts[0] if textPart.Content.MsgType != event.MsgText { textPart, mediaPart = mediaPart, textPart } if (!mediaPart.Content.MsgType.IsMedia() && mediaPart.Content.MsgType != event.MsgNotice) || textPart.Content.MsgType != event.MsgText { return false } merged := MergeCaption(textPart, mediaPart) if merged != nil { cm.Parts = []*ConvertedMessagePart{merged} return true } return false } type ConvertedEditPart struct { Part *database.Message Type event.Type // The Content and Extra fields will be put inside `m.new_content` automatically. // SetEdit must NOT be called by the network connector. Content *event.MessageEventContent Extra map[string]any // TopLevelExtra can be used to specify custom fields at the top level of the content rather than inside `m.new_content`. TopLevelExtra map[string]any // NewMentions can be used to specify new mentions that should ping the users again. // Mentions inside the edited content will not ping. NewMentions *event.Mentions DontBridge bool } type ConvertedEdit struct { ModifiedParts []*ConvertedEditPart DeletedParts []*database.Message // Warning: added parts will be sent at the end of the room. // If other messages have been sent after the message being edited, // these new parts will not be next to the existing parts. AddedParts *ConvertedMessage } // BridgeName contains information about the network that a connector bridges to. type BridgeName struct { // The displayname of the network, e.g. `Discord` DisplayName string `json:"displayname"` // The URL to the website of the network, e.g. `https://discord.com` NetworkURL string `json:"network_url"` // The icon of the network as a mxc:// URI NetworkIcon id.ContentURIString `json:"network_icon"` // An identifier uniquely identifying the network, e.g. `discord` NetworkID string `json:"network_id"` // An identifier uniquely identifying the bridge software. // The Go import path is a good choice here (e.g. github.com/octocat/discordbridge) BeeperBridgeType string `json:"beeper_bridge_type"` // The default appservice port to use in the example config, defaults to 8080 if unset // Official mautrix bridges will use ports defined in https://mau.fi/ports DefaultPort uint16 `json:"default_port,omitempty"` // The default command prefix to use in the example config, defaults to NetworkID if unset. Must include the ! prefix. DefaultCommandPrefix string `json:"default_command_prefix,omitempty"` } func (bn BridgeName) AsBridgeInfoSection() event.BridgeInfoSection { return event.BridgeInfoSection{ ID: bn.BeeperBridgeType, DisplayName: bn.DisplayName, AvatarURL: bn.NetworkIcon, ExternalURL: bn.NetworkURL, } } // NetworkConnector is the main interface that a network connector must implement. type NetworkConnector interface { // Init is called when the bridge is initialized. The connector should store the bridge instance for later use. // This should not do any network calls or other blocking operations. Init(*Bridge) // Start is called when the bridge is starting. // The connector should do any non-user-specific startup actions necessary. // User logins will be loaded separately, so the connector should not load them here. Start(context.Context) error // GetName returns the name of the bridge and some additional metadata, // which is used to fill `m.bridge` events among other things. // // The first call happens *before* the config is loaded, because the data here is also used to // fill parts of the example config (like the default username template and bot localpart). // The output can still be adjusted based on config variables, but the function must have // default values when called without a config. GetName() BridgeName // GetDBMetaTypes returns struct types that are used to store connector-specific metadata in various tables. // All fields are optional. If a field isn't provided, then the corresponding table will have no custom metadata. // This will be called before Init, it should have a hardcoded response. GetDBMetaTypes() database.MetaTypes // GetCapabilities returns the general capabilities of the network connector. // Note that most capabilities are scoped to rooms and are returned by [NetworkAPI.GetCapabilities] instead. GetCapabilities() *NetworkGeneralCapabilities // GetConfig returns all the parts of the network connector's config file. Specifically: // - example: a string containing an example config file // - data: an interface to unmarshal the actual config into // - upgrader: a config upgrader to ensure all fields are present and to do any migrations from old configs GetConfig() (example string, data any, upgrader configupgrade.Upgrader) // LoadUserLogin is called when a UserLogin is loaded from the database in order to fill the [UserLogin.Client] field. // // This is called within the bridge's global cache lock, so it must not do any slow operations, // such as connecting to the network. Instead, connecting should happen when [NetworkAPI.Connect] is called later. LoadUserLogin(ctx context.Context, login *UserLogin) error // GetLoginFlows returns a list of login flows that the network supports. GetLoginFlows() []LoginFlow // CreateLogin is called when a user wants to log in to the network. // // This should generally not do any work, it should just return a LoginProcess that remembers // the user and will execute the requested flow. The actual work should start when [LoginProcess.Start] is called. CreateLogin(ctx context.Context, user *User, flowID string) (LoginProcess, error) // GetBridgeInfoVersion returns version numbers for bridge info and room capabilities respectively. // When the versions change, the bridge will automatically resend bridge info to all rooms. GetBridgeInfoVersion() (info, capabilities int) } type StoppableNetwork interface { NetworkConnector // Stop is called when the bridge is stopping, after all network clients have been disconnected. Stop() } // DirectMediableNetwork is an optional interface that network connectors can implement to support direct media access. // // If the Matrix connector has direct media enabled, SetUseDirectMedia will be called // before the Start method of the network connector. Download will then be called // whenever someone wants to download a direct media `mxc://` URI which was generated // by calling GenerateContentURI on the Matrix connector. type DirectMediableNetwork interface { NetworkConnector SetUseDirectMedia() Download(ctx context.Context, mediaID networkid.MediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) } // IdentifierValidatingNetwork is an optional interface that network connectors can implement to validate the shape of user IDs. // // This should not perform any checks to see if the user ID actually exists on the network, just that the user ID looks valid. type IdentifierValidatingNetwork interface { NetworkConnector ValidateUserID(id networkid.UserID) bool } type TransactionIDGeneratingNetwork interface { NetworkConnector GenerateTransactionID(userID id.UserID, roomID id.RoomID, eventType event.Type) networkid.RawTransactionID } type PortalBridgeInfoFillingNetwork interface { NetworkConnector FillPortalBridgeInfo(portal *Portal, content *event.BridgeEventContent) } // ConfigValidatingNetwork is an optional interface that network connectors can implement to validate config fields // before the bridge is started. // // When the ValidateConfig method is called, the config data will already be unmarshaled into the // object returned by [NetworkConnector.GetConfig]. // // This mechanism is usually used to refuse bridge startup if a mandatory field has an invalid value. type ConfigValidatingNetwork interface { NetworkConnector ValidateConfig() error } // MaxFileSizeingNetwork is an optional interface that network connectors can implement // to find out the maximum file size that can be uploaded to Matrix. // // The SetMaxFileSize will be called asynchronously soon after startup. // Before the function is called, the connector may assume a default limit of 50 MiB. type MaxFileSizeingNetwork interface { NetworkConnector SetMaxFileSize(maxSize int64) } type NetworkResettingNetwork interface { NetworkConnector // ResetHTTPTransport should recreate the HTTP client used by the bridge. // It should refetch settings from the Matrix connector using GetHTTPClientSettings if applicable. ResetHTTPTransport() // ResetNetworkConnections should forcefully disconnect and restart any persistent network connections. // ResetHTTPTransport will usually be called before this, so resetting the transport is not necessary here. ResetNetworkConnections() } type RemoteEchoHandler func(RemoteMessage, *database.Message) (bool, error) type MatrixMessageResponse struct { DB *database.Message StreamOrder int64 // If Pending is set, the bridge will not save the provided message to the database. // This should only be used if AddPendingToSave has been called. Pending bool // If RemovePending is set, the bridge will remove the provided transaction ID from pending messages // after saving the provided message to the database. This should be used with AddPendingToIgnore. RemovePending networkid.TransactionID // An optional function that is called after the message is saved to the database. // Will not be called if the message is not saved for some reason. PostSave func(context.Context, *database.Message) } type OutgoingTimeoutConfig struct { CheckInterval time.Duration NoEchoTimeout time.Duration NoEchoMessage string NoAckTimeout time.Duration NoAckMessage string } type NetworkGeneralCapabilities struct { // Does the network connector support disappearing messages? // This flag enables the message disappearing loop in the bridge. DisappearingMessages bool // Should the bridge re-request user info on incoming messages even if the ghost already has info? // By default, info is only requested for ghosts with no name, and other updating is left to events. AggressiveUpdateInfo bool // Should the bridge call HandleMatrixReadReceipt with fake data when receiving a new message? // This should be enabled if the network requires each message to be marked as read independently, // and doesn't automatically do it when sending a message. ImplicitReadReceipts bool // If the bridge uses the pending message mechanism ([MatrixMessage.AddPendingToSave]) // to handle asynchronous message responses, this field can be set to enable // automatic timeout errors in case the asynchronous response never arrives. OutgoingMessageTimeouts *OutgoingTimeoutConfig // Capabilities related to the provisioning API. Provisioning ProvisioningCapabilities } // NetworkAPI is an interface representing a remote network client for a single user login. // // Implementations of this interface are stored in [UserLogin.Client]. // The [NetworkConnector.LoadUserLogin] method is responsible for filling the Client field with a NetworkAPI. type NetworkAPI interface { // Connect is called to actually connect to the remote network. // If there's no persistent connection, this may just check access token validity, or even do nothing at all. // This method isn't allowed to return errors, because any connection errors should be sent // using the bridge state mechanism (UserLogin.BridgeState.Send) Connect(ctx context.Context) // Disconnect should disconnect from the remote network. // A clean disconnection is preferred, but it should not take too long. Disconnect() // IsLoggedIn should return whether the access tokens in this NetworkAPI are valid. // This should not do any IO operations, it should only return cached data which is updated elsewhere. IsLoggedIn() bool // LogoutRemote should invalidate the access tokens in this NetworkAPI if possible // and disconnect from the remote network. LogoutRemote(ctx context.Context) // IsThisUser should return whether the given remote network user ID is the same as this login. // This is used when the bridge wants to convert a user login ID to a user ID. IsThisUser(ctx context.Context, userID networkid.UserID) bool // GetChatInfo returns info for a given chat. Any fields that are nil will be ignored and not processed at all, // while empty strings will change the relevant value in the room to be an empty string. // For example, a nil name will mean the room name is not changed, while an empty string name will remove the name. GetChatInfo(ctx context.Context, portal *Portal) (*ChatInfo, error) // GetUserInfo returns info for a given user. Like chat info, fields can be nil to skip them. GetUserInfo(ctx context.Context, ghost *Ghost) (*UserInfo, error) // GetCapabilities returns the bridging capabilities in a given room. // This can simply return a static list if the remote network has no per-chat capability differences, // but all calls will include the portal, because some networks do have per-chat differences. GetCapabilities(ctx context.Context, portal *Portal) *event.RoomFeatures // HandleMatrixMessage is called when a message is sent from Matrix in an existing portal room. // This function should convert the message as appropriate, send it over to the remote network, // and return the info so the central bridge can store it in the database. // // This is only called for normal non-edit messages. For other types of events, see the optional extra interfaces (`XHandlingNetworkAPI`). HandleMatrixMessage(ctx context.Context, msg *MatrixMessage) (message *MatrixMessageResponse, err error) } type ConnectBackgroundParams struct { // RawData is the raw data in the push that triggered the background connection. RawData json.RawMessage // ExtraData is the data returned by [PushParsingNetwork.ParsePushNotification]. // It's only present for native pushes. Relayed pushes will only have the raw data. ExtraData any } // BackgroundSyncingNetworkAPI is an optional interface that network connectors can implement to support background resyncs. type BackgroundSyncingNetworkAPI interface { NetworkAPI // ConnectBackground is called in place of Connect for background resyncs. // The client should connect to the remote network, handle pending messages, and then disconnect. // This call should block until the entire sync is complete and the client is disconnected. ConnectBackground(ctx context.Context, params *ConnectBackgroundParams) error } // CredentialExportingNetworkAPI is an optional interface that networks connectors can implement to support export of // the credentials associated with that login. Credential type is bridge specific. type CredentialExportingNetworkAPI interface { NetworkAPI ExportCredentials(ctx context.Context) any } // FetchMessagesParams contains the parameters for a message history pagination request. type FetchMessagesParams struct { // The portal to fetch messages in. Always present. Portal *Portal // When fetching messages inside a thread, the ID of the thread. ThreadRoot networkid.MessageID // Whether to fetch new messages instead of old ones. Forward bool // The oldest known message in the thread or the portal. If Forward is true, this is the newest known message instead. // If the portal doesn't have any bridged messages, this will be nil. AnchorMessage *database.Message // The cursor returned by the previous call to FetchMessages with the same portal and thread root. // This will not be present in Forward calls. Cursor networkid.PaginationCursor // The preferred number of messages to return. The returned batch can be bigger or smaller // without any side effects, but the network connector should aim for this number. Count int // When a forward backfill is triggered by a [RemoteChatResyncBackfillBundle], this will contain // the bundled data returned by the event. It can be used as an optimization to avoid fetching // messages that were already provided by the remote network, while still supporting fetching // more messages if the limit is higher. BundledData any // When the messages are being fetched for a queued backfill, this is the task object. Task *database.BackfillTask } // BackfillReaction is an individual reaction to a message in a history pagination request. // // The target message is always the BackfillMessage that contains this item. // Optionally, the reaction can target a specific part by specifying TargetPart. // If not specified, the first part (sorted lexicographically) is targeted. type BackfillReaction struct { // Optional part of the message that the reaction targets. // If nil, the reaction targets the first part of the message. TargetPart *networkid.PartID // Optional timestamp for the reaction. // If unset, the reaction will have a fake timestamp that is slightly after the message timestamp. Timestamp time.Time Sender EventSender EmojiID networkid.EmojiID Emoji string ExtraContent map[string]any DBMetadata any } // BackfillMessage is an individual message in a history pagination request. type BackfillMessage struct { *ConvertedMessage Sender EventSender ID networkid.MessageID TxnID networkid.TransactionID Timestamp time.Time StreamOrder int64 Reactions []*BackfillReaction ShouldBackfillThread bool LastThreadMessage networkid.MessageID } var ( _ RemoteMessageWithTransactionID = (*BackfillMessage)(nil) _ RemoteEventWithTimestamp = (*BackfillMessage)(nil) ) func (b *BackfillMessage) GetType() RemoteEventType { return RemoteEventMessage } func (b *BackfillMessage) GetPortalKey() networkid.PortalKey { panic("GetPortalKey called for BackfillMessage") } func (b *BackfillMessage) AddLogContext(c zerolog.Context) zerolog.Context { return c } func (b *BackfillMessage) GetSender() EventSender { return b.Sender } func (b *BackfillMessage) GetID() networkid.MessageID { return b.ID } func (b *BackfillMessage) GetTransactionID() networkid.TransactionID { return b.TxnID } func (b *BackfillMessage) GetTimestamp() time.Time { return b.Timestamp } func (b *BackfillMessage) ConvertMessage(ctx context.Context, portal *Portal, intent MatrixAPI) (*ConvertedMessage, error) { return b.ConvertedMessage, nil } // FetchMessagesResponse contains the response for a message history pagination request. type FetchMessagesResponse struct { // The messages to backfill. Messages should always be sorted in chronological order (oldest to newest). Messages []*BackfillMessage // The next cursor to use for fetching more messages. Cursor networkid.PaginationCursor // Whether there are more messages that can be backfilled. // This field is required. If it is false, FetchMessages will not be called again. HasMore bool // Whether the batch contains new messages rather than old ones. // Cursor, HasMore and the progress fields will be ignored when this is present. Forward bool // When sending forward backfill (or the first batch in a room), this field can be set // to mark the messages as read immediately after backfilling. MarkRead bool // Should the bridge check each message against the database to ensure it's not a duplicate before bridging? // By default, the bridge will only drop messages that are older than the last bridged message for forward backfills, // or newer than the first for backward. AggressiveDeduplication bool // When HasMore is true, one of the following fields can be set to report backfill progress: // Approximate backfill progress as a number between 0 and 1. ApproxProgress float64 // Approximate number of messages remaining that can be backfilled. ApproxRemainingCount int // Approximate total number of messages in the chat. ApproxTotalCount int // An optional function that is called after the backfill batch has been sent. CompleteCallback func() } // BackfillingNetworkAPI is an optional interface that network connectors can implement to support backfilling message history. type BackfillingNetworkAPI interface { NetworkAPI // FetchMessages returns a batch of messages to backfill in a portal room. // For details on the input and output, see the documentation of [FetchMessagesParams] and [FetchMessagesResponse]. FetchMessages(ctx context.Context, fetchParams FetchMessagesParams) (*FetchMessagesResponse, error) } // BackfillingNetworkAPIWithLimits is an optional interface that network connectors can implement to customize // the limit for backwards backfilling tasks. It is recommended to implement this by reading the MaxBatchesOverride // config field with network-specific keys for different room types. type BackfillingNetworkAPIWithLimits interface { BackfillingNetworkAPI // GetBackfillMaxBatchCount is called before a backfill task is executed to determine the maximum number of batches // that should be backfilled. Return values less than 0 are treated as unlimited. GetBackfillMaxBatchCount(ctx context.Context, portal *Portal, task *database.BackfillTask) int } // EditHandlingNetworkAPI is an optional interface that network connectors can implement to handle message edits. type EditHandlingNetworkAPI interface { NetworkAPI // HandleMatrixEdit is called when a previously bridged message is edited in a portal room. // The central bridge module will save the [*database.Message] after this function returns, // so the network connector is allowed to mutate the provided object. HandleMatrixEdit(ctx context.Context, msg *MatrixEdit) error } type PollHandlingNetworkAPI interface { NetworkAPI HandleMatrixPollStart(ctx context.Context, msg *MatrixPollStart) (*MatrixMessageResponse, error) HandleMatrixPollVote(ctx context.Context, msg *MatrixPollVote) (*MatrixMessageResponse, error) } // ReactionHandlingNetworkAPI is an optional interface that network connectors can implement to handle message reactions. type ReactionHandlingNetworkAPI interface { NetworkAPI // PreHandleMatrixReaction is called as the first step of handling a reaction. It returns the emoji ID, // sender user ID and max reaction count to allow the central bridge module to de-duplicate the reaction // if appropriate. PreHandleMatrixReaction(ctx context.Context, msg *MatrixReaction) (MatrixReactionPreResponse, error) // HandleMatrixReaction is called after confirming that the reaction is not a duplicate. // This is the method that should actually send the reaction to the remote network. // The returned [database.Reaction] object may be empty: the central bridge module already has // all the required fields and will fill them automatically if they're empty. However, network // connectors are allowed to set fields themselves if any extra fields are necessary. HandleMatrixReaction(ctx context.Context, msg *MatrixReaction) (reaction *database.Reaction, err error) // HandleMatrixReactionRemove is called when a redaction event is received pointing at a previously // bridged reaction. The network connector should remove the reaction from the remote network. HandleMatrixReactionRemove(ctx context.Context, msg *MatrixReactionRemove) error } // RedactionHandlingNetworkAPI is an optional interface that network connectors can implement to handle message deletions. type RedactionHandlingNetworkAPI interface { NetworkAPI // HandleMatrixMessageRemove is called when a previously bridged message is deleted in a portal room. HandleMatrixMessageRemove(ctx context.Context, msg *MatrixMessageRemove) error } // ReadReceiptHandlingNetworkAPI is an optional interface that network connectors can implement to handle read receipts. type ReadReceiptHandlingNetworkAPI interface { NetworkAPI // HandleMatrixReadReceipt is called when a read receipt is sent in a portal room. // This will be called even if the target message is not a bridged message. // Network connectors must gracefully handle [MatrixReadReceipt.ExactMessage] being nil. // The exact handling is up to the network connector. HandleMatrixReadReceipt(ctx context.Context, msg *MatrixReadReceipt) error } // ChatViewingNetworkAPI is an optional interface that network connectors can implement to handle viewing chat status. type ChatViewingNetworkAPI interface { NetworkAPI // HandleMatrixViewingChat is called when the user opens a portal room. // This will never be called by the standard appservice connector, // as Matrix doesn't have any standard way of signaling chat open status. // Clients are expected to call this every 5 seconds. There is no signal for closing a chat. HandleMatrixViewingChat(ctx context.Context, msg *MatrixViewingChat) error } // TypingHandlingNetworkAPI is an optional interface that network connectors can implement to handle typing events. type TypingHandlingNetworkAPI interface { NetworkAPI // HandleMatrixTyping is called when a user starts typing in a portal room. // In the future, the central bridge module will likely get a loop to automatically repeat // calls to this function until the user stops typing. HandleMatrixTyping(ctx context.Context, msg *MatrixTyping) error } type MarkedUnreadHandlingNetworkAPI interface { NetworkAPI HandleMarkedUnread(ctx context.Context, msg *MatrixMarkedUnread) error } type MuteHandlingNetworkAPI interface { NetworkAPI HandleMute(ctx context.Context, msg *MatrixMute) error } type TagHandlingNetworkAPI interface { NetworkAPI HandleRoomTag(ctx context.Context, msg *MatrixRoomTag) error } // RoomNameHandlingNetworkAPI is an optional interface that network connectors can implement to handle room name changes. type RoomNameHandlingNetworkAPI interface { NetworkAPI // HandleMatrixRoomName is called when the name of a portal room is changed. // This method should update the Name and NameSet fields of the Portal with // the new name and return true if the change was successful. // If the change is not successful, then the fields should not be updated. HandleMatrixRoomName(ctx context.Context, msg *MatrixRoomName) (bool, error) } // RoomAvatarHandlingNetworkAPI is an optional interface that network connectors can implement to handle room avatar changes. type RoomAvatarHandlingNetworkAPI interface { NetworkAPI // HandleMatrixRoomAvatar is called when the avatar of a portal room is changed. // This method should update the AvatarID, AvatarHash and AvatarMXC fields // with the new avatar details and return true if the change was successful. // If the change is not successful, then the fields should not be updated. HandleMatrixRoomAvatar(ctx context.Context, msg *MatrixRoomAvatar) (bool, error) } // RoomTopicHandlingNetworkAPI is an optional interface that network connectors can implement to handle room topic changes. type RoomTopicHandlingNetworkAPI interface { NetworkAPI // HandleMatrixRoomTopic is called when the topic of a portal room is changed. // This method should update the Topic and TopicSet fields of the Portal with // the new topic and return true if the change was successful. // If the change is not successful, then the fields should not be updated. HandleMatrixRoomTopic(ctx context.Context, msg *MatrixRoomTopic) (bool, error) } type DisappearTimerChangingNetworkAPI interface { NetworkAPI // HandleMatrixDisappearingTimer is called when the disappearing timer of a portal room is changed. // This method should update the Disappear field of the Portal with the new timer and return true // if the change was successful. If the change is not successful, then the field should not be updated. HandleMatrixDisappearingTimer(ctx context.Context, msg *MatrixDisappearingTimer) (bool, error) } // DeleteChatHandlingNetworkAPI is an optional interface that network connectors // can implement to delete a chat from the remote network. type DeleteChatHandlingNetworkAPI interface { NetworkAPI // HandleMatrixDeleteChat is called when the user explicitly deletes a chat. HandleMatrixDeleteChat(ctx context.Context, msg *MatrixDeleteChat) error } // MessageRequestAcceptingNetworkAPI is an optional interface that network connectors // can implement to accept message requests from the remote network. type MessageRequestAcceptingNetworkAPI interface { NetworkAPI // HandleMatrixAcceptMessageRequest is called when the user accepts a message request. HandleMatrixAcceptMessageRequest(ctx context.Context, msg *MatrixAcceptMessageRequest) error } type BeeperAIStreamHandlingNetworkAPI interface { NetworkAPI HandleMatrixBeeperAIStream(ctx context.Context, msg *MatrixBeeperAIStream) error } type ResolveIdentifierResponse struct { // Ghost is the ghost of the user that the identifier resolves to. // This field should be set whenever possible. However, it is not required, // and the central bridge module will not try to create a ghost if it is not set. Ghost *Ghost // UserID is the user ID of the user that the identifier resolves to. UserID networkid.UserID // UserInfo contains the info of the user that the identifier resolves to. // If both this and the Ghost field are set, the central bridge module will // automatically update the ghost's info with the data here. UserInfo *UserInfo // Chat contains info about the direct chat with the resolved user. // This field is required when createChat is true in the ResolveIdentifier call, // and optional otherwise. Chat *CreateChatResponse } var SpecialValueDMRedirectedToBot = networkid.UserID("__fi.mau.bridgev2.dm_redirected_to_bot::" + random.String(10)) type CreateChatResponse struct { PortalKey networkid.PortalKey // Portal and PortalInfo are not required, the caller will fetch them automatically based on PortalKey if necessary. Portal *Portal PortalInfo *ChatInfo // If a start DM request (CreateChatWithGhost or ResolveIdentifier) returns the DM to a different user, // this field should have the user ID of said different user. DMRedirectedTo networkid.UserID FailedParticipants map[networkid.UserID]*CreateChatFailedParticipant } type CreateChatFailedParticipant struct { Reason string `json:"reason"` InviteEventType string `json:"invite_event_type,omitempty"` InviteContent *event.Content `json:"invite_content,omitempty"` UserMXID id.UserID `json:"user_mxid,omitempty"` DMRoomMXID id.RoomID `json:"dm_room_mxid,omitempty"` } // IdentifierResolvingNetworkAPI is an optional interface that network connectors can implement to support starting new direct chats. type IdentifierResolvingNetworkAPI interface { NetworkAPI // ResolveIdentifier is called when the user wants to start a new chat. // This can happen via the `resolve-identifier` or `start-chat` bridge bot commands, // or the corresponding provisioning API endpoints. ResolveIdentifier(ctx context.Context, identifier string, createChat bool) (*ResolveIdentifierResponse, error) } // GhostDMCreatingNetworkAPI is an optional extension to IdentifierResolvingNetworkAPI for starting chats with pre-validated user IDs. type GhostDMCreatingNetworkAPI interface { IdentifierResolvingNetworkAPI // CreateChatWithGhost may be called instead of [IdentifierResolvingNetworkAPI.ResolveIdentifier] // when starting a chat with an internal user identifier that has been pre-validated using // [IdentifierValidatingNetwork.ValidateUserID]. If this is not implemented, ResolveIdentifier // will be used instead (by stringifying the ghost ID). CreateChatWithGhost(ctx context.Context, ghost *Ghost) (*CreateChatResponse, error) } // ContactListingNetworkAPI is an optional interface that network connectors can implement to provide the user's contact list. type ContactListingNetworkAPI interface { NetworkAPI GetContactList(ctx context.Context) ([]*ResolveIdentifierResponse, error) } type UserSearchingNetworkAPI interface { IdentifierResolvingNetworkAPI SearchUsers(ctx context.Context, query string) ([]*ResolveIdentifierResponse, error) } type GroupCreatingNetworkAPI interface { IdentifierResolvingNetworkAPI CreateGroup(ctx context.Context, params *GroupCreateParams) (*CreateChatResponse, error) } type PersonalFilteringCustomizingNetworkAPI interface { NetworkAPI CustomizePersonalFilteringSpace(req *mautrix.ReqCreateRoom) } type ProvisioningCapabilities struct { ResolveIdentifier ResolveIdentifierCapabilities `json:"resolve_identifier"` GroupCreation map[string]GroupTypeCapabilities `json:"group_creation"` } type ResolveIdentifierCapabilities struct { // Can DMs be created after resolving an identifier? CreateDM bool `json:"create_dm"` // Can users be looked up by phone number? LookupPhone bool `json:"lookup_phone"` // Can users be looked up by email address? LookupEmail bool `json:"lookup_email"` // Can users be looked up by network-specific username? LookupUsername bool `json:"lookup_username"` // Can any phone number be contacted without having to validate it via lookup first? AnyPhone bool `json:"any_phone"` // Can a contact list be retrieved from the bridge? ContactList bool `json:"contact_list"` // Can users be searched by name on the remote network? Search bool `json:"search"` } type GroupTypeCapabilities struct { TypeDescription string `json:"type_description"` Name GroupFieldCapability `json:"name"` Username GroupFieldCapability `json:"username"` Avatar GroupFieldCapability `json:"avatar"` Topic GroupFieldCapability `json:"topic"` Disappear GroupFieldCapability `json:"disappear"` Participants GroupFieldCapability `json:"participants"` Parent GroupFieldCapability `json:"parent"` } type GroupFieldCapability struct { // Is setting this field allowed at all in the create request? // Even if false, the network connector should attempt to set the metadata after group creation, // as the allowed flag can't be enforced properly when creating a group for an existing Matrix room. Allowed bool `json:"allowed"` // Is setting this field mandatory for the creation to succeed? Required bool `json:"required,omitempty"` // The minimum/maximum length of the field, if applicable. // For members, length means the number of members excluding the creator. MinLength int `json:"min_length,omitempty"` MaxLength int `json:"max_length,omitempty"` // Only for the disappear field: allowed disappearing settings DisappearSettings *event.DisappearingTimerCapability `json:"settings,omitempty"` // This can be used to tell provisionutil not to call ValidateUserID on each participant. // It only meant to allow hacks where ResolveIdentifier returns a fake ID that isn't actually valid for MXIDs. SkipIdentifierValidation bool `json:"-"` } type GroupCreateParams struct { Type string `json:"type,omitempty"` Username string `json:"username,omitempty"` // Clients may also provide MXIDs here, but provisionutil will normalize them, so bridges only need to handle network IDs Participants []networkid.UserID `json:"participants,omitempty"` Parent *networkid.PortalKey `json:"parent,omitempty"` Name *event.RoomNameEventContent `json:"name,omitempty"` Avatar *event.RoomAvatarEventContent `json:"avatar,omitempty"` Topic *event.TopicEventContent `json:"topic,omitempty"` Disappear *event.BeeperDisappearingTimer `json:"disappear,omitempty"` // An existing room ID to bridge to. If unset, a new room will be created. RoomID id.RoomID `json:"room_id,omitempty"` } type MembershipChangeType struct { From event.Membership To event.Membership IsSelf bool } var ( AcceptInvite = MembershipChangeType{From: event.MembershipInvite, To: event.MembershipJoin, IsSelf: true} RevokeInvite = MembershipChangeType{From: event.MembershipInvite, To: event.MembershipLeave} RejectInvite = MembershipChangeType{From: event.MembershipInvite, To: event.MembershipLeave, IsSelf: true} BanInvited = MembershipChangeType{From: event.MembershipInvite, To: event.MembershipBan} ProfileChange = MembershipChangeType{From: event.MembershipJoin, To: event.MembershipJoin, IsSelf: true} Leave = MembershipChangeType{From: event.MembershipJoin, To: event.MembershipLeave, IsSelf: true} Kick = MembershipChangeType{From: event.MembershipJoin, To: event.MembershipLeave} BanJoined = MembershipChangeType{From: event.MembershipJoin, To: event.MembershipBan} Invite = MembershipChangeType{From: event.MembershipLeave, To: event.MembershipInvite} Join = MembershipChangeType{From: event.MembershipLeave, To: event.MembershipJoin} BanLeft = MembershipChangeType{From: event.MembershipLeave, To: event.MembershipBan} Knock = MembershipChangeType{From: event.MembershipLeave, To: event.MembershipKnock, IsSelf: true} AcceptKnock = MembershipChangeType{From: event.MembershipKnock, To: event.MembershipInvite} RejectKnock = MembershipChangeType{From: event.MembershipKnock, To: event.MembershipLeave} RetractKnock = MembershipChangeType{From: event.MembershipKnock, To: event.MembershipLeave, IsSelf: true} BanKnocked = MembershipChangeType{From: event.MembershipKnock, To: event.MembershipBan} Unban = MembershipChangeType{From: event.MembershipBan, To: event.MembershipLeave} ) type GhostOrUserLogin interface { isGhostOrUserLogin() } func (*Ghost) isGhostOrUserLogin() {} func (*UserLogin) isGhostOrUserLogin() {} type MatrixMembershipChange struct { MatrixRoomMeta[*event.MemberEventContent] Target GhostOrUserLogin Type MembershipChangeType } type MatrixMembershipResult struct { RedirectTo networkid.UserID } type MembershipHandlingNetworkAPI interface { NetworkAPI HandleMatrixMembership(ctx context.Context, msg *MatrixMembershipChange) (*MatrixMembershipResult, error) } type SinglePowerLevelChange struct { OrigLevel int NewLevel int NewIsSet bool } type UserPowerLevelChange struct { Target GhostOrUserLogin SinglePowerLevelChange } type MatrixPowerLevelChange struct { MatrixRoomMeta[*event.PowerLevelsEventContent] Users map[id.UserID]*UserPowerLevelChange Events map[string]*SinglePowerLevelChange UsersDefault *SinglePowerLevelChange EventsDefault *SinglePowerLevelChange StateDefault *SinglePowerLevelChange Invite *SinglePowerLevelChange Kick *SinglePowerLevelChange Ban *SinglePowerLevelChange Redact *SinglePowerLevelChange } type PowerLevelHandlingNetworkAPI interface { NetworkAPI HandleMatrixPowerLevels(ctx context.Context, msg *MatrixPowerLevelChange) (bool, error) } type PushType int func (pt PushType) String() string { return pt.GoString() } func PushTypeFromString(str string) PushType { switch strings.TrimPrefix(strings.ToLower(str), "pushtype") { case "web": return PushTypeWeb case "apns": return PushTypeAPNs case "fcm": return PushTypeFCM default: return PushTypeUnknown } } func (pt PushType) GoString() string { switch pt { case PushTypeUnknown: return "PushTypeUnknown" case PushTypeWeb: return "PushTypeWeb" case PushTypeAPNs: return "PushTypeAPNs" case PushTypeFCM: return "PushTypeFCM" default: return fmt.Sprintf("PushType(%d)", int(pt)) } } const ( PushTypeUnknown PushType = iota PushTypeWeb PushTypeAPNs PushTypeFCM ) type WebPushConfig struct { VapidKey string `json:"vapid_key"` } type FCMPushConfig struct { SenderID string `json:"sender_id"` } type APNsPushConfig struct { BundleID string `json:"bundle_id"` } type PushConfig struct { Web *WebPushConfig `json:"web,omitempty"` FCM *FCMPushConfig `json:"fcm,omitempty"` APNs *APNsPushConfig `json:"apns,omitempty"` // If Native is true, it means the network supports registering for pushes // that are delivered directly to the app without the use of a push relay. Native bool `json:"native,omitempty"` } // PushableNetworkAPI is an optional interface that network connectors can implement // to support waking up the wrapper app using push notifications. type PushableNetworkAPI interface { NetworkAPI // RegisterPushNotifications is called when the wrapper app wants to register a push token with the remote network. RegisterPushNotifications(ctx context.Context, pushType PushType, token string) error // GetPushConfigs is used to find which types of push notifications the remote network can provide. GetPushConfigs() *PushConfig } // PushParsingNetwork is an optional interface that network connectors can implement // to support parsing native push notifications from networks. type PushParsingNetwork interface { NetworkConnector // ParsePushNotification is called when a native push is received. // It must return the corresponding user login ID to wake up, plus optionally data to pass to the wakeup call. ParsePushNotification(ctx context.Context, data json.RawMessage) (networkid.UserLoginID, any, error) } type RemoteEventType int func (ret RemoteEventType) String() string { switch ret { case RemoteEventUnknown: return "RemoteEventUnknown" case RemoteEventMessage: return "RemoteEventMessage" case RemoteEventMessageUpsert: return "RemoteEventMessageUpsert" case RemoteEventEdit: return "RemoteEventEdit" case RemoteEventReaction: return "RemoteEventReaction" case RemoteEventReactionRemove: return "RemoteEventReactionRemove" case RemoteEventReactionSync: return "RemoteEventReactionSync" case RemoteEventMessageRemove: return "RemoteEventMessageRemove" case RemoteEventReadReceipt: return "RemoteEventReadReceipt" case RemoteEventDeliveryReceipt: return "RemoteEventDeliveryReceipt" case RemoteEventMarkUnread: return "RemoteEventMarkUnread" case RemoteEventTyping: return "RemoteEventTyping" case RemoteEventChatInfoChange: return "RemoteEventChatInfoChange" case RemoteEventChatResync: return "RemoteEventChatResync" case RemoteEventChatDelete: return "RemoteEventChatDelete" case RemoteEventBackfill: return "RemoteEventBackfill" default: return fmt.Sprintf("RemoteEventType(%d)", int(ret)) } } const ( RemoteEventUnknown RemoteEventType = iota RemoteEventMessage RemoteEventMessageUpsert RemoteEventEdit RemoteEventReaction RemoteEventReactionRemove RemoteEventReactionSync RemoteEventMessageRemove RemoteEventReadReceipt RemoteEventDeliveryReceipt RemoteEventMarkUnread RemoteEventTyping RemoteEventChatInfoChange RemoteEventChatResync RemoteEventChatDelete RemoteEventBackfill ) // RemoteEvent represents a single event from the remote network, such as a message or a reaction. // // When a [NetworkAPI] receives an event from the remote network, it should convert it into a [RemoteEvent] // and pass it to the bridge for processing using [Bridge.QueueRemoteEvent]. type RemoteEvent interface { GetType() RemoteEventType GetPortalKey() networkid.PortalKey AddLogContext(c zerolog.Context) zerolog.Context GetSender() EventSender } type RemoteEventWithUncertainPortalReceiver interface { RemoteEvent PortalReceiverIsUncertain() bool } type RemotePreHandler interface { RemoteEvent PreHandle(ctx context.Context, portal *Portal) } type RemotePostHandler interface { RemoteEvent PostHandle(ctx context.Context, portal *Portal) } type RemoteChatInfoChange interface { RemoteEvent GetChatInfoChange(ctx context.Context) (*ChatInfoChange, error) } type RemoteChatResync interface { RemoteEvent } type RemoteChatResyncWithInfo interface { RemoteChatResync GetChatInfo(ctx context.Context, portal *Portal) (*ChatInfo, error) } type RemoteChatResyncBackfill interface { RemoteChatResync CheckNeedsBackfill(ctx context.Context, latestMessage *database.Message) (bool, error) } type RemoteChatResyncBackfillBundle interface { RemoteChatResyncBackfill GetBundledBackfillData() any } type RemoteBackfill interface { RemoteEvent GetBackfillData(ctx context.Context, portal *Portal) (*FetchMessagesResponse, error) } type RemoteDeleteOnlyForMe interface { RemoteEvent DeleteOnlyForMe() bool } type RemoteChatDelete interface { RemoteDeleteOnlyForMe } type RemoteChatDeleteWithChildren interface { RemoteChatDelete DeleteChildren() bool } type RemoteEventThatMayCreatePortal interface { RemoteEvent ShouldCreatePortal() bool } type RemoteEventWithTargetMessage interface { RemoteEvent GetTargetMessage() networkid.MessageID } type RemoteEventWithBundledParts interface { RemoteEventWithTargetMessage GetTargetDBMessage() []*database.Message } type RemoteEventWithTargetPart interface { RemoteEventWithTargetMessage GetTargetMessagePart() networkid.PartID } type RemoteEventWithTimestamp interface { RemoteEvent GetTimestamp() time.Time } type RemoteEventWithStreamOrder interface { RemoteEvent GetStreamOrder() int64 } type RemoteMessage interface { RemoteEvent GetID() networkid.MessageID ConvertMessage(ctx context.Context, portal *Portal, intent MatrixAPI) (*ConvertedMessage, error) } type UpsertResult struct { SubEvents []RemoteEvent SaveParts bool ContinueMessageHandling bool } type RemoteMessageUpsert interface { RemoteMessage HandleExisting(ctx context.Context, portal *Portal, intent MatrixAPI, existing []*database.Message) (UpsertResult, error) } type RemoteMessageWithTransactionID interface { RemoteMessage GetTransactionID() networkid.TransactionID } type RemoteEdit interface { RemoteEventWithTargetMessage ConvertEdit(ctx context.Context, portal *Portal, intent MatrixAPI, existing []*database.Message) (*ConvertedEdit, error) } type RemoteReaction interface { RemoteEventWithTargetMessage GetReactionEmoji() (string, networkid.EmojiID) } type ReactionSyncUser struct { Reactions []*BackfillReaction // Whether the list contains all reactions the user has sent HasAllReactions bool // If the list doesn't contain all reactions from the user, // then this field can be set to remove old reactions if there are more than a certain number. MaxCount int } type ReactionSyncData struct { Users map[networkid.UserID]*ReactionSyncUser // Whether the map contains all users who have reacted to the message HasAllUsers bool } func (rsd *ReactionSyncData) ToBackfill() []*BackfillReaction { var reactions []*BackfillReaction for _, user := range rsd.Users { reactions = append(reactions, user.Reactions...) } return reactions } type RemoteReactionSync interface { RemoteEventWithTargetMessage GetReactions() *ReactionSyncData } type RemoteReactionWithExtraContent interface { RemoteReaction GetReactionExtraContent() map[string]any } type RemoteReactionWithMeta interface { RemoteReaction GetReactionDBMetadata() any } type RemoteReactionRemove interface { RemoteEventWithTargetMessage GetRemovedEmojiID() networkid.EmojiID } type RemoteMessageRemove interface { RemoteEventWithTargetMessage } // Deprecated: Renamed to RemoteReadReceipt. type RemoteReceipt = RemoteReadReceipt type RemoteReadReceipt interface { RemoteEvent GetLastReceiptTarget() networkid.MessageID GetReceiptTargets() []networkid.MessageID GetReadUpTo() time.Time } type RemoteReadReceiptWithStreamOrder interface { RemoteReadReceipt GetReadUpToStreamOrder() int64 } type RemoteDeliveryReceipt interface { RemoteEvent GetReceiptTargets() []networkid.MessageID } type RemoteMarkUnread interface { RemoteEvent GetUnread() bool } type RemoteTyping interface { RemoteEvent GetTimeout() time.Duration } type TypingType int const ( TypingTypeText TypingType = iota TypingTypeUploadingMedia TypingTypeRecordingMedia ) type RemoteTypingWithType interface { RemoteTyping GetTypingType() TypingType } type OrigSender struct { User *User UserID id.UserID RequiresDisambiguation bool DisambiguatedName string FormattedName string PerMessageProfile event.BeeperPerMessageProfile event.MemberEventContent } type MatrixEventBase[ContentType any] struct { // The raw event being bridged. Event *event.Event // The parsed content struct of the event. Custom fields can be found in Event.Content.Raw. Content ContentType // The room where the event happened. Portal *Portal // The original sender user ID. Only present in case the event is being relayed (and Sender is not the same user). OrigSender *OrigSender InputTransactionID networkid.RawTransactionID } type MatrixMessage struct { MatrixEventBase[*event.MessageEventContent] ThreadRoot *database.Message ReplyTo *database.Message pendingSaves []*outgoingMessage } type MatrixEdit struct { MatrixEventBase[*event.MessageEventContent] EditTarget *database.Message } type MatrixPollStart struct { MatrixMessage Content *event.PollStartEventContent } type MatrixPollVote struct { MatrixMessage VoteTo *database.Message Content *event.PollResponseEventContent } type MatrixReaction struct { MatrixEventBase[*event.ReactionEventContent] TargetMessage *database.Message PreHandleResp *MatrixReactionPreResponse // When EmojiID is blank and there's already an existing reaction, this is the old reaction that is being overridden. ReactionToOverride *database.Reaction // When MaxReactions is >0 in the pre-response, this is the list of previous reactions that should be preserved. ExistingReactionsToKeep []*database.Reaction } type MatrixReactionPreResponse struct { SenderID networkid.UserID EmojiID networkid.EmojiID Emoji string MaxReactions int } type MatrixReactionRemove struct { MatrixEventBase[*event.RedactionEventContent] TargetReaction *database.Reaction } type MatrixMessageRemove struct { MatrixEventBase[*event.RedactionEventContent] TargetMessage *database.Message } type MatrixRoomMeta[ContentType any] struct { MatrixEventBase[ContentType] PrevContent ContentType IsStateRequest bool } type MatrixRoomName = MatrixRoomMeta[*event.RoomNameEventContent] type MatrixRoomAvatar = MatrixRoomMeta[*event.RoomAvatarEventContent] type MatrixRoomTopic = MatrixRoomMeta[*event.TopicEventContent] type MatrixDisappearingTimer = MatrixRoomMeta[*event.BeeperDisappearingTimer] type MatrixReadReceipt struct { Portal *Portal // The event ID that the receipt is targeting EventID id.EventID // The exact message that was read. This may be nil if the event ID isn't a message. ExactMessage *database.Message // The timestamp that the user has read up to. This is either the timestamp of the message // (if one is present) or the timestamp of the receipt. ReadUpTo time.Time // The ReadUpTo timestamp of the previous message LastRead time.Time // The receipt metadata. Receipt event.ReadReceipt // Whether the receipt is implicit, i.e. triggered by an incoming timeline event rather than an explicit receipt. Implicit bool } type MatrixTyping struct { Portal *Portal IsTyping bool Type TypingType } type MatrixViewingChat struct { // The portal that the user is viewing. This will be nil when the user switches to a chat from a different bridge. Portal *Portal } type MatrixDeleteChat = MatrixEventBase[*event.BeeperChatDeleteEventContent] type MatrixAcceptMessageRequest = MatrixEventBase[*event.BeeperAcceptMessageRequestEventContent] type MatrixBeeperAIStream = MatrixEventBase[*event.BeeperAIStreamEventContent] type MatrixMarkedUnread = MatrixRoomMeta[*event.MarkedUnreadEventContent] type MatrixMute = MatrixRoomMeta[*event.BeeperMuteEventContent] type MatrixRoomTag = MatrixRoomMeta[*event.TagEventContent]