From c7348f29b03ca6c2b1bcf1dfafcb9d641f3f06a5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 29 Aug 2018 00:40:54 +0300 Subject: [PATCH 1/8] Initial desegregation of users and automatic config updating --- .gitignore | 1 + Gopkg.lock | 4 +- commands.go | 4 +- config/bridge.go | 16 +- config/config.go | 1 - config/recursivemap.go | 115 ++++++++++ config/registration.go | 2 +- config/update.go | 100 +++++++++ database/message.go | 35 +-- database/portal.go | 74 +++++-- database/puppet.go | 35 ++- database/user.go | 45 ++-- example-config.yaml | 19 +- formatting.go | 144 +++++++----- main.go | 77 +++++-- matrix.go | 26 ++- portal.go | 205 ++++++++---------- puppet.go | 92 +++----- user.go | 134 ++++++------ vendor/maunium.net/go/gomatrix/client.go | 18 +- vendor/maunium.net/go/gomatrix/events.go | 62 ++++-- .../go/mautrix-appservice/appservice.go | 27 ++- .../go/mautrix-appservice/intent.go | 6 +- .../go/mautrix-appservice/statestore.go | 39 ++-- 24 files changed, 806 insertions(+), 475 deletions(-) create mode 100644 config/recursivemap.go create mode 100644 config/update.go diff --git a/.gitignore b/.gitignore index 6a0c71d..8c8540e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ *.session *.json *.db +*.log diff --git a/Gopkg.lock b/Gopkg.lock index c23225f..41db0d9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -123,7 +123,7 @@ ".", "format" ] - revision = "ead1f970c8f56d1854cb9eb4a54c03aa6dafd753" + revision = "42a3133c4980e4b1ea5fb52329d977f592d67cf0" [[projects]] branch = "master" @@ -141,7 +141,7 @@ branch = "master" name = "maunium.net/go/mautrix-appservice" packages = ["."] - revision = "269f2ab602126a2de94bc86a457392426cce1ab2" + revision = "37d4449056015cea5d0a4420bba595c61ad32007" [solve-meta] analyzer-name = "dep" diff --git a/commands.go b/commands.go index fbbd5bd..34255ef 100644 --- a/commands.go +++ b/commands.go @@ -48,7 +48,7 @@ type CommandEvent struct { func (ce *CommandEvent) Reply(msg string) { _, err := ce.Bot.SendNotice(string(ce.RoomID), msg) if err != nil { - ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.ID, err) + ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err) } } @@ -56,7 +56,7 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes args := strings.Split(message, " ") cmd := strings.ToLower(args[0]) ce := &CommandEvent{ - Bot: handler.bridge.AppService.BotIntent(), + Bot: handler.bridge.AS.BotIntent(), Bridge: handler.bridge, Handler: handler, RoomID: roomID, diff --git a/config/bridge.go b/config/bridge.go index 11c55ba..47a6860 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -56,12 +56,7 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } -type DisplaynameTemplateArgs struct { - Displayname string -} - type UsernameTemplateArgs struct { - Receiver string UserID string } @@ -74,14 +69,9 @@ func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string { return buf.String() } -func (bc BridgeConfig) FormatUsername(receiver types.MatrixUserID, userID types.WhatsAppID) string { +func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string { var buf bytes.Buffer - receiver = strings.Replace(receiver, "@", "=40", 1) - receiver = strings.Replace(receiver, ":", "=3", 1) - bc.usernameTemplate.Execute(&buf, UsernameTemplateArgs{ - Receiver: receiver, - UserID: userID, - }) + bc.usernameTemplate.Execute(&buf, userID) return buf.String() } @@ -92,7 +82,7 @@ func (bc BridgeConfig) MarshalYAML() (interface{}, error) { Name: "{{.Name}}", Short: "{{.Short}}", }) - bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}") + bc.UsernameTemplate = bc.FormatUsername("{{.}}") return bc, nil } diff --git a/config/config.go b/config/config.go index 53680c9..230ecf1 100644 --- a/config/config.go +++ b/config/config.go @@ -78,7 +78,6 @@ func (config *Config) Save(path string) error { func (config *Config) MakeAppService() (*appservice.AppService, error) { as := appservice.Create() - as.LogConfig = config.Logging as.HomeserverDomain = config.Homeserver.Domain as.HomeserverURL = config.Homeserver.Address as.Host.Hostname = config.AppService.Hostname diff --git a/config/recursivemap.go b/config/recursivemap.go new file mode 100644 index 0000000..49059be --- /dev/null +++ b/config/recursivemap.go @@ -0,0 +1,115 @@ +// 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 . + +package config + +import ( + "strings" +) + +type RecursiveMap map[interface{}]interface{} + +func (rm RecursiveMap) GetDefault(path string, defVal interface{}) interface{} { + val, ok := rm.Get(path) + if !ok { + return defVal + } + return val +} + +func (rm RecursiveMap) GetMap(path string) RecursiveMap { + val := rm.GetDefault(path, nil) + if val == nil { + return nil + } + + newRM, ok := val.(map[interface{}]interface{}) + if ok { + return RecursiveMap(newRM) + } + return nil +} + +func (rm RecursiveMap) Get(path string) (interface{}, bool) { + if index := strings.IndexRune(path, '.'); index >= 0 { + key := path[:index] + path = path[index+1:] + + submap := rm.GetMap(key) + if submap == nil { + return nil, false + } + return submap.Get(path) + } + val, ok := rm[path] + return val, ok +} + +func (rm RecursiveMap) GetIntDefault(path string, defVal int) int { + val, ok := rm.GetInt(path) + if !ok { + return defVal + } + return val +} + +func (rm RecursiveMap) GetInt(path string) (int, bool) { + val, ok := rm.Get(path) + if !ok { + return 0, ok + } + intVal, ok := val.(int) + return intVal, ok +} + +func (rm RecursiveMap) GetStringDefault(path string, defVal string) string { + val, ok := rm.GetString(path) + if !ok { + return defVal + } + return val +} + +func (rm RecursiveMap) GetString(path string) (string, bool) { + val, ok := rm.Get(path) + if !ok { + return "", ok + } + strVal, ok := val.(string) + return strVal, ok +} + +func (rm RecursiveMap) Set(path string, value interface{}) { + if index := strings.IndexRune(path, '.'); index >= 0 { + key := path[:index] + path = path[index+1:] + nextRM := rm.GetMap(key) + if nextRM == nil { + nextRM = make(RecursiveMap) + rm[key] = nextRM + } + nextRM.Set(path, value) + return + } + rm[path] = value +} + +func (rm RecursiveMap) CopyFrom(otherRM RecursiveMap, path string) { + val, ok := otherRM.Get(path) + if ok { + rm.Set(path, val) + } +} \ No newline at end of file diff --git a/config/registration.go b/config/registration.go index e1e08b5..a764444 100644 --- a/config/registration.go +++ b/config/registration.go @@ -56,7 +56,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration) registration.SenderLocalpart = config.AppService.Bot.Username userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", - config.Bridge.FormatUsername(".+", "[0-9]+"), + config.Bridge.FormatUsername("[0-9]+"), config.Homeserver.Domain)) if err != nil { return err diff --git a/config/update.go b/config/update.go new file mode 100644 index 0000000..5c390ab --- /dev/null +++ b/config/update.go @@ -0,0 +1,100 @@ +// 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 . + +package config + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +func Update(path, basePath string) error { + oldCfgData, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + oldCfg := make(RecursiveMap) + err = yaml.Unmarshal(oldCfgData, &oldCfg) + if err != nil { + return err + } + + baseCfgData, err := ioutil.ReadFile(basePath) + if err != nil { + return err + } + + baseCfg := make(RecursiveMap) + err = yaml.Unmarshal(baseCfgData, &baseCfg) + if err != nil { + return err + } + + err = runUpdate(oldCfg, baseCfg) + if err != nil { + return err + } + + newCfgData, err := yaml.Marshal(&baseCfg) + if err != nil { + return err + } + + return ioutil.WriteFile(path, newCfgData, 0600) +} + +func runUpdate(oldCfg, newCfg RecursiveMap) error { + cp := func(path string) { + newCfg.CopyFrom(oldCfg, path) + } + + cp("homeserver.address") + cp("homeserver.domain") + + cp("appservice.address") + cp("appservice.hostname") + cp("appservice.port") + + cp("appservice.database.type") + cp("appservice.database.uri") + cp("appservice.state_store_path") + + cp("appservice.id") + cp("appservice.bot.username") + cp("appservice.bot.displayname") + cp("appservice.bot.avatar") + + cp("appservice.bot.as_token") + cp("appservice.bot.hs_token") + + cp("bridge.username_template") + cp("bridge.displayname_template") + + cp("bridge.command_prefix") + + cp("bridge.permissions") + + cp("logging.directory") + cp("logging.file_name_format") + cp("logging.file_date_format") + cp("logging.file_mode") + cp("logging.timestamp_format") + cp("logging.print_level") + + return nil +} diff --git a/database/message.go b/database/message.go index 47d17f9..d4e3a8c 100644 --- a/database/message.go +++ b/database/message.go @@ -30,12 +30,13 @@ type MessageQuery struct { func (mq *MessageQuery) CreateTable() error { _, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message ( - owner VARCHAR(255), - jid VARCHAR(255), - mxid VARCHAR(255) NOT NULL UNIQUE, + chat_jid VARCHAR(25) NOT NULL, + chat_receiver VARCHAR(25) NOT NULL, + jid VARCHAR(255) NOT NULL, + mxid VARCHAR(255) NOT NULL UNIQUE, - PRIMARY KEY (owner, jid), - FOREIGN KEY (owner) REFERENCES user(mxid) + PRIMARY KEY (chat_jid, jid), + FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) )`) return err } @@ -47,8 +48,8 @@ func (mq *MessageQuery) New() *Message { } } -func (mq *MessageQuery) GetAll(owner types.MatrixUserID) (messages []*Message) { - rows, err := mq.db.Query("SELECT * FROM message WHERE owner=?", owner) +func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) { + rows, err := mq.db.Query("SELECT * FROM message WHERE chat_jid=? AND chat_receiver=?", chat.JID, chat.Receiver) if err != nil || rows == nil { return nil } @@ -59,8 +60,8 @@ func (mq *MessageQuery) GetAll(owner types.MatrixUserID) (messages []*Message) { return } -func (mq *MessageQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppMessageID) *Message { - return mq.get("SELECT * FROM message WHERE owner=? AND jid=?", owner, jid) +func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.WhatsAppMessageID) *Message { + return mq.get("SELECT * FROM message WHERE chat_jid=? AND chat_receiver=? AND jid=?", chat.JID, chat.Receiver, jid) } func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message { @@ -79,13 +80,13 @@ type Message struct { db *Database log log.Logger - Owner types.MatrixUserID - JID types.WhatsAppMessageID - MXID types.MatrixEventID + Chat PortalKey + JID types.WhatsAppMessageID + MXID types.MatrixEventID } func (msg *Message) Scan(row Scannable) *Message { - err := row.Scan(&msg.Owner, &msg.JID, &msg.MXID) + err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID) if err != nil { if err != sql.ErrNoRows { msg.log.Errorln("Database scan failed:", err) @@ -96,17 +97,17 @@ func (msg *Message) Scan(row Scannable) *Message { } func (msg *Message) Insert() error { - _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?)", msg.Owner, msg.JID, msg.MXID) + _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID) if err != nil { - msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err) + msg.log.Warnfln("Failed to update %s: %v", msg.Chat, msg.JID, err) } return err } func (msg *Message) Update() error { - _, err := msg.db.Exec("UPDATE portal SET mxid=? WHERE owner=? AND jid=?", msg.MXID, msg.Owner, msg.JID) + _, err := msg.db.Exec("UPDATE portal SET mxid=? WHERE chat_jid=? AND chat_receiver=? AND jid=?", msg.MXID, msg.Chat.JID, msg.Chat.Receiver, msg.JID) if err != nil { - msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err) + msg.log.Warnfln("Failed to update %s: %v", msg.Chat, msg.JID, err) } return err } diff --git a/database/portal.go b/database/portal.go index 6a2ed54..1a8586d 100644 --- a/database/portal.go +++ b/database/portal.go @@ -18,11 +18,41 @@ package database import ( "database/sql" + "strings" log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/types" ) +type PortalKey struct { + JID types.WhatsAppID + Receiver types.WhatsAppID +} + +func GroupPortalKey(jid types.WhatsAppID) PortalKey { + return PortalKey{ + JID: jid, + Receiver: jid, + } +} + +func NewPortalKey(jid, receiver types.WhatsAppID) PortalKey { + if strings.HasSuffix(jid, "@g.us") { + receiver = jid + } + return PortalKey{ + JID: jid, + Receiver: receiver, + } +} + +func (key PortalKey) String() string { + if key.Receiver == key.JID { + return key.JID + } + return key.JID + "-" + key.Receiver +} + type PortalQuery struct { db *Database log log.Logger @@ -30,16 +60,16 @@ type PortalQuery struct { func (pq *PortalQuery) CreateTable() error { _, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS portal ( - jid VARCHAR(255), - owner VARCHAR(255), - mxid VARCHAR(255) UNIQUE, + jid VARCHAR(25), + receiver VARCHAR(25), + mxid VARCHAR(255) UNIQUE, 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) + PRIMARY KEY (jid, receiver), + FOREIGN KEY (receiver) REFERENCES user(mxid) )`) return err } @@ -51,8 +81,8 @@ func (pq *PortalQuery) New() *Portal { } } -func (pq *PortalQuery) GetAll(owner types.MatrixUserID) (portals []*Portal) { - rows, err := pq.db.Query("SELECT * FROM portal WHERE owner=?", owner) +func (pq *PortalQuery) GetAll() (portals []*Portal) { + rows, err := pq.db.Query("SELECT * FROM portal") if err != nil || rows == nil { return nil } @@ -63,8 +93,8 @@ func (pq *PortalQuery) GetAll(owner types.MatrixUserID) (portals []*Portal) { return } -func (pq *PortalQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppID) *Portal { - return pq.get("SELECT * FROM portal WHERE jid=? AND owner=?", jid, owner) +func (pq *PortalQuery) GetByJID(key PortalKey) *Portal { + return pq.get("SELECT * FROM portal WHERE jid=? AND receiver=?", key.JID, key.Receiver) } func (pq *PortalQuery) GetByMXID(mxid types.MatrixRoomID) *Portal { @@ -83,9 +113,8 @@ type Portal struct { db *Database log log.Logger - JID types.WhatsAppID - MXID types.MatrixRoomID - Owner types.MatrixUserID + Key PortalKey + MXID types.MatrixRoomID Name string Topic string @@ -93,7 +122,7 @@ type Portal struct { } func (portal *Portal) Scan(row Scannable) *Portal { - err := row.Scan(&portal.JID, &portal.Owner, &portal.MXID, &portal.Name, &portal.Topic, &portal.Avatar) + err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &portal.MXID, &portal.Name, &portal.Topic, &portal.Avatar) if err != nil { if err != sql.ErrNoRows { portal.log.Errorln("Database scan failed:", err) @@ -103,15 +132,18 @@ func (portal *Portal) Scan(row Scannable) *Portal { return portal } -func (portal *Portal) Insert() error { - var mxid *string +func (portal *Portal) mxidPtr() *string { if len(portal.MXID) > 0 { - mxid = &portal.MXID + return &portal.MXID } + return nil +} + +func (portal *Portal) Insert() error { _, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?, ?, ?, ?)", - portal.JID, portal.Owner, mxid, portal.Name, portal.Topic, portal.Avatar) + portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar) if err != nil { - portal.log.Warnfln("Failed to insert %s->%s: %v", portal.JID, portal.Owner, err) + portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) } return err } @@ -121,10 +153,10 @@ func (portal *Portal) Update() error { if len(portal.MXID) > 0 { mxid = &portal.MXID } - _, err := portal.db.Exec("UPDATE portal SET mxid=?, name=?, topic=?, avatar=? WHERE jid=? AND owner=?", - mxid, portal.Name, portal.Topic, portal.Avatar, portal.JID, portal.Owner) + _, err := portal.db.Exec("UPDATE portal SET mxid=?, name=?, topic=?, avatar=? WHERE jid=? AND receiver=?", + mxid, portal.Name, portal.Topic, portal.Avatar, portal.Key.JID, portal.Key.Receiver) if err != nil { - portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err) + portal.log.Warnfln("Failed to update %s: %v", portal.Key, err) } return err } diff --git a/database/puppet.go b/database/puppet.go index 434fbe0..401a5c8 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -30,13 +30,9 @@ type PuppetQuery struct { func (pq *PuppetQuery) CreateTable() error { _, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet ( - jid VARCHAR(255), - receiver VARCHAR(255), - + jid VARCHAR(25) PRIMARY KEY, displayname VARCHAR(255), - avatar VARCHAR(255), - - PRIMARY KEY(jid, receiver) + avatar VARCHAR(255) )`) return err } @@ -48,8 +44,8 @@ func (pq *PuppetQuery) New() *Puppet { } } -func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) { - rows, err := pq.db.Query("SELECT * FROM puppet WHERE receiver=%s") +func (pq *PuppetQuery) GetAll() (puppets []*Puppet) { + rows, err := pq.db.Query("SELECT * FROM puppet") if err != nil || rows == nil { return nil } @@ -60,8 +56,8 @@ func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) { return } -func (pq *PuppetQuery) Get(jid types.WhatsAppID, receiver types.MatrixUserID) *Puppet { - row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=? AND receiver=?", jid, receiver) +func (pq *PuppetQuery) Get(jid types.WhatsAppID) *Puppet { + row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=?", jid) if row == nil { return nil } @@ -72,15 +68,13 @@ type Puppet struct { db *Database log log.Logger - JID types.WhatsAppID - Receiver types.MatrixUserID - + JID types.WhatsAppID Displayname string Avatar string } func (puppet *Puppet) Scan(row Scannable) *Puppet { - err := row.Scan(&puppet.JID, &puppet.Receiver, &puppet.Displayname, &puppet.Avatar) + err := row.Scan(&puppet.JID, &puppet.Displayname, &puppet.Avatar) if err != nil { if err != sql.ErrNoRows { puppet.log.Errorln("Database scan failed:", err) @@ -91,20 +85,19 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet { } func (puppet *Puppet) Insert() error { - _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)", - puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar) + _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?)", + puppet.JID, puppet.Displayname, puppet.Avatar) if err != nil { - puppet.log.Errorfln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err) + puppet.log.Errorfln("Failed to insert %s: %v", puppet.JID, err) } return err } func (puppet *Puppet) Update() error { - _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=? AND receiver=?", - puppet.Displayname, puppet.Avatar, - puppet.JID, puppet.Receiver) + _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=?", + puppet.Displayname, puppet.Avatar, puppet.JID) if err != nil { - puppet.log.Errorfln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err) + puppet.log.Errorfln("Failed to update %s->%s: %v", puppet.JID, err) } return err } diff --git a/database/user.go b/database/user.go index 15dc793..b650cb3 100644 --- a/database/user.go +++ b/database/user.go @@ -32,6 +32,7 @@ type UserQuery struct { func (uq *UserQuery) CreateTable() error { _, err := uq.db.Exec(`CREATE TABLE IF NOT EXISTS user ( mxid VARCHAR(255) PRIMARY KEY, + jid VARCHAR(25) UNIQUE, management_room VARCHAR(255), @@ -64,7 +65,7 @@ func (uq *UserQuery) GetAll() (users []*User) { return } -func (uq *UserQuery) Get(userID types.MatrixUserID) *User { +func (uq *UserQuery) GetByMXID(userID types.MatrixUserID) *User { row := uq.db.QueryRow("SELECT * FROM user WHERE mxid=?", userID) if row == nil { return nil @@ -72,18 +73,27 @@ func (uq *UserQuery) Get(userID types.MatrixUserID) *User { return uq.New().Scan(row) } +func (uq *UserQuery) GetByJID(userID types.WhatsAppID) *User { + row := uq.db.QueryRow("SELECT * FROM user WHERE jid=?", userID) + if row == nil { + return nil + } + return uq.New().Scan(row) +} + type User struct { db *Database log log.Logger - ID types.MatrixUserID + MXID types.MatrixUserID + JID types.WhatsAppID ManagementRoom types.MatrixRoomID Session *whatsapp.Session } func (user *User) Scan(row Scannable) *User { sess := whatsapp.Session{} - err := row.Scan(&user.ID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken, + err := row.Scan(&user.MXID, &user.JID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken, &sess.EncKey, &sess.MacKey, &sess.Wid) if err != nil { if err != sql.ErrNoRows { @@ -99,23 +109,32 @@ func (user *User) Scan(row Scannable) *User { return user } -func (user *User) Insert() error { - var sess whatsapp.Session +func (user *User) jidPtr() *string { + if len(user.JID) > 0 { + return &user.JID + } + return nil +} + +func (user *User) sessionUnptr() (sess whatsapp.Session) { if user.Session != nil { sess = *user.Session } - _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.ID, user.ManagementRoom, + return +} + +func (user *User) Insert() error { + sess := user.sessionUnptr() + _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, user.jidPtr(), user.ManagementRoom, sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid) return err } func (user *User) Update() error { - var sess whatsapp.Session - if user.Session != nil { - sess = *user.Session - } - _, err := user.db.Exec("UPDATE user SET management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?", - user.ManagementRoom, - sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, user.ID) + sess := user.sessionUnptr() + _, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?", + user.jidPtr(), user.ManagementRoom, + sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, + user.MXID) return err } diff --git a/example-config.yaml b/example-config.yaml index d3a1f60..bbc609c 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -21,7 +21,6 @@ appservice: type: sqlite3 # The database URI. Usually file name. https://github.com/mattn/go-sqlite3#connection-string uri: mautrix-whatsapp.db - # Path to the Matrix room state store. state_store_path: ./mx-state.json @@ -43,15 +42,15 @@ appservice: # Bridge config. Currently unused. bridge: # Localpart template of MXIDs for WhatsApp users. - # {{.Receiver}} is replaced with the WhatsApp user ID of the Matrix user receiving messages. - # {{.UserID}} is replaced with the user ID of the WhatsApp user. - username_template: "whatsapp_{{.Receiver}}_{{.UserID}}" + # {{.}} is replaced with the phone number of the WhatsApp user. + username_template: whatsapp_{{.}} # Displayname template for WhatsApp users. - # {{.Name}} - display name - # {{.Short}} - short display name (usually first name) - # {{.Notify}} - nickname (maybe set by the target WhatsApp user) + # {{.Notify}} - nickname set by the WhatsApp user # {{.Jid}} - phone number (international format) - displayname_template: "{{if .Name}}{{.Name}}{{else if .Notify}}{{.Notify}}{{else if .Short}}{{.Short}}{{else}}{{.Jid}}{{end}}" + # The following variables are also available, but will cause problems on multi-user instances: + # {{.Name}} - display name from contact list + # {{.Short}} - short display name from contact list + displayname_template: "{{if .Notify}}{{.Notify}}{{else}}{{.Jid}}{{end}} (WA)" # The prefix for commands. Only required in non-management rooms. command_prefix: "!wa" @@ -72,8 +71,8 @@ bridge: logging: # The directory for log files. Will be created if not found. directory: ./logs - # Available variables: .date for the file date and .index for different log files on the same day. - file_name_format: "{{.date}}-{{.index}.log" + # Available variables: .Date for the file date and .Index for different log files on the same day. + file_name_format: "{{.Date}}-{{.Index}}.log" # Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants file_date_format: 2006-01-02 # Log file permissions. diff --git a/formatting.go b/formatting.go index 7d04cc0..bd3eb3c 100644 --- a/formatting.go +++ b/formatting.go @@ -18,58 +18,71 @@ package main import ( "fmt" + "html" "regexp" "strings" + "maunium.net/go/gomatrix" "maunium.net/go/gomatrix/format" "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) -func (user *User) newHTMLParser() *format.HTMLParser { - return &format.HTMLParser{ - TabsToSpaces: 4, - Newline: "\n", - - PillConverter: func(mxid, eventID string) string { - if mxid[0] == '@' { - puppet := user.GetPuppetByMXID(mxid) - fmt.Println(mxid, puppet) - if puppet != nil { - return "@" + puppet.PhoneNumber() - } - } - return mxid - }, - BoldConverter: func(text string) string { - return fmt.Sprintf("*%s*", text) - }, - ItalicConverter: func(text string) string { - return fmt.Sprintf("_%s_", text) - }, - StrikethroughConverter: func(text string) string { - return fmt.Sprintf("~%s~", text) - }, - MonospaceConverter: func(text string) string { - return fmt.Sprintf("```%s```", text) - }, - MonospaceBlockConverter: func(text string) string { - return fmt.Sprintf("```%s```", text) - }, - } -} - var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)") var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)") var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)") var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```") var mentionRegex = regexp.MustCompile("@[0-9]+") -func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regexp.Regexp]func(string) string, map[*regexp.Regexp]func(string) string) { - return map[*regexp.Regexp]string{ - italicRegex: "$1$2$3", - boldRegex: "$1$2$3", - strikethroughRegex: "$1$2$3", - }, map[*regexp.Regexp]func(string) string{ +type Formatter struct { + bridge *Bridge + + matrixHTMLParser *format.HTMLParser + + waReplString map[*regexp.Regexp]string + waReplFunc map[*regexp.Regexp]func(string) string + waReplFuncText map[*regexp.Regexp]func(string) string +} + +func NewFormatter(bridge *Bridge) *Formatter { + formatter := &Formatter{ + bridge: bridge, + matrixHTMLParser: &format.HTMLParser{ + TabsToSpaces: 4, + Newline: "\n", + + PillConverter: func(mxid, eventID string) string { + if mxid[0] == '@' { + puppet := bridge.GetPuppetByMXID(mxid) + fmt.Println(mxid, puppet) + if puppet != nil { + return "@" + puppet.PhoneNumber() + } + } + return mxid + }, + BoldConverter: func(text string) string { + return fmt.Sprintf("*%s*", text) + }, + ItalicConverter: func(text string) string { + return fmt.Sprintf("_%s_", text) + }, + StrikethroughConverter: func(text string) string { + return fmt.Sprintf("~%s~", text) + }, + MonospaceConverter: func(text string) string { + return fmt.Sprintf("```%s```", text) + }, + MonospaceBlockConverter: func(text string) string { + return fmt.Sprintf("```%s```", text) + }, + }, + waReplString: map[*regexp.Regexp]string{ + italicRegex: "$1$2$3", + boldRegex: "$1$2$3", + strikethroughRegex: "$1$2$3", + }, + } + formatter.waReplFunc = map[*regexp.Regexp]func(string) string{ codeBlockRegex: func(str string) string { str = str[3 : len(str)-3] if strings.ContainsRune(str, '\n') { @@ -78,18 +91,47 @@ func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regex return fmt.Sprintf("%s", str) }, mentionRegex: func(str string) string { - jid := str[1:] + whatsappExt.NewUserSuffix - puppet := user.GetPuppetByJID(jid) - mxid := puppet.MXID - if jid == user.JID() { - mxid = user.ID - } - return fmt.Sprintf(`%s`, mxid, puppet.Displayname) - }, - }, map[*regexp.Regexp]func(string)string { - mentionRegex: func(str string) string { - puppet := user.GetPuppetByJID(str[1:] + whatsappExt.NewUserSuffix) - return puppet.Displayname + mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) + return fmt.Sprintf(`%s`, mxid, displayname) }, } + formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{ + mentionRegex: func(str string) string { + _, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) + return displayname + }, + } + return formatter +} + +func (formatter *Formatter) getMatrixInfoByJID(jid string) (mxid, displayname string) { + if user := formatter.bridge.GetUserByJID(jid); user != nil { + mxid = user.MXID + displayname = user.MXID + } else if puppet := formatter.bridge.GetPuppetByJID(jid); puppet != nil { + mxid = puppet.MXID + displayname = puppet.Displayname + } + return +} + +func (formatter *Formatter) ParseWhatsApp(content *gomatrix.Content) { + output := html.EscapeString(content.Body) + for regex, replacement := range formatter.waReplString { + output = regex.ReplaceAllString(output, replacement) + } + for regex, replacer := range formatter.waReplFunc { + output = regex.ReplaceAllStringFunc(output, replacer) + } + if output != content.Body { + content.FormattedBody = output + content.Format = gomatrix.FormatHTML + for regex, replacer := range formatter.waReplFuncText { + content.Body = regex.ReplaceAllStringFunc(content.Body, replacer) + } + } +} + +func (formatter *Formatter) ParseMatrix(html string) string { + return formatter.matrixHTMLParser.Parse(html) } diff --git a/main.go b/main.go index c5a05ac..50bac7e 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "os/signal" + "sync" "syscall" flag "maunium.net/go/mauflag" @@ -31,6 +32,7 @@ import ( ) var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String() +var baseConfigPath = flag.MakeFull("b", "base-config", "The path to the example config file.", "example-config.yaml").String() var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String() var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool() var wantHelp, _ = flag.MakeHelpFlag() @@ -58,29 +60,47 @@ func (bridge *Bridge) GenerateRegistration() { } type Bridge struct { - AppService *appservice.AppService + AS *appservice.AppService EventProcessor *appservice.EventProcessor MatrixHandler *MatrixHandler Config *config.Config DB *database.Database Log log.Logger + StateStore *AutosavingStateStore + Bot *appservice.IntentAPI + Formatter *Formatter - StateStore *AutosavingStateStore - - users map[types.MatrixUserID]*User - managementRooms map[types.MatrixRoomID]*User + usersByMXID map[types.MatrixUserID]*User + usersByJID map[types.WhatsAppID]*User + usersLock sync.Mutex + managementRooms map[types.MatrixRoomID]*User + managementRoomsLock sync.Mutex + portalsByMXID map[types.MatrixRoomID]*Portal + portalsByJID map[database.PortalKey]*Portal + portalsLock sync.Mutex + puppets map[types.WhatsAppID]*Puppet + puppetsLock sync.Mutex } func NewBridge() *Bridge { bridge := &Bridge{ - users: make(map[types.MatrixUserID]*User), + usersByMXID: make(map[types.MatrixUserID]*User), + usersByJID: make(map[types.WhatsAppID]*User), managementRooms: make(map[types.MatrixRoomID]*User), + portalsByMXID: make(map[types.MatrixRoomID]*Portal), + portalsByJID: make(map[database.PortalKey]*Portal), + puppets: make(map[types.WhatsAppID]*Puppet), } - var err error + err := config.Update(*configPath, *baseConfigPath) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to update config:", err) + os.Exit(10) + } + bridge.Config, err = config.Load(*configPath) if err != nil { fmt.Fprintln(os.Stderr, "Failed to load config:", err) - os.Exit(10) + os.Exit(11) } return bridge } @@ -88,46 +108,55 @@ func NewBridge() *Bridge { func (bridge *Bridge) Init() { var err error - bridge.AppService, err = bridge.Config.MakeAppService() + bridge.AS, err = bridge.Config.MakeAppService() if err != nil { fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err) - os.Exit(11) + os.Exit(12) } - bridge.AppService.Init() - bridge.Log = bridge.AppService.Log + bridge.AS.Init() + bridge.Bot = bridge.AS.BotIntent() + + bridge.Log = log.Create() + bridge.Config.Logging.Configure(bridge.Log) log.DefaultLogger = bridge.Log.(*log.BasicLogger) - bridge.AppService.Log = log.Sub("Matrix") + err = log.OpenFile() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to open log file:", err) + os.Exit(13) + } + bridge.AS.Log = log.Sub("Matrix") bridge.Log.Debugln("Initializing state store") bridge.StateStore = NewAutosavingStateStore(bridge.Config.AppService.StateStore) err = bridge.StateStore.Load() if err != nil { bridge.Log.Fatalln("Failed to load state store:", err) - os.Exit(12) + os.Exit(14) } - bridge.AppService.StateStore = bridge.StateStore + bridge.AS.StateStore = bridge.StateStore bridge.Log.Debugln("Initializing database") bridge.DB, err = database.New(bridge.Config.AppService.Database.URI) if err != nil { bridge.Log.Fatalln("Failed to initialize database:", err) - os.Exit(13) + os.Exit(15) } bridge.Log.Debugln("Initializing Matrix event processor") - bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService) + bridge.EventProcessor = appservice.NewEventProcessor(bridge.AS) bridge.Log.Debugln("Initializing Matrix event handler") bridge.MatrixHandler = NewMatrixHandler(bridge) + bridge.Formatter = NewFormatter(bridge) } func (bridge *Bridge) Start() { err := bridge.DB.CreateTables() if err != nil { bridge.Log.Fatalln("Failed to create database tables:", err) - os.Exit(14) + os.Exit(16) } bridge.Log.Debugln("Starting application service HTTP server") - go bridge.AppService.Start() + go bridge.AS.Start() bridge.Log.Debugln("Starting event processor") go bridge.EventProcessor.Start() go bridge.UpdateBotProfile() @@ -140,18 +169,18 @@ func (bridge *Bridge) UpdateBotProfile() { var err error if botConfig.Avatar == "remove" { - err = bridge.AppService.BotIntent().SetAvatarURL("") + err = bridge.AS.BotIntent().SetAvatarURL("") } else if len(botConfig.Avatar) > 0 { - err = bridge.AppService.BotIntent().SetAvatarURL(botConfig.Avatar) + err = bridge.AS.BotIntent().SetAvatarURL(botConfig.Avatar) } if err != nil { bridge.Log.Warnln("Failed to update bot avatar:", err) } if botConfig.Displayname == "remove" { - err = bridge.AppService.BotIntent().SetDisplayName("") + err = bridge.AS.BotIntent().SetDisplayName("") } else if len(botConfig.Avatar) > 0 { - err = bridge.AppService.BotIntent().SetDisplayName(botConfig.Displayname) + err = bridge.AS.BotIntent().SetDisplayName(botConfig.Displayname) } if err != nil { bridge.Log.Warnln("Failed to update bot displayname:", err) @@ -165,7 +194,7 @@ func (bridge *Bridge) StartUsers() { } func (bridge *Bridge) Stop() { - bridge.AppService.Stop() + bridge.AS.Stop() bridge.EventProcessor.Stop() err := bridge.StateStore.Save() if err != nil { diff --git a/matrix.go b/matrix.go index adb76d4..de63727 100644 --- a/matrix.go +++ b/matrix.go @@ -35,7 +35,7 @@ type MatrixHandler struct { func NewMatrixHandler(bridge *Bridge) *MatrixHandler { handler := &MatrixHandler{ bridge: bridge, - as: bridge.AppService, + as: bridge.AS, log: bridge.Log.Sub("Matrix"), cmd: NewCommandHandler(bridge), } @@ -50,7 +50,7 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler { func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) { intent := mx.as.BotIntent() - user := mx.bridge.GetUser(evt.Sender) + user := mx.bridge.GetUserByMXID(evt.Sender) if user == nil { return } @@ -85,7 +85,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) { for mxid, _ := range members.Joined { if mxid == intent.UserID || mxid == evt.Sender { continue - } else if _, _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok { + } else if _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok { hasPuppets = true continue } @@ -96,7 +96,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) { } if !hasPuppets { - user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender)) + 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.") mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) @@ -110,12 +110,12 @@ func (mx *MatrixHandler) HandleMembership(evt *gomatrix.Event) { } func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) { - user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender)) - if user == nil || !user.Whitelisted { + user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) + if user == nil || !user.Whitelisted || !user.IsLoggedIn() { return } - portal := user.GetPortalByMXID(evt.RoomID) + portal := mx.bridge.GetPortalByMXID(evt.RoomID) if portal == nil || portal.IsPrivateChat() { return } @@ -124,7 +124,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) { var err error switch evt.Type { case gomatrix.StateRoomName: - resp, err = user.Conn.UpdateGroupSubject(evt.Content.Name, portal.JID) + resp, err = user.Conn.UpdateGroupSubject(evt.Content.Name, portal.Key.JID) case gomatrix.StateRoomAvatar: return case gomatrix.StateTopic: @@ -140,7 +140,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) { func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) { roomID := types.MatrixRoomID(evt.RoomID) - user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender)) + user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) if !user.Whitelisted { return @@ -158,8 +158,12 @@ func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) { } } - portal := user.GetPortalByMXID(roomID) + if !user.IsLoggedIn() { + return + } + + portal := mx.bridge.GetPortalByMXID(roomID) if portal != nil { - portal.HandleMatrixMessage(evt) + portal.HandleMatrixMessage(user, evt) } } diff --git a/portal.go b/portal.go index 1538431..e597d1c 100644 --- a/portal.go +++ b/portal.go @@ -20,7 +20,6 @@ import ( "bytes" "encoding/hex" "fmt" - "html" "image" "image/gif" "image/jpeg" @@ -41,57 +40,56 @@ import ( "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) -func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { - user.portalsLock.Lock() - defer user.portalsLock.Unlock() - portal, ok := user.portalsByMXID[mxid] +func (bridge *Bridge) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { + bridge.portalsLock.Lock() + defer bridge.portalsLock.Unlock() + portal, ok := bridge.portalsByMXID[mxid] if !ok { - dbPortal := user.bridge.DB.Portal.GetByMXID(mxid) - if dbPortal == nil || dbPortal.Owner != user.ID { + dbPortal := bridge.DB.Portal.GetByMXID(mxid) + if dbPortal == nil { return nil } - portal = user.NewPortal(dbPortal) - user.portalsByJID[portal.JID] = portal + portal = bridge.NewPortal(dbPortal) + bridge.portalsByJID[portal.Key] = portal if len(portal.MXID) > 0 { - user.portalsByMXID[portal.MXID] = portal + bridge.portalsByMXID[portal.MXID] = portal } } return portal } -func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal { - user.portalsLock.Lock() - defer user.portalsLock.Unlock() - portal, ok := user.portalsByJID[jid] +func (bridge *Bridge) GetPortalByJID(key database.PortalKey) *Portal { + bridge.portalsLock.Lock() + defer bridge.portalsLock.Unlock() + portal, ok := bridge.portalsByJID[key] if !ok { - dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid) + dbPortal := bridge.DB.Portal.GetByJID(key) if dbPortal == nil { - dbPortal = user.bridge.DB.Portal.New() - dbPortal.JID = jid - dbPortal.Owner = user.ID + dbPortal = bridge.DB.Portal.New() + dbPortal.Key = key dbPortal.Insert() } - portal = user.NewPortal(dbPortal) - user.portalsByJID[portal.JID] = portal + portal = bridge.NewPortal(dbPortal) + bridge.portalsByJID[portal.Key] = portal if len(portal.MXID) > 0 { - user.portalsByMXID[portal.MXID] = portal + bridge.portalsByMXID[portal.MXID] = portal } } return portal } -func (user *User) GetAllPortals() []*Portal { - user.portalsLock.Lock() - defer user.portalsLock.Unlock() - dbPortals := user.bridge.DB.Portal.GetAll(user.ID) +func (bridge *Bridge) GetAllPortals() []*Portal { + bridge.portalsLock.Lock() + defer bridge.portalsLock.Unlock() + dbPortals := bridge.DB.Portal.GetAll() output := make([]*Portal, len(dbPortals)) for index, dbPortal := range dbPortals { - portal, ok := user.portalsByJID[dbPortal.JID] + portal, ok := bridge.portalsByJID[dbPortal.Key] if !ok { - portal = user.NewPortal(dbPortal) - user.portalsByJID[dbPortal.JID] = portal + portal = bridge.NewPortal(dbPortal) + bridge.portalsByJID[portal.Key] = portal if len(dbPortal.MXID) > 0 { - user.portalsByMXID[dbPortal.MXID] = portal + bridge.portalsByMXID[dbPortal.MXID] = portal } } output[index] = portal @@ -99,19 +97,17 @@ func (user *User) GetAllPortals() []*Portal { return output } -func (user *User) NewPortal(dbPortal *database.Portal) *Portal { +func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal { return &Portal{ Portal: dbPortal, - user: user, - bridge: user.bridge, - log: user.log.Sub(fmt.Sprintf("Portal/%s", dbPortal.JID)), + bridge: bridge, + log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)), } } type Portal struct { *database.Portal - user *User bridge *Bridge log log.Logger @@ -126,9 +122,16 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { changed = true } for _, participant := range metadata.Participants { - puppet := portal.user.GetPuppetByJID(participant.JID) + puppet := portal.bridge.GetPuppetByJID(participant.JID) puppet.Intent().EnsureJoined(portal.MXID) + user := portal.bridge.GetUserByJID(participant.JID) + if !portal.bridge.AS.StateStore.IsInvited(portal.MXID, user.MXID) { + portal.MainIntent().InviteUser(portal.MXID, &gomatrix.ReqInviteUser{ + UserID: user.MXID, + }) + } + expectedLevel := 0 if participant.IsSuperAdmin { expectedLevel = 95 @@ -136,9 +139,8 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { expectedLevel = 50 } changed = levels.EnsureUserLevel(puppet.MXID, expectedLevel) || changed - - if participant.JID == portal.user.JID() { - changed = levels.EnsureUserLevel(portal.user.ID, expectedLevel) || changed + if user != nil { + changed = levels.EnsureUserLevel(user.MXID, expectedLevel) || changed } } if changed { @@ -146,10 +148,10 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { } } -func (portal *Portal) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool { +func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInfo) bool { if avatar == nil { var err error - avatar, err = portal.user.Conn.GetProfilePicThumb(portal.JID) + avatar, err = user.Conn.GetProfilePicThumb(portal.Key.JID) if err != nil { portal.log.Errorln(err) return false @@ -184,7 +186,7 @@ func (portal *Portal) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool { func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool { if portal.Name != name { - intent := portal.user.GetPuppetByJID(setBy).Intent() + intent := portal.bridge.GetPuppetByJID(setBy).Intent() _, err := intent.SetRoomName(portal.MXID, name) if err == nil { portal.Name = name @@ -197,7 +199,7 @@ func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool { func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool { if portal.Topic != topic { - intent := portal.user.GetPuppetByJID(setBy).Intent() + intent := portal.bridge.GetPuppetByJID(setBy).Intent() _, err := intent.SetRoomTopic(portal.MXID, topic) if err == nil { portal.Topic = topic @@ -208,8 +210,8 @@ func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool { return false } -func (portal *Portal) UpdateMetadata() bool { - metadata, err := portal.user.Conn.GetGroupMetaData(portal.JID) +func (portal *Portal) UpdateMetadata(user *User) bool { + metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID) if err != nil { portal.log.Errorln(err) return false @@ -221,25 +223,23 @@ func (portal *Portal) UpdateMetadata() bool { return update } -func (portal *Portal) Sync(contact whatsapp.Contact) { +func (portal *Portal) Sync(user *User, contact whatsapp.Contact) { + if portal.IsPrivateChat() { + return + } + if len(portal.MXID) == 0 { - if !portal.IsPrivateChat() { - portal.Name = contact.Name - } - err := portal.CreateMatrixRoom() + portal.Name = contact.Name + err := portal.CreateMatrixRoom([]string{user.MXID}) if err != nil { portal.log.Errorln("Failed to create portal room:", err) return } } - if portal.IsPrivateChat() { - return - } - update := false - update = portal.UpdateMetadata() || update - update = portal.UpdateAvatar(nil) || update + update = portal.UpdateMetadata(user) || update + update = portal.UpdateAvatar(user, nil) || update if update { portal.Update() } @@ -277,11 +277,12 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { } changed := false for _, jid := range jids { - puppet := portal.user.GetPuppetByJID(jid) + puppet := portal.bridge.GetPuppetByJID(jid) changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed - if jid == portal.user.JID() { - changed = levels.EnsureUserLevel(portal.user.ID, newLevel) || changed + user := portal.bridge.GetUserByJID(jid) + if user != nil { + changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed } } if changed { @@ -312,15 +313,15 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) { newLevel = 50 } changed := false - changed = levels.EnsureEventLevel(gomatrix.StateRoomName, true, newLevel) || changed - changed = levels.EnsureEventLevel(gomatrix.StateRoomAvatar, true, newLevel) || changed - changed = levels.EnsureEventLevel(gomatrix.StateTopic, true, newLevel) || changed + changed = levels.EnsureEventLevel(gomatrix.StateRoomName, newLevel) || changed + changed = levels.EnsureEventLevel(gomatrix.StateRoomAvatar, newLevel) || changed + changed = levels.EnsureEventLevel(gomatrix.StateTopic, newLevel) || changed if changed { portal.MainIntent().SetPowerLevels(portal.MXID, levels) } } -func (portal *Portal) CreateMatrixRoom() error { +func (portal *Portal) CreateMatrixRoom(invite []string) error { portal.roomCreateLock.Lock() defer portal.roomCreateLock.Unlock() if len(portal.MXID) > 0 { @@ -330,7 +331,6 @@ func (portal *Portal) CreateMatrixRoom() error { name := portal.Name topic := portal.Topic isPrivateChat := false - invite := []string{portal.user.ID} if portal.IsPrivateChat() { name = "" topic = "WhatsApp private chat" @@ -360,18 +360,18 @@ func (portal *Portal) CreateMatrixRoom() error { } func (portal *Portal) IsPrivateChat() bool { - return strings.HasSuffix(portal.JID, whatsappExt.NewUserSuffix) + return strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix) } func (portal *Portal) MainIntent() *appservice.IntentAPI { if portal.IsPrivateChat() { - return portal.user.GetPuppetByJID(portal.JID).Intent() + return portal.bridge.GetPuppetByJID(portal.Key.JID).Intent() } - return portal.bridge.AppService.BotIntent() + return portal.bridge.AS.BotIntent() } func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool { - msg := portal.bridge.DB.Message.GetByJID(portal.Owner, id) + msg := portal.bridge.DB.Message.GetByJID(portal.Key, id) if msg != nil { return true } @@ -380,7 +380,7 @@ func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool { func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) { msg := portal.bridge.DB.Message.New() - msg.Owner = portal.Owner + msg.Chat = portal.Key msg.JID = jid msg.MXID = mxid msg.Insert() @@ -392,7 +392,7 @@ func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.In // TODO handle own messages in private chats properly return nil } - return portal.user.GetPuppetByJID(portal.user.JID()).Intent() + return portal.bridge.GetPuppetByJID(portal.Key.Receiver).Intent() } else if portal.IsPrivateChat() { return portal.MainIntent() } else if len(info.SenderJid) == 0 { @@ -402,14 +402,14 @@ func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.In return nil } } - return portal.user.GetPuppetByJID(info.SenderJid).Intent() + return portal.bridge.GetPuppetByJID(info.SenderJid).Intent() } func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageInfo) { if len(info.QuotedMessageID) == 0 { return } - message := portal.bridge.DB.Message.GetByJID(portal.Owner, info.QuotedMessageID) + message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID) if message != nil { event, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) if err != nil { @@ -421,29 +421,12 @@ func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageI return } -func (portal *Portal) FormatWhatsAppMessage(content *gomatrix.Content) { - output := html.EscapeString(content.Body) - for regex, replacement := range portal.user.waReplString { - output = regex.ReplaceAllString(output, replacement) - } - for regex, replacer := range portal.user.waReplFunc { - output = regex.ReplaceAllStringFunc(output, replacer) - } - if output != content.Body { - content.FormattedBody = output - content.Format = gomatrix.FormatHTML - for regex, replacer := range portal.user.waReplFuncText { - content.Body = regex.ReplaceAllStringFunc(content.Body, replacer) - } - } -} - -func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) { +func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) { if portal.IsDuplicate(message.Info.Id) { return } - err := portal.CreateMatrixRoom() + err := portal.CreateMatrixRoom([]string{source.MXID}) if err != nil { portal.log.Errorln("Failed to create portal room:", err) return @@ -459,7 +442,7 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) { MsgType: gomatrix.MsgText, } - portal.FormatWhatsAppMessage(content) + portal.bridge.Formatter.ParseWhatsApp(content) portal.SetReply(content, message.Info) intent.UserTyping(portal.MXID, false, 0) @@ -472,12 +455,12 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) { portal.log.Debugln("Handled message", message.Info.Id, "->", resp.EventID) } -func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) { +func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) { if portal.IsDuplicate(info.Id) { return } - err := portal.CreateMatrixRoom() + err := portal.CreateMatrixRoom([]string{source.MXID}) if err != nil { portal.log.Errorln("Failed to create portal room:", err) return @@ -559,7 +542,7 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn MsgType: gomatrix.MsgNotice, } - portal.FormatWhatsAppMessage(captionContent) + portal.bridge.Formatter.ParseWhatsApp(captionContent) _, err := intent.SendMassagedMessageEvent(portal.MXID, gomatrix.EventMessage, captionContent, ts) if err != nil { @@ -612,7 +595,7 @@ func (portal *Portal) downloadThumbnail(evt *gomatrix.Event) []byte { return buf.Bytes() } -func (portal *Portal) preprocessMatrixMedia(evt *gomatrix.Event, mediaType whatsapp.MediaType) *MediaUpload { +func (portal *Portal) preprocessMatrixMedia(sender *User, evt *gomatrix.Event, mediaType whatsapp.MediaType) *MediaUpload { if evt.Content.Info == nil { evt.Content.Info = &gomatrix.FileInfo{} } @@ -630,7 +613,7 @@ func (portal *Portal) preprocessMatrixMedia(evt *gomatrix.Event, mediaType whats return nil } - url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := portal.user.Conn.Upload(bytes.NewReader(content), mediaType) + url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(content), mediaType) if err != nil { portal.log.Errorfln("Failed to upload media in %s: %v", evt.ID, err) return nil @@ -657,8 +640,8 @@ type MediaUpload struct { Thumbnail []byte } -func (portal *Portal) GetMessage(jid types.WhatsAppMessageID) *waProto.WebMessageInfo { - node, err := portal.user.Conn.LoadMessagesBefore(portal.JID, jid, 1) +func (portal *Portal) GetMessage(user *User, jid types.WhatsAppMessageID) *waProto.WebMessageInfo { + node, err := user.Conn.LoadMessagesBefore(portal.Key.JID, jid, 1) if err != nil { return nil } @@ -670,7 +653,7 @@ func (portal *Portal) GetMessage(jid types.WhatsAppMessageID) *waProto.WebMessag if !ok { return nil } - node, err = portal.user.Conn.LoadMessagesAfter(portal.JID, msg.GetKey().GetId(), 1) + node, err = user.Conn.LoadMessagesAfter(portal.Key.JID, msg.GetKey().GetId(), 1) if err != nil { return nil } @@ -682,7 +665,11 @@ func (portal *Portal) GetMessage(jid types.WhatsAppMessageID) *waProto.WebMessag return msg } -func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { +func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) { + if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver { + return + } + ts := uint64(evt.Timestamp / 1000) status := waProto.WebMessageInfo_ERROR fromMe := true @@ -690,7 +677,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { Key: &waProto.MessageKey{ FromMe: &fromMe, Id: makeMessageID(), - RemoteJid: &portal.JID, + RemoteJid: &portal.Key.JID, }, MessageTimestamp: &ts, Message: &waProto.Message{}, @@ -702,12 +689,12 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { evt.Content.RemoveReplyFallback() msg := portal.bridge.DB.Message.GetByMXID(replyToID) if msg != nil { - origMsg := portal.GetMessage(msg.JID) + origMsg := portal.GetMessage(sender, msg.JID) if origMsg != nil { ctxInfo.StanzaId = &msg.JID replyMsgSender := origMsg.GetParticipant() if origMsg.GetKey().GetFromMe() { - replyMsgSender = portal.user.JID() + replyMsgSender = sender.JID } ctxInfo.Participant = &replyMsgSender ctxInfo.QuotedMessage = []*waProto.Message{origMsg.Message} @@ -719,7 +706,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { case gomatrix.MsgText, gomatrix.MsgEmote: text := evt.Content.Body if evt.Content.Format == gomatrix.FormatHTML { - text = portal.user.htmlParser.Parse(evt.Content.FormattedBody) + text = portal.bridge.Formatter.ParseMatrix(evt.Content.FormattedBody) } if evt.Content.MsgType == gomatrix.MsgEmote { text = "/me " + text @@ -737,7 +724,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { info.Message.Conversation = &text } case gomatrix.MsgImage: - media := portal.preprocessMatrixMedia(evt, whatsapp.MediaImage) + media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaImage) if media == nil { return } @@ -752,7 +739,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { FileLength: &media.FileLength, } case gomatrix.MsgVideo: - media := portal.preprocessMatrixMedia(evt, whatsapp.MediaVideo) + media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaVideo) if media == nil { return } @@ -769,7 +756,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { FileLength: &media.FileLength, } case gomatrix.MsgAudio: - media := portal.preprocessMatrixMedia(evt, whatsapp.MediaAudio) + media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaAudio) if media == nil { return } @@ -784,7 +771,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { FileLength: &media.FileLength, } case gomatrix.MsgFile: - media := portal.preprocessMatrixMedia(evt, whatsapp.MediaDocument) + media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaDocument) if media == nil { return } @@ -800,7 +787,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { portal.log.Debugln("Unhandled Matrix event:", evt) return } - err = portal.user.Conn.Send(info) + err = sender.Conn.Send(info) portal.MarkHandled(info.GetKey().GetId(), evt.ID) if err != nil { portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) diff --git a/puppet.go b/puppet.go index 5b58511..587cabc 100644 --- a/puppet.go +++ b/puppet.go @@ -30,105 +30,83 @@ import ( "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) -func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) { +func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.WhatsAppID, bool) { userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", - bridge.Config.Bridge.FormatUsername("(.+)", "([0-9]+)"), + bridge.Config.Bridge.FormatUsername("([0-9]+)"), bridge.Config.Homeserver.Domain)) if err != nil { bridge.Log.Warnln("Failed to compile puppet user ID regex:", err) - return "", "", false + return "", false } match := userIDRegex.FindStringSubmatch(string(mxid)) - if match == nil || len(match) != 3 { - return "", "", false + if match == nil || len(match) != 2 { + return "", false } - receiver := types.MatrixUserID(match[1]) - receiver = strings.Replace(receiver, "=40", "@", 1) - colonIndex := strings.LastIndex(receiver, "=3") - receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):] jid := types.WhatsAppID(match[2] + whatsappExt.NewUserSuffix) - return receiver, jid, true + return jid, true } func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { - receiver, jid, ok := bridge.ParsePuppetMXID(mxid) + jid, ok := bridge.ParsePuppetMXID(mxid) if !ok { return nil } - user := bridge.GetUser(receiver) - if user == nil { - return nil - } - - return user.GetPuppetByJID(jid) + return bridge.GetPuppetByJID(jid) } -func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { - receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid) - if !ok || receiver != user.ID { - return nil - } - - return user.GetPuppetByJID(jid) -} - -func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet { - user.puppetsLock.Lock() - defer user.puppetsLock.Unlock() - puppet, ok := user.puppets[jid] +func (bridge *Bridge) GetPuppetByJID(jid types.WhatsAppID) *Puppet { + bridge.puppetsLock.Lock() + defer bridge.puppetsLock.Unlock() + puppet, ok := bridge.puppets[jid] if !ok { - dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID) + dbPuppet := bridge.DB.Puppet.Get(jid) if dbPuppet == nil { - dbPuppet = user.bridge.DB.Puppet.New() + dbPuppet = bridge.DB.Puppet.New() dbPuppet.JID = jid - dbPuppet.Receiver = user.ID dbPuppet.Insert() } - puppet = user.NewPuppet(dbPuppet) - user.puppets[puppet.JID] = puppet + puppet = bridge.NewPuppet(dbPuppet) + bridge.puppets[puppet.JID] = puppet } return puppet } -func (user *User) GetAllPuppets() []*Puppet { - user.puppetsLock.Lock() - defer user.puppetsLock.Unlock() - dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID) +func (bridge *Bridge) GetAllPuppets() []*Puppet { + bridge.puppetsLock.Lock() + defer bridge.puppetsLock.Unlock() + dbPuppets := bridge.DB.Puppet.GetAll() output := make([]*Puppet, len(dbPuppets)) for index, dbPuppet := range dbPuppets { - puppet, ok := user.puppets[dbPuppet.JID] + puppet, ok := bridge.puppets[dbPuppet.JID] if !ok { - puppet = user.NewPuppet(dbPuppet) - user.puppets[dbPuppet.JID] = puppet + puppet = bridge.NewPuppet(dbPuppet) + bridge.puppets[dbPuppet.JID] = puppet } output[index] = puppet } return output } -func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet { +func (bridge *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet { return &Puppet{ Puppet: dbPuppet, - user: user, - bridge: user.bridge, - log: user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), + bridge: bridge, + log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), MXID: fmt.Sprintf("@%s:%s", - user.bridge.Config.Bridge.FormatUsername( - dbPuppet.Receiver, + bridge.Config.Bridge.FormatUsername( strings.Replace( dbPuppet.JID, whatsappExt.NewUserSuffix, "", 1)), - user.bridge.Config.Homeserver.Domain), + bridge.Config.Homeserver.Domain), } } type Puppet struct { *database.Puppet - user *User bridge *Bridge log log.Logger @@ -143,13 +121,13 @@ func (puppet *Puppet) PhoneNumber() string { } func (puppet *Puppet) Intent() *appservice.IntentAPI { - return puppet.bridge.AppService.Intent(puppet.MXID) + return puppet.bridge.AS.Intent(puppet.MXID) } -func (puppet *Puppet) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool { +func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicInfo) bool { if avatar == nil { var err error - avatar, err = puppet.user.Conn.GetProfilePicThumb(puppet.JID) + avatar, err = source.Conn.GetProfilePicThumb(puppet.JID) if err != nil { puppet.log.Errorln(err) return false @@ -184,11 +162,11 @@ func (puppet *Puppet) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool { return true } -func (puppet *Puppet) Sync(contact whatsapp.Contact) { +func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) { puppet.Intent().EnsureRegistered() - if contact.Jid == puppet.user.JID() { - contact.Notify = puppet.user.Conn.Info.Pushname + if contact.Jid == source.JID { + contact.Notify = source.Conn.Info.Pushname } newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact) if puppet.Displayname != newName { @@ -201,7 +179,7 @@ func (puppet *Puppet) Sync(contact whatsapp.Contact) { } } - if puppet.UpdateAvatar(nil) { + if puppet.UpdateAvatar(source, nil) { puppet.Update() } } diff --git a/user.go b/user.go index 385e8ba..2155752 100644 --- a/user.go +++ b/user.go @@ -17,14 +17,11 @@ package main import ( - "regexp" "strings" - "sync" "time" "github.com/Rhymen/go-whatsapp" "github.com/skip2/go-qrcode" - "maunium.net/go/gomatrix/format" log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/types" @@ -41,31 +38,42 @@ type User struct { Admin bool Whitelisted bool jid string - - portalsByMXID map[types.MatrixRoomID]*Portal - portalsByJID map[types.WhatsAppID]*Portal - portalsLock sync.Mutex - puppets map[types.WhatsAppID]*Puppet - puppetsLock sync.Mutex - - htmlParser *format.HTMLParser - - waReplString map[*regexp.Regexp]string - waReplFunc map[*regexp.Regexp]func(string) string - waReplFuncText map[*regexp.Regexp]func(string) string } -func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User { - user, ok := bridge.users[userID] +func (bridge *Bridge) GetUserByMXID(userID types.MatrixUserID) *User { + bridge.usersLock.Lock() + defer bridge.usersLock.Unlock() + user, ok := bridge.usersByMXID[userID] if !ok { - dbUser := bridge.DB.User.Get(userID) + dbUser := bridge.DB.User.GetByMXID(userID) if dbUser == nil { dbUser = bridge.DB.User.New() - dbUser.ID = userID + dbUser.MXID = userID dbUser.Insert() } user = bridge.NewUser(dbUser) - bridge.users[user.ID] = user + bridge.usersByMXID[user.MXID] = user + if len(user.ManagementRoom) > 0 { + bridge.managementRooms[user.ManagementRoom] = user + } + } + return user +} + + +func (bridge *Bridge) GetUserByJID(userID types.WhatsAppID) *User { + bridge.usersLock.Lock() + defer bridge.usersLock.Unlock() + user, ok := bridge.usersByJID[userID] + if !ok { + dbUser := bridge.DB.User.GetByMXID(userID) + if dbUser == nil { + dbUser = bridge.DB.User.New() + dbUser.MXID = userID + dbUser.Insert() + } + user = bridge.NewUser(dbUser) + bridge.usersByJID[user.JID] = user if len(user.ManagementRoom) > 0 { bridge.managementRooms[user.ManagementRoom] = user } @@ -74,13 +82,15 @@ func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User { } func (bridge *Bridge) GetAllUsers() []*User { + bridge.usersLock.Lock() + defer bridge.usersLock.Unlock() dbUsers := bridge.DB.User.GetAll() output := make([]*User, len(dbUsers)) for index, dbUser := range dbUsers { - user, ok := bridge.users[dbUser.ID] + user, ok := bridge.usersByMXID[dbUser.MXID] if !ok { user = bridge.NewUser(dbUser) - bridge.users[user.ID] = user + bridge.usersByMXID[user.MXID] = user if len(user.ManagementRoom) > 0 { bridge.managementRooms[user.ManagementRoom] = user } @@ -94,15 +104,10 @@ func (bridge *Bridge) NewUser(dbUser *database.User) *User { user := &User{ User: dbUser, bridge: bridge, - log: bridge.Log.Sub("User").Sub(string(dbUser.ID)), - portalsByMXID: make(map[types.MatrixRoomID]*Portal), - portalsByJID: make(map[types.WhatsAppID]*Portal), - puppets: make(map[types.WhatsAppID]*Puppet), + log: bridge.Log.Sub("User").Sub(string(dbUser.MXID)), } - user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.ID) - user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.ID) - user.htmlParser = user.newHTMLParser() - user.waReplString, user.waReplFunc, user.waReplFuncText = user.newWhatsAppFormatMaps() + user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID) + user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.MXID) return user } @@ -152,7 +157,6 @@ func (user *User) RestoreSession() bool { sess, err := user.Conn.RestoreSession(*user.Session) if err != nil { user.log.Errorln("Failed to restore session:", err) - //user.SetSession(nil) return false } user.SetSession(&sess) @@ -162,8 +166,12 @@ func (user *User) RestoreSession() bool { return false } +func (user *User) IsLoggedIn() bool { + return user.Conn != nil +} + func (user *User) Login(roomID types.MatrixRoomID) { - bot := user.bridge.AppService.BotClient() + bot := user.bridge.AS.BotClient() qrChan := make(chan string, 2) go func() { @@ -194,38 +202,24 @@ func (user *User) Login(roomID types.MatrixRoomID) { qrChan <- "error" return } + 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) JID() string { - if user.Conn == nil { - return "" - } - if len(user.jid) == 0 { - user.jid = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) - } - return user.jid -} - 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.GetPuppetByJID(contact.Jid) - puppet.Sync(contact) + puppet := user.bridge.GetPuppetByJID(contact.Jid) + puppet.Sync(user, contact) + } else { + portal := user.bridge.GetPortalByJID(database.GroupPortalKey(contact.Jid)) + portal.Sync(user, contact) } - - if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") { - // No messages sent -> don't bridge - continue - } - - portal := user.GetPortalByJID(contact.Jid) - portal.Sync(contact) } } @@ -237,33 +231,41 @@ func (user *User) HandleJSONParseError(err error) { user.log.Errorln("WhatsApp JSON parse error:", err) } +func (user *User) PortalKey(jid types.WhatsAppID) database.PortalKey { + return database.NewPortalKey(jid, user.JID) +} + +func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal { + return user.bridge.GetPortalByJID(user.PortalKey(jid)) +} + func (user *User) HandleTextMessage(message whatsapp.TextMessage) { portal := user.GetPortalByJID(message.Info.RemoteJid) - portal.HandleTextMessage(message) + portal.HandleTextMessage(user, message) } func (user *User) HandleImageMessage(message whatsapp.ImageMessage) { portal := user.GetPortalByJID(message.Info.RemoteJid) - portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Caption) + portal.HandleMediaMessage(user, message.Download, message.Thumbnail, message.Info, message.Type, message.Caption) } func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { portal := user.GetPortalByJID(message.Info.RemoteJid) - portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Caption) + portal.HandleMediaMessage(user, message.Download, message.Thumbnail, message.Info, message.Type, message.Caption) } func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) { portal := user.GetPortalByJID(message.Info.RemoteJid) - portal.HandleMediaMessage(message.Download, nil, message.Info, message.Type, "") + portal.HandleMediaMessage(user, message.Download, nil, message.Info, message.Type, "") } func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) { portal := user.GetPortalByJID(message.Info.RemoteJid) - portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Title) + portal.HandleMediaMessage(user, message.Download, message.Thumbnail, message.Info, message.Type, message.Title) } func (user *User) HandlePresence(info whatsappExt.Presence) { - puppet := user.GetPuppetByJID(info.SenderJID) + puppet := user.bridge.GetPuppetByJID(info.SenderJID) switch info.Status { case whatsappExt.PresenceUnavailable: puppet.Intent().SetPresence("offline") @@ -277,6 +279,12 @@ func (user *User) HandlePresence(info whatsappExt.Presence) { } case whatsappExt.PresenceComposing: portal := user.GetPortalByJID(info.JID) + if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + if puppet.typingIn == portal.MXID { + return + } + puppet.Intent().UserTyping(puppet.typingIn, false, 0) + } puppet.typingIn = portal.MXID puppet.typingAt = time.Now().Unix() puppet.Intent().UserTyping(portal.MXID, true, 15*1000) @@ -290,9 +298,9 @@ func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) { return } - intent := user.GetPuppetByJID(info.SenderJID).Intent() + intent := user.bridge.GetPuppetByJID(info.SenderJID).Intent() for _, id := range info.IDs { - msg := user.bridge.DB.Message.GetByJID(user.ID, id) + msg := user.bridge.DB.Message.GetByJID(portal.Key, id) if msg == nil { continue } @@ -308,11 +316,11 @@ func (user *User) HandleCommand(cmd whatsappExt.Command) { switch cmd.Type { case whatsappExt.CommandPicture: if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) { - puppet := user.GetPuppetByJID(cmd.JID) - puppet.UpdateAvatar(cmd.ProfilePicInfo) + puppet := user.bridge.GetPuppetByJID(cmd.JID) + puppet.UpdateAvatar(user, cmd.ProfilePicInfo) } else { portal := user.GetPortalByJID(cmd.JID) - portal.UpdateAvatar(cmd.ProfilePicInfo) + portal.UpdateAvatar(user, cmd.ProfilePicInfo) } } } diff --git a/vendor/maunium.net/go/gomatrix/client.go b/vendor/maunium.net/go/gomatrix/client.go index 14549ba..0806138 100644 --- a/vendor/maunium.net/go/gomatrix/client.go +++ b/vendor/maunium.net/go/gomatrix/client.go @@ -463,7 +463,7 @@ func (cli *Client) SetAvatarURL(url string) (err error) { // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}) (resp *RespSendEvent, err error) { txnID := txnID() - urlPath := cli.BuildURL("rooms", roomID, "send", string(eventType), txnID) + urlPath := cli.BuildURL("rooms", roomID, "send", eventType.String(), txnID) _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) return } @@ -472,7 +472,7 @@ func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJ // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. func (cli *Client) SendMassagedMessageEvent(roomID string, eventType EventType, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { txnID := txnID() - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", string(eventType), txnID}, map[string]string{ + urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", eventType.String(), txnID}, map[string]string{ "ts": strconv.FormatInt(ts, 10), }) _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) @@ -482,7 +482,7 @@ func (cli *Client) SendMassagedMessageEvent(roomID string, eventType EventType, // SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { - urlPath := cli.BuildURL("rooms", roomID, "state", string(eventType), stateKey) + urlPath := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey) _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) return } @@ -490,7 +490,7 @@ func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey s // SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", string(eventType), stateKey}, map[string]string{ + urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{ "ts": strconv.FormatInt(ts, 10), }) _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) @@ -500,7 +500,7 @@ func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, st // SendText sends an m.room.message event into the given room with a msgtype of m.text // See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, "m.room.message", Content{ + return cli.SendMessageEvent(roomID, EventMessage, Content{ MsgType: MsgText, Body: text, }) @@ -509,7 +509,7 @@ func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) { // SendImage sends an m.room.message event into the given room with a msgtype of m.image // See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, "m.room.message", Content{ + return cli.SendMessageEvent(roomID, EventMessage, Content{ MsgType: MsgImage, Body: body, URL: url, @@ -519,7 +519,7 @@ func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) { // SendVideo sends an m.room.message event into the given room with a msgtype of m.video // See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, "m.room.message", Content{ + return cli.SendMessageEvent(roomID, EventMessage, Content{ MsgType: MsgVideo, Body: body, URL: url, @@ -529,7 +529,7 @@ func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) { // SendNotice sends an m.room.message event into the given room with a msgtype of m.notice // See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, "m.room.message", Content{ + return cli.SendMessageEvent(roomID, EventMessage, Content{ MsgType: MsgNotice, Body: text, }) @@ -622,7 +622,7 @@ func (cli *Client) SetPresence(status string) (err error) { // the HTTP response body, or return an error. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey func (cli *Client) StateEvent(roomID string, eventType EventType, stateKey string, outContent interface{}) (err error) { - u := cli.BuildURL("rooms", roomID, "state", string(eventType), stateKey) + u := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey) _, err = cli.MakeRequest("GET", u, nil, outContent) return } diff --git a/vendor/maunium.net/go/gomatrix/events.go b/vendor/maunium.net/go/gomatrix/events.go index fcd0538..9b4ef3e 100644 --- a/vendor/maunium.net/go/gomatrix/events.go +++ b/vendor/maunium.net/go/gomatrix/events.go @@ -5,28 +5,44 @@ import ( "sync" ) -type EventType string +type EventType struct { + Type string + IsState bool +} + +func (et *EventType) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &et.Type) +} + +func (et *EventType) MarshalJSON() ([]byte, error) { + return json.Marshal(&et.Type) +} + +func (et *EventType) String() string { + return et.Type +} + type MessageType string // State events -const ( - StateAliases EventType = "m.room.aliases" - StateCanonicalAlias = "m.room.canonical_alias" - StateCreate = "m.room.create" - StateJoinRules = "m.room.join_rules" - StateMember = "m.room.member" - StatePowerLevels = "m.room.power_levels" - StateRoomName = "m.room.name" - StateTopic = "m.room.topic" - StateRoomAvatar = "m.room.avatar" - StatePinnedEvents = "m.room.pinned_events" +var ( + StateAliases = EventType{"m.room.aliases", true} + StateCanonicalAlias = EventType{"m.room.canonical_alias", true} + StateCreate = EventType{"m.room.create", true} + StateJoinRules = EventType{"m.room.join_rules", true} + StateMember = EventType{"m.room.member", true} + StatePowerLevels = EventType{"m.room.power_levels", true} + StateRoomName = EventType{"m.room.name", true} + StateTopic = EventType{"m.room.topic", true} + StateRoomAvatar = EventType{"m.room.avatar", true} + StatePinnedEvents = EventType{"m.room.pinned_events", true} ) // Message events -const ( - EventRedaction EventType = "m.room.redaction" - EventMessage = "m.room.message" - EventSticker = "m.sticker" +var ( + EventRedaction = EventType{"m.room.redaction", false} + EventMessage = EventType{"m.room.message", false} + EventSticker = EventType{"m.sticker", false} ) // Msgtypes @@ -258,12 +274,12 @@ func (pl *PowerLevels) EnsureUserLevel(userID string, level int) bool { return false } -func (pl *PowerLevels) GetEventLevel(eventType EventType, isState bool) int { +func (pl *PowerLevels) GetEventLevel(eventType EventType) int { pl.eventsLock.RLock() defer pl.eventsLock.RUnlock() level, ok := pl.Events[eventType] if !ok { - if isState { + if eventType.IsState { return pl.StateDefault() } return pl.EventsDefault @@ -271,20 +287,20 @@ func (pl *PowerLevels) GetEventLevel(eventType EventType, isState bool) int { return level } -func (pl *PowerLevels) SetEventLevel(eventType EventType, isState bool, level int) { +func (pl *PowerLevels) SetEventLevel(eventType EventType, level int) { pl.eventsLock.Lock() defer pl.eventsLock.Unlock() - if (isState && level == pl.StateDefault()) || (!isState && level == pl.EventsDefault) { + if (eventType.IsState && level == pl.StateDefault()) || (!eventType.IsState && level == pl.EventsDefault) { delete(pl.Events, eventType) } else { pl.Events[eventType] = level } } -func (pl *PowerLevels) EnsureEventLevel(eventType EventType, isState bool, level int) bool { - existingLevel := pl.GetEventLevel(eventType, isState) +func (pl *PowerLevels) EnsureEventLevel(eventType EventType, level int) bool { + existingLevel := pl.GetEventLevel(eventType) if existingLevel != level { - pl.SetEventLevel(eventType, isState, level) + pl.SetEventLevel(eventType, level) return true } return false diff --git a/vendor/maunium.net/go/mautrix-appservice/appservice.go b/vendor/maunium.net/go/mautrix-appservice/appservice.go index 1ae68a2..e089b32 100644 --- a/vendor/maunium.net/go/mautrix-appservice/appservice.go +++ b/vendor/maunium.net/go/mautrix-appservice/appservice.go @@ -2,17 +2,19 @@ package appservice import ( "fmt" + "html/template" "io/ioutil" "os" + "path/filepath" "gopkg.in/yaml.v2" - "maunium.net/go/maulogger" - "strings" - "net/http" "errors" "maunium.net/go/gomatrix" + "maunium.net/go/maulogger" + "net/http" "regexp" + "strings" ) // EventChannelSize is the size for the Events channel in Appservice instances. @@ -263,15 +265,24 @@ func CreateLogConfig() LogConfig { } } +type FileFormatData struct { + Date string + Index int +} + // GetFileFormat returns a mauLogger-compatible logger file format based on the data in the struct. func (lc LogConfig) GetFileFormat() maulogger.LoggerFileFormat { - path := lc.FileNameFormat - if len(lc.Directory) > 0 { - path = lc.Directory + "/" + path - } + os.MkdirAll(lc.Directory, 0700) + path := filepath.Join(lc.Directory, lc.FileNameFormat) + tpl, _ := template.New("fileformat").Parse(path) return func(now string, i int) string { - return fmt.Sprintf(path, now, i) + var buf strings.Builder + tpl.Execute(&buf, FileFormatData{ + Date: now, + Index: i, + }) + return buf.String() } } diff --git a/vendor/maunium.net/go/mautrix-appservice/intent.go b/vendor/maunium.net/go/mautrix-appservice/intent.go index 957d529..3aa864b 100644 --- a/vendor/maunium.net/go/mautrix-appservice/intent.go +++ b/vendor/maunium.net/go/mautrix-appservice/intent.go @@ -201,19 +201,19 @@ func (intent *IntentAPI) RedactEvent(roomID, eventID string, req *gomatrix.ReqRe } func (intent *IntentAPI) SetRoomName(roomID, roomName string) (*gomatrix.RespSendEvent, error) { - return intent.SendStateEvent(roomID, "m.room.name", "", map[string]interface{}{ + return intent.SendStateEvent(roomID, gomatrix.StateRoomName, "", map[string]interface{}{ "name": roomName, }) } func (intent *IntentAPI) SetRoomAvatar(roomID, avatarURL string) (*gomatrix.RespSendEvent, error) { - return intent.SendStateEvent(roomID, "m.room.avatar", "", map[string]interface{}{ + return intent.SendStateEvent(roomID, gomatrix.StateRoomAvatar, "", map[string]interface{}{ "url": avatarURL, }) } func (intent *IntentAPI) SetRoomTopic(roomID, topic string) (*gomatrix.RespSendEvent, error) { - return intent.SendStateEvent(roomID, "m.room.topic", "", map[string]interface{}{ + return intent.SendStateEvent(roomID, gomatrix.StateTopic, "", map[string]interface{}{ "topic": topic, }) } diff --git a/vendor/maunium.net/go/mautrix-appservice/statestore.go b/vendor/maunium.net/go/mautrix-appservice/statestore.go index 944e865..14f9b74 100644 --- a/vendor/maunium.net/go/mautrix-appservice/statestore.go +++ b/vendor/maunium.net/go/mautrix-appservice/statestore.go @@ -15,13 +15,15 @@ type StateStore interface { SetTyping(roomID, userID string, timeout int64) IsInRoom(roomID, userID string) bool + IsInvited(roomID, userID string) bool + IsMembership(roomID, userID string, allowedMemberships ...string) bool SetMembership(roomID, userID, membership string) SetPowerLevels(roomID string, levels *gomatrix.PowerLevels) GetPowerLevels(roomID string) *gomatrix.PowerLevels GetPowerLevel(roomID, userID string) int - GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType, isState bool) int - HasPowerLevel(roomID, userID string, eventType gomatrix.EventType, isState bool) bool + GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType) int + HasPowerLevel(roomID, userID string, eventType gomatrix.EventType) bool } func (as *AppService) UpdateState(evt *gomatrix.Event) { @@ -126,7 +128,21 @@ func (store *BasicStateStore) GetMembership(roomID, userID string) string { } func (store *BasicStateStore) IsInRoom(roomID, userID string) bool { - return store.GetMembership(roomID, userID) == "join" + return store.IsMembership(roomID, userID, "join") +} + +func (store *BasicStateStore) IsInvited(roomID, userID string) bool { + return store.IsMembership(roomID, userID, "join", "invite") +} + +func (store *BasicStateStore) IsMembership(roomID, userID string, allowedMemberships ...string) bool { + membership := store.GetMembership(roomID, userID) + for _, allowedMembership := range allowedMemberships { + if allowedMembership == membership { + return true + } + } + return false } func (store *BasicStateStore) SetMembership(roomID, userID, membership string) { @@ -160,19 +176,10 @@ func (store *BasicStateStore) GetPowerLevel(roomID, userID string) int { return store.GetPowerLevels(roomID).GetUserLevel(userID) } -func (store *BasicStateStore) GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType, isState bool) int { - levels := store.GetPowerLevels(roomID) - switch eventType { - case "kick": - return levels.Kick() - case "invite": - return levels.Invite() - case "redact": - return levels.Redact() - } - return levels.GetEventLevel(eventType, isState) +func (store *BasicStateStore) GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType) int { + return store.GetPowerLevels(roomID).GetEventLevel(eventType) } -func (store *BasicStateStore) HasPowerLevel(roomID, userID string, eventType gomatrix.EventType, isState bool) bool { - return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType, isState) +func (store *BasicStateStore) HasPowerLevel(roomID, userID string, eventType gomatrix.EventType) bool { + return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType) } From 5a1a6f9c3c6abed37d645563259a1ac4be27107c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 29 Aug 2018 23:48:15 +0300 Subject: [PATCH 2/8] Remove config update stuff --- config/recursivemap.go | 115 ----------------------------------------- config/update.go | 100 ----------------------------------- example-config.yaml | 2 +- main.go | 18 +++---- 4 files changed, 8 insertions(+), 227 deletions(-) delete mode 100644 config/recursivemap.go delete mode 100644 config/update.go diff --git a/config/recursivemap.go b/config/recursivemap.go deleted file mode 100644 index 49059be..0000000 --- a/config/recursivemap.go +++ /dev/null @@ -1,115 +0,0 @@ -// 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 . - -package config - -import ( - "strings" -) - -type RecursiveMap map[interface{}]interface{} - -func (rm RecursiveMap) GetDefault(path string, defVal interface{}) interface{} { - val, ok := rm.Get(path) - if !ok { - return defVal - } - return val -} - -func (rm RecursiveMap) GetMap(path string) RecursiveMap { - val := rm.GetDefault(path, nil) - if val == nil { - return nil - } - - newRM, ok := val.(map[interface{}]interface{}) - if ok { - return RecursiveMap(newRM) - } - return nil -} - -func (rm RecursiveMap) Get(path string) (interface{}, bool) { - if index := strings.IndexRune(path, '.'); index >= 0 { - key := path[:index] - path = path[index+1:] - - submap := rm.GetMap(key) - if submap == nil { - return nil, false - } - return submap.Get(path) - } - val, ok := rm[path] - return val, ok -} - -func (rm RecursiveMap) GetIntDefault(path string, defVal int) int { - val, ok := rm.GetInt(path) - if !ok { - return defVal - } - return val -} - -func (rm RecursiveMap) GetInt(path string) (int, bool) { - val, ok := rm.Get(path) - if !ok { - return 0, ok - } - intVal, ok := val.(int) - return intVal, ok -} - -func (rm RecursiveMap) GetStringDefault(path string, defVal string) string { - val, ok := rm.GetString(path) - if !ok { - return defVal - } - return val -} - -func (rm RecursiveMap) GetString(path string) (string, bool) { - val, ok := rm.Get(path) - if !ok { - return "", ok - } - strVal, ok := val.(string) - return strVal, ok -} - -func (rm RecursiveMap) Set(path string, value interface{}) { - if index := strings.IndexRune(path, '.'); index >= 0 { - key := path[:index] - path = path[index+1:] - nextRM := rm.GetMap(key) - if nextRM == nil { - nextRM = make(RecursiveMap) - rm[key] = nextRM - } - nextRM.Set(path, value) - return - } - rm[path] = value -} - -func (rm RecursiveMap) CopyFrom(otherRM RecursiveMap, path string) { - val, ok := otherRM.Get(path) - if ok { - rm.Set(path, val) - } -} \ No newline at end of file diff --git a/config/update.go b/config/update.go deleted file mode 100644 index 5c390ab..0000000 --- a/config/update.go +++ /dev/null @@ -1,100 +0,0 @@ -// 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 . - -package config - -import ( - "io/ioutil" - - "gopkg.in/yaml.v2" -) - -func Update(path, basePath string) error { - oldCfgData, err := ioutil.ReadFile(path) - if err != nil { - return err - } - - oldCfg := make(RecursiveMap) - err = yaml.Unmarshal(oldCfgData, &oldCfg) - if err != nil { - return err - } - - baseCfgData, err := ioutil.ReadFile(basePath) - if err != nil { - return err - } - - baseCfg := make(RecursiveMap) - err = yaml.Unmarshal(baseCfgData, &baseCfg) - if err != nil { - return err - } - - err = runUpdate(oldCfg, baseCfg) - if err != nil { - return err - } - - newCfgData, err := yaml.Marshal(&baseCfg) - if err != nil { - return err - } - - return ioutil.WriteFile(path, newCfgData, 0600) -} - -func runUpdate(oldCfg, newCfg RecursiveMap) error { - cp := func(path string) { - newCfg.CopyFrom(oldCfg, path) - } - - cp("homeserver.address") - cp("homeserver.domain") - - cp("appservice.address") - cp("appservice.hostname") - cp("appservice.port") - - cp("appservice.database.type") - cp("appservice.database.uri") - cp("appservice.state_store_path") - - cp("appservice.id") - cp("appservice.bot.username") - cp("appservice.bot.displayname") - cp("appservice.bot.avatar") - - cp("appservice.bot.as_token") - cp("appservice.bot.hs_token") - - cp("bridge.username_template") - cp("bridge.displayname_template") - - cp("bridge.command_prefix") - - cp("bridge.permissions") - - cp("logging.directory") - cp("logging.file_name_format") - cp("logging.file_date_format") - cp("logging.file_mode") - cp("logging.timestamp_format") - cp("logging.print_level") - - return nil -} diff --git a/example-config.yaml b/example-config.yaml index bbc609c..616c142 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -29,7 +29,7 @@ appservice: # Appservice bot details. bot: # Username of the appservice bot. - username: whatsapp + username: whatsappbot # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty # to leave display name/avatar as-is. displayname: WhatsApp bridge bot diff --git a/main.go b/main.go index 50bac7e..eda5f89 100644 --- a/main.go +++ b/main.go @@ -91,16 +91,12 @@ func NewBridge() *Bridge { portalsByJID: make(map[database.PortalKey]*Portal), puppets: make(map[types.WhatsAppID]*Puppet), } - err := config.Update(*configPath, *baseConfigPath) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to update config:", err) - os.Exit(10) - } + var err error bridge.Config, err = config.Load(*configPath) if err != nil { fmt.Fprintln(os.Stderr, "Failed to load config:", err) - os.Exit(11) + os.Exit(10) } return bridge } @@ -111,7 +107,7 @@ func (bridge *Bridge) Init() { bridge.AS, err = bridge.Config.MakeAppService() if err != nil { fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err) - os.Exit(12) + os.Exit(11) } bridge.AS.Init() bridge.Bot = bridge.AS.BotIntent() @@ -122,7 +118,7 @@ func (bridge *Bridge) Init() { err = log.OpenFile() if err != nil { fmt.Fprintln(os.Stderr, "Failed to open log file:", err) - os.Exit(13) + os.Exit(12) } bridge.AS.Log = log.Sub("Matrix") @@ -131,7 +127,7 @@ func (bridge *Bridge) Init() { err = bridge.StateStore.Load() if err != nil { bridge.Log.Fatalln("Failed to load state store:", err) - os.Exit(14) + os.Exit(13) } bridge.AS.StateStore = bridge.StateStore @@ -139,7 +135,7 @@ func (bridge *Bridge) Init() { bridge.DB, err = database.New(bridge.Config.AppService.Database.URI) if err != nil { bridge.Log.Fatalln("Failed to initialize database:", err) - os.Exit(15) + os.Exit(14) } bridge.Log.Debugln("Initializing Matrix event processor") @@ -153,7 +149,7 @@ func (bridge *Bridge) Start() { err := bridge.DB.CreateTables() if err != nil { bridge.Log.Fatalln("Failed to create database tables:", err) - os.Exit(16) + os.Exit(15) } bridge.Log.Debugln("Starting application service HTTP server") go bridge.AS.Start() From 347854dc8cffe1deb745a11f4dcf20d547bd782f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 30 Aug 2018 01:10:03 +0300 Subject: [PATCH 3/8] Update dependencies --- Gopkg.lock | 8 +++---- Gopkg.toml | 2 +- vendor/github.com/Rhymen/go-whatsapp/conn.go | 6 +++++ .../github.com/Rhymen/go-whatsapp/session.go | 16 ++++++++++++-- vendor/maunium.net/go/gomatrix/events.go | 22 ++++++++++++++----- 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 41db0d9..5334d34 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,7 +2,7 @@ [[projects]] - branch = "master" + branch = "develop" name = "github.com/Rhymen/go-whatsapp" packages = [ ".", @@ -13,7 +13,7 @@ "crypto/curve25519", "crypto/hkdf" ] - revision = "82b902133ab6093f864dfc11fb9c5648f82f0ee9" + revision = "00ef431f94f17f125f842d0c7d4e9b68294c6559" source = "github.com/tulir/go-whatsapp" [[projects]] @@ -123,7 +123,7 @@ ".", "format" ] - revision = "42a3133c4980e4b1ea5fb52329d977f592d67cf0" + revision = "b018830e10612c04065723de7aa49f35b37864a6" [[projects]] branch = "master" @@ -146,6 +146,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "8b494649cb598fd6a3862d4de5946f464288424b6e6e561e2b32c8d0ea1cb634" + inputs-digest = "16c945243c327e861a6dc4fa2b43010d00453b8ee71427b95fb5dfcc6cb6ebee" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 926c998..6d31530 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -26,7 +26,7 @@ [[constraint]] - branch = "master" + branch = "develop" name = "github.com/Rhymen/go-whatsapp" source = "github.com/tulir/go-whatsapp" diff --git a/vendor/github.com/Rhymen/go-whatsapp/conn.go b/vendor/github.com/Rhymen/go-whatsapp/conn.go index de9720e..1038f63 100644 --- a/vendor/github.com/Rhymen/go-whatsapp/conn.go +++ b/vendor/github.com/Rhymen/go-whatsapp/conn.go @@ -89,6 +89,9 @@ type Conn struct { Info *Info Store *Store ServerLastSeen time.Time + + longClientName string + shortClientName string } type wsMsg struct { @@ -122,6 +125,9 @@ func NewConn(timeout time.Duration) (*Conn, error) { msgCount: 0, msgTimeout: timeout, Store: newStore(), + + longClientName: "github.com/rhymen/go-whatsapp", + shortClientName: "go-whatsapp", } go wac.readPump() diff --git a/vendor/github.com/Rhymen/go-whatsapp/session.go b/vendor/github.com/Rhymen/go-whatsapp/session.go index 34693d4..1a8c8f3 100644 --- a/vendor/github.com/Rhymen/go-whatsapp/session.go +++ b/vendor/github.com/Rhymen/go-whatsapp/session.go @@ -84,6 +84,18 @@ func newInfoFromReq(info map[string]interface{}) *Info { return ret } +/* +SetClientName sets the long and short client names that are sent to WhatsApp when logging in and displayed in the +WhatsApp Web device list. As the values are only sent when logging in, changing them after logging in is not possible. + */ +func (wac *Conn) SetClientName(long, short string) error { + if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { + return fmt.Errorf("cannot change client name after logging in") + } + wac.longClientName, wac.shortClientName = long, short + return nil +} + /* Login is the function that creates a new whatsapp session and logs you in. If you do not want to scan the qr code every time, you should save the returned session and use RestoreSession the next time. Login takes a writable channel @@ -122,7 +134,7 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) { session.ClientId = base64.StdEncoding.EncodeToString(clientId) //oldVersion=8691 - login := []interface{}{"admin", "init", []int{0, 3, 225}, []string{"github.com/rhymen/go-whatsapp", "go-whatsapp"}, session.ClientId, true} + login := []interface{}{"admin", "init", []int{0, 3, 225}, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true} loginChan, err := wac.write(login) if err != nil { return session, fmt.Errorf("error writing login: %v\n", err) @@ -235,7 +247,7 @@ func (wac *Conn) RestoreSession(session Session) (Session, error) { wac.listener["s1"] = make(chan string, 1) //admin init - init := []interface{}{"admin", "init", []int{0, 3, 225}, []string{"github.com/rhymen/go-whatsapp", "go-whatsapp"}, session.ClientId, true} + init := []interface{}{"admin", "init", []int{0, 3, 225}, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true} initChan, err := wac.write(init) if err != nil { wac.session = nil diff --git a/vendor/maunium.net/go/gomatrix/events.go b/vendor/maunium.net/go/gomatrix/events.go index 9b4ef3e..b9f98f7 100644 --- a/vendor/maunium.net/go/gomatrix/events.go +++ b/vendor/maunium.net/go/gomatrix/events.go @@ -11,7 +11,19 @@ type EventType struct { } func (et *EventType) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &et.Type) + err := json.Unmarshal(data, &et.Type) + if err != nil { + return err + } + + switch et.Type { + case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type, + StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateTopic.Type, StatePinnedEvents.Type: + et.IsState = true + default: + et.IsState = false + } + return nil } func (et *EventType) MarshalJSON() ([]byte, error) { @@ -199,7 +211,7 @@ type PowerLevels struct { UsersDefault int `json:"users_default,omitempty"` eventsLock sync.RWMutex `json:"-"` - Events map[EventType]int `json:"events,omitempty"` + Events map[string]int `json:"events,omitempty"` EventsDefault int `json:"events_default,omitempty"` StateDefaultPtr *int `json:"state_default,omitempty"` @@ -277,7 +289,7 @@ func (pl *PowerLevels) EnsureUserLevel(userID string, level int) bool { func (pl *PowerLevels) GetEventLevel(eventType EventType) int { pl.eventsLock.RLock() defer pl.eventsLock.RUnlock() - level, ok := pl.Events[eventType] + level, ok := pl.Events[eventType.String()] if !ok { if eventType.IsState { return pl.StateDefault() @@ -291,9 +303,9 @@ func (pl *PowerLevels) SetEventLevel(eventType EventType, level int) { pl.eventsLock.Lock() defer pl.eventsLock.Unlock() if (eventType.IsState && level == pl.StateDefault()) || (!eventType.IsState && level == pl.EventsDefault) { - delete(pl.Events, eventType) + delete(pl.Events, eventType.String()) } else { - pl.Events[eventType] = level + pl.Events[eventType.String()] = level } } From 22cdf519f21ebd8f6dc20c0bb3fcd3ffd9f072e5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 30 Aug 2018 01:10:26 +0300 Subject: [PATCH 4/8] Fix desegregation changes to make the bridge work again --- commands.go | 2 +- database/message.go | 12 +++++------ database/portal.go | 4 +++- database/puppet.go | 5 ++++- database/user.go | 50 ++++++++++++++++++++++++++++++--------------- formatting.go | 3 ++- main.go | 8 ++++---- portal.go | 40 +++++++++++++++++++++++------------- user.go | 15 +++++++++----- 9 files changed, 89 insertions(+), 50 deletions(-) diff --git a/commands.go b/commands.go index 34255ef..5b795fc 100644 --- a/commands.go +++ b/commands.go @@ -56,7 +56,7 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes args := strings.Split(message, " ") cmd := strings.ToLower(args[0]) ce := &CommandEvent{ - Bot: handler.bridge.AS.BotIntent(), + Bot: handler.bridge.Bot, Bridge: handler.bridge, Handler: handler, RoomID: roomID, diff --git a/database/message.go b/database/message.go index d4e3a8c..733a91e 100644 --- a/database/message.go +++ b/database/message.go @@ -30,12 +30,12 @@ type MessageQuery struct { func (mq *MessageQuery) CreateTable() error { _, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message ( - chat_jid VARCHAR(25) NOT NULL, - chat_receiver VARCHAR(25) NOT NULL, - jid VARCHAR(255) NOT NULL, + chat_jid VARCHAR(25), + chat_receiver VARCHAR(25), + jid VARCHAR(255), mxid VARCHAR(255) NOT NULL UNIQUE, - PRIMARY KEY (chat_jid, jid), + PRIMARY KEY (chat_jid, chat_receiver, jid), FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) )`) return err @@ -97,9 +97,9 @@ func (msg *Message) Scan(row Scannable) *Message { } func (msg *Message) Insert() error { - _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID) + _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID) if err != nil { - msg.log.Warnfln("Failed to update %s: %v", msg.Chat, msg.JID, err) + msg.log.Warnfln("Failed to insert %s: %v", msg.Chat, msg.JID, err) } return err } diff --git a/database/portal.go b/database/portal.go index 1a8586d..31f3a58 100644 --- a/database/portal.go +++ b/database/portal.go @@ -122,13 +122,15 @@ type Portal struct { } func (portal *Portal) Scan(row Scannable) *Portal { - err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &portal.MXID, &portal.Name, &portal.Topic, &portal.Avatar) + var mxid sql.NullString + err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar) if err != nil { if err != sql.ErrNoRows { portal.log.Errorln("Database scan failed:", err) } return nil } + portal.MXID = mxid.String return portal } diff --git a/database/puppet.go b/database/puppet.go index 401a5c8..1b11dcf 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -74,13 +74,16 @@ type Puppet struct { } func (puppet *Puppet) Scan(row Scannable) *Puppet { - err := row.Scan(&puppet.JID, &puppet.Displayname, &puppet.Avatar) + var displayname, avatar sql.NullString + err := row.Scan(&puppet.JID, &displayname, &avatar) if err != nil { if err != sql.ErrNoRows { puppet.log.Errorln("Database scan failed:", err) } return nil } + puppet.Displayname = displayname.String + puppet.Avatar = avatar.String return puppet } diff --git a/database/user.go b/database/user.go index b650cb3..686ce02 100644 --- a/database/user.go +++ b/database/user.go @@ -18,10 +18,12 @@ package database import ( "database/sql" + "strings" "github.com/Rhymen/go-whatsapp" log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/types" + "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) type UserQuery struct { @@ -40,8 +42,7 @@ func (uq *UserQuery) CreateTable() error { client_token VARCHAR(255), server_token VARCHAR(255), enc_key BLOB, - mac_key BLOB, - wid VARCHAR(255) + mac_key BLOB )`) return err } @@ -74,7 +75,7 @@ func (uq *UserQuery) GetByMXID(userID types.MatrixUserID) *User { } func (uq *UserQuery) GetByJID(userID types.WhatsAppID) *User { - row := uq.db.QueryRow("SELECT * FROM user WHERE jid=?", userID) + row := uq.db.QueryRow("SELECT * FROM user WHERE jid=?", stripSuffix(userID)) if row == nil { return nil } @@ -92,28 +93,42 @@ type User struct { } func (user *User) Scan(row Scannable) *User { - sess := whatsapp.Session{} - err := row.Scan(&user.MXID, &user.JID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken, - &sess.EncKey, &sess.MacKey, &sess.Wid) + var managementRoom, clientID, clientToken, serverToken, jid sql.NullString + var encKey, macKey []byte + err := row.Scan(&user.MXID, &jid, &managementRoom, &clientID, &clientToken, &serverToken, &encKey, &macKey) if err != nil { if err != sql.ErrNoRows { user.log.Errorln("Database scan failed:", err) } return nil } - if len(sess.ClientId) > 0 { - user.Session = &sess + if len(jid.String) > 0 && len(clientID.String) > 0 { + user.JID = jid.String + whatsappExt.NewUserSuffix + user.Session = &whatsapp.Session{ + ClientId: clientID.String, + ClientToken: clientToken.String, + ServerToken: serverToken.String, + EncKey: encKey, + MacKey: macKey, + Wid: jid.String + whatsappExt.OldUserSuffix, + } } else { user.Session = nil } return user } -func (user *User) jidPtr() *string { - if len(user.JID) > 0 { - return &user.JID +func stripSuffix(jid types.WhatsAppID) string { + if len(jid) == 0 { + return jid } - return nil + + index := strings.IndexRune(jid, '@') + if index < 0 { + return jid + } + + return jid[:index] } func (user *User) sessionUnptr() (sess whatsapp.Session) { @@ -125,16 +140,17 @@ func (user *User) sessionUnptr() (sess whatsapp.Session) { func (user *User) Insert() error { sess := user.sessionUnptr() - _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, user.jidPtr(), user.ManagementRoom, - sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid) + _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, stripSuffix(user.JID), + user.ManagementRoom, + sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey) return err } func (user *User) Update() error { sess := user.sessionUnptr() - _, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?", - user.jidPtr(), user.ManagementRoom, - sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, + _, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=? WHERE mxid=?", + stripSuffix(user.JID), user.ManagementRoom, + sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, user.MXID) return err } diff --git a/formatting.go b/formatting.go index bd3eb3c..896710e 100644 --- a/formatting.go +++ b/formatting.go @@ -24,6 +24,7 @@ import ( "maunium.net/go/gomatrix" "maunium.net/go/gomatrix/format" + "maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) @@ -104,7 +105,7 @@ func NewFormatter(bridge *Bridge) *Formatter { return formatter } -func (formatter *Formatter) getMatrixInfoByJID(jid string) (mxid, displayname string) { +func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid, displayname string) { if user := formatter.bridge.GetUserByJID(jid); user != nil { mxid = user.MXID displayname = user.MXID diff --git a/main.go b/main.go index eda5f89..5594e2c 100644 --- a/main.go +++ b/main.go @@ -165,18 +165,18 @@ func (bridge *Bridge) UpdateBotProfile() { var err error if botConfig.Avatar == "remove" { - err = bridge.AS.BotIntent().SetAvatarURL("") + err = bridge.Bot.SetAvatarURL("") } else if len(botConfig.Avatar) > 0 { - err = bridge.AS.BotIntent().SetAvatarURL(botConfig.Avatar) + err = bridge.Bot.SetAvatarURL(botConfig.Avatar) } if err != nil { bridge.Log.Warnln("Failed to update bot avatar:", err) } if botConfig.Displayname == "remove" { - err = bridge.AS.BotIntent().SetDisplayName("") + err = bridge.Bot.SetDisplayName("") } else if len(botConfig.Avatar) > 0 { - err = bridge.AS.BotIntent().SetDisplayName(botConfig.Displayname) + err = bridge.Bot.SetDisplayName(botConfig.Displayname) } if err != nil { bridge.Log.Warnln("Failed to update bot displayname:", err) diff --git a/portal.go b/portal.go index e597d1c..98457e8 100644 --- a/portal.go +++ b/portal.go @@ -126,7 +126,7 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { puppet.Intent().EnsureJoined(portal.MXID) user := portal.bridge.GetUserByJID(participant.JID) - if !portal.bridge.AS.StateStore.IsInvited(portal.MXID, user.MXID) { + if user != nil && !portal.bridge.AS.StateStore.IsInvited(portal.MXID, user.MXID) { portal.MainIntent().InviteUser(portal.MXID, &gomatrix.ReqInviteUser{ UserID: user.MXID, }) @@ -144,7 +144,10 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { } } if changed { - portal.MainIntent().SetPowerLevels(portal.MXID, levels) + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels:", err) + } } } @@ -258,10 +261,10 @@ func (portal *Portal) GetBasePowerLevels() *gomatrix.PowerLevels { Users: map[string]int{ portal.MainIntent().UserID: 100, }, - Events: map[gomatrix.EventType]int{ - gomatrix.StateRoomName: anyone, - gomatrix.StateRoomAvatar: anyone, - gomatrix.StateTopic: anyone, + Events: map[string]int{ + gomatrix.StateRoomName.Type: anyone, + gomatrix.StateRoomAvatar.Type: anyone, + gomatrix.StateTopic.Type: anyone, }, } } @@ -286,7 +289,10 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { } } if changed { - portal.MainIntent().SetPowerLevels(portal.MXID, levels) + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels:", err) + } } } @@ -300,7 +306,10 @@ func (portal *Portal) RestrictMessageSending(restrict bool) { } else { levels.EventsDefault = 0 } - portal.MainIntent().SetPowerLevels(portal.MXID, levels) + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels:", err) + } } func (portal *Portal) RestrictMetadataChanges(restrict bool) { @@ -317,7 +326,10 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) { changed = levels.EnsureEventLevel(gomatrix.StateRoomAvatar, newLevel) || changed changed = levels.EnsureEventLevel(gomatrix.StateTopic, newLevel) || changed if changed { - portal.MainIntent().SetPowerLevels(portal.MXID, levels) + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels:", err) + } } } @@ -367,7 +379,7 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI { if portal.IsPrivateChat() { return portal.bridge.GetPuppetByJID(portal.Key.JID).Intent() } - return portal.bridge.AS.BotIntent() + return portal.bridge.Bot } func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool { @@ -386,13 +398,13 @@ func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.Matrix msg.Insert() } -func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.IntentAPI { +func (portal *Portal) GetMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { if info.FromMe { if portal.IsPrivateChat() { // TODO handle own messages in private chats properly return nil } - return portal.bridge.GetPuppetByJID(portal.Key.Receiver).Intent() + return portal.bridge.GetPuppetByJID(user.JID).Intent() } else if portal.IsPrivateChat() { return portal.MainIntent() } else if len(info.SenderJid) == 0 { @@ -432,7 +444,7 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa return } - intent := portal.GetMessageIntent(message.Info) + intent := portal.GetMessageIntent(source, message.Info) if intent == nil { return } @@ -466,7 +478,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, return } - intent := portal.GetMessageIntent(info) + intent := portal.GetMessageIntent(source, info) if intent == nil { return } diff --git a/user.go b/user.go index 2155752..0c44f5a 100644 --- a/user.go +++ b/user.go @@ -37,7 +37,6 @@ type User struct { Admin bool Whitelisted bool - jid string } func (bridge *Bridge) GetUserByMXID(userID types.MatrixUserID) *User { @@ -53,6 +52,9 @@ func (bridge *Bridge) GetUserByMXID(userID types.MatrixUserID) *User { } user = bridge.NewUser(dbUser) bridge.usersByMXID[user.MXID] = user + if len(user.JID) > 0 { + bridge.usersByJID[user.JID] = user + } if len(user.ManagementRoom) > 0 { bridge.managementRooms[user.ManagementRoom] = user } @@ -66,13 +68,12 @@ func (bridge *Bridge) GetUserByJID(userID types.WhatsAppID) *User { defer bridge.usersLock.Unlock() user, ok := bridge.usersByJID[userID] if !ok { - dbUser := bridge.DB.User.GetByMXID(userID) + dbUser := bridge.DB.User.GetByJID(userID) if dbUser == nil { - dbUser = bridge.DB.User.New() - dbUser.MXID = userID - dbUser.Insert() + return nil } user = bridge.NewUser(dbUser) + bridge.usersByMXID[user.MXID] = user bridge.usersByJID[user.JID] = user if len(user.ManagementRoom) > 0 { bridge.managementRooms[user.ManagementRoom] = user @@ -91,6 +92,9 @@ func (bridge *Bridge) GetAllUsers() []*User { if !ok { user = bridge.NewUser(dbUser) bridge.usersByMXID[user.MXID] = user + if len(user.JID) > 0 { + bridge.usersByJID[user.JID] = user + } if len(user.ManagementRoom) > 0 { bridge.managementRooms[user.ManagementRoom] = user } @@ -147,6 +151,7 @@ func (user *User) Connect(evenIfNoSession bool) bool { return false } user.Conn = whatsappExt.ExtendConn(conn) + user.Conn.SetClientName("Mautrix-WhatsApp bridge", "mx-wa") user.log.Debugln("WhatsApp connection successful") user.Conn.AddHandler(user) return user.RestoreSession() From 79851a62b41f55544e039c0156beabae9216ee43 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 31 Aug 2018 00:13:08 +0300 Subject: [PATCH 5/8] Add locking for whatsapp->matrix messages for desegregated group chats --- Gopkg.lock | 6 +- example-config.yaml | 2 +- portal.go | 112 ++++++++++++++---- .../go/mautrix-appservice/intent.go | 10 ++ .../go/mautrix-appservice/statestore.go | 2 + 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 5334d34..5bca2ba 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -87,7 +87,7 @@ "curve25519", "hkdf" ] - revision = "614d502a4dac94afa3a6ce146bd1736da82514c6" + revision = "182538f80094b6a8efaade63a8fd8e0d9d5843dd" [[projects]] branch = "master" @@ -102,7 +102,7 @@ branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "d99a578cf41bfccdeaf48b0845c823a4b8b0ad5e" + revision = "49385e6e15226593f68b26af201feec29d5bba22" [[projects]] name = "gopkg.in/russross/blackfriday.v2" @@ -141,7 +141,7 @@ branch = "master" name = "maunium.net/go/mautrix-appservice" packages = ["."] - revision = "37d4449056015cea5d0a4420bba595c61ad32007" + revision = "fb756247f82716de7698b8200f28f16b4fd04a6b" [solve-meta] analyzer-name = "dep" diff --git a/example-config.yaml b/example-config.yaml index 616c142..4839aee 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -64,7 +64,7 @@ bridge: # domain - All users on that homeserver # mxid - Specific user permissions: - "example.com": full + "example.com": user "@admin:example.com": admin # Logging config. diff --git a/portal.go b/portal.go index 98457e8..27635e1 100644 --- a/portal.go +++ b/portal.go @@ -102,6 +102,9 @@ func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal { Portal: dbPortal, bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)), + + messageLocks: make(map[types.WhatsAppMessageID]sync.Mutex), + recentlyHandled: [20]types.WhatsAppMessageID{}, } } @@ -111,7 +114,80 @@ type Portal struct { bridge *Bridge log log.Logger - roomCreateLock sync.Mutex + roomCreateLock sync.Mutex + messageLocksLock sync.Mutex + messageLocks map[types.WhatsAppMessageID]sync.Mutex + + recentlyHandled [20]types.WhatsAppMessageID + recentlyHandledLock sync.Mutex + recentlyHandledIndex uint8 +} + +func (portal *Portal) getMessageLock(messageID types.WhatsAppMessageID) sync.Mutex { + portal.messageLocksLock.Lock() + defer portal.messageLocksLock.Unlock() + lock, ok := portal.messageLocks[messageID] + if !ok { + portal.messageLocks[messageID] = lock + } + return lock +} + +func (portal *Portal) deleteMessageLock(messageID types.WhatsAppMessageID) { + portal.messageLocksLock.Lock() + delete(portal.messageLocks, messageID) + portal.messageLocksLock.Unlock() +} + +func (portal *Portal) isRecentlyHandled(id types.WhatsAppMessageID) bool { + start := portal.recentlyHandledIndex + for i := start; i != start; i = (i - 1) % 20 { + if portal.recentlyHandled[i] == id { + return true + } + } + return false +} + +func (portal *Portal) isDuplicate(id types.WhatsAppMessageID) bool { + msg := portal.bridge.DB.Message.GetByJID(portal.Key, id) + if msg != nil { + return true + } + return false +} + +func (portal *Portal) markHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) { + msg := portal.bridge.DB.Message.New() + msg.Chat = portal.Key + msg.JID = jid + msg.MXID = mxid + msg.Insert() + + portal.recentlyHandledLock.Lock() + index := portal.recentlyHandledIndex + portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % 20 + portal.recentlyHandledLock.Unlock() + portal.recentlyHandled[index] = jid +} + +func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bool) { + if portal.isRecentlyHandled(id) { + return nil, false + } + lock := portal.getMessageLock(id) + lock.Lock() + if portal.isDuplicate(id) { + lock.Unlock() + return nil, false + } + return &lock, true +} + +func (portal *Portal) finishHandling(id types.WhatsAppMessageID, mxid types.MatrixEventID) { + portal.markHandled(id, mxid) + portal.deleteMessageLock(id) + portal.log.Debugln("Handled message", id, "->", mxid) } func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { @@ -238,6 +314,8 @@ func (portal *Portal) Sync(user *User, contact whatsapp.Contact) { portal.log.Errorln("Failed to create portal room:", err) return } + } else { + portal.MainIntent().EnsureInvited(portal.MXID, user.MXID) } update := false @@ -382,22 +460,6 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI { return portal.bridge.Bot } -func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool { - msg := portal.bridge.DB.Message.GetByJID(portal.Key, id) - if msg != nil { - return true - } - return false -} - -func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) { - msg := portal.bridge.DB.Message.New() - msg.Chat = portal.Key - msg.JID = jid - msg.MXID = mxid - msg.Insert() -} - func (portal *Portal) GetMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { if info.FromMe { if portal.IsPrivateChat() { @@ -434,9 +496,11 @@ func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageI } func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) { - if portal.IsDuplicate(message.Info.Id) { + lock, ok := portal.startHandling(message.Info.Id) + if !ok { return } + defer lock.Unlock() err := portal.CreateMatrixRoom([]string{source.MXID}) if err != nil { @@ -463,14 +527,15 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) return } - portal.MarkHandled(message.Info.Id, resp.EventID) - portal.log.Debugln("Handled message", message.Info.Id, "->", resp.EventID) + portal.finishHandling(message.Info.Id, resp.EventID) } func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) { - if portal.IsDuplicate(info.Id) { + lock, ok := portal.startHandling(info.Id) + if !ok { return } + defer lock.Unlock() err := portal.CreateMatrixRoom([]string{source.MXID}) if err != nil { @@ -563,8 +628,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, // TODO store caption mxid? } - portal.MarkHandled(info.Id, resp.EventID) - portal.log.Debugln("Handled message", info.Id, "->", resp.EventID) + portal.finishHandling(info.Id, resp.EventID) } func makeMessageID() *string { @@ -799,8 +863,8 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) { portal.log.Debugln("Unhandled Matrix event:", evt) return } + portal.markHandled(info.GetKey().GetId(), evt.ID) err = sender.Conn.Send(info) - portal.MarkHandled(info.GetKey().GetId(), evt.ID) if err != nil { portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) } else { diff --git a/vendor/maunium.net/go/mautrix-appservice/intent.go b/vendor/maunium.net/go/mautrix-appservice/intent.go index 3aa864b..e940b3e 100644 --- a/vendor/maunium.net/go/mautrix-appservice/intent.go +++ b/vendor/maunium.net/go/mautrix-appservice/intent.go @@ -231,3 +231,13 @@ func (intent *IntentAPI) SetAvatarURL(avatarURL string) error { } return intent.Client.SetAvatarURL(avatarURL) } + +func (intent *IntentAPI) EnsureInvited(roomID, userID string) error { + if !intent.as.StateStore.IsInvited(roomID, userID) { + _, err := intent.Client.InviteUser(roomID, &gomatrix.ReqInviteUser{ + UserID: userID, + }) + return err + } + return nil +} \ No newline at end of file diff --git a/vendor/maunium.net/go/mautrix-appservice/statestore.go b/vendor/maunium.net/go/mautrix-appservice/statestore.go index 14f9b74..543d98d 100644 --- a/vendor/maunium.net/go/mautrix-appservice/statestore.go +++ b/vendor/maunium.net/go/mautrix-appservice/statestore.go @@ -30,6 +30,8 @@ func (as *AppService) UpdateState(evt *gomatrix.Event) { switch evt.Type { case gomatrix.StateMember: as.StateStore.SetMembership(evt.RoomID, evt.GetStateKey(), evt.Content.Membership) + case gomatrix.StatePowerLevels: + as.StateStore.SetPowerLevels(evt.RoomID, &evt.Content.PowerLevels) } } From e4a78832ad941563df07e4ffb7b6f0ecbb7edf0c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 31 Aug 2018 11:24:27 +0300 Subject: [PATCH 6/8] Switch back to go-whatsapp upstream --- Gopkg.lock | 11 ++-- Gopkg.toml | 5 +- .../github.com/Rhymen/go-whatsapp/handler.go | 52 +++++++------------ .../github.com/Rhymen/go-whatsapp/session.go | 4 +- vendor/github.com/mattn/go-isatty/.travis.yml | 4 ++ .../mattn/go-isatty/isatty_others.go | 2 +- 6 files changed, 35 insertions(+), 43 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 5bca2ba..7960368 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,7 +2,7 @@ [[projects]] - branch = "develop" + branch = "master" name = "github.com/Rhymen/go-whatsapp" packages = [ ".", @@ -13,8 +13,7 @@ "crypto/curve25519", "crypto/hkdf" ] - revision = "00ef431f94f17f125f842d0c7d4e9b68294c6559" - source = "github.com/tulir/go-whatsapp" + revision = "c31092027237441cffba1b9cb148eadf7c83c3d2" [[projects]] name = "github.com/fatih/color" @@ -55,8 +54,8 @@ [[projects]] name = "github.com/mattn/go-isatty" packages = ["."] - revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" - version = "v0.0.3" + revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" + version = "v0.0.4" [[projects]] name = "github.com/mattn/go-sqlite3" @@ -146,6 +145,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "16c945243c327e861a6dc4fa2b43010d00453b8ee71427b95fb5dfcc6cb6ebee" + inputs-digest = "9158f20cc827fadd5ed71302b767c938595986ef9e0623496eddfb4f95f75c76" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 6d31530..7a5e75f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -26,9 +26,10 @@ [[constraint]] - branch = "develop" + branch = "master" name = "github.com/Rhymen/go-whatsapp" - source = "github.com/tulir/go-whatsapp" +# branch = "develop" +# source = "github.com/tulir/go-whatsapp" [[constraint]] name = "github.com/mattn/go-sqlite3" diff --git a/vendor/github.com/Rhymen/go-whatsapp/handler.go b/vendor/github.com/Rhymen/go-whatsapp/handler.go index 586a0ff..62522bd 100644 --- a/vendor/github.com/Rhymen/go-whatsapp/handler.go +++ b/vendor/github.com/Rhymen/go-whatsapp/handler.go @@ -72,7 +72,7 @@ type JsonMessageHandler interface { /** The RawMessageHandler interface needs to be implemented to receive raw messages dispatched by the dispatcher. Raw messages are the raw protobuf structs instead of the easy-to-use structs in TextMessageHandler, ImageMessageHandler, etc.. - */ +*/ type RawMessageHandler interface { Handler HandleRawMessage(message *proto.WebMessageInfo) @@ -96,51 +96,45 @@ func (wac *Conn) handle(message interface{}) { } case string: for _, h := range wac.handler { - x, ok := h.(JsonMessageHandler) - if !ok { - continue + if x, ok := h.(JsonMessageHandler); ok { + go x.HandleJsonMessage(m) } - go x.HandleJsonMessage(m) } case TextMessage: for _, h := range wac.handler { - x, ok := h.(TextMessageHandler) - if !ok { - continue + if x, ok := h.(TextMessageHandler); ok { + go x.HandleTextMessage(m) } - go x.HandleTextMessage(m) } case ImageMessage: for _, h := range wac.handler { - x, ok := h.(ImageMessageHandler) - if !ok { - continue + if x, ok := h.(ImageMessageHandler); ok { + go x.HandleImageMessage(m) } - go x.HandleImageMessage(m) } case VideoMessage: for _, h := range wac.handler { - x, ok := h.(VideoMessageHandler) - if !ok { - continue + if x, ok := h.(VideoMessageHandler); ok { + go x.HandleVideoMessage(m) } - go x.HandleVideoMessage(m) } case AudioMessage: for _, h := range wac.handler { - x, ok := h.(AudioMessageHandler) - if !ok { - continue + if x, ok := h.(AudioMessageHandler); ok { + go x.HandleAudioMessage(m) } - go x.HandleAudioMessage(m) } case DocumentMessage: for _, h := range wac.handler { - x, ok := h.(DocumentMessageHandler) - if !ok { - continue + if x, ok := h.(DocumentMessageHandler); ok { + go x.HandleDocumentMessage(m) + } + } + case *proto.WebMessageInfo: + for _, h := range wac.handler { + if x, ok := h.(RawMessageHandler); ok { + go x.HandleRawMessage(m) } - go x.HandleDocumentMessage(m) } } @@ -157,13 +151,7 @@ func (wac *Conn) dispatch(msg interface{}) { if con, ok := message.Content.([]interface{}); ok { for a := range con { if v, ok := con[a].(*proto.WebMessageInfo); ok { - for _, h := range wac.handler { - x, ok := h.(RawMessageHandler) - if !ok { - continue - } - go x.HandleRawMessage(v) - } + wac.handle(v) wac.handle(parseProtoMessage(v)) } } diff --git a/vendor/github.com/Rhymen/go-whatsapp/session.go b/vendor/github.com/Rhymen/go-whatsapp/session.go index 1a8c8f3..8b46431 100644 --- a/vendor/github.com/Rhymen/go-whatsapp/session.go +++ b/vendor/github.com/Rhymen/go-whatsapp/session.go @@ -87,9 +87,9 @@ func newInfoFromReq(info map[string]interface{}) *Info { /* SetClientName sets the long and short client names that are sent to WhatsApp when logging in and displayed in the WhatsApp Web device list. As the values are only sent when logging in, changing them after logging in is not possible. - */ +*/ func (wac *Conn) SetClientName(long, short string) error { - if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { + if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { return fmt.Errorf("cannot change client name after logging in") } wac.longClientName, wac.shortClientName = long, short diff --git a/vendor/github.com/mattn/go-isatty/.travis.yml b/vendor/github.com/mattn/go-isatty/.travis.yml index b9f8b23..5597e02 100644 --- a/vendor/github.com/mattn/go-isatty/.travis.yml +++ b/vendor/github.com/mattn/go-isatty/.travis.yml @@ -2,6 +2,10 @@ language: go go: - tip +os: + - linux + - osx + before_install: - go get github.com/mattn/goveralls - go get golang.org/x/tools/cmd/cover diff --git a/vendor/github.com/mattn/go-isatty/isatty_others.go b/vendor/github.com/mattn/go-isatty/isatty_others.go index ff4de3d..9d8b4a5 100644 --- a/vendor/github.com/mattn/go-isatty/isatty_others.go +++ b/vendor/github.com/mattn/go-isatty/isatty_others.go @@ -3,7 +3,7 @@ package isatty -// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 // terminal. This is also always false on this environment. func IsCygwinTerminal(fd uintptr) bool { return false From ed27fa775ef60f54b7c03cb1daafe56c98faa1b6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 1 Sep 2018 23:38:03 +0300 Subject: [PATCH 7/8] Re-break everything and fix Matrix->WhatsApp replies --- Gopkg.lock | 4 +- config/bridge.go | 17 +++- config/registration.go | 4 +- database/message.go | 47 ++++++++--- database/puppet.go | 22 ++--- database/user.go | 18 ++++- matrix.go | 4 + portal.go | 81 +++++++++---------- puppet.go | 7 +- .../go/mautrix-appservice/generator.go | 8 +- .../maunium.net/go/mautrix-appservice/http.go | 5 +- .../go/mautrix-appservice/protocol.go | 2 +- .../go/mautrix-appservice/registration.go | 2 +- 13 files changed, 135 insertions(+), 86 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 7960368..53f4509 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -101,7 +101,7 @@ branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "49385e6e15226593f68b26af201feec29d5bba22" + revision = "fa5fdf94c78965f1aa8423f0cc50b8b8d728b05a" [[projects]] name = "gopkg.in/russross/blackfriday.v2" @@ -140,7 +140,7 @@ branch = "master" name = "maunium.net/go/mautrix-appservice" packages = ["."] - revision = "fb756247f82716de7698b8200f28f16b4fd04a6b" + revision = "4e24d1dd7bd9d89f946ec56cb4350ce777d17bfe" [solve-meta] analyzer-name = "dep" diff --git a/config/bridge.go b/config/bridge.go index 47a6860..dff33e4 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -60,13 +60,24 @@ type UsernameTemplateArgs struct { UserID string } -func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string { +func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) { var buf bytes.Buffer if index := strings.IndexRune(contact.Jid, '@'); index > 0 { contact.Jid = "+" + contact.Jid[:index] } bc.displaynameTemplate.Execute(&buf, contact) - return buf.String() + var quality int8 + switch { + case len(contact.Notify) > 0: + quality = 3 + case len(contact.Name) > 0 || len(contact.Short) > 0: + quality = 2 + case len(contact.Jid) > 0: + quality = 1 + default: + quality = 0 + } + return buf.String(), quality } func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string { @@ -76,7 +87,7 @@ func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string { } func (bc BridgeConfig) MarshalYAML() (interface{}, error) { - bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{ + bc.DisplaynameTemplate, _ = bc.FormatDisplayname(whatsapp.Contact{ Jid: "{{.Jid}}", Notify: "{{.Notify}}", Name: "{{.Name}}", diff --git a/config/registration.go b/config/registration.go index a764444..ba0759a 100644 --- a/config/registration.go +++ b/config/registration.go @@ -24,7 +24,7 @@ import ( ) func (config *Config) NewRegistration() (*appservice.Registration, error) { - registration := appservice.CreateRegistration("mautrix-whatsapp") + registration := appservice.CreateRegistration() err := config.copyToRegistration(registration) if err != nil { @@ -37,7 +37,7 @@ func (config *Config) NewRegistration() (*appservice.Registration, error) { } func (config *Config) GetRegistration() (*appservice.Registration, error) { - registration := appservice.CreateRegistration("mautrix-whatsapp") + registration := appservice.CreateRegistration() err := config.copyToRegistration(registration) if err != nil { diff --git a/database/message.go b/database/message.go index 733a91e..a118e0a 100644 --- a/database/message.go +++ b/database/message.go @@ -17,8 +17,11 @@ package database import ( + "bytes" "database/sql" + "encoding/json" + waProto "github.com/Rhymen/go-whatsapp/binary/proto" log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/types" ) @@ -32,8 +35,10 @@ func (mq *MessageQuery) CreateTable() error { _, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message ( chat_jid VARCHAR(25), chat_receiver VARCHAR(25), - jid VARCHAR(255), - mxid VARCHAR(255) NOT NULL UNIQUE, + jid VARCHAR(255), + mxid VARCHAR(255) NOT NULL UNIQUE, + sender VARCHAR(25) NOT NULL, + content BLOB NOT NULL, PRIMARY KEY (chat_jid, chat_receiver, jid), FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) @@ -80,34 +85,54 @@ type Message struct { db *Database log log.Logger - Chat PortalKey + Chat PortalKey JID types.WhatsAppMessageID MXID types.MatrixEventID + Sender types.WhatsAppID + Content *waProto.Message } func (msg *Message) Scan(row Scannable) *Message { - err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID) + var content []byte + err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &content) if err != nil { if err != sql.ErrNoRows { msg.log.Errorln("Database scan failed:", err) } return nil } + + msg.parseBinaryContent(content) + return msg } -func (msg *Message) Insert() error { - _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID) +func (msg *Message) parseBinaryContent(content []byte) { + msg.Content = &waProto.Message{} + reader := bytes.NewReader(content) + // dec := gob.NewDecoder(reader) + dec := json.NewDecoder(reader) + err := dec.Decode(msg.Content) if err != nil { - msg.log.Warnfln("Failed to insert %s: %v", msg.Chat, msg.JID, err) + msg.log.Warnln("Failed to decode message content:", err) } - return err } -func (msg *Message) Update() error { - _, err := msg.db.Exec("UPDATE portal SET mxid=? WHERE chat_jid=? AND chat_receiver=? AND jid=?", msg.MXID, msg.Chat.JID, msg.Chat.Receiver, msg.JID) +func (msg *Message) binaryContent() []byte { + var buf bytes.Buffer + //enc := gob.NewEncoder(&buf) + enc := json.NewEncoder(&buf) + err := enc.Encode(msg.Content) if err != nil { - msg.log.Warnfln("Failed to update %s: %v", msg.Chat, msg.JID, err) + msg.log.Warnln("Failed to encode message content:", err) + } + return buf.Bytes() +} + +func (msg *Message) Insert() error { + _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.binaryContent()) + if err != nil { + msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err) } return err } diff --git a/database/puppet.go b/database/puppet.go index 1b11dcf..f5ebc17 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -30,9 +30,10 @@ type PuppetQuery struct { func (pq *PuppetQuery) CreateTable() error { _, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet ( - jid VARCHAR(25) PRIMARY KEY, - displayname VARCHAR(255), - avatar VARCHAR(255) + jid VARCHAR(25) PRIMARY KEY, + avatar VARCHAR(255), + displayname VARCHAR(255), + name_quality TINYINT )`) return err } @@ -69,8 +70,9 @@ type Puppet struct { log log.Logger JID types.WhatsAppID - Displayname string Avatar string + Displayname string + NameQuality int8 } func (puppet *Puppet) Scan(row Scannable) *Puppet { @@ -88,19 +90,19 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet { } func (puppet *Puppet) Insert() error { - _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?)", - puppet.JID, puppet.Displayname, puppet.Avatar) + _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)", + puppet.JID, puppet.Avatar, puppet.Displayname, puppet.NameQuality) if err != nil { - puppet.log.Errorfln("Failed to insert %s: %v", puppet.JID, err) + puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err) } return err } func (puppet *Puppet) Update() error { - _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=?", - puppet.Displayname, puppet.Avatar, puppet.JID) + _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, name_quality=?, avatar=? WHERE jid=?", + puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.JID) if err != nil { - puppet.log.Errorfln("Failed to update %s->%s: %v", puppet.JID, err) + puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err) } return err } diff --git a/database/user.go b/database/user.go index 686ce02..695785d 100644 --- a/database/user.go +++ b/database/user.go @@ -131,6 +131,14 @@ func stripSuffix(jid types.WhatsAppID) string { return jid[:index] } +func (user *User) jidPtr() *string { + if len(user.JID) > 0 { + str := stripSuffix(user.JID) + return &str + } + return nil +} + func (user *User) sessionUnptr() (sess whatsapp.Session) { if user.Session != nil { sess = *user.Session @@ -140,17 +148,23 @@ func (user *User) sessionUnptr() (sess whatsapp.Session) { func (user *User) Insert() error { sess := user.sessionUnptr() - _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, stripSuffix(user.JID), + _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, user.jidPtr(), user.ManagementRoom, sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey) + if err != nil { + user.log.Warnfln("Failed to insert %s: %v", user.MXID, err) + } return err } func (user *User) Update() error { sess := user.sessionUnptr() _, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=? WHERE mxid=?", - stripSuffix(user.JID), user.ManagementRoom, + user.jidPtr(), user.ManagementRoom, sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, user.MXID) + if err != nil { + user.log.Warnfln("Failed to update %s: %v", user.MXID, err) + } return err } diff --git a/matrix.go b/matrix.go index de63727..cd12447 100644 --- a/matrix.go +++ b/matrix.go @@ -139,6 +139,10 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) { } func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) { + if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { + return + } + roomID := types.MatrixRoomID(evt.RoomID) user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) diff --git a/portal.go b/portal.go index 27635e1..d619dea 100644 --- a/portal.go +++ b/portal.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "encoding/gob" "encoding/hex" "fmt" "image" @@ -121,6 +122,8 @@ type Portal struct { recentlyHandled [20]types.WhatsAppMessageID recentlyHandledLock sync.Mutex recentlyHandledIndex uint8 + + isPrivate *bool } func (portal *Portal) getMessageLock(messageID types.WhatsAppMessageID) sync.Mutex { @@ -157,18 +160,33 @@ func (portal *Portal) isDuplicate(id types.WhatsAppMessageID) bool { return false } -func (portal *Portal) markHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) { +func init() { + gob.Register(&waProto.Message{}) +} + +func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) { msg := portal.bridge.DB.Message.New() msg.Chat = portal.Key - msg.JID = jid + msg.JID = message.GetKey().GetId() msg.MXID = mxid + if message.GetKey().GetFromMe() { + msg.Sender = source.JID + } else if portal.IsPrivateChat() { + msg.Sender = portal.Key.JID + } else { + msg.Sender = message.GetKey().GetParticipant() + if len(msg.Sender) == 0 { + msg.Sender = message.GetParticipant() + } + } + msg.Content = message.Message msg.Insert() portal.recentlyHandledLock.Lock() index := portal.recentlyHandledIndex portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % 20 portal.recentlyHandledLock.Unlock() - portal.recentlyHandled[index] = jid + portal.recentlyHandled[index] = msg.JID } func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bool) { @@ -184,8 +202,9 @@ func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bo return &lock, true } -func (portal *Portal) finishHandling(id types.WhatsAppMessageID, mxid types.MatrixEventID) { - portal.markHandled(id, mxid) +func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) { + portal.markHandled(source, message, mxid) + id := message.GetKey().GetId() portal.deleteMessageLock(id) portal.log.Debugln("Handled message", id, "->", mxid) } @@ -450,7 +469,11 @@ func (portal *Portal) CreateMatrixRoom(invite []string) error { } func (portal *Portal) IsPrivateChat() bool { - return strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix) + if portal.isPrivate == nil { + val := strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix) + portal.isPrivate = &val + } + return *portal.isPrivate } func (portal *Portal) MainIntent() *appservice.IntentAPI { @@ -527,7 +550,7 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) return } - portal.finishHandling(message.Info.Id, resp.EventID) + portal.finishHandling(source, message.Info.Source, resp.EventID) } func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) { @@ -628,7 +651,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, // TODO store caption mxid? } - portal.finishHandling(info.Id, resp.EventID) + portal.finishHandling(source, info.Source, resp.EventID) } func makeMessageID() *string { @@ -716,31 +739,6 @@ type MediaUpload struct { Thumbnail []byte } -func (portal *Portal) GetMessage(user *User, jid types.WhatsAppMessageID) *waProto.WebMessageInfo { - node, err := user.Conn.LoadMessagesBefore(portal.Key.JID, jid, 1) - if err != nil { - return nil - } - msgs, ok := node.Content.([]interface{}) - if !ok { - return nil - } - msg, ok := msgs[0].(*waProto.WebMessageInfo) - if !ok { - return nil - } - node, err = user.Conn.LoadMessagesAfter(portal.Key.JID, msg.GetKey().GetId(), 1) - if err != nil { - return nil - } - msgs, ok = node.Content.([]interface{}) - if !ok { - return nil - } - msg, _ = msgs[0].(*waProto.WebMessageInfo) - return msg -} - func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) { if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver { return @@ -764,17 +762,10 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) { if len(replyToID) > 0 { evt.Content.RemoveReplyFallback() msg := portal.bridge.DB.Message.GetByMXID(replyToID) - if msg != nil { - origMsg := portal.GetMessage(sender, msg.JID) - if origMsg != nil { - ctxInfo.StanzaId = &msg.JID - replyMsgSender := origMsg.GetParticipant() - if origMsg.GetKey().GetFromMe() { - replyMsgSender = sender.JID - } - ctxInfo.Participant = &replyMsgSender - ctxInfo.QuotedMessage = []*waProto.Message{origMsg.Message} - } + if msg != nil && msg.Content != nil { + ctxInfo.StanzaId = &msg.JID + ctxInfo.Participant = &msg.Sender + ctxInfo.QuotedMessage = []*waProto.Message{msg.Content} } } var err error @@ -863,7 +854,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) { portal.log.Debugln("Unhandled Matrix event:", evt) return } - portal.markHandled(info.GetKey().GetId(), evt.ID) + portal.markHandled(sender, info, evt.ID) err = sender.Conn.Send(info) if err != nil { portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) diff --git a/puppet.go b/puppet.go index 587cabc..b66aab1 100644 --- a/puppet.go +++ b/puppet.go @@ -43,7 +43,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.WhatsAppID return "", false } - jid := types.WhatsAppID(match[2] + whatsappExt.NewUserSuffix) + jid := types.WhatsAppID(match[1] + whatsappExt.NewUserSuffix) return jid, true } @@ -168,11 +168,12 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) { if contact.Jid == source.JID { contact.Notify = source.Conn.Info.Pushname } - newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact) - if puppet.Displayname != newName { + newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact) + if puppet.Displayname != newName && quality >= puppet.NameQuality { err := puppet.Intent().SetDisplayName(newName) if err == nil { puppet.Displayname = newName + puppet.NameQuality = quality puppet.Update() } else { puppet.log.Warnln("Failed to set display name:", err) diff --git a/vendor/maunium.net/go/mautrix-appservice/generator.go b/vendor/maunium.net/go/mautrix-appservice/generator.go index 6a40b3b..e78889e 100644 --- a/vendor/maunium.net/go/mautrix-appservice/generator.go +++ b/vendor/maunium.net/go/mautrix-appservice/generator.go @@ -44,14 +44,16 @@ func GenerateRegistration(asName, botName string, reserveRooms, reserveUsers boo boldCyan.Println("Generating appservice config and registration.") reader := bufio.NewReader(os.Stdin) + registration := CreateRegistration() + config := Create() + registration.RateLimited = false + name, err := readString(reader, "Enter name for appservice", asName) if err != nil { fmt.Println("Failed to read user Input:", err) return } - registration := CreateRegistration(name) - config := Create() - registration.RateLimited = false + registration.ID = name registration.SenderLocalpart, err = readString(reader, "Enter bot username", botName) if err != nil { diff --git a/vendor/maunium.net/go/mautrix-appservice/http.go b/vendor/maunium.net/go/mautrix-appservice/http.go index 6e88f3f..3a0757d 100644 --- a/vendor/maunium.net/go/mautrix-appservice/http.go +++ b/vendor/maunium.net/go/mautrix-appservice/http.go @@ -1,11 +1,11 @@ package appservice import ( + "context" "encoding/json" + "github.com/gorilla/mux" "io/ioutil" "net/http" - "github.com/gorilla/mux" - "context" "time" ) @@ -106,7 +106,6 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) { } for _, event := range eventList.Events { - as.Log.Debugln("Received event", event.ID) as.UpdateState(event) as.Events <- event } diff --git a/vendor/maunium.net/go/mautrix-appservice/protocol.go b/vendor/maunium.net/go/mautrix-appservice/protocol.go index 2b422c9..0523ba5 100644 --- a/vendor/maunium.net/go/mautrix-appservice/protocol.go +++ b/vendor/maunium.net/go/mautrix-appservice/protocol.go @@ -2,8 +2,8 @@ package appservice import ( "encoding/json" - "net/http" "maunium.net/go/gomatrix" + "net/http" ) // EventList contains a list of events. diff --git a/vendor/maunium.net/go/mautrix-appservice/registration.go b/vendor/maunium.net/go/mautrix-appservice/registration.go index 85d9c41..b632db0 100644 --- a/vendor/maunium.net/go/mautrix-appservice/registration.go +++ b/vendor/maunium.net/go/mautrix-appservice/registration.go @@ -21,7 +21,7 @@ type Registration struct { } // CreateRegistration creates a Registration with random appservice and homeserver tokens. -func CreateRegistration(name string) *Registration { +func CreateRegistration() *Registration { return &Registration{ AppToken: RandomString(64), ServerToken: RandomString(64), From 68c0190594b575c408d24506d0dbca0d0e47165e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 1 Sep 2018 23:53:47 +0300 Subject: [PATCH 8/8] Rename binary content en/decode functions --- database/message.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/database/message.go b/database/message.go index a118e0a..f89c618 100644 --- a/database/message.go +++ b/database/message.go @@ -102,15 +102,14 @@ func (msg *Message) Scan(row Scannable) *Message { return nil } - msg.parseBinaryContent(content) + msg.decodeBinaryContent(content) return msg } -func (msg *Message) parseBinaryContent(content []byte) { +func (msg *Message) decodeBinaryContent(content []byte) { msg.Content = &waProto.Message{} reader := bytes.NewReader(content) - // dec := gob.NewDecoder(reader) dec := json.NewDecoder(reader) err := dec.Decode(msg.Content) if err != nil { @@ -118,9 +117,8 @@ func (msg *Message) parseBinaryContent(content []byte) { } } -func (msg *Message) binaryContent() []byte { +func (msg *Message) encodeBinaryContent() []byte { var buf bytes.Buffer - //enc := gob.NewEncoder(&buf) enc := json.NewEncoder(&buf) err := enc.Encode(msg.Content) if err != nil { @@ -130,7 +128,7 @@ func (msg *Message) binaryContent() []byte { } func (msg *Message) Insert() error { - _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.binaryContent()) + _, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.encodeBinaryContent()) if err != nil { msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err) }