Add handling for group metadata changes and refactor things

Group metadata temporarily broken until power level bridging is implemented
This commit is contained in:
Tulir Asokan 2018-08-26 01:55:21 +03:00
parent ccfa85e44a
commit d3a178ecf3
19 changed files with 247 additions and 59 deletions

View file

@ -37,10 +37,10 @@
* [ ] Join
* [ ] Leave
* [ ] Kick
* [ ] Group metadata changes
* [ ] Title
* [ ] Avatar
* [ ] Description
* [x] Group metadata changes
* [x] Title
* [x] Avatar
* [x] Description
* [x] Initial group metadata
* [ ] User metadata changes
* [ ] Display name

View file

@ -87,7 +87,7 @@ func (msg *Message) Scan(row Scannable) *Message {
err := row.Scan(&msg.Owner, &msg.JID, &msg.MXID)
if err != nil {
if err != sql.ErrNoRows {
msg.log.Fatalln("Database scan failed:", err)
msg.log.Errorln("Database scan failed:", err)
}
return nil
}

View file

@ -33,9 +33,9 @@ func (pq *PortalQuery) CreateTable() error {
owner VARCHAR(255),
mxid VARCHAR(255) UNIQUE,
name VARCHAR(255),
topic VARCHAR(255),
avatar VARCHAR(255),
name VARCHAR(255) NOT NULL,
topic VARCHAR(255) NOT NULL,
avatar VARCHAR(255) NOT NULL,
PRIMARY KEY (jid, owner),
FOREIGN KEY (owner) REFERENCES user(mxid)
@ -95,7 +95,7 @@ func (portal *Portal) Scan(row Scannable) *Portal {
err := row.Scan(&portal.JID, &portal.Owner, &portal.MXID, &portal.Name, &portal.Topic, &portal.Avatar)
if err != nil {
if err != sql.ErrNoRows {
portal.log.Fatalln("Database scan failed:", err)
portal.log.Errorln("Database scan failed:", err)
}
return nil
}

View file

@ -82,7 +82,7 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
err := row.Scan(&puppet.JID, &puppet.Receiver, &puppet.Displayname, &puppet.Avatar)
if err != nil {
if err != sql.ErrNoRows {
puppet.log.Fatalln("Database scan failed:", err)
puppet.log.Errorln("Database scan failed:", err)
}
return nil
}

View file

@ -86,7 +86,7 @@ func (user *User) Scan(row Scannable) *User {
&sess.EncKey, &sess.MacKey, &sess.Wid)
if err != nil {
if err != sql.ErrNoRows {
user.log.Fatalln("Database scan failed:", err)
user.log.Errorln("Database scan failed:", err)
}
return nil
}

View file

@ -78,7 +78,7 @@ func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regex
return fmt.Sprintf("<code>%s</code>", str)
},
mentionRegex: func(str string) string {
jid := str[1:] + whatsapp_ext.NewUserSuffix
jid := str[1:] + whatsappExt.NewUserSuffix
puppet := user.GetPuppetByJID(jid)
return fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Displayname)
},

View file

@ -118,19 +118,24 @@ type Portal struct {
roomCreateLock sync.Mutex
}
func (portal *Portal) SyncParticipants(metadata *whatsapp_ext.GroupInfo) {
func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
for _, participant := range metadata.Participants {
intent := portal.user.GetPuppetByJID(participant.JID).Intent()
intent.EnsureJoined(portal.MXID)
// TODO set power levels
}
}
func (portal *Portal) UpdateAvatar() bool {
avatar, err := portal.user.Conn.GetProfilePicThumb(portal.JID)
if err != nil {
portal.log.Errorln(err)
return false
func (portal *Portal) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool {
if avatar == nil {
var err error
avatar, err = portal.user.Conn.GetProfilePicThumb(portal.JID)
if err != nil {
portal.log.Errorln(err)
return false
}
}
if portal.Avatar == avatar.Tag {
return false
}
@ -157,11 +162,12 @@ func (portal *Portal) UpdateAvatar() bool {
return true
}
func (portal *Portal) UpdateName(metadata *whatsapp_ext.GroupInfo) bool {
if portal.Name != metadata.Name {
_, err := portal.MainIntent().SetRoomName(portal.MXID, metadata.Name)
func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool {
if portal.Name != name {
intent := portal.user.GetPuppetByJID(setBy).Intent()
_, err := intent.SetRoomName(portal.MXID, name)
if err == nil {
portal.Name = metadata.Name
portal.Name = name
return true
}
portal.log.Warnln("Failed to set room name:", err)
@ -169,11 +175,12 @@ func (portal *Portal) UpdateName(metadata *whatsapp_ext.GroupInfo) bool {
return false
}
func (portal *Portal) UpdateTopic(metadata *whatsapp_ext.GroupInfo) bool {
if portal.Topic != metadata.Topic {
_, err := portal.MainIntent().SetRoomTopic(portal.MXID, metadata.Topic)
func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool {
if portal.Topic != topic {
intent := portal.user.GetPuppetByJID(setBy).Intent()
_, err := intent.SetRoomTopic(portal.MXID, topic)
if err == nil {
portal.Topic = metadata.Topic
portal.Topic = topic
return true
}
portal.log.Warnln("Failed to set room topic:", err)
@ -189,8 +196,8 @@ func (portal *Portal) UpdateMetadata() bool {
}
portal.SyncParticipants(metadata)
update := false
update = portal.UpdateName(metadata) || update
update = portal.UpdateTopic(metadata) || update
update = portal.UpdateName(metadata.Name, metadata.NameSetBy) || update
update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy) || update
return update
}
@ -212,7 +219,7 @@ func (portal *Portal) Sync(contact whatsapp.Contact) {
update := false
update = portal.UpdateMetadata() || update
update = portal.UpdateAvatar() || update
update = portal.UpdateAvatar(nil) || update
if update {
portal.Update()
}
@ -251,7 +258,7 @@ func (portal *Portal) CreateMatrixRoom() error {
}
func (portal *Portal) IsPrivateChat() bool {
return strings.HasSuffix(portal.JID, whatsapp_ext.NewUserSuffix)
return strings.HasSuffix(portal.JID, whatsappExt.NewUserSuffix)
}
func (portal *Portal) MainIntent() *appservice.IntentAPI {
@ -589,7 +596,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
}
ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1)
for index, mention := range ctxInfo.MentionedJid {
ctxInfo.MentionedJid[index] = mention[1:] + whatsapp_ext.NewUserSuffix
ctxInfo.MentionedJid[index] = mention[1:] + whatsappExt.NewUserSuffix
}
if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil {
info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{

View file

@ -47,7 +47,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUser
receiver = strings.Replace(receiver, "=40", "@", 1)
colonIndex := strings.LastIndex(receiver, "=3")
receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):]
jid := types.WhatsAppID(match[2] + whatsapp_ext.NewUserSuffix)
jid := types.WhatsAppID(match[2] + whatsappExt.NewUserSuffix)
return receiver, jid, true
}
@ -120,7 +120,7 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
dbPuppet.Receiver,
strings.Replace(
dbPuppet.JID,
whatsapp_ext.NewUserSuffix, "", 1)),
whatsappExt.NewUserSuffix, "", 1)),
user.bridge.Config.Homeserver.Domain),
}
}
@ -139,14 +139,14 @@ type Puppet struct {
}
func (puppet *Puppet) PhoneNumber() string {
return strings.Replace(puppet.JID, whatsapp_ext.NewUserSuffix, "", 1)
return strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1)
}
func (puppet *Puppet) Intent() *appservice.IntentAPI {
return puppet.bridge.AppService.Intent(puppet.MXID)
}
func (puppet *Puppet) UpdateAvatar(avatar *whatsapp_ext.ProfilePicInfo) bool {
func (puppet *Puppet) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool {
if avatar == nil {
var err error
avatar, err = puppet.user.Conn.GetProfilePicThumb(puppet.JID)

View file

@ -17,6 +17,7 @@
package main
import (
"maunium.net/go/gomatrix"
"maunium.net/go/mautrix-appservice"
"encoding/json"
"io/ioutil"
@ -65,3 +66,8 @@ func (store *AutosavingStateStore) SetMembership(roomID, userID, membership stri
store.BasicStateStore.SetMembership(roomID, userID, membership)
store.Save()
}
func (store *AutosavingStateStore) SetPowerLevels(roomID string, levels gomatrix.PowerLevels) {
store.BasicStateStore.SetPowerLevels(roomID, levels)
store.Save()
}

54
user.go
View file

@ -33,7 +33,7 @@ import (
type User struct {
*database.User
Conn *whatsapp_ext.ExtendedConn
Conn *whatsappExt.ExtendedConn
bridge *Bridge
log log.Logger
@ -134,7 +134,7 @@ func (user *User) Connect(evenIfNoSession bool) bool {
user.log.Errorln("Failed to connect to WhatsApp:", err)
return false
}
user.Conn = whatsapp_ext.ExtendConn(conn)
user.Conn = whatsappExt.ExtendConn(conn)
user.log.Debugln("WhatsApp connection successful")
user.Conn.AddHandler(user)
return user.RestoreSession()
@ -194,14 +194,14 @@ func (user *User) Login(roomID types.MatrixRoomID) {
}
func (user *User) JID() string {
return strings.Replace(user.Conn.Info.Wid, whatsapp_ext.OldUserSuffix, whatsapp_ext.NewUserSuffix, 1)
return strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1)
}
func (user *User) Sync() {
user.log.Debugln("Syncing...")
user.Conn.Contacts()
for jid, contact := range user.Conn.Store.Contacts {
if strings.HasSuffix(jid, whatsapp_ext.NewUserSuffix) {
if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) {
puppet := user.GetPuppetByJID(contact.Jid)
puppet.Sync(contact)
}
@ -249,12 +249,12 @@ func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) {
portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Title)
}
func (user *User) HandlePresence(info whatsapp_ext.Presence) {
func (user *User) HandlePresence(info whatsappExt.Presence) {
puppet := user.GetPuppetByJID(info.SenderJID)
switch info.Status {
case whatsapp_ext.PresenceUnavailable:
case whatsappExt.PresenceUnavailable:
puppet.Intent().SetPresence("offline")
case whatsapp_ext.PresenceAvailable:
case whatsappExt.PresenceAvailable:
if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
puppet.Intent().UserTyping(puppet.typingIn, false, 0)
puppet.typingIn = ""
@ -262,7 +262,7 @@ func (user *User) HandlePresence(info whatsapp_ext.Presence) {
} else {
puppet.Intent().SetPresence("online")
}
case whatsapp_ext.PresenceComposing:
case whatsappExt.PresenceComposing:
portal := user.GetPortalByJID(info.JID)
puppet.typingIn = portal.MXID
puppet.typingAt = time.Now().Unix()
@ -270,8 +270,8 @@ func (user *User) HandlePresence(info whatsapp_ext.Presence) {
}
}
func (user *User) HandleMsgInfo(info whatsapp_ext.MsgInfo) {
if (info.Command == whatsapp_ext.MsgInfoCommandAck || info.Command == whatsapp_ext.MsgInfoCommandAcks) && info.Acknowledgement == whatsapp_ext.AckMessageRead {
func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) {
if (info.Command == whatsappExt.MsgInfoCommandAck || info.Command == whatsappExt.MsgInfoCommandAcks) && info.Acknowledgement == whatsappExt.AckMessageRead {
portal := user.GetPortalByJID(info.ToJID)
if len(portal.MXID) == 0 {
return
@ -291,11 +291,37 @@ func (user *User) HandleMsgInfo(info whatsapp_ext.MsgInfo) {
}
}
func (user *User) HandleCommand(cmd whatsapp_ext.Command) {
func (user *User) HandleCommand(cmd whatsappExt.Command) {
switch cmd.Type {
case whatsapp_ext.CommandPicture:
puppet := user.GetPuppetByJID(cmd.JID)
puppet.UpdateAvatar(cmd.ProfilePicInfo)
case whatsappExt.CommandPicture:
if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) {
puppet := user.GetPuppetByJID(cmd.JID)
puppet.UpdateAvatar(cmd.ProfilePicInfo)
} else {
portal := user.GetPortalByJID(cmd.JID)
portal.UpdateAvatar(cmd.ProfilePicInfo)
}
}
}
func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
if cmd.Command != whatsappExt.ChatUpdateCommandAction {
return
}
portal := user.GetPortalByJID(cmd.JID)
if len(portal.MXID) == 0 {
return
}
switch cmd.Data.Action {
case whatsappExt.ChatActionNameChange:
portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID)
case whatsappExt.ChatActionAddTopic:
portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID)
case whatsappExt.ChatActionRemoveTopic:
portal.UpdateTopic("", cmd.Data.SenderJID)
// TODO power level updates
}
}

144
whatsapp-ext/chat.go Normal file
View file

@ -0,0 +1,144 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsappExt
import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type ChatUpdateCommand string
const (
ChatUpdateCommandAction ChatUpdateCommand = "action"
)
type ChatUpdate struct {
JID string `json:"id"`
Command ChatUpdateCommand `json:"cmd"`
Data ChatUpdateData `json:"data"`
}
type ChatActionType string
const (
ChatActionNameChange ChatActionType = "subject"
ChatActionAddTopic ChatActionType = "desc_add"
ChatActionRemoveTopic ChatActionType = "desc_remove"
ChatActionRestrict ChatActionType = "restrict"
ChatActionAnnounce ChatActionType = "announce"
ChatActionPromote ChatActionType = "promote"
ChatActionDemote ChatActionType = "demote"
)
type ChatUpdateData struct {
Action ChatActionType
SenderJID string
NameChange struct {
Name string `json:"subject"`
SetAt int64 `json:"s_t"`
SetBy string `json:"s_o"`
}
AddTopic struct {
Topic string `json:"desc"`
ID string `json:"descId"`
SetAt int64 `json:"descTime"`
}
RemoveTopic struct {
ID string `json:"descId"`
}
Restrict bool
Announce bool
PermissionChange struct {
JIDs []string `json:"participants"`
}
}
func (cud *ChatUpdateData) UnmarshalJSON(data []byte) error {
var arr []json.RawMessage
err := json.Unmarshal(data, &arr)
if err != nil {
return err
} else if len(arr) < 3 {
return nil
}
err = json.Unmarshal(arr[0], &cud.Action)
if err != nil {
return err
}
err = json.Unmarshal(arr[1], &cud.SenderJID)
if err != nil {
return err
}
cud.SenderJID = strings.Replace(cud.SenderJID, OldUserSuffix, NewUserSuffix, 1)
var unmarshalTo interface{}
switch cud.Action {
case ChatActionNameChange:
unmarshalTo = &cud.NameChange
case ChatActionAddTopic:
unmarshalTo = &cud.AddTopic
case ChatActionRemoveTopic:
unmarshalTo = &cud.RemoveTopic
case ChatActionRestrict:
unmarshalTo = &cud.Restrict
case ChatActionAnnounce:
unmarshalTo = &cud.Announce
case ChatActionPromote, ChatActionDemote:
unmarshalTo = &cud.PermissionChange
default:
return nil
}
err = json.Unmarshal(arr[2], unmarshalTo)
if err != nil {
return err
}
cud.NameChange.SetBy = strings.Replace(cud.NameChange.SetBy, OldUserSuffix, NewUserSuffix, 1)
return nil
}
type ChatUpdateHandler interface {
whatsapp.Handler
HandleChatUpdate(ChatUpdate)
}
func (ext *ExtendedConn) handleMessageChatUpdate(message []byte) {
var event ChatUpdate
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1)
for _, handler := range ext.handlers {
chatUpdateHandler, ok := handler.(ChatUpdateHandler)
if !ok {
continue
}
go chatUpdateHandler.HandleChatUpdate(event)
}
}

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"encoding/json"
@ -41,7 +41,7 @@ type CommandHandler interface {
HandleCommand(Command)
}
func (ext *ExtendedConn) handleMessageCommand(msgType JSONMessageType, message []byte) {
func (ext *ExtendedConn) handleMessageCommand(message []byte) {
var event Command
err := json.Unmarshal(message, &event)
if err != nil {

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"encoding/json"

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"encoding/json"
@ -34,6 +34,7 @@ const (
MessageConn JSONMessageType = "Conn"
MessageProps JSONMessageType = "Props"
MessageCmd JSONMessageType = "Cmd"
MessageChat JSONMessageType = "Chat"
)
func (ext *ExtendedConn) AddHandler(handler whatsapp.Handler) {
@ -85,7 +86,9 @@ func (ext *ExtendedConn) HandleJsonMessage(message string) {
case MessageMsgInfo, MessageMsg:
ext.handleMessageMsgInfo(msgType, msg[1])
case MessageCmd:
ext.handleMessageCommand(msgType, msg[1])
ext.handleMessageCommand(msg[1])
case MessageChat:
ext.handleMessageChatUpdate(msg[1])
default:
for _, handler := range ext.handlers {
ujmHandler, ok := handler.(UnhandledJSONMessageHandler)

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"encoding/json"

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"encoding/json"

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"github.com/Rhymen/go-whatsapp"

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"encoding/json"

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
package whatsappExt
import (
"fmt"
@ -83,6 +83,8 @@ func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) {
for index, participant := range info.Participants {
info.Participants[index].JID = strings.Replace(participant.JID, OldUserSuffix, NewUserSuffix, 1)
}
info.NameSetBy = strings.Replace(info.NameSetBy, OldUserSuffix, NewUserSuffix, 1)
info.TopicSetBy = strings.Replace(info.TopicSetBy, OldUserSuffix, NewUserSuffix, 1)
return info, nil
}