bridgev2: add remote->matrix room tagging and muting interfaces

This commit is contained in:
Tulir Asokan 2024-06-20 13:26:11 +03:00
commit 59b99dee70
6 changed files with 160 additions and 11 deletions

View file

@ -8,6 +8,7 @@ package matrix
import (
"context"
"errors"
"fmt"
"time"
@ -19,6 +20,7 @@ import (
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
)
// ASIntent implements the bridge ghost API interface using a real Matrix homeserver as the backend.
@ -249,3 +251,31 @@ func (as *ASIntent) DeleteRoom(ctx context.Context, roomID id.RoomID, puppetsOnl
}
return nil
}
func (as *ASIntent) TagRoom(ctx context.Context, roomID id.RoomID, tag event.RoomTag, isTagged bool) error {
if isTagged {
return as.Matrix.AddTagWithCustomData(ctx, roomID, tag, &event.TagMetadata{
MauDoublePuppetSource: as.Connector.AS.DoublePuppetValue,
})
} else {
if tag == "" {
// TODO clear all tags?
}
return as.Matrix.RemoveTag(ctx, roomID, tag)
}
}
func (as *ASIntent) MuteRoom(ctx context.Context, roomID id.RoomID, until time.Time) error {
if !until.IsZero() && until.Before(time.Now()) {
err := as.Matrix.DeletePushRule(ctx, "global", pushrules.RoomRule, string(roomID))
// If the push rule doesn't exist, everything is fine
if errors.Is(err, mautrix.MNotFound) {
err = nil
}
return err
} else {
return as.Matrix.PutPushRule(ctx, "global", pushrules.RoomRule, string(roomID), &mautrix.ReqPutPushRule{
Actions: []pushrules.PushActionType{pushrules.ActionDontNotify},
})
}
}

View file

@ -58,4 +58,7 @@ type MatrixAPI interface {
DeleteRoom(ctx context.Context, roomID id.RoomID, puppetsOnly bool) error
InviteUser(ctx context.Context, roomID id.RoomID, userID id.UserID) error
EnsureJoined(ctx context.Context, roomID id.RoomID) error
TagRoom(ctx context.Context, roomID id.RoomID, tag event.RoomTag, isTagged bool) error
MuteRoom(ctx context.Context, roomID id.RoomID, until time.Time) error
}

View file

