Skip to content

Commit

Permalink
Merge pull request #495 from maltee1/invite_command
Browse files Browse the repository at this point in the history
UUID->ServiceID & invite command
  • Loading branch information
tulir committed Apr 6, 2024
2 parents efc22ef + 9bf99eb commit 7599cd0
Show file tree
Hide file tree
Showing 5 changed files with 507 additions and 194 deletions.
221 changes: 218 additions & 3 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ func (br *SignalBridge) RegisterCommands() {
cmdInviteLink,
cmdResetInviteLink,
cmdCreate,
cmdInvite,
cmdListInvited,
cmdRevokeInvite,
)
}

Expand Down Expand Up @@ -284,6 +287,218 @@ func fnPM(ce *WrappedCommandEvent) {
}
}

var cmdInvite = &commands.FullHandler{
Func: wrapCommand(fnInvite),
Name: "invite",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Invite a user by phone number.",
Args: "<_international phone number_>",
},
RequiresLogin: true,
RequiresPortal: true,
}

func fnInvite(ce *WrappedCommandEvent) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `invite <international phone number>`")
return
}
number, err := strconv.ParseUint(numberCleaner.Replace(strings.Join(ce.Args, "")), 10, 64)
if err != nil {
ce.Reply("Failed to parse number")
return
}

user := ce.User
var aci, pni uuid.UUID
e164 := fmt.Sprintf("+%d", number)
var recipient *types.Recipient

if recipient, err = user.Client.ContactByE164(ce.Ctx, e164); err != nil {
ce.Reply("Error looking up number in local contact list: %v", err)
return
} else if recipient != nil && (recipient.ACI != uuid.Nil || recipient.PNI != uuid.Nil) {
// TODO maybe lookup PNI if there's only ACI and E164 stored?
aci = recipient.ACI
pni = recipient.PNI
} else if resp, err := user.Client.LookupPhone(ce.Ctx, number); err != nil {
ce.ZLog.Err(err).Uint64("e164", number).Msg("Failed to lookup number on server")
ce.Reply("Error looking up number on server: %v", err)
return
} else {
aci = resp[number].ACI
pni = resp[number].PNI
if aci == uuid.Nil && pni == uuid.Nil {
ce.Reply("+%d doesn't seem to be on Signal", number)
return
}
recipient, err = user.Client.Store.RecipientStore.UpdateRecipientE164(ce.Ctx, aci, pni, e164)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to save recipient entry after looking up phone")
}
aci, pni = recipient.ACI, recipient.PNI
}
ce.ZLog.Debug().
Uint64("e164", number).
Stringer("aci", aci).
Stringer("pni", pni).
Msg("Found Invite target user")

var groupChange signalmeow.GroupChange
if aci != uuid.Nil {
groupChange.AddMembers = []*signalmeow.AddMember{
{
GroupMember: signalmeow.GroupMember{
ACI: aci,
Role: signalmeow.GroupMember_DEFAULT,
},
},
}
} else {
groupChange.AddPendingMembers = []*signalmeow.PendingMember{
{
ServiceID: libsignalgo.NewPNIServiceID(pni),
AddedByUserID: ce.User.SignalID,
Role: signalmeow.GroupMember_DEFAULT,
},
}
}
revision, err := ce.User.Client.UpdateGroup(ce.Ctx, &groupChange, ce.Portal.GroupID())
if err != nil {
ce.Reply("Failed to update group: %w", err)
return
}
ce.Portal.Revision = revision
if aci != uuid.Nil {
group, err := ce.User.Client.RetrieveGroupByID(ce.Ctx, ce.Portal.GroupID(), revision)
if err != nil {
ce.Reply("Failed to fetch group after invite: %w", err)
}
ce.Portal.SyncParticipants(ce.Ctx, user, group)
ce.Portal.Update(ce.Ctx)
return
}
ce.Portal.Update(ce.Ctx)
ce.Reply("Invited " + e164)
}

var cmdListInvited = &commands.FullHandler{
Func: wrapCommand(fnListInvited),
Name: "list-invited",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "list pending invites",
Args: "<_international phone number_>",
},
RequiresLogin: true,
RequiresPortal: true,
}

func fnListInvited(ce *WrappedCommandEvent) {
group, err := ce.User.Client.RetrieveGroupByID(ce.Ctx, ce.Portal.GroupID(), ce.Portal.Revision)
if err != nil {
ce.Reply("Failed to fetch group info: %w", err)
return
}
var memberList []string
for _, pendingMember := range group.PendingMembers {
recipientString, err := pendingMemberToString(ce.Ctx, ce.User, pendingMember)
if err != nil {
ce.Reply("Failed to fetch recipient for %s: %w", pendingMember.ServiceID, err)
continue
}
memberList = append(memberList, recipientString)
}
if len(memberList) == 0 {
ce.Reply("No pending Invites")
} else {
ce.Reply(strings.Join(memberList, "\n"))
}
}

