diff options
Diffstat (limited to 'vendor/maunium.net/go/mautrix/appservice/appservice.go')
-rw-r--r-- | vendor/maunium.net/go/mautrix/appservice/appservice.go | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/appservice/appservice.go b/vendor/maunium.net/go/mautrix/appservice/appservice.go new file mode 100644 index 00000000..099e4b27 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/appservice.go @@ -0,0 +1,350 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "strings" + "sync" + "syscall" + "time" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/rs/zerolog" + "golang.org/x/net/publicsuffix" + "gopkg.in/yaml.v3" + "maunium.net/go/maulogger/v2/maulogadapt" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// EventChannelSize is the size for the Events channel in Appservice instances. +var EventChannelSize = 64 +var OTKChannelSize = 4 + +// Create a blank appservice instance. +func Create() *AppService { + jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + as := &AppService{ + Log: zerolog.Nop(), + clients: make(map[id.UserID]*mautrix.Client), + intents: make(map[id.UserID]*IntentAPI), + HTTPClient: &http.Client{Timeout: 180 * time.Second, Jar: jar}, + StateStore: mautrix.NewMemoryStateStore().(StateStore), + Router: mux.NewRouter(), + UserAgent: mautrix.DefaultUserAgent, + txnIDC: NewTransactionIDCache(128), + Live: true, + Ready: false, + ProcessID: getDefaultProcessID(), + + Events: make(chan *event.Event, EventChannelSize), + ToDeviceEvents: make(chan *event.Event, EventChannelSize), + OTKCounts: make(chan *mautrix.OTKCount, OTKChannelSize), + DeviceLists: make(chan *mautrix.DeviceLists, EventChannelSize), + QueryHandler: &QueryHandlerStub{}, + } + + as.Router.HandleFunc("/transactions/{txnID}", as.PutTransaction).Methods(http.MethodPut) + as.Router.HandleFunc("/rooms/{roomAlias}", as.GetRoom).Methods(http.MethodGet) + as.Router.HandleFunc("/users/{userID}", as.GetUser).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/app/v1/transactions/{txnID}", as.PutTransaction).Methods(http.MethodPut) + as.Router.HandleFunc("/_matrix/app/v1/rooms/{roomAlias}", as.GetRoom).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/app/v1/users/{userID}", as.GetUser).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/app/v1/ping", as.PostPing).Methods(http.MethodPost) + as.Router.HandleFunc("/_matrix/app/unstable/fi.mau.msc2659/ping", as.PostPing).Methods(http.MethodPost) + as.Router.HandleFunc("/_matrix/mau/live", as.GetLive).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/mau/ready", as.GetReady).Methods(http.MethodGet) + + return as +} + +// QueryHandler handles room alias and user ID queries from the homeserver. +type QueryHandler interface { + QueryAlias(alias string) bool + QueryUser(userID id.UserID) bool +} + +type QueryHandlerStub struct{} + +func (qh *QueryHandlerStub) QueryAlias(alias string) bool { + return false +} + +func (qh *QueryHandlerStub) QueryUser(userID id.UserID) bool { + return false +} + +type WebsocketHandler func(WebsocketCommand) (ok bool, data interface{}) + +type StateStore interface { + mautrix.StateStore + + IsRegistered(userID id.UserID) bool + MarkRegistered(userID id.UserID) + + GetPowerLevel(roomID id.RoomID, userID id.UserID) int + GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int + HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool +} + +// AppService is the main config for all appservices. +// It also serves as the appservice instance struct. +type AppService struct { + HomeserverDomain string + hsURLForClient *url.URL + Host HostConfig + + Registration *Registration + Log zerolog.Logger + + txnIDC *TransactionIDCache + + Events chan *event.Event + ToDeviceEvents chan *event.Event + DeviceLists chan *mautrix.DeviceLists + OTKCounts chan *mautrix.OTKCount + QueryHandler QueryHandler + StateStore StateStore + + Router *mux.Router + UserAgent string + server *http.Server + HTTPClient *http.Client + botClient *mautrix.Client + botIntent *IntentAPI + + DefaultHTTPRetries int + + Live bool + Ready bool + + clients map[id.UserID]*mautrix.Client + clientsLock sync.RWMutex + intents map[id.UserID]*IntentAPI + intentsLock sync.RWMutex + + ws *websocket.Conn + wsWriteLock sync.Mutex + StopWebsocket func(error) + websocketHandlers map[string]WebsocketHandler + websocketHandlersLock sync.RWMutex + websocketRequests map[int]chan<- *WebsocketCommand + websocketRequestsLock sync.RWMutex + websocketRequestID int32 + // ProcessID is an identifier sent to the websocket proxy for debugging connections + ProcessID string + + DoublePuppetValue string + GetProfile func(userID id.UserID, roomID id.RoomID) *event.MemberEventContent +} + +const DoublePuppetKey = "fi.mau.double_puppet_source" + +func getDefaultProcessID() string { + pid := syscall.Getpid() + uid := syscall.Getuid() + hostname, _ := os.Hostname() + return fmt.Sprintf("%s-%d-%d", hostname, uid, pid) +} + +func (as *AppService) PrepareWebsocket() { + as.websocketHandlersLock.Lock() + defer as.websocketHandlersLock.Unlock() + if as.websocketHandlers == nil { + as.websocketHandlers = make(map[string]WebsocketHandler, 32) + as.websocketRequests = make(map[int]chan<- *WebsocketCommand) + } +} + +// HostConfig contains info about how to host the appservice. +type HostConfig struct { + Hostname string `yaml:"hostname"` + Port uint16 `yaml:"port"` + TLSKey string `yaml:"tls_key,omitempty"` + TLSCert string `yaml:"tls_cert,omitempty"` +} + +// Address gets the whole address of the Appservice. +func (hc *HostConfig) Address() string { + return fmt.Sprintf("%s:%d", hc.Hostname, hc.Port) +} + +func (hc *HostConfig) IsUnixSocket() bool { + return strings.HasPrefix(hc.Hostname, "/") +} + +func (hc *HostConfig) IsConfigured() bool { + return hc.IsUnixSocket() || hc.Port != 0 +} + +// Save saves this config into a file at the given path. +func (as *AppService) Save(path string) error { + data, err := yaml.Marshal(as) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +// YAML returns the config in YAML format. +func (as *AppService) YAML() (string, error) { + data, err := yaml.Marshal(as) + if err != nil { + return "", err + } + return string(data), nil +} + +func (as *AppService) BotMXID() id.UserID { + return id.NewUserID(as.Registration.SenderLocalpart, as.HomeserverDomain) +} + +func (as *AppService) makeIntent(userID id.UserID) *IntentAPI { + as.intentsLock.Lock() + defer as.intentsLock.Unlock() + + intent, ok := as.intents[userID] + if ok { + return intent + } + + localpart, homeserver, err := userID.Parse() + if err != nil || len(localpart) == 0 || homeserver != as.HomeserverDomain { + if err != nil { + as.Log.Error().Err(err). + Str("user_id", userID.String()). + Msg("Failed to parse user ID") + } else if len(localpart) == 0 { + as.Log.Error().Err(err). + Str("user_id", userID.String()). + Msg("Failed to make intent: localpart is empty") + } else if homeserver != as.HomeserverDomain { + as.Log.Error().Err(err). + Str("user_id", userID.String()). + Str("expected_homeserver", as.HomeserverDomain). + Msg("Failed to make intent: homeserver doesn't match") + } + return nil + } + intent = as.NewIntentAPI(localpart) + as.intents[userID] = intent + return intent +} + +func (as *AppService) Intent(userID id.UserID) *IntentAPI { + as.intentsLock.RLock() + intent, ok := as.intents[userID] + as.intentsLock.RUnlock() + if !ok { + return as.makeIntent(userID) + } + return intent +} + +func (as *AppService) BotIntent() *IntentAPI { + if as.botIntent == nil { + as.botIntent = as.makeIntent(as.BotMXID()) + } + return as.botIntent +} + +func (as *AppService) SetHomeserverURL(homeserverURL string) error { + parsedURL, err := url.Parse(homeserverURL) + if err != nil { + return err + } + + as.hsURLForClient = parsedURL + if as.hsURLForClient.Scheme == "unix" { + as.hsURLForClient.Scheme = "http" + as.hsURLForClient.Host = "unix" + as.hsURLForClient.Path = "" + } else if as.hsURLForClient.Scheme == "" { + as.hsURLForClient.Scheme = "https" + } + as.hsURLForClient.RawPath = parsedURL.EscapedPath() + + jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + as.HTTPClient = &http.Client{Timeout: 180 * time.Second, Jar: jar} + if parsedURL.Scheme == "unix" { + as.HTTPClient.Transport = &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", parsedURL.Path) + }, + } + } + return nil +} + +func (as *AppService) NewMautrixClient(userID id.UserID) *mautrix.Client { + client := &mautrix.Client{ + HomeserverURL: as.hsURLForClient, + UserID: userID, + SetAppServiceUserID: true, + AccessToken: as.Registration.AppToken, + UserAgent: as.UserAgent, + StateStore: as.StateStore, + Log: as.Log.With().Str("as_user_id", userID.String()).Logger(), + Client: as.HTTPClient, + DefaultHTTPRetries: as.DefaultHTTPRetries, + } + client.Logger = maulogadapt.ZeroAsMau(&client.Log) + return client +} + +func (as *AppService) NewExternalMautrixClient(userID id.UserID, token string, homeserverURL string) (*mautrix.Client, error) { + client := as.NewMautrixClient(userID) + client.AccessToken = token + if homeserverURL != "" { + client.Client = &http.Client{Timeout: 180 * time.Second} + var err error + client.HomeserverURL, err = mautrix.ParseAndNormalizeBaseURL(homeserverURL) + if err != nil { + return nil, err + } + } + return client, nil +} + +func (as *AppService) makeClient(userID id.UserID) *mautrix.Client { + as.clientsLock.Lock() + defer as.clientsLock.Unlock() + + client, ok := as.clients[userID] + if !ok { + client = as.NewMautrixClient(userID) + as.clients[userID] = client + } + return client +} + +func (as *AppService) Client(userID id.UserID) *mautrix.Client { + as.clientsLock.RLock() + client, ok := as.clients[userID] + as.clientsLock.RUnlock() + if !ok { + return as.makeClient(userID) + } + return client +} + +func (as *AppService) BotClient() *mautrix.Client { + if as.botClient == nil { + as.botClient = as.makeClient(as.BotMXID()) + } + return as.botClient +} |