Skip to content

Commit

Permalink
Merge remote-tracking branch 'maltee1/groupinfo_matrix_to_signal' int…
Browse files Browse the repository at this point in the history
…o tulir/pni-sending
  • Loading branch information
tulir committed Mar 22, 2024
2 parents fabded7 + 407dbfd commit 9c0b8ec
Show file tree
Hide file tree
Showing 16 changed files with 1,694 additions and 28 deletions.
15 changes: 8 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@
* [x] Message edits
* [x] Message reactions
* [x] Message redactions
* [ ] Group info changes
* [ ] Name
* [ ] Avatar
* [ ] Topic
* [x] Group info changes
* [x] Name
* [x] Avatar
* [x] Topic
* [ ] Membership actions
* [ ] Join (accepting invites)
* [ ] Invite
* [ ] Leave
* [ ] Kick/Ban/Unban
* [x] Invite
* [x] Leave
* [x] Kick/Ban/Unban
* [x] Group permissions
* [x] Typing notifications
* [x] Read receipts
* [x] Delivery receipts (sent after message is bridged)
Expand Down
270 changes: 270 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ package main

import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"

"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/skip2/go-qrcode"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridge/commands"
Expand All @@ -31,6 +36,7 @@ import (

"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)

var (
Expand Down Expand Up @@ -63,6 +69,9 @@ func (br *SignalBridge) RegisterCommands() {
cmdDeletePortal,
cmdDeleteAllPortals,
cmdCleanupLostPortals,
cmdInviteLink,
cmdResetInviteLink,
cmdCreate,
)
}

Expand Down Expand Up @@ -650,3 +659,264 @@ func fnCleanupLostPortals(ce *WrappedCommandEvent) {
}
ce.Reply("Finished cleaning up portals")
}

var cmdInviteLink = &commands.FullHandler{
Func: wrapCommand(fnInviteLink),
Name: "invite-link",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Get the invite link for the corresponding Signal Group",
},
RequiresLogin: true,
}

func fnInviteLink(ce *WrappedCommandEvent) {
if ce.Portal == nil {
ce.Reply("This is not a portal room")
return
}
if ce.Portal.IsPrivateChat() {
ce.Reply("Invite Links are not available for private chats")
return
}
inviteLinkPassword, err := ce.Portal.GetInviteLink(ce.Ctx, ce.User)
if err != nil {
ce.Reply("Error getting invite link %w", err)
return
}
ce.Reply(inviteLinkPassword)
}

var cmdResetInviteLink = &commands.FullHandler{
Func: wrapCommand(fnResetInviteLink),
Name: "reset-invite-link",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Generate a new invite link password",
},
RequiresLogin: true,
}

func fnResetInviteLink(ce *WrappedCommandEvent) {
if ce.Portal == nil {
ce.Reply("This is not a portal room")
return
}
if ce.Portal.IsPrivateChat() {
ce.Reply("Invite Links are not available for private chats")
return
}
err := ce.Portal.ResetInviteLink(ce.Ctx, ce.User)
if err != nil {
ce.Reply("Error setting new invite link %w", err)
}
inviteLink, err := ce.Portal.GetInviteLink(ce.Ctx, ce.User)
if err != nil {
ce.Reply("Error getting new invite link %w", err)
return
}
ce.Reply(inviteLink)
}

var cmdCreate = &commands.FullHandler{
Func: wrapCommand(fnCreate),
Name: "create",
Help: commands.HelpMeta{
Section: HelpSectionCreatingPortals,
Description: "Create a Signal group chat for the current Matrix room.",
},
RequiresLogin: true,
}

