Browse Source

Fix and add things

* Fix user ID reservation in registration
* Fix some database things
* Add commands
* Add basic contact syncing and portal creation
* Add better error logging
Tulir Asokan 6 years ago
parent
commit
a9124b89bd
13 changed files with 453 additions and 96 deletions
  1. 104 0
      commands.go
  2. 13 6
      config/bridge.go
  3. 1 1
      config/registration.go
  4. 14 4
      database/database.go
  5. 15 2
      database/portal.go
  6. 12 2
      database/puppet.go
  7. 11 7
      database/user.go
  8. 6 4
      example-config.yaml
  9. 21 8
      main.go
  10. 58 17
      matrix.go
  11. 55 4
      portal.go
  12. 27 6
      puppet.go
  13. 116 35
      user.go

+ 104 - 0
commands.go

@@ -0,0 +1,104 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"maunium.net/go/mautrix-whatsapp/types"
+	"strings"
+	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/maulogger"
+)
+
+type CommandHandler struct {
+	bridge *Bridge
+	log    maulogger.Logger
+}
+
+func NewCommandHandler(bridge *Bridge) *CommandHandler {
+	return &CommandHandler{
+		bridge: bridge,
+		log:    bridge.Log.Sub("Command handler"),
+	}
+}
+
+type CommandEvent struct {
+	Bot     *appservice.IntentAPI
+	Bridge  *Bridge
+	Handler *CommandHandler
+	RoomID  types.MatrixRoomID
+	User    *User
+	Args    []string
+}
+
+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)
+	}
+}
+
+func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, message string) {
+	args := strings.Split(message, " ")
+	cmd := strings.ToLower(args[0])
+	ce := &CommandEvent{
+		Bot:     handler.bridge.AppService.BotIntent(),
+		Bridge:  handler.bridge,
+		Handler: handler,
+		RoomID:  roomID,
+		User:    user,
+		Args:    args[1:],
+	}
+	switch cmd {
+	case "login":
+		handler.CommandLogin(ce)
+	case "logout":
+		handler.CommandLogout(ce)
+	case "help":
+		handler.CommandHelp(ce)
+	}
+}
+
+func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
+	if ce.User.Session != nil {
+		ce.Reply("You're already logged in.")
+		return
+	}
+
+	ce.User.Connect(true)
+	ce.User.Login(ce.RoomID)
+}
+
+func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
+	if ce.User.Session == nil {
+		ce.Reply("You're not logged in.")
+		return
+	}
+	err := ce.User.Conn.Logout()
+	if err != nil {
+		ce.User.log.Warnln("Error while logging out:", err)
+		ce.Reply("Error while logging out (see logs for details)")
+		return
+	}
+	ce.User.Conn = nil
+	ce.User.Session = nil
+	ce.User.Update()
+	ce.Reply("Logged out successfully.")
+}
+
+func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
+	ce.Reply("Help is not yet implemented 3:")
+}

+ 13 - 6
config/bridge.go

@@ -22,6 +22,8 @@ import (
 	"maunium.net/go/mautrix-appservice"
 	"strings"
 	"strconv"
+	"github.com/Rhymen/go-whatsapp"
+	"maunium.net/go/mautrix-whatsapp/types"
 )
 
 type BridgeConfig struct {
@@ -62,16 +64,16 @@ type UsernameTemplateArgs struct {
 	UserID   string
 }
 
-func (bc BridgeConfig) FormatDisplayname(displayname string) string {
+func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string {
 	var buf bytes.Buffer
-	bc.displaynameTemplate.Execute(&buf, DisplaynameTemplateArgs{
-		Displayname: displayname,
-	})
+	bc.displaynameTemplate.Execute(&buf, contact)
 	return buf.String()
 }
 
-func (bc BridgeConfig) FormatUsername(receiver, userID string) string {
+func (bc BridgeConfig) FormatUsername(receiver types.MatrixUserID, 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,
@@ -80,7 +82,12 @@ func (bc BridgeConfig) FormatUsername(receiver, userID string) string {
 }
 
 func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
-	bc.DisplaynameTemplate = bc.FormatDisplayname("{{.Displayname}}")
+	bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{
+		Jid: "{{.Jid}}",
+		Notify: "{{.Notify}}",
+		Name: "{{.Name}}",
+		Short: "{{.Short}}",
+	})
 	bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}")
 	return bc, nil
 }

+ 1 - 1
config/registration.go

@@ -55,7 +55,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]+", "[0-9]+"),
+		config.Bridge.FormatUsername(".+", "[0-9]+"),
 		config.Homeserver.Domain))
 	if err != nil {
 		return err

+ 14 - 4
database/database.go

@@ -56,10 +56,20 @@ func New(file string) (*Database, error) {
 	return db, nil
 }
 
-func (db *Database) CreateTables() {
-	db.User.CreateTable()
-	db.Portal.CreateTable()
-	db.Puppet.CreateTable()
+func (db *Database) CreateTables() error {
+	err := db.User.CreateTable()
+	if err != nil {
+		return err
+	}
+	err = db.Portal.CreateTable()
+	if err != nil {
+		return err
+	}
+	err = db.Puppet.CreateTable()
+	if err != nil {
+		return err
+	}
+	return nil
 }
 
 type Scannable interface {

+ 15 - 2
database/portal.go

@@ -19,6 +19,7 @@ package database
 import (
 	log "maunium.net/go/maulogger"
 	"maunium.net/go/mautrix-whatsapp/types"
+	"database/sql"
 )
 
 type PortalQuery struct {
@@ -33,7 +34,7 @@ func (pq *PortalQuery) CreateTable() error {
 		mxid  VARCHAR(255) NOT NULL UNIQUE,
 
 		PRIMARY KEY (jid, owner),
-		FOREIGN KEY owner REFERENCES user(mxid)
+		FOREIGN KEY (owner) REFERENCES user(mxid)
 	)`)
 	return err
 }
@@ -80,22 +81,34 @@ type Portal struct {
 	JID   types.WhatsAppID
 	MXID  types.MatrixRoomID
 	Owner types.MatrixUserID
+
+	Name   string
+	Avatar string
 }
 
 func (portal *Portal) Scan(row Scannable) *Portal {
 	err := row.Scan(&portal.JID, &portal.MXID, &portal.Owner)
 	if err != nil {
-		portal.log.Fatalln("Database scan failed:", err)
+		if err != sql.ErrNoRows {
+			portal.log.Fatalln("Database scan failed:", err)
+		}
+		return nil
 	}
 	return portal
 }
 
 func (portal *Portal) Insert() error {
 	_, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?)", portal.JID, portal.Owner, portal.MXID)
+	if err != nil {
+		portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err)
+	}
 	return err
 }
 
 func (portal *Portal) Update() error {
 	_, err := portal.db.Exec("UPDATE portal SET mxid=? WHERE jid=? AND owner=?", portal.MXID, portal.JID, portal.Owner)
+	if err != nil {
+		portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err)
+	}
 	return err
 }

+ 12 - 2
database/puppet.go

@@ -19,6 +19,7 @@ package database
 import (
 	log "maunium.net/go/maulogger"
 	"maunium.net/go/mautrix-whatsapp/types"
+	"database/sql"
 )
 
 type PuppetQuery struct {
@@ -59,7 +60,7 @@ func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) {
 }
 
 func (pq *PuppetQuery) Get(jid types.WhatsAppID, receiver types.MatrixUserID) *Puppet {
-	row := pq.db.QueryRow("SELECT * FROM user WHERE jid=? AND receiver=?", jid, receiver)
+	row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=? AND receiver=?", jid, receiver)
 	if row == nil {
 		return nil
 	}
@@ -80,7 +81,10 @@ type Puppet struct {
 func (puppet *Puppet) Scan(row Scannable) *Puppet {
 	err := row.Scan(&puppet.JID, &puppet.Receiver, &puppet.Displayname, &puppet.Avatar)
 	if err != nil {
-		puppet.log.Fatalln("Database scan failed:", err)
+		if err != sql.ErrNoRows {
+			puppet.log.Fatalln("Database scan failed:", err)
+		}
+		return nil
 	}
 	return puppet
 }
@@ -88,6 +92,9 @@ 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)
+	if err != nil {
+		puppet.log.Errorln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err)
+	}
 	return err
 }
 
@@ -95,5 +102,8 @@ 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)
+	if err != nil {
+		puppet.log.Errorln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err)
+	}
 	return err
 }

+ 11 - 7
database/user.go

@@ -20,6 +20,7 @@ import (
 	log "maunium.net/go/maulogger"
 	"github.com/Rhymen/go-whatsapp"
 	"maunium.net/go/mautrix-whatsapp/types"
+	"database/sql"
 )
 
 type UserQuery struct {
@@ -45,8 +46,8 @@ func (uq *UserQuery) CreateTable() error {
 
 func (uq *UserQuery) New() *User {
 	return &User{
-		db:  uq.db,
-		log: uq.log,
+		db:     uq.db,
+		log:    uq.log,
 	}
 }
 
@@ -74,17 +75,20 @@ type User struct {
 	db  *Database
 	log log.Logger
 
-	UserID         types.MatrixUserID
+	ID             types.MatrixUserID
 	ManagementRoom types.MatrixRoomID
 	Session        *whatsapp.Session
 }
 
 func (user *User) Scan(row Scannable) *User {
 	sess := whatsapp.Session{}
-	err := row.Scan(&user.UserID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken,
+	err := row.Scan(&user.ID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken,
 		&sess.EncKey, &sess.MacKey, &sess.Wid)
 	if err != nil {
-		user.log.Fatalln("Database scan failed:", err)
+		if err != sql.ErrNoRows {
+			user.log.Fatalln("Database scan failed:", err)
+		}
+		return nil
 	}
 	if len(sess.ClientId) > 0 {
 		user.Session = &sess
@@ -99,7 +103,7 @@ func (user *User) Insert() error {
 	if user.Session != nil {
 		sess = *user.Session
 	}
-	_, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.UserID, user.ManagementRoom,
+	_, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.ID, user.ManagementRoom,
 		sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid)
 	return err
 }
@@ -111,6 +115,6 @@ func (user *User) Update() error {
 	}
 	_, 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.UserID)
+		sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, user.ID)
 	return err
 }

+ 6 - 4
example-config.yaml

@@ -43,12 +43,14 @@ 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.
+  # {{.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}}"
   # Displayname template for WhatsApp users.
-  # {{.displayname}} is replaced with the display name of the WhatsApp user.
-  displayname_template: "{{.Displayname}}"
+  # {{.Name}}   - display name
+  # {{.Short}}  - short display name (usually first name)
+  # {{.Notify}} - nickname (set by the target WhatsApp user)
+  displayname_template: "{{if .Name}}{{.Name}}{{else if .Notify}}{{.Notify}}{{else if .Short}}{{.Short}}{{else}}Unnamed user{{end}}"
 
   # The prefix for commands. Only required in non-management rooms.
   command_prefix: "!wa"

+ 21 - 8
main.go

@@ -26,7 +26,6 @@ import (
 	"maunium.net/go/mautrix-appservice"
 	log "maunium.net/go/maulogger"
 	"maunium.net/go/mautrix-whatsapp/database"
-	"maunium.net/go/gomatrix"
 	"maunium.net/go/mautrix-whatsapp/types"
 )
 
@@ -60,18 +59,21 @@ func (bridge *Bridge) GenerateRegistration() {
 type Bridge struct {
 	AppService     *appservice.AppService
 	EventProcessor *appservice.EventProcessor
+	MatrixHandler  *MatrixHandler
 	Config         *config.Config
 	DB             *database.Database
 	Log            log.Logger
 
 	StateStore *AutosavingStateStore
 
-	users map[types.MatrixUserID]*User
+	users           map[types.MatrixUserID]*User
+	managementRooms map[types.MatrixRoomID]*User
 }
 
 func NewBridge() *Bridge {
 	bridge := &Bridge{
-		users: make(map[types.MatrixUserID]*User),
+		users:           make(map[types.MatrixUserID]*User),
+		managementRooms: make(map[types.MatrixRoomID]*User),
 	}
 	var err error
 	bridge.Config, err = config.Load(*configPath)
@@ -111,23 +113,28 @@ func (bridge *Bridge) Init() {
 		os.Exit(13)
 	}
 
-	bridge.Log.Debugln("Initializing event processor")
+	bridge.Log.Debugln("Initializing Matrix event processor")
 	bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService)
-	bridge.EventProcessor.On(gomatrix.EventMessage, bridge.HandleMessage)
-	bridge.EventProcessor.On(gomatrix.StateMember, bridge.HandleMembership)
+	bridge.Log.Debugln("Initializing Matrix event handler")
+	bridge.MatrixHandler = NewMatrixHandler(bridge)
 }
 
 func (bridge *Bridge) Start() {
-	bridge.DB.CreateTables()
+	err := bridge.DB.CreateTables()
+	if err != nil {
+		bridge.Log.Fatalln("Failed to create database tables:", err)
+		os.Exit(14)
+	}
 	bridge.Log.Debugln("Starting application service HTTP server")
 	go bridge.AppService.Start()
 	bridge.Log.Debugln("Starting event processor")
 	go bridge.EventProcessor.Start()
-	bridge.Log.Debugln("Updating bot profile")
 	go bridge.UpdateBotProfile()
+	go bridge.StartUsers()
 }
 
 func (bridge *Bridge) UpdateBotProfile() {
+	bridge.Log.Debugln("Updating bot profile")
 	botConfig := bridge.Config.AppService.Bot
 
 	var err error
@@ -150,6 +157,12 @@ func (bridge *Bridge) UpdateBotProfile() {
 	}
 }
 
+func (bridge *Bridge) StartUsers() {
+	for _, user := range bridge.GetAllUsers() {
+		go user.Start()
+	}
+}
+
 func (bridge *Bridge) Stop() {
 	bridge.AppService.Stop()
 	bridge.EventProcessor.Stop()

+ 58 - 17
matrix.go

@@ -18,26 +18,49 @@ package main
 
 import (
 	"maunium.net/go/gomatrix"
+	"maunium.net/go/mautrix-whatsapp/types"
+	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/maulogger"
+	"strings"
 )
 
-func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) {
-	intent := bridge.AppService.BotIntent()
+type MatrixHandler struct {
+	bridge *Bridge
+	as     *appservice.AppService
+	log    maulogger.Logger
+	cmd    *CommandHandler
+}
+
+func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
+	handler := &MatrixHandler{
+		bridge: bridge,
+		as:     bridge.AppService,
+		log:    bridge.Log.Sub("Matrix"),
+		cmd:    NewCommandHandler(bridge),
+	}
+	bridge.EventProcessor.On(gomatrix.EventMessage, handler.HandleMessage)
+	bridge.EventProcessor.On(gomatrix.StateMember, handler.HandleMembership)
+	return handler
+}
+
+func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) {
+	intent := mx.as.BotIntent()
 
 	resp, err := intent.JoinRoom(evt.RoomID, "", nil)
 	if err != nil {
-		bridge.Log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
+		mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
 		return
 	}
 
 	members, err := intent.JoinedMembers(resp.RoomID)
 	if err != nil {
-		bridge.Log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender)
+		mx.log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender)
 		intent.LeaveRoom(resp.RoomID)
 		return
 	}
 
 	if len(members.Joined) < 2 {
-		bridge.Log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender)
+		mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender)
 		intent.LeaveRoom(resp.RoomID)
 		return
 	}
@@ -46,31 +69,49 @@ func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) {
 	for mxid, _ := range members.Joined {
 		if mxid == intent.UserID || mxid == evt.Sender {
 			continue
-		} else if _, _, ok := bridge.ParsePuppetMXID(mxid); ok {
+		} else if _, _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok {
 			hasPuppets = true
 			continue
 		}
-		bridge.Log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender)
+		mx.log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender)
 		intent.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
 		intent.LeaveRoom(resp.RoomID)
 		return
 	}
 
 	if !hasPuppets {
-		user := bridge.GetUser(evt.Sender)
-		user.ManagementRoom = resp.RoomID
-		user.Update()
-		intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
-		bridge.Log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
+		user := mx.bridge.GetUser(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)
 	}
 }
 
-func (bridge *Bridge) HandleMembership(evt *gomatrix.Event) {
-	bridge.Log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey())
-	if evt.Content.Membership == "invite" && evt.GetStateKey() == bridge.AppService.BotMXID() {
-		bridge.HandleBotInvite(evt)
+func (mx *MatrixHandler) HandleMembership(evt *gomatrix.Event) {
+	mx.log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey())
+	if evt.Content.Membership == "invite" && evt.GetStateKey() == mx.as.BotMXID() {
+		mx.HandleBotInvite(evt)
 	}
 }
 
-func (bridge *Bridge) HandleMessage(evt *gomatrix.Event) {
+func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) {
+	roomID := types.MatrixRoomID(evt.RoomID)
+	user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
+
+	if evt.Content.MsgType == gomatrix.MsgText {
+		commandPrefix := mx.bridge.Config.Bridge.CommandPrefix
+		hasCommandPrefix := strings.HasPrefix(evt.Content.Body, commandPrefix)
+		if hasCommandPrefix {
+			evt.Content.Body = strings.TrimLeft(evt.Content.Body[len(commandPrefix):], " ")
+		}
+		if hasCommandPrefix || roomID == user.ManagementRoom {
+			mx.cmd.Handle(roomID, user, evt.Content.Body)
+			return
+		}
+	}
+
+	portal := user.GetPortalByMXID(roomID)
+	if portal != nil {
+		portal.HandleMessage(evt)
+	}
 }

+ 55 - 4
portal.go

@@ -21,13 +21,16 @@ import (
 	log "maunium.net/go/maulogger"
 	"fmt"
 	"maunium.net/go/mautrix-whatsapp/types"
+	"maunium.net/go/gomatrix"
+	"strings"
+	"maunium.net/go/mautrix-appservice"
 )
 
 func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
 	portal, ok := user.portalsByMXID[mxid]
 	if !ok {
 		dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
-		if dbPortal == nil || dbPortal.Owner != user.UserID {
+		if dbPortal == nil || dbPortal.Owner != user.ID {
 			return nil
 		}
 		portal = user.NewPortal(dbPortal)
@@ -42,9 +45,12 @@ func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
 func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
 	portal, ok := user.portalsByJID[jid]
 	if !ok {
-		dbPortal := user.bridge.DB.Portal.GetByJID(user.UserID, jid)
+		dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid)
 		if dbPortal == nil {
-			return nil
+			dbPortal = user.bridge.DB.Portal.New()
+			dbPortal.JID = jid
+			dbPortal.Owner = user.ID
+			dbPortal.Insert()
 		}
 		portal = user.NewPortal(dbPortal)
 		user.portalsByJID[portal.JID] = portal
@@ -56,7 +62,7 @@ func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
 }
 
 func (user *User) GetAllPortals() []*Portal {
-	dbPortals := user.bridge.DB.Portal.GetAll(user.UserID)
+	dbPortals := user.bridge.DB.Portal.GetAll(user.ID)
 	output := make([]*Portal, len(dbPortals))
 	for index, dbPortal := range dbPortals {
 		portal, ok := user.portalsByJID[dbPortal.JID]
@@ -88,3 +94,48 @@ type Portal struct {
 	bridge *Bridge
 	log    log.Logger
 }
+
+func (portal *Portal) CreateMatrixRoom() error {
+	if len(portal.MXID) > 0 {
+		return nil
+	}
+
+	name := portal.Name
+	topic := ""
+	isPrivateChat := false
+	if strings.HasSuffix(portal.JID, "s.whatsapp.net") {
+		puppet := portal.user.GetPuppetByJID(portal.JID)
+		name = puppet.Displayname
+		topic = "WhatsApp private chat"
+		isPrivateChat = true
+	}
+	resp, err := portal.MainIntent().CreateRoom(&gomatrix.ReqCreateRoom{
+		Visibility: "private",
+		Name:       name,
+		Topic:      topic,
+		Invite:     []string{portal.user.ID},
+		Preset:     "private_chat",
+		IsDirect:   isPrivateChat,
+	})
+	if err != nil {
+		return err
+	}
+	portal.MXID = resp.RoomID
+	portal.Update()
+	return nil
+}
+
+func (portal *Portal) IsPrivateChat() bool {
+	return strings.HasSuffix(portal.JID, puppetJIDStrippedSuffix)
+}
+
+func (portal *Portal) MainIntent() *appservice.IntentAPI {
+	if portal.IsPrivateChat() {
+		return portal.user.GetPuppetByJID(portal.JID).Intent()
+	}
+	return portal.bridge.AppService.BotIntent()
+}
+
+func (portal *Portal) HandleMessage(evt *gomatrix.Event) {
+	portal.log.Debugln("Received event:", evt)
+}

+ 27 - 6
puppet.go

@@ -23,8 +23,11 @@ import (
 	"regexp"
 	"maunium.net/go/mautrix-whatsapp/types"
 	"strings"
+	"maunium.net/go/mautrix-appservice"
 )
 
+const puppetJIDStrippedSuffix = "@s.whatsapp.net"
+
 func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
 	userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
 		bridge.Config.Bridge.FormatUsername("([0-9]+)", "([0-9]+)"),
@@ -38,11 +41,12 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUser
 		return "", "", false
 	}
 
-	receiver := match[1]
+	receiver := types.MatrixUserID(match[1])
 	receiver = strings.Replace(receiver, "=40", "@", 1)
 	colonIndex := strings.LastIndex(receiver, "=3")
 	receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):]
-	return types.MatrixUserID(receiver), types.WhatsAppID(match[2]), true
+	jid := types.WhatsAppID(match[2] + puppetJIDStrippedSuffix)
+	return receiver, jid, true
 }
 
 func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
@@ -61,7 +65,7 @@ func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
 
 func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
 	receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid)
-	if !ok || receiver != user.UserID {
+	if !ok || receiver != user.ID {
 		return nil
 	}
 
@@ -71,9 +75,12 @@ func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
 func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
 	puppet, ok := user.puppets[jid]
 	if !ok {
-		dbPuppet := user.bridge.DB.Puppet.Get(jid, user.UserID)
+		dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID)
 		if dbPuppet == nil {
-			return nil
+			dbPuppet = user.bridge.DB.Puppet.New()
+			dbPuppet.JID = jid
+			dbPuppet.Receiver = user.ID
+			dbPuppet.Insert()
 		}
 		puppet = user.NewPuppet(dbPuppet)
 		user.puppets[puppet.JID] = puppet
@@ -82,7 +89,7 @@ func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
 }
 
 func (user *User) GetAllPuppets() []*Puppet {
-	dbPuppets := user.bridge.DB.Puppet.GetAll(user.UserID)
+	dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID)
 	output := make([]*Puppet, len(dbPuppets))
 	for index, dbPuppet := range dbPuppets {
 		puppet, ok := user.puppets[dbPuppet.JID]
@@ -101,6 +108,14 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
 		user:   user,
 		bridge: user.bridge,
 		log:    user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)),
+
+		MXID: fmt.Sprintf("@%s:%s",
+			user.bridge.Config.Bridge.FormatUsername(
+				dbPuppet.Receiver,
+				strings.Replace(
+					dbPuppet.JID,
+					puppetJIDStrippedSuffix, "", 1)),
+			user.bridge.Config.Homeserver.Domain),
 	}
 }
 
@@ -110,4 +125,10 @@ type Puppet struct {
 	user   *User
 	bridge *Bridge
 	log    log.Logger
+
+	MXID types.MatrixUserID
+}
+
+func (puppet *Puppet) Intent() *appservice.IntentAPI {
+	return puppet.bridge.AppService.Intent(puppet.MXID)
 }

+ 116 - 35
user.go

@@ -20,11 +20,11 @@ import (
 	"maunium.net/go/mautrix-whatsapp/database"
 	"github.com/Rhymen/go-whatsapp"
 	"time"
-	"fmt"
-	"os"
 	"github.com/skip2/go-qrcode"
 	log "maunium.net/go/maulogger"
 	"maunium.net/go/mautrix-whatsapp/types"
+	"strings"
+	"encoding/json"
 )
 
 type User struct {
@@ -45,10 +45,14 @@ func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
 		dbUser := bridge.DB.User.Get(userID)
 		if dbUser == nil {
 			dbUser = bridge.DB.User.New()
+			dbUser.ID = userID
 			dbUser.Insert()
 		}
 		user = bridge.NewUser(dbUser)
-		bridge.users[user.UserID] = user
+		bridge.users[user.ID] = user
+		if len(user.ManagementRoom) > 0 {
+			bridge.managementRooms[user.ManagementRoom] = user
+		}
 	}
 	return user
 }
@@ -57,57 +61,87 @@ func (bridge *Bridge) GetAllUsers() []*User {
 	dbUsers := bridge.DB.User.GetAll()
 	output := make([]*User, len(dbUsers))
 	for index, dbUser := range dbUsers {
-		user, ok := bridge.users[dbUser.UserID]
+		user, ok := bridge.users[dbUser.ID]
 		if !ok {
 			user = bridge.NewUser(dbUser)
-			bridge.users[user.UserID] = user
+			bridge.users[user.ID] = user
+			if len(user.ManagementRoom) > 0 {
+				bridge.managementRooms[user.ManagementRoom] = user
+			}
 		}
 		output[index] = user
 	}
 	return output
 }
 
-func (bridge *Bridge) InitWhatsApp() {
-	users := bridge.GetAllUsers()
-	for _, user := range users {
-		user.Connect()
+func (bridge *Bridge) NewUser(dbUser *database.User) *User {
+	return &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),
 	}
 }
 
-func (bridge *Bridge) NewUser(dbUser *database.User) *User {
-	return &User{
-		User:   dbUser,
-		bridge: bridge,
-		log:    bridge.Log.Sub("User").Sub(dbUser.UserID),
+func (user *User) SetManagementRoom(roomID types.MatrixRoomID) {
+	existingUser, ok := user.bridge.managementRooms[roomID]
+	if ok {
+		existingUser.ManagementRoom = ""
+		existingUser.Update()
+	}
+
+	user.ManagementRoom = roomID
+	user.bridge.managementRooms[user.ManagementRoom] = user
+	user.Update()
+}
+
+func (user *User) SetSession(session *whatsapp.Session) {
+	user.Session = session
+	user.Update()
+}
+
+func (user *User) Start() {
+	if user.Connect(false) {
+		user.Sync()
 	}
 }
 
-func (user *User) Connect() {
+func (user *User) Connect(evenIfNoSession bool) bool {
+	if user.Conn != nil {
+		return true
+	} else if !evenIfNoSession && user.Session == nil {
+		return false
+	}
+	user.log.Debugln("Connecting to WhatsApp")
 	var err error
 	user.Conn, err = whatsapp.NewConn(20 * time.Second)
 	if err != nil {
 		user.log.Errorln("Failed to connect to WhatsApp:", err)
-		return
+		return false
 	}
+	user.log.Debugln("WhatsApp connection successful")
 	user.Conn.AddHandler(user)
-	user.RestoreSession()
+	return user.RestoreSession()
 }
 
-func (user *User) RestoreSession() {
+func (user *User) RestoreSession() bool {
 	if user.Session != nil {
 		sess, err := user.Conn.RestoreSession(*user.Session)
 		if err != nil {
 			user.log.Errorln("Failed to restore session:", err)
-			user.Session = nil
-			return
+			//user.SetSession(nil)
+			return false
 		}
-		user.Session = &sess
-		user.log.Debugln("Session restored")
+		user.SetSession(&sess)
+		user.log.Debugln("Session restored successfully")
+		return true
 	}
-	return
+	return false
 }
 
-func (user *User) Login(roomID string) {
+func (user *User) Login(roomID types.MatrixRoomID) {
 	bot := user.bridge.AppService.BotClient()
 
 	qrChan := make(chan string, 2)
@@ -130,7 +164,7 @@ func (user *User) Login(roomID string) {
 			return
 		}
 
-		bot.SendImage(roomID, string(qrCode), resp.ContentURI)
+		bot.SendImage(roomID, string(code), resp.ContentURI)
 	}()
 	session, err := user.Conn.Login(qrChan)
 	if err != nil {
@@ -145,32 +179,79 @@ func (user *User) Login(roomID string) {
 	go user.Sync()
 }
 
+func (user *User) SyncPuppet(contact whatsapp.Contact) {
+	puppet := user.GetPuppetByJID(contact.Jid)
+	puppet.Intent().EnsureRegistered()
+
+	newName := user.bridge.Config.Bridge.FormatDisplayname(contact)
+	puppet.log.Debugln(puppet.Displayname, newName, contact.Name)
+	if puppet.Displayname != newName {
+		puppet.Displayname = newName
+		puppet.Update()
+		puppet.Intent().SetDisplayName(puppet.Displayname)
+	}
+}
+
+func (user *User) SyncPortal(contact whatsapp.Contact) {
+	portal := user.GetPortalByJID(contact.Jid)
+
+	if len(portal.MXID) == 0 {
+		if !portal.IsPrivateChat() {
+			portal.Name = contact.Name
+		}
+		err := portal.CreateMatrixRoom()
+		if err != nil {
+			user.log.Errorln("Failed to create portal:", err)
+			return
+		}
+	}
+
+	if !portal.IsPrivateChat() && portal.Name != contact.Name {
+		portal.Name = contact.Name
+		portal.Update()
+		// TODO add SetRoomName function to intent API
+		portal.MainIntent().SendStateEvent(portal.MXID, "m.room.name", "", map[string]interface{}{
+			"name": portal.Name,
+		})
+	}
+}
+
 func (user *User) Sync() {
-	chats, err := user.Conn.Chats()
-	if err != nil {
-		user.log.Warnln("Failed to get chats")
-		return
+	user.log.Debugln("Syncing...")
+	user.Conn.Contacts()
+	user.log.Debugln(user.Conn.Store.Contacts)
+	for jid, contact := range user.Conn.Store.Contacts {
+		dat, _ := json.Marshal(&contact)
+		user.log.Debugln(string(dat))
+		if strings.HasSuffix(jid, puppetJIDStrippedSuffix) {
+			user.SyncPuppet(contact)
+		}
+
+		if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") {
+			// Don't bridge yet
+			continue
+		}
+
+		user.SyncPortal(contact)
 	}
-	user.log.Debugln(chats)
 }
 
 func (user *User) HandleError(err error) {
 	user.log.Errorln("WhatsApp error:", err)
-	fmt.Fprintf(os.Stderr, "%v", err)
 }
 
 func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
-	fmt.Println(message)
+	user.log.Debugln("Text message:", message)
 }
 
 func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
-	fmt.Println(message)
+	user.log.Debugln("Image message:", message)
 }
 
 func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
-	fmt.Println(message)
+	user.log.Debugln("Video message:", message)
 }
 
 func (user *User) HandleJsonMessage(message string) {
-	fmt.Println(message)
+	user.log.Debugln("JSON message:", message)
 }