Răsfoiți Sursa

Add database models and improve config/main

Tulir Asokan 6 ani în urmă
părinte
comite
fd3c1fb77c
11 a modificat fișierele cu 440 adăugiri și 16 ștergeri
  1. 1 1
      README.md
  2. 1 1
      config/bridge.go
  3. 6 3
      config/config.go
  4. 1 1
      config/registration.go
  5. 51 0
      database/database.go
  6. 100 0
      database/portal.go
  7. 99 0
      database/user.go
  8. 11 7
      example-config.yaml
  9. 122 1
      main.go
  10. 31 2
      matrix.go
  11. 17 0
      user.go

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

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

+ 6 - 3
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

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

+ 51 - 0
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 <https://www.gnu.org/licenses/>.
+
+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
+}

+ 100 - 0
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 <https://www.gnu.org/licenses/>.
+
+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
+}

+ 99 - 0
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 <https://www.gnu.org/licenses/>.
+
+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
+}

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

+ 122 - 1
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 <path>] [-r <path>] [-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)

+ 31 - 2
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{}{}
 }

+ 17 - 0
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 <https://www.gnu.org/licenses/>.
+
+package main