func fnCreate(ce *WrappedCommandEvent) {
if ce.Portal != nil {
ce.Reply("This is already a portal room")
return
}

roomState, err := ce.Bot.State(ce.Ctx, ce.RoomID)
if err != nil {
ce.Reply("Failed to get room state: %w", err)
return
}
members := roomState[event.StateMember]
powerLevelsRaw, ok := roomState[event.StatePowerLevels][""]
if !ok {
ce.Reply("Failed to get room power levels")
return
}
powerLevelsRaw.Content.ParseRaw(event.StatePowerLevels)
powerLevels := powerLevelsRaw.Content.AsPowerLevels()
joinRulesRaw, ok := roomState[event.StateJoinRules][""]
if !ok {
ce.Reply("Failed to get join rules")
return
}
joinRulesRaw.Content.ParseRaw(event.StateJoinRules)
joinRule := joinRulesRaw.Content.AsJoinRules().JoinRule
roomNameEventRaw, ok := roomState[event.StateRoomName][""]
if !ok {
ce.Reply("Failed to get room name")
return
}
roomNameEventRaw.Content.ParseRaw(event.StateRoomName)
roomName := roomNameEventRaw.Content.AsRoomName().Name
if len(roomName) == 0 {
ce.Reply("Please set a name for the room first")
return
}
roomTopic := ""
roomTopicEvent, ok := roomState[event.StateTopic][""]
if ok {
roomTopicEvent.Content.ParseRaw(event.StateTopic)
roomTopic = roomTopicEvent.Content.AsTopic().Topic
}
roomAvatarEvent, ok := roomState[event.StateRoomAvatar][""]
var avatarHash string
var avatarURL id.ContentURI
var avatarBytes []byte
if ok {
roomAvatarEvent.Content.ParseRaw(event.StateRoomAvatar)
avatarURL = roomAvatarEvent.Content.AsRoomAvatar().URL
if !avatarURL.IsEmpty() {
avatarBytes, err = ce.Bot.DownloadBytes(ce.Ctx, avatarURL)
if err != nil {
ce.ZLog.Err(err).Stringer("Failed to download updated avatar %s", avatarURL)
return
}
hash := sha256.Sum256(avatarBytes)
avatarHash = hex.EncodeToString(hash[:])
log.Debug().Stringers("%s set the group avatar to %s", []fmt.Stringer{ce.User.MXID, avatarURL})
}
}
var encryptionEvent *event.EncryptionEventContent
encryptionEventContent, ok := roomState[event.StateEncryption][""]
if ok {
encryptionEventContent.Content.ParseRaw(event.StateEncryption)
encryptionEvent = encryptionEventContent.Content.AsEncryption()
}
var participants []*signalmeow.GroupMember
var bannedMembers []*signalmeow.BannedMember
participantDedup := make(map[uuid.UUID]bool)
participantDedup[uuid.Nil] = true
for key, member := range members {
mxid := id.UserID(key)
member.Content.ParseRaw(event.StateMember)
content := member.Content.AsMember()
membership := content.Membership
var uuid uuid.UUID
puppet := ce.Bridge.GetPuppetByMXID(mxid)
if puppet != nil {
uuid = puppet.SignalID
} else {
user := ce.Bridge.GetUserByMXID(mxid)
if user != nil && user.IsLoggedIn() {
uuid = user.SignalID
}
}
role := signalmeow.GroupMember_DEFAULT
if powerLevels.GetUserLevel(mxid) >= 50 {
role = signalmeow.GroupMember_ADMINISTRATOR
}
if !participantDedup[uuid] {
participantDedup[uuid] = true
// invites should be added on signal and then auto-joined
// joined members that need to be pending-Members should have their signal invite auto-accepted
if membership == event.MembershipJoin || membership == event.MembershipInvite {
participants = append(participants, &signalmeow.GroupMember{
UserID: uuid,
Role: role,
})
} else if membership == event.MembershipBan {
bannedMembers = append(bannedMembers, &signalmeow.BannedMember{
UserID: uuid,
})
}
}
}
addFromInviteLinkAccess := signalmeow.AccessControl_UNSATISFIABLE
if joinRule == event.JoinRulePublic {
addFromInviteLinkAccess = signalmeow.AccessControl_ANY
} else if joinRule == event.JoinRuleKnock {
addFromInviteLinkAccess = signalmeow.AccessControl_ADMINISTRATOR
}
var inviteLinkPassword types.SerializedInviteLinkPassword
if addFromInviteLinkAccess != signalmeow.AccessControl_UNSATISFIABLE {
inviteLinkPassword = signalmeow.GenerateInviteLinkPassword()
}
membersAccess := signalmeow.AccessControl_MEMBER
if powerLevels.Invite() >= 50 {
membersAccess = signalmeow.AccessControl_ADMINISTRATOR
}
attributesAccess := signalmeow.AccessControl_MEMBER
if powerLevels.StateDefault() >= 50 {
attributesAccess = signalmeow.AccessControl_ADMINISTRATOR
}
announcementsOnly := false
if powerLevels.EventsDefault >= 50 {
announcementsOnly = true
}
ce.ZLog.Info().
Str("room_name", roomName).
Any("participants", participants).
Msg("Creating Signal group for Matrix room")
group, err := ce.User.Client.CreateGroupOnServer(ce.Ctx, &signalmeow.Group{
Title: roomName,
Description: roomTopic,
Members: participants,
AccessControl: &signalmeow.GroupAccessControl{
Members: membersAccess,
Attributes: attributesAccess,
AddFromInviteLink: addFromInviteLinkAccess,
},
InviteLinkPassword: &inviteLinkPassword,
BannedMembers: bannedMembers,
AnnouncementsOnly: announcementsOnly,
}, avatarBytes)
if err != nil {
ce.Reply("Failed to create group: %v", err)
return
}
gid := group.GroupIdentifier
ce.ZLog.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("group_id", gid)
})
portal := ce.User.GetPortalByChatID(gid.String())
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
if len(portal.MXID) != 0 {
ce.ZLog.Warn().Msg("Detected race condition in room creation")
// TODO race condition, clean up the old room
}
portal.MXID = ce.RoomID
portal.Name = roomName
portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
if !portal.Encrypted && ce.Bridge.Config.Bridge.Encryption.Default {
_, err = portal.MainIntent().SendStateEvent(ce.Ctx, portal.MXID, event.StateEncryption, "", portal.GetEncryptionEventContent())
if err != nil {
ce.ZLog.Err(err).Msg("Failed to enable encryption in room")
if errors.Is(err, mautrix.MForbidden) {
ce.Reply("I don't seem to have permission to enable encryption in this room.")
} else {
ce.Reply("Failed to enable encryption in room: %v", err)
}
}
portal.Encrypted = true
}
revision, err := ce.User.Client.UpdateGroup(ce.Ctx, &signalmeow.GroupChange{}, gid)
if err != nil {
ce.Reply("Failed to update Group")
return
}
portal.Revision = revision
portal.AvatarHash = avatarHash
portal.AvatarURL = avatarURL
portal.AvatarPath = group.AvatarPath
portal.AvatarSet = true
err = portal.Update(ce.Ctx)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to save portal after creating group")
}
portal.UpdateBridgeInfo(ce.Ctx)
ce.Reply("Successfully created Signal group %s", gid.String())
}
1 change: 1 addition & 0 deletions config/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type BridgeConfig struct {
PublicPortals bool `yaml:"public_portals"`
CaptionInMessage bool `yaml:"caption_in_message"`
FederateRooms bool `yaml:"federate_rooms"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`

DoublePuppetConfig bridgeconfig.DoublePuppetConfig `yaml:",inline"`

Expand Down
1 change: 1 addition & 0 deletions config/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Int, "bridge", "encryption", "rotation", "milliseconds")
helper.Copy(up.Int, "bridge", "encryption", "rotation", "messages")
helper.Copy(up.Bool, "bridge", "encryption", "rotation", "disable_device_change_key_rotation")
helper.Copy(up.Bool, "bridge", "bridge_matrix_leave")

