Эх сурвалжийг харах

[BREAKING] Merge pull request #5 from tulir/desegregate

Desegregate users
Tulir Asokan 6 жил өмнө
parent
commit
c888cb14a0

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@
 *.session
 *.json
 *.db
+*.log

+ 8 - 9
Gopkg.lock

@@ -13,8 +13,7 @@
     "crypto/curve25519",
     "crypto/hkdf"
   ]
-  revision = "82b902133ab6093f864dfc11fb9c5648f82f0ee9"
-  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"
@@ -87,7 +86,7 @@
     "curve25519",
     "hkdf"
   ]
-  revision = "614d502a4dac94afa3a6ce146bd1736da82514c6"
+  revision = "182538f80094b6a8efaade63a8fd8e0d9d5843dd"
 
 [[projects]]
   branch = "master"
@@ -102,7 +101,7 @@
   branch = "master"
   name = "golang.org/x/sys"
   packages = ["unix"]
-  revision = "d99a578cf41bfccdeaf48b0845c823a4b8b0ad5e"
+  revision = "fa5fdf94c78965f1aa8423f0cc50b8b8d728b05a"
 
 [[projects]]
   name = "gopkg.in/russross/blackfriday.v2"
@@ -123,7 +122,7 @@
     ".",
     "format"
   ]
-  revision = "ead1f970c8f56d1854cb9eb4a54c03aa6dafd753"
+  revision = "b018830e10612c04065723de7aa49f35b37864a6"
 
 [[projects]]
   branch = "master"
@@ -141,11 +140,11 @@
   branch = "master"
   name = "maunium.net/go/mautrix-appservice"
   packages = ["."]
-  revision = "269f2ab602126a2de94bc86a457392426cce1ab2"
+  revision = "4e24d1dd7bd9d89f946ec56cb4350ce777d17bfe"
 
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "8b494649cb598fd6a3862d4de5946f464288424b6e6e561e2b32c8d0ea1cb634"
+  inputs-digest = "9158f20cc827fadd5ed71302b767c938595986ef9e0623496eddfb4f95f75c76"
   solver-name = "gps-cdcl"
   solver-version = 1

+ 2 - 1
Gopkg.toml

@@ -28,7 +28,8 @@
 [[constraint]]
   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"

+ 2 - 2
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.Bot,
 		Bridge:  handler.bridge,
 		Handler: handler,
 		RoomID:  roomID,

+ 17 - 16
config/bridge.go

@@ -56,43 +56,44 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	return err
 }
 
-type DisplaynameTemplateArgs struct {
-	Displayname string
-}
-
 type UsernameTemplateArgs struct {
-	Receiver string
 	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(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()
 }
 
 func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
-	bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{
+	bc.DisplaynameTemplate, _ = bc.FormatDisplayname(whatsapp.Contact{
 		Jid:    "{{.Jid}}",
 		Notify: "{{.Notify}}",
 		Name:   "{{.Name}}",
 		Short:  "{{.Short}}",
 	})
-	bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}")
+	bc.UsernameTemplate = bc.FormatUsername("{{.}}")
 	return bc, nil
 }
 

+ 0 - 1
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

+ 3 - 3
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 {
@@ -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

+ 45 - 21
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"
 )
