diff --git a/config/bridge.go b/config/bridge.go
index e57155c..a6bd4c5 100644
--- a/config/bridge.go
+++ b/config/bridge.go
@@ -19,12 +19,19 @@ package config
import (
"bytes"
"text/template"
+ "maunium.net/go/mautrix-appservice"
+ "strings"
+ "strconv"
)
type BridgeConfig struct {
- UsernameTemplate string `yaml:"username_template"`
- DisplaynameTemplate string `yaml:"displayname_template"`
- StateStore string `yaml:"state_store_path"`
+ UsernameTemplate string `yaml:"username_template"`
+ DisplaynameTemplate string `yaml:"displayname_template"`
+
+ CommandPrefix string `yaml:"command_prefix"`
+
+ Permissions PermissionConfig `yaml:"permissions"`
+
usernameTemplate *template.Template `yaml:"-"`
displaynameTemplate *template.Template `yaml:"-"`
}
@@ -77,3 +84,87 @@ func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}")
return bc, nil
}
+
+type PermissionConfig map[string]PermissionLevel
+
+type PermissionLevel int
+
+const (
+ PermissionLevelDefault PermissionLevel = 0
+ PermissionLevelUser PermissionLevel = 10
+ PermissionLevelAdmin PermissionLevel = 100
+)
+
+func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ rawPC := make(map[string]string)
+ err := unmarshal(&rawPC)
+ if err != nil {
+ return err
+ }
+
+ if *pc == nil {
+ *pc = make(map[string]PermissionLevel)
+ }
+ for key, value := range rawPC {
+ switch strings.ToLower(value) {
+ case "user":
+ (*pc)[key] = PermissionLevelUser
+ case "admin":
+ (*pc)[key] = PermissionLevelAdmin
+ default:
+ val, err := strconv.Atoi(value)
+ if err != nil {
+ (*pc)[key] = PermissionLevelDefault
+ } else {
+ (*pc)[key] = PermissionLevel(val)
+ }
+ }
+ }
+ return nil
+}
+
+func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
+ if *pc == nil {
+ return nil, nil
+ }
+ rawPC := make(map[string]string)
+ for key, value := range *pc {
+ switch value {
+ case PermissionLevelUser:
+ rawPC[key] = "user"
+ case PermissionLevelAdmin:
+ rawPC[key] = "admin"
+ default:
+ rawPC[key] = strconv.Itoa(int(value))
+ }
+ }
+ return rawPC, nil
+}
+
+func (pc PermissionConfig) IsWhitelisted(userID string) bool {
+ return pc.GetPermissionLevel(userID) >= 10
+}
+
+func (pc PermissionConfig) IsAdmin(userID string) bool {
+ return pc.GetPermissionLevel(userID) >= 100
+}
+
+func (pc PermissionConfig) GetPermissionLevel(userID string) PermissionLevel {
+ permissions, ok := pc[userID]
+ if ok {
+ return permissions
+ }
+
+ _, homeserver := appservice.ParseUserID(userID)
+ permissions, ok = pc[homeserver]
+ if len(homeserver) > 0 && ok {
+ return permissions
+ }
+
+ permissions, ok = pc["*"]
+ if ok {
+ return permissions
+ }
+
+ return PermissionLevelDefault
+}
diff --git a/config/config.go b/config/config.go
index 11af894..5817e25 100644
--- a/config/config.go
+++ b/config/config.go
@@ -38,6 +38,8 @@ type Config struct {
URI string `yaml:"uri"`
} `yaml:"database"`
+ StateStore string `yaml:"state_store_path"`
+
ID string `yaml:"id"`
Bot struct {
Username string `yaml:"username"`
diff --git a/config/registration.go b/config/registration.go
index 1ea1724..25a8a78 100644
--- a/config/registration.go
+++ b/config/registration.go
@@ -54,7 +54,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration)
registration.RateLimited = false
registration.SenderLocalpart = config.AppService.Bot.Username
- userIDRegex, err := regexp.Compile(fmt.Sprintf("@%s:%s",
+ userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
config.Bridge.FormatUsername("[0-9]+", "[0-9]+"),
config.Homeserver.Domain))
if err != nil {
diff --git a/database/portal.go b/database/portal.go
index 115cb8d..8055ed3 100644
--- a/database/portal.go
+++ b/database/portal.go
@@ -18,6 +18,7 @@ package database
import (
log "maunium.net/go/maulogger"
+ "maunium.net/go/mautrix-whatsapp/types"
)
type PortalQuery struct {
@@ -44,7 +45,7 @@ func (pq *PortalQuery) New() *Portal {
}
}
-func (pq *PortalQuery) GetAll(owner string) (portals []*Portal) {
+func (pq *PortalQuery) GetAll(owner types.MatrixUserID) (portals []*Portal) {
rows, err := pq.db.Query("SELECT * FROM portal WHERE owner=?", owner)
if err != nil || rows == nil {
return nil
@@ -56,11 +57,11 @@ func (pq *PortalQuery) GetAll(owner string) (portals []*Portal) {
return
}
-func (pq *PortalQuery) GetByJID(owner, jid string) *Portal {
+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) GetByMXID(mxid string) *Portal {
+func (pq *PortalQuery) GetByMXID(mxid types.MatrixRoomID) *Portal {
return pq.get("SELECT * FROM portal WHERE mxid=?", mxid)
}
@@ -76,9 +77,9 @@ type Portal struct {
db *Database
log log.Logger
- JID string
- MXID string
- Owner string
+ JID types.WhatsAppID
+ MXID types.MatrixRoomID
+ Owner types.MatrixUserID
}
func (portal *Portal) Scan(row Scannable) *Portal {
diff --git a/database/puppet.go b/database/puppet.go
index 0154afb..8e44be3 100644
--- a/database/puppet.go
+++ b/database/puppet.go
@@ -18,6 +18,7 @@ package database
import (
log "maunium.net/go/maulogger"
+ "maunium.net/go/mautrix-whatsapp/types"
)
type PuppetQuery struct {
@@ -45,8 +46,8 @@ func (pq *PuppetQuery) New() *Puppet {
}
}
-func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
- rows, err := pq.db.Query("SELECT * FROM puppet")
+func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) {
+ rows, err := pq.db.Query("SELECT * FROM puppet WHERE receiver=%s")
if err != nil || rows == nil {
return nil
}
@@ -57,7 +58,7 @@ func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
return
}
-func (pq *PuppetQuery) Get(jid, receiver string) *Puppet {
+func (pq *PuppetQuery) Get(jid types.WhatsAppID, receiver types.MatrixUserID) *Puppet {
row := pq.db.QueryRow("SELECT * FROM user WHERE jid=? AND receiver=?", jid, receiver)
if row == nil {
return nil
@@ -69,8 +70,8 @@ type Puppet struct {
db *Database
log log.Logger
- JID string
- Receiver string
+ JID types.WhatsAppID
+ Receiver types.MatrixUserID
Displayname string
Avatar string
diff --git a/database/user.go b/database/user.go
index 967e89c..5e35d90 100644
--- a/database/user.go
+++ b/database/user.go
@@ -19,6 +19,7 @@ package database
import (
log "maunium.net/go/maulogger"
"github.com/Rhymen/go-whatsapp"
+ "maunium.net/go/mautrix-whatsapp/types"
)
type UserQuery struct {
@@ -61,7 +62,7 @@ func (uq *UserQuery) GetAll() (users []*User) {
return
}
-func (uq *UserQuery) Get(userID string) *User {
+func (uq *UserQuery) Get(userID types.MatrixUserID) *User {
row := uq.db.QueryRow("SELECT * FROM user WHERE mxid=?", userID)
if row == nil {
return nil
@@ -73,8 +74,8 @@ type User struct {
db *Database
log log.Logger
- UserID string
- ManagementRoom string
+ UserID types.MatrixUserID
+ ManagementRoom types.MatrixRoomID
Session *whatsapp.Session
}
diff --git a/example-config.yaml b/example-config.yaml
index 0b09b83..c90908b 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -22,12 +22,15 @@ appservice:
# 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
+
# The unique ID of this appservice.
id: whatsapp
# Appservice bot details.
bot:
# Username of the appservice bot.
- username: whatsappbot
+ username: whatsapp
# 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
@@ -46,8 +49,21 @@ bridge:
# Displayname template for WhatsApp users.
# {{.displayname}} is replaced with the display name of the WhatsApp user.
displayname_template: "{{.Displayname}}"
- # Path to the Matrix room state store.
- state_store_path: ./mx-state.json
+
+ # The prefix for commands. Only required in non-management rooms.
+ command_prefix: "!wa"
+
+ # Permissions for using the bridge.
+ # Permitted values:
+ # user - Access to use the bridge to chat with a WhatsApp account.
+ # admin - User level and some additional administration tools
+ # Permitted keys:
+ # * - All Matrix users
+ # domain - All users on that homeserver
+ # mxid - Specific user
+ permissions:
+ "example.com": full
+ "@admin:example.com": admin
# Logging config.
logging:
diff --git a/main.go b/main.go
index 5e248d5..a7bae96 100644
--- a/main.go
+++ b/main.go
@@ -17,13 +17,8 @@
package main
import (
- "github.com/Rhymen/go-whatsapp"
- "time"
"fmt"
"os"
- "bufio"
- "encoding/gob"
- "github.com/mdp/qrterminal"
"maunium.net/go/mautrix-whatsapp/config"
flag "maunium.net/go/mauflag"
"os/signal"
@@ -31,6 +26,8 @@ import (
"maunium.net/go/mautrix-appservice"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/database"
+ "maunium.net/go/gomatrix"
+ "maunium.net/go/mautrix-whatsapp/types"
)
var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
@@ -61,20 +58,21 @@ func (bridge *Bridge) GenerateRegistration() {
}
type Bridge struct {
- AppService *appservice.AppService
- Config *config.Config
- DB *database.Database
- Log log.Logger
+ AppService *appservice.AppService
+ EventProcessor *appservice.EventProcessor
+ Config *config.Config
+ DB *database.Database
+ Log log.Logger
StateStore *AutosavingStateStore
- MatrixListener *MatrixListener
-
- users map[string]*User
+ users map[types.MatrixUserID]*User
}
func NewBridge() *Bridge {
- bridge := &Bridge{}
+ bridge := &Bridge{
+ users: make(map[types.MatrixUserID]*User),
+ }
var err error
bridge.Config, err = config.Load(*configPath)
if err != nil {
@@ -97,7 +95,8 @@ func (bridge *Bridge) Init() {
log.DefaultLogger = bridge.Log.(*log.BasicLogger)
bridge.AppService.Log = log.Sub("Matrix")
- bridge.StateStore = NewAutosavingStateStore(bridge.Config.Bridge.StateStore)
+ 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)
@@ -105,19 +104,26 @@ func (bridge *Bridge) Init() {
}
bridge.AppService.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)
}
- bridge.MatrixListener = NewMatrixListener(bridge)
+ bridge.Log.Debugln("Initializing event processor")
+ bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService)
+ bridge.EventProcessor.On(gomatrix.EventMessage, bridge.HandleMessage)
+ bridge.EventProcessor.On(gomatrix.StateMember, bridge.HandleMembership)
}
func (bridge *Bridge) Start() {
bridge.DB.CreateTables()
+ bridge.Log.Debugln("Starting application service HTTP server")
go bridge.AppService.Start()
- go bridge.MatrixListener.Start()
+ bridge.Log.Debugln("Starting event processor")
+ go bridge.EventProcessor.Start()
+ bridge.Log.Debugln("Updating bot profile")
go bridge.UpdateBotProfile()
}
@@ -146,7 +152,7 @@ func (bridge *Bridge) UpdateBotProfile() {
func (bridge *Bridge) Stop() {
bridge.AppService.Stop()
- bridge.MatrixListener.Stop()
+ bridge.EventProcessor.Stop()
err := bridge.StateStore.Save()
if err != nil {
bridge.Log.Warnln("Failed to save state store:", err)
@@ -190,90 +196,3 @@ func main() {
NewBridge().Main()
}
-
-func temp() {
- wac, err := whatsapp.NewConn(20 * time.Second)
- if err != nil {
- panic(err)
- }
-
- wac.AddHandler(myHandler{})
-
- sess, err := LoadSession("whatsapp.session")
- if err != nil {
- fmt.Println(err)
- sess, err = Login(wac)
- } else {
- sess, err = wac.RestoreSession(sess)
- }
- if err != nil {
- panic(err)
- }
- SaveSession(sess, "whatsapp.session")
-
- reader := bufio.NewReader(os.Stdin)
- for {
- fmt.Print("receiver> ")
- receiver, _ := reader.ReadString('\n')
- fmt.Print("message> ")
- message, _ := reader.ReadString('\n')
- wac.Send(whatsapp.TextMessage{
- Info: whatsapp.MessageInfo{
- RemoteJid: fmt.Sprintf("%s@s.whatsapp.net", receiver),
- },
- Text: message,
- })
- fmt.Println(receiver, message)
- }
-}
-
-func Login(wac *whatsapp.Conn) (whatsapp.Session, error) {
- qrChan := make(chan string)
- go func() {
- qrterminal.Generate(<-qrChan, qrterminal.L, os.Stdout)
- }()
- return wac.Login(qrChan)
-}
-
-func SaveSession(session whatsapp.Session, fileName string) {
- file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
- if err != nil {
- panic(err)
- }
-
- enc := gob.NewEncoder(file)
- enc.Encode(session)
-}
-
-func LoadSession(fileName string) (sess whatsapp.Session, err error) {
- file, err := os.OpenFile(fileName, os.O_RDONLY, 0600)
- if err != nil {
- return sess, err
- }
-
- dec := gob.NewDecoder(file)
- dec.Decode(sess)
- return
-}
-
-type myHandler struct{}
-
-func (myHandler) HandleError(err error) {
- fmt.Fprintf(os.Stderr, "%v", err)
-}
-
-func (myHandler) HandleTextMessage(message whatsapp.TextMessage) {
- fmt.Println(message)
-}
-
-func (myHandler) HandleImageMessage(message whatsapp.ImageMessage) {
- fmt.Println(message)
-}
-
-func (myHandler) HandleVideoMessage(message whatsapp.VideoMessage) {
- fmt.Println(message)
-}
-
-func (myHandler) HandleJsonMessage(message string) {
- fmt.Println(message)
-}
diff --git a/matrix.go b/matrix.go
index 110bba1..f69ff9b 100644
--- a/matrix.go
+++ b/matrix.go
@@ -17,96 +17,60 @@
package main
import (
- log "maunium.net/go/maulogger"
- "maunium.net/go/mautrix-appservice"
"maunium.net/go/gomatrix"
)
-type MatrixListener struct {
- bridge *Bridge
- as *appservice.AppService
- log log.Logger
- stop chan struct{}
-}
-
-func NewMatrixListener(bridge *Bridge) *MatrixListener {
- return &MatrixListener{
- bridge: bridge,
- as: bridge.AppService,
- stop: make(chan struct{}, 1),
- log: bridge.Log.Sub("Matrix Listener"),
- }
-}
-
-func (ml *MatrixListener) Start() {
- for {
- select {
- case evt := <-ml.bridge.AppService.Events:
- ml.log.Debugln("Received Matrix event:", evt)
- switch evt.Type {
- case gomatrix.StateMember:
- ml.HandleMembership(evt)
- case gomatrix.EventMessage:
- ml.HandleMessage(evt)
- }
- case <-ml.stop:
- return
- }
- }
-}
-
-func (ml *MatrixListener) HandleBotInvite(evt *gomatrix.Event) {
- intent := ml.as.BotIntent()
+func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) {
+ intent := bridge.AppService.BotIntent()
resp, err := intent.JoinRoom(evt.RoomID, "", nil)
if err != nil {
- ml.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
+ bridge.Log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
return
}
members, err := intent.JoinedMembers(resp.RoomID)
if err != nil {
- ml.log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender)
+ bridge.Log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender)
intent.LeaveRoom(resp.RoomID)
return
}
if len(members.Joined) < 2 {
- ml.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender)
+ bridge.Log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender)
intent.LeaveRoom(resp.RoomID)
return
}
+
+ hasPuppets := false
for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == evt.Sender {
continue
- } else if true { // TODO check if mxid is WhatsApp puppet
-
+ } else if _, _, ok := bridge.ParsePuppetMXID(mxid); ok {
+ hasPuppets = true
continue
}
- ml.log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender)
+ bridge.Log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender)
intent.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
intent.LeaveRoom(resp.RoomID)
return
}
- user := ml.bridge.GetUser(evt.Sender)
- user.ManagementRoom = resp.RoomID
- user.Update()
- intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
- ml.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
-}
-
-func (ml *MatrixListener) HandleMembership(evt *gomatrix.Event) {
- ml.log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey())
- if evt.Content.Membership == "invite" && evt.GetStateKey() == ml.as.BotMXID() {
- ml.HandleBotInvite(evt)
+ if !hasPuppets {
+ user := bridge.GetUser(evt.Sender)
+ user.ManagementRoom = resp.RoomID
+ user.Update()
+ intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
+ bridge.Log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
}
}
-func (ml *MatrixListener) HandleMessage(evt *gomatrix.Event) {
-
+func (bridge *Bridge) HandleMembership(evt *gomatrix.Event) {
+ bridge.Log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey())
+ if evt.Content.Membership == "invite" && evt.GetStateKey() == bridge.AppService.BotMXID() {
+ bridge.HandleBotInvite(evt)
+ }
}
-func (ml *MatrixListener) Stop() {
- ml.stop <- struct{}{}
+func (bridge *Bridge) HandleMessage(evt *gomatrix.Event) {
}
diff --git a/portal.go b/portal.go
index 2f3ea77..4d35185 100644
--- a/portal.go
+++ b/portal.go
@@ -20,9 +20,10 @@ import (
"maunium.net/go/mautrix-whatsapp/database"
log "maunium.net/go/maulogger"
"fmt"
+ "maunium.net/go/mautrix-whatsapp/types"
)
-func (user *User) GetPortalByMXID(mxid string) *Portal {
+func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
portal, ok := user.portalsByMXID[mxid]
if !ok {
dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
@@ -38,7 +39,7 @@ func (user *User) GetPortalByMXID(mxid string) *Portal {
return portal
}
-func (user *User) GetPortalByJID(jid string) *Portal {
+func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
portal, ok := user.portalsByJID[jid]
if !ok {
dbPortal := user.bridge.DB.Portal.GetByJID(user.UserID, jid)
diff --git a/puppet.go b/puppet.go
new file mode 100644
index 0000000..417f564
--- /dev/null
+++ b/puppet.go
@@ -0,0 +1,113 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package main
+
+import (
+ "maunium.net/go/mautrix-whatsapp/database"
+ log "maunium.net/go/maulogger"
+ "fmt"
+ "regexp"
+ "maunium.net/go/mautrix-whatsapp/types"
+ "strings"
+)
+
+func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
+ userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
+ bridge.Config.Bridge.FormatUsername("([0-9]+)", "([0-9]+)"),
+ bridge.Config.Homeserver.Domain))
+ if err != nil {
+ bridge.Log.Warnln("Failed to compile puppet user ID regex:", err)
+ return "", "", false
+ }
+ match := userIDRegex.FindStringSubmatch(string(mxid))
+ if match == nil || len(match) != 3 {
+ return "", "", false
+ }
+
+ receiver := match[1]
+ receiver = strings.Replace(receiver, "=40", "@", 1)
+ colonIndex := strings.LastIndex(receiver, "=3")
+ receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):]
+ return types.MatrixUserID(receiver), types.WhatsAppID(match[2]), true
+}
+
+func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
+ receiver, 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.UserID {
+ return nil
+ }
+
+ return user.GetPuppetByJID(jid)
+}
+
+func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
+ puppet, ok := user.puppets[jid]
+ if !ok {
+ dbPuppet := user.bridge.DB.Puppet.Get(jid, user.UserID)
+ if dbPuppet == nil {
+ return nil
+ }
+ puppet = user.NewPuppet(dbPuppet)
+ user.puppets[puppet.JID] = puppet
+ }
+ return puppet
+}
+
+func (user *User) GetAllPuppets() []*Puppet {
+ dbPuppets := user.bridge.DB.Puppet.GetAll(user.UserID)
+ output := make([]*Puppet, len(dbPuppets))
+ for index, dbPuppet := range dbPuppets {
+ puppet, ok := user.puppets[dbPuppet.JID]
+ if !ok {
+ puppet = user.NewPuppet(dbPuppet)
+ user.puppets[dbPuppet.JID] = puppet
+ }
+ output[index] = puppet
+ }
+ return output
+}
+
+func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
+ return &Puppet{
+ Puppet: dbPuppet,
+ user: user,
+ bridge: user.bridge,
+ log: user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)),
+ }
+}
+
+type Puppet struct {
+ *database.Puppet
+
+ user *User
+ bridge *Bridge
+ log log.Logger
+}
diff --git a/types/types.go b/types/types.go
new file mode 100644
index 0000000..6d1da77
--- /dev/null
+++ b/types/types.go
@@ -0,0 +1,26 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package types
+
+// WhatsAppID is a WhatsApp JID.
+type WhatsAppID = string
+
+// MatrixUserID is the ID of a Matrix user.
+type MatrixUserID = string
+
+// MatrixRoomID is the internal room ID of a Matrix room.
+type MatrixRoomID = string
diff --git a/user.go b/user.go
index 579f3ab..6a98275 100644
--- a/user.go
+++ b/user.go
@@ -24,6 +24,7 @@ import (
"os"
"github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger"
+ "maunium.net/go/mautrix-whatsapp/types"
)
type User struct {
@@ -33,12 +34,12 @@ type User struct {
bridge *Bridge
log log.Logger
- portalsByMXID map[string]*Portal
- portalsByJID map[string]*Portal
- puppets map[string]*Portal
+ portalsByMXID map[types.MatrixRoomID]*Portal
+ portalsByJID map[types.WhatsAppID]*Portal
+ puppets map[types.WhatsAppID]*Puppet
}
-func (bridge *Bridge) GetUser(userID string) *User {
+func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
user, ok := bridge.users[userID]
if !ok {
dbUser := bridge.DB.User.Get(userID)