diff --git a/config/bridge.go b/config/bridge.go index 521c56d6..5adc6351 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -48,6 +48,7 @@ type BridgeConfig struct { MessageErrorNotices bool `yaml:"message_error_notices"` SyncDirectChatList bool `yaml:"sync_direct_chat_list"` ResendBridgeInfo bool `yaml:"resend_bridge_info"` + PublicPortals bool `yaml:"public_portals"` CaptionInMessage bool `yaml:"caption_in_message"` FederateRooms bool `yaml:"federate_rooms"` diff --git a/example-config.yaml b/example-config.yaml index 1a208a4c..3d1cd453 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -131,6 +131,9 @@ bridge: # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. # This field will automatically be changed back to false after it, except if the config file is not writable. resend_bridge_info: false + # Whether or not to make portals of groups that don't need approval of an admin to join by invite + # link publicly joinable on Matrix. + public_portals: false # Send captions in the same message as images. This will send data compatible with both MSC2530. # This is currently not supported in most clients. caption_in_message: false diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 31304509..5d98f7f1 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -140,6 +140,7 @@ type BannedMember struct { type GroupChange struct { groupMasterKey types.SerializedGroupMasterKey + Revision uint32 AddMembers []*AddMember DeleteMembers []*uuid.UUID ModifyMemberRoles []*RoleMember @@ -150,6 +151,7 @@ type GroupChange struct { ModifyTitle *string ModifyAvatar *string ModifyDisappearingMessagesTimer *uint32 + ModifyAttributesAccess *AccessControl ModifyMemberAccess *AccessControl ModifyAddFromInviteLinkAccess *AccessControl AddRequestingMembers []*RequestingMember @@ -594,7 +596,7 @@ type GroupCache struct { activeCalls map[types.GroupIdentifier]string } -func DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) { +func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) { masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey) groupMasterKey := masterKeyFromBytes(masterKeyBytes) log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger() @@ -629,6 +631,8 @@ func DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContext decryptedGroupChange := &GroupChange{groupMasterKey: groupMasterKey} + decryptedGroupChange.Revision = encryptedActions.Revision + if encryptedActions.ModifyTitle != nil { titleBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyTitle.Title) if err != nil { @@ -675,6 +679,11 @@ func DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContext }, JoinFromInviteLink: addMember.JoinFromInviteLink, }) + err = cli.Store.ProfileKeyStore.StoreProfileKey(ctx, userID, *profileKey) + if err != nil { + log.Err(err).Msg("failed to store profile key") + return nil, err + } } for _, deleteMember := range encryptedActions.DeleteMembers { @@ -702,5 +711,217 @@ func DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContext Role: GroupMemberRole(modifyMemberRole.Role), }) } + + for _, modifyProfileKey := range encryptedActions.ModifyMemberProfileKeys { + if modifyProfileKey == nil { + continue + } + encryptedUserID := libsignalgo.UUIDCiphertext(modifyProfileKey.UserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + encryptedProfileKey := libsignalgo.ProfileKeyCiphertext(modifyProfileKey.ProfileKey) + profileKey, err := groupSecretParams.DecryptProfileKey(encryptedProfileKey, userID) + if err != nil { + log.Err(err).Msg("DecryptProfileKey ProfileKey error") + return nil, err + } + decryptedGroupChange.ModifyMemberProfileKeys = append(decryptedGroupChange.ModifyMemberProfileKeys, &ProfileKeyMember{ + UserID: userID, + ProfileKey: *profileKey, + }) + cli.Store.ProfileKeyStore.StoreProfileKey(ctx, userID, *profileKey) + if err != nil { + log.Err(err).Msg("failed to store profile key") + return nil, err + } + } + + for _, addPendingMember := range encryptedActions.AddPendingMembers { + if addPendingMember == nil { + continue + } + pendingMember := addPendingMember.Added + encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + encryptedProfileKey := libsignalgo.ProfileKeyCiphertext(pendingMember.Member.ProfileKey) + profileKey, err := groupSecretParams.DecryptProfileKey(encryptedProfileKey, userID) + if err != nil { + log.Err(err).Msg("DecryptProfileKey ProfileKey error") + return nil, err + } + encryptedAddedByUserID := pendingMember.AddedByUserId + addedByUserId, err := groupSecretParams.DecryptUUID(libsignalgo.UUIDCiphertext(encryptedAddedByUserID)) + if err != nil { + log.Err(err).Msg("DecryptUUID addedByUserId error") + return nil, err + } + decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, &PendingMember{ + GroupMember: GroupMember{ + UserID: userID, + ProfileKey: *profileKey, + Role: GroupMemberRole(pendingMember.Member.Role), + JoinedAtRevision: pendingMember.Member.JoinedAtRevision, + }, + AddedByUserID: addedByUserId, + Timestamp: addPendingMember.Added.Timestamp, + }) + cli.Store.ProfileKeyStore.StoreProfileKey(ctx, userID, *profileKey) + if err != nil { + log.Err(err).Msg("failed to store profile key") + return nil, err + } + } + + for _, deletePendingMember := range encryptedActions.DeletePendingMembers { + if deletePendingMember == nil { + continue + } + encryptedUserID := libsignalgo.UUIDCiphertext(deletePendingMember.DeletedUserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID) + } + + for _, promotePendingMember := range encryptedActions.PromotePendingMembers { + if promotePendingMember == nil { + continue + } + encryptedUserID := libsignalgo.UUIDCiphertext(promotePendingMember.UserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + encryptedProfileKey := libsignalgo.ProfileKeyCiphertext(promotePendingMember.ProfileKey) + profileKey, err := groupSecretParams.DecryptProfileKey(encryptedProfileKey, userID) + if err != nil { + log.Err(err).Msg("DecryptProfileKey ProfileKey error") + return nil, err + } + decryptedGroupChange.PromotePendingMembers = append(decryptedGroupChange.PromotePendingMembers, &ProfileKeyMember{ + UserID: userID, + ProfileKey: *profileKey, + }) + cli.Store.ProfileKeyStore.StoreProfileKey(ctx, userID, *profileKey) + if err != nil { + log.Err(err).Msg("failed to store profile key") + return nil, err + } + } + + for _, addRequestingMember := range encryptedActions.AddRequestingMembers { + if addRequestingMember == nil { + continue + } + requestingMember := addRequestingMember.Added + encryptedUserID := libsignalgo.UUIDCiphertext(requestingMember.UserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + encryptedProfileKey := libsignalgo.ProfileKeyCiphertext(requestingMember.ProfileKey) + profileKey, err := groupSecretParams.DecryptProfileKey(encryptedProfileKey, userID) + if err != nil { + log.Err(err).Msg("DecryptProfileKey ProfileKey error") + return nil, err + } + decryptedGroupChange.AddRequestingMembers = append(decryptedGroupChange.AddRequestingMembers, &RequestingMember{ + UserID: userID, + ProfileKey: *profileKey, + Timestamp: addRequestingMember.Added.Timestamp, + }) + cli.Store.ProfileKeyStore.StoreProfileKey(ctx, userID, *profileKey) + if err != nil { + log.Err(err).Msg("failed to store profile key") + return nil, err + } + } + + for _, deleteRequestingMember := range encryptedActions.DeleteRequestingMembers { + if deleteRequestingMember == nil { + continue + } + encryptedUserID := libsignalgo.UUIDCiphertext(deleteRequestingMember.DeletedUserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &userID) + } + + for _, promoteRequestingMember := range encryptedActions.PromoteRequestingMembers { + if promoteRequestingMember == nil { + continue + } + encryptedUserID := libsignalgo.UUIDCiphertext(promoteRequestingMember.UserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + decryptedGroupChange.PromoteRequestingMembers = append(decryptedGroupChange.PromoteRequestingMembers, &RoleMember{ + UserID: userID, + Role: GroupMemberRole(promoteRequestingMember.Role), + }) + } + + for _, addBannedMember := range encryptedActions.AddBannedMembers { + if addBannedMember == nil { + continue + } + bannedMember := addBannedMember.Added + encryptedUserID := libsignalgo.UUIDCiphertext(bannedMember.UserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + decryptedGroupChange.AddBannedMembers = append(decryptedGroupChange.AddBannedMembers, &BannedMember{ + UserID: userID, + Timestamp: addBannedMember.Added.Timestamp, + }) + } + + for _, deleteBannedMember := range encryptedActions.DeleteBannedMembers { + if deleteBannedMember == nil { + continue + } + encryptedUserID := libsignalgo.UUIDCiphertext(deleteBannedMember.DeletedUserId) + userID, err := groupSecretParams.DecryptUUID(encryptedUserID) + if err != nil { + log.Err(err).Msg("DecryptUUID UserId error") + return nil, err + } + decryptedGroupChange.DeleteBannedMembers = append(decryptedGroupChange.DeleteBannedMembers, &userID) + } + + if encryptedActions.ModifyAttributesAccess != nil { + decryptedGroupChange.ModifyAttributesAccess = (*AccessControl)(&encryptedActions.ModifyAttributesAccess.AttributesAccess) + } + + if encryptedActions.ModifyMemberAccess != nil { + decryptedGroupChange.ModifyMemberAccess = (*AccessControl)(&encryptedActions.ModifyMemberAccess.MembersAccess) + } + + if encryptedActions.ModifyAddFromInviteLinkAccess != nil { + decryptedGroupChange.ModifyAddFromInviteLinkAccess = (*AccessControl)(&encryptedActions.ModifyAddFromInviteLinkAccess.AddFromInviteLinkAccess) + } + + if encryptedActions.ModifyAnnouncementsOnly != nil { + decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly + } + return decryptedGroupChange, nil } diff --git a/portal.go b/portal.go index abcc15bf..b87b682f 100644 --- a/portal.go +++ b/portal.go @@ -899,6 +899,12 @@ func (portal *Portal) handleSignalDataMessage(source *User, sender *Puppet, msg } } +type groupChangeMemberAction struct { + UserId uuid.UUID + MemberEvent event.MemberEventContent + ensureJoined bool +} + func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, groupMeta *signalpb.GroupContextV2, ts uint64) { log := portal.log.With(). Str("action", "handle signal group change"). @@ -907,7 +913,11 @@ func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, grou Uint32("new_revision", groupMeta.GetRevision()). Logger() ctx := log.WithContext(context.TODO()) - groupChange, err := signalmeow.DecryptGroupChange(ctx, groupMeta) + groupChange, err := source.Client.DecryptGroupChange(ctx, groupMeta) + if groupChange.Revision <= portal.Revision { + return + } + portal.Revision = groupChange.Revision if err != nil { log.Err(err).Msg("Handling GroupChange failed") return @@ -922,6 +932,24 @@ func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, grou portal.updateAvatarWithInfo(ctx, source, groupChange, sender) } intent := sender.IntentFor(portal) + for _, deleteBannedMember := range groupChange.DeleteBannedMembers { + puppet := portal.bridge.GetPuppetBySignalID(*deleteBannedMember) + if puppet == nil { + log.Warn().Stringer("signal_user_id", deleteBannedMember).Msg("Couldn't get puppet for deleted member") + continue + } + reqUnbanUser := mautrix.ReqUnbanUser{ + UserID: puppet.IntentFor(portal).UserID, + } + _, err := intent.UnbanUser(ctx, portal.MXID, &reqUnbanUser) + if errors.Is(err, mautrix.MForbidden) { + reqUnbanUser.Reason = fmt.Sprintf("Unbanned by %s", sender.GetDisplayname()) + _, err = portal.MainIntent().UnbanUser(ctx, portal.MXID, &reqUnbanUser) + } + if err != nil { + log.Err(err).Msg("Unbanning User failed") + } + } for _, addMember := range groupChange.AddMembers { puppet := portal.bridge.GetPuppetBySignalID(addMember.UserID) if puppet == nil { @@ -948,7 +976,8 @@ func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, grou log.Err(err).Msg("inviting User failed") } } - for _, deleteMember := range groupChange.DeleteMembers { + + for _, deleteMember := range append(groupChange.DeleteMembers, append(groupChange.DeletePendingMembers, groupChange.DeleteRequestingMembers...)...) { puppet := portal.bridge.GetPuppetBySignalID(*deleteMember) if puppet == nil { log.Warn().Stringer("signal_user_id", deleteMember).Msg("Couldn't get puppet for deleted member") @@ -986,6 +1015,99 @@ func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, grou log.Err(err).Msg("Changing power level failed") } } + for _, addPendingMember := range groupChange.AddPendingMembers { + puppet := portal.bridge.GetPuppetBySignalID(addPendingMember.UserID) + if puppet == nil { + log.Warn().Stringer("signal_user_id", addPendingMember.UserID).Msg("Couldn't get puppet for added pending member") + continue + } + reqInviteUser := mautrix.ReqInviteUser{ + Reason: "", + UserID: puppet.IntentFor(portal).UserID, + } + _, err = intent.InviteUser(ctx, portal.MXID, &reqInviteUser) + if errors.Is(err, mautrix.MForbidden) { + reqInviteUser.Reason = fmt.Sprintf("Added by %s", sender.GetDisplayname()) + _, err = portal.MainIntent().InviteUser(ctx, portal.MXID, &reqInviteUser) + } + if err != nil { + log.Err(err).Msg("inviting User failed") + } + } + for _, addBannedMember := range groupChange.AddBannedMembers { + puppet := portal.bridge.GetPuppetBySignalID(addBannedMember.UserID) + if puppet == nil { + log.Warn().Stringer("signal_user_id", addBannedMember.UserID).Msg("Couldn't get puppet for deleted member") + continue + } + reqBanUser := mautrix.ReqBanUser{ + UserID: puppet.IntentFor(portal).UserID, + } + _, err := intent.BanUser(ctx, portal.MXID, &reqBanUser) + if errors.Is(err, mautrix.MForbidden) { + reqBanUser.Reason = fmt.Sprintf("Banned by %s", sender.GetDisplayname()) + _, err = portal.MainIntent().BanUser(ctx, portal.MXID, &reqBanUser) + } + if err != nil { + log.Err(err).Msg("banning User failed") + } + } + for _, promoteRequestingMember := range groupChange.PromoteRequestingMembers { + puppet := portal.bridge.GetPuppetBySignalID(promoteRequestingMember.UserID) + if puppet == nil { + log.Warn().Stringer("signal_user_id", promoteRequestingMember.UserID).Msg("Couldn't get puppet for promoted requesting member") + continue + } + puppet.IntentFor(portal).EnsureJoined(ctx, portal.MXID) + } + // TODO: addRequestingMembers (knocks not yet it mautrix-go) + if groupChange.ModifyAttributesAccess != nil || groupChange.ModifyAnnouncementsOnly != nil || groupChange.ModifyMemberAccess != nil { + levels, err := intent.PowerLevels(ctx, portal.MXID) + if err != nil { + log.Err(err).Msg("Couldn't get power levels") + + } else { + if groupChange.ModifyAnnouncementsOnly != nil { + level := 0 + if *groupChange.ModifyAnnouncementsOnly { + level = 50 + } + levels.EventsDefault = level + } + if groupChange.ModifyAttributesAccess != nil { + level := 0 + if *groupChange.ModifyAttributesAccess == signalmeow.AccessControl_ADMINISTRATOR { + level = 50 + } + levels.EnsureEventLevel(event.StateRoomName, level) + levels.EnsureEventLevel(event.StateTopic, level) + levels.EnsureEventLevel(event.StateRoomAvatar, level) + } + if groupChange.ModifyMemberAccess != nil { + level := 0 + if *groupChange.ModifyMemberAccess == signalmeow.AccessControl_ADMINISTRATOR { + level = 50 + } + levels.InvitePtr = &level + } + _, err = intent.SetPowerLevels(ctx, portal.MXID, levels) + if errors.Is(err, mautrix.MForbidden) { + _, err = portal.MainIntent().SetPowerLevels(ctx, portal.MXID, levels) + } + if err != nil { + log.Err(err).Msg("Couldn't set power levels") + } + } + } + // TODO: join rules (not in mautrix-go) + // if groupChange.ModifyAddFromInviteLinkAccess != nil { + // joinRule := event.JoinRuleInvite + // if *groupChange.ModifyAddFromInviteLinkAccess == signalmeow.AccessControl_ADMINISTRATOR { + // joinRule = event.JoinRuleKnock + // } else if *groupChange.ModifyAddFromInviteLinkAccess == signalmeow.AccessControl_ANY && portal.bridge.Config.Bridge.PublicPortals { + // joinRule = event.JoinRulePublic + // } + // } } func (portal *Portal) handleSignalReaction(sender *Puppet, react *signalpb.DataMessage_Reaction, ts uint64) {