updated deps

This commit is contained in:
Aine
2023-06-16 16:29:28 +03:00
parent 4c11919a46
commit f9d05d94c9
64 changed files with 12175 additions and 5221 deletions

View File

@@ -1,3 +1,29 @@
## v0.15.3 (2023-06-16)
* *(synapseadmin)* Added wrappers for some Synapse admin API endpoints.
* *(pushrules)* Implemented new `event_property_is` and `event_property_contains`
push rule condition kinds as per MSC3758 and MSC3966.
* *(bridge)* Moved websocket code from mautrix-imessage to enable all bridges
to use appservice websockets easily.
* *(bridge)* Added retrying for appservice pings.
* *(types)* Removed unstable field for MSC3952 (intentional mentions).
* *(client)* Deprecated `OldEventIgnorer` and added `Client.DontProcessOldEvents`
to replace it.
* *(client)* Added `MoveInviteState` sync handler for moving state events in
the invite section of sync inside the invite event itself.
* *(crypto)* Added option to not rotate keys when devices change.
* *(crypto)* Added additional duplicate message index check if decryption fails
because the keys had been ratcheted forward.
* *(client)* Stabilized support for asynchronous uploads.
* `UnstableCreateMXC` and `UnstableUploadAsync` were renamed to `CreateMXC`
and `UploadAsync` respectively.
* *(util/dbutil)* Added option to use a separate database connection pool for
read-only transactions.
* This is mostly meant for SQLite and it enables read-only transactions that
don't lock the database, even when normal transactions are configured to
acquire a write lock immediately.
* *(util/dbutil)* Enabled caller info in zerolog by default.
## v0.15.2 (2023-05-16)
* *(client)* Changed member-fetching methods to clear existing member info in

View File