@ -287,6 +287,8 @@ const (
RemoteEventReadReceipt
RemoteEventDeliveryReceipt
RemoteEventTyping
RemoteEventChatTag
RemoteEventChatMute
)
// RemoteEvent represents a single event from the remote network, such as a message or a reaction.
@ -374,6 +376,18 @@ type RemoteTypingWithType interface {
GetTypingType() TypingType
}
type RemoteChatTag interface {
RemoteEvent
GetTag() (tag event.RoomTag, remove bool)
}
var Unmuted = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
type RemoteChatMute interface {
RemoteEvent
GetMutedUntil() time.Time
}
// SimpleRemoteEvent is a simple implementation of RemoteEvent that can be used with struct fields and some callbacks.
type SimpleRemoteEvent[T any] struct {
Type RemoteEventType

View file

@ -804,6 +804,10 @@ func (portal *Portal) handleRemoteEvent(source *UserLogin, evt RemoteEvent) {
portal.handleRemoteDeliveryReceipt(ctx, source, evt.(RemoteReceipt))
case RemoteEventTyping:
portal.handleRemoteTyping(ctx, source, evt.(RemoteTyping))
case RemoteEventChatTag:
portal.handleRemoteChatTag(ctx, source, evt.(RemoteChatTag))
case RemoteEventChatMute:
portal.handleRemoteChatMute(ctx, source, evt.(RemoteChatMute))
default:
log.Warn().Int("type", int(evt.GetType())).Msg("Got remote event with unknown type")
}
@ -1224,6 +1228,37 @@ func (portal *Portal) handleRemoteTyping(ctx context.Context, source *UserLogin,
}
}
func (portal *Portal) handleRemoteChatTag(ctx context.Context, source *UserLogin, evt RemoteChatTag) {
if !evt.GetSender().IsFromMe {
zerolog.Ctx(ctx).Warn().Msg("Ignoring chat tag event from non-self user")
return
}
dp := source.User.DoublePuppet(ctx)
if dp == nil {
return
}
tag, isTagged := evt.GetTag()
err := dp.TagRoom(ctx, portal.MXID, tag, isTagged)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to bridge chat tag event")
}
}
func (portal *Portal) handleRemoteChatMute(ctx context.Context, source *UserLogin, evt RemoteChatMute) {
if !evt.GetSender().IsFromMe {
zerolog.Ctx(ctx).Warn().Msg("Ignoring chat mute event from non-self user")
return
}
dp := source.User.DoublePuppet(ctx)
if dp == nil {
return
}
err := dp.MuteRoom(ctx, portal.MXID, evt.GetMutedUntil())
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to bridge chat mute event")
}
}
type PortalInfo struct {
Name *string
Topic *string
@ -1233,6 +1268,13 @@ type PortalInfo struct {
IsDirectChat *bool
IsSpace *bool
UserLocal *UserLocalPortalInfo
}
type UserLocalPortalInfo struct {
MutedUntil *time.Time
Tag *event.RoomTag
}
func (portal *Portal) UpdateName(ctx context.Context, name string, sender *Ghost, ts time.Time) bool {
@ -1445,6 +1487,28 @@ func (portal *Portal) SyncParticipants(ctx context.Context, members []networkid.
return expectedUserIDs, extraFunctionalMembers, nil
}
func (portal *Portal) updateUserLocalInfo(ctx context.Context, info *UserLocalPortalInfo, source *UserLogin) {
if portal.MXID == "" || info == nil {
return
}
dp := source.User.DoublePuppet(ctx)
if dp == nil {
return
}
if info.MutedUntil != nil {
err := dp.MuteRoom(ctx, portal.MXID, *info.MutedUntil)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to mute room")
}
}
if info.Tag != nil {
err := dp.TagRoom(ctx, portal.MXID, *info.Tag, *info.Tag != "")
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to tag room")
}
}
}
func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, source *UserLogin, sender *Ghost, ts time.Time) {
changed := false
if info.Name != nil {
@ -1469,6 +1533,7 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, source *
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to ensure user portal row exists")
}
portal.updateUserLocalInfo(ctx, info.UserLocal, source)
}
if changed {
portal.UpdateBridgeInfo(ctx)
@ -1599,6 +1664,7 @@ func (portal *Portal) CreateMatrixRoom(ctx context.Context, source *UserLogin, i
if portal.Parent != nil {
// TODO add m.space.child event
}
portal.updateUserLocalInfo(ctx, info.UserLocal, source)
if !isBeeper {
_, _, err = portal.SyncParticipants(ctx, info.Members, source)
if err != nil {

View file

@ -1925,15 +1925,13 @@ func (cli *Client) SetReadMarkers(ctx context.Context, roomID id.RoomID, content
return
}
func (cli *Client) AddTag(ctx context.Context, roomID id.RoomID, tag string, order float64) error {
var tagData event.Tag
if order == order {
tagData.Order = json.Number(strconv.FormatFloat(order, 'e', -1, 64))
}
return cli.AddTagWithCustomData(ctx, roomID, tag, tagData)
func (cli *Client) AddTag(ctx context.Context, roomID id.RoomID, tag event.RoomTag, order float64) error {
return cli.AddTagWithCustomData(ctx, roomID, tag, &event.TagMetadata{
Order: json.Number(strconv.FormatFloat(order, 'e', -1, 64)),
})
}
func (cli *Client) AddTagWithCustomData(ctx context.Context, roomID id.RoomID, tag string, data interface{}) (err error) {
func (cli *Client) AddTagWithCustomData(ctx context.Context, roomID id.RoomID, tag event.RoomTag, data any) (err error) {
urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
return
@ -1944,13 +1942,13 @@ func (cli *Client) GetTags(ctx context.Context, roomID id.RoomID) (tags event.Ta
return
}
func (cli *Client) GetTagsWithCustomData(ctx context.Context, roomID id.RoomID, resp interface{}) (err error) {
func (cli *Client) GetTagsWithCustomData(ctx context.Context, roomID id.RoomID, resp any) (err error) {
urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags")
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
func (cli *Client) RemoveTag(ctx context.Context, roomID id.RoomID, tag string) (err error) {
func (cli *Client) RemoveTag(ctx context.Context, roomID id.RoomID, tag event.RoomTag) (err error) {
urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag)
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
return

View file

@ -8,6 +8,7 @@ package event
import (
"encoding/json"
"strings"
"maunium.net/go/mautrix/id"
)
@ -18,10 +19,47 @@ type TagEventContent struct {
Tags Tags `json:"tags"`
}
type Tags map[string]Tag
type Tags map[RoomTag]TagMetadata
type Tag struct {
type RoomTag string
const (
RoomTagFavourite RoomTag = "m.favourite"
RoomTagLowPriority RoomTag = "m.lowpriority"
RoomTagServerNotice RoomTag = "m.server_notice"
)
func (rt RoomTag) IsUserDefined() bool {
return strings.HasPrefix(string(rt), "u.")
}
func (rt RoomTag) String() string {
return string(rt)
}
func (rt RoomTag) Name() string {
if rt.IsUserDefined() {
return string(rt[2:])
}
switch rt {
case RoomTagFavourite:
return "Favourite"
case RoomTagLowPriority:
return "Low priority"
case RoomTagServerNotice:
return "Server notice"
default:
return ""
}
}
// Deprecated: type alias
type Tag = TagMetadata
type TagMetadata struct {
Order json.Number `json:"order,omitempty"`
MauDoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"`
}
// DirectChatsEventContent represents the content of a m.direct account data event.