mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
event: implement disappearing timer types (#399)
Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
parent
a547c0636c
commit
1d484e01d0
10 changed files with 127 additions and 14 deletions
|
|
@ -12,28 +12,41 @@ import (
|
|||
"time"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/jsontime"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// DisappearingType represents the type of a disappearing message timer.
|
||||
type DisappearingType string
|
||||
// Deprecated: use [event.DisappearingType]
|
||||
type DisappearingType = event.DisappearingType
|
||||
|
||||
// Deprecated: use constants in event package
|
||||
const (
|
||||
DisappearingTypeNone DisappearingType = ""
|
||||
DisappearingTypeAfterRead DisappearingType = "after_read"
|
||||
DisappearingTypeAfterSend DisappearingType = "after_send"
|
||||
DisappearingTypeNone = event.DisappearingTypeNone
|
||||
DisappearingTypeAfterRead = event.DisappearingTypeAfterRead
|
||||
DisappearingTypeAfterSend = event.DisappearingTypeAfterSend
|
||||
)
|
||||
|
||||
// DisappearingSetting represents a disappearing message timer setting
|
||||
// by combining a type with a timer and an optional start timestamp.
|
||||
type DisappearingSetting struct {
|
||||
Type DisappearingType
|
||||
Type event.DisappearingType
|
||||
Timer time.Duration
|
||||
DisappearAt time.Time
|
||||
}
|
||||
|
||||
func (ds DisappearingSetting) ToEventContent() *event.BeeperDisappearingTimer {
|
||||
if ds.Type == event.DisappearingTypeNone || ds.Timer == 0 {
|
||||
return nil
|
||||
}
|
||||
return &event.BeeperDisappearingTimer{
|
||||
Type: ds.Type,
|
||||
Timer: jsontime.MS(ds.Timer),
|
||||
}
|
||||
}
|
||||
|
||||
type DisappearingMessageQuery struct {
|
||||
BridgeID networkid.BridgeID
|
||||
*dbutil.QueryHelper[*DisappearingMessage]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"go.mau.fi/util/dbutil"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
|
|
@ -34,9 +35,20 @@ type PortalQuery struct {
|
|||
*dbutil.QueryHelper[*Portal]
|
||||
}
|
||||
|
||||
type CapStateFlags uint32
|
||||
|
||||
func (csf CapStateFlags) Has(flag CapStateFlags) bool {
|
||||
return csf&flag != 0
|
||||
}
|
||||
|
||||
const (
|
||||
CapStateFlagDisappearingTimerSet CapStateFlags = 1 << iota
|
||||
)
|
||||
|
||||
type CapabilityState struct {
|
||||
Source networkid.UserLoginID `json:"source"`
|
||||
ID string `json:"id"`
|
||||
Flags CapStateFlags `json:"flags"`
|
||||
}
|
||||
|
||||
type Portal struct {
|
||||
|
|
@ -208,7 +220,7 @@ func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
|
|||
}
|
||||
if disappearType.Valid {
|
||||
p.Disappear = DisappearingSetting{
|
||||
Type: DisappearingType(disappearType.String),
|
||||
Type: event.DisappearingType(disappearType.String),
|
||||
Timer: time.Duration(disappearTimer.Int64),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1101,7 +1101,7 @@ func (portal *Portal) handleMatrixMessage(ctx context.Context, sender *UserLogin
|
|||
}
|
||||
portal.sendSuccessStatus(ctx, evt, resp.StreamOrder, message.MXID)
|
||||
}
|
||||
if portal.Disappear.Type != database.DisappearingTypeNone {
|
||||
if portal.Disappear.Type != event.DisappearingTypeNone {
|
||||
go portal.Bridge.DisappearLoop.Add(ctx, &database.DisappearingMessage{
|
||||
RoomID: portal.MXID,
|
||||
EventID: message.MXID,
|
||||
|
|
@ -2281,6 +2281,7 @@ func (portal *Portal) sendConvertedMessage(
|
|||
allSuccess := true
|
||||
for i, part := range converted.Parts {
|
||||
portal.applyRelationMeta(ctx, part.Content, replyTo, threadRoot, prevThreadEvent)
|
||||
part.Content.BeeperDisappearingTimer = converted.Disappear.ToEventContent()
|
||||
dbMessage := &database.Message{
|
||||
ID: id,
|
||||
PartID: part.ID,
|
||||
|
|
@ -2325,8 +2326,8 @@ func (portal *Portal) sendConvertedMessage(
|
|||
logContext(log.Err(err)).Str("part_id", string(part.ID)).Msg("Failed to save message part to database")
|
||||
allSuccess = false
|
||||
}
|
||||
if converted.Disappear.Type != database.DisappearingTypeNone && !dbMessage.HasFakeMXID() {
|
||||
if converted.Disappear.Type == database.DisappearingTypeAfterSend && converted.Disappear.DisappearAt.IsZero() {
|
||||
if converted.Disappear.Type != event.DisappearingTypeNone && !dbMessage.HasFakeMXID() {
|
||||
if converted.Disappear.Type == event.DisappearingTypeAfterSend && converted.Disappear.DisappearAt.IsZero() {
|
||||
converted.Disappear.DisappearAt = dbMessage.Timestamp.Add(converted.Disappear.Timer)
|
||||
}
|
||||
portal.Bridge.DisappearLoop.Add(ctx, &database.DisappearingMessage{
|
||||
|
|
@ -3648,6 +3649,15 @@ func (portal *Portal) UpdateCapabilities(ctx context.Context, source *UserLogin,
|
|||
portal.CapState = database.CapabilityState{
|
||||
Source: source.ID,
|
||||
ID: capID,
|
||||
Flags: portal.CapState.Flags,
|
||||
}
|
||||
if caps.DisappearingTimer != nil && !portal.CapState.Flags.Has(database.CapStateFlagDisappearingTimerSet) {
|
||||
zerolog.Ctx(ctx).Debug().Msg("Disappearing timer capability was added, sending disappearing timer state event")
|
||||
success = portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent())
|
||||
if !success {
|
||||
return false
|
||||
}
|
||||
portal.CapState.Flags |= database.CapStateFlagDisappearingTimerSet
|
||||
}
|
||||
portal.lastCapUpdate = time.Now()
|
||||
if implicit {
|
||||
|
|
@ -4030,7 +4040,7 @@ func DisappearingMessageNotice(expiration time.Duration, implicit bool) *event.M
|
|||
|
||||
func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting database.DisappearingSetting, sender MatrixAPI, ts time.Time, implicit, save bool) bool {
|
||||
if setting.Timer == 0 {
|
||||
setting.Type = ""
|
||||
setting.Type = event.DisappearingTypeNone
|
||||
}
|
||||
if portal.Disappear.Timer == setting.Timer && portal.Disappear.Type == setting.Type {
|
||||
return false
|
||||
|
|
@ -4046,6 +4056,9 @@ func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting dat
|
|||
if portal.MXID == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
portal.sendRoomMeta(ctx, sender, ts, event.StateBeeperDisappearingTimer, "", setting.ToEventContent())
|
||||
|
||||
content := DisappearingMessageNotice(setting.Timer, implicit)
|
||||
if sender == nil {
|
||||
sender = portal.Bridge.Bot
|
||||
|
|
@ -4333,6 +4346,13 @@ func (portal *Portal) createMatrixRoomInLoop(ctx context.Context, source *UserLo
|
|||
Type: event.StateBeeperRoomFeatures,
|
||||
Content: event.Content{Parsed: roomFeatures},
|
||||
})
|
||||
if roomFeatures.DisappearingTimer != nil {
|
||||
req.InitialState = append(req.InitialState, &event.Event{
|
||||
Type: event.StateBeeperDisappearingTimer,
|
||||
Content: event.Content{Parsed: portal.Disappear.ToEventContent()},
|
||||
})
|
||||
portal.CapState.Flags |= database.CapStateFlagDisappearingTimerSet
|
||||
}
|
||||
if req.Topic == "" {
|
||||
// Add explicit topic event if topic is empty to ensure the event is set.
|
||||
// This ensures that there won't be an extra event later if PUT /state/... is called.
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ func (portal *Portal) compileBatchMessage(ctx context.Context, source *UserLogin
|
|||
for i, part := range msg.Parts {
|
||||
partIDs = append(partIDs, part.ID)
|
||||
portal.applyRelationMeta(ctx, part.Content, replyTo, threadRoot, prevThreadEvent)
|
||||
part.Content.BeeperDisappearingTimer = msg.Disappear.ToEventContent()
|
||||
evtID := portal.Bridge.Matrix.GenerateDeterministicEventID(portal.MXID, portal.PortalKey, msg.ID, part.ID)
|
||||
dbMessage := &database.Message{
|
||||
ID: msg.ID,
|
||||
|
|
@ -379,8 +380,8 @@ func (portal *Portal) compileBatchMessage(ctx context.Context, source *UserLogin
|
|||
prevThreadEvent.MXID = evtID
|
||||
out.PrevThreadEvents[*msg.ThreadRoot] = evtID
|
||||
}
|
||||
if msg.Disappear.Type != database.DisappearingTypeNone {
|
||||
if msg.Disappear.Type == database.DisappearingTypeAfterSend && msg.Disappear.DisappearAt.IsZero() {
|
||||
if msg.Disappear.Type != event.DisappearingTypeNone {
|
||||
if msg.Disappear.Type == event.DisappearingTypeAfterSend && msg.Disappear.DisappearAt.IsZero() {
|
||||
msg.Disappear.DisappearAt = msg.Timestamp.Add(msg.Disappear.Timer)
|
||||
}
|
||||
out.Disappear = append(out.Disappear, &database.DisappearingMessage{
|
||||
|
|
|
|||
21
event/capabilities.d.ts
vendored
21
event/capabilities.d.ts
vendored
|
|
@ -41,6 +41,8 @@ export interface RoomFeatures {
|
|||
delete_max_age?: seconds
|
||||
/** Whether deleting messages just for yourself is supported. No message age limit. */
|
||||
delete_for_me?: boolean
|
||||
/** Allowed configuration options for disappearing timers. */
|
||||
disappearing_timer?: DisappearingTimerCapability
|
||||
|
||||
/** Whether reactions are supported. */
|
||||
reaction?: CapabilitySupportLevel
|
||||
|
|
@ -57,6 +59,7 @@ export interface RoomFeatures {
|
|||
|
||||
declare type integer = number
|
||||
declare type seconds = integer
|
||||
declare type milliseconds = integer
|
||||
declare type MIMEClass = "image" | "audio" | "video" | "text" | "font" | "model" | "application"
|
||||
declare type MIMETypeOrPattern =
|
||||
"*/*"
|
||||
|
|
@ -106,6 +109,24 @@ export interface FileFeatures {
|
|||
view_once?: boolean
|
||||
}
|
||||
|
||||
export enum DisappearingType {
|
||||
None = "",
|
||||
AfterRead = "after_read",
|
||||
AfterSend = "after_send",
|
||||
}
|
||||
|
||||
export interface DisappearingTimerCapability {
|
||||
types: DisappearingType[]
|
||||
timers: milliseconds[]
|
||||
/**
|
||||
* Whether clients should omit the empty disappearing_timer object in messages that they don't want to disappear
|
||||
*
|
||||
* Generally, bridged rooms will want the object to be always present, while native Matrix rooms don't,
|
||||
* so the hardcoded features for Matrix rooms should set this to true, while bridges will not.
|
||||
*/
|
||||
omit_empty_timer?: true
|
||||
}
|
||||
|
||||
/**
|
||||
* The support level for a feature. These are integers rather than booleans
|
||||
* to accurately represent what the bridge is doing and hopefully make the
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ type RoomFeatures struct {
|
|||
DeleteForMe bool `json:"delete_for_me,omitempty"`
|
||||
DeleteMaxAge *jsontime.Seconds `json:"delete_max_age,omitempty"`
|
||||
|
||||
DisappearingTimer *DisappearingTimerCapability `json:"disappearing_timer,omitempty"`
|
||||
|
||||
Reaction CapabilitySupportLevel `json:"reaction,omitempty"`
|
||||
ReactionCount int `json:"reaction_count,omitempty"`
|
||||
AllowedReactions []string `json:"allowed_reactions,omitempty"`
|
||||
|
|
@ -67,6 +69,13 @@ type FormattingFeatureMap map[FormattingFeature]CapabilitySupportLevel
|
|||
|
||||
type FileFeatureMap map[CapabilityMsgType]*FileFeatures
|
||||
|
||||
type DisappearingTimerCapability struct {
|
||||
Types []DisappearingType `json:"types"`
|
||||
Timers []jsontime.Milliseconds `json:"timers"`
|
||||
|
||||
OmitEmptyTimer bool `json:"omit_empty_timer,omitempty"`
|
||||
}
|
||||
|
||||
type CapabilityMsgType = MessageType
|
||||
|
||||
// Message types which are used for event capability signaling, but aren't real values for the msgtype field.
|
||||
|
|
@ -231,6 +240,7 @@ func (rf *RoomFeatures) Hash() []byte {
|
|||
hashValue(hasher, "delete", rf.Delete)
|
||||
hashBool(hasher, "delete_for_me", rf.DeleteForMe)
|
||||
hashInt(hasher, "delete_max_age", rf.DeleteMaxAge.Get())
|
||||
hashValue(hasher, "disappearing_timer", rf.DisappearingTimer)
|
||||
|
||||
hashValue(hasher, "reaction", rf.Reaction)
|
||||
hashInt(hasher, "reaction_count", rf.ReactionCount)
|
||||
|
|
@ -249,6 +259,22 @@ func (rf *RoomFeatures) Hash() []byte {
|
|||
return hasher.Sum(nil)
|
||||
}
|
||||
|
||||
func (dtc *DisappearingTimerCapability) Hash() []byte {
|
||||
if dtc == nil {
|
||||
return nil
|
||||
}
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("types"))
|
||||
for _, t := range dtc.Types {
|
||||
hasher.Write([]byte(t))
|
||||
}
|
||||
hasher.Write([]byte("timers"))
|
||||
for _, timer := range dtc.Timers {
|
||||
hashInt(hasher, "", timer.Milliseconds())
|
||||
}
|
||||
return hasher.Sum(nil)
|
||||
}
|
||||
|
||||
func (ff *FileFeatures) Hash() []byte {
|
||||
hasher := sha256.New()
|
||||
hashMap(hasher, "mime_types", ff.MimeTypes)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ var TypeMap = map[Type]reflect.Type{
|
|||
|
||||
StateElementFunctionalMembers: reflect.TypeOf(ElementFunctionalMembersContent{}),
|
||||
StateBeeperRoomFeatures: reflect.TypeOf(RoomFeatures{}),
|
||||
StateBeeperDisappearingTimer: reflect.TypeOf(BeeperDisappearingTimer{}),
|
||||
|
||||
EventMessage: reflect.TypeOf(MessageEventContent{}),
|
||||
EventSticker: reflect.TypeOf(MessageEventContent{}),
|
||||
|
|
|
|||
|
|
@ -138,6 +138,8 @@ type MessageEventContent struct {
|
|||
|
||||
BeeperLinkPreviews []*BeeperLinkPreview `json:"com.beeper.linkpreviews,omitempty"`
|
||||
|
||||
BeeperDisappearingTimer *BeeperDisappearingTimer `json:"com.beeper.disappearing_timer,omitempty"`
|
||||
|
||||
MSC1767Audio *MSC1767Audio `json:"org.matrix.msc1767.audio,omitempty"`
|
||||
MSC3245Voice *MSC3245Voice `json:"org.matrix.msc3245.voice,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"encoding/base64"
|
||||
"slices"
|
||||
|
||||
"go.mau.fi/util/jsontime"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
|
|
@ -207,6 +209,20 @@ type BridgeEventContent struct {
|
|||
BeeperRoomTypeV2 string `json:"com.beeper.room_type.v2,omitempty"`
|
||||
}
|
||||
|
||||
// DisappearingType represents the type of a disappearing message timer.
|
||||
type DisappearingType string
|
||||
|
||||
const (
|
||||
DisappearingTypeNone DisappearingType = ""
|
||||
DisappearingTypeAfterRead DisappearingType = "after_read"
|
||||
DisappearingTypeAfterSend DisappearingType = "after_send"
|
||||
)
|
||||
|
||||
type BeeperDisappearingTimer struct {
|
||||
Type DisappearingType `json:"type"`
|
||||
Timer jsontime.Milliseconds `json:"timer"`
|
||||
}
|
||||
|
||||
type SpaceChildEventContent struct {
|
||||
Via []string `json:"via,omitempty"`
|
||||
Order string `json:"order,omitempty"`
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ func (et *Type) GuessClass() TypeClass {
|
|||
StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateServerACL.Type, StateTopic.Type,
|
||||
StatePinnedEvents.Type, StateTombstone.Type, StateEncryption.Type, StateBridge.Type, StateHalfShotBridge.Type,
|
||||
StateSpaceParent.Type, StateSpaceChild.Type, StatePolicyRoom.Type, StatePolicyServer.Type, StatePolicyUser.Type,
|
||||
StateElementFunctionalMembers.Type, StateBeeperRoomFeatures.Type:
|
||||
StateElementFunctionalMembers.Type, StateBeeperRoomFeatures.Type, StateBeeperDisappearingTimer.Type:
|
||||
return StateEventType
|
||||
case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type:
|
||||
return EphemeralEventType
|
||||
|
|
@ -202,6 +202,7 @@ var (
|
|||
|
||||
StateElementFunctionalMembers = Type{"io.element.functional_members", StateEventType}
|
||||
StateBeeperRoomFeatures = Type{"com.beeper.room_features", StateEventType}
|
||||
StateBeeperDisappearingTimer = Type{"com.beeper.disappearing_timer", StateEventType}
|
||||
)
|
||||
|
||||
// Message events
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue