Fix and add things

* Fix user ID reservation in registration
* Fix some database things
* Add commands
* Add basic contact syncing and portal creation
* Add better error logging
This commit is contained in:
Tulir Asokan 2018-08-18 22:57:08 +03:00
parent edd4f817e4
commit a9124b89bd
13 changed files with 455 additions and 98 deletions

104
commands.go Normal file
View file

@ -0,0 +1,104 @@
// 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
import (
"maunium.net/go/mautrix-whatsapp/types"
"strings"
"maunium.net/go/mautrix-appservice"
"maunium.net/go/maulogger"
)
type CommandHandler struct {
bridge *Bridge
log maulogger.Logger
}
func NewCommandHandler(bridge *Bridge) *CommandHandler {
return &CommandHandler{
bridge: bridge,
log: bridge.Log.Sub("Command handler"),
}
}
type CommandEvent struct {
Bot *appservice.IntentAPI
Bridge *Bridge
Handler *CommandHandler
RoomID types.MatrixRoomID
User *User
Args []string
}
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)
}
}
func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, message string) {
args := strings.Split(message, " ")
cmd := strings.ToLower(args[0])
ce := &CommandEvent{
Bot: handler.bridge.AppService.BotIntent(),
Bridge: handler.bridge,
Handler: handler,
RoomID: roomID,
User: user,
Args: args[1:],
}
switch cmd {
case "login":
handler.CommandLogin(ce)
case "logout":
handler.CommandLogout(ce)
case "help":
handler.CommandHelp(ce)
}
}
func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
if ce.User.Session != nil {
ce.Reply("You're already logged in.")
return
}
ce.User.Connect(true)
ce.User.Login(ce.RoomID)
}
func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
if ce.User.Session == nil {
ce.Reply("You're not logged in.")
return
}
err := ce.User.Conn.Logout()
if err != nil {
ce.User.log.Warnln("Error while logging out:", err)
ce.Reply("Error while logging out (see logs for details)")
return
}
ce.User.Conn = nil
ce.User.Session = nil
ce.User.Update()
ce.Reply("Logged out successfully.")
}
func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
ce.Reply("Help is not yet implemented 3:")
}

View file