@@ -403,7 +403,7 @@ func (cli *Client) MakeFullRequest(params FullRequest) ([]byte, error) {
return nil, err
}
if params.Handler == nil {
params.Handler = cli.handleNormalResponse
params.Handler = handleNormalResponse
}
req.Header.Set("User-Agent", cli.UserAgent)
if len(cli.AccessToken) > 0 {
@@ -441,7 +441,7 @@ func (cli *Client) doRetry(req *http.Request, cause error, retries int, backoff
return cli.executeCompiledRequest(req, retries-1, backoff*2, responseJSON, handler)
}
func (cli *Client) readRequestBody(req *http.Request, res *http.Response) ([]byte, error) {
func readRequestBody(req *http.Request, res *http.Response) ([]byte, error) {
contents, err := io.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{
@@ -463,12 +463,12 @@ func closeTemp(log *zerolog.Logger, file *os.File) {
}
}
func (cli *Client) streamResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
func streamResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
log := zerolog.Ctx(req.Context())
file, err := os.CreateTemp("", "mautrix-response-")
if err != nil {
log.Warn().Err(err).Msg("Failed to create temporary file for streaming response")
_, err = cli.handleNormalResponse(req, res, responseJSON)
_, err = handleNormalResponse(req, res, responseJSON)
return nil, err
}
defer closeTemp(log, file)
@@ -483,8 +483,8 @@ func (cli *Client) streamResponse(req *http.Request, res *http.Response, respons
}
}
func (cli *Client) handleNormalResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
if contents, err := cli.readRequestBody(req, res); err != nil {
func handleNormalResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
if contents, err := readRequestBody(req, res); err != nil {
return nil, err
} else if responseJSON == nil {
return contents, nil
@@ -502,8 +502,8 @@ func (cli *Client) handleNormalResponse(req *http.Request, res *http.Response, r
}
}
func (cli *Client) handleResponseError(req *http.Request, res *http.Response) ([]byte, error) {
contents, err := cli.readRequestBody(req, res)
func ParseErrorResponse(req *http.Request, res *http.Response) ([]byte, error) {
contents, err := readRequestBody(req, res)
if err != nil {
return contents, err
}
@@ -522,7 +522,7 @@ func (cli *Client) handleResponseError(req *http.Request, res *http.Response) ([
// parseBackoffFromResponse extracts the backoff time specified in the Retry-After header if present. See
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After.
func (cli *Client) parseBackoffFromResponse(req *http.Request, res *http.Response, now time.Time, fallback time.Duration) time.Duration {
func parseBackoffFromResponse(req *http.Request, res *http.Response, now time.Time, fallback time.Duration) time.Duration {
retryAfterHeaderValue := res.Header.Get("Retry-After")
if retryAfterHeaderValue == "" {
return fallback
@@ -573,14 +573,14 @@ func (cli *Client) executeCompiledRequest(req *http.Request, retries int, backof
if retries > 0 && cli.shouldRetry(res) {
if res.StatusCode == http.StatusTooManyRequests {
backoff = cli.parseBackoffFromResponse(req, res, time.Now(), backoff)
backoff = parseBackoffFromResponse(req, res, time.Now(), backoff)
}
return cli.doRetry(req, fmt.Errorf("HTTP %d", res.StatusCode), retries, backoff, responseJSON, handler)
}
var body []byte
if res.StatusCode < 200 || res.StatusCode >= 300 {
body, err = cli.handleResponseError(req, res)
body, err = ParseErrorResponse(req, res)
cli.LogRequestDone(req, res, nil, len(body), duration)
} else {
body, err = handler(req, res, responseJSON)
@@ -657,7 +657,7 @@ func (cli *Client) FullSyncRequest(req ReqSync) (resp *RespSync, err error) {
MaxAttempts: 1,
}
if req.StreamResponse {
fullReq.Handler = cli.streamResponse
fullReq.Handler = streamResponse
}
start := time.Now()
_, err = cli.MakeFullRequest(fullReq)
@@ -1413,10 +1413,11 @@ func (cli *Client) DownloadBytesContext(ctx context.Context, mxcURL id.ContentUR
return io.ReadAll(resp.Body)
}
// UnstableCreateMXC creates a blank Matrix content URI to allow uploading the content asynchronously later.
// See https://github.com/matrix-org/matrix-spec-proposals/pull/2246
func (cli *Client) UnstableCreateMXC() (*RespCreateMXC, error) {
u, _ := url.Parse(cli.BuildURL(MediaURLPath{"unstable", "fi.mau.msc2246", "create"}))
// CreateMXC creates a blank Matrix content URI to allow uploading the content asynchronously later.
//
// See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
func (cli *Client) CreateMXC() (*RespCreateMXC, error) {
u, _ := url.Parse(cli.BuildURL(MediaURLPath{"v1", "create"}))
var m RespCreateMXC
_, err := cli.MakeFullRequest(FullRequest{
Method: http.MethodPost,
@@ -1426,19 +1427,22 @@ func (cli *Client) UnstableCreateMXC() (*RespCreateMXC, error) {
return &m, err
}
// UnstableUploadAsync creates a blank content URI with UnstableCreateMXC, starts uploading the data in the background
// and returns the created MXC immediately. See https://github.com/matrix-org/matrix-spec-proposals/pull/2246 for more info.
func (cli *Client) UnstableUploadAsync(req ReqUploadMedia) (*RespCreateMXC, error) {
resp, err := cli.UnstableCreateMXC()
// UploadAsync creates a blank content URI with CreateMXC, starts uploading the data in the background
// and returns the created MXC immediately.
//
// See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
// and https://spec.matrix.org/v1.7/client-server-api/#put_matrixmediav3uploadservernamemediaid
func (cli *Client) UploadAsync(req ReqUploadMedia) (*RespCreateMXC, error) {
resp, err := cli.CreateMXC()
if err != nil {
return nil, err
}
req.UnstableMXC = resp.ContentURI
req.UploadURL = resp.UploadURL
req.MXC = resp.ContentURI
req.UnstableUploadURL = resp.UnstableUploadURL
go func() {
_, err = cli.UploadMedia(req)
if err != nil {
cli.Log.Error().Str("mxc", req.UnstableMXC.String()).Err(err).Msg("Async upload of media failed")
cli.Log.Error().Str("mxc", req.MXC.String()).Err(err).Msg("Async upload of media failed")
}
}()
return resp, nil
@@ -1474,13 +1478,13 @@ type ReqUploadMedia struct {
ContentType string
FileName string
// UnstableMXC specifies an existing MXC URI which doesn't have content yet to upload into.
// See https://github.com/matrix-org/matrix-spec-proposals/pull/2246 for more info.
UnstableMXC id.ContentURI
// MXC specifies an existing MXC URI which doesn't have content yet to upload into.
// See https://spec.matrix.org/unstable/client-server-api/#put_matrixmediav3uploadservernamemediaid
MXC id.ContentURI
// UploadURL specifies the URL to upload the content to (MSC3870)
// UnstableUploadURL specifies the URL to upload the content to. MXC must also be set.
// see https://github.com/matrix-org/matrix-spec-proposals/pull/3870 for more info
UploadURL string
UnstableUploadURL string
}
func (cli *Client) tryUploadMediaToURL(url, contentType string, content io.Reader) (*http.Response, error) {
@@ -1508,7 +1512,7 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro
} else {
data.Content = nil
}
resp, err := cli.tryUploadMediaToURL(data.UploadURL, data.ContentType, reader)
resp, err := cli.tryUploadMediaToURL(data.UnstableUploadURL, data.ContentType, reader)
if err == nil {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
// Everything is fine
@@ -1517,10 +1521,12 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro
err = fmt.Errorf("HTTP %d", resp.StatusCode)
}
if retries <= 0 {
cli.Log.Warn().Str("url", data.UploadURL).Err(err).Msg("Error uploading media to external URL, not retrying")
cli.Log.Warn().Str("url", data.UnstableUploadURL).Err(err).
Msg("Error uploading media to external URL, not retrying")
return nil, err
}
cli.Log.Warn().Str("url", data.UploadURL).Err(err).Msg("Error uploading media to external URL, retrying")
cli.Log.Warn().Str("url", data.UnstableUploadURL).Err(err).
Msg("Error uploading media to external URL, retrying")
retries--
}
@@ -1529,7 +1535,7 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro
query["filename"] = data.FileName
}
notifyURL := cli.BuildURLWithQuery(MediaURLPath{"unstable", "fi.mau.msc2246", "upload", data.UnstableMXC.Homeserver, data.UnstableMXC.FileID, "complete"}, query)
notifyURL := cli.BuildURLWithQuery(MediaURLPath{"unstable", "com.beeper.msc3870", "upload", data.MXC.Homeserver, data.MXC.FileID, "complete"}, query)
var m *RespMediaUpload
_, err := cli.MakeFullRequest(FullRequest{
@@ -1545,15 +1551,18 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro
}
// UploadMedia uploads the given data to the content repository and returns an MXC URI.
// See https://spec.matrix.org/v1.2/client-server-api/#post_matrixmediav3upload
// See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav3upload
func (cli *Client) UploadMedia(data ReqUploadMedia) (*RespMediaUpload, error) {
if data.UploadURL != "" {
if data.UnstableUploadURL != "" {
if data.MXC.IsEmpty() {
return nil, errors.New("MXC must also be set when uploading to external URL")
}
return cli.uploadMediaToURL(data)
}
u, _ := url.Parse(cli.BuildURL(MediaURLPath{"v3", "upload"}))
method := http.MethodPost
if !data.UnstableMXC.IsEmpty() {
u, _ = url.Parse(cli.BuildURL(MediaURLPath{"unstable", "fi.mau.msc2246", "upload", data.UnstableMXC.Homeserver, data.UnstableMXC.FileID}))
if !data.MXC.IsEmpty() {
u, _ = url.Parse(cli.BuildURL(MediaURLPath{"v3", "upload", data.MXC.Homeserver, data.MXC.FileID}))
method = http.MethodPut
}
if len(data.FileName) > 0 {

View File

@@ -189,7 +189,7 @@ func (helper *CryptoHelper) Init() error {
func (helper *CryptoHelper) Close() error {
if helper != nil && helper.dbForManagedStores != nil {
err := helper.dbForManagedStores.RawDB.Close()
err := helper.dbForManagedStores.Close()
if err != nil {
return err
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -163,6 +164,26 @@ func removeItem(slice []uint, item uint) ([]uint, bool) {
const missedIndexCutoff = 10
func (mach *OlmMachine) checkUndecryptableMessageIndexDuplication(ctx context.Context, sess *InboundGroupSession, evt *event.Event, content *event.EncryptedEventContent) (uint, error) {
log := *zerolog.Ctx(ctx)
messageIndex, decodeErr := parseMessageIndex(content.MegolmCiphertext)
if decodeErr != nil {
log.Warn().Err(decodeErr).Msg("Failed to parse message index to check if it's a duplicate for message that failed to decrypt")
return 0, fmt.Errorf("%w (also failed to parse message index)", olm.UnknownMessageIndex)
}
firstKnown := sess.Internal.FirstKnownIndex()
log = log.With().Uint("message_index", messageIndex).Uint32("first_known_index", firstKnown).Logger()
if ok, err := mach.CryptoStore.ValidateMessageIndex(ctx, sess.SenderKey, content.SessionID, evt.ID, messageIndex, evt.Timestamp); err != nil {
log.Debug().Err(err).Msg("Failed to check if message index is duplicate")
return messageIndex, fmt.Errorf("%w (failed to check if index is duplicate; received: %d, earliest known: %d)", olm.UnknownMessageIndex, messageIndex, firstKnown)
} else if !ok {
log.Debug().Msg("Failed to decrypt message due to unknown index and found duplicate")
return messageIndex, fmt.Errorf("%w %d (also failed to decrypt because earliest known index is %d)", DuplicateMessageIndex, messageIndex, firstKnown)
}
log.Debug().Msg("Failed to decrypt message due to unknown index, but index is not duplicate")
return messageIndex, fmt.Errorf("%w (not duplicate index; received: %d, earliest known: %d)", olm.UnknownMessageIndex, messageIndex, firstKnown)
}
func (mach *OlmMachine) actuallyDecryptMegolmEvent(ctx context.Context, evt *event.Event, encryptionRoomID id.RoomID, content *event.EncryptedEventContent) (*InboundGroupSession, []byte, uint, error) {
mach.megolmDecryptLock.Lock()
defer mach.megolmDecryptLock.Unlock()
@@ -177,11 +198,15 @@ func (mach *OlmMachine) actuallyDecryptMegolmEvent(ctx context.Context, evt *eve
}
plaintext, messageIndex, err := sess.Internal.Decrypt(content.MegolmCiphertext)
if err != nil {
if errors.Is(err, olm.UnknownMessageIndex) && mach.RatchetKeysOnDecrypt {
messageIndex, err = mach.checkUndecryptableMessageIndexDuplication(ctx, sess, evt, content)
return sess, nil, messageIndex, fmt.Errorf("failed to decrypt megolm event: %w", err)
}
return sess, nil, 0, fmt.Errorf("failed to decrypt megolm event: %w", err)
} else if ok, err := mach.CryptoStore.ValidateMessageIndex(ctx, sess.SenderKey, content.SessionID, evt.ID, messageIndex, evt.Timestamp); err != nil {
return sess, nil, messageIndex, fmt.Errorf("failed to check if message index is duplicate: %w", err)
} else if !ok {
return sess, nil, messageIndex, DuplicateMessageIndex
return sess, nil, messageIndex, fmt.Errorf("%w %d", DuplicateMessageIndex, messageIndex)
}
expectedMessageIndex := sess.RatchetSafety.NextIndex

View File

@@ -198,6 +198,9 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT
// This is called automatically whenever a device list change is noticed in ProcessSyncResponse and usually does
// not need to be called manually.
func (mach *OlmMachine) OnDevicesChanged(userID id.UserID) {
if mach.DisableDeviceChangeKeyRotation {
return
}
for _, roomID := range mach.StateStore.FindSharedRooms(userID) {
mach.Log.Debug().
Str("user_id", userID.String()).

View File

@@ -61,7 +61,7 @@ func IsShareError(err error) bool {
return err == SessionExpired || err == SessionNotShared || err == NoGroupSession
}
func parseMessageIndex(ciphertext []byte) (uint64, error) {
func parseMessageIndex(ciphertext []byte) (uint, error) {
decoded := make([]byte, base64.RawStdEncoding.DecodedLen(len(ciphertext)))
var err error
_, err = base64.RawStdEncoding.Decode(decoded, ciphertext)
@@ -74,7 +74,7 @@ func parseMessageIndex(ciphertext []byte) (uint64, error) {
if read <= 0 {
return 0, fmt.Errorf("failed to decode varint, read value %d", read)
}
return index, nil
return uint(index), nil
}
// EncryptMegolmEvent encrypts data with the m.megolm.v1.aes-sha2 algorithm.
@@ -102,6 +102,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID
Str("event_type", evtType.Type).
Str("room_id", roomID.String()).
Str("session_id", session.ID().String()).
Uint("expected_index", session.Internal.MessageIndex()).
Logger()
log.Trace().Msg("Encrypting event...")
ciphertext, err := session.Encrypt(plaintext)
@@ -112,7 +113,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID
if err != nil {
log.Warn().Err(err).Msg("Failed to get megolm message index of encrypted event")
} else {
log = log.With().Uint64("message_index", idx).Logger()
log = log.With().Uint("message_index", idx).Logger()
}
log.Debug().Msg("Encrypted event successfully")
err = mach.CryptoStore.UpdateOutboundGroupSession(session)

View File

@@ -73,6 +73,8 @@ type OlmMachine struct {
RatchetKeysOnDecrypt bool
DeleteFullyUsedKeysOnDecrypt bool
DeleteKeysOnDeviceDelete bool
DisableDeviceChangeKeyRotation bool
}
// StateStore is used by OlmMachine to get room state information that's needed for encryption.

View File

@@ -230,12 +230,14 @@ func (s *InboundGroupSession) Decrypt(message []byte) ([]byte, uint, error) {
if err != nil {
return nil, 0, err
}
messageCopy := make([]byte, len(message))
copy(messageCopy, message)
plaintext := make([]byte, decryptMaxPlaintextLen)
var messageIndex uint32
r := C.olm_group_decrypt(
(*C.OlmInboundGroupSession)(s.int),
(*C.uint8_t)(&message[0]),
C.size_t(len(message)),
(*C.uint8_t)(&messageCopy[0]),
C.size_t(len(messageCopy)),
(*C.uint8_t)(&plaintext[0]),
C.size_t(len(plaintext)),
(*C.uint32_t)(&messageIndex))

View File

@@ -326,12 +326,13 @@ func (s *Session) Decrypt(message string, msgType id.OlmMsgType) ([]byte, error)
if err != nil {
return nil, err
}
messageCopy := []byte(message)
plaintext := make([]byte, decryptMaxPlaintextLen)
r := C.olm_decrypt(
(*C.OlmSession)(s.int),
C.size_t(msgType),
unsafe.Pointer(&([]byte(message))[0]),
C.size_t(len(message)),
unsafe.Pointer(&(messageCopy)[0]),
C.size_t(len(messageCopy)),
unsafe.Pointer(&plaintext[0]),
C.size_t(len(plaintext)))
if r == errorVal() {

View File

@@ -95,9 +95,9 @@ func (e HTTPError) Error() string {
return fmt.Sprintf("failed to %s %s: %s (HTTP %d): %s", e.Request.Method, e.Request.URL.Path,
e.RespError.ErrCode, e.Response.StatusCode, e.RespError.Err)
} else {
msg := fmt.Sprintf("failed to %s %s: %s", e.Request.Method, e.Request.URL.Path, e.Response.Status)
msg := fmt.Sprintf("failed to %s %s: HTTP %d", e.Request.Method, e.Request.URL.Path, e.Response.StatusCode)
if len(e.ResponseBody) > 0 {
msg = fmt.Sprintf("%s\n%s", msg, e.ResponseBody)
msg = fmt.Sprintf("%s: %s", msg, e.ResponseBody)
}
return msg
}

View File

@@ -97,8 +97,7 @@ type MessageEventContent struct {
FileName string `json:"filename,omitempty"`
Mentions *Mentions `json:"m.mentions,omitempty"`
UnstableMentions *Mentions `json:"org.matrix.msc3952.mentions,omitempty"`
Mentions *Mentions `json:"m.mentions,omitempty"`
// Edits and relations
NewContent *MessageEventContent `json:"m.new_content,omitempty"`

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2022 Tulir Asokan
// 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
@@ -38,9 +38,11 @@ type PushCondKind string
// The allowed push condition kinds as specified in https://spec.matrix.org/v1.2/client-server-api/#conditions-1
const (
KindEventMatch PushCondKind = "event_match"
KindContainsDisplayName PushCondKind = "contains_display_name"
KindRoomMemberCount PushCondKind = "room_member_count"
KindEventMatch PushCondKind = "event_match"
KindContainsDisplayName PushCondKind = "contains_display_name"
KindRoomMemberCount PushCondKind = "room_member_count"
KindEventPropertyIs PushCondKind = "event_property_is"
KindEventPropertyContains PushCondKind = "event_property_contains"
// MSC3664: https://github.com/matrix-org/matrix-spec-proposals/pull/3664
@@ -56,6 +58,8 @@ type PushCondition struct {
Key string `json:"key,omitempty"`
// The glob-style pattern to match the field against. Only applicable if kind is EventMatch.
Pattern string `json:"pattern,omitempty"`
// The exact value to match the field against. Only applicable if kind is EventPropertyIs or EventPropertyContains.
Value any `json:"value,omitempty"`
// The condition that needs to be fulfilled for RoomMemberCount-type conditions.
// A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found.
MemberCountCondition string `json:"is,omitempty"`
@@ -70,8 +74,8 @@ var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$")
// Match checks if this condition is fulfilled for the given event in the given room.
func (cond *PushCondition) Match(room Room, evt *event.Event) bool {
switch cond.Kind {
case KindEventMatch:
return cond.matchValue(room, evt)
case KindEventMatch, KindEventPropertyIs, KindEventPropertyContains:
return cond.matchValue(evt)
case KindRelatedEventMatch, KindUnstableRelatedEventMatch:
return cond.matchRelatedEvent(room, evt)
case KindContainsDisplayName:
@@ -101,13 +105,13 @@ func splitWithEscaping(s string, separator, escape byte) []string {
return tokens
}
func hackyNestedGet(data map[string]interface{}, path []string) (interface{}, bool) {
func hackyNestedGet(data map[string]any, path []string) (any, bool) {
val, ok := data[path[0]]
if len(path) == 1 {
// We don't have any more path parts, return the value regardless of whether it exists or not.
return val, ok
} else if ok {
if mapVal, ok := val.(map[string]interface{}); ok {
if mapVal, ok := val.(map[string]any); ok {
val, ok = hackyNestedGet(mapVal, path[1:])
if ok {
return val, true
@@ -138,37 +142,103 @@ func stringifyForPushCondition(val interface{}) string {
}
}
func (cond *PushCondition) matchValue(room Room, evt *event.Event) bool {
func (cond *PushCondition) getValue(evt *event.Event) (any, bool) {
key, subkey, _ := strings.Cut(cond.Key, ".")
pattern, err := glob.Compile(cond.Pattern)
if err != nil {
return false
}
switch key {
case "type":
return pattern.MatchString(evt.Type.String())
return evt.Type.Type, true
case "sender":
return pattern.MatchString(string(evt.Sender))
return evt.Sender.String(), true
case "room_id":
return pattern.MatchString(string(evt.RoomID))
return evt.RoomID.String(), true
case "state_key":
if evt.StateKey == nil {
return false
return nil, false
}
return pattern.MatchString(*evt.StateKey)
return *evt.StateKey, true
case "content":
// Split the match key with escaping to implement https://github.com/matrix-org/matrix-spec-proposals/pull/3873
splitKey := splitWithEscaping(subkey, '.', '\\')
// Then do a hacky nested get that supports combining parts for the backwards-compat part of MSC3873
val, ok := hackyNestedGet(evt.Content.Raw, splitKey)
if !ok {
return cond.Pattern == ""
return hackyNestedGet(evt.Content.Raw, splitKey)
default:
return nil, false
}
}
func numberToInt64(a any) int64 {
switch typed := a.(type) {
case float64:
return int64(typed)
case float32:
return int64(typed)
case int:
return int64(typed)
case int8:
return int64(typed)
case int16:
return int64(typed)
case int32:
return int64(typed)
case int64:
return typed
case uint:
return int64(typed)
case uint8:
return int64(typed)
case uint16:
return int64(typed)
case uint32:
return int64(typed)
case uint64:
return int64(typed)
default:
return 0
}
}
func valueEquals(a, b any) bool {
// Convert floats to ints when comparing numbers (the JSON parser generates floats, but Matrix only allows integers)
// Also allow other numeric types in case something generates events manually without json
switch a.(type) {
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
switch b.(type) {
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return numberToInt64(a) == numberToInt64(b)
}
}
return a == b
}
func (cond *PushCondition) matchValue(evt *event.Event) bool {
val, ok := cond.getValue(evt)
if !ok {
return false
}
switch cond.Kind {
case KindEventMatch, KindRelatedEventMatch, KindUnstableRelatedEventMatch:
pattern, err := glob.Compile(cond.Pattern)
if err != nil {
return false
}
return pattern.MatchString(stringifyForPushCondition(val))
default:
case KindEventPropertyIs:
return valueEquals(val, cond.Value)
case KindEventPropertyContains:
valArr, ok := val.([]any)
if !ok {
return false
}
for _, item := range valArr {
if valueEquals(item, cond.Value) {
return true
}
}
return false
default:
panic(fmt.Errorf("matchValue called for unknown condition kind %s", cond.Kind))
}
}
@@ -209,7 +279,7 @@ func (cond *PushCondition) matchRelatedEvent(room Room, evt *event.Event) bool {
} else if evt = eventfulRoom.GetEvent(relatesTo.EventID); evt == nil {
return false
} else {
return cond.matchValue(room, evt)
return cond.matchValue(evt)
}
}

View File

@@ -134,6 +134,12 @@ func (rule *PushRule) Match(room Room, evt *event.Event) bool {
if rule == nil || !rule.Enabled {
return false
}
if rule.RuleID == ".m.rule.contains_display_name" || rule.RuleID == ".m.rule.contains_user_name" || rule.RuleID == ".m.rule.roomnotif" {
if _, containsMentions := evt.Content.Raw["m.mentions"]; containsMentions {
// Disable legacy mention push rules when the event contains the new mentions key
return false
}
}
switch rule.Type {
case OverrideRule, UnderrideRule:
return rule.matchConditions(room, evt)

View File

@@ -21,6 +21,8 @@ const (
AuthTypeToken AuthType = "m.login.token"
AuthTypeDummy AuthType = "m.login.dummy"
AuthTypeAppservice AuthType = "m.login.application_service"
AuthTypeSynapseJWT AuthType = "org.matrix.login.jwt"
)
type IdentifierType string

View File

@@ -108,11 +108,12 @@ type RespMediaUpload struct {
ContentURI id.ContentURI `json:"content_uri"`
}
// RespCreateMXC is the JSON response for /_matrix/media/v3/create as specified in https://github.com/matrix-org/matrix-spec-proposals/pull/2246
// RespCreateMXC is the JSON response for https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
type RespCreateMXC struct {
ContentURI id.ContentURI `json:"content_uri"`
UnusedExpiresAt int `json:"unused_expires_at,omitempty"`
UploadURL string `json:"upload_url,omitempty"`
UnstableUploadURL string `json:"com.beeper.msc3870.upload_url,omitempty"`
}
// RespPreviewURL is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url

View File

@@ -62,9 +62,9 @@ func (es EventSource) String() string {
case EventSourceLeave:
typeName = "left room " + typeName
default:
return fmt.Sprintf("unknown (%d)", es)
return fmt.Sprintf("unknown (%s+%d)", typeName, es)
}
es &^= roomableTypes
es &^= roomSections
}
if es&encryptableTypes != 0 && es&EventSourceDecrypted != 0 {
typeName += " (decrypted)"
@@ -72,7 +72,7 @@ func (es EventSource) String() string {
}
es &^= primaryTypes
if es != 0 {
return fmt.Sprintf("unknown (%d)", es)
return fmt.Sprintf("unknown (%s+%d)", typeName, es)
}
return typeName
}
@@ -263,11 +263,9 @@ func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) *Filter {
return s.FilterJSON
}
// OldEventIgnorer is an utility struct for bots to ignore events from before the bot joined the room.
// OldEventIgnorer is a utility struct for bots to ignore events from before the bot joined the room.
//
// Create a struct and call Register with your DefaultSyncer to register the sync handler, e.g.:
//
// (&OldEventIgnorer{UserID: cli.UserID}).Register(cli.Syncer.(mautrix.ExtensibleSyncer))
// Deprecated: Use Client.DontProcessOldEvents instead.
type OldEventIgnorer struct {
UserID id.UserID
}
@@ -276,9 +274,21 @@ func (oei *OldEventIgnorer) Register(syncer ExtensibleSyncer) {
syncer.OnSync(oei.DontProcessOldEvents)
}
// DontProcessOldEvents returns true if a sync response should be processed. May modify the response to remove
// stuff that shouldn't be processed.
func (oei *OldEventIgnorer) DontProcessOldEvents(resp *RespSync, since string) bool {
return dontProcessOldEvents(oei.UserID, resp, since)
}
// DontProcessOldEvents is a sync handler that removes rooms that the user just joined.
// It's meant for bots to ignore events from before the bot joined the room.
//
// To use it, register it with your Syncer, e.g.:
//
// cli.Syncer.(mautrix.ExtensibleSyncer).OnSync(cli.DontProcessOldEvents)
func (cli *Client) DontProcessOldEvents(resp *RespSync, since string) bool {
return dontProcessOldEvents(cli.UserID, resp, since)
}
func dontProcessOldEvents(userID id.UserID, resp *RespSync, since string) bool {
if since == "" {
return false
}
@@ -292,7 +302,7 @@ func (oei *OldEventIgnorer) DontProcessOldEvents(resp *RespSync, since string) b
for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
evt := roomData.Timeline.Events[i]
if evt.Type == event.StateMember && evt.GetStateKey() == string(oei.UserID) {
if evt.Type == event.StateMember && evt.GetStateKey() == string(userID) {
membership, _ := evt.Content.Raw["membership"].(string)
if membership == "join" {
_, ok := resp.Rooms.Join[roomID]
@@ -308,3 +318,34 @@ func (oei *OldEventIgnorer) DontProcessOldEvents(resp *RespSync, since string) b
}
return true
}
// MoveInviteState is a sync handler that moves events from the state event list to the InviteRoomState in the invite event.
//
// To use it, register it with your Syncer, e.g.:
//
// cli.Syncer.(mautrix.ExtensibleSyncer).OnSync(cli.MoveInviteState)
func (cli *Client) MoveInviteState(resp *RespSync, _ string) bool {
for _, meta := range resp.Rooms.Invite {
var inviteState []event.StrippedState
var inviteEvt *event.Event
for _, evt := range meta.State.Events {
if evt.Type == event.StateMember && evt.GetStateKey() == cli.UserID.String() {
inviteEvt = evt
} else {
evt.Type.Class = event.StateEventType
_ = evt.Content.ParseRaw(evt.Type)
inviteState = append(inviteState, event.StrippedState{
Content: evt.Content,
Type: evt.Type,
StateKey: evt.GetStateKey(),
Sender: evt.Sender,
})
}
}
if inviteEvt != nil {
inviteEvt.Unsigned.InviteRoomState = inviteState
meta.State.Events = []*event.Event{inviteEvt}
}
}
return true
}

28
vendor/maunium.net/go/mautrix/url.go generated vendored
View File

@@ -31,7 +31,7 @@ func ParseAndNormalizeBaseURL(homeserverURL string) (*url.URL, error) {
}
// BuildURL builds a URL with the given path parts
func BuildURL(baseURL *url.URL, path ...interface{}) *url.URL {
func BuildURL(baseURL *url.URL, path ...any) *url.URL {
createdURL := *baseURL
rawParts := make([]string, len(path)+1)
rawParts[0] = strings.TrimSuffix(createdURL.RawPath, "/")
@@ -62,30 +62,36 @@ func (cli *Client) BuildURL(urlPath PrefixableURLPath) string {
// BuildClientURL builds a URL with the Client's homeserver and appservice user ID set already.
// This method also automatically prepends the client API prefix (/_matrix/client).
func (cli *Client) BuildClientURL(urlPath ...interface{}) string {
func (cli *Client) BuildClientURL(urlPath ...any) string {
return cli.BuildURLWithQuery(ClientURLPath(urlPath), nil)
}
type PrefixableURLPath interface {
FullPath() []interface{}
FullPath() []any
}
type BaseURLPath []interface{}
type BaseURLPath []any
func (bup BaseURLPath) FullPath() []interface{} {
func (bup BaseURLPath) FullPath() []any {
return bup
}
type ClientURLPath []interface{}
type ClientURLPath []any
func (cup ClientURLPath) FullPath() []interface{} {
return append([]interface{}{"_matrix", "client"}, []interface{}(cup)...)
func (cup ClientURLPath) FullPath() []any {
return append([]any{"_matrix", "client"}, []any(cup)...)
}
type MediaURLPath []interface{}
type MediaURLPath []any
func (mup MediaURLPath) FullPath() []interface{} {
return append([]interface{}{"_matrix", "media"}, []interface{}(mup)...)
func (mup MediaURLPath) FullPath() []any {
return append([]any{"_matrix", "media"}, []any(mup)...)
}
type SynapseAdminURLPath []any
func (saup SynapseAdminURLPath) FullPath() []any {
return append([]any{"_synapse", "admin"}, []any(saup)...)
}
// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver

View File

@@ -71,8 +71,12 @@ type loggingDB struct {
}
func (ld *loggingDB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*LoggingTxn, error) {
targetDB := ld.db.RawDB
if opts != nil && opts.ReadOnly && ld.db.ReadOnlyDB != nil {
targetDB = ld.db.ReadOnlyDB
}
start := time.Now()
tx, err := ld.db.RawDB.BeginTx(ctx, opts)
tx, err := targetDB.BeginTx(ctx, opts)
ld.db.Log.QueryTiming(ctx, "Begin", "", nil, -1, time.Since(start), err)
if err != nil {
return nil, err
@@ -102,7 +106,10 @@ type LoggingTxn struct {
func (lt *LoggingTxn) Commit() error {
start := time.Now()
err := lt.UnderlyingTx.Commit()
lt.endLog()
lt.EndTime = time.Now()
if !lt.noTotalLog {
lt.db.Log.QueryTiming(lt.ctx, "<Transaction>", "", nil, -1, lt.EndTime.Sub(lt.StartTime), nil)
}
lt.db.Log.QueryTiming(lt.ctx, "Commit", "", nil, -1, time.Since(start), err)
return err
}
@@ -110,16 +117,12 @@ func (lt *LoggingTxn) Commit() error {
func (lt *LoggingTxn) Rollback() error {
start := time.Now()
err := lt.UnderlyingTx.Rollback()
lt.endLog()
lt.db.Log.QueryTiming(lt.ctx, "Rollback", "", nil, -1, time.Since(start), err)
return err
}
func (lt *LoggingTxn) endLog() {
lt.EndTime = time.Now()
if !lt.noTotalLog {
lt.db.Log.QueryTiming(lt.ctx, "<Transaction>", "", nil, -1, lt.EndTime.Sub(lt.StartTime), nil)
}
lt.db.Log.QueryTiming(lt.ctx, "Rollback", "", nil, -1, time.Since(start), err)
return err
}
type LoggingRows struct {

View File

@@ -10,6 +10,7 @@ import (
"context"
"database/sql"
"fmt"
"net/url"
"regexp"
"strings"
"time"
@@ -35,12 +36,13 @@ func (dialect Dialect) String() string {
}
func ParseDialect(engine string) (Dialect, error) {
switch strings.ToLower(engine) {
case "postgres", "postgresql", "pgx":
engine = strings.ToLower(engine)
if strings.HasPrefix(engine, "postgres") || engine == "pgx" {
return Postgres, nil
case "sqlite3", "sqlite", "litestream", "sqlite3-fk-wal":
} else if strings.HasPrefix(engine, "sqlite") || strings.HasPrefix(engine, "litestream") {
return SQLite, nil
default:
} else {
return DialectUnknown, fmt.Errorf("unknown dialect '%s'", engine)
}
}
@@ -109,6 +111,7 @@ var (
type Database struct {
loggingDB
RawDB *sql.DB
ReadOnlyDB *sql.DB
Owner string
VersionTable string
Log DatabaseLogger
@@ -171,10 +174,11 @@ func NewWithDialect(uri, rawDialect string) (*Database, error) {
if err != nil {
return nil, err
}
return NewWithDB(db, rawDialect)
}
type Config struct {
type PoolConfig struct {
Type string `yaml:"type"`
URI string `yaml:"uri"`
@@ -185,54 +189,101 @@ type Config struct {
ConnMaxLifetime string `yaml:"conn_max_lifetime"`
}
type Config struct {
PoolConfig `yaml:",inline"`
ReadOnlyPool PoolConfig `yaml:"ro_pool"`
}
func (db *Database) Close() error {
err := db.RawDB.Close()
if db.ReadOnlyDB != nil {
err2 := db.ReadOnlyDB.Close()
if err == nil {
err = fmt.Errorf("closing read-only db failed: %w", err)
} else {
err = fmt.Errorf("%w (closing read-only db also failed: %v)", err, err2)
}
}
return err
}
func (db *Database) Configure(cfg Config) error {
db.RawDB.SetMaxOpenConns(cfg.MaxOpenConns)
db.RawDB.SetMaxIdleConns(cfg.MaxIdleConns)
if err := db.configure(db.ReadOnlyDB, cfg.ReadOnlyPool); err != nil {
return err
}
return db.configure(db.RawDB, cfg.PoolConfig)
}
func (db *Database) configure(rawDB *sql.DB, cfg PoolConfig) error {
if rawDB == nil {
return nil
}
rawDB.SetMaxOpenConns(cfg.MaxOpenConns)
rawDB.SetMaxIdleConns(cfg.MaxIdleConns)
if len(cfg.ConnMaxIdleTime) > 0 {
maxIdleTimeDuration, err := time.ParseDuration(cfg.ConnMaxIdleTime)
if err != nil {
return fmt.Errorf("failed to parse max_conn_idle_time: %w", err)
}
db.RawDB.SetConnMaxIdleTime(maxIdleTimeDuration)
rawDB.SetConnMaxIdleTime(maxIdleTimeDuration)
}
if len(cfg.ConnMaxLifetime) > 0 {
maxLifetimeDuration, err := time.ParseDuration(cfg.ConnMaxLifetime)
if err != nil {
return fmt.Errorf("failed to parse max_conn_idle_time: %w", err)
}
db.RawDB.SetConnMaxLifetime(maxLifetimeDuration)
rawDB.SetConnMaxLifetime(maxLifetimeDuration)
}
return nil
}
func NewFromConfig(owner string, cfg Config, logger DatabaseLogger) (*Database, error) {
dialect, err := ParseDialect(cfg.Type)
wrappedDB, err := NewWithDialect(cfg.URI, cfg.Type)
if err != nil {
return nil, err
}
conn, err := sql.Open(cfg.Type, cfg.URI)
if err != nil {
return nil, err
}
if logger == nil {
logger = NoopLogger
}
wrappedDB := &Database{
RawDB: conn,
Owner: owner,
Dialect: dialect,
Log: logger,
IgnoreForeignTables: true,
VersionTable: "version",
wrappedDB.Owner = owner
if logger != nil {
wrappedDB.Log = logger
}
if cfg.ReadOnlyPool.MaxOpenConns > 0 {
if cfg.ReadOnlyPool.Type == "" {
cfg.ReadOnlyPool.Type = cfg.Type
}
roUri := cfg.ReadOnlyPool.URI
if roUri == "" {
uriParts := strings.Split(cfg.URI, "?")
var qs url.Values
if len(uriParts) == 2 {
var err error
qs, err = url.ParseQuery(uriParts[1])
if err != nil {
return nil, err
}
qs.Del("_txlock")
}
qs.Set("_query_only", "true")
roUri = uriParts[0] + "?" + qs.Encode()
}
wrappedDB.ReadOnlyDB, err = sql.Open(cfg.ReadOnlyPool.Type, roUri)
if err != nil {
return nil, err
}
}
err = wrappedDB.Configure(cfg)
if err != nil {
return nil, err
}
wrappedDB.loggingDB.UnderlyingExecable = conn
wrappedDB.loggingDB.db = wrappedDB
return wrappedDB, nil
}

View File

@@ -80,6 +80,11 @@ func ZeroLoggerPtr(log *zerolog.Logger, cfg ...ZeroLogSettings) DatabaseLogger {
wrapped := &zeroLogger{l: log}
if len(cfg) > 0 {
wrapped.ZeroLogSettings = cfg[0]
} else {
wrapped.ZeroLogSettings = ZeroLogSettings{
CallerSkipFrame: 2, // Skip LoggingExecable.ExecContext and zeroLogger.QueryTiming
Caller: true,
}
}
return wrapped
}

View File

@@ -80,3 +80,14 @@ func (db *Database) DoTxn(ctx context.Context, opts *sql.TxOptions, fn func(ctx
log.Trace().Msg("Commit successful")
return nil
}
func (db *Database) Conn(ctx context.Context) ContextExecable {
if ctx == nil {
return db
}
txn, ok := ctx.Value(ContextKeyDatabaseTransaction).(Transaction)
if ok {
return txn
}
return db
}

54
vendor/maunium.net/go/mautrix/util/formatduration.go generated vendored Normal file
View File

@@ -0,0 +1,54 @@
// 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 util
import (
"errors"
"fmt"
"strings"
"time"
)
var Day = 24 * time.Hour
var Week = 7 * Day
func pluralize(value int, unit string) string {
if value == 1 {
return "1 " + unit
}
return fmt.Sprintf("%d %ss", value, unit)
}
func appendDurationPart(time, unit time.Duration, name string, parts *[]string) (remainder time.Duration) {
if time < unit {
return time
}
value := int(time / unit)
remainder = time % unit
*parts = append(*parts, pluralize(value, name))
return
}
func FormatDuration(d time.Duration) string {
if d < 0 {
panic(errors.New("FormatDuration: negative duration"))
} else if d < time.Second {
return "now"
}
parts := make([]string, 0, 2)
d = appendDurationPart(d, Week, "week", &parts)
d = appendDurationPart(d, Day, "day", &parts)
d = appendDurationPart(d, time.Hour, "hour", &parts)
d = appendDurationPart(d, time.Minute, "minute", &parts)
d = appendDurationPart(d, time.Second, "second", &parts)
if len(parts) > 2 {
parts[0] = strings.Join(parts[:len(parts)-1], ", ")
parts[1] = parts[len(parts)-1]
parts = parts[:2]
}
return strings.Join(parts, " and ")
}

View File

@@ -7,7 +7,7 @@ import (
"strings"
)
const Version = "v0.15.2"
const Version = "v0.15.3"
var GoModVersion = ""
var Commit = ""