helper.Copy(up.Str, "bridge", "provisioning", "prefix")
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
Expand Down
3 changes: 2 additions & 1 deletion example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ bridge:
# Disable rotating keys when a user's devices change?
# You should not enable this option unless you understand all the implications.
disable_device_change_key_rotation: false

# Should leaving the room on Matrix make the user leave on Signal?
bridge_matrix_leave: true
# Settings for provisioning API
provisioning:
# Prefix for the provisioning API paths.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f
golang.org/x/net v0.22.0
google.golang.org/protobuf v1.33.0
maunium.net/go/mautrix v0.18.0
maunium.net/go/mautrix v0.18.1-0.20240322180408-ade00e8603f9
nhooyr.io/websocket v1.8.10
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.18.0 h1:sNsApeSWB8x0hLjGcdmi5JqO6Tvp2PVkiSStz+Yas6k=
maunium.net/go/mautrix v0.18.0/go.mod h1:STwJZ+6CAeiEQs7fYCkd5aC12XR5DXANE6Swy/PBKGo=
maunium.net/go/mautrix v0.18.1-0.20240322180408-ade00e8603f9 h1:Xl741d8hAFdBPJPT/ydc6zTQM4R4L+5/d1X+AevLdXY=
maunium.net/go/mautrix v0.18.1-0.20240322180408-ade00e8603f9/go.mod h1:STwJZ+6CAeiEQs7fYCkd5aC12XR5DXANE6Swy/PBKGo=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
Loading

0 comments on commit 9c0b8ec

Please # to comment.