diff --git a/README.md b/README.md index 06e410e1..5a893ddf 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,23 @@ # mautrix-go [![GoDoc](https://godoc.org/maunium.net/go/mautrix?status.svg)](https://godoc.org/maunium.net/go/mautrix) -A Golang Matrix framework. +A Golang Matrix framework. Used by [gomuks](https://matrix.org/docs/projects/client/gomuks), +[go-neb](https://github.com/matrix-org/go-neb), [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp) +and others. + +Matrix room: [`#maunium:maunium.net`](https://matrix.to/#/#maunium:maunium.net) This project is based on [matrix-org/gomatrix](https://github.com/matrix-org/gomatrix). The original project is licensed under [Apache 2.0](https://github.com/matrix-org/gomatrix/blob/master/LICENSE). +In addition to the basic client API features the original project has, this framework also has: + +* Appservice support (Intent API like mautrix-python, room state storage, etc) +* End-to-end encryption support (incl. interactive SAS verification) +* Structs for parsing event content +* Helpers for parsing and generating Matrix HTML +* Helpers for handling push rules + This project contains modules that are licensed under Apache 2.0: * [maunium.net/go/mautrix/crypto/canonicaljson](crypto/canonicaljson) diff --git a/appservice/http.go b/appservice/http.go index bcdbbc1e..ad4b2392 100644 --- a/appservice/http.go +++ b/appservice/http.go @@ -11,6 +11,7 @@ import ( "encoding/json" "io/ioutil" "net/http" + "strings" "time" "github.com/gorilla/mux" @@ -51,27 +52,38 @@ func (as *AppService) Stop() { return } - ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() _ = as.server.Shutdown(ctx) as.server = nil } // CheckServerToken checks if the given request originated from the Matrix homeserver. -func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) bool { - query := r.URL.Query() - val, ok := query["access_token"] - if !ok { +func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) (isValid bool) { + authHeader := r.Header.Get("Authorization") + if len(authHeader) > 0 && strings.HasPrefix(authHeader, "Bearer ") { + isValid = authHeader[len("Bearer "):] == as.Registration.ServerToken + } else { + queryToken := r.URL.Query().Get("access_token") + if len(queryToken) > 0 { + isValid = queryToken == as.Registration.ServerToken + } else { + Error{ + ErrorCode: ErrUnknownToken, + HTTPStatus: http.StatusForbidden, + Message: "Missing access token", + }.Write(w) + return + } + } + if !isValid { Error{ - ErrorCode: ErrForbidden, + ErrorCode: ErrUnknownToken, HTTPStatus: http.StatusForbidden, - Message: "Bad token supplied.", + Message: "Incorrect access token", }.Write(w) - return false } - for _, str := range val { - return str == as.Registration.ServerToken - } - return false + return } // PutTransaction handles a /transactions PUT call from the homeserver. @@ -86,7 +98,7 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) { Error{ ErrorCode: ErrNoTransactionID, HTTPStatus: http.StatusBadRequest, - Message: "Missing transaction ID.", + Message: "Missing transaction ID", }.Write(w) return } @@ -94,9 +106,9 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil || len(body) == 0 { Error{ - ErrorCode: ErrNoBody, + ErrorCode: ErrNotJSON, HTTPStatus: http.StatusBadRequest, - Message: "Missing request body.", + Message: "Missing request body", }.Write(w) return } @@ -111,11 +123,19 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) { if err != nil { as.Log.Warnfln("Failed to parse JSON of transaction %s: %v", txnID, err) Error{ - ErrorCode: ErrInvalidJSON, + ErrorCode: ErrBadJSON, HTTPStatus: http.StatusBadRequest, - Message: "Failed to parse body JSON.", + Message: "Failed to parse body JSON", }.Write(w) } else { + if as.Registration.EphemeralEvents { + if eventList.EphemeralEvents != nil { + as.handleEvents(eventList.EphemeralEvents, event.EphemeralEventType) + } else if eventList.SoruEphemeralEvents != nil { + as.handleEvents(eventList.SoruEphemeralEvents, event.EphemeralEventType) + } + } + as.handleEvents(eventList.Events, event.UnknownEventType) for _, evt := range eventList.Events { if evt.StateKey != nil { evt.Type.Class = event.StateEventType @@ -134,6 +154,30 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) { as.lastProcessedTransaction = txnID } +func (as *AppService) handleEvents(evts []*event.Event, typeClass event.TypeClass) { + for _, evt := range evts { + if typeClass != event.UnknownEventType { + evt.Type.Class = typeClass + } else if evt.StateKey != nil { + evt.Type.Class = event.StateEventType + } else { + evt.Type.Class = event.MessageEventType + } + err := evt.Content.ParseRaw(evt.Type) + if err != nil { + if evt.ID != "" { + as.Log.Debugfln("Failed to parse content of %s (%s): %v", evt.ID, evt.Type.Type, err) + } else { + as.Log.Debugfln("Failed to parse content of a %s: %v", evt.Type.Type, err) + } + } + if evt.Type.IsState() { + as.UpdateState(evt) + } + as.Events <- evt + } +} + // GetRoom handles a /rooms GET call from the homeserver. func (as *AppService) GetRoom(w http.ResponseWriter, r *http.Request) { if !as.CheckServerToken(w, r) { diff --git a/appservice/protocol.go b/appservice/protocol.go index e6ec1f4c..b6cc13e6 100644 --- a/appservice/protocol.go +++ b/appservice/protocol.go @@ -15,7 +15,9 @@ import ( // EventList contains a list of events. type EventList struct { - Events []*event.Event `json:"events"` + Events []*event.Event `json:"events"` + EphemeralEvents []*event.Event `json:"ephemeral"` + SoruEphemeralEvents []*event.Event `json:"de.sorunome.msc2409.ephemeral"` } // EventListener is a function that receives events. @@ -23,12 +25,14 @@ type EventListener func(evt *event.Event) // WriteBlankOK writes a blank OK message as a reply to a HTTP request. func WriteBlankOK(w http.ResponseWriter) { + w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("{}")) } // Respond responds to a HTTP request with a JSON object. func Respond(w http.ResponseWriter, data interface{}) error { + w.Header().Add("Content-Type", "application/json") dataStr, err := json.Marshal(data) if err != nil { return err @@ -45,6 +49,7 @@ type Error struct { } func (err Error) Write(w http.ResponseWriter) { + w.Header().Add("Content-Type", "application/json") w.WriteHeader(err.HTTPStatus) _ = Respond(w, &err) } @@ -54,13 +59,13 @@ type ErrorCode string // Native ErrorCodes const ( - ErrForbidden ErrorCode = "M_FORBIDDEN" - ErrUnknown ErrorCode = "M_UNKNOWN" + ErrUnknownToken ErrorCode = "M_UNKNOWN_TOKEN" + ErrBadJSON ErrorCode = "M_BAD_JSON" + ErrNotJSON ErrorCode = "M_NOT_JSON" + ErrUnknown ErrorCode = "M_UNKNOWN" ) // Custom ErrorCodes const ( ErrNoTransactionID ErrorCode = "NET.MAUNIUM.NO_TRANSACTION_ID" - ErrNoBody ErrorCode = "NET.MAUNIUM.NO_REQUEST_BODY" - ErrInvalidJSON ErrorCode = "NET.MAUNIUM.INVALID_JSON" ) diff --git a/appservice/registration.go b/appservice/registration.go index c7a9f1ba..729c951c 100644 --- a/appservice/registration.go +++ b/appservice/registration.go @@ -23,6 +23,7 @@ type Registration struct { SenderLocalpart string `yaml:"sender_localpart"` RateLimited bool `yaml:"rate_limited"` Namespaces Namespaces `yaml:"namespaces"` + EphemeralEvents bool `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty"` } // CreateRegistration creates a Registration with random appservice and homeserver tokens. diff --git a/crypto/decryptmegolm.go b/crypto/decryptmegolm.go index 47c97507..f42a0354 100644 --- a/crypto/decryptmegolm.go +++ b/crypto/decryptmegolm.go @@ -8,8 +8,8 @@ package crypto import ( "encoding/json" - - "github.com/pkg/errors" + "errors" + "fmt" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -18,7 +18,7 @@ import ( var ( IncorrectEncryptedContentType = errors.New("event content is not instance of *event.EncryptedEventContent") NoSessionFound = errors.New("failed to decrypt megolm event: no session with given ID found") - DuplicateMessageIndex = errors.New("duplicate message index") + DuplicateMessageIndex = errors.New("duplicate megolm message index") WrongRoom = errors.New("encrypted megolm event is not intended for this room") DeviceKeyMismatch = errors.New("device keys in event and verified device info do not match") ) @@ -39,14 +39,14 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro } sess, err := mach.CryptoStore.GetGroupSession(evt.RoomID, content.SenderKey, content.SessionID) if err != nil { - return nil, errors.Wrap(err, "failed to get group session") + return nil, fmt.Errorf("failed to get group session: %w", err) } else if sess == nil { mach.checkIfWedged(evt) - return nil, NoSessionFound + return nil, fmt.Errorf("%w (ID %s)", NoSessionFound, content.SessionID) } plaintext, messageIndex, err := sess.Internal.Decrypt(content.MegolmCiphertext) if err != nil { - return nil, errors.Wrap(err, "failed to decrypt megolm event") + return nil, fmt.Errorf("failed to decrypt megolm event: %w", err) } else if !mach.CryptoStore.ValidateMessageIndex(content.SenderKey, content.SessionID, evt.ID, messageIndex, evt.Timestamp) { return nil, DuplicateMessageIndex } @@ -72,7 +72,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro megolmEvt := &megolmEvent{} err = json.Unmarshal(plaintext, &megolmEvt) if err != nil { - return nil, errors.Wrap(err, "failed to parse megolm payload") + return nil, fmt.Errorf("failed to parse megolm payload: %w", err) } else if megolmEvt.RoomID != evt.RoomID { return nil, WrongRoom } @@ -82,7 +82,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro if event.IsUnsupportedContentType(err) { mach.Log.Warn("Unsupported event type %s in encrypted event %s", megolmEvt.Type.Repr(), evt.ID) } else { - return nil, errors.Wrap(err, "failed to parse content of megolm payload event") + return nil, fmt.Errorf("failed to parse content of megolm payload event: %w", err) } } if content.RelatesTo != nil { diff --git a/crypto/decryptolm.go b/crypto/decryptolm.go index ce09e37e..6dd6f38e 100644 --- a/crypto/decryptolm.go +++ b/crypto/decryptolm.go @@ -8,8 +8,8 @@ package crypto import ( "encoding/json" - - "github.com/pkg/errors" + "errors" + "fmt" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -76,7 +76,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic mach.Log.Warn("Found matching session yet decryption failed for sender %s with key %s", sender, senderKey) mach.markDeviceForUnwedging(sender, senderKey) } - return nil, errors.Wrap(err, "failed to decrypt olm event") + return nil, fmt.Errorf("failed to decrypt olm event: %w", err) } // Decryption failed with every known session or no known sessions, let's try to create a new session. @@ -92,13 +92,13 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic session, err := mach.createInboundSession(senderKey, ciphertext) if err != nil { mach.markDeviceForUnwedging(sender, senderKey) - return nil, errors.Wrap(err, "failed to create new session from prekey message") + return nil, fmt.Errorf("failed to create new session from prekey message: %w", err) } - mach.Log.Trace("Created inbound session %s for %s/%s (sender key: %s)", session.ID(), sender, deviceID, senderKey) + mach.Log.Debug("Created inbound olm session %s for %s/%s (sender key: %s)", session.ID(), sender, deviceID, senderKey) plaintext, err = session.Decrypt(ciphertext, olmType) if err != nil { - return nil, errors.Wrap(err, "failed to decrypt olm event with session created from prekey message") + return nil, fmt.Errorf("failed to decrypt olm event with session created from prekey message: %w", err) } err = mach.CryptoStore.UpdateSession(senderKey, session) @@ -110,7 +110,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic var olmEvt DecryptedOlmEvent err = json.Unmarshal(plaintext, &olmEvt) if err != nil { - return nil, errors.Wrap(err, "failed to parse olm payload") + return nil, fmt.Errorf("failed to parse olm payload: %w", err) } if sender != olmEvt.Sender { return nil, SenderMismatch @@ -122,7 +122,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic err = olmEvt.Content.ParseRaw(olmEvt.Type) if err != nil && !event.IsUnsupportedContentType(err) { - return nil, errors.Wrap(err, "failed to parse content of olm payload event") + return nil, fmt.Errorf("failed to parse content of olm payload event: %w", err) } olmEvt.SenderKey = senderKey @@ -133,13 +133,13 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic func (mach *OlmMachine) tryDecryptOlmCiphertext(senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) ([]byte, error) { sessions, err := mach.CryptoStore.GetSessions(senderKey) if err != nil { - return nil, errors.Wrapf(err, "failed to get session for %s", senderKey) + return nil, fmt.Errorf("failed to get session for %s: %w", senderKey, err) } for _, session := range sessions { if olmType == id.OlmMsgTypePreKey { matches, err := session.Internal.MatchesInboundSession(ciphertext) if err != nil { - return nil, errors.Wrap(err, "failed to check if ciphertext matches inbound session") + return nil, fmt.Errorf("failed to check if ciphertext matches inbound session: %w", err) } else if !matches { continue } diff --git a/crypto/devicelist.go b/crypto/devicelist.go index bb4e45df..1e9d4a89 100644 --- a/crypto/devicelist.go +++ b/crypto/devicelist.go @@ -7,7 +7,8 @@ package crypto import ( - "github.com/pkg/errors" + "errors" + "fmt" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/olm" @@ -163,7 +164,7 @@ func (mach *OlmMachine) validateDevice(userID id.UserID, deviceID id.DeviceID, d ok, err := olm.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey) if err != nil { - return existing, errors.Wrap(err, "failed to verify signature") + return existing, fmt.Errorf("failed to verify signature: %w", err) } else if !ok { return existing, InvalidKeySignature } diff --git a/crypto/encryptmegolm.go b/crypto/encryptmegolm.go index bb3d0c46..a82a1123 100644 --- a/crypto/encryptmegolm.go +++ b/crypto/encryptmegolm.go @@ -8,8 +8,8 @@ package crypto import ( "encoding/json" - - "github.com/pkg/errors" + "errors" + "fmt" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" @@ -52,7 +52,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(roomID id.RoomID, evtType event.Type, mach.Log.Trace("Encrypting event of type %s for %s", evtType.Type, roomID) session, err := mach.CryptoStore.GetOutboundGroupSession(roomID) if err != nil { - return nil, errors.Wrap(err, "failed to get outbound group session") + return nil, fmt.Errorf("failed to get outbound group session: %w", err) } else if session == nil { return nil, NoGroupSession } @@ -89,6 +89,11 @@ func (mach *OlmMachine) newOutboundGroupSession(roomID id.RoomID) *OutboundGroup return session } +type deviceSessionWrapper struct { + session *OlmSession + identity *DeviceIdentity +} + // ShareGroupSession shares a group session for a specific room with all the devices of the given user list. // // For devices with TrustStateBlacklisted, a m.room_key.withheld event with code=m.blacklisted is sent. @@ -97,7 +102,7 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e mach.Log.Debug("Sharing group session for room %s to %v", roomID, users) session, err := mach.CryptoStore.GetOutboundGroupSession(roomID) if err != nil { - return errors.Wrap(err, "failed to get previous outbound group session") + return fmt.Errorf("failed to get previous outbound group session: %w", err) } else if session != nil && session.Shared && !session.Expired() { return AlreadyShared } @@ -105,8 +110,9 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e session = mach.newOutboundGroupSession(roomID) } - toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)} + withheldCount := 0 toDeviceWithheld := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)} + olmSessions := make(map[id.UserID]map[id.DeviceID]deviceSessionWrapper) missingSessions := make(map[id.UserID]map[id.DeviceID]*DeviceIdentity) missingUserSessions := make(map[id.DeviceID]*DeviceIdentity) var fetchKeys []id.UserID @@ -122,9 +128,10 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e mach.Log.Trace("%s has no devices, skipping", userID) } else { mach.Log.Trace("Trying to encrypt group session %s for %s", session.ID(), userID) - toDevice.Messages[userID] = make(map[id.DeviceID]*event.Content) toDeviceWithheld.Messages[userID] = make(map[id.DeviceID]*event.Content) - mach.encryptGroupSessionForUser(session, userID, devices, toDevice.Messages[userID], toDeviceWithheld.Messages[userID], missingUserSessions) + olmSessions[userID] = make(map[id.DeviceID]deviceSessionWrapper) + mach.findOlmSessionsForUser(session, userID, devices, olmSessions[userID], toDeviceWithheld.Messages[userID], missingUserSessions) + withheldCount += len(toDeviceWithheld.Messages[userID]) if len(missingUserSessions) > 0 { missingSessions[userID] = missingUserSessions missingUserSessions = make(map[id.DeviceID]*DeviceIdentity) @@ -132,9 +139,6 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e if len(toDeviceWithheld.Messages[userID]) == 0 { delete(toDeviceWithheld.Messages, userID) } - if len(toDevice.Messages[userID]) == 0 { - delete(toDevice.Messages, userID) - } } } @@ -146,10 +150,12 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e } } - mach.Log.Trace("Creating missing outbound sessions") - err = mach.createOutboundSessions(missingSessions) - if err != nil { - mach.Log.Error("Failed to create missing outbound sessions: %v", err) + if len(missingSessions) > 0 { + mach.Log.Trace("Creating missing outbound sessions") + err = mach.createOutboundSessions(missingSessions) + if err != nil { + mach.Log.Error("Failed to create missing outbound sessions: %v", err) + } } for userID, devices := range missingSessions { @@ -157,10 +163,10 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e // No missing sessions continue } - output, ok := toDevice.Messages[userID] + output, ok := olmSessions[userID] if !ok { - output = make(map[id.DeviceID]*event.Content) - toDevice.Messages[userID] = output + output = make(map[id.DeviceID]deviceSessionWrapper) + olmSessions[userID] = output } withheld, ok := toDeviceWithheld.Messages[userID] if !ok { @@ -168,35 +174,62 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e toDeviceWithheld.Messages[userID] = withheld } mach.Log.Trace("Trying to encrypt group session %s for %s (post-fetch retry)", session.ID(), userID) - mach.encryptGroupSessionForUser(session, userID, devices, output, withheld, nil) + mach.findOlmSessionsForUser(session, userID, devices, output, withheld, nil) + withheldCount += len(toDeviceWithheld.Messages[userID]) if len(toDeviceWithheld.Messages[userID]) == 0 { delete(toDeviceWithheld.Messages, userID) } - if len(toDevice.Messages[userID]) == 0 { - delete(toDevice.Messages, userID) + } + + err = mach.encryptAndSendGroupSession(session, olmSessions) + if err != nil { + return fmt.Errorf("failed to share group session: %w", err) + } + + if len(toDeviceWithheld.Messages) > 0 { + mach.Log.Trace("Sending to-device messages to %d devices of %d users to report withheld keys in %s", withheldCount, len(toDeviceWithheld.Messages), roomID) + // TODO remove the next 4 lines once clients support m.room_key.withheld + _, err = mach.Client.SendToDevice(event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld) + if err != nil { + mach.Log.Warn("Failed to report withheld keys in %s (legacy event type): %v", roomID, err) + } + _, err = mach.Client.SendToDevice(event.ToDeviceRoomKeyWithheld, toDeviceWithheld) + if err != nil { + mach.Log.Warn("Failed to report withheld keys in %s: %v", roomID, err) } } - mach.Log.Trace("Sending to-device to %d users to share group session for %s", len(toDevice.Messages), roomID) - _, err = mach.Client.SendToDevice(event.ToDeviceEncrypted, toDevice) - if err != nil { - return errors.Wrap(err, "failed to share group session") - } - - mach.Log.Trace("Sending to-device messages to %d users to report withheld keys in %s", len(toDeviceWithheld.Messages), roomID) - // TODO remove the next line once clients support m.room_key.withheld - _, _ = mach.Client.SendToDevice(event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld) - _, err = mach.Client.SendToDevice(event.ToDeviceRoomKeyWithheld, toDeviceWithheld) - if err != nil { - mach.Log.Warn("Failed to report withheld keys in %s: %v", roomID, err) - } - - mach.Log.Debug("Group session for %s successfully shared", roomID) + mach.Log.Debug("Group session %s for %s successfully shared", session.ID(), roomID) session.Shared = true return mach.CryptoStore.AddOutboundGroupSession(session) } -func (mach *OlmMachine) encryptGroupSessionForUser(session *OutboundGroupSession, userID id.UserID, devices map[id.DeviceID]*DeviceIdentity, output, withheld map[id.DeviceID]*event.Content, missingOutput map[id.DeviceID]*DeviceIdentity) { +func (mach *OlmMachine) encryptAndSendGroupSession(session *OutboundGroupSession, olmSessions map[id.UserID]map[id.DeviceID]deviceSessionWrapper) error { + deviceCount := 0 + toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)} + for userID, sessions := range olmSessions { + if len(sessions) == 0 { + continue + } + output := make(map[id.DeviceID]*event.Content) + toDevice.Messages[userID] = output + for deviceID, device := range sessions { + device.session.Lock() + // We intentionally defer in a loop as it's the safest way of making sure nothing gets locked permanently. + defer device.session.Unlock() + content := mach.encryptOlmEvent(device.session, device.identity, event.ToDeviceRoomKey, session.ShareContent()) + output[deviceID] = &event.Content{Parsed: content} + deviceCount++ + mach.Log.Trace("Encrypted group session %s for %s of %s", session.ID(), deviceID, userID) + } + } + + mach.Log.Trace("Sending to-device to %d devices of %d users to share group session %s", deviceCount, len(toDevice.Messages), session.ID()) + _, err := mach.Client.SendToDevice(event.ToDeviceEncrypted, toDevice) + return err +} + +func (mach *OlmMachine) findOlmSessionsForUser(session *OutboundGroupSession, userID id.UserID, devices map[id.DeviceID]*DeviceIdentity, output map[id.DeviceID]deviceSessionWrapper, withheld map[id.DeviceID]*event.Content, missingOutput map[id.DeviceID]*DeviceIdentity) { for deviceID, device := range devices { userKey := UserDevice{UserID: userID, DeviceID: deviceID} if state := session.Users[userKey]; state != OGSNotShared { @@ -233,10 +266,11 @@ func (mach *OlmMachine) encryptGroupSessionForUser(session *OutboundGroupSession missingOutput[deviceID] = device } } else { - content := mach.encryptOlmEvent(deviceSession, device, event.ToDeviceRoomKey, session.ShareContent()) - output[deviceID] = &event.Content{Parsed: content} + output[deviceID] = deviceSessionWrapper{ + session: deviceSession, + identity: device, + } session.Users[userKey] = OGSAlreadyShared - mach.Log.Trace("Encrypted group session %s for %s of %s", session.ID(), deviceID, userID) } } } diff --git a/crypto/encryptolm.go b/crypto/encryptolm.go index 1ab0bca7..7dc3814a 100644 --- a/crypto/encryptolm.go +++ b/crypto/encryptolm.go @@ -8,8 +8,7 @@ package crypto import ( "encoding/json" - - "github.com/pkg/errors" + "fmt" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/olm" @@ -69,7 +68,7 @@ func (mach *OlmMachine) createOutboundSessions(input map[id.UserID]map[id.Device Timeout: 10 * 1000, }) if err != nil { - return errors.Wrap(err, "failed to claim keys") + return fmt.Errorf("failed to claim keys: %w", err) } for userID, user := range resp.OneTimeKeys { for deviceID, oneTimeKeys := range user { diff --git a/crypto/keyexport.go b/crypto/keyexport.go index bebdc32b..511a56dd 100644 --- a/crypto/keyexport.go +++ b/crypto/keyexport.go @@ -20,7 +20,6 @@ import ( "fmt" "math" - "github.com/pkg/errors" "golang.org/x/crypto/pbkdf2" "maunium.net/go/mautrix/crypto/olm" @@ -91,7 +90,7 @@ func exportSessions(sessions []*InboundGroupSession) ([]ExportedSession, error) for i, session := range sessions { key, err := session.Internal.Export(session.Internal.FirstKnownIndex()) if err != nil { - return nil, errors.Wrap(err, "failed to export session") + return nil, fmt.Errorf("failed to export session: %w", err) } export[i] = ExportedSession{ Algorithm: id.AlgorithmMegolmV1, diff --git a/crypto/keyimport.go b/crypto/keyimport.go index 4c8fc163..4144f070 100644 --- a/crypto/keyimport.go +++ b/crypto/keyimport.go @@ -15,8 +15,8 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" - - "github.com/pkg/errors" + "errors" + "fmt" "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/id" @@ -85,7 +85,7 @@ func decryptKeyExport(passphrase string, exportData []byte) ([]ExportedSession, var sessionsJSON []ExportedSession err := json.Unmarshal(unencryptedData, &sessionsJSON) if err != nil { - return nil, errors.Wrap(err, "invalid export json") + return nil, fmt.Errorf("invalid export json: %w", err) } return sessionsJSON, nil } @@ -97,7 +97,7 @@ func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, er igsInternal, err := olm.InboundGroupSessionImport([]byte(session.SessionKey)) if err != nil { - return false, errors.Wrap(err, "failed to import session") + return false, fmt.Errorf("failed to import session: %w", err) } else if igsInternal.ID() != session.SessionID { return false, ErrMismatchingExportedSessionID } @@ -116,8 +116,9 @@ func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, er } err = mach.CryptoStore.PutGroupSession(igs.RoomID, igs.SenderKey, igs.ID(), igs) if err != nil { - return false, errors.Wrap(err, "failed to store imported session") + return false, fmt.Errorf("failed to store imported session: %w", err) } + mach.markSessionReceived(igs.ID()) return true, nil } diff --git a/crypto/keysharing.go b/crypto/keysharing.go index 3fd73010..8b67fbca 100644 --- a/crypto/keysharing.go +++ b/crypto/keysharing.go @@ -134,6 +134,7 @@ func (mach *OlmMachine) importForwardedRoomKey(evt *DecryptedOlmEvent, content * mach.Log.Error("Failed to store new inbound group session: %v", err) return false } + mach.markSessionReceived(content.SessionID) mach.Log.Trace("Created inbound group session %s/%s/%s", content.RoomID, content.SenderKey, content.SessionID) return true } diff --git a/crypto/machine.go b/crypto/machine.go index e9ec8d09..085fed65 100644 --- a/crypto/machine.go +++ b/crypto/machine.go @@ -7,11 +7,11 @@ package crypto import ( + "errors" + "fmt" "sync" "time" - "github.com/pkg/errors" - "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/crypto/ssss" "maunium.net/go/mautrix/id" @@ -52,6 +52,9 @@ type OlmMachine struct { roomKeyRequestFilled *sync.Map keyVerificationTransactionState *sync.Map + keyWaiters map[id.SessionID]chan struct{} + keyWaitersLock sync.Mutex + CrossSigningKeys *CrossSigningKeysCache crossSigningPubkeys *CrossSigningPublicKeysCache } @@ -86,6 +89,8 @@ func NewOlmMachine(client *mautrix.Client, log Logger, cryptoStore Store, stateS roomKeyRequestFilled: &sync.Map{}, keyVerificationTransactionState: &sync.Map{}, + + keyWaiters: make(map[id.SessionID]chan struct{}), } mach.AllowKeyShare = mach.defaultAllowKeyShare return mach @@ -266,7 +271,7 @@ func (mach *OlmMachine) GetOrFetchDevice(userID id.UserID, deviceID id.DeviceID) // get device identity device, err := mach.CryptoStore.GetDevice(userID, deviceID) if err != nil { - return nil, errors.Wrap(err, "failed to get sender device from store") + return nil, fmt.Errorf("failed to get sender device from store: %w", err) } else if device != nil { return device, nil } @@ -276,9 +281,9 @@ func (mach *OlmMachine) GetOrFetchDevice(userID id.UserID, deviceID id.DeviceID) if device, ok = devices[deviceID]; ok { return device, nil } - return nil, errors.Errorf("Failed to get identity for device %v", deviceID) + return nil, fmt.Errorf("didn't get identity for device %s of %s", deviceID, userID) } - return nil, errors.Errorf("Error fetching devices for user %v", userID) + return nil, fmt.Errorf("didn't get any devices for %s", userID) } // SendEncryptedToDevice sends an Olm-encrypted event to the given user device. @@ -298,9 +303,12 @@ func (mach *OlmMachine) SendEncryptedToDevice(device *DeviceIdentity, content ev return err } if olmSess == nil { - return errors.Errorf("Did not find created outbound session for device %v", device.DeviceID) + return fmt.Errorf("didn't find created outbound session for device %s of %s", device.DeviceID, device.UserID) } + olmSess.Lock() + defer olmSess.Unlock() + encrypted := mach.encryptOlmEvent(olmSess, device, event.ToDeviceForwardedRoomKey, content) encryptedContent := &event.Content{Parsed: &encrypted} @@ -329,8 +337,40 @@ func (mach *OlmMachine) createGroupSession(senderKey id.SenderKey, signingKey id err = mach.CryptoStore.PutGroupSession(roomID, senderKey, sessionID, igs) if err != nil { mach.Log.Error("Failed to store new inbound group session: %v", err) + return + } + mach.markSessionReceived(sessionID) + mach.Log.Debug("Received inbound group session %s / %s / %s", roomID, senderKey, sessionID) +} + +func (mach *OlmMachine) markSessionReceived(id id.SessionID) { + mach.keyWaitersLock.Lock() + ch, ok := mach.keyWaiters[id] + if ok { + close(ch) + delete(mach.keyWaiters, id) + } + mach.keyWaitersLock.Unlock() +} + +// WaitForSession waits for the given Megolm session to arrive. +func (mach *OlmMachine) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { + mach.keyWaitersLock.Lock() + ch, ok := mach.keyWaiters[sessionID] + if !ok { + ch := make(chan struct{}) + mach.keyWaiters[sessionID] = ch + } + mach.keyWaitersLock.Unlock() + select { + case <-ch: + return true + case <-time.After(timeout): + sess, err := mach.CryptoStore.GetGroupSession(roomID, senderKey, sessionID) + // Check if the session somehow appeared in the store without telling us + // We accept withheld sessions as received, as then the decryption attempt will show the error. + return sess != nil || errors.Is(err, ErrGroupSessionWithheld) } - mach.Log.Trace("Created inbound group session %s/%s/%s", roomID, senderKey, sessionID) } func (mach *OlmMachine) receiveRoomKey(evt *DecryptedOlmEvent, content *event.RoomKeyEventContent) { diff --git a/crypto/sessions.go b/crypto/sessions.go index c84d64e6..57af43ba 100644 --- a/crypto/sessions.go +++ b/crypto/sessions.go @@ -7,11 +7,11 @@ package crypto import ( + "errors" "strings" + "sync" "time" - "github.com/pkg/errors" - "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/event" @@ -43,6 +43,20 @@ type OlmSession struct { Internal olm.Session ExpirationMixin id id.SessionID + // This is unexported so gob wouldn't insist on trying to marshaling it + lock sync.Locker +} + +func (session *OlmSession) SetLock(lock sync.Locker) { + session.lock = lock +} + +func (session *OlmSession) Lock() { + session.lock.Lock() +} + +func (session *OlmSession) Unlock() { + session.lock.Unlock() } func (session *OlmSession) ID() id.SessionID { @@ -55,6 +69,7 @@ func (session *OlmSession) ID() id.SessionID { func wrapSession(session *olm.Session) *OlmSession { return &OlmSession{ Internal: *session, + lock: &sync.Mutex{}, ExpirationMixin: ExpirationMixin{ TimeMixin: TimeMixin{ CreationTime: time.Now(), diff --git a/crypto/sql_store.go b/crypto/sql_store.go index 4d276f0a..54789574 100644 --- a/crypto/sql_store.go +++ b/crypto/sql_store.go @@ -10,9 +10,9 @@ import ( "database/sql" "fmt" "strings" + "sync" "github.com/lib/pq" - "github.com/pkg/errors" "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/crypto/sql_store_upgrade" @@ -31,6 +31,9 @@ type SQLCryptoStore struct { SyncToken string PickleKey []byte Account *OlmAccount + + olmSessionCache map[id.SenderKey]map[id.SessionID]*OlmSession + olmSessionCacheLock sync.Mutex } var _ Store = (*SQLCryptoStore)(nil) @@ -45,6 +48,8 @@ func NewSQLCryptoStore(db *sql.DB, dialect string, accountID string, deviceID id PickleKey: pickleKey, AccountID: accountID, DeviceID: deviceID, + + olmSessionCache: make(map[id.SenderKey]map[id.SessionID]*OlmSession), } } @@ -125,7 +130,12 @@ func (store *SQLCryptoStore) GetAccount() (*OlmAccount, error) { // HasSession returns whether there is an Olm session for the given sender key. func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool { - // TODO this may need to be changed if olm sessions start expiring + store.olmSessionCacheLock.Lock() + cache, ok := store.olmSessionCache[key] + store.olmSessionCacheLock.Unlock() + if ok && len(cache) > 0 { + return true + } var sessionID id.SessionID err := store.DB.QueryRow("SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 LIMIT 1", key, store.AccountID).Scan(&sessionID) @@ -137,53 +147,88 @@ func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool { // GetSessions returns all the known Olm sessions for a sender key. func (store *SQLCryptoStore) GetSessions(key id.SenderKey) (OlmSessionList, error) { - rows, err := store.DB.Query("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id", + rows, err := store.DB.Query("SELECT session_id, session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id", key, store.AccountID) if err != nil { return nil, err } list := OlmSessionList{} + store.olmSessionCacheLock.Lock() + defer store.olmSessionCacheLock.Unlock() + cache := store.getOlmSessionCache(key) for rows.Next() { - sess := OlmSession{Internal: *olm.NewBlankSession()} + sess := OlmSession{Internal: *olm.NewBlankSession(), lock: &sync.Mutex{}} var sessionBytes []byte - err := rows.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime) + var sessionID id.SessionID + err := rows.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.UseTime) if err != nil { return nil, err + } else if existing, ok := cache[sessionID]; ok { + list = append(list, existing) + } else { + err = sess.Internal.Unpickle(sessionBytes, store.PickleKey) + if err != nil { + return nil, err + } + list = append(list, &sess) + cache[sess.ID()] = &sess } - err = sess.Internal.Unpickle(sessionBytes, store.PickleKey) - if err != nil { - return nil, err - } - list = append(list, &sess) } return list, nil } +func (store *SQLCryptoStore) getOlmSessionCache(key id.SenderKey) map[id.SessionID]*OlmSession { + data, ok := store.olmSessionCache[key] + if !ok { + data = make(map[id.SessionID]*OlmSession) + store.olmSessionCache[key] = data + } + return data +} + // GetLatestSession retrieves the Olm session for a given sender key from the database that has the largest ID. func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*OlmSession, error) { - row := store.DB.QueryRow("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id DESC LIMIT 1", + store.olmSessionCacheLock.Lock() + defer store.olmSessionCacheLock.Unlock() + + row := store.DB.QueryRow("SELECT session_id, session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id DESC LIMIT 1", key, store.AccountID) - sess := OlmSession{Internal: *olm.NewBlankSession()} + + sess := OlmSession{Internal: *olm.NewBlankSession(), lock: &sync.Mutex{}} var sessionBytes []byte - err := row.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime) + var sessionID id.SessionID + + err := row.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.UseTime) if err == sql.ErrNoRows { return nil, nil } else if err != nil { return nil, err } - return &sess, sess.Internal.Unpickle(sessionBytes, store.PickleKey) + + cache := store.getOlmSessionCache(key) + if oldSess, ok := cache[sessionID]; ok { + return oldSess, nil + } else if err = sess.Internal.Unpickle(sessionBytes, store.PickleKey); err != nil { + return nil, err + } else { + cache[sessionID] = &sess + return &sess, nil + } } // AddSession persists an Olm session for a sender in the database. func (store *SQLCryptoStore) AddSession(key id.SenderKey, session *OlmSession) error { + store.olmSessionCacheLock.Lock() + defer store.olmSessionCacheLock.Unlock() sessionBytes := session.Internal.Pickle(store.PickleKey) _, err := store.DB.Exec("INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_used, account_id) VALUES ($1, $2, $3, $4, $5, $6)", session.ID(), key, sessionBytes, session.CreationTime, session.UseTime, store.AccountID) + store.getOlmSessionCache(key)[session.ID()] = session return err } // UpdateSession replaces the Olm session for a sender in the database. -func (store *SQLCryptoStore) UpdateSession(key id.SenderKey, session *OlmSession) error { +func (store *SQLCryptoStore) UpdateSession(_ id.SenderKey, session *OlmSession) error { sessionBytes := session.Internal.Pickle(store.PickleKey) _, err := store.DB.Exec("UPDATE crypto_olm_session SET session=$1, last_used=$2 WHERE session_id=$3 AND account_id=$4", sessionBytes, session.UseTime, session.ID(), store.AccountID) @@ -224,7 +269,7 @@ func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.Send } else if err != nil { return nil, err } else if withheldCode.Valid { - return nil, ErrGroupSessionWithheld + return nil, fmt.Errorf("%w (%s)", ErrGroupSessionWithheld, withheldCode.String) } igs := olm.NewBlankInboundGroupSession() err = igs.Unpickle(sessionBytes, store.PickleKey) @@ -492,18 +537,18 @@ func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceI err = fmt.Errorf("unsupported dialect %s", store.Dialect) } if err != nil { - return errors.Wrap(err, "failed to add user to tracked users list") + return fmt.Errorf("failed to add user to tracked users list: %w", err) } _, err = tx.Exec("DELETE FROM crypto_device WHERE user_id=$1", userID) if err != nil { _ = tx.Rollback() - return errors.Wrap(err, "failed to delete old devices") + return fmt.Errorf("failed to delete old devices: %w", err) } if len(devices) == 0 { err = tx.Commit() if err != nil { - return errors.Wrap(err, "failed to commit changes (no devices added)") + return fmt.Errorf("failed to commit changes (no devices added): %w", err) } return nil } @@ -533,12 +578,12 @@ func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceI _, err = tx.Exec("INSERT INTO crypto_device (user_id, device_id, identity_key, signing_key, trust, deleted, name) VALUES "+valueString, values...) if err != nil { _ = tx.Rollback() - return errors.Wrap(err, "failed to insert new devices") + return fmt.Errorf("failed to insert new devices: %w", err) } } err = tx.Commit() if err != nil { - return errors.Wrap(err, "failed to commit changes") + return fmt.Errorf("failed to commit changes: %w", err) } return nil } diff --git a/crypto/sql_store_upgrade/upgrade.go b/crypto/sql_store_upgrade/upgrade.go index a4bd2cdd..001f7039 100644 --- a/crypto/sql_store_upgrade/upgrade.go +++ b/crypto/sql_store_upgrade/upgrade.go @@ -2,14 +2,15 @@ package sql_store_upgrade import ( "database/sql" + "errors" "fmt" "strings" - - "github.com/pkg/errors" ) type upgradeFunc func(*sql.Tx, string) error +var ErrUnknownDialect = errors.New("unknown dialect") + var Upgrades = [...]upgradeFunc{ func(tx *sql.Tx, _ string) error { for _, query := range []string{ @@ -153,7 +154,7 @@ var Upgrades = [...]upgradeFunc{ } } } else { - return errors.New("unknown dialect: " + dialect) + return fmt.Errorf("%w (%s)", ErrUnknownDialect, dialect) } return nil }, @@ -203,7 +204,7 @@ var Upgrades = [...]upgradeFunc{ return err } } else { - return errors.New("unknown dialect: " + dialect) + return fmt.Errorf("%w (%s)", ErrUnknownDialect, dialect) } return nil }, diff --git a/crypto/store.go b/crypto/store.go index af49ac35..cca7c4da 100644 --- a/crypto/store.go +++ b/crypto/store.go @@ -9,6 +9,7 @@ package crypto import ( "encoding/gob" "errors" + "fmt" "os" "sort" "sync" @@ -65,6 +66,7 @@ var ErrGroupSessionWithheld = errors.New("group session has been withheld") // General implementation details: // * Get methods should not return errors if the requested data does not exist in the store, they should simply return nil. // * Update methods may assume that the pointer is the same as what has earlier been added to or fetched from the store. +// * OlmSessions should be cached so that the mutex works. Alternatively, implementations can use OlmSession.SetLock to provide a custom mutex implementation. type Store interface { // Flush ensures that everything in the store is persisted to disk. // This doesn't have to do anything, e.g. for database-backed implementations that persist everything immediately. @@ -311,10 +313,10 @@ func (gs *GobStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, se gs.lock.Lock() session, ok := gs.getGroupSessions(roomID, senderKey)[sessionID] if !ok { - _, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID] + withheld, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID] gs.lock.Unlock() if ok { - return nil, ErrGroupSessionWithheld + return nil, fmt.Errorf("%w (%s)", ErrGroupSessionWithheld, withheld.Code) } return nil, nil } diff --git a/crypto/verification.go b/crypto/verification.go index 415936df..c2fe26ec 100644 --- a/crypto/verification.go +++ b/crypto/verification.go @@ -11,6 +11,7 @@ package crypto import ( "context" "encoding/json" + "errors" "fmt" "math/rand" "sort" @@ -19,8 +20,6 @@ import ( "sync" "time" - "github.com/pkg/errors" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/canonicaljson" "maunium.net/go/mautrix/crypto/olm" @@ -28,11 +27,14 @@ import ( "maunium.net/go/mautrix/id" ) -// ErrUnknownTransaction is returned when a key verification message is received with an unknown transaction ID. -var ErrUnknownTransaction = errors.New("Unknown transaction") - -// ErrUnknownVerificationMethod is returned when the verification method in a received m.key.verification.start is unknown. -var ErrUnknownVerificationMethod = errors.New("Unknown verification method") +var ( + ErrUnknownUserForTransaction = errors.New("unknown user for transaction") + ErrTransactionAlreadyExists = errors.New("transaction already exists") + // ErrUnknownTransaction is returned when a key verification message is received with an unknown transaction ID. + ErrUnknownTransaction = errors.New("unknown transaction") + // ErrUnknownVerificationMethod is returned when the verification method in a received m.key.verification.start is unknown. + ErrUnknownVerificationMethod = errors.New("unknown verification method") +) type VerificationHooks interface { // VerifySASMatch receives the generated SAS and its method, as well as the device that is being verified. @@ -133,7 +135,7 @@ func (mach *OlmMachine) getTransactionState(transactionID string, userID id.User _ = mach.SendInRoomSASVerificationCancel(verState.inRoomID, userID, transactionID, reason, event.VerificationCancelUserMismatch) } mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) - return nil, errors.New(reason) + return nil, fmt.Errorf("%w %s: %s", ErrUnknownUserForTransaction, transactionID, userID) } return verState, nil } @@ -648,7 +650,7 @@ func (mach *OlmMachine) NewSASVerificationWith(device *DeviceIdentity, hooks Ver verState.startEventCanonical = string(canonical) _, loaded := mach.keyVerificationTransactionState.LoadOrStore(device.UserID.String()+":"+transactionID, verState) if loaded { - return "", errors.New("Transaction already exists") + return "", ErrTransactionAlreadyExists } mach.timeoutAfter(verState, transactionID, timeout) diff --git a/error.go b/error.go index bd3e790c..52234d02 100644 --- a/error.go +++ b/error.go @@ -69,7 +69,7 @@ type HTTPError struct { } func (e HTTPError) Is(err error) bool { - return errors.Is(e.RespError, err) || errors.Is(e.WrappedError, err) + return (e.RespError != nil && errors.Is(e.RespError, err)) || (e.WrappedError != nil && errors.Is(e.WrappedError, err)) } func (e HTTPError) IsStatus(code int) bool { diff --git a/event/encryption.go b/event/encryption.go index 58539906..4c9bdac3 100644 --- a/event/encryption.go +++ b/event/encryption.go @@ -9,8 +9,6 @@ package event import ( "encoding/json" - "github.com/pkg/errors" - "maunium.net/go/mautrix/id" ) @@ -58,7 +56,7 @@ func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error { return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext) case id.AlgorithmMegolmV1: if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' { - return errors.New("input doesn't look like a JSON string") + return id.InputNotJSONString } content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1] } diff --git a/event/type.go b/event/type.go index 8b466fd6..ae144650 100644 --- a/event/type.go +++ b/event/type.go @@ -32,8 +32,10 @@ func (tc TypeClass) Name() string { } const ( + // Unknown events + UnknownEventType TypeClass = iota // Normal message events - MessageEventType TypeClass = iota + MessageEventType // State events StateEventType // Ephemeral events @@ -42,8 +44,6 @@ const ( AccountDataEventType // Device-to-device events ToDeviceEventType - // Unknown events - UnknownEventType ) type Type struct { diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 00000000..ff9be9bd --- /dev/null +++ b/example/go.mod @@ -0,0 +1,5 @@ +module mautrix-example + +go 1.15 + +require maunium.net/go/mautrix v0.7.6 diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 00000000..4df7e47b --- /dev/null +++ b/example/go.sum @@ -0,0 +1,33 @@ +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +maunium.net/go/mautrix v0.7.6 h1:jB9oCimPq0mVyolwQBC/9N1fu21AU+Ryq837cLf4gOo= +maunium.net/go/mautrix v0.7.6/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= diff --git a/example/main.go b/example/main.go new file mode 100644 index 00000000..fa7cc3ee --- /dev/null +++ b/example/main.go @@ -0,0 +1,64 @@ +// Copyright (C) 2017 Tulir Asokan +// Copyright (C) 2018-2020 Luca Weiss +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package main + +import ( + "flag" + "fmt" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "os" +) + +var homeserver = flag.String("homeserver", "", "Matrix homeserver") +var username = flag.String("username", "", "Matrix username localpart") +var password = flag.String("password", "", "Matrix password") + +func main() { + flag.Parse() + if *username == "" || *password == "" || *homeserver == "" { + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + os.Exit(1) + } + + fmt.Println("Logging into", *homeserver, "as", *username) + client, err := mautrix.NewClient(*homeserver, "", "") + if err != nil { + panic(err) + } + _, err = client.Login(&mautrix.ReqLogin{ + Type: "m.login.password", + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: *username}, + Password: *password, + StoreCredentials: true, + }) + if err != nil { + panic(err) + } + fmt.Println("Login successful") + + syncer := client.Syncer.(*mautrix.DefaultSyncer) + syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, evt *event.Event) { + fmt.Printf("<%[1]s> %[4]s (%[2]s/%[3]s)\n", evt.Sender, evt.Type.String(), evt.ID, evt.Content.AsMessage().Body) + }) + + err = client.Sync() + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index 59532be0..d234f1c0 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,13 @@ require ( github.com/gorilla/mux v1.7.4 github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v1.14.0 - github.com/pkg/errors v0.9.1 github.com/russross/blackfriday/v2 v2.0.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/stretchr/testify v1.6.1 github.com/tidwall/gjson v1.6.0 github.com/tidwall/sjson v1.1.1 - golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d - golang.org/x/net v0.0.0-20200602114024-627f9648deb9 + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 + golang.org/x/net v0.0.0-20201026091529-146b70c837a4 gopkg.in/yaml.v2 v2.3.0 maunium.net/go/maulogger/v2 v2.1.1 ) diff --git a/go.sum b/go.sum index 823caaaf..a5756c82 100644 --- a/go.sum +++ b/go.sum @@ -12,68 +12,55 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= -github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs= +golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= diff --git a/requests.go b/requests.go index b3043f6e..77b0d65d 100644 --- a/requests.go +++ b/requests.go @@ -19,6 +19,8 @@ const ( AuthTypeMSISDN = "m.login.msisdn" AuthTypeToken = "m.login.token" AuthTypeDummy = "m.login.dummy" + + AuthTypeAppservice = "uk.half-shot.msc2778.login.application_service" ) type IdentifierType string diff --git a/responses.go b/responses.go index d5daeabb..ee11f56f 100644 --- a/responses.go +++ b/responses.go @@ -119,10 +119,19 @@ type RespRegister struct { type RespLoginFlows struct { Flows []struct { - Type string `json:"type"` + Type AuthType `json:"type"` } `json:"flows"` } +func (rlf *RespLoginFlows) HasFlow(flowType AuthType) bool { + for _, flow := range rlf.Flows { + if flow.Type == flowType { + return true + } + } + return false +} + // RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login type RespLogin struct { AccessToken string `json:"access_token"` diff --git a/version.go b/version.go index 43c72fa5..5859cde9 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package mautrix -const Version = "v0.7.6" +const Version = "v0.7.13"