summaryrefslogtreecommitdiffstats
path: root/vendor/maunium.net/go/mautrix/appservice/appservice.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/mautrix/appservice/appservice.go')
-rw-r--r--vendor/maunium.net/go/mautrix/appservice/appservice.go350
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
+}