@ -22,6 +22,8 @@ import (
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"strings" "strings"
"strconv" "strconv"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix-whatsapp/types"
) )
type BridgeConfig struct { type BridgeConfig struct {
@ -62,16 +64,16 @@ type UsernameTemplateArgs struct {
UserID string UserID string
} }
func (bc BridgeConfig) FormatDisplayname(displayname string) string { func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string {
var buf bytes.Buffer var buf bytes.Buffer
bc.displaynameTemplate.Execute(&buf, DisplaynameTemplateArgs{ bc.displaynameTemplate.Execute(&buf, contact)
Displayname: displayname,
})
return buf.String() return buf.String()
} }
func (bc BridgeConfig) FormatUsername(receiver, userID string) string { func (bc BridgeConfig) FormatUsername(receiver types.MatrixUserID, userID types.WhatsAppID) string {
var buf bytes.Buffer var buf bytes.Buffer
receiver = strings.Replace(receiver, "@", "=40", 1)
receiver = strings.Replace(receiver, ":", "=3", 1)
bc.usernameTemplate.Execute(&buf, UsernameTemplateArgs{ bc.usernameTemplate.Execute(&buf, UsernameTemplateArgs{
Receiver: receiver, Receiver: receiver,
UserID: userID, UserID: userID,
@ -80,7 +82,12 @@ func (bc BridgeConfig) FormatUsername(receiver, userID string) string {
} }
func (bc BridgeConfig) MarshalYAML() (interface{}, error) { func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
bc.DisplaynameTemplate = bc.FormatDisplayname("{{.Displayname}}") bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{
Jid: "{{.Jid}}",
Notify: "{{.Notify}}",
Name: "{{.Name}}",
Short: "{{.Short}}",
})
bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}") bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}")
return bc, nil return bc, nil
} }

View file

@ -55,7 +55,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration)
registration.SenderLocalpart = config.AppService.Bot.Username 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.Bridge.FormatUsername(".+", "[0-9]+"),
config.Homeserver.Domain)) config.Homeserver.Domain))
if err != nil { if err != nil {
return err return err

View file

@ -56,10 +56,20 @@ func New(file string) (*Database, error) {
return db, nil return db, nil
} }
func (db *Database) CreateTables() { func (db *Database) CreateTables() error {
db.User.CreateTable() err := db.User.CreateTable()
db.Portal.CreateTable() if err != nil {
db.Puppet.CreateTable() return err
}
err = db.Portal.CreateTable()
if err != nil {
return err
}
err = db.Puppet.CreateTable()
if err != nil {
return err
}
return nil
} }
type Scannable interface { type Scannable interface {

View file

@ -19,6 +19,7 @@ package database
import ( import (
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"database/sql"
) )
type PortalQuery struct { type PortalQuery struct {
@ -33,7 +34,7 @@ func (pq *PortalQuery) CreateTable() error {
mxid VARCHAR(255) NOT NULL UNIQUE, mxid VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (jid, owner), PRIMARY KEY (jid, owner),
FOREIGN KEY owner REFERENCES user(mxid) FOREIGN KEY (owner) REFERENCES user(mxid)
)`) )`)
return err return err
} }
@ -80,22 +81,34 @@ type Portal struct {
JID types.WhatsAppID JID types.WhatsAppID
MXID types.MatrixRoomID MXID types.MatrixRoomID
Owner types.MatrixUserID Owner types.MatrixUserID
Name string
Avatar string
} }
func (portal *Portal) Scan(row Scannable) *Portal { func (portal *Portal) Scan(row Scannable) *Portal {
err := row.Scan(&portal.JID, &portal.MXID, &portal.Owner) err := row.Scan(&portal.JID, &portal.MXID, &portal.Owner)
if err != nil { if err != nil {
if err != sql.ErrNoRows {
portal.log.Fatalln("Database scan failed:", err) portal.log.Fatalln("Database scan failed:", err)
} }
return nil
}
return portal return portal
} }
func (portal *Portal) Insert() error { func (portal *Portal) Insert() error {
_, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?)", portal.JID, portal.Owner, portal.MXID) _, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?)", portal.JID, portal.Owner, portal.MXID)
if err != nil {
portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err)
}
return err return err
} }
func (portal *Portal) Update() error { func (portal *Portal) Update() error {
_, err := portal.db.Exec("UPDATE portal SET mxid=? WHERE jid=? AND owner=?", portal.MXID, portal.JID, portal.Owner) _, err := portal.db.Exec("UPDATE portal SET mxid=? WHERE jid=? AND owner=?", portal.MXID, portal.JID, portal.Owner)
if err != nil {
portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err)
}
return err return err
} }

View file

@ -19,6 +19,7 @@ package database
import ( import (
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"database/sql"
) )
type PuppetQuery struct { type PuppetQuery struct {
@ -59,7 +60,7 @@ func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) {
} }
func (pq *PuppetQuery) Get(jid types.WhatsAppID, receiver types.MatrixUserID) *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) row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=? AND receiver=?", jid, receiver)
if row == nil { if row == nil {
return nil return nil
} }
@ -80,14 +81,20 @@ type Puppet struct {
func (puppet *Puppet) Scan(row Scannable) *Puppet { func (puppet *Puppet) Scan(row Scannable) *Puppet {
err := row.Scan(&puppet.JID, &puppet.Receiver, &puppet.Displayname, &puppet.Avatar) err := row.Scan(&puppet.JID, &puppet.Receiver, &puppet.Displayname, &puppet.Avatar)
if err != nil { if err != nil {
if err != sql.ErrNoRows {
puppet.log.Fatalln("Database scan failed:", err) puppet.log.Fatalln("Database scan failed:", err)
} }
return nil
}
return puppet return puppet
} }
func (puppet *Puppet) Insert() error { func (puppet *Puppet) Insert() error {
_, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)", _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)",
puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar) puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar)
if err != nil {
puppet.log.Errorln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err)
}
return err return err
} }
@ -95,5 +102,8 @@ func (puppet *Puppet) Update() error {
_, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=? AND receiver=?", _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=? AND receiver=?",
puppet.Displayname, puppet.Avatar, puppet.Displayname, puppet.Avatar,
puppet.JID, puppet.Receiver) puppet.JID, puppet.Receiver)
if err != nil {
puppet.log.Errorln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err)
}
return err return err
} }

View file

@ -20,6 +20,7 @@ import (
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"database/sql"
) )
type UserQuery struct { type UserQuery struct {
@ -74,18 +75,21 @@ type User struct {
db *Database db *Database
log log.Logger log log.Logger
UserID types.MatrixUserID ID types.MatrixUserID
ManagementRoom types.MatrixRoomID ManagementRoom types.MatrixRoomID
Session *whatsapp.Session Session *whatsapp.Session
} }
func (user *User) Scan(row Scannable) *User { func (user *User) Scan(row Scannable) *User {
sess := whatsapp.Session{} sess := whatsapp.Session{}
err := row.Scan(&user.UserID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken, err := row.Scan(&user.ID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken,
&sess.EncKey, &sess.MacKey, &sess.Wid) &sess.EncKey, &sess.MacKey, &sess.Wid)
if err != nil { if err != nil {
if err != sql.ErrNoRows {
user.log.Fatalln("Database scan failed:", err) user.log.Fatalln("Database scan failed:", err)
} }
return nil
}
if len(sess.ClientId) > 0 { if len(sess.ClientId) > 0 {
user.Session = &sess user.Session = &sess
} else { } else {
@ -99,7 +103,7 @@ func (user *User) Insert() error {
if user.Session != nil { if user.Session != nil {
sess = *user.Session sess = *user.Session
} }
_, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.UserID, user.ManagementRoom, _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.ID, user.ManagementRoom,
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid) sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid)
return err return err
} }
@ -111,6 +115,6 @@ func (user *User) Update() error {
} }
_, err := user.db.Exec("UPDATE user SET management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?", _, err := user.db.Exec("UPDATE user SET management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?",
user.ManagementRoom, user.ManagementRoom,
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, user.UserID) sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, user.ID)
return err return err
} }

View file

@ -43,12 +43,14 @@ appservice:
# Bridge config. Currently unused. # Bridge config. Currently unused.
bridge: bridge:
# Localpart template of MXIDs for WhatsApp users. # Localpart template of MXIDs for WhatsApp users.
# {{.receiver}} is replaced with the WhatsApp user ID of the Matrix user receiving messages. # {{.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. # {{.UserID}} is replaced with the user ID of the WhatsApp user.
username_template: "whatsapp_{{.Receiver}}_{{.UserID}}" username_template: "whatsapp_{{.Receiver}}_{{.UserID}}"
# Displayname template for WhatsApp users. # Displayname template for WhatsApp users.
# {{.displayname}} is replaced with the display name of the WhatsApp user. # {{.Name}} - display name
displayname_template: "{{.Displayname}}" # {{.Short}} - short display name (usually first name)
# {{.Notify}} - nickname (set by the target WhatsApp user)
displayname_template: "{{if .Name}}{{.Name}}{{else if .Notify}}{{.Notify}}{{else if .Short}}{{.Short}}{{else}}Unnamed user{{end}}"
# The prefix for commands. Only required in non-management rooms. # The prefix for commands. Only required in non-management rooms.
command_prefix: "!wa" command_prefix: "!wa"

25
main.go
View file