@@ -30,12 +33,15 @@ 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,
-
-		PRIMARY KEY (owner, jid),
-		FOREIGN KEY (owner) REFERENCES user(mxid)
+		chat_jid      VARCHAR(25),
+		chat_receiver VARCHAR(25),
+		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)
 	)`)
 	return err
 }
@@ -47,8 +53,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 +65,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,34 +85,52 @@ 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
+	Sender  types.WhatsAppID
+	Content *waProto.Message
 }
 
 func (msg *Message) Scan(row Scannable) *Message {
-	err := row.Scan(&msg.Owner, &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.decodeBinaryContent(content)
+
 	return msg
 }
 
-func (msg *Message) Insert() error {
-	_, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?)", msg.Owner, msg.JID, msg.MXID)
+func (msg *Message) decodeBinaryContent(content []byte) {
+	msg.Content = &waProto.Message{}
+	reader := bytes.NewReader(content)
+	dec := json.NewDecoder(reader)
+	err := dec.Decode(msg.Content)
 	if err != nil {
-		msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, 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 owner=? AND jid=?", msg.MXID, msg.Owner, msg.JID)
+func (msg *Message) encodeBinaryContent() []byte {
+	var buf bytes.Buffer
+	enc := json.NewEncoder(&buf)
+	err := enc.Encode(msg.Content)
+	if err != nil {
+		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.encodeBinaryContent())
 	if err != nil {
-		msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err)
+		msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
 	}
 	return err
 }

+ 55 - 21
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,25 +122,30 @@ 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)
+	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
 }
 
-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 +155,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
 }

+ 20 - 22
database/puppet.go

@@ -30,13 +30,10 @@ type PuppetQuery struct {
 
 func (pq *PuppetQuery) CreateTable() error {
 	_, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet (
-		jid      VARCHAR(255),
-		receiver VARCHAR(255),
-
-		displayname VARCHAR(255),
-		avatar      VARCHAR(255),
-
-		PRIMARY KEY(jid, receiver)
+		jid          VARCHAR(25) PRIMARY KEY,
+		avatar       VARCHAR(255),
+		displayname  VARCHAR(255),
+		name_quality TINYINT
 	)`)
 	return err
 }
@@ -48,8 +45,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 +57,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,39 +69,40 @@ type Puppet struct {
 	db  *Database
 	log log.Logger
 
-	JID      types.WhatsAppID
-	Receiver types.MatrixUserID
-
-	Displayname string
+	JID         types.WhatsAppID
 	Avatar      string
+	Displayname string
+	NameQuality int8
 }
 
 func (puppet *Puppet) Scan(row Scannable) *Puppet {
-	err := row.Scan(&puppet.JID, &puppet.Receiver, &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
 }
 
 func (puppet *Puppet) Insert() error {
 	_, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)",
-		puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar)
+		puppet.JID, puppet.Avatar, puppet.Displayname, puppet.NameQuality)
 	if err != nil {
-		puppet.log.Errorfln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, 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=? AND receiver=?",
-		puppet.Displayname, puppet.Avatar,
-		puppet.JID, puppet.Receiver)
+	_, 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, puppet.Receiver, err)
+		puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
 	}
 	return err
 }

+ 68 - 19
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 {
@@ -32,6 +34,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),
 
@@ -39,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
 }
@@ -64,7 +66,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,50 +74,97 @@ 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=?", stripSuffix(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,
-		&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) Insert() error {
-	var sess whatsapp.Session
+func stripSuffix(jid types.WhatsAppID) string {
+	if len(jid) == 0 {
+		return jid
+	}
+
+	index := strings.IndexRune(jid, '@')
+	if index < 0 {
+		return jid
+	}
+
+	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
 	}
-	_, 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
+}
+
+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)
+	if err != nil {
+		user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
+	}
 	return err
 }
 
 func (user *User) Update() error {
-	var sess whatsapp.Session
-	if user.Session != nil {
-		sess = *user.Session
+	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=?",
+		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)
 	}
-	_, 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)
 	return err
 }

+ 11 - 12
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
 
@@ -30,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
@@ -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"
@@ -65,15 +64,15 @@ bridge:
   #   domain - All users on that homeserver
   #     mxid - Specific user
   permissions:
-    "example.com": full
+    "example.com": user
     "@admin:example.com": admin
 
 # Logging config.
 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.

+ 92 - 49
formatting.go

@@ -18,58 +18,72 @@ package main
 
 import (
 	"fmt"
+	"html"
 	"regexp"
 	"strings"
 
+	"maunium.net/go/gomatrix"
 	"maunium.net/go/gomatrix/format"
+	"maunium.net/go/mautrix-whatsapp/types"
 	"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<em>$2</em>$3",
-		boldRegex:          "$1<strong>$2</strong>$3",
-		strikethroughRegex: "$1<del>$2</del>$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<em>$2</em>$3",
+			boldRegex:          "$1<strong>$2</strong>$3",
+			strikethroughRegex: "$1<del>$2</del>$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 +92,47 @@ func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regex
 			return fmt.Sprintf("<code>%s</code>", str)
 		},
 		mentionRegex: func(str string) string {
-			jid := str[1:] + whatsappExt.NewUserSuffix
-			puppet := user.GetPuppetByJID(jid)
-			mxid := puppet.MXID
-			if jid == user.JID() {
-				mxid = user.ID
-			}
-			return fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, puppet.Displayname)
+			mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix)
+			return fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname)
 		},
-	}, map[*regexp.Regexp]func(string)string {
+	}
+	formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{
 		mentionRegex: func(str string) string {
-			puppet := user.GetPuppetByJID(str[1:] + whatsappExt.NewUserSuffix)
-			return puppet.Displayname
+			_, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix)
+			return displayname
 		},
 	}
+	return formatter
+}
+
+func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (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)
 }

+ 47 - 22
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,24 +60,38 @@ 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
-
-	users           map[types.MatrixUserID]*User
-	managementRooms map[types.MatrixRoomID]*User
+	StateStore     *AutosavingStateStore
+	Bot            *appservice.IntentAPI
+	Formatter      *Formatter
+
+	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
 	bridge.Config, err = config.Load(*configPath)
 	if err != nil {
@@ -88,46 +104,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)
 	}
-	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(12)
+	}
+	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(13)
 	}
-	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(14)
 	}
 
 	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(15)
 	}
 	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 +165,18 @@ func (bridge *Bridge) UpdateBotProfile() {
 
 	var err error
 	if botConfig.Avatar == "remove" {
-		err = bridge.AppService.BotIntent().SetAvatarURL("")
+		err = bridge.Bot.SetAvatarURL("")
 	} else if len(botConfig.Avatar) > 0 {
-		err = bridge.AppService.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.AppService.BotIntent().SetDisplayName("")
+		err = bridge.Bot.SetDisplayName("")
 	} else if len(botConfig.Avatar) > 0 {
-		err = bridge.AppService.BotIntent().SetDisplayName(botConfig.Displayname)
+		err = bridge.Bot.SetDisplayName(botConfig.Displayname)
 	}
 	if err != nil {
 		bridge.Log.Warnln("Failed to update bot displayname:", err)
@@ -165,7 +190,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 {

+ 19 - 11
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:
@@ -139,8 +139,12 @@ 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.GetUser(types.MatrixUserID(evt.Sender))
+	user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
 
 	if !user.Whitelisted {
 		return
@@ -158,8 +162,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)
 	}
 }

+ 225 - 171
portal.go

@@ -18,9 +18,9 @@ package main
 
 import (
 	"bytes"
+	"encoding/gob"
 	"encoding/hex"
 	"fmt"
-	"html"
 	"image"
 	"image/gif"
 	"image/jpeg"
@@ -41,57 +41,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,23 +98,115 @@ 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)),
+
+		messageLocks:    make(map[types.WhatsAppMessageID]sync.Mutex),
+		recentlyHandled: [20]types.WhatsAppMessageID{},
 	}
 }
 
 type Portal struct {
 	*database.Portal
 
-	user   *User
 	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
+
+	isPrivate *bool
+}
+
+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 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 = 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] = msg.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(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)
 }
 
 func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
@@ -126,9 +217,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 user != nil && !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,20 +234,22 @@ 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 {
-		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) 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 +284,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 +297,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 +308,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 +321,25 @@ 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
+	} else {
+		portal.MainIntent().EnsureInvited(portal.MXID, user.MXID)
 	}
 
 	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()
 	}
