From 3e75ffaf05eff96f02728313cee13a619166e3ae Mon Sep 17 00:00:00 2001 From: Karmanyaah Malhotra Date: Sat, 27 Feb 2021 19:57:42 -0500 Subject: [PATCH] handle leave chat from matrix side --- custompuppet.go | 496 ++++++++++++++++++++++--------------------- database/portal.go | 2 +- groupmeExt/client.go | 10 + portal.go | 29 ++- puppet.go | 3 +- user.go | 1 + 6 files changed, 279 insertions(+), 262 deletions(-) diff --git a/custompuppet.go b/custompuppet.go index 5f49e7d..5b6a286 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -17,8 +17,16 @@ package main import ( + "crypto/hmac" + "crypto/sha512" + "encoding/hex" "errors" - // "github.com/Rhymen/go-whatsapp" + "time" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" ) var ( @@ -26,246 +34,246 @@ var ( ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") ) -//func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error { -// prevCustomMXID := puppet.CustomMXID -// if puppet.customIntent != nil { -// puppet.stopSyncing() -// } -// puppet.CustomMXID = mxid -// puppet.AccessToken = accessToken -// -// err := puppet.StartCustomMXID() -// if err != nil { -// return err -// } -// -// if len(prevCustomMXID) > 0 { -// delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID) -// } -// if len(puppet.CustomMXID) > 0 { -// puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet -// } -// puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence -// puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts -// puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID) -// puppet.Update() -// // TODO leave rooms with default puppet -// return nil -//} -// -//func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { -// puppet.log.Debugfln("Logging into %s with shared secret", mxid) -// mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret)) -// mac.Write([]byte(mxid)) -// resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ -// Type: mautrix.AuthTypePassword, -// Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, -// Password: hex.EncodeToString(mac.Sum(nil)), -// DeviceID: "WhatsApp Bridge", -// InitialDeviceDisplayName: "WhatsApp Bridge", -// }) -// if err != nil { -// return "", err -// } -// return resp.AccessToken, nil -//} -// -//func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { -// if len(puppet.CustomMXID) == 0 { -// return nil, ErrNoCustomMXID -// } -// client, err := mautrix.NewClient(puppet.bridge.AS.HomeserverURL, puppet.CustomMXID, puppet.AccessToken) -// if err != nil { -// return nil, err -// } -// client.Logger = puppet.bridge.AS.Log.Sub(string(puppet.CustomMXID)) -// client.Syncer = puppet -// client.Store = puppet -// -// ia := puppet.bridge.AS.NewIntentAPI("custom") -// ia.Client = client -// ia.Localpart, _, _ = puppet.CustomMXID.Parse() -// ia.UserID = puppet.CustomMXID -// ia.IsCustomPuppet = true -// return ia, nil -//} -// -//func (puppet *Puppet) clearCustomMXID() { -// puppet.CustomMXID = "" -// puppet.AccessToken = "" -// puppet.customIntent = nil -// puppet.customTypingIn = nil -// puppet.customUser = nil -//} -// -//func (puppet *Puppet) StartCustomMXID() error { -// if len(puppet.CustomMXID) == 0 { -// puppet.clearCustomMXID() -// return nil -// } -// intent, err := puppet.newCustomIntent() -// if err != nil { -// puppet.clearCustomMXID() -// return err -// } -// resp, err := intent.Whoami() -// if err != nil { -// puppet.clearCustomMXID() -// return err -// } -// if resp.UserID != puppet.CustomMXID { -// puppet.clearCustomMXID() -// return ErrMismatchingMXID -// } -// puppet.customIntent = intent -// puppet.customTypingIn = make(map[id.RoomID]bool) -// puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) -// puppet.startSyncing() -// return nil -//} -// -//func (puppet *Puppet) startSyncing() { -// if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { -// return -// } -// go func() { -// puppet.log.Debugln("Starting syncing...") -// puppet.customIntent.SyncPresence = "offline" -// err := puppet.customIntent.Sync() -// if err != nil { -// puppet.log.Errorln("Fatal error syncing:", err) -// } -// }() -//} -// -//func (puppet *Puppet) stopSyncing() { -// if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { -// return -// } -// puppet.customIntent.StopSync() -//} -// -//func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error { -// if !puppet.customUser.IsConnected() { -// puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp") -// return nil -// } -// for roomID, events := range resp.Rooms.Join { -// portal := puppet.bridge.GetPortalByMXID(roomID) -// if portal == nil { -// continue -// } -// for _, evt := range events.Ephemeral.Events { -// err := evt.Content.ParseRaw(evt.Type) -// if err != nil { -// continue -// } -// switch evt.Type { -// case event.EphemeralEventReceipt: -// if puppet.EnableReceipts { -// go puppet.handleReceiptEvent(portal, evt) -// } -// case event.EphemeralEventTyping: -// go puppet.handleTypingEvent(portal, evt) -// } -// } -// } -// if puppet.EnablePresence { -// for _, evt := range resp.Presence.Events { -// if evt.Sender != puppet.CustomMXID { -// continue -// } -// err := evt.Content.ParseRaw(evt.Type) -// if err != nil { -// continue -// } -// go puppet.handlePresenceEvent(evt) -// } -// } -// return nil -//} -// -//func (puppet *Puppet) handlePresenceEvent(event *event.Event) { -// // presence := whatsapp.PresenceAvailable -// // if event.Content.Raw["presence"].(string) != "online" { -// // presence = whatsapp.PresenceUnavailable -// // puppet.customUser.log.Debugln("Marking offline") -// // } else { -// // puppet.customUser.log.Debugln("Marking online") -// // } -// // _, err := puppet.customUser.Conn.Presence("", presence) -// // if err != nil { -// // puppet.customUser.log.Warnln("Failed to set presence:", err) -// // } -//} -// -//func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) { -// // for eventID, receipts := range *event.Content.AsReceipt() { -// // if _, ok := receipts.Read[puppet.CustomMXID]; !ok { -// // continue -// // } -// // message := puppet.bridge.DB.Message.GetByMXID(eventID) -// // if message == nil { -// // continue -// // } -// // puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID) -// // _, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID) -// // if err != nil { -// // puppet.customUser.log.Warnln("Error marking read:", err) -// // } -// // } -//} -// -//func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) { -// // isTyping := false -// // for _, userID := range evt.Content.AsTyping().UserIDs { -// // if userID == puppet.CustomMXID { -// // isTyping = true -// // break -// // } -// // } -// // if puppet.customTypingIn[evt.RoomID] != isTyping { -// // puppet.customTypingIn[evt.RoomID] = isTyping -// // presence := whatsapp.PresenceComposing -// // if !isTyping { -// // puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID) -// // presence = whatsapp.PresencePaused -// // } else { -// // puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID) -// // } -// // _, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence) -// // if err != nil { -// // puppet.customUser.log.Warnln("Error setting typing:", err) -// // } -// // } -//} -// -//func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { -// puppet.log.Warnln("Sync error:", err) -// return 10 * time.Second, nil -//} -// -//func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter { -// everything := []event.Type{{Type: "*"}} -// return &mautrix.Filter{ -// Presence: mautrix.FilterPart{ -// Senders: []id.UserID{puppet.CustomMXID}, -// Types: []event.Type{event.EphemeralEventPresence}, -// }, -// AccountData: mautrix.FilterPart{NotTypes: everything}, -// Room: mautrix.RoomFilter{ -// Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}}, -// IncludeLeave: false, -// AccountData: mautrix.FilterPart{NotTypes: everything}, -// State: mautrix.FilterPart{NotTypes: everything}, -// Timeline: mautrix.FilterPart{NotTypes: everything}, -// }, -// } -//} -// -//func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {} -//func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() } -//func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {} -//func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" } -//func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch } -//func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } +func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error { + prevCustomMXID := puppet.CustomMXID + if puppet.customIntent != nil { + puppet.stopSyncing() + } + puppet.CustomMXID = mxid + puppet.AccessToken = accessToken + + err := puppet.StartCustomMXID() + if err != nil { + return err + } + + if len(prevCustomMXID) > 0 { + delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID) + } + if len(puppet.CustomMXID) > 0 { + puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } + puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence + puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts + puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID) + puppet.Update() + // TODO leave rooms with default puppet + return nil +} + +func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { + puppet.log.Debugfln("Logging into %s with shared secret", mxid) + mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret)) + mac.Write([]byte(mxid)) + resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, + Password: hex.EncodeToString(mac.Sum(nil)), + DeviceID: "WhatsApp Bridge", + InitialDeviceDisplayName: "WhatsApp Bridge", + }) + if err != nil { + return "", err + } + return resp.AccessToken, nil +} + +func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { + if len(puppet.CustomMXID) == 0 { + return nil, ErrNoCustomMXID + } + client, err := mautrix.NewClient(puppet.bridge.AS.HomeserverURL, puppet.CustomMXID, puppet.AccessToken) + if err != nil { + return nil, err + } + client.Logger = puppet.bridge.AS.Log.Sub(string(puppet.CustomMXID)) + client.Syncer = puppet + client.Store = puppet + + ia := puppet.bridge.AS.NewIntentAPI("custom") + ia.Client = client + ia.Localpart, _, _ = puppet.CustomMXID.Parse() + ia.UserID = puppet.CustomMXID + ia.IsCustomPuppet = true + return ia, nil +} + +func (puppet *Puppet) clearCustomMXID() { + puppet.CustomMXID = "" + puppet.AccessToken = "" + puppet.customIntent = nil + puppet.customTypingIn = nil + puppet.customUser = nil +} + +func (puppet *Puppet) StartCustomMXID() error { + if len(puppet.CustomMXID) == 0 { + puppet.clearCustomMXID() + return nil + } + intent, err := puppet.newCustomIntent() + if err != nil { + puppet.clearCustomMXID() + return err + } + resp, err := intent.Whoami() + if err != nil { + puppet.clearCustomMXID() + return err + } + if resp.UserID != puppet.CustomMXID { + puppet.clearCustomMXID() + return ErrMismatchingMXID + } + puppet.customIntent = intent + puppet.customTypingIn = make(map[id.RoomID]bool) + puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) + puppet.startSyncing() + return nil +} + +func (puppet *Puppet) startSyncing() { + if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + return + } + go func() { + puppet.log.Debugln("Starting syncing...") + puppet.customIntent.SyncPresence = "offline" + err := puppet.customIntent.Sync() + if err != nil { + puppet.log.Errorln("Fatal error syncing:", err) + } + }() +} + +func (puppet *Puppet) stopSyncing() { + if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + return + } + puppet.customIntent.StopSync() +} + +func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error { + if !puppet.customUser.IsConnected() { + puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp") + return nil + } + for roomID, events := range resp.Rooms.Join { + portal := puppet.bridge.GetPortalByMXID(roomID) + if portal == nil { + continue + } + for _, evt := range events.Ephemeral.Events { + err := evt.Content.ParseRaw(evt.Type) + if err != nil { + continue + } + switch evt.Type { + case event.EphemeralEventReceipt: + if puppet.EnableReceipts { + go puppet.handleReceiptEvent(portal, evt) + } + case event.EphemeralEventTyping: + go puppet.handleTypingEvent(portal, evt) + } + } + } + if puppet.EnablePresence { + for _, evt := range resp.Presence.Events { + if evt.Sender != puppet.CustomMXID { + continue + } + err := evt.Content.ParseRaw(evt.Type) + if err != nil { + continue + } + go puppet.handlePresenceEvent(evt) + } + } + return nil +} + +func (puppet *Puppet) handlePresenceEvent(event *event.Event) { + // presence := whatsapp.PresenceAvailable + // if event.Content.Raw["presence"].(string) != "online" { + // presence = whatsapp.PresenceUnavailable + // puppet.customUser.log.Debugln("Marking offline") + // } else { + // puppet.customUser.log.Debugln("Marking online") + // } + // _, err := puppet.customUser.Conn.Presence("", presence) + // if err != nil { + // puppet.customUser.log.Warnln("Failed to set presence:", err) + // } +} + +func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) { + // for eventID, receipts := range *event.Content.AsReceipt() { + // if _, ok := receipts.Read[puppet.CustomMXID]; !ok { + // continue + // } + // message := puppet.bridge.DB.Message.GetByMXID(eventID) + // if message == nil { + // continue + // } + // puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID) + // _, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID) + // if err != nil { + // puppet.customUser.log.Warnln("Error marking read:", err) + // } + // } +} + +func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) { + // isTyping := false + // for _, userID := range evt.Content.AsTyping().UserIDs { + // if userID == puppet.CustomMXID { + // isTyping = true + // break + // } + // } + // if puppet.customTypingIn[evt.RoomID] != isTyping { + // puppet.customTypingIn[evt.RoomID] = isTyping + // presence := whatsapp.PresenceComposing + // if !isTyping { + // puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID) + // presence = whatsapp.PresencePaused + // } else { + // puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID) + // } + // _, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence) + // if err != nil { + // puppet.customUser.log.Warnln("Error setting typing:", err) + // } + // } +} + +func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { + puppet.log.Warnln("Sync error:", err) + return 10 * time.Second, nil +} + +func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter { + everything := []event.Type{{Type: "*"}} + return &mautrix.Filter{ + Presence: mautrix.FilterPart{ + Senders: []id.UserID{puppet.CustomMXID}, + Types: []event.Type{event.EphemeralEventPresence}, + }, + AccountData: mautrix.FilterPart{NotTypes: everything}, + Room: mautrix.RoomFilter{ + Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}}, + IncludeLeave: false, + AccountData: mautrix.FilterPart{NotTypes: everything}, + State: mautrix.FilterPart{NotTypes: everything}, + Timeline: mautrix.FilterPart{NotTypes: everything}, + }, + } +} + +func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {} +func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() } +func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {} +func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" } +func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch } +func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } diff --git a/database/portal.go b/database/portal.go index 10879f4..c8c3e2a 100644 --- a/database/portal.go +++ b/database/portal.go @@ -171,7 +171,7 @@ func (portal *Portal) Update() { } func (portal *Portal) Delete() { - ans := portal.db.Delete(&portal) + ans := portal.db.Where("jid = ? AND receiver = ?", portal.Key.JID, portal.Key.Receiver).Delete(&portal) if ans.Error != nil { portal.log.Warnfln("Failed to delete %s: %v", portal.Key, ans.Error) } diff --git a/groupmeExt/client.go b/groupmeExt/client.go index 86e0eb8..1802cc4 100644 --- a/groupmeExt/client.go +++ b/groupmeExt/client.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/karmanyaahm/groupme" + "maunium.net/go/mautrix-whatsapp/types" ) type Client struct { @@ -50,3 +51,12 @@ func (c Client) LoadMessagesBefore(groupID, lastMessageID string, num int) ([]*g } return i.Messages, nil } + +func (c *Client) RemoveFromGroup(uid, groupID types.GroupMeID) error { + + group, err := c.ShowGroup(context.TODO(), groupme.ID(groupID)) + if err != nil { + return err + } + return c.RemoveMember(context.TODO(), groupme.ID(groupID), group.GetMemberByUserID(groupme.ID(uid)).ID) +} diff --git a/portal.go b/portal.go index e893f77..748dae5 100644 --- a/portal.go +++ b/portal.go @@ -2238,21 +2238,20 @@ func (portal *Portal) Cleanup(puppetsOnly bool) { } func (portal *Portal) HandleMatrixLeave(sender *User) { - // if portal.IsPrivateChat() { - // portal.log.Debugln("User left private chat portal, cleaning up and deleting...") - // portal.Delete() - // portal.Cleanup(false) - // return - // } else { - // // TODO should we somehow deduplicate this call if this leave was sent by the bridge? - // resp, err := sender.Conn.LeaveGroup(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() - // } + if portal.IsPrivateChat() { + portal.log.Debugln("User left private chat portal, cleaning up and deleting...") + portal.Delete() + portal.Cleanup(false) + return + } else { + // TODO should we somehow deduplicate this call if this leave was sent by the bridge? + err := sender.Client.RemoveFromGroup(sender.JID, portal.Key.JID) + if err != nil { + portal.log.Errorfln("Failed to leave group as %s: %v", sender.MXID, err) + return + } + portal.CleanupIfEmpty() + } } func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) { diff --git a/puppet.go b/puppet.go index 9282a9f..c72f257 100644 --- a/puppet.go +++ b/puppet.go @@ -288,8 +288,7 @@ func (puppet *Puppet) Sync(source *User, contact groupme.Member) { if contact.UserID.String() == "system" { puppet.log.Warnln("Trying to sync system puppet") - puppet. - portal.Sync(puppet.bridge.GetUserByJID(portal.Key.Receiver), groupme.Group{}) + // portal.Sync(puppet.bridge.GetUserByJID(portal.Key.Receiver), groupme.Group{}) //TODO permissoins idk if its fine to use portal owner return diff --git a/user.go b/user.go index 90c5848..f755dfa 100644 --- a/user.go +++ b/user.go @@ -592,6 +592,7 @@ func (user *User) syncPortals(chatMap map[string]groupme.Group, createAll bool) } } } + //TODO: handle leave from groupme side user.UpdateDirectChats(nil) user.log.Infoln("Finished syncing portals") select {