mautrix-go/bridgev2/networkinterface.go
Tulir Asokan e3d5267485
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
bridgev2/networkinterface: don't allow returning errors in Connect
2024-11-29 18:21:54 +02:00

1200 lines
42 KiB
Go

// 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"
"fmt"
"strings"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/configupgrade"
"go.mau.fi/util/ptr"
"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
}
type ConvertedMessage struct {
ReplyTo *networkid.MessageOptionalPartID
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 += "<br><br>" + textPart.Content.FormattedBody
} 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
}
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)
}
type StoppableNetwork interface {
// 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)
}
type IdentifierValidatingNetwork interface {
NetworkConnector
ValidateUserID(id networkid.UserID) bool
}
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 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 FileRestriction struct {
MaxSize int64
MimeTypes []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
}
type NetworkRoomCapabilities struct {
FormattedText bool
UserMentions bool
RoomMentions bool
LocationMessages bool
Captions bool
MaxTextLength int
MaxCaptionLength int
Polls bool
Threads bool
Replies bool
Edits bool
EditMaxCount int
EditMaxAge time.Duration
Deletes bool
DeleteMaxAge time.Duration
DefaultFileRestriction *FileRestriction
Files map[event.MessageType]FileRestriction
ReadReceipts bool
Reactions bool
ReactionCount int
AllowedReactions []string
}
// 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) *NetworkRoomCapabilities
// 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)
}
// 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(ctx context.Context, fetchParams FetchMessagesParams) (*FetchMessagesResponse, error)
}
type BackfillingNetworkAPIWithLimits interface {
BackfillingNetworkAPI
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
}
// 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 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
}
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
}
// 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)
}
// 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, name string, users ...networkid.UserID) (*CreateChatResponse, error)
}
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
// Deprecated: Use Target instead
TargetGhost *Ghost
// Deprecated: Use Target instead
TargetUserLogin *UserLogin
}
type MembershipHandlingNetworkAPI interface {
NetworkAPI
HandleMatrixMembership(ctx context.Context, msg *MatrixMembershipChange) (bool, 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"`
Native bool `json:"native,omitempty"`
}
type PushableNetworkAPI interface {
RegisterPushNotifications(ctx context.Context, pushType PushType, token string) error
GetPushConfigs() *PushConfig
}
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 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 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
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
}
type MatrixMessage struct {
MatrixEventBase[*event.MessageEventContent]
ThreadRoot *database.Message
ReplyTo *database.Message
}
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
}
type MatrixRoomName = MatrixRoomMeta[*event.RoomNameEventContent]
type MatrixRoomAvatar = MatrixRoomMeta[*event.RoomAvatarEventContent]
type MatrixRoomTopic = MatrixRoomMeta[*event.TopicEventContent]
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
}
type MatrixTyping struct {
Portal *Portal
IsTyping bool
Type TypingType
}
type MatrixMarkedUnread = MatrixRoomMeta[*event.MarkedUnreadEventContent]
type MatrixMute = MatrixRoomMeta[*event.BeeperMuteEventContent]
type MatrixRoomTag = MatrixRoomMeta[*event.TagEventContent]