Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client,bridgev2: allow extended profiles for non-beeper servers using MSC4133 #281

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bridgev2/bridgeconfig/appservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (buc *BotUserConfig) UnmarshalYAML(node *yaml.Node) error {
return err
}
*buc = (BotUserConfig)(sbuc)
if buc.Avatar != "" && buc.Avatar != "remove" {
if buc.Avatar != "" {
buc.ParsedAvatar, err = id.ParseContentURI(buc.Avatar)
if err != nil {
return fmt.Errorf("%w in bot avatar", err)
Expand Down
62 changes: 30 additions & 32 deletions bridgev2/ghost.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,16 @@ type UserInfo struct {
ExtraUpdates ExtraUpdater[*Ghost]
}

func (ghost *Ghost) UpdateName(ctx context.Context, name string) bool {
func (ghost *Ghost) updateName(name string) bool {
if ghost.Name == name && ghost.NameSet {
return false
}
ghost.Name = name
ghost.NameSet = false
err := ghost.Intent.SetDisplayName(ctx, name)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set display name")
} else {
ghost.NameSet = true
}
return true
}

func (ghost *Ghost) UpdateAvatar(ctx context.Context, avatar *Avatar) bool {
func (ghost *Ghost) updateAvatar(ctx context.Context, avatar *Avatar) bool {
if ghost.AvatarID == avatar.ID && ghost.AvatarSet {
return false
}
Expand All @@ -171,15 +165,10 @@ func (ghost *Ghost) UpdateAvatar(ctx context.Context, avatar *Avatar) bool {
ghost.AvatarMXC = ""
}
ghost.AvatarSet = false
if err := ghost.Intent.SetAvatarURL(ctx, ghost.AvatarMXC); err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set avatar URL")
} else {
ghost.AvatarSet = true
}
return true
}

func (ghost *Ghost) UpdateContactInfo(ctx context.Context, identifiers []string, isBot *bool) bool {
func (ghost *Ghost) updateContactInfo(identifiers []string, isBot *bool) bool {
if identifiers != nil {
slices.Sort(identifiers)
}
Expand All @@ -194,21 +183,6 @@ func (ghost *Ghost) UpdateContactInfo(ctx context.Context, identifiers []string,
if isBot != nil {
ghost.IsBot = *isBot
}
bridgeName := ghost.Bridge.Network.GetName()
meta := &event.BeeperProfileExtra{
RemoteID: string(ghost.ID),
Identifiers: ghost.Identifiers,
Service: bridgeName.BeeperBridgeType,
Network: bridgeName.NetworkID,
IsBridgeBot: false,
IsNetworkBot: ghost.IsBot,
}
err := ghost.Intent.SetExtraProfileMeta(ctx, meta)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set extra profile metadata")
} else {
ghost.ContactInfoSet = true
}
return true
}

Expand Down Expand Up @@ -264,17 +238,41 @@ func (ghost *Ghost) UpdateInfo(ctx context.Context, info *UserInfo) {
oldName := ghost.Name
oldAvatar := ghost.AvatarMXC
if info.Name != nil {
update = ghost.UpdateName(ctx, *info.Name) || update
update = ghost.updateName(*info.Name) || update
}
if info.Avatar != nil {
update = ghost.UpdateAvatar(ctx, info.Avatar) || update
update = ghost.updateAvatar(ctx, info.Avatar) || update
}
if info.Identifiers != nil || info.IsBot != nil {
update = ghost.UpdateContactInfo(ctx, info.Identifiers, info.IsBot) || update
update = ghost.updateContactInfo(info.Identifiers, info.IsBot) || update
}
if info.ExtraUpdates != nil {
update = info.ExtraUpdates(ctx, ghost) || update
}
if update {
bridgeName := ghost.Bridge.Network.GetName()
err := ghost.Intent.SetProfile(ctx, &event.ExtendedProfile[event.BeeperProfileExtra]{
StandardProfile: event.StandardProfile{
Displayname: ghost.Name,
AvatarURL: ghost.AvatarMXC,
},
Extra: event.BeeperProfileExtra{
RemoteID: string(ghost.ID),
Identifiers: ghost.Identifiers,
Service: bridgeName.BeeperBridgeType,
Network: bridgeName.NetworkID,
IsBridgeBot: false,
IsNetworkBot: ghost.IsBot,
},
})
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set profile")
} else {
ghost.NameSet = true
ghost.AvatarSet = true
ghost.ContactInfoSet = true
}
}
if oldName != ghost.Name || oldAvatar != ghost.AvatarMXC {
ghost.updateDMPortals(ctx)
}
Expand Down
41 changes: 11 additions & 30 deletions bridgev2/matrix/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,39 +375,20 @@ func (br *Connector) fetchMediaConfig(ctx context.Context) {

func (br *Connector) UpdateBotProfile(ctx context.Context) {
br.Log.Debug().Msg("Updating bot profile")
botConfig := &br.Config.AppService.Bot

var err error
var mxc id.ContentURI
if botConfig.Avatar == "remove" {
err = br.Bot.SetAvatarURL(ctx, mxc)
} else if !botConfig.ParsedAvatar.IsEmpty() {
err = br.Bot.SetAvatarURL(ctx, botConfig.ParsedAvatar)
}
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot avatar")
}

if botConfig.Displayname == "remove" {
err = br.Bot.SetDisplayName(ctx, "")
} else if len(botConfig.Displayname) > 0 {
err = br.Bot.SetDisplayName(ctx, botConfig.Displayname)
}
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot displayname")
}

if br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) {
br.Log.Debug().Msg("Setting contact info on the appservice bot")
netName := br.Bridge.Network.GetName()
err = br.Bot.BeeperUpdateProfile(ctx, event.BeeperProfileExtra{
netName := br.Bridge.Network.GetName()
err := br.Bot.SetProfile(ctx, event.ExtendedProfile[event.BeeperProfileExtra]{
StandardProfile: event.StandardProfile{
Displayname: br.Config.AppService.Bot.Displayname,
AvatarURL: br.Config.AppService.Bot.ParsedAvatar.CUString(),
},
Extra: event.BeeperProfileExtra{
Service: netName.BeeperBridgeType,
Network: netName.NetworkID,
IsBridgeBot: true,
})
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot contact info")
}
},
})
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot profile")
}
}

Expand Down
4 changes: 4 additions & 0 deletions bridgev2/matrix/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,10 @@ func (as *ASIntent) SetExtraProfileMeta(ctx context.Context, data any) error {
return as.Matrix.BeeperUpdateProfile(ctx, data)
}

func (as *ASIntent) SetProfile(ctx context.Context, profile any) error {
return as.Matrix.SetProfile(ctx, profile)
}

func (as *ASIntent) GetMXID() id.UserID {
return as.Matrix.UserID
}
Expand Down
3 changes: 1 addition & 2 deletions bridgev2/matrix/mxmain/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ appservice:
bot:
# Username of the appservice bot.
username: $<<.NetworkID>>bot
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
# to leave display name/avatar as-is.
# Display name and avatar for bot. If empty, profile info won't be changed.
displayname: $<<.DisplayName>> bridge bot
avatar: $<<.NetworkIcon>>

Expand Down
4 changes: 4 additions & 0 deletions bridgev2/matrixinterface.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,13 @@ type MatrixAPI interface {
UploadMedia(ctx context.Context, roomID id.RoomID, data []byte, fileName, mimeType string) (url id.ContentURIString, file *event.EncryptedFileInfo, err error)
UploadMediaStream(ctx context.Context, roomID id.RoomID, size int64, requireFile bool, cb FileStreamCallback) (url id.ContentURIString, file *event.EncryptedFileInfo, err error)

// Deprecated: use SetProfile instead
SetDisplayName(ctx context.Context, name string) error
// Deprecated: use SetProfile instead
SetAvatarURL(ctx context.Context, avatarURL id.ContentURIString) error
// Deprecated: use SetProfile instead
SetExtraProfileMeta(ctx context.Context, data any) error
SetProfile(ctx context.Context, profile any) error

CreateRoom(ctx context.Context, req *mautrix.ReqCreateRoom) (id.RoomID, error)
DeleteRoom(ctx context.Context, roomID id.RoomID, puppetsOnly bool) error
Expand Down
57 changes: 57 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"time"

"github.com/rs/zerolog"
"github.com/tidwall/gjson"
"go.mau.fi/util/retryafter"

"maunium.net/go/mautrix/crypto/backup"
Expand Down Expand Up @@ -1023,7 +1024,63 @@ func (cli *Client) SetAvatarURL(ctx context.Context, url id.ContentURI) (err err
return nil
}

// SetProfile replaces the user's entire profile.
//
// If MSC4133 (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) is supported, this is a single PUT call.
// Otherwise, the provided data will be parsed and the displayname and avatar are sent in separate requests.
func (cli *Client) SetProfile(ctx context.Context, data any) (err error) {
return cli.setOrUpdateProfile(ctx, data, http.MethodPut)
}

// UpdateProfile updates the provided fields in the user's entire profile.
//
// If MSC4133 (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) is supported, this is a single PATCH call.
// Otherwise, the provided data will be parsed and the displayname and avatar are sent in separate requests.
func (cli *Client) UpdateProfile(ctx context.Context, data any) (err error) {
return cli.setOrUpdateProfile(ctx, data, http.MethodPatch)
}

func (cli *Client) setOrUpdateProfile(ctx context.Context, data any, method string) (err error) {
if cli.SpecVersions.Supports(FeatureExtendedProfiles) || cli.SpecVersions.Supports(BeeperFeatureArbitraryProfileMeta) {
urlPath := cli.BuildClientURL("v3", "profile", cli.UserID)
_, err = cli.MakeRequest(ctx, method, urlPath, data, nil)
} else if cli.SpecVersions.Supports(UnstableFeatureExtendedProfiles) {
urlPath := cli.BuildClientURL("unstable", "uk.tcpip.msc4133", "profile", cli.UserID)
_, err = cli.MakeRequest(ctx, method, urlPath, data, nil)
sumnerevans marked this conversation as resolved.
Show resolved Hide resolved
} else {
dataJSON, err := json.Marshal(data)
if err != nil {
return err
}
vals := gjson.GetManyBytes(dataJSON, "displayname", "avatar_url")
if vals[0].Exists() || method == http.MethodPut {
err = cli.SetDisplayName(ctx, vals[0].Str)
if err != nil {
return fmt.Errorf("failed to set display name: %w", err)
}
}
if vals[1].Exists() {
parsed, err := id.ParseContentURI(vals[1].Str)
if err != nil {
return fmt.Errorf("failed to parse avatar URL: %w", err)
}
err = cli.SetAvatarURL(ctx, parsed)
if err != nil {
return fmt.Errorf("failed to set avatar URL: %w", err)
}
} else if method == http.MethodPut {
err = cli.SetAvatarURL(ctx, id.ContentURI{})
if err != nil {
return fmt.Errorf("failed to set avatar URL: %w", err)
}
}
}
return nil
}

// BeeperUpdateProfile sets custom fields in the user's profile.
//
// Deprecated: Updating profiles is being added to the Matrix spec in MSC4133. Use UpdateProfile instead.
func (cli *Client) BeeperUpdateProfile(ctx context.Context, data any) (err error) {
urlPath := cli.BuildClientURL("v3", "profile", cli.UserID)
_, err = cli.MakeRequest(ctx, http.MethodPatch, urlPath, data, nil)
Expand Down
39 changes: 39 additions & 0 deletions event/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
package event

import (
"bytes"
"encoding/json"
"errors"

"maunium.net/go/mautrix/id"
)
Expand Down Expand Up @@ -43,6 +45,43 @@ type MemberEventContent struct {
Reason string `json:"reason,omitempty"`
}

type StandardProfile struct {
Displayname string `json:"displayname,omitempty"`
AvatarURL id.ContentURIString `json:"avatar_url,omitempty"`
}

type ExtendedProfile[T any] struct {
StandardProfile
Extra T
}

func (ep *ExtendedProfile[T]) MarshalJSON() ([]byte, error) {
data, err := json.Marshal(ep.StandardProfile)
if err != nil {
return nil, err
}
extraData, err := json.Marshal(ep.Extra)
if err != nil {
return nil, err
}
if len(extraData) == 0 || bytes.Equal(extraData, []byte("{}")) || bytes.Equal(extraData, []byte("null")) {
return data, nil
} else if extraData[0] != '{' || extraData[len(extraData)-1] != '}' {
return nil, errors.New("unexpected type marshaling profile extra data: not an object")
}
data[len(data)-1] = ','
data = append(data, extraData[1:]...)
return data, nil
}

func (ep *ExtendedProfile[T]) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, &ep.StandardProfile)
if err != nil {
return err
}
return json.Unmarshal(data, &ep.Extra)
}

type ThirdPartyInvite struct {
DisplayName string `json:"display_name"`
Signed struct {
Expand Down
3 changes: 3 additions & 0 deletions versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ var (
FeatureAppservicePing = UnstableFeature{UnstableFlag: "fi.mau.msc2659.stable", SpecVersion: SpecV17}
FeatureAuthenticatedMedia = UnstableFeature{UnstableFlag: "org.matrix.msc3916.stable", SpecVersion: SpecV111}

UnstableFeatureExtendedProfiles = UnstableFeature{UnstableFlag: "uk.tcpip.msc4133"}
FeatureExtendedProfiles = UnstableFeature{UnstableFlag: "uk.tcpip.msc4133.stable"}

BeeperFeatureHungry = UnstableFeature{UnstableFlag: "com.beeper.hungry"}
BeeperFeatureBatchSending = UnstableFeature{UnstableFlag: "com.beeper.batch_sending"}
BeeperFeatureRoomYeeting = UnstableFeature{UnstableFlag: "com.beeper.room_yeeting"}
Expand Down
Loading