@@ -258,10 +358,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,
 		},
 	}
 }
@@ -277,15 +377,19 @@ 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 {
-		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)
+		}
 	}
 }
 
@@ -299,7 +403,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) {
@@ -312,15 +419,18 @@ 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)
+		_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
+		if err != nil {
+			portal.log.Errorln("Failed to change power levels:", err)
+		}
 	}
 }
 
-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 +440,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,39 +469,27 @@ func (portal *Portal) CreateMatrixRoom() error {
 }
 
 func (portal *Portal) IsPrivateChat() bool {
-	return strings.HasSuffix(portal.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 {
 	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.Bot
 }
 
-func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool {
-	msg := portal.bridge.DB.Message.GetByJID(portal.Owner, 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.Owner = portal.Owner
-	msg.JID = jid
-	msg.MXID = mxid
-	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.user.GetPuppetByJID(portal.user.JID()).Intent()
+		return portal.bridge.GetPuppetByJID(user.JID).Intent()
 	} else if portal.IsPrivateChat() {
 		return portal.MainIntent()
 	} else if len(info.SenderJid) == 0 {
@@ -402,14 +499,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,35 +518,20 @@ 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) {
-	if portal.IsDuplicate(message.Info.Id) {
+func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) {
+	lock, ok := portal.startHandling(message.Info.Id)
+	if !ok {
 		return
 	}
+	defer lock.Unlock()
 
-	err := portal.CreateMatrixRoom()
+	err := portal.CreateMatrixRoom([]string{source.MXID})
 	if err != nil {
 		portal.log.Errorln("Failed to create portal room:", err)
 		return
 	}
 
-	intent := portal.GetMessageIntent(message.Info)
+	intent := portal.GetMessageIntent(source, message.Info)
 	if intent == nil {
 		return
 	}
@@ -459,7 +541,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)
@@ -468,22 +550,23 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
 		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(source, message.Info.Source, resp.EventID)
 }
 
-func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) {
-	if portal.IsDuplicate(info.Id) {
+func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) {
+	lock, ok := portal.startHandling(info.Id)
+	if !ok {
 		return
 	}
+	defer lock.Unlock()
 
-	err := portal.CreateMatrixRoom()
+	err := portal.CreateMatrixRoom([]string{source.MXID})
 	if err != nil {
 		portal.log.Errorln("Failed to create portal room:", err)
 		return
 	}
 
-	intent := portal.GetMessageIntent(info)
+	intent := portal.GetMessageIntent(source, info)
 	if intent == nil {
 		return
 	}
@@ -559,7 +642,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 {
@@ -568,8 +651,7 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
 		// TODO store caption mxid?
 	}
 
-	portal.MarkHandled(info.Id, resp.EventID)
-	portal.log.Debugln("Handled message", info.Id, "->", resp.EventID)
+	portal.finishHandling(source, info.Source, resp.EventID)
 }
 
 func makeMessageID() *string {
@@ -612,7 +694,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 +712,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,32 +739,11 @@ type MediaUpload struct {
 	Thumbnail     []byte
 }
 
-func (portal *Portal) GetMessage(jid types.WhatsAppMessageID) *waProto.WebMessageInfo {
-	node, err := portal.user.Conn.LoadMessagesBefore(portal.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 = portal.user.Conn.LoadMessagesAfter(portal.JID, msg.GetKey().GetId(), 1)
-	if err != nil {
-		return nil
-	}
-	msgs, ok = node.Content.([]interface{})
-	if !ok {
-		return nil
+func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) {
+	if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver {
+		return
 	}
-	msg, _ = msgs[0].(*waProto.WebMessageInfo)
-	return msg
-}
 
-func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
 	ts := uint64(evt.Timestamp / 1000)
 	status := waProto.WebMessageInfo_ERROR
 	fromMe := true
@@ -690,7 +751,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{},
@@ -701,17 +762,10 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
 	if len(replyToID) > 0 {
 		evt.Content.RemoveReplyFallback()
 		msg := portal.bridge.DB.Message.GetByMXID(replyToID)
-		if msg != nil {
-			origMsg := portal.GetMessage(msg.JID)
-			if origMsg != nil {
-				ctxInfo.StanzaId = &msg.JID
-				replyMsgSender := origMsg.GetParticipant()
-				if origMsg.GetKey().GetFromMe() {
-					replyMsgSender = portal.user.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
@@ -719,7 +773,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 +791,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 +806,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 +823,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 +838,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,8 +854,8 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
 		portal.log.Debugln("Unhandled Matrix event:", evt)
 		return
 	}
-	err = portal.user.Conn.Send(info)
-	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)
 	} else {

+ 39 - 60
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
+	jid := types.WhatsAppID(match[1] + whatsappExt.NewUserSuffix)
+	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)
-}
-
-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)
+	return bridge.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,24 +162,25 @@ 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 {
+	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)
 		}
 	}
 
-	if puppet.UpdateAvatar(nil) {
+	if puppet.UpdateAvatar(source, nil) {
 		puppet.Update()
 	}
 }

+ 77 - 64
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"
@@ -40,32 +37,44 @@ 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.JID) > 0 {
+			bridge.usersByJID[user.JID] = 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.GetByJID(userID)
+		if dbUser == nil {
+			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
 		}
@@ -74,13 +83,18 @@ 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.JID) > 0 {
+				bridge.usersByJID[user.JID] = user
+			}
 			if len(user.ManagementRoom) > 0 {
 				bridge.managementRooms[user.ManagementRoom] = user
 			}
@@ -94,15 +108,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
 }
 
@@ -142,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()
@@ -152,7 +162,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 +171,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 +207,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)
-		}
-
-		if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") {
-			// No messages sent -> don't bridge
-			continue
+			puppet := user.bridge.GetPuppetByJID(contact.Jid)
+			puppet.Sync(user, contact)
+		} else {
+			portal := user.bridge.GetPortalByJID(database.GroupPortalKey(contact.Jid))
+			portal.Sync(user, contact)
 		}
-
-		portal := user.GetPortalByJID(contact.Jid)
-		portal.Sync(contact)
 	}
 }
 
@@ -237,33 +236,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 +284,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 +303,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 +321,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)
 		}
 	}
 }

+ 6 - 0
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()

+ 20 - 32
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))
 					}
 				}

+ 14 - 2
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

+ 4 - 0
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

+ 1 - 1
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

+ 9 - 9
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
 }

+ 55 - 27
vendor/maunium.net/go/gomatrix/events.go

@@ -5,28 +5,56 @@ import (
 	"sync"
 )
 
-type EventType string
+type EventType struct {
+	Type    string
+	IsState bool
+}
+
+func (et *EventType) UnmarshalJSON(data []byte) error {
+	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) {
+	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
@@ -183,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"`
@@ -258,12 +286,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]
+	level, ok := pl.Events[eventType.String()]
 	if !ok {
-		if isState {
+		if eventType.IsState {
 			return pl.StateDefault()
 		}
 		return pl.EventsDefault
@@ -271,20 +299,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) {
-		delete(pl.Events, eventType)
+	if (eventType.IsState && level == pl.StateDefault()) || (!eventType.IsState && level == pl.EventsDefault) {
+		delete(pl.Events, eventType.String())
 	} else {
-		pl.Events[eventType] = level
+		pl.Events[eventType.String()] = 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

+ 19 - 8
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()
 	}
 }
 

+ 5 - 3
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 {

+ 2 - 3
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
 	}

+ 13 - 3
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,
 	})
 }
@@ -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
+}

+ 1 - 1
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.

+ 1 - 1
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),

+ 25 - 16
vendor/maunium.net/go/mautrix-appservice/statestore.go

@@ -15,19 +15,23 @@ 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) {
 	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)
 	}
 }
 
@@ -126,7 +130,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 +178,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)
 }