@ -26,7 +26,6 @@ import (
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/gomatrix"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
) )
@ -60,6 +59,7 @@ func (bridge *Bridge) GenerateRegistration() {
type Bridge struct { type Bridge struct {
AppService *appservice.AppService AppService *appservice.AppService
EventProcessor *appservice.EventProcessor EventProcessor *appservice.EventProcessor
MatrixHandler *MatrixHandler
Config *config.Config Config *config.Config
DB *database.Database DB *database.Database
Log log.Logger Log log.Logger
@ -67,11 +67,13 @@ type Bridge struct {
StateStore *AutosavingStateStore StateStore *AutosavingStateStore
users map[types.MatrixUserID]*User users map[types.MatrixUserID]*User
managementRooms map[types.MatrixRoomID]*User
} }
func NewBridge() *Bridge { func NewBridge() *Bridge {
bridge := &Bridge{ bridge := &Bridge{
users: make(map[types.MatrixUserID]*User), users: make(map[types.MatrixUserID]*User),
managementRooms: make(map[types.MatrixRoomID]*User),
} }
var err error var err error
bridge.Config, err = config.Load(*configPath) bridge.Config, err = config.Load(*configPath)
@ -111,23 +113,28 @@ func (bridge *Bridge) Init() {
os.Exit(13) os.Exit(13)
} }
bridge.Log.Debugln("Initializing event processor") bridge.Log.Debugln("Initializing Matrix event processor")
bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService) bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService)
bridge.EventProcessor.On(gomatrix.EventMessage, bridge.HandleMessage) bridge.Log.Debugln("Initializing Matrix event handler")
bridge.EventProcessor.On(gomatrix.StateMember, bridge.HandleMembership) bridge.MatrixHandler = NewMatrixHandler(bridge)
} }
func (bridge *Bridge) Start() { func (bridge *Bridge) Start() {
bridge.DB.CreateTables() err := bridge.DB.CreateTables()
if err != nil {
bridge.Log.Fatalln("Failed to create database tables:", err)
os.Exit(14)
}
bridge.Log.Debugln("Starting application service HTTP server") bridge.Log.Debugln("Starting application service HTTP server")
go bridge.AppService.Start() go bridge.AppService.Start()
bridge.Log.Debugln("Starting event processor") bridge.Log.Debugln("Starting event processor")
go bridge.EventProcessor.Start() go bridge.EventProcessor.Start()
bridge.Log.Debugln("Updating bot profile")
go bridge.UpdateBotProfile() go bridge.UpdateBotProfile()
go bridge.StartUsers()
} }
func (bridge *Bridge) UpdateBotProfile() { func (bridge *Bridge) UpdateBotProfile() {
bridge.Log.Debugln("Updating bot profile")
botConfig := bridge.Config.AppService.Bot botConfig := bridge.Config.AppService.Bot
var err error var err error
@ -150,6 +157,12 @@ func (bridge *Bridge) UpdateBotProfile() {
} }
} }
func (bridge *Bridge) StartUsers() {
for _, user := range bridge.GetAllUsers() {
go user.Start()
}
}
func (bridge *Bridge) Stop() { func (bridge *Bridge) Stop() {
bridge.AppService.Stop() bridge.AppService.Stop()
bridge.EventProcessor.Stop() bridge.EventProcessor.Stop()

View file

@ -18,26 +18,49 @@ package main
import ( import (
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-appservice"
"maunium.net/go/maulogger"
"strings"
) )
func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) { type MatrixHandler struct {
intent := bridge.AppService.BotIntent() bridge *Bridge
as *appservice.AppService
log maulogger.Logger
cmd *CommandHandler
}
func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
handler := &MatrixHandler{
bridge: bridge,
as: bridge.AppService,
log: bridge.Log.Sub("Matrix"),
cmd: NewCommandHandler(bridge),
}
bridge.EventProcessor.On(gomatrix.EventMessage, handler.HandleMessage)
bridge.EventProcessor.On(gomatrix.StateMember, handler.HandleMembership)
return handler
}
func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) {
intent := mx.as.BotIntent()
resp, err := intent.JoinRoom(evt.RoomID, "", nil) resp, err := intent.JoinRoom(evt.RoomID, "", nil)
if err != nil { if err != nil {
bridge.Log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender) mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
return return
} }
members, err := intent.JoinedMembers(resp.RoomID) members, err := intent.JoinedMembers(resp.RoomID)
if err != nil { if err != nil {
bridge.Log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender) mx.log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender)
intent.LeaveRoom(resp.RoomID) intent.LeaveRoom(resp.RoomID)
return return
} }
if len(members.Joined) < 2 { if len(members.Joined) < 2 {
bridge.Log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender) mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender)
intent.LeaveRoom(resp.RoomID) intent.LeaveRoom(resp.RoomID)
return return
} }
@ -46,31 +69,49 @@ func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) {
for mxid, _ := range members.Joined { for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == evt.Sender { if mxid == intent.UserID || mxid == evt.Sender {
continue continue
} else if _, _, ok := bridge.ParsePuppetMXID(mxid); ok { } else if _, _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok {
hasPuppets = true hasPuppets = true
continue continue
} }
bridge.Log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender) mx.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.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
intent.LeaveRoom(resp.RoomID) intent.LeaveRoom(resp.RoomID)
return return
} }
if !hasPuppets { if !hasPuppets {
user := bridge.GetUser(evt.Sender) user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
user.ManagementRoom = resp.RoomID user.SetManagementRoom(types.MatrixRoomID(resp.RoomID))
user.Update() intent.SendNotice(string(user.ManagementRoom), "This room has been registered as your bridge management/status room.")
intent.SendNotice(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)
bridge.Log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
} }
} }
func (bridge *Bridge) HandleMembership(evt *gomatrix.Event) { func (mx *MatrixHandler) HandleMembership(evt *gomatrix.Event) {
bridge.Log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey()) mx.log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey())
if evt.Content.Membership == "invite" && evt.GetStateKey() == bridge.AppService.BotMXID() { if evt.Content.Membership == "invite" && evt.GetStateKey() == mx.as.BotMXID() {
bridge.HandleBotInvite(evt) mx.HandleBotInvite(evt)
} }
} }
func (bridge *Bridge) HandleMessage(evt *gomatrix.Event) { func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) {
roomID := types.MatrixRoomID(evt.RoomID)
user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
if evt.Content.MsgType == gomatrix.MsgText {
commandPrefix := mx.bridge.Config.Bridge.CommandPrefix
hasCommandPrefix := strings.HasPrefix(evt.Content.Body, commandPrefix)
if hasCommandPrefix {
evt.Content.Body = strings.TrimLeft(evt.Content.Body[len(commandPrefix):], " ")
}
if hasCommandPrefix || roomID == user.ManagementRoom {
mx.cmd.Handle(roomID, user, evt.Content.Body)
return
}
}
portal := user.GetPortalByMXID(roomID)
if portal != nil {
portal.HandleMessage(evt)
}
} }

View file

@ -21,13 +21,16 @@ import (
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"fmt" "fmt"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/gomatrix"
"strings"
"maunium.net/go/mautrix-appservice"
) )
func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
portal, ok := user.portalsByMXID[mxid] portal, ok := user.portalsByMXID[mxid]
if !ok { if !ok {
dbPortal := user.bridge.DB.Portal.GetByMXID(mxid) dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
if dbPortal == nil || dbPortal.Owner != user.UserID { if dbPortal == nil || dbPortal.Owner != user.ID {
return nil return nil
} }
portal = user.NewPortal(dbPortal) portal = user.NewPortal(dbPortal)
@ -42,9 +45,12 @@ func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal { func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
portal, ok := user.portalsByJID[jid] portal, ok := user.portalsByJID[jid]
if !ok { if !ok {
dbPortal := user.bridge.DB.Portal.GetByJID(user.UserID, jid) dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid)
if dbPortal == nil { if dbPortal == nil {
return nil dbPortal = user.bridge.DB.Portal.New()
dbPortal.JID = jid
dbPortal.Owner = user.ID
dbPortal.Insert()
} }
portal = user.NewPortal(dbPortal) portal = user.NewPortal(dbPortal)
user.portalsByJID[portal.JID] = portal user.portalsByJID[portal.JID] = portal
@ -56,7 +62,7 @@ func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
} }
func (user *User) GetAllPortals() []*Portal { func (user *User) GetAllPortals() []*Portal {
dbPortals := user.bridge.DB.Portal.GetAll(user.UserID) dbPortals := user.bridge.DB.Portal.GetAll(user.ID)
output := make([]*Portal, len(dbPortals)) output := make([]*Portal, len(dbPortals))
for index, dbPortal := range dbPortals { for index, dbPortal := range dbPortals {
portal, ok := user.portalsByJID[dbPortal.JID] portal, ok := user.portalsByJID[dbPortal.JID]
@ -88,3 +94,48 @@ type Portal struct {
bridge *Bridge bridge *Bridge
log log.Logger log log.Logger
} }
func (portal *Portal) CreateMatrixRoom() error {
if len(portal.MXID) > 0 {
return nil
}
name := portal.Name
topic := ""
isPrivateChat := false
if strings.HasSuffix(portal.JID, "s.whatsapp.net") {
puppet := portal.user.GetPuppetByJID(portal.JID)
name = puppet.Displayname
topic = "WhatsApp private chat"
isPrivateChat = true
}
resp, err := portal.MainIntent().CreateRoom(&gomatrix.ReqCreateRoom{
Visibility: "private",
Name: name,
Topic: topic,
Invite: []string{portal.user.ID},
Preset: "private_chat",
IsDirect: isPrivateChat,
})
if err != nil {
return err
}
portal.MXID = resp.RoomID
portal.Update()
return nil
}
func (portal *Portal) IsPrivateChat() bool {
return strings.HasSuffix(portal.JID, puppetJIDStrippedSuffix)
}
func (portal *Portal) MainIntent() *appservice.IntentAPI {
if portal.IsPrivateChat() {
return portal.user.GetPuppetByJID(portal.JID).Intent()
}
return portal.bridge.AppService.BotIntent()
}
func (portal *Portal) HandleMessage(evt *gomatrix.Event) {
portal.log.Debugln("Received event:", evt)
}

View file

@ -23,8 +23,11 @@ import (
"regexp" "regexp"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"strings" "strings"
"maunium.net/go/mautrix-appservice"
) )
const puppetJIDStrippedSuffix = "@s.whatsapp.net"
func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) { func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
bridge.Config.Bridge.FormatUsername("([0-9]+)", "([0-9]+)"), bridge.Config.Bridge.FormatUsername("([0-9]+)", "([0-9]+)"),
@ -38,11 +41,12 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUser
return "", "", false return "", "", false
} }
receiver := match[1] receiver := types.MatrixUserID(match[1])
receiver = strings.Replace(receiver, "=40", "@", 1) receiver = strings.Replace(receiver, "=40", "@", 1)
colonIndex := strings.LastIndex(receiver, "=3") colonIndex := strings.LastIndex(receiver, "=3")
receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):] receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):]
return types.MatrixUserID(receiver), types.WhatsAppID(match[2]), true jid := types.WhatsAppID(match[2] + puppetJIDStrippedSuffix)
return receiver, jid, true
} }
func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
@ -61,7 +65,7 @@ func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid) receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid)
if !ok || receiver != user.UserID { if !ok || receiver != user.ID {
return nil return nil
} }
@ -71,9 +75,12 @@ func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet { func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
puppet, ok := user.puppets[jid] puppet, ok := user.puppets[jid]
if !ok { if !ok {
dbPuppet := user.bridge.DB.Puppet.Get(jid, user.UserID) dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID)
if dbPuppet == nil { if dbPuppet == nil {
return nil dbPuppet = user.bridge.DB.Puppet.New()
dbPuppet.JID = jid
dbPuppet.Receiver = user.ID
dbPuppet.Insert()
} }
puppet = user.NewPuppet(dbPuppet) puppet = user.NewPuppet(dbPuppet)
user.puppets[puppet.JID] = puppet user.puppets[puppet.JID] = puppet
@ -82,7 +89,7 @@ func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
} }
func (user *User) GetAllPuppets() []*Puppet { func (user *User) GetAllPuppets() []*Puppet {
dbPuppets := user.bridge.DB.Puppet.GetAll(user.UserID) dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID)
output := make([]*Puppet, len(dbPuppets)) output := make([]*Puppet, len(dbPuppets))
for index, dbPuppet := range dbPuppets { for index, dbPuppet := range dbPuppets {
puppet, ok := user.puppets[dbPuppet.JID] puppet, ok := user.puppets[dbPuppet.JID]
@ -101,6 +108,14 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
user: user, user: user,
bridge: user.bridge, bridge: user.bridge,
log: user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), log: user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)),
MXID: fmt.Sprintf("@%s:%s",
user.bridge.Config.Bridge.FormatUsername(
dbPuppet.Receiver,
strings.Replace(
dbPuppet.JID,
puppetJIDStrippedSuffix, "", 1)),
user.bridge.Config.Homeserver.Domain),
} }
} }
@ -110,4 +125,10 @@ type Puppet struct {
user *User user *User
bridge *Bridge bridge *Bridge
log log.Logger log log.Logger
MXID types.MatrixUserID
}
func (puppet *Puppet) Intent() *appservice.IntentAPI {
return puppet.bridge.AppService.Intent(puppet.MXID)
} }

147
user.go
View file

@ -20,11 +20,11 @@ import (
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"time" "time"
"fmt"
"os"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"strings"
"encoding/json"
) )
type User struct { type User struct {
@ -45,10 +45,14 @@ func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
dbUser := bridge.DB.User.Get(userID) dbUser := bridge.DB.User.Get(userID)
if dbUser == nil { if dbUser == nil {
dbUser = bridge.DB.User.New() dbUser = bridge.DB.User.New()
dbUser.ID = userID
dbUser.Insert() dbUser.Insert()
} }
user = bridge.NewUser(dbUser) user = bridge.NewUser(dbUser)
bridge.users[user.UserID] = user bridge.users[user.ID] = user
if len(user.ManagementRoom) > 0 {
bridge.managementRooms[user.ManagementRoom] = user
}
} }
return user return user
} }
@ -57,57 +61,87 @@ func (bridge *Bridge) GetAllUsers() []*User {
dbUsers := bridge.DB.User.GetAll() dbUsers := bridge.DB.User.GetAll()
output := make([]*User, len(dbUsers)) output := make([]*User, len(dbUsers))
for index, dbUser := range dbUsers { for index, dbUser := range dbUsers {
user, ok := bridge.users[dbUser.UserID] user, ok := bridge.users[dbUser.ID]
if !ok { if !ok {
user = bridge.NewUser(dbUser) user = bridge.NewUser(dbUser)
bridge.users[user.UserID] = user bridge.users[user.ID] = user
if len(user.ManagementRoom) > 0 {
bridge.managementRooms[user.ManagementRoom] = user
}
} }
output[index] = user output[index] = user
} }
return output return output
} }
func (bridge *Bridge) InitWhatsApp() {
users := bridge.GetAllUsers()
for _, user := range users {
user.Connect()
}
}
func (bridge *Bridge) NewUser(dbUser *database.User) *User { func (bridge *Bridge) NewUser(dbUser *database.User) *User {
return &User{ return &User{
User: dbUser, User: dbUser,
bridge: bridge, bridge: bridge,
log: bridge.Log.Sub("User").Sub(dbUser.UserID), 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),
} }
} }
func (user *User) Connect() { func (user *User) SetManagementRoom(roomID types.MatrixRoomID) {
existingUser, ok := user.bridge.managementRooms[roomID]
if ok {
existingUser.ManagementRoom = ""
existingUser.Update()
}
user.ManagementRoom = roomID
user.bridge.managementRooms[user.ManagementRoom] = user
user.Update()
}
func (user *User) SetSession(session *whatsapp.Session) {
user.Session = session
user.Update()
}
func (user *User) Start() {
if user.Connect(false) {
user.Sync()
}
}
func (user *User) Connect(evenIfNoSession bool) bool {
if user.Conn != nil {
return true
} else if !evenIfNoSession && user.Session == nil {
return false
}
user.log.Debugln("Connecting to WhatsApp")
var err error var err error
user.Conn, err = whatsapp.NewConn(20 * time.Second) user.Conn, err = whatsapp.NewConn(20 * time.Second)
if err != nil { if err != nil {
user.log.Errorln("Failed to connect to WhatsApp:", err) user.log.Errorln("Failed to connect to WhatsApp:", err)
return return false
} }
user.log.Debugln("WhatsApp connection successful")
user.Conn.AddHandler(user) user.Conn.AddHandler(user)
user.RestoreSession() return user.RestoreSession()
} }
func (user *User) RestoreSession() { func (user *User) RestoreSession() bool {
if user.Session != nil { if user.Session != nil {
sess, err := user.Conn.RestoreSession(*user.Session) sess, err := user.Conn.RestoreSession(*user.Session)
if err != nil { if err != nil {
user.log.Errorln("Failed to restore session:", err) user.log.Errorln("Failed to restore session:", err)
user.Session = nil //user.SetSession(nil)
return return false
} }
user.Session = &sess user.SetSession(&sess)
user.log.Debugln("Session restored") user.log.Debugln("Session restored successfully")
return true
} }
return return false
} }
func (user *User) Login(roomID string) { func (user *User) Login(roomID types.MatrixRoomID) {
bot := user.bridge.AppService.BotClient() bot := user.bridge.AppService.BotClient()
qrChan := make(chan string, 2) qrChan := make(chan string, 2)
@ -130,7 +164,7 @@ func (user *User) Login(roomID string) {
return return
} }
bot.SendImage(roomID, string(qrCode), resp.ContentURI) bot.SendImage(roomID, string(code), resp.ContentURI)
}() }()
session, err := user.Conn.Login(qrChan) session, err := user.Conn.Login(qrChan)
if err != nil { if err != nil {
@ -145,32 +179,79 @@ func (user *User) Login(roomID string) {
go user.Sync() go user.Sync()
} }
func (user *User) Sync() { func (user *User) SyncPuppet(contact whatsapp.Contact) {
chats, err := user.Conn.Chats() puppet := user.GetPuppetByJID(contact.Jid)
puppet.Intent().EnsureRegistered()
newName := user.bridge.Config.Bridge.FormatDisplayname(contact)
puppet.log.Debugln(puppet.Displayname, newName, contact.Name)
if puppet.Displayname != newName {
puppet.Displayname = newName
puppet.Update()
puppet.Intent().SetDisplayName(puppet.Displayname)
}
}
func (user *User) SyncPortal(contact whatsapp.Contact) {
portal := user.GetPortalByJID(contact.Jid)
if len(portal.MXID) == 0 {
if !portal.IsPrivateChat() {
portal.Name = contact.Name
}
err := portal.CreateMatrixRoom()
if err != nil { if err != nil {
user.log.Warnln("Failed to get chats") user.log.Errorln("Failed to create portal:", err)
return return
} }
user.log.Debugln(chats) }
if !portal.IsPrivateChat() && portal.Name != contact.Name {
portal.Name = contact.Name
portal.Update()
// TODO add SetRoomName function to intent API
portal.MainIntent().SendStateEvent(portal.MXID, "m.room.name", "", map[string]interface{}{
"name": portal.Name,
})
}
}
func (user *User) Sync() {
user.log.Debugln("Syncing...")
user.Conn.Contacts()
user.log.Debugln(user.Conn.Store.Contacts)
for jid, contact := range user.Conn.Store.Contacts {
dat, _ := json.Marshal(&contact)
user.log.Debugln(string(dat))
if strings.HasSuffix(jid, puppetJIDStrippedSuffix) {
user.SyncPuppet(contact)
}
if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") {
// Don't bridge yet
continue
}
user.SyncPortal(contact)
}
} }
func (user *User) HandleError(err error) { func (user *User) HandleError(err error) {
user.log.Errorln("WhatsApp error:", err) user.log.Errorln("WhatsApp error:", err)
fmt.Fprintf(os.Stderr, "%v", err)
} }
func (user *User) HandleTextMessage(message whatsapp.TextMessage) { func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
fmt.Println(message) user.log.Debugln("Text message:", message)
} }
func (user *User) HandleImageMessage(message whatsapp.ImageMessage) { func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
fmt.Println(message) user.log.Debugln("Image message:", message)
} }
func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
fmt.Println(message) user.log.Debugln("Video message:", message)
} }
func (user *User) HandleJsonMessage(message string) { func (user *User) HandleJsonMessage(message string) {
fmt.Println(message) user.log.Debugln("JSON message:", message)
} }