From abe73684d03fa7d3560ab46aaafebbbe35cbbabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Wed, 5 Dec 2018 09:20:39 +0100 Subject: [PATCH 1/8] Check errors for GetGroupMetaData When a request for GroupMetadata fails, WhatsApp sends an JSON object containing a status field: `{"status":500}`. In my tests, it sends the codes 401, 404 and 500 which might have the meaning of the HTTP status codes. At least, we don't have data to update the portal and should stop doing anything. --- portal.go | 10 ++++++++++ whatsapp-ext/whatsapp.go | 2 ++ 2 files changed, 12 insertions(+) diff --git a/portal.go b/portal.go index a538e31..45ef236 100644 --- a/portal.go +++ b/portal.go @@ -316,6 +316,16 @@ func (portal *Portal) UpdateMetadata(user *User) bool { portal.log.Errorln(err) return false } + if metadata.Status != 0 { + // 401: access denied + // 404: group does (no longer) exist + // 500: ??? happens with status@broadcast + + // TODO: update the room, e.g. change priority level + // to send messages to moderator + return false + } + portal.SyncParticipants(metadata) update := false update = portal.UpdateName(metadata.Name, metadata.NameSetBy) || update diff --git a/whatsapp-ext/whatsapp.go b/whatsapp-ext/whatsapp.go index 740a299..2dfe558 100644 --- a/whatsapp-ext/whatsapp.go +++ b/whatsapp-ext/whatsapp.go @@ -61,6 +61,8 @@ type GroupInfo struct { GroupCreated int64 `json:"creation"` + Status int16 `json:"status"` + Participants []struct { JID string `json:"id"` IsAdmin bool `json:"isAdmin"` From 95c8c014502e2023d49d1a356bc5ba4c9819dcdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Wed, 5 Dec 2018 10:30:07 +0100 Subject: [PATCH 2/8] Check errors of GetProfilePicThumb When a request for GetProfilePicThumb fails, WhatsApp sends an JSON object containing a status field: `{"status":404}`. In my tests, it did send the codes 401 and 404 which might correspond to the HTTP status codes. At least, we don't have data to update the avatar and should stop doing so. --- portal.go | 6 ++++++ whatsapp-ext/whatsapp.go | 2 ++ 2 files changed, 8 insertions(+) diff --git a/portal.go b/portal.go index 45ef236..10a9803 100644 --- a/portal.go +++ b/portal.go @@ -258,6 +258,12 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf } } + if avatar.Status != 0 { + // 401: ??? + // 404: ??? + return false + } + if portal.Avatar == avatar.Tag { return false } diff --git a/whatsapp-ext/whatsapp.go b/whatsapp-ext/whatsapp.go index 2dfe558..9f38345 100644 --- a/whatsapp-ext/whatsapp.go +++ b/whatsapp-ext/whatsapp.go @@ -95,6 +95,8 @@ func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) { type ProfilePicInfo struct { URL string `json:"eurl"` Tag string `json:"tag"` + + Status int16 `json:"status"` } func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) { From 0d49bd8d08db93ffa6b61adb98932ad61864c6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Wed, 5 Dec 2018 10:45:14 +0100 Subject: [PATCH 3/8] Portal.CreateMatrixRoom: ensure the user exists In the log some message show up with `/_matrix/client/r0/createRoom code=403 wrapped=M_FORBIDDEN: Application service has not registered this user`. This is caused by the handling of text messages coming from unknown users: mautrix-whatsapp/vendor/github.com/Rhymen/go-whatsapp/handler.go:106 mautrix-whatsapp/user.go:250 mautrix-whatsapp/portal.go:551 Hence, before creating a Matrix room, we must ensure the user for this room exists. --- portal.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/portal.go b/portal.go index 10a9803..41e0be7 100644 --- a/portal.go +++ b/portal.go @@ -455,6 +455,11 @@ func (portal *Portal) CreateMatrixRoom(invite []string) error { return nil } + intent := portal.MainIntent() + if err := intent.EnsureRegistered(); err != nil { + return err + } + name := portal.Name topic := portal.Topic isPrivateChat := false @@ -464,7 +469,7 @@ func (portal *Portal) CreateMatrixRoom(invite []string) error { isPrivateChat = true } - resp, err := portal.MainIntent().CreateRoom(&gomatrix.ReqCreateRoom{ + resp, err := intent.CreateRoom(&gomatrix.ReqCreateRoom{ Visibility: "private", Name: name, Topic: topic, From ebfc5e214a8fba8c57108cca9837ad4af6a0022e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Fri, 7 Dec 2018 14:31:55 +0100 Subject: [PATCH 4/8] Mention `help` in the first message in management room To better guide the user after the first contact, tell him to use `help` to see the list of commands. --- matrix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix.go b/matrix.go index cd12447..8cbeade 100644 --- a/matrix.go +++ b/matrix.go @@ -98,7 +98,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) { if !hasPuppets { user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) user.SetManagementRoom(types.MatrixRoomID(resp.RoomID)) - intent.SendNotice(string(user.ManagementRoom), "This room has been registered as your bridge management/status room.") + intent.SendNotice(string(user.ManagementRoom), "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.") mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) } } From a626d14a3ffd5872fd33061466b12047c80d02f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Fri, 7 Dec 2018 14:42:57 +0100 Subject: [PATCH 5/8] No sync on startup; new command `import contacts` Having an import of all contacts on each startup and after login is very annoying, if you have a big list of contacts. If you choose to not join a room with all contacts, you get the invitation over and over on each restart of the service. Better is to have a command for the management room to explicitly start the import. --- commands.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ main.go | 2 +- user.go | 23 +---------------------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/commands.go b/commands.go index 213c4e1..6f467a8 100644 --- a/commands.go +++ b/commands.go @@ -19,9 +19,12 @@ package main import ( "strings" + "github.com/Rhymen/go-whatsapp" "maunium.net/go/maulogger" "maunium.net/go/mautrix-appservice" + "maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/types" + "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) type CommandHandler struct { @@ -74,6 +77,8 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes handler.CommandLogout(ce) case "help": handler.CommandHelp(ce) + case "import": + handler.CommandImport(ce) default: ce.Reply("Unknown Command") } @@ -121,5 +126,44 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { cmdPrefix + cmdHelpHelp, cmdPrefix + cmdLoginHelp, cmdPrefix + cmdLogoutHelp, + cmdPrefix + cmdImportHelp, }, "\n")) } + +const cmdImportHelp = `import JID|contacts - Open up a room for JID or for each WhatsApp contact` + +// CommandImport handles import command +func (handler *CommandHandler) CommandImport(ce *CommandEvent) { + // ensure all messages go to the management room + ce.RoomID = ce.User.ManagementRoom + + user := ce.User + + if ce.Args[0] == "contacts" { + handler.log.Debugln("Importing all contacts of", user) + _, err := user.Conn.Contacts() + if err != nil { + handler.log.Errorln("Error on update of contacts of user", user, ":", err) + return + } + + for jid, contact := range user.Conn.Store.Contacts { + if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) { + puppet := user.bridge.GetPuppetByJID(contact.Jid) + puppet.Sync(user, contact) + } else { + portal := user.bridge.GetPortalByJID(database.GroupPortalKey(contact.Jid)) + portal.Sync(user, contact) + } + } + + ce.Reply("Importing all contacts done") + } else { + jid := ce.Args[0] + whatsappExt.NewUserSuffix + handler.log.Debugln("Importing", jid, "for", user) + puppet := user.bridge.GetPuppetByJID(jid) + + contact := whatsapp.Contact { Jid: jid } + puppet.Sync(user, contact) + } +} diff --git a/main.go b/main.go index 5594e2c..9965727 100644 --- a/main.go +++ b/main.go @@ -185,7 +185,7 @@ func (bridge *Bridge) UpdateBotProfile() { func (bridge *Bridge) StartUsers() { for _, user := range bridge.GetAllUsers() { - go user.Start() + go user.Connect(false) } } diff --git a/user.go b/user.go index 0c44f5a..6f9129f 100644 --- a/user.go +++ b/user.go @@ -132,12 +132,6 @@ func (user *User) SetSession(session *whatsapp.Session) { user.Update() } -func (user *User) Start() { - if user.Connect(false) { - user.Sync() - } -} - func (user *User) Connect(evenIfNoSession bool) bool { if user.Conn != nil { return true @@ -210,22 +204,7 @@ func (user *User) Login(roomID types.MatrixRoomID) { user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) user.Session = &session user.Update() - bot.SendNotice(roomID, "Successfully logged in. Synchronizing chats...") - go user.Sync() -} - -func (user *User) Sync() { - user.log.Debugln("Syncing...") - user.Conn.Contacts() - for jid, contact := range user.Conn.Store.Contacts { - if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) { - puppet := user.bridge.GetPuppetByJID(contact.Jid) - puppet.Sync(user, contact) - } else { - portal := user.bridge.GetPortalByJID(database.GroupPortalKey(contact.Jid)) - portal.Sync(user, contact) - } - } + bot.SendNotice(roomID, "Successfully logged in. Now, you may ask for `import contacts`.") } func (user *User) HandleError(err error) { From da5e3993332792579b2b3185a908bc0f7713f2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Fri, 7 Dec 2018 17:02:51 +0100 Subject: [PATCH 6/8] CommandEvent.Reply: Send all messages to management room Because the Bridge Bot doesn't participate in the rooms it can't send any messages to this room. Hence, we should send all replies to the management room. --- commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands.go b/commands.go index 6f467a8..e23c7a3 100644 --- a/commands.go +++ b/commands.go @@ -52,7 +52,7 @@ type CommandEvent struct { // Reply sends a reply to command as notice func (ce *CommandEvent) Reply(msg string) { - _, err := ce.Bot.SendNotice(string(ce.RoomID), msg) + _, err := ce.Bot.SendNotice(string(ce.User.ManagementRoom), msg) if err != nil { ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err) } From 3c7d77a456b61c37f0fdb7fff7b774bd18076092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Fri, 7 Dec 2018 17:05:39 +0100 Subject: [PATCH 7/8] ExtendedConn.HandleJsonMessage: call jsonParseError In case of something goes wrong for parsing the message as JSON message, we should tell this the error handler. --- whatsapp-ext/jsonmessage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/whatsapp-ext/jsonmessage.go b/whatsapp-ext/jsonmessage.go index 232f3fb..8a7fd44 100644 --- a/whatsapp-ext/jsonmessage.go +++ b/whatsapp-ext/jsonmessage.go @@ -68,6 +68,7 @@ func (ext *ExtendedConn) HandleJsonMessage(message string) { msg := JSONMessage{} err := json.Unmarshal([]byte(message), &msg) if err != nil || len(msg) < 2 { + ext.jsonParseError(err) return } From ae2ccb0d389eea3a9c61a5e807dff538fb85291c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sommer?= Date: Fri, 7 Dec 2018 23:31:15 +0100 Subject: [PATCH 8/8] CommandHelp: don't print cmdprefix in management room In the management room the user don't need to prefix the commands with the command-prefix. Hence, don't show the prefix in the command list. --- commands.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commands.go b/commands.go index e23c7a3..7f53ce3 100644 --- a/commands.go +++ b/commands.go @@ -121,7 +121,11 @@ const cmdHelpHelp = `help - Prints this help` // CommandHelp handles help command func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { - cmdPrefix := handler.bridge.Config.Bridge.CommandPrefix + " " + cmdPrefix := "" + if ce.User.ManagementRoom != ce.RoomID { + cmdPrefix = handler.bridge.Config.Bridge.CommandPrefix + " " + } + ce.Reply(strings.Join([]string{ cmdPrefix + cmdHelpHelp, cmdPrefix + cmdLoginHelp,