forked from MirrorHub/mautrix-whatsapp
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:
parent
edd4f817e4
commit
a9124b89bd
13 changed files with 455 additions and 98 deletions
104
commands.go
Normal file
104
commands.go
Normal 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:")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
29
main.go
|
@ -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()
|
||||
|
|
75
matrix.go
75
matrix.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
59
portal.go
59
portal.go
|
@ -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)
|
||||
}
|
||||
|
|
33
puppet.go
33
puppet.go
|
@ -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
155
user.go
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue