diff --git a/README.md b/README.md
index 71ed349..c71f8c4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# mautrix-whatsapp
-A Matrix-Whatsapp puppeting bridge based the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp)
+A Matrix-WhatsApp puppeting bridge based the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp)
implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project.
Work in progress, please check back later.
diff --git a/config/bridge.go b/config/bridge.go
index 97ebaf8..c6e02f6 100644
--- a/config/bridge.go
+++ b/config/bridge.go
@@ -1,4 +1,4 @@
-// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge.
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
diff --git a/config/config.go b/config/config.go
index 9690258..e74afd4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,4 +1,4 @@
-// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge.
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
@@ -33,7 +33,10 @@ type Config struct {
Hostname string `yaml:"hostname"`
Port uint16 `yaml:"port"`
- Database string `yaml:"database"`
+ Database struct {
+ Type string `yaml:"type"`
+ URI string `yaml:"uri"`
+ } `yaml:"database"`
ID string `yaml:"id"`
Bot struct {
@@ -70,7 +73,7 @@ func (config *Config) Save(path string) error {
return ioutil.WriteFile(path, data, 0600)
}
-func (config *Config) Appservice() (*appservice.Config, error) {
+func (config *Config) MakeAppService() (*appservice.AppService, error) {
as := appservice.Create()
as.LogConfig = config.Logging
as.HomeserverDomain = config.Homeserver.Domain
diff --git a/config/registration.go b/config/registration.go
index 429e3b3..2b78fe5 100644
--- a/config/registration.go
+++ b/config/registration.go
@@ -1,4 +1,4 @@
-// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge.
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
diff --git a/database/database.go b/database/database.go
new file mode 100644
index 0000000..429cc70
--- /dev/null
+++ b/database/database.go
@@ -0,0 +1,51 @@
+// 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 database
+
+import (
+ "database/sql"
+ _ "github.com/mattn/go-sqlite3"
+ log "maunium.net/go/maulogger"
+)
+
+type Database struct {
+ *sql.DB
+ log *log.Sublogger
+
+ User *UserQuery
+}
+
+func New(file string) (*Database, error) {
+ conn, err := sql.Open("sqlite3", file)
+ if err != nil {
+ return nil, err
+ }
+
+ db := &Database{
+ DB: conn,
+ log: log.CreateSublogger("Database", log.LevelDebug),
+ }
+ db.User = &UserQuery{
+ db: db,
+ log: log.CreateSublogger("Database/User", log.LevelDebug),
+ }
+ return db, nil
+}
+
+type Scannable interface {
+ Scan(...interface{}) error
+}
diff --git a/database/portal.go b/database/portal.go
new file mode 100644
index 0000000..af2feca
--- /dev/null
+++ b/database/portal.go
@@ -0,0 +1,100 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package database
+
+import (
+ log "maunium.net/go/maulogger"
+)
+
+type PortalQuery struct {
+ db *Database
+ log *log.Sublogger
+}
+
+func (pq *PortalQuery) CreateTable() error {
+ _, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS portal (
+ jid VARCHAR(255),
+ owner VARCHAR(255),
+ mxid VARCHAR(255) NOT NULL UNIQUE,
+
+ PRIMARY KEY (jid, owner),
+ FOREIGN KEY owner REFERENCES user(mxid)
+ )`)
+ return err
+}
+
+func (pq *PortalQuery) New() *Portal {
+ return &Portal{
+ db: pq.db,
+ log: pq.log,
+ }
+}
+
+func (pq *PortalQuery) GetAll() (portals []*Portal) {
+ rows, err := pq.db.Query("SELECT * FROM portal")
+ if err != nil || rows == nil {
+ return nil
+ }
+ defer rows.Close()
+ for rows.Next() {
+ portals = append(portals, pq.New().Scan(rows))
+ }
+ return
+}
+
+func (pq *PortalQuery) GetByJID(owner, jid string) *Portal {
+ return pq.get("SELECT * FROM portal WHERE jid=? AND owner=?", jid, owner)
+}
+
+func (pq *PortalQuery) GetByMXID(mxid string) *Portal {
+ return pq.get("SELECT * FROM portal WHERE mxid=?", mxid)
+}
+
+func (pq *PortalQuery) get(query string, args ...interface{}) *Portal {
+ row := pq.db.QueryRow(query, args...)
+ if row == nil {
+ return nil
+ }
+ return pq.New().Scan(row)
+}
+
+type Portal struct {
+ db *Database
+ log *log.Sublogger
+
+ JID string
+ MXID string
+ Owner string
+}
+
+func (portal *Portal) Scan(row Scannable) *Portal {
+ err := row.Scan(&portal.JID, &portal.MXID, &portal.Owner)
+ if err != nil {
+ portal.log.Fatalln("Database scan failed:", err)
+ }
+ return portal
+}
+
+func (portal *Portal) Insert() error {
+ _, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?)", portal.JID, portal.Owner, portal.MXID)
+ return err
+}
+
+func (portal *Portal) Update() error {
+ _, err := portal.db.Exec("UPDATE portal SET mxid=? WHERE jid=? AND owner=?", portal.MXID, portal.JID, portal.Owner)
+ return err
+}
diff --git a/database/user.go b/database/user.go
new file mode 100644
index 0000000..4d90f66
--- /dev/null
+++ b/database/user.go
@@ -0,0 +1,99 @@
+// 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 database
+
+import (
+ log "maunium.net/go/maulogger"
+ "github.com/Rhymen/go-whatsapp"
+)
+
+type UserQuery struct {
+ db *Database
+ log *log.Sublogger
+}
+
+func (uq *UserQuery) CreateTable() error {
+ _, err := uq.db.Exec(`CREATE TABLE IF NOT EXISTS user (
+ mxid VARCHAR(255) PRIMARY KEY,
+
+ client_id VARCHAR(255),
+ client_token VARCHAR(255),
+ server_token VARCHAR(255),
+ enc_key BLOB,
+ mac_key BLOB,
+ wid VARCHAR(255)
+ )`)
+ return err
+}
+
+func (uq *UserQuery) New() *User {
+ return &User{
+ db: uq.db,
+ log: uq.log,
+ }
+}
+
+func (uq *UserQuery) GetAll() (users []*User) {
+ rows, err := uq.db.Query("SELECT * FROM user")
+ if err != nil || rows == nil {
+ return nil
+ }
+ defer rows.Close()
+ for rows.Next() {
+ users = append(users, uq.New().Scan(rows))
+ }
+ return
+}
+
+func (uq *UserQuery) Get(userID string) *User {
+ row := uq.db.QueryRow("SELECT * FROM user WHERE mxid=?", userID)
+ if row == nil {
+ return nil
+ }
+ return uq.New().Scan(row)
+}
+
+type User struct {
+ db *Database
+ log *log.Sublogger
+
+ UserID string
+
+ session whatsapp.Session
+}
+
+func (user *User) Scan(row Scannable) *User {
+ err := row.Scan(&user.UserID, &user.session.ClientId, &user.session.ClientToken, &user.session.ServerToken,
+ &user.session.EncKey, &user.session.MacKey, &user.session.Wid)
+ if err != nil {
+ user.log.Fatalln("Database scan failed:", err)
+ }
+ return user
+}
+
+func (user *User) Insert() error {
+ _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?)", user.UserID, user.session.ClientId,
+ user.session.ClientToken, user.session.ServerToken, user.session.EncKey, user.session.MacKey, user.session.Wid)
+ return err
+}
+
+func (user *User) Update() error {
+ _, err := user.db.Exec("UPDATE user SET client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?",
+ user.session.ClientId, user.session.ClientToken, user.session.ServerToken, user.session.EncKey, user.session.MacKey,
+ user.session.Wid, user.UserID)
+ return err
+}
diff --git a/example-config.yaml b/example-config.yaml
index d43b15c..a06d4be 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -15,8 +15,12 @@ appservice:
hostname: 0.0.0.0
port: 8080
- # The full URI to the database. Only SQLite is currently supported.
- database: sqlite:///mautrix-whatsapp.db
+ # Database config.
+ database:
+ # The database type. Only "sqlite3" is supported.
+ type: sqlite3
+ # The database URI. Usually file name. https://github.com/mattn/go-sqlite3#connection-string
+ uri: mautrix-whatsapp.db
# The unique ID of this appservice.
id: whatsapp
@@ -35,12 +39,12 @@ 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.
+ # 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}}"
- # Displayname template for Whatsapp users.
- # {{.displayname}} is replaced with the display name of the Whatsapp user.
+ # Displayname template for WhatsApp users.
+ # {{.displayname}} is replaced with the display name of the WhatsApp user.
displayname_template: "{{.Displayname}}"
# Logging config.
diff --git a/main.go b/main.go
index 70523be..eff3133 100644
--- a/main.go
+++ b/main.go
@@ -1,4 +1,4 @@
-// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge.
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
@@ -24,9 +24,130 @@ import (
"bufio"
"encoding/gob"
"github.com/mdp/qrterminal"
+ "maunium.net/go/mautrix-whatsapp/config"
+ flag "maunium.net/go/mauflag"
+ "os/signal"
+ "syscall"
+ "maunium.net/go/mautrix-appservice"
+ log "maunium.net/go/maulogger"
+ "maunium.net/go/mautrix-whatsapp/database"
)
+var configPath = flag.MakeFull("c", "config", "The path to your config file.", "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()
+
+func (bridge *Bridge) GenerateRegistration() {
+ reg, err := bridge.Config.NewRegistration()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to generate registration:", err)
+ os.Exit(20)
+ }
+
+ err = reg.Save(*registrationPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to save registration:", err)
+ os.Exit(21)
+ }
+
+ err = bridge.Config.Save(*configPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to save config:", err)
+ os.Exit(22)
+ }
+ fmt.Println("Registration generated. Add the path to the registration to your Synapse config restart it, then start the bridge.")
+ os.Exit(0)
+}
+
+type Bridge struct {
+ AppService *appservice.AppService
+ Config *config.Config
+ DB *database.Database
+ Log *log.Logger
+
+ MatrixListener *MatrixListener
+}
+
+func NewBridge() *Bridge {
+ bridge := &Bridge{}
+ var err error
+ bridge.Config, err = config.Load(*configPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to load config:", err)
+ os.Exit(10)
+ }
+ return bridge
+}
+
+func (bridge *Bridge) Init() {
+ var err error
+ bridge.AppService, 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.Parent
+ log.DefaultLogger = bridge.Log
+ bridge.AppService.Log = log.CreateSublogger("Matrix", log.LevelDebug)
+
+ bridge.DB, err = database.New(bridge.Config.AppService.Database.URI)
+ if err != nil {
+ bridge.Log.Fatalln("Failed to initialize database:", err)
+ os.Exit(12)
+ }
+
+ bridge.MatrixListener = NewMatrixListener(bridge)
+}
+
+func (bridge *Bridge) Start() {
+ bridge.AppService.Start()
+ bridge.MatrixListener.Start()
+}
+
+func (bridge *Bridge) Stop() {
+ bridge.AppService.Stop()
+ bridge.MatrixListener.Stop()
+}
+
+func (bridge *Bridge) Main() {
+ if *generateRegistration {
+ bridge.GenerateRegistration()
+ return
+ }
+
+ bridge.Init()
+ bridge.Log.Infoln("Bridge initialization complete, starting...")
+ bridge.Start()
+ bridge.Log.Infoln("Bridge started!")
+
+ c := make(chan os.Signal)
+ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+ <-c
+
+ bridge.Log.Infoln("Interrupt received, stopping...")
+ bridge.Stop()
+ bridge.Log.Infoln("Bridge stopped.")
+ os.Exit(0)
+}
+
func main() {
+ flag.SetHelpTitles("mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.", "[-h] [-c ] [-r ] [-g]")
+ err := flag.Parse()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ flag.PrintHelp()
+ os.Exit(1)
+ } else if *wantHelp {
+ flag.PrintHelp()
+ os.Exit(0)
+ }
+
+ NewBridge().Main()
+}
+
+func temp() {
wac, err := whatsapp.NewConn(20 * time.Second)
if err != nil {
panic(err)
diff --git a/matrix.go b/matrix.go
index b78268b..ee70b4b 100644
--- a/matrix.go
+++ b/matrix.go
@@ -1,4 +1,4 @@
-// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge.
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
@@ -16,6 +16,35 @@
package main
-func InitMatrix() {
+import (
+ log "maunium.net/go/maulogger"
+)
+type MatrixListener struct {
+ bridge *Bridge
+ log *log.Sublogger
+ stop chan struct{}
+}
+
+func NewMatrixListener(bridge *Bridge) *MatrixListener {
+ return &MatrixListener{
+ bridge: bridge,
+ stop: make(chan struct{}, 1),
+ log: bridge.Log.CreateSublogger("Matrix", log.LevelDebug),
+ }
+}
+
+func (ml *MatrixListener) Start() {
+ for {
+ select {
+ case evt := <-ml.bridge.AppService.Events:
+ log.Debugln("Received Matrix event:", evt)
+ case <-ml.stop:
+ return
+ }
+ }
+}
+
+func (ml *MatrixListener) Stop() {
+ ml.stop <- struct{}{}
}
diff --git a/user.go b/user.go
new file mode 100644
index 0000000..460fbb3
--- /dev/null
+++ b/user.go
@@ -0,0 +1,17 @@
+// 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