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"
"strings"
"strconv"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix-whatsapp/types"
)
type BridgeConfig struct {
@ -62,16 +64,16 @@ type UsernameTemplateArgs struct {
UserID string
}
func (bc BridgeConfig) FormatDisplayname(displayname string) string {
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string {
var buf bytes.Buffer
bc.displaynameTemplate.Execute(&buf, DisplaynameTemplateArgs{
Displayname: displayname,
})
bc.displaynameTemplate.Execute(&buf, contact)
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
receiver = strings.Replace(receiver, "@", "=40", 1)
receiver = strings.Replace(receiver, ":", "=3", 1)
bc.usernameTemplate.Execute(&buf, UsernameTemplateArgs{
Receiver: receiver,
UserID: userID,
@ -80,7 +82,12 @@ func (bc BridgeConfig) FormatUsername(receiver, userID string) string {
}
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}}")
return bc, nil
}

View file

@ -55,7 +55,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration)
registration.SenderLocalpart = config.AppService.Bot.Username
userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
config.Bridge.FormatUsername("[0-9]+", "[0-9]+"),
config.Bridge.FormatUsername(".+", "[0-9]+"),
config.Homeserver.Domain))
if err != nil {
return err

View file

@ -56,10 +56,20 @@ func New(file string) (*Database, error) {
return db, nil
}
func (db *Database) CreateTables() {
db.User.CreateTable()
db.Portal.CreateTable()
db.Puppet.CreateTable()
func (db *Database) CreateTables() error {
err := db.User.CreateTable()
if err != nil {
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 {

View file

@ -19,6 +19,7 @@ package database
import (
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types"
"database/sql"
)
type PortalQuery struct {
@ -33,7 +34,7 @@ func (pq *PortalQuery) CreateTable() error {
mxid VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (jid, owner),
FOREIGN KEY owner REFERENCES user(mxid)
FOREIGN KEY (owner) REFERENCES user(mxid)
)`)
return err
}
@ -80,22 +81,34 @@ type Portal struct {
JID types.WhatsAppID
MXID types.MatrixRoomID
Owner types.MatrixUserID
Name string
Avatar 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)
if err != sql.ErrNoRows {
portal.log.Fatalln("Database scan failed:", err)
}
return nil
}
return portal
}
func (portal *Portal) Insert() error {
_, 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
}
func (portal *Portal) Update() error {
_, 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
}

View file

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

View file

@ -20,6 +20,7 @@ import (
log "maunium.net/go/maulogger"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix-whatsapp/types"
"database/sql"
)
type UserQuery struct {
@ -45,8 +46,8 @@ func (uq *UserQuery) CreateTable() error {
func (uq *UserQuery) New() *User {
return &User{
db: uq.db,
log: uq.log,
db: uq.db,
log: uq.log,
}
}
@ -74,17 +75,20 @@ type User struct {
db *Database
log log.Logger
UserID types.MatrixUserID
ID types.MatrixUserID
ManagementRoom types.MatrixRoomID
Session *whatsapp.Session
}
func (user *User) Scan(row Scannable) *User {
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)
if err != nil {
user.log.Fatalln("Database scan failed:", err)
if err != sql.ErrNoRows {
user.log.Fatalln("Database scan failed:", err)
}
return nil
}
if len(sess.ClientId) > 0 {
user.Session = &sess
@ -99,7 +103,7 @@ func (user *User) Insert() error {
if user.Session != nil {
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)
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=?",
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
}

View file

@ -43,12 +43,14 @@ 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.
# {{.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: "{{.Displayname}}"
# {{.Name}} - display name
# {{.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.
command_prefix: "!wa"

29
main.go
View file

@ -26,7 +26,6 @@ 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"
)
@ -60,18 +59,21 @@ func (bridge *Bridge) GenerateRegistration() {
type Bridge struct {
AppService *appservice.AppService
EventProcessor *appservice.EventProcessor
MatrixHandler *MatrixHandler
Config *config.Config
DB *database.Database
Log log.Logger
StateStore *AutosavingStateStore
users map[types.MatrixUserID]*User
users map[types.MatrixUserID]*User
managementRooms map[types.MatrixRoomID]*User
}
func NewBridge() *Bridge {
bridge := &Bridge{
users: make(map[types.MatrixUserID]*User),
users: make(map[types.MatrixUserID]*User),
managementRooms: make(map[types.MatrixRoomID]*User),
}
var err error
bridge.Config, err = config.Load(*configPath)
@ -111,23 +113,28 @@ func (bridge *Bridge) Init() {
os.Exit(13)
}
bridge.Log.Debugln("Initializing event processor")
bridge.Log.Debugln("Initializing Matrix event processor")
bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService)
bridge.EventProcessor.On(gomatrix.EventMessage, bridge.HandleMessage)
bridge.EventProcessor.On(gomatrix.StateMember, bridge.HandleMembership)
bridge.Log.Debugln("Initializing Matrix event handler")
bridge.MatrixHandler = NewMatrixHandler(bridge)
}
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")
go bridge.AppService.Start()
bridge.Log.Debugln("Starting event processor")
go bridge.EventProcessor.Start()
bridge.Log.Debugln("Updating bot profile")
go bridge.UpdateBotProfile()
go bridge.StartUsers()
}
func (bridge *Bridge) UpdateBotProfile() {
bridge.Log.Debugln("Updating bot profile")
botConfig := bridge.Config.AppService.Bot
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() {
bridge.AppService.Stop()
bridge.EventProcessor.Stop()

View file

@ -18,26 +18,49 @@ package main
import (
"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) {
intent := bridge.AppService.BotIntent()
type MatrixHandler struct {
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)
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
}
members, err := intent.JoinedMembers(resp.RoomID)
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)
return
}
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)
return
}
@ -46,31 +69,49 @@ func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) {
for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == evt.Sender {
continue
} else if _, _, ok := bridge.ParsePuppetMXID(mxid); ok {
} else if _, _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok {
hasPuppets = true
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.LeaveRoom(resp.RoomID)
return
}
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)
user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
user.SetManagementRoom(types.MatrixRoomID(resp.RoomID))
intent.SendNotice(string(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)
}
}
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 (mx *MatrixHandler) HandleMembership(evt *gomatrix.Event) {
mx.log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey())
if evt.Content.Membership == "invite" && evt.GetStateKey() == mx.as.BotMXID() {
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"
"fmt"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/gomatrix"
"strings"
"maunium.net/go/mautrix-appservice"
)
func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
portal, ok := user.portalsByMXID[mxid]
if !ok {
dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
if dbPortal == nil || dbPortal.Owner != user.UserID {
if dbPortal == nil || dbPortal.Owner != user.ID {
return nil
}
portal = user.NewPortal(dbPortal)
@ -42,9 +45,12 @@ func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *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)
dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid)
if dbPortal == nil {
return nil
dbPortal = user.bridge.DB.Portal.New()
dbPortal.JID = jid
dbPortal.Owner = user.ID
dbPortal.Insert()
}
portal = user.NewPortal(dbPortal)
user.portalsByJID[portal.JID] = portal
@ -56,7 +62,7 @@ func (user *User) GetPortalByJID(jid types.WhatsAppID) *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))
for index, dbPortal := range dbPortals {
portal, ok := user.portalsByJID[dbPortal.JID]
@ -88,3 +94,48 @@ type Portal struct {
bridge *Bridge
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"
"maunium.net/go/mautrix-whatsapp/types"
"strings"
"maunium.net/go/mautrix-appservice"
)
const puppetJIDStrippedSuffix = "@s.whatsapp.net"
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]+)"),
@ -38,11 +41,12 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUser
return "", "", false
}
receiver := match[1]
receiver := types.MatrixUserID(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
jid := types.WhatsAppID(match[2] + puppetJIDStrippedSuffix)
return receiver, jid, true
}
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 {
receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid)
if !ok || receiver != user.UserID {
if !ok || receiver != user.ID {
return nil
}
@ -71,9 +75,12 @@ func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
puppet, ok := user.puppets[jid]
if !ok {
dbPuppet := user.bridge.DB.Puppet.Get(jid, user.UserID)
dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID)
if dbPuppet == nil {
return nil
dbPuppet = user.bridge.DB.Puppet.New()
dbPuppet.JID = jid
dbPuppet.Receiver = user.ID
dbPuppet.Insert()
}
puppet = user.NewPuppet(dbPuppet)
user.puppets[puppet.JID] = puppet
@ -82,7 +89,7 @@ func (user *User) GetPuppetByJID(jid types.WhatsAppID) *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))
for index, dbPuppet := range dbPuppets {
puppet, ok := user.puppets[dbPuppet.JID]
@ -101,6 +108,14 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
user: user,
bridge: user.bridge,
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
bridge *Bridge
log log.Logger
MXID types.MatrixUserID
}
func (puppet *Puppet) Intent() *appservice.IntentAPI {
return puppet.bridge.AppService.Intent(puppet.MXID)
}

155
user.go
View file

@ -20,11 +20,11 @@ import (
"maunium.net/go/mautrix-whatsapp/database"
"github.com/Rhymen/go-whatsapp"
"time"
"fmt"
"os"
"github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types"
"strings"
"encoding/json"
)
type User struct {
@ -45,10 +45,14 @@ func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
dbUser := bridge.DB.User.Get(userID)
if dbUser == nil {
dbUser = bridge.DB.User.New()
dbUser.ID = userID
dbUser.Insert()
}
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
}
@ -57,57 +61,87 @@ func (bridge *Bridge) GetAllUsers() []*User {
dbUsers := bridge.DB.User.GetAll()
output := make([]*User, len(dbUsers))
for index, dbUser := range dbUsers {
user, ok := bridge.users[dbUser.UserID]
user, ok := bridge.users[dbUser.ID]
if !ok {
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
}
return output
}
func (bridge *Bridge) InitWhatsApp() {
users := bridge.GetAllUsers()
for _, user := range users {
user.Connect()
}
}
func (bridge *Bridge) NewUser(dbUser *database.User) *User {
return &User{
User: dbUser,
bridge: bridge,
log: bridge.Log.Sub("User").Sub(dbUser.UserID),
User: dbUser,
bridge: bridge,
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
user.Conn, err = whatsapp.NewConn(20 * time.Second)
if err != nil {
user.log.Errorln("Failed to connect to WhatsApp:", err)
return
return false
}
user.log.Debugln("WhatsApp connection successful")
user.Conn.AddHandler(user)
user.RestoreSession()
return user.RestoreSession()
}
func (user *User) RestoreSession() {
func (user *User) RestoreSession() bool {
if user.Session != nil {
sess, err := user.Conn.RestoreSession(*user.Session)
if err != nil {
user.log.Errorln("Failed to restore session:", err)
user.Session = nil
return
//user.SetSession(nil)
return false
}
user.Session = &sess
user.log.Debugln("Session restored")
user.SetSession(&sess)
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()
qrChan := make(chan string, 2)
@ -130,7 +164,7 @@ func (user *User) Login(roomID string) {
return
}
bot.SendImage(roomID, string(qrCode), resp.ContentURI)
bot.SendImage(roomID, string(code), resp.ContentURI)
}()
session, err := user.Conn.Login(qrChan)
if err != nil {
@ -145,32 +179,79 @@ func (user *User) Login(roomID string) {
go user.Sync()
}
func (user *User) Sync() {
chats, err := user.Conn.Chats()
if err != nil {
user.log.Warnln("Failed to get chats")
return
func (user *User) SyncPuppet(contact whatsapp.Contact) {
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 {
user.log.Errorln("Failed to create portal:", err)
return
}
}
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)
}
user.log.Debugln(chats)
}
func (user *User) HandleError(err error) {
user.log.Errorln("WhatsApp error:", err)
fmt.Fprintf(os.Stderr, "%v", err)
}
func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
fmt.Println(message)
user.log.Debugln("Text message:", message)
}
func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
fmt.Println(message)
user.log.Debugln("Image message:", message)
}
func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
fmt.Println(message)
user.log.Debugln("Video message:", message)
}
func (user *User) HandleJsonMessage(message string) {
fmt.Println(message)
user.log.Debugln("JSON message:", message)
}