mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
client,event,bridgev2: add support for Beeper's custom ephemeral events and AI stream events (#457)
This commit is contained in:
parent
77f0658365
commit
fef4326fbc
17 changed files with 418 additions and 8 deletions
|
|
@ -222,6 +222,17 @@ func (intent *IntentAPI) SendMessageEvent(ctx context.Context, roomID id.RoomID,
|
|||
return intent.Client.SendMessageEvent(ctx, roomID, eventType, contentJSON, extra...)
|
||||
}
|
||||
|
||||
func (intent *IntentAPI) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON any, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) {
|
||||
if err := intent.EnsureJoined(ctx, roomID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !intent.SpecVersions.Supports(mautrix.BeeperFeatureEphemeralEvents) {
|
||||
return nil, mautrix.MUnrecognized.WithMessage("Homeserver does not advertise com.beeper.ephemeral support")
|
||||
}
|
||||
contentJSON = intent.AddDoublePuppetValue(contentJSON)
|
||||
return intent.Client.BeeperSendEphemeralEvent(ctx, roomID, eventType, contentJSON, extra...)
|
||||
}
|
||||
|
||||
// Deprecated: use SendMessageEvent with mautrix.ReqSendEvent.Timestamp instead
|
||||
func (intent *IntentAPI) SendMassagedMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) {
|
||||
return intent.SendMessageEvent(ctx, roomID, eventType, contentJSON, mautrix.ReqSendEvent{Timestamp: ts})
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ var (
|
|||
ErrMediaConvertFailed error = WrapErrorInStatus(errors.New("failed to convert media")).WithMessage("failed to convert media").WithIsCertain(true).WithSendNotice(true)
|
||||
ErrMembershipNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group membership")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
|
||||
ErrDeleteChatNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support deleting chats")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
|
||||
ErrBeeperAIStreamNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support Beeper AI stream events")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
|
||||
ErrPowerLevelsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group power levels")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
|
||||
ErrRemoteEchoTimeout = WrapErrorInStatus(errors.New("remote echo timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld)
|
||||
ErrRemoteAckTimeout = WrapErrorInStatus(errors.New("remote ack timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld)
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
|
|||
br.EventProcessor.On(event.EventReaction, br.handleRoomEvent)
|
||||
br.EventProcessor.On(event.EventRedaction, br.handleRoomEvent)
|
||||
br.EventProcessor.On(event.EventEncrypted, br.handleEncryptedEvent)
|
||||
br.EventProcessor.On(event.EphemeralEventEncrypted, br.handleEncryptedEvent)
|
||||
br.EventProcessor.On(event.StateMember, br.handleRoomEvent)
|
||||
br.EventProcessor.On(event.StatePowerLevels, br.handleRoomEvent)
|
||||
br.EventProcessor.On(event.StateRoomName, br.handleRoomEvent)
|
||||
|
|
@ -156,6 +157,7 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
|
|||
br.EventProcessor.On(event.BeeperAcceptMessageRequest, br.handleRoomEvent)
|
||||
br.EventProcessor.On(event.EphemeralEventReceipt, br.handleEphemeralEvent)
|
||||
br.EventProcessor.On(event.EphemeralEventTyping, br.handleEphemeralEvent)
|
||||
br.EventProcessor.On(event.BeeperEphemeralEventAIStream, br.handleEphemeralEvent)
|
||||
br.Bot = br.AS.BotIntent()
|
||||
br.Crypto = NewCryptoHelper(br)
|
||||
br.Bridge.Commands.(*commands.Processor).AddHandlers(
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ type ASIntent struct {
|
|||
|
||||
var _ bridgev2.MatrixAPI = (*ASIntent)(nil)
|
||||
var _ bridgev2.MarkAsDMMatrixAPI = (*ASIntent)(nil)
|
||||
var _ bridgev2.EphemeralSendingMatrixAPI = (*ASIntent)(nil)
|
||||
|
||||
func (as *ASIntent) SendMessage(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, extra *bridgev2.MatrixSendExtra) (*mautrix.RespSendEvent, error) {
|
||||
if extra == nil {
|
||||
|
|
@ -84,6 +85,21 @@ func (as *ASIntent) SendMessage(ctx context.Context, roomID id.RoomID, eventType
|
|||
return as.Matrix.SendMessageEvent(ctx, roomID, eventType, content, mautrix.ReqSendEvent{Timestamp: extra.Timestamp.UnixMilli()})
|
||||
}
|
||||
|
||||
func (as *ASIntent) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, txnID string) (*mautrix.RespSendEvent, error) {
|
||||
if !as.Connector.SpecVersions.Supports(mautrix.BeeperFeatureEphemeralEvents) {
|
||||
return nil, mautrix.MUnrecognized.WithMessage("Homeserver does not advertise com.beeper.ephemeral support")
|
||||
}
|
||||
if encrypted, err := as.Matrix.StateStore.IsEncrypted(ctx, roomID); err != nil {
|
||||
return nil, fmt.Errorf("failed to check if room is encrypted: %w", err)
|
||||
} else if encrypted && as.Connector.Crypto != nil {
|
||||
if err = as.Connector.Crypto.Encrypt(ctx, roomID, eventType, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eventType = event.EventEncrypted
|
||||
}
|
||||
return as.Matrix.BeeperSendEphemeralEvent(ctx, roomID, eventType, content, mautrix.ReqSendEvent{TransactionID: txnID})
|
||||
}
|
||||
|
||||
func (as *ASIntent) fillMemberEvent(ctx context.Context, roomID id.RoomID, userID id.UserID, content *event.Content) {
|
||||
targetContent, ok := content.Parsed.(*event.MemberEventContent)
|
||||
if !ok || targetContent.Displayname != "" || targetContent.AvatarURL != "" {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ func (br *Connector) handleEphemeralEvent(ctx context.Context, evt *event.Event)
|
|||
case event.EphemeralEventTyping:
|
||||
typingContent := evt.Content.AsTyping()
|
||||
typingContent.UserIDs = slices.DeleteFunc(typingContent.UserIDs, br.shouldIgnoreEventFromUser)
|
||||
case event.BeeperEphemeralEventAIStream:
|
||||
if br.shouldIgnoreEvent(evt) {
|
||||
return
|
||||
}
|
||||
}
|
||||
br.Bridge.QueueMatrixEvent(ctx, evt)
|
||||
}
|
||||
|
|
@ -231,7 +235,6 @@ func (br *Connector) postDecrypt(ctx context.Context, original, decrypted *event
|
|||
go br.sendSuccessCheckpoint(ctx, decrypted, status.MsgStepDecrypted, retryCount)
|
||||
decrypted.Mautrix.CheckpointSent = true
|
||||
decrypted.Mautrix.DecryptionDuration = duration
|
||||
decrypted.Mautrix.EventSource |= event.SourceDecrypted
|
||||
br.EventProcessor.Dispatch(ctx, decrypted)
|
||||
if errorEventID != nil && *errorEventID != "" {
|
||||
_, _ = br.Bot.RedactEvent(ctx, decrypted.RoomID, *errorEventID)
|
||||
|
|
|
|||
|
|
@ -217,3 +217,8 @@ type MarkAsDMMatrixAPI interface {
|
|||
MatrixAPI
|
||||
MarkAsDM(ctx context.Context, roomID id.RoomID, otherUser id.UserID) error
|
||||
}
|
||||
|
||||
type EphemeralSendingMatrixAPI interface {
|
||||
MatrixAPI
|
||||
BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, txnID string) (*mautrix.RespSendEvent, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -726,6 +726,11 @@ type MessageRequestAcceptingNetworkAPI interface {
|
|||
HandleMatrixAcceptMessageRequest(ctx context.Context, msg *MatrixAcceptMessageRequest) error
|
||||
}
|
||||
|
||||
type BeeperAIStreamHandlingNetworkAPI interface {
|
||||
NetworkAPI
|
||||
HandleMatrixBeeperAIStream(ctx context.Context, msg *MatrixBeeperAIStream) error
|
||||
}
|
||||
|
||||
type ResolveIdentifierResponse struct {
|
||||
// Ghost is the ghost of the user that the identifier resolves to.
|
||||
// This field should be set whenever possible. However, it is not required,
|
||||
|
|
@ -1439,6 +1444,7 @@ type MatrixViewingChat struct {
|
|||
|
||||
type MatrixDeleteChat = MatrixEventBase[*event.BeeperChatDeleteEventContent]
|
||||
type MatrixAcceptMessageRequest = MatrixEventBase[*event.BeeperAcceptMessageRequestEventContent]
|
||||
type MatrixBeeperAIStream = MatrixEventBase[*event.BeeperAIStreamEventContent]
|
||||
type MatrixMarkedUnread = MatrixRoomMeta[*event.MarkedUnreadEventContent]
|
||||
type MatrixMute = MatrixRoomMeta[*event.BeeperMuteEventContent]
|
||||
type MatrixRoomTag = MatrixRoomMeta[*event.TagEventContent]
|
||||
|
|
|
|||
|
|
@ -697,6 +697,8 @@ func (portal *Portal) handleMatrixEvent(ctx context.Context, sender *User, evt *
|
|||
return portal.handleMatrixReceipts(ctx, evt)
|
||||
case event.EphemeralEventTyping:
|
||||
return portal.handleMatrixTyping(ctx, evt)
|
||||
case event.BeeperEphemeralEventAIStream:
|
||||
return portal.handleMatrixAIStream(ctx, sender, evt)
|
||||
default:
|
||||
return EventHandlingResultIgnored
|
||||
}
|
||||
|
|
@ -941,6 +943,50 @@ func (portal *Portal) handleMatrixTyping(ctx context.Context, evt *event.Event)
|
|||
return EventHandlingResultSuccess
|
||||
}
|
||||
|
||||
func (portal *Portal) handleMatrixAIStream(ctx context.Context, sender *User, evt *event.Event) EventHandlingResult {
|
||||
log := zerolog.Ctx(ctx)
|
||||
if sender == nil {
|
||||
log.Error().Msg("Missing sender for Matrix AI stream event")
|
||||
return EventHandlingResultIgnored
|
||||
}
|
||||
login, _, err := portal.FindPreferredLogin(ctx, sender, true)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get user login to handle Matrix AI stream event")
|
||||
return EventHandlingResultFailed.WithMSSError(err)
|
||||
}
|
||||
var origSender *OrigSender
|
||||
if login == nil {
|
||||
if portal.Relay == nil {
|
||||
return EventHandlingResultIgnored
|
||||
}
|
||||
login = portal.Relay
|
||||
origSender = &OrigSender{
|
||||
User: sender,
|
||||
UserID: sender.MXID,
|
||||
}
|
||||
}
|
||||
content, ok := evt.Content.Parsed.(*event.BeeperAIStreamEventContent)
|
||||
if !ok {
|
||||
log.Error().Type("content_type", evt.Content.Parsed).Msg("Unexpected parsed content type")
|
||||
return EventHandlingResultFailed.WithMSSError(fmt.Errorf("%w: %T", ErrUnexpectedParsedContentType, evt.Content.Parsed))
|
||||
}
|
||||
api, ok := login.Client.(BeeperAIStreamHandlingNetworkAPI)
|
||||
if !ok {
|
||||
return EventHandlingResultIgnored.WithMSSError(ErrBeeperAIStreamNotSupported)
|
||||
}
|
||||
err = api.HandleMatrixBeeperAIStream(ctx, &MatrixBeeperAIStream{
|
||||
Event: evt,
|
||||
Content: content,
|
||||
Portal: portal,
|
||||
OrigSender: origSender,
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to handle Matrix AI stream event")
|
||||
return EventHandlingResultFailed.WithMSSError(err)
|
||||
}
|
||||
return EventHandlingResultSuccess.WithMSS()
|
||||
}
|
||||
|
||||
func (portal *Portal) sendTypings(ctx context.Context, userIDs []id.UserID, typing bool) {
|
||||
for _, userID := range userIDs {
|
||||
login, ok := portal.currentlyTypingLogins[userID]
|
||||
|
|
|
|||
42
client.go
42
client.go
|
|
@ -1359,6 +1359,48 @@ func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, event
|
|||
return
|
||||
}
|
||||
|
||||
// BeeperSendEphemeralEvent sends an ephemeral event into a room using Beeper's unstable endpoint.
|
||||
// contentJSON should be a value that can be encoded as JSON using json.Marshal.
|
||||
func (cli *Client) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON any, extra ...ReqSendEvent) (resp *RespSendEvent, err error) {
|
||||
var req ReqSendEvent
|
||||
if len(extra) > 0 {
|
||||
req = extra[0]
|
||||
}
|
||||
|
||||
var txnID string
|
||||
if len(req.TransactionID) > 0 {
|
||||
txnID = req.TransactionID
|
||||
} else {
|
||||
txnID = cli.TxnID()
|
||||
}
|
||||
|
||||
queryParams := map[string]string{}
|
||||
if req.Timestamp > 0 {
|
||||
queryParams["ts"] = strconv.FormatInt(req.Timestamp, 10)
|
||||
}
|
||||
|
||||
if !req.DontEncrypt && cli != nil && cli.Crypto != nil && eventType != event.EventEncrypted {
|
||||
var isEncrypted bool
|
||||
isEncrypted, err = cli.StateStore.IsEncrypted(ctx, roomID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to check if room is encrypted: %w", err)
|
||||
return
|
||||
}
|
||||
if isEncrypted {
|
||||
if contentJSON, err = cli.Crypto.Encrypt(ctx, roomID, eventType, contentJSON); err != nil {
|
||||
err = fmt.Errorf("failed to encrypt event: %w", err)
|
||||
return
|
||||
}
|
||||
eventType = event.EventEncrypted
|
||||
}
|
||||
}
|
||||
|
||||
urlData := ClientURLPath{"unstable", "com.beeper.ephemeral", "rooms", roomID, "ephemeral", eventType.String(), txnID}
|
||||
urlPath := cli.BuildURLWithQuery(urlData, queryParams)
|
||||
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
|
||||
return
|
||||
}
|
||||
|
||||
// SendStateEvent sends a state event into a room. See https://spec.matrix.org/v1.16/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
|
||||
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
||||
func (cli *Client) SendStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON any, extra ...ReqSendEvent) (resp *RespSendEvent, err error) {
|
||||
|
|
|
|||
158
client_ephemeral_test.go
Normal file
158
client_ephemeral_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright (c) 2026 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 mautrix_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func TestClient_SendEphemeralEvent_UsesUnstablePathTxnAndTS(t *testing.T) {
|
||||
roomID := id.RoomID("!room:example.com")
|
||||
evtType := event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType}
|
||||
txnID := "txn-123"
|
||||
|
||||
var gotPath string
|
||||
var gotQueryTS string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotQueryTS = r.URL.Query().Get("ts")
|
||||
assert.Equal(t, http.MethodPut, r.Method)
|
||||
_, _ = w.Write([]byte(`{"event_id":"$evt"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cli, err := mautrix.NewClient(ts.URL, "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cli.BeeperSendEphemeralEvent(
|
||||
context.Background(),
|
||||
roomID,
|
||||
evtType,
|
||||
map[string]any{"foo": "bar"},
|
||||
mautrix.ReqSendEvent{TransactionID: txnID, Timestamp: 1234},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, strings.Contains(gotPath, "/_matrix/client/unstable/com.beeper.ephemeral/rooms/"))
|
||||
assert.True(t, strings.HasSuffix(gotPath, "/ephemeral/com.example.ephemeral/"+txnID))
|
||||
assert.Equal(t, "1234", gotQueryTS)
|
||||
}
|
||||
|
||||
func TestClient_SendEphemeralEvent_UnsupportedReturnsMUnrecognized(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"errcode":"M_UNRECOGNIZED","error":"Unrecognized endpoint"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cli, err := mautrix.NewClient(ts.URL, "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cli.BeeperSendEphemeralEvent(
|
||||
context.Background(),
|
||||
id.RoomID("!room:example.com"),
|
||||
event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType},
|
||||
map[string]any{"foo": "bar"},
|
||||
)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, mautrix.MUnrecognized))
|
||||
}
|
||||
|
||||
func TestClient_SendEphemeralEvent_EncryptsInEncryptedRooms(t *testing.T) {
|
||||
roomID := id.RoomID("!room:example.com")
|
||||
evtType := event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType}
|
||||
txnID := "txn-encrypted"
|
||||
|
||||
stateStore := mautrix.NewMemoryStateStore()
|
||||
err := stateStore.SetEncryptionEvent(context.Background(), roomID, &event.EncryptionEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
fakeCrypto := &fakeCryptoHelper{
|
||||
encryptedContent: &event.EncryptedEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
MegolmCiphertext: []byte("ciphertext"),
|
||||
},
|
||||
}
|
||||
|
||||
var gotPath string
|
||||
var gotBody map[string]any
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
assert.Equal(t, http.MethodPut, r.Method)
|
||||
err := json.NewDecoder(r.Body).Decode(&gotBody)
|
||||
require.NoError(t, err)
|
||||
_, _ = w.Write([]byte(`{"event_id":"$evt"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cli, err := mautrix.NewClient(ts.URL, "", "")
|
||||
require.NoError(t, err)
|
||||
cli.StateStore = stateStore
|
||||
cli.Crypto = fakeCrypto
|
||||
|
||||
_, err = cli.BeeperSendEphemeralEvent(
|
||||
context.Background(),
|
||||
roomID,
|
||||
evtType,
|
||||
map[string]any{"foo": "bar"},
|
||||
mautrix.ReqSendEvent{TransactionID: txnID},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, strings.HasSuffix(gotPath, "/ephemeral/m.room.encrypted/"+txnID))
|
||||
assert.Equal(t, string(id.AlgorithmMegolmV1), gotBody["algorithm"])
|
||||
assert.Equal(t, 1, fakeCrypto.encryptCalls)
|
||||
assert.Equal(t, roomID, fakeCrypto.lastRoomID)
|
||||
assert.Equal(t, evtType, fakeCrypto.lastEventType)
|
||||
}
|
||||
|
||||
type fakeCryptoHelper struct {
|
||||
encryptCalls int
|
||||
lastRoomID id.RoomID
|
||||
lastEventType event.Type
|
||||
lastEncryptInput any
|
||||
encryptedContent *event.EncryptedEventContent
|
||||
}
|
||||
|
||||
func (f *fakeCryptoHelper) Encrypt(_ context.Context, roomID id.RoomID, eventType event.Type, content any) (*event.EncryptedEventContent, error) {
|
||||
f.encryptCalls++
|
||||
f.lastRoomID = roomID
|
||||
f.lastEventType = eventType
|
||||
f.lastEncryptInput = content
|
||||
return f.encryptedContent, nil
|
||||
}
|
||||
|
||||
func (f *fakeCryptoHelper) Decrypt(context.Context, *event.Event) (*event.Event, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeCryptoHelper) WaitForSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *fakeCryptoHelper) RequestSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) {
|
||||
}
|
||||
|
||||
func (f *fakeCryptoHelper) Init(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -213,6 +213,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event
|
|||
TrustSource: device,
|
||||
ForwardedKeys: forwardedKeys,
|
||||
WasEncrypted: true,
|
||||
EventSource: evt.Mautrix.EventSource | event.SourceDecrypted,
|
||||
ReceivedAt: evt.Mautrix.ReceivedAt,
|
||||
},
|
||||
}, nil
|
||||
|
|
|
|||
|
|
@ -214,6 +214,15 @@ func (content *MessageEventContent) RemovePerMessageProfileFallback() {
|
|||
}
|
||||
}
|
||||
|
||||
type BeeperAIStreamEventContent struct {
|
||||
TurnID string `json:"turn_id"`
|
||||
Seq int `json:"seq"`
|
||||
Part map[string]any `json:"part"`
|
||||
TargetEvent id.EventID `json:"target_event,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
|
||||
}
|
||||
|
||||
type BeeperEncodedOrder struct {
|
||||
order int64
|
||||
suborder int16
|
||||
|
|
|
|||
|
|
@ -76,9 +76,11 @@ var TypeMap = map[Type]reflect.Type{
|
|||
AccountDataMarkedUnread: reflect.TypeOf(MarkedUnreadEventContent{}),
|
||||
AccountDataBeeperMute: reflect.TypeOf(BeeperMuteEventContent{}),
|
||||
|
||||
EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}),
|
||||
EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}),
|
||||
EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}),
|
||||
EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}),
|
||||
EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}),
|
||||
EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}),
|
||||
EphemeralEventEncrypted: reflect.TypeOf(EncryptedEventContent{}),
|
||||
BeeperEphemeralEventAIStream: reflect.TypeOf(BeeperAIStreamEventContent{}),
|
||||
|
||||
InRoomVerificationReady: reflect.TypeOf(VerificationReadyEventContent{}),
|
||||
InRoomVerificationStart: reflect.TypeOf(VerificationStartEventContent{}),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ type PowerLevelsEventContent struct {
|
|||
Events map[string]int `json:"events,omitempty"`
|
||||
EventsDefault int `json:"events_default,omitempty"`
|
||||
|
||||
beeperEphemeralLock sync.RWMutex
|
||||
BeeperEphemeral map[string]int `json:"com.beeper.ephemeral,omitempty"`
|
||||
|
||||
Notifications *NotificationPowerLevels `json:"notifications,omitempty"`
|
||||
|
||||
StateDefaultPtr *int `json:"state_default,omitempty"`
|
||||
|
|
@ -37,6 +40,8 @@ type PowerLevelsEventContent struct {
|
|||
BanPtr *int `json:"ban,omitempty"`
|
||||
RedactPtr *int `json:"redact,omitempty"`
|
||||
|
||||
BeeperEphemeralDefaultPtr *int `json:"com.beeper.ephemeral_default,omitempty"`
|
||||
|
||||
// This is not a part of power levels, it's added by mautrix-go internally in certain places
|
||||
// in order to detect creator power accurately.
|
||||
CreateEvent *Event `json:"-"`
|
||||
|
|
@ -51,6 +56,7 @@ func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent {
|
|||
UsersDefault: pl.UsersDefault,
|
||||
Events: maps.Clone(pl.Events),
|
||||
EventsDefault: pl.EventsDefault,
|
||||
BeeperEphemeral: maps.Clone(pl.BeeperEphemeral),
|
||||
StateDefaultPtr: ptr.Clone(pl.StateDefaultPtr),
|
||||
|
||||
Notifications: pl.Notifications.Clone(),
|
||||
|
|
@ -60,6 +66,8 @@ func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent {
|
|||
BanPtr: ptr.Clone(pl.BanPtr),
|
||||
RedactPtr: ptr.Clone(pl.RedactPtr),
|
||||
|
||||
BeeperEphemeralDefaultPtr: ptr.Clone(pl.BeeperEphemeralDefaultPtr),
|
||||
|
||||
CreateEvent: pl.CreateEvent,
|
||||
}
|
||||
}
|
||||
|
|
@ -119,6 +127,13 @@ func (pl *PowerLevelsEventContent) StateDefault() int {
|
|||
return 50
|
||||
}
|
||||
|
||||
func (pl *PowerLevelsEventContent) BeeperEphemeralDefault() int {
|
||||
if pl.BeeperEphemeralDefaultPtr != nil {
|
||||
return *pl.BeeperEphemeralDefaultPtr
|
||||
}
|
||||
return pl.EventsDefault
|
||||
}
|
||||
|
||||
func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int {
|
||||
if pl.isCreator(userID) {
|
||||
return math.MaxInt
|
||||
|
|
@ -202,6 +217,29 @@ func (pl *PowerLevelsEventContent) GetEventLevel(eventType Type) int {
|
|||
return level
|
||||
}
|
||||
|
||||
func (pl *PowerLevelsEventContent) GetBeeperEphemeralLevel(eventType Type) int {
|
||||
pl.beeperEphemeralLock.RLock()
|
||||
defer pl.beeperEphemeralLock.RUnlock()
|
||||
level, ok := pl.BeeperEphemeral[eventType.String()]
|
||||
if !ok {
|
||||
return pl.BeeperEphemeralDefault()
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func (pl *PowerLevelsEventContent) SetBeeperEphemeralLevel(eventType Type, level int) {
|
||||
pl.beeperEphemeralLock.Lock()
|
||||
defer pl.beeperEphemeralLock.Unlock()
|
||||
if level == pl.BeeperEphemeralDefault() {
|
||||
delete(pl.BeeperEphemeral, eventType.String())
|
||||
} else {
|
||||
if pl.BeeperEphemeral == nil {
|
||||
pl.BeeperEphemeral = make(map[string]int)
|
||||
}
|
||||
pl.BeeperEphemeral[eventType.String()] = level
|
||||
}
|
||||
}
|
||||
|
||||
func (pl *PowerLevelsEventContent) SetEventLevel(eventType Type, level int) {
|
||||
pl.eventsLock.Lock()
|
||||
defer pl.eventsLock.Unlock()
|
||||
|
|
|
|||
67
event/powerlevels_ephemeral_test.go
Normal file
67
event/powerlevels_ephemeral_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) 2026 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 event_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
func TestPowerLevelsEventContent_BeeperEphemeralDefaultFallsBackToEventsDefault(t *testing.T) {
|
||||
pl := &event.PowerLevelsEventContent{
|
||||
EventsDefault: 45,
|
||||
}
|
||||
|
||||
assert.Equal(t, 45, pl.BeeperEphemeralDefault())
|
||||
|
||||
override := 60
|
||||
pl.BeeperEphemeralDefaultPtr = &override
|
||||
assert.Equal(t, 60, pl.BeeperEphemeralDefault())
|
||||
}
|
||||
|
||||
func TestPowerLevelsEventContent_GetSetBeeperEphemeralLevel(t *testing.T) {
|
||||
pl := &event.PowerLevelsEventContent{
|
||||
EventsDefault: 25,
|
||||
}
|
||||
evtType := event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType}
|
||||
|
||||
assert.Equal(t, 25, pl.GetBeeperEphemeralLevel(evtType))
|
||||
|
||||
pl.SetBeeperEphemeralLevel(evtType, 50)
|
||||
assert.Equal(t, 50, pl.GetBeeperEphemeralLevel(evtType))
|
||||
require.NotNil(t, pl.BeeperEphemeral)
|
||||
assert.Equal(t, 50, pl.BeeperEphemeral[evtType.String()])
|
||||
|
||||
pl.SetBeeperEphemeralLevel(evtType, 25)
|
||||
_, exists := pl.BeeperEphemeral[evtType.String()]
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestPowerLevelsEventContent_CloneCopiesBeeperEphemeralFields(t *testing.T) {
|
||||
override := 70
|
||||
pl := &event.PowerLevelsEventContent{
|
||||
EventsDefault: 35,
|
||||
BeeperEphemeral: map[string]int{"com.example.ephemeral": 90},
|
||||
BeeperEphemeralDefaultPtr: &override,
|
||||
}
|
||||
|
||||
cloned := pl.Clone()
|
||||
require.NotNil(t, cloned)
|
||||
require.NotNil(t, cloned.BeeperEphemeralDefaultPtr)
|
||||
assert.Equal(t, 70, *cloned.BeeperEphemeralDefaultPtr)
|
||||
assert.Equal(t, 90, cloned.BeeperEphemeral["com.example.ephemeral"])
|
||||
|
||||
cloned.BeeperEphemeral["com.example.ephemeral"] = 99
|
||||
*cloned.BeeperEphemeralDefaultPtr = 71
|
||||
|
||||
assert.Equal(t, 90, pl.BeeperEphemeral["com.example.ephemeral"])
|
||||
assert.Equal(t, 70, *pl.BeeperEphemeralDefaultPtr)
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ func (et *Type) GuessClass() TypeClass {
|
|||
StateElementFunctionalMembers.Type, StateBeeperRoomFeatures.Type, StateBeeperDisappearingTimer.Type,
|
||||
StateMSC4391BotCommand.Type, StateRoomPolicy.Type, StateUnstableRoomPolicy.Type:
|
||||
return StateEventType
|
||||
case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type:
|
||||
case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type, BeeperEphemeralEventAIStream.Type:
|
||||
return EphemeralEventType
|
||||
case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type,
|
||||
AccountDataFullyRead.Type, AccountDataIgnoredUserList.Type, AccountDataMarkedUnread.Type,
|
||||
|
|
@ -250,9 +250,11 @@ var (
|
|||
|
||||
// Ephemeral events
|
||||
var (
|
||||
EphemeralEventReceipt = Type{"m.receipt", EphemeralEventType}
|
||||
EphemeralEventTyping = Type{"m.typing", EphemeralEventType}
|
||||
EphemeralEventPresence = Type{"m.presence", EphemeralEventType}
|
||||
EphemeralEventReceipt = Type{"m.receipt", EphemeralEventType}
|
||||
EphemeralEventTyping = Type{"m.typing", EphemeralEventType}
|
||||
EphemeralEventPresence = Type{"m.presence", EphemeralEventType}
|
||||
EphemeralEventEncrypted = Type{"m.room.encrypted", EphemeralEventType}
|
||||
BeeperEphemeralEventAIStream = Type{"com.beeper.ai.stream_event", EphemeralEventType}
|
||||
)
|
||||
|
||||
// Account data events
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ var (
|
|||
BeeperFeatureAccountDataMute = UnstableFeature{UnstableFlag: "com.beeper.account_data_mute"}
|
||||
BeeperFeatureInboxState = UnstableFeature{UnstableFlag: "com.beeper.inbox_state"}
|
||||
BeeperFeatureArbitraryMemberChange = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_member_change"}
|
||||
BeeperFeatureEphemeralEvents = UnstableFeature{UnstableFlag: "com.beeper.ephemeral"}
|
||||
)
|
||||
|
||||
func (versions *RespVersions) Supports(feature UnstableFeature) bool {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue