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