func pendingMemberToString(ctx context.Context, user *User, pendingMember *signalmeow.PendingMember) (string, error) {
var pni, aci uuid.UUID
if pendingMember.ServiceID.Type == libsignalgo.ServiceIDTypeACI {
aci = pendingMember.ServiceID.UUID
} else {
pni = pendingMember.ServiceID.UUID
}
recipient, err := user.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
if err != nil {
return "", err
}
if recipient.E164 != "" {
return recipient.E164, nil
} else {
return "Unidentified User", nil
}
}

var cmdRevokeInvite = &commands.FullHandler{
Func: wrapCommand(fnRevokeInvite),
Name: "revoke-invite",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Revoke an invite by phone number.",
Args: "<_international phone number_>",
},
RequiresLogin: true,
RequiresPortal: true,
}

func fnRevokeInvite(ce *WrappedCommandEvent) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `RevokeInvite <international phone number>`")
return
}
e164 := "+" + numberCleaner.Replace(strings.Join(ce.Args, ""))

user := ce.User
var serviceID libsignalgo.ServiceID
group, err := ce.User.Client.RetrieveGroupByID(ce.Ctx, ce.Portal.GroupID(), ce.Portal.Revision)
if err != nil {
ce.Reply("Failed to fetch group info: %w", err)
return
}
var pni, aci uuid.UUID
for _, pendingMember := range group.PendingMembers {
if pendingMember.ServiceID.Type == libsignalgo.ServiceIDTypeACI {
aci = pendingMember.ServiceID.UUID
} else {
pni = pendingMember.ServiceID.UUID
}
recipient, err := user.Client.Store.RecipientStore.LoadAndUpdateRecipient(ce.Ctx, aci, pni, nil)
if err != nil {
continue
}
if recipient.E164 == e164 {
serviceID = pendingMember.ServiceID
break
}
}
if serviceID.UUID == uuid.Nil {
ce.Reply("User not in Group")
return
}
var groupChange signalmeow.GroupChange
groupChange.DeletePendingMembers = []*libsignalgo.ServiceID{&serviceID}
revision, err := ce.User.Client.UpdateGroup(ce.Ctx, &groupChange, ce.Portal.GroupID())
if err != nil {
ce.Reply("Failed to update group: %w", err)
return
}
if aci != uuid.Nil {
target := ce.Bridge.GetPuppetBySignalID(aci)
if target != nil {
ce.Bot.SendCustomMembershipEvent(ce.Ctx, ce.Portal.MXID, target.IntentFor(ce.Portal).UserID, event.MembershipLeave, "removed by "+user.MXID.String())
}
}
ce.Portal.Revision = revision
ce.Portal.Update(ce.Ctx)
ce.Reply("Revoked the invitation for " + e164)
}

var cmdResolvePhone = &commands.FullHandler{
Func: wrapCommand(fnResolvePhone),
Name: "resolve-phone",
Expand Down Expand Up @@ -822,12 +1037,12 @@ func fnCreate(ce *WrappedCommandEvent) {
// 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,
ACI: uuid,
Role: role,
})
} else if membership == event.MembershipBan {
bannedMembers = append(bannedMembers, &signalmeow.BannedMember{
UserID: uuid,
ServiceID: libsignalgo.NewACIServiceID(uuid),
})
}
}
Expand Down
26 changes: 10 additions & 16 deletions pkg/libsignalgo/groupsecretparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ package libsignalgo
import "C"
import (
"crypto/rand"
"fmt"
"runtime"
"unsafe"

Expand Down Expand Up @@ -139,41 +138,36 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
return CopySignalOwnedBufferToBytes(ciphertext), nil
}

func (gsp *GroupSecretParams) DecryptUUID(ciphertextUUID UUIDCiphertext) (uuid.UUID, error) {
// TODO this should probably be DecryptServiceID

func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) {
u := C.SignalServiceIdFixedWidthBinaryBytes{}
signalFfiError := C.signal_group_secret_params_decrypt_service_id(
&u,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextUUID)),
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextServiceID)),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextUUID)
runtime.KeepAlive(ciphertextServiceID)
if signalFfiError != nil {
return uuid.Nil, wrapError(signalFfiError)
return EmptyServiceID, wrapError(signalFfiError)
}

serviceID := ServiceIDFromCFixedBytes(&u)
if serviceID.Type != ServiceIDTypeACI {
return uuid.Nil, fmt.Errorf("unexpected service ID type %d", serviceID.Type)
}
return serviceID.UUID, nil
return serviceID, nil
}

func (gsp *GroupSecretParams) EncryptUUID(uuid uuid.UUID) (*UUIDCiphertext, error) {
var cipherTextUUID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
func (gsp *GroupSecretParams) EncryptServiceID(serviceID ServiceID) (*UUIDCiphertext, error) {
var cipherTextServiceID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_encrypt_service_id(
&cipherTextUUID,
&cipherTextServiceID,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
NewACIServiceID(uuid).CFixedBytes(),
serviceID.CFixedBytes(),
)
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result UUIDCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextUUID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextServiceID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
return &result, nil
}

Expand Down

0 comments on commit 7599cd0

Please sign in to comment.