diff --git a/commands.go b/commands.go index 92fcc0a..3fb85a5 100644 --- a/commands.go +++ b/commands.go @@ -900,7 +900,7 @@ func (handler *CommandHandler) CommandInvite(ce *CommandEvent) { userNumbers := strings.Split(ce.Args[1], ",") - if strings.HasSuffix(conversationId, whatsappExt.NewUserSuffix) { + if strings.HasSuffix(conversationId, skypeExt.NewUserSuffix) { ce.Reply("**Usage:** `invite ,...`") return } diff --git a/matrix.go b/matrix.go index 6dbbe77..bc5795e 100644 --- a/matrix.go +++ b/matrix.go @@ -3,6 +3,8 @@ package main import ( "fmt" skype "github.com/kelaresg/go-skypeapi" + "github.com/kelaresg/matrix-skype/database" + "maunium.net/go/mautrix" "strconv" "strings" @@ -52,6 +54,28 @@ func (mx *MatrixHandler) HandleEncryption(evt *event.Event) { } } +func (mx *MatrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { + resp, err := intent.JoinRoomByID(evt.RoomID) + if err != nil { + mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err) + return nil + } + + members, err := intent.JoinedMembers(resp.RoomID) + if err != nil { + mx.log.Debugfln("Failed to get members in room %s after accepting invite from %s as %s: %v", resp.RoomID, evt.Sender, intent.UserID, err) + _, _ = intent.LeaveRoom(resp.RoomID) + return nil + } + + if len(members.Joined) < 2 { + mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender, "as", intent.UserID) + _, _ = intent.LeaveRoom(resp.RoomID) + return nil + } + return members +} + func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { intent := mx.as.BotIntent() @@ -114,6 +138,102 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { } } +func (mx *MatrixHandler) handlePrivatePortal(roomID id.RoomID, inviter *User, puppet *Puppet, key database.PortalKey) { + portal := mx.bridge.GetPortalByJID(key) + + if len(portal.MXID) == 0 { + mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) + return + } + + err := portal.MainIntent().EnsureInvited(portal.MXID, inviter.MXID) + if err != nil { + mx.log.Warnfln("Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.JID, err) + mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) + return + } + intent := puppet.DefaultIntent() + _, _ = intent.SendNotice(roomID, "You already have a private chat portal with me at %s") + mx.log.Debugln("Leaving private chat room", roomID, "as", puppet.MXID, "after accepting invite from", inviter.MXID, "as we already have chat with the user") + _, _ = intent.LeaveRoom(roomID) +} + + +func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { + portal.MXID = roomID + portal.Topic = "WhatsApp private chat" + _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) + if portal.bridge.Config.Bridge.PrivateChatPortalMeta { + portal.Name = puppet.Displayname + portal.AvatarURL = puppet.AvatarURL + portal.Avatar = puppet.Avatar + _, _ = portal.MainIntent().SetRoomName(portal.MXID, portal.Name) + _, _ = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) + } else { + portal.Name = "" + } + portal.log.Infoln("Created private chat portal in %s after invite from", roomID, inviter.MXID) + intent := puppet.DefaultIntent() + + if mx.bridge.Config.Bridge.Encryption.Default { + _, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: mx.bridge.Bot.UserID}) + if err != nil { + portal.log.Warnln("Failed to invite bridge bot to enable e2be:", err) + } + err = mx.bridge.Bot.EnsureJoined(roomID) + if err != nil { + portal.log.Warnln("Failed to join as bridge bot to enable e2be:", err) + } + _, err = intent.SendStateEvent(roomID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}) + if err != nil { + portal.log.Warnln("Failed to enable e2be:", err) + } + mx.as.StateStore.SetMembership(roomID, inviter.MXID, event.MembershipJoin) + mx.as.StateStore.SetMembership(roomID, puppet.MXID, event.MembershipJoin) + mx.as.StateStore.SetMembership(roomID, mx.bridge.Bot.UserID, event.MembershipJoin) + portal.Encrypted = true + } + portal.Update() + portal.UpdateBridgeInfo() + _, _ = intent.SendNotice(roomID, "Private chat portal created") + + err := portal.FillInitialHistory(inviter) + if err != nil { + portal.log.Errorln("Failed to fill history:", err) + } + + inviter.addPortalToCommunity(portal) + inviter.addPuppetToCommunity(puppet) +} + +func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) { + intent := puppet.DefaultIntent() + members := mx.joinAndCheckMembers(evt, intent) + if members == nil { + return + } + var hasBridgeBot, hasOtherUsers bool + for mxid, _ := range members.Joined { + if mxid == intent.UserID || mxid == inviter.MXID { + continue + } else if mxid == mx.bridge.Bot.UserID { + hasBridgeBot = true + } else { + hasOtherUsers = true + } + } + if !hasBridgeBot && !hasOtherUsers { + key := database.NewPortalKey(puppet.JID, inviter.JID) + mx.handlePrivatePortal(evt.RoomID, inviter, puppet, key) + } else if !hasBridgeBot { + mx.log.Debugln("Leaving multi-user room", evt.RoomID, "as", puppet.MXID, "after accepting invite from", evt.Sender) + _, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.") + _, _ = intent.LeaveRoom(evt.RoomID) + } else { + _, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a WhatsApp group.") + } +} + func (mx *MatrixHandler) HandleMembership(evt *event.Event) { if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { return @@ -126,10 +246,6 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { content := evt.Content.AsMember() if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() { mx.HandleBotInvite(evt) - } - - portal := mx.bridge.GetPortalByMXID(evt.RoomID) - if portal == nil { return } @@ -138,6 +254,15 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { return } + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal == nil { + puppet := mx.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + if content.Membership == event.MembershipInvite && puppet != nil { + mx.HandlePuppetInvite(evt, user, puppet) + } + return + } + isSelf := id.UserID(evt.GetStateKey()) == evt.Sender if content.Membership == event.MembershipLeave { if id.UserID(evt.GetStateKey()) == evt.Sender { if evt.Unsigned.PrevContent != nil { @@ -152,6 +277,8 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { } else { portal.HandleMatrixKick(user, evt) } + } else if content.Membership == event.MembershipInvite && !isSelf { + portal.HandleMatrixInvite(user, evt) } } diff --git a/portal.go b/portal.go index 6c3337f..b79503f 100644 --- a/portal.go +++ b/portal.go @@ -1015,6 +1015,43 @@ var ( StateHalfShotBridgeInfo = event.Type{Type: "uk.half-shot.bridge", Class: event.StateEventType} ) +func (portal *Portal) getBridgeInfo() (string, BridgeInfoContent) { + bridgeInfo := BridgeInfoContent{ + BridgeBot: portal.bridge.Bot.UserID, + Creator: portal.MainIntent().UserID, + Protocol: BridgeInfoSection{ + ID: "skype", + DisplayName: "Skype", + AvatarURL: id.ContentURIString(portal.bridge.Config.AppService.Bot.Avatar), + ExternalURL: "https://www.skype.com/", + }, + Channel: BridgeInfoSection{ + ID: portal.Key.JID, + DisplayName: portal.Name, + AvatarURL: portal.AvatarURL.CUString(), + }, + } + bridgeInfoStateKey := fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) + return bridgeInfoStateKey, bridgeInfo +} + +func (portal *Portal) UpdateBridgeInfo() { + if len(portal.MXID) == 0 { + portal.log.Debugln("Not updating bridge info: no Matrix room created") + return + } + portal.log.Debugln("Updating bridge info...") + stateKey, content := portal.getBridgeInfo() + _, err := portal.MainIntent().SendStateEvent(portal.MXID, StateBridgeInfo, stateKey, content) + if err != nil { + portal.log.Warnln("Failed to update m.bridge:", err) + } + _, err = portal.MainIntent().SendStateEvent(portal.MXID, StateHalfShotBridgeInfo, stateKey, content) + if err != nil { + portal.log.Warnln("Failed to update uk.half-shot.bridge:", err) + } +} + func (portal *Portal) CreateMatrixRoom(user *User) error { portal.roomCreateLock.Lock() defer portal.roomCreateLock.Unlock() @@ -1024,9 +1061,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { intent := portal.MainIntent() if err := intent.EnsureRegistered(); err != nil { - fmt.Println() - fmt.Println("CreateMatrixRoom0: ", err) - fmt.Println() return err } @@ -1035,9 +1069,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { var metadata *skypeExt.GroupInfo if portal.IsPrivateChat() { - fmt.Println() - fmt.Println("CreateMatrixRoom1: ") - fmt.Println() puppet := portal.bridge.GetPuppetByJID(portal.Key.JID+skypeExt.NewUserSuffix) if portal.bridge.Config.Bridge.PrivateChatPortalMeta { portal.Name = puppet.Displayname @@ -1048,15 +1079,9 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { } portal.Topic = "skype private chat" } else if portal.IsStatusBroadcastRoom() { - fmt.Println() - fmt.Println("CreateMatrixRoom2: ") - fmt.Println() portal.Name = "skype Status Broadcast" portal.Topic = "skype status updates from your contacts" } else { - fmt.Println() - fmt.Println("CreateMatrixRoom3: ") - fmt.Println() var err error metadata, err = user.Conn.GetGroupMetaData(portal.Key.JID) if err == nil { @@ -1068,7 +1093,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { if key == user.JID { continue } - fmt.Println("CreateMatrixRoom3.1: ", key) if contact, ok := user.Conn.Store.Contacts[key]; ok { if len(portalName) > 0 { portalName = portalName + ", " + contact.DisplayName @@ -2355,6 +2379,35 @@ func (portal *Portal) Delete() { portal.bridge.portalsLock.Unlock() } +func (portal *Portal) GetMatrixUsers() ([]id.UserID, error) { + members, err := portal.MainIntent().JoinedMembers(portal.MXID) + if err != nil { + return nil, errors.Wrap(err, "failed to get member list") + } + var users []id.UserID + for userID := range members.Joined { + _, isPuppet := portal.bridge.ParsePuppetMXID(userID) + if !isPuppet && userID != portal.bridge.Bot.UserID { + users = append(users, userID) + } + } + return users, nil +} + +func (portal *Portal) CleanupIfEmpty() { + users, err := portal.GetMatrixUsers() + if err != nil { + portal.log.Errorfln("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err) + return + } + + if len(users) == 0 { + portal.log.Infoln("Room seems to be empty, cleaning up...") + portal.Delete() + portal.Cleanup(false) + } +} + func (portal *Portal) Cleanup(puppetsOnly bool) { if len(portal.MXID) == 0 { return @@ -2401,9 +2454,45 @@ func (portal *Portal) HandleMatrixLeave(sender *User) { portal.Delete() portal.Cleanup(false) return + } else { + // TODO should we somehow deduplicate this call if this leave was sent by the bridge? + err := sender.Conn.HandleGroupLeave(portal.Key.JID) + if err != nil { + portal.log.Errorfln("Failed to leave group as %s: %v", sender.MXID, err) + return + } + //portal.log.Infoln("Leave response:", <-resp) + portal.CleanupIfEmpty() } } -func (portal *Portal) HandleMatrixKick(sender *User, event *event.Event) { - // TODO +func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) { + number, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) + puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + fmt.Println("HandleMatrixKick", puppet) + if puppet != nil { + number = strings.Replace(number, skypeExt.NewUserSuffix, "", 1) + err := sender.Conn.HandleGroupKick(portal.Key.JID, []string{number}) + if err != nil { + portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err) + return + } + //portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp) + } +} + +func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) { + number, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) + puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + fmt.Println("HandleMatrixInvite", puppet) + if puppet != nil { + number = strings.Replace(number, "8:", "", 1) + number = strings.Replace(number, skypeExt.NewUserSuffix, "", 1) + err := sender.Conn.HandleGroupInvite(portal.Key.JID, []string{number}) + if err != nil { + portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err) + return + } + //portal.log.Infoln("Add %s response: %s", puppet.JID, <-resp) + } } diff --git a/puppet.go b/puppet.go index 02623ba..bcf5cd7 100644 --- a/puppet.go +++ b/puppet.go @@ -19,7 +19,6 @@ import ( ) func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.SkypeID, bool) { - fmt.Println("ParsePuppetMXID: ", bridge.Config.Bridge.FormatUsername("(.*)")) userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", bridge.Config.Bridge.FormatUsername("(.*)"), bridge.Config.Homeserver.Domain)) @@ -31,8 +30,15 @@ func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.SkypeID, bool) { if match == nil || len(match) != 2 { return "", false } - - jid := types.SkypeID(match[1] + whatsappExt.NewUserSuffix) + realId := match[1] + cond1 := "8-live-" + cond2 := "8-" + if strings.HasPrefix(realId, cond1) { + realId = strings.Replace(realId, cond1, "8:live:", 1) + } else if strings.HasPrefix(realId, cond2){ + realId = strings.Replace(realId, cond2, "8:", 1) + } + jid := types.SkypeID(realId + skypeExt.NewUserSuffix) return jid, true } diff --git a/skype-ext/skype.go b/skype-ext/skype.go index 7b4be96..2b915d3 100644 --- a/skype-ext/skype.go +++ b/skype-ext/skype.go @@ -275,9 +275,10 @@ func (ext *ExtendedConn) HandleGroupShare(groupJid string) (err error, link stri func (ext *ExtendedConn) HandleGroupKick(groupJid string, numbers[]string) (err error) { for _, number := range numbers{ - err = ext.Conn.RemoveMember(groupJid, number) - if err != nil { - fmt.Printf("%s Handle kick err", err) + if err == nil { + err = ext.Conn.RemoveMember(groupJid, number) + } else { + _ = ext.Conn.RemoveMember(groupJid, number) } } return