forked from MirrorHub/mautrix-whatsapp
[BREAKING] Merge pull request #5 from tulir/desegregate
Desegregate users
This commit is contained in:
commit
c888cb14a0
32 changed files with 909 additions and 613 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@
|
||||||
*.session
|
*.session
|
||||||
*.json
|
*.json
|
||||||
*.db
|
*.db
|
||||||
|
*.log
|
||||||
|
|
17
Gopkg.lock
generated
17
Gopkg.lock
generated
|
@ -13,8 +13,7 @@
|
||||||
"crypto/curve25519",
|
"crypto/curve25519",
|
||||||
"crypto/hkdf"
|
"crypto/hkdf"
|
||||||
]
|
]
|
||||||
revision = "82b902133ab6093f864dfc11fb9c5648f82f0ee9"
|
revision = "c31092027237441cffba1b9cb148eadf7c83c3d2"
|
||||||
source = "github.com/tulir/go-whatsapp"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/fatih/color"
|
name = "github.com/fatih/color"
|
||||||
|
@ -55,8 +54,8 @@
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/mattn/go-isatty"
|
name = "github.com/mattn/go-isatty"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
|
||||||
version = "v0.0.3"
|
version = "v0.0.4"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/mattn/go-sqlite3"
|
name = "github.com/mattn/go-sqlite3"
|
||||||
|
@ -87,7 +86,7 @@
|
||||||
"curve25519",
|
"curve25519",
|
||||||
"hkdf"
|
"hkdf"
|
||||||
]
|
]
|
||||||
revision = "614d502a4dac94afa3a6ce146bd1736da82514c6"
|
revision = "182538f80094b6a8efaade63a8fd8e0d9d5843dd"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
@ -102,7 +101,7 @@
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/sys"
|
name = "golang.org/x/sys"
|
||||||
packages = ["unix"]
|
packages = ["unix"]
|
||||||
revision = "d99a578cf41bfccdeaf48b0845c823a4b8b0ad5e"
|
revision = "fa5fdf94c78965f1aa8423f0cc50b8b8d728b05a"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "gopkg.in/russross/blackfriday.v2"
|
name = "gopkg.in/russross/blackfriday.v2"
|
||||||
|
@ -123,7 +122,7 @@
|
||||||
".",
|
".",
|
||||||
"format"
|
"format"
|
||||||
]
|
]
|
||||||
revision = "ead1f970c8f56d1854cb9eb4a54c03aa6dafd753"
|
revision = "b018830e10612c04065723de7aa49f35b37864a6"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
@ -141,11 +140,11 @@
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "maunium.net/go/mautrix-appservice"
|
name = "maunium.net/go/mautrix-appservice"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "269f2ab602126a2de94bc86a457392426cce1ab2"
|
revision = "4e24d1dd7bd9d89f946ec56cb4350ce777d17bfe"
|
||||||
|
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "8b494649cb598fd6a3862d4de5946f464288424b6e6e561e2b32c8d0ea1cb634"
|
inputs-digest = "9158f20cc827fadd5ed71302b767c938595986ef9e0623496eddfb4f95f75c76"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/Rhymen/go-whatsapp"
|
name = "github.com/Rhymen/go-whatsapp"
|
||||||
source = "github.com/tulir/go-whatsapp"
|
# branch = "develop"
|
||||||
|
# source = "github.com/tulir/go-whatsapp"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/mattn/go-sqlite3"
|
name = "github.com/mattn/go-sqlite3"
|
||||||
|
|
|
@ -48,7 +48,7 @@ type CommandEvent struct {
|
||||||
func (ce *CommandEvent) Reply(msg string) {
|
func (ce *CommandEvent) Reply(msg string) {
|
||||||
_, err := ce.Bot.SendNotice(string(ce.RoomID), msg)
|
_, err := ce.Bot.SendNotice(string(ce.RoomID), msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.ID, err)
|
ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes
|
||||||
args := strings.Split(message, " ")
|
args := strings.Split(message, " ")
|
||||||
cmd := strings.ToLower(args[0])
|
cmd := strings.ToLower(args[0])
|
||||||
ce := &CommandEvent{
|
ce := &CommandEvent{
|
||||||
Bot: handler.bridge.AppService.BotIntent(),
|
Bot: handler.bridge.Bot,
|
||||||
Bridge: handler.bridge,
|
Bridge: handler.bridge,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
|
|
|
@ -56,43 +56,44 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplaynameTemplateArgs struct {
|
|
||||||
Displayname string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UsernameTemplateArgs struct {
|
type UsernameTemplateArgs struct {
|
||||||
Receiver string
|
|
||||||
UserID string
|
UserID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string {
|
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if index := strings.IndexRune(contact.Jid, '@'); index > 0 {
|
if index := strings.IndexRune(contact.Jid, '@'); index > 0 {
|
||||||
contact.Jid = "+" + contact.Jid[:index]
|
contact.Jid = "+" + contact.Jid[:index]
|
||||||
}
|
}
|
||||||
bc.displaynameTemplate.Execute(&buf, contact)
|
bc.displaynameTemplate.Execute(&buf, contact)
|
||||||
return buf.String()
|
var quality int8
|
||||||
|
switch {
|
||||||
|
case len(contact.Notify) > 0:
|
||||||
|
quality = 3
|
||||||
|
case len(contact.Name) > 0 || len(contact.Short) > 0:
|
||||||
|
quality = 2
|
||||||
|
case len(contact.Jid) > 0:
|
||||||
|
quality = 1
|
||||||
|
default:
|
||||||
|
quality = 0
|
||||||
|
}
|
||||||
|
return buf.String(), quality
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc BridgeConfig) FormatUsername(receiver types.MatrixUserID, userID types.WhatsAppID) string {
|
func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
receiver = strings.Replace(receiver, "@", "=40", 1)
|
bc.usernameTemplate.Execute(&buf, userID)
|
||||||
receiver = strings.Replace(receiver, ":", "=3", 1)
|
|
||||||
bc.usernameTemplate.Execute(&buf, UsernameTemplateArgs{
|
|
||||||
Receiver: receiver,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
|
func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
|
||||||
bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{
|
bc.DisplaynameTemplate, _ = bc.FormatDisplayname(whatsapp.Contact{
|
||||||
Jid: "{{.Jid}}",
|
Jid: "{{.Jid}}",
|
||||||
Notify: "{{.Notify}}",
|
Notify: "{{.Notify}}",
|
||||||
Name: "{{.Name}}",
|
Name: "{{.Name}}",
|
||||||
Short: "{{.Short}}",
|
Short: "{{.Short}}",
|
||||||
})
|
})
|
||||||
bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}")
|
bc.UsernameTemplate = bc.FormatUsername("{{.}}")
|
||||||
return bc, nil
|
return bc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,6 @@ func (config *Config) Save(path string) error {
|
||||||
|
|
||||||
func (config *Config) MakeAppService() (*appservice.AppService, error) {
|
func (config *Config) MakeAppService() (*appservice.AppService, error) {
|
||||||
as := appservice.Create()
|
as := appservice.Create()
|
||||||
as.LogConfig = config.Logging
|
|
||||||
as.HomeserverDomain = config.Homeserver.Domain
|
as.HomeserverDomain = config.Homeserver.Domain
|
||||||
as.HomeserverURL = config.Homeserver.Address
|
as.HomeserverURL = config.Homeserver.Address
|
||||||
as.Host.Hostname = config.AppService.Hostname
|
as.Host.Hostname = config.AppService.Hostname
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (config *Config) NewRegistration() (*appservice.Registration, error) {
|
func (config *Config) NewRegistration() (*appservice.Registration, error) {
|
||||||
registration := appservice.CreateRegistration("mautrix-whatsapp")
|
registration := appservice.CreateRegistration()
|
||||||
|
|
||||||
err := config.copyToRegistration(registration)
|
err := config.copyToRegistration(registration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -37,7 +37,7 @@ func (config *Config) NewRegistration() (*appservice.Registration, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) GetRegistration() (*appservice.Registration, error) {
|
func (config *Config) GetRegistration() (*appservice.Registration, error) {
|
||||||
registration := appservice.CreateRegistration("mautrix-whatsapp")
|
registration := appservice.CreateRegistration()
|
||||||
|
|
||||||
err := config.copyToRegistration(registration)
|
err := config.copyToRegistration(registration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -56,7 +56,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]+"),
|
config.Bridge.FormatUsername("[0-9]+"),
|
||||||
config.Homeserver.Domain))
|
config.Homeserver.Domain))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -17,8 +17,11 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
|
||||||
log "maunium.net/go/maulogger"
|
log "maunium.net/go/maulogger"
|
||||||
"maunium.net/go/mautrix-whatsapp/types"
|
"maunium.net/go/mautrix-whatsapp/types"
|
||||||
)
|
)
|
||||||
|
@ -30,12 +33,15 @@ type MessageQuery struct {
|
||||||
|
|
||||||
func (mq *MessageQuery) CreateTable() error {
|
func (mq *MessageQuery) CreateTable() error {
|
||||||
_, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message (
|
_, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message (
|
||||||
owner VARCHAR(255),
|
chat_jid VARCHAR(25),
|
||||||
|
chat_receiver VARCHAR(25),
|
||||||
jid VARCHAR(255),
|
jid VARCHAR(255),
|
||||||
mxid VARCHAR(255) NOT NULL UNIQUE,
|
mxid VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
sender VARCHAR(25) NOT NULL,
|
||||||
|
content BLOB NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (owner, jid),
|
PRIMARY KEY (chat_jid, chat_receiver, jid),
|
||||||
FOREIGN KEY (owner) REFERENCES user(mxid)
|
FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver)
|
||||||
)`)
|
)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -47,8 +53,8 @@ func (mq *MessageQuery) New() *Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) GetAll(owner types.MatrixUserID) (messages []*Message) {
|
func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
|
||||||
rows, err := mq.db.Query("SELECT * FROM message WHERE owner=?", owner)
|
rows, err := mq.db.Query("SELECT * FROM message WHERE chat_jid=? AND chat_receiver=?", chat.JID, chat.Receiver)
|
||||||
if err != nil || rows == nil {
|
if err != nil || rows == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -59,8 +65,8 @@ func (mq *MessageQuery) GetAll(owner types.MatrixUserID) (messages []*Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppMessageID) *Message {
|
func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.WhatsAppMessageID) *Message {
|
||||||
return mq.get("SELECT * FROM message WHERE owner=? AND jid=?", owner, jid)
|
return mq.get("SELECT * FROM message WHERE chat_jid=? AND chat_receiver=? AND jid=?", chat.JID, chat.Receiver, jid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message {
|
func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message {
|
||||||
|
@ -79,34 +85,52 @@ type Message struct {
|
||||||
db *Database
|
db *Database
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
Owner types.MatrixUserID
|
Chat PortalKey
|
||||||
JID types.WhatsAppMessageID
|
JID types.WhatsAppMessageID
|
||||||
MXID types.MatrixEventID
|
MXID types.MatrixEventID
|
||||||
|
Sender types.WhatsAppID
|
||||||
|
Content *waProto.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *Message) Scan(row Scannable) *Message {
|
func (msg *Message) Scan(row Scannable) *Message {
|
||||||
err := row.Scan(&msg.Owner, &msg.JID, &msg.MXID)
|
var content []byte
|
||||||
|
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
msg.log.Errorln("Database scan failed:", err)
|
msg.log.Errorln("Database scan failed:", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msg.decodeBinaryContent(content)
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *Message) Insert() error {
|
func (msg *Message) decodeBinaryContent(content []byte) {
|
||||||
_, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?)", msg.Owner, msg.JID, msg.MXID)
|
msg.Content = &waProto.Message{}
|
||||||
|
reader := bytes.NewReader(content)
|
||||||
|
dec := json.NewDecoder(reader)
|
||||||
|
err := dec.Decode(msg.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err)
|
msg.log.Warnln("Failed to decode message content:", err)
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *Message) Update() error {
|
func (msg *Message) encodeBinaryContent() []byte {
|
||||||
_, err := msg.db.Exec("UPDATE portal SET mxid=? WHERE owner=? AND jid=?", msg.MXID, msg.Owner, msg.JID)
|
var buf bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
err := enc.Encode(msg.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err)
|
msg.log.Warnln("Failed to encode message content:", err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Message) Insert() error {
|
||||||
|
_, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.encodeBinaryContent())
|
||||||
|
if err != nil {
|
||||||
|
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,41 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger"
|
log "maunium.net/go/maulogger"
|
||||||
"maunium.net/go/mautrix-whatsapp/types"
|
"maunium.net/go/mautrix-whatsapp/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PortalKey struct {
|
||||||
|
JID types.WhatsAppID
|
||||||
|
Receiver types.WhatsAppID
|
||||||
|
}
|
||||||
|
|
||||||
|
func GroupPortalKey(jid types.WhatsAppID) PortalKey {
|
||||||
|
return PortalKey{
|
||||||
|
JID: jid,
|
||||||
|
Receiver: jid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPortalKey(jid, receiver types.WhatsAppID) PortalKey {
|
||||||
|
if strings.HasSuffix(jid, "@g.us") {
|
||||||
|
receiver = jid
|
||||||
|
}
|
||||||
|
return PortalKey{
|
||||||
|
JID: jid,
|
||||||
|
Receiver: receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key PortalKey) String() string {
|
||||||
|
if key.Receiver == key.JID {
|
||||||
|
return key.JID
|
||||||
|
}
|
||||||
|
return key.JID + "-" + key.Receiver
|
||||||
|
}
|
||||||
|
|
||||||
type PortalQuery struct {
|
type PortalQuery struct {
|
||||||
db *Database
|
db *Database
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
@ -30,16 +60,16 @@ type PortalQuery struct {
|
||||||
|
|
||||||
func (pq *PortalQuery) CreateTable() error {
|
func (pq *PortalQuery) CreateTable() error {
|
||||||
_, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS portal (
|
_, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS portal (
|
||||||
jid VARCHAR(255),
|
jid VARCHAR(25),
|
||||||
owner VARCHAR(255),
|
receiver VARCHAR(25),
|
||||||
mxid VARCHAR(255) UNIQUE,
|
mxid VARCHAR(255) UNIQUE,
|
||||||
|
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
topic VARCHAR(255) NOT NULL,
|
topic VARCHAR(255) NOT NULL,
|
||||||
avatar VARCHAR(255) NOT NULL,
|
avatar VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (jid, owner),
|
PRIMARY KEY (jid, receiver),
|
||||||
FOREIGN KEY (owner) REFERENCES user(mxid)
|
FOREIGN KEY (receiver) REFERENCES user(mxid)
|
||||||
)`)
|
)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -51,8 +81,8 @@ func (pq *PortalQuery) New() *Portal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pq *PortalQuery) GetAll(owner types.MatrixUserID) (portals []*Portal) {
|
func (pq *PortalQuery) GetAll() (portals []*Portal) {
|
||||||
rows, err := pq.db.Query("SELECT * FROM portal WHERE owner=?", owner)
|
rows, err := pq.db.Query("SELECT * FROM portal")
|
||||||
if err != nil || rows == nil {
|
if err != nil || rows == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -63,8 +93,8 @@ func (pq *PortalQuery) GetAll(owner types.MatrixUserID) (portals []*Portal) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pq *PortalQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppID) *Portal {
|
func (pq *PortalQuery) GetByJID(key PortalKey) *Portal {
|
||||||
return pq.get("SELECT * FROM portal WHERE jid=? AND owner=?", jid, owner)
|
return pq.get("SELECT * FROM portal WHERE jid=? AND receiver=?", key.JID, key.Receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pq *PortalQuery) GetByMXID(mxid types.MatrixRoomID) *Portal {
|
func (pq *PortalQuery) GetByMXID(mxid types.MatrixRoomID) *Portal {
|
||||||
|
@ -83,9 +113,8 @@ type Portal struct {
|
||||||
db *Database
|
db *Database
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
JID types.WhatsAppID
|
Key PortalKey
|
||||||
MXID types.MatrixRoomID
|
MXID types.MatrixRoomID
|
||||||
Owner types.MatrixUserID
|
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
Topic string
|
Topic string
|
||||||
|
@ -93,25 +122,30 @@ type Portal struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) Scan(row Scannable) *Portal {
|
func (portal *Portal) Scan(row Scannable) *Portal {
|
||||||
err := row.Scan(&portal.JID, &portal.Owner, &portal.MXID, &portal.Name, &portal.Topic, &portal.Avatar)
|
var mxid sql.NullString
|
||||||
|
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
portal.log.Errorln("Database scan failed:", err)
|
portal.log.Errorln("Database scan failed:", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
portal.MXID = mxid.String
|
||||||
return portal
|
return portal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) Insert() error {
|
func (portal *Portal) mxidPtr() *string {
|
||||||
var mxid *string
|
|
||||||
if len(portal.MXID) > 0 {
|
if len(portal.MXID) > 0 {
|
||||||
mxid = &portal.MXID
|
return &portal.MXID
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Insert() error {
|
||||||
_, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?, ?, ?, ?)",
|
_, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
portal.JID, portal.Owner, mxid, portal.Name, portal.Topic, portal.Avatar)
|
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Warnfln("Failed to insert %s->%s: %v", portal.JID, portal.Owner, err)
|
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -121,10 +155,10 @@ func (portal *Portal) Update() error {
|
||||||
if len(portal.MXID) > 0 {
|
if len(portal.MXID) > 0 {
|
||||||
mxid = &portal.MXID
|
mxid = &portal.MXID
|
||||||
}
|
}
|
||||||
_, err := portal.db.Exec("UPDATE portal SET mxid=?, name=?, topic=?, avatar=? WHERE jid=? AND owner=?",
|
_, err := portal.db.Exec("UPDATE portal SET mxid=?, name=?, topic=?, avatar=? WHERE jid=? AND receiver=?",
|
||||||
mxid, portal.Name, portal.Topic, portal.Avatar, portal.JID, portal.Owner)
|
mxid, portal.Name, portal.Topic, portal.Avatar, portal.Key.JID, portal.Key.Receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err)
|
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,10 @@ type PuppetQuery struct {
|
||||||
|
|
||||||
func (pq *PuppetQuery) CreateTable() error {
|
func (pq *PuppetQuery) CreateTable() error {
|
||||||
_, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet (
|
_, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet (
|
||||||
jid VARCHAR(255),
|
jid VARCHAR(25) PRIMARY KEY,
|
||||||
receiver VARCHAR(255),
|
|
||||||
|
|
||||||
displayname VARCHAR(255),
|
|
||||||
avatar VARCHAR(255),
|
avatar VARCHAR(255),
|
||||||
|
displayname VARCHAR(255),
|
||||||
PRIMARY KEY(jid, receiver)
|
name_quality TINYINT
|
||||||
)`)
|
)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -48,8 +45,8 @@ func (pq *PuppetQuery) New() *Puppet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) {
|
func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
|
||||||
rows, err := pq.db.Query("SELECT * FROM puppet WHERE receiver=%s")
|
rows, err := pq.db.Query("SELECT * FROM puppet")
|
||||||
if err != nil || rows == nil {
|
if err != nil || rows == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -60,8 +57,8 @@ func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pq *PuppetQuery) Get(jid types.WhatsAppID, receiver types.MatrixUserID) *Puppet {
|
func (pq *PuppetQuery) Get(jid types.WhatsAppID) *Puppet {
|
||||||
row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=? AND receiver=?", jid, receiver)
|
row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=?", jid)
|
||||||
if row == nil {
|
if row == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -73,38 +70,39 @@ type Puppet struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
JID types.WhatsAppID
|
JID types.WhatsAppID
|
||||||
Receiver types.MatrixUserID
|
|
||||||
|
|
||||||
Displayname string
|
|
||||||
Avatar string
|
Avatar string
|
||||||
|
Displayname string
|
||||||
|
NameQuality int8
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
var displayname, avatar sql.NullString
|
||||||
|
err := row.Scan(&puppet.JID, &displayname, &avatar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
puppet.log.Errorln("Database scan failed:", err)
|
puppet.log.Errorln("Database scan failed:", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
puppet.Displayname = displayname.String
|
||||||
|
puppet.Avatar = avatar.String
|
||||||
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.Avatar, puppet.Displayname, puppet.NameQuality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Errorfln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err)
|
puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (puppet *Puppet) Update() error {
|
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=?, name_quality=?, avatar=? WHERE jid=?",
|
||||||
puppet.Displayname, puppet.Avatar,
|
puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.JID)
|
||||||
puppet.JID, puppet.Receiver)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Errorfln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err)
|
puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,12 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Rhymen/go-whatsapp"
|
"github.com/Rhymen/go-whatsapp"
|
||||||
log "maunium.net/go/maulogger"
|
log "maunium.net/go/maulogger"
|
||||||
"maunium.net/go/mautrix-whatsapp/types"
|
"maunium.net/go/mautrix-whatsapp/types"
|
||||||
|
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserQuery struct {
|
type UserQuery struct {
|
||||||
|
@ -32,6 +34,7 @@ type UserQuery struct {
|
||||||
func (uq *UserQuery) CreateTable() error {
|
func (uq *UserQuery) CreateTable() error {
|
||||||
_, err := uq.db.Exec(`CREATE TABLE IF NOT EXISTS user (
|
_, err := uq.db.Exec(`CREATE TABLE IF NOT EXISTS user (
|
||||||
mxid VARCHAR(255) PRIMARY KEY,
|
mxid VARCHAR(255) PRIMARY KEY,
|
||||||
|
jid VARCHAR(25) UNIQUE,
|
||||||
|
|
||||||
management_room VARCHAR(255),
|
management_room VARCHAR(255),
|
||||||
|
|
||||||
|
@ -39,8 +42,7 @@ func (uq *UserQuery) CreateTable() error {
|
||||||
client_token VARCHAR(255),
|
client_token VARCHAR(255),
|
||||||
server_token VARCHAR(255),
|
server_token VARCHAR(255),
|
||||||
enc_key BLOB,
|
enc_key BLOB,
|
||||||
mac_key BLOB,
|
mac_key BLOB
|
||||||
wid VARCHAR(255)
|
|
||||||
)`)
|
)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -64,7 +66,7 @@ func (uq *UserQuery) GetAll() (users []*User) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uq *UserQuery) Get(userID types.MatrixUserID) *User {
|
func (uq *UserQuery) GetByMXID(userID types.MatrixUserID) *User {
|
||||||
row := uq.db.QueryRow("SELECT * FROM user WHERE mxid=?", userID)
|
row := uq.db.QueryRow("SELECT * FROM user WHERE mxid=?", userID)
|
||||||
if row == nil {
|
if row == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -72,50 +74,97 @@ func (uq *UserQuery) Get(userID types.MatrixUserID) *User {
|
||||||
return uq.New().Scan(row)
|
return uq.New().Scan(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) GetByJID(userID types.WhatsAppID) *User {
|
||||||
|
row := uq.db.QueryRow("SELECT * FROM user WHERE jid=?", stripSuffix(userID))
|
||||||
|
if row == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return uq.New().Scan(row)
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
db *Database
|
db *Database
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
ID types.MatrixUserID
|
MXID types.MatrixUserID
|
||||||
|
JID types.WhatsAppID
|
||||||
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{}
|
var managementRoom, clientID, clientToken, serverToken, jid sql.NullString
|
||||||
err := row.Scan(&user.ID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken,
|
var encKey, macKey []byte
|
||||||
&sess.EncKey, &sess.MacKey, &sess.Wid)
|
err := row.Scan(&user.MXID, &jid, &managementRoom, &clientID, &clientToken, &serverToken, &encKey, &macKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
user.log.Errorln("Database scan failed:", err)
|
user.log.Errorln("Database scan failed:", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if len(sess.ClientId) > 0 {
|
if len(jid.String) > 0 && len(clientID.String) > 0 {
|
||||||
user.Session = &sess
|
user.JID = jid.String + whatsappExt.NewUserSuffix
|
||||||
|
user.Session = &whatsapp.Session{
|
||||||
|
ClientId: clientID.String,
|
||||||
|
ClientToken: clientToken.String,
|
||||||
|
ServerToken: serverToken.String,
|
||||||
|
EncKey: encKey,
|
||||||
|
MacKey: macKey,
|
||||||
|
Wid: jid.String + whatsappExt.OldUserSuffix,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
user.Session = nil
|
user.Session = nil
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) Insert() error {
|
func stripSuffix(jid types.WhatsAppID) string {
|
||||||
var sess whatsapp.Session
|
if len(jid) == 0 {
|
||||||
|
return jid
|
||||||
|
}
|
||||||
|
|
||||||
|
index := strings.IndexRune(jid, '@')
|
||||||
|
if index < 0 {
|
||||||
|
return jid
|
||||||
|
}
|
||||||
|
|
||||||
|
return jid[:index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) jidPtr() *string {
|
||||||
|
if len(user.JID) > 0 {
|
||||||
|
str := stripSuffix(user.JID)
|
||||||
|
return &str
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) sessionUnptr() (sess whatsapp.Session) {
|
||||||
if user.Session != nil {
|
if user.Session != nil {
|
||||||
sess = *user.Session
|
sess = *user.Session
|
||||||
}
|
}
|
||||||
_, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.ID, user.ManagementRoom,
|
return
|
||||||
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid)
|
}
|
||||||
|
|
||||||
|
func (user *User) Insert() error {
|
||||||
|
sess := user.sessionUnptr()
|
||||||
|
_, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, user.jidPtr(),
|
||||||
|
user.ManagementRoom,
|
||||||
|
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) Update() error {
|
func (user *User) Update() error {
|
||||||
var sess whatsapp.Session
|
sess := user.sessionUnptr()
|
||||||
if user.Session != nil {
|
_, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=? WHERE mxid=?",
|
||||||
sess = *user.Session
|
user.jidPtr(), user.ManagementRoom,
|
||||||
|
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey,
|
||||||
|
user.MXID)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnfln("Failed to update %s: %v", user.MXID, err)
|
||||||
}
|
}
|
||||||
_, 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.ID)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ appservice:
|
||||||
type: sqlite3
|
type: sqlite3
|
||||||
# The database URI. Usually file name. https://github.com/mattn/go-sqlite3#connection-string
|
# The database URI. Usually file name. https://github.com/mattn/go-sqlite3#connection-string
|
||||||
uri: mautrix-whatsapp.db
|
uri: mautrix-whatsapp.db
|
||||||
|
|
||||||
# Path to the Matrix room state store.
|
# Path to the Matrix room state store.
|
||||||
state_store_path: ./mx-state.json
|
state_store_path: ./mx-state.json
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@ appservice:
|
||||||
# Appservice bot details.
|
# Appservice bot details.
|
||||||
bot:
|
bot:
|
||||||
# Username of the appservice bot.
|
# Username of the appservice bot.
|
||||||
username: whatsapp
|
username: whatsappbot
|
||||||
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
||||||
# to leave display name/avatar as-is.
|
# to leave display name/avatar as-is.
|
||||||
displayname: WhatsApp bridge bot
|
displayname: WhatsApp bridge bot
|
||||||
|
@ -43,15 +42,15 @@ 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.
|
# {{.}} is replaced with the phone number of the WhatsApp user.
|
||||||
# {{.UserID}} is replaced with the user ID of the WhatsApp user.
|
username_template: whatsapp_{{.}}
|
||||||
username_template: "whatsapp_{{.Receiver}}_{{.UserID}}"
|
|
||||||
# Displayname template for WhatsApp users.
|
# Displayname template for WhatsApp users.
|
||||||
# {{.Name}} - display name
|
# {{.Notify}} - nickname set by the WhatsApp user
|
||||||
# {{.Short}} - short display name (usually first name)
|
|
||||||
# {{.Notify}} - nickname (maybe set by the target WhatsApp user)
|
|
||||||
# {{.Jid}} - phone number (international format)
|
# {{.Jid}} - phone number (international format)
|
||||||
displayname_template: "{{if .Name}}{{.Name}}{{else if .Notify}}{{.Notify}}{{else if .Short}}{{.Short}}{{else}}{{.Jid}}{{end}}"
|
# The following variables are also available, but will cause problems on multi-user instances:
|
||||||
|
# {{.Name}} - display name from contact list
|
||||||
|
# {{.Short}} - short display name from contact list
|
||||||
|
displayname_template: "{{if .Notify}}{{.Notify}}{{else}}{{.Jid}}{{end}} (WA)"
|
||||||
|
|
||||||
# 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"
|
||||||
|
@ -65,15 +64,15 @@ bridge:
|
||||||
# domain - All users on that homeserver
|
# domain - All users on that homeserver
|
||||||
# mxid - Specific user
|
# mxid - Specific user
|
||||||
permissions:
|
permissions:
|
||||||
"example.com": full
|
"example.com": user
|
||||||
"@admin:example.com": admin
|
"@admin:example.com": admin
|
||||||
|
|
||||||
# Logging config.
|
# Logging config.
|
||||||
logging:
|
logging:
|
||||||
# The directory for log files. Will be created if not found.
|
# The directory for log files. Will be created if not found.
|
||||||
directory: ./logs
|
directory: ./logs
|
||||||
# Available variables: .date for the file date and .index for different log files on the same day.
|
# Available variables: .Date for the file date and .Index for different log files on the same day.
|
||||||
file_name_format: "{{.date}}-{{.index}.log"
|
file_name_format: "{{.Date}}-{{.Index}}.log"
|
||||||
# Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants
|
# Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants
|
||||||
file_date_format: 2006-01-02
|
file_date_format: 2006-01-02
|
||||||
# Log file permissions.
|
# Log file permissions.
|
||||||
|
|
|
@ -18,21 +18,42 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"maunium.net/go/gomatrix"
|
||||||
"maunium.net/go/gomatrix/format"
|
"maunium.net/go/gomatrix/format"
|
||||||
|
"maunium.net/go/mautrix-whatsapp/types"
|
||||||
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (user *User) newHTMLParser() *format.HTMLParser {
|
var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)")
|
||||||
return &format.HTMLParser{
|
var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)")
|
||||||
|
var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)")
|
||||||
|
var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```")
|
||||||
|
var mentionRegex = regexp.MustCompile("@[0-9]+")
|
||||||
|
|
||||||
|
type Formatter struct {
|
||||||
|
bridge *Bridge
|
||||||
|
|
||||||
|
matrixHTMLParser *format.HTMLParser
|
||||||
|
|
||||||
|
waReplString map[*regexp.Regexp]string
|
||||||
|
waReplFunc map[*regexp.Regexp]func(string) string
|
||||||
|
waReplFuncText map[*regexp.Regexp]func(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormatter(bridge *Bridge) *Formatter {
|
||||||
|
formatter := &Formatter{
|
||||||
|
bridge: bridge,
|
||||||
|
matrixHTMLParser: &format.HTMLParser{
|
||||||
TabsToSpaces: 4,
|
TabsToSpaces: 4,
|
||||||
Newline: "\n",
|
Newline: "\n",
|
||||||
|
|
||||||
PillConverter: func(mxid, eventID string) string {
|
PillConverter: func(mxid, eventID string) string {
|
||||||
if mxid[0] == '@' {
|
if mxid[0] == '@' {
|
||||||
puppet := user.GetPuppetByMXID(mxid)
|
puppet := bridge.GetPuppetByMXID(mxid)
|
||||||
fmt.Println(mxid, puppet)
|
fmt.Println(mxid, puppet)
|
||||||
if puppet != nil {
|
if puppet != nil {
|
||||||
return "@" + puppet.PhoneNumber()
|
return "@" + puppet.PhoneNumber()
|
||||||
|
@ -55,21 +76,14 @@ func (user *User) newHTMLParser() *format.HTMLParser {
|
||||||
MonospaceBlockConverter: func(text string) string {
|
MonospaceBlockConverter: func(text string) string {
|
||||||
return fmt.Sprintf("```%s```", text)
|
return fmt.Sprintf("```%s```", text)
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
waReplString: map[*regexp.Regexp]string{
|
||||||
|
|
||||||
var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)")
|
|
||||||
var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)")
|
|
||||||
var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)")
|
|
||||||
var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```")
|
|
||||||
var mentionRegex = regexp.MustCompile("@[0-9]+")
|
|
||||||
|
|
||||||
func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regexp.Regexp]func(string) string, map[*regexp.Regexp]func(string) string) {
|
|
||||||
return map[*regexp.Regexp]string{
|
|
||||||
italicRegex: "$1<em>$2</em>$3",
|
italicRegex: "$1<em>$2</em>$3",
|
||||||
boldRegex: "$1<strong>$2</strong>$3",
|
boldRegex: "$1<strong>$2</strong>$3",
|
||||||
strikethroughRegex: "$1<del>$2</del>$3",
|
strikethroughRegex: "$1<del>$2</del>$3",
|
||||||
}, map[*regexp.Regexp]func(string) string{
|
},
|
||||||
|
}
|
||||||
|
formatter.waReplFunc = map[*regexp.Regexp]func(string) string{
|
||||||
codeBlockRegex: func(str string) string {
|
codeBlockRegex: func(str string) string {
|
||||||
str = str[3 : len(str)-3]
|
str = str[3 : len(str)-3]
|
||||||
if strings.ContainsRune(str, '\n') {
|
if strings.ContainsRune(str, '\n') {
|
||||||
|
@ -78,18 +92,47 @@ func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regex
|
||||||
return fmt.Sprintf("<code>%s</code>", str)
|
return fmt.Sprintf("<code>%s</code>", str)
|
||||||
},
|
},
|
||||||
mentionRegex: func(str string) string {
|
mentionRegex: func(str string) string {
|
||||||
jid := str[1:] + whatsappExt.NewUserSuffix
|
mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix)
|
||||||
puppet := user.GetPuppetByJID(jid)
|
return fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname)
|
||||||
mxid := puppet.MXID
|
},
|
||||||
if jid == user.JID() {
|
|
||||||
mxid = user.ID
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, puppet.Displayname)
|
formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{
|
||||||
},
|
|
||||||
}, map[*regexp.Regexp]func(string)string {
|
|
||||||
mentionRegex: func(str string) string {
|
mentionRegex: func(str string) string {
|
||||||
puppet := user.GetPuppetByJID(str[1:] + whatsappExt.NewUserSuffix)
|
_, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix)
|
||||||
return puppet.Displayname
|
return displayname
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid, displayname string) {
|
||||||
|
if user := formatter.bridge.GetUserByJID(jid); user != nil {
|
||||||
|
mxid = user.MXID
|
||||||
|
displayname = user.MXID
|
||||||
|
} else if puppet := formatter.bridge.GetPuppetByJID(jid); puppet != nil {
|
||||||
|
mxid = puppet.MXID
|
||||||
|
displayname = puppet.Displayname
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter *Formatter) ParseWhatsApp(content *gomatrix.Content) {
|
||||||
|
output := html.EscapeString(content.Body)
|
||||||
|
for regex, replacement := range formatter.waReplString {
|
||||||
|
output = regex.ReplaceAllString(output, replacement)
|
||||||
|
}
|
||||||
|
for regex, replacer := range formatter.waReplFunc {
|
||||||
|
output = regex.ReplaceAllStringFunc(output, replacer)
|
||||||
|
}
|
||||||
|
if output != content.Body {
|
||||||
|
content.FormattedBody = output
|
||||||
|
content.Format = gomatrix.FormatHTML
|
||||||
|
for regex, replacer := range formatter.waReplFuncText {
|
||||||
|
content.Body = regex.ReplaceAllStringFunc(content.Body, replacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter *Formatter) ParseMatrix(html string) string {
|
||||||
|
return formatter.matrixHTMLParser.Parse(html)
|
||||||
}
|
}
|
||||||
|
|
63
main.go
63
main.go
|
@ -20,6 +20,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
flag "maunium.net/go/mauflag"
|
flag "maunium.net/go/mauflag"
|
||||||
|
@ -31,6 +32,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
|
var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
|
||||||
|
var baseConfigPath = flag.MakeFull("b", "base-config", "The path to the example config file.", "example-config.yaml").String()
|
||||||
var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String()
|
var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String()
|
||||||
var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool()
|
var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool()
|
||||||
var wantHelp, _ = flag.MakeHelpFlag()
|
var wantHelp, _ = flag.MakeHelpFlag()
|
||||||
|
@ -58,24 +60,38 @@ func (bridge *Bridge) GenerateRegistration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Bridge struct {
|
type Bridge struct {
|
||||||
AppService *appservice.AppService
|
AS *appservice.AppService
|
||||||
EventProcessor *appservice.EventProcessor
|
EventProcessor *appservice.EventProcessor
|
||||||
MatrixHandler *MatrixHandler
|
MatrixHandler *MatrixHandler
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
DB *database.Database
|
DB *database.Database
|
||||||
Log log.Logger
|
Log log.Logger
|
||||||
|
|
||||||
StateStore *AutosavingStateStore
|
StateStore *AutosavingStateStore
|
||||||
|
Bot *appservice.IntentAPI
|
||||||
|
Formatter *Formatter
|
||||||
|
|
||||||
users map[types.MatrixUserID]*User
|
usersByMXID map[types.MatrixUserID]*User
|
||||||
|
usersByJID map[types.WhatsAppID]*User
|
||||||
|
usersLock sync.Mutex
|
||||||
managementRooms map[types.MatrixRoomID]*User
|
managementRooms map[types.MatrixRoomID]*User
|
||||||
|
managementRoomsLock sync.Mutex
|
||||||
|
portalsByMXID map[types.MatrixRoomID]*Portal
|
||||||
|
portalsByJID map[database.PortalKey]*Portal
|
||||||
|
portalsLock sync.Mutex
|
||||||
|
puppets map[types.WhatsAppID]*Puppet
|
||||||
|
puppetsLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBridge() *Bridge {
|
func NewBridge() *Bridge {
|
||||||
bridge := &Bridge{
|
bridge := &Bridge{
|
||||||
users: make(map[types.MatrixUserID]*User),
|
usersByMXID: make(map[types.MatrixUserID]*User),
|
||||||
|
usersByJID: make(map[types.WhatsAppID]*User),
|
||||||
managementRooms: make(map[types.MatrixRoomID]*User),
|
managementRooms: make(map[types.MatrixRoomID]*User),
|
||||||
|
portalsByMXID: make(map[types.MatrixRoomID]*Portal),
|
||||||
|
portalsByJID: make(map[database.PortalKey]*Portal),
|
||||||
|
puppets: make(map[types.WhatsAppID]*Puppet),
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
bridge.Config, err = config.Load(*configPath)
|
bridge.Config, err = config.Load(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -88,46 +104,55 @@ func NewBridge() *Bridge {
|
||||||
func (bridge *Bridge) Init() {
|
func (bridge *Bridge) Init() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
bridge.AppService, err = bridge.Config.MakeAppService()
|
bridge.AS, err = bridge.Config.MakeAppService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err)
|
fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err)
|
||||||
os.Exit(11)
|
os.Exit(11)
|
||||||
}
|
}
|
||||||
bridge.AppService.Init()
|
bridge.AS.Init()
|
||||||
bridge.Log = bridge.AppService.Log
|
bridge.Bot = bridge.AS.BotIntent()
|
||||||
|
|
||||||
|
bridge.Log = log.Create()
|
||||||
|
bridge.Config.Logging.Configure(bridge.Log)
|
||||||
log.DefaultLogger = bridge.Log.(*log.BasicLogger)
|
log.DefaultLogger = bridge.Log.(*log.BasicLogger)
|
||||||
bridge.AppService.Log = log.Sub("Matrix")
|
err = log.OpenFile()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Failed to open log file:", err)
|
||||||
|
os.Exit(12)
|
||||||
|
}
|
||||||
|
bridge.AS.Log = log.Sub("Matrix")
|
||||||
|
|
||||||
bridge.Log.Debugln("Initializing state store")
|
bridge.Log.Debugln("Initializing state store")
|
||||||
bridge.StateStore = NewAutosavingStateStore(bridge.Config.AppService.StateStore)
|
bridge.StateStore = NewAutosavingStateStore(bridge.Config.AppService.StateStore)
|
||||||
err = bridge.StateStore.Load()
|
err = bridge.StateStore.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Log.Fatalln("Failed to load state store:", err)
|
bridge.Log.Fatalln("Failed to load state store:", err)
|
||||||
os.Exit(12)
|
os.Exit(13)
|
||||||
}
|
}
|
||||||
bridge.AppService.StateStore = bridge.StateStore
|
bridge.AS.StateStore = bridge.StateStore
|
||||||
|
|
||||||
bridge.Log.Debugln("Initializing database")
|
bridge.Log.Debugln("Initializing database")
|
||||||
bridge.DB, err = database.New(bridge.Config.AppService.Database.URI)
|
bridge.DB, err = database.New(bridge.Config.AppService.Database.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Log.Fatalln("Failed to initialize database:", err)
|
bridge.Log.Fatalln("Failed to initialize database:", err)
|
||||||
os.Exit(13)
|
os.Exit(14)
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.Log.Debugln("Initializing Matrix event processor")
|
bridge.Log.Debugln("Initializing Matrix event processor")
|
||||||
bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService)
|
bridge.EventProcessor = appservice.NewEventProcessor(bridge.AS)
|
||||||
bridge.Log.Debugln("Initializing Matrix event handler")
|
bridge.Log.Debugln("Initializing Matrix event handler")
|
||||||
bridge.MatrixHandler = NewMatrixHandler(bridge)
|
bridge.MatrixHandler = NewMatrixHandler(bridge)
|
||||||
|
bridge.Formatter = NewFormatter(bridge)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) Start() {
|
func (bridge *Bridge) Start() {
|
||||||
err := bridge.DB.CreateTables()
|
err := bridge.DB.CreateTables()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Log.Fatalln("Failed to create database tables:", err)
|
bridge.Log.Fatalln("Failed to create database tables:", err)
|
||||||
os.Exit(14)
|
os.Exit(15)
|
||||||
}
|
}
|
||||||
bridge.Log.Debugln("Starting application service HTTP server")
|
bridge.Log.Debugln("Starting application service HTTP server")
|
||||||
go bridge.AppService.Start()
|
go bridge.AS.Start()
|
||||||
bridge.Log.Debugln("Starting event processor")
|
bridge.Log.Debugln("Starting event processor")
|
||||||
go bridge.EventProcessor.Start()
|
go bridge.EventProcessor.Start()
|
||||||
go bridge.UpdateBotProfile()
|
go bridge.UpdateBotProfile()
|
||||||
|
@ -140,18 +165,18 @@ func (bridge *Bridge) UpdateBotProfile() {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if botConfig.Avatar == "remove" {
|
if botConfig.Avatar == "remove" {
|
||||||
err = bridge.AppService.BotIntent().SetAvatarURL("")
|
err = bridge.Bot.SetAvatarURL("")
|
||||||
} else if len(botConfig.Avatar) > 0 {
|
} else if len(botConfig.Avatar) > 0 {
|
||||||
err = bridge.AppService.BotIntent().SetAvatarURL(botConfig.Avatar)
|
err = bridge.Bot.SetAvatarURL(botConfig.Avatar)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Log.Warnln("Failed to update bot avatar:", err)
|
bridge.Log.Warnln("Failed to update bot avatar:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if botConfig.Displayname == "remove" {
|
if botConfig.Displayname == "remove" {
|
||||||
err = bridge.AppService.BotIntent().SetDisplayName("")
|
err = bridge.Bot.SetDisplayName("")
|
||||||
} else if len(botConfig.Avatar) > 0 {
|
} else if len(botConfig.Avatar) > 0 {
|
||||||
err = bridge.AppService.BotIntent().SetDisplayName(botConfig.Displayname)
|
err = bridge.Bot.SetDisplayName(botConfig.Displayname)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Log.Warnln("Failed to update bot displayname:", err)
|
bridge.Log.Warnln("Failed to update bot displayname:", err)
|
||||||
|
@ -165,7 +190,7 @@ func (bridge *Bridge) StartUsers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) Stop() {
|
func (bridge *Bridge) Stop() {
|
||||||
bridge.AppService.Stop()
|
bridge.AS.Stop()
|
||||||
bridge.EventProcessor.Stop()
|
bridge.EventProcessor.Stop()
|
||||||
err := bridge.StateStore.Save()
|
err := bridge.StateStore.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
30
matrix.go
30
matrix.go
|
@ -35,7 +35,7 @@ type MatrixHandler struct {
|
||||||
func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
|
func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
|
||||||
handler := &MatrixHandler{
|
handler := &MatrixHandler{
|
||||||
bridge: bridge,
|
bridge: bridge,
|
||||||
as: bridge.AppService,
|
as: bridge.AS,
|
||||||
log: bridge.Log.Sub("Matrix"),
|
log: bridge.Log.Sub("Matrix"),
|
||||||
cmd: NewCommandHandler(bridge),
|
cmd: NewCommandHandler(bridge),
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
|
||||||
func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) {
|
func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) {
|
||||||
intent := mx.as.BotIntent()
|
intent := mx.as.BotIntent()
|
||||||
|
|
||||||
user := mx.bridge.GetUser(evt.Sender)
|
user := mx.bridge.GetUserByMXID(evt.Sender)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ func (mx *MatrixHandler) 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 := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok {
|
} else if _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok {
|
||||||
hasPuppets = true
|
hasPuppets = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasPuppets {
|
if !hasPuppets {
|
||||||
user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
|
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
|
||||||
user.SetManagementRoom(types.MatrixRoomID(resp.RoomID))
|
user.SetManagementRoom(types.MatrixRoomID(resp.RoomID))
|
||||||
intent.SendNotice(string(user.ManagementRoom), "This room has been registered as your bridge management/status room.")
|
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)
|
mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
|
||||||
|
@ -110,12 +110,12 @@ func (mx *MatrixHandler) HandleMembership(evt *gomatrix.Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) {
|
func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) {
|
||||||
user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
|
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
|
||||||
if user == nil || !user.Whitelisted {
|
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
portal := user.GetPortalByMXID(evt.RoomID)
|
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
|
||||||
if portal == nil || portal.IsPrivateChat() {
|
if portal == nil || portal.IsPrivateChat() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) {
|
||||||
var err error
|
var err error
|
||||||
switch evt.Type {
|
switch evt.Type {
|
||||||
case gomatrix.StateRoomName:
|
case gomatrix.StateRoomName:
|
||||||
resp, err = user.Conn.UpdateGroupSubject(evt.Content.Name, portal.JID)
|
resp, err = user.Conn.UpdateGroupSubject(evt.Content.Name, portal.Key.JID)
|
||||||
case gomatrix.StateRoomAvatar:
|
case gomatrix.StateRoomAvatar:
|
||||||
return
|
return
|
||||||
case gomatrix.StateTopic:
|
case gomatrix.StateTopic:
|
||||||
|
@ -139,8 +139,12 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) {
|
func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) {
|
||||||
|
if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
roomID := types.MatrixRoomID(evt.RoomID)
|
roomID := types.MatrixRoomID(evt.RoomID)
|
||||||
user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender))
|
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
|
||||||
|
|
||||||
if !user.Whitelisted {
|
if !user.Whitelisted {
|
||||||
return
|
return
|
||||||
|
@ -158,8 +162,12 @@ func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
portal := user.GetPortalByMXID(roomID)
|
if !user.IsLoggedIn() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
portal := mx.bridge.GetPortalByMXID(roomID)
|
||||||
if portal != nil {
|
if portal != nil {
|
||||||
portal.HandleMatrixMessage(evt)
|
portal.HandleMatrixMessage(user, evt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
398
portal.go
398
portal.go
|
@ -18,9 +18,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"image"
|
"image"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
@ -41,57 +41,56 @@ import (
|
||||||
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
|
func (bridge *Bridge) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
|
||||||
user.portalsLock.Lock()
|
bridge.portalsLock.Lock()
|
||||||
defer user.portalsLock.Unlock()
|
defer bridge.portalsLock.Unlock()
|
||||||
portal, ok := user.portalsByMXID[mxid]
|
portal, ok := bridge.portalsByMXID[mxid]
|
||||||
if !ok {
|
if !ok {
|
||||||
dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
|
dbPortal := bridge.DB.Portal.GetByMXID(mxid)
|
||||||
if dbPortal == nil || dbPortal.Owner != user.ID {
|
if dbPortal == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
portal = user.NewPortal(dbPortal)
|
portal = bridge.NewPortal(dbPortal)
|
||||||
user.portalsByJID[portal.JID] = portal
|
bridge.portalsByJID[portal.Key] = portal
|
||||||
if len(portal.MXID) > 0 {
|
if len(portal.MXID) > 0 {
|
||||||
user.portalsByMXID[portal.MXID] = portal
|
bridge.portalsByMXID[portal.MXID] = portal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return portal
|
return portal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
|
func (bridge *Bridge) GetPortalByJID(key database.PortalKey) *Portal {
|
||||||
user.portalsLock.Lock()
|
bridge.portalsLock.Lock()
|
||||||
defer user.portalsLock.Unlock()
|
defer bridge.portalsLock.Unlock()
|
||||||
portal, ok := user.portalsByJID[jid]
|
portal, ok := bridge.portalsByJID[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid)
|
dbPortal := bridge.DB.Portal.GetByJID(key)
|
||||||
if dbPortal == nil {
|
if dbPortal == nil {
|
||||||
dbPortal = user.bridge.DB.Portal.New()
|
dbPortal = bridge.DB.Portal.New()
|
||||||
dbPortal.JID = jid
|
dbPortal.Key = key
|
||||||
dbPortal.Owner = user.ID
|
|
||||||
dbPortal.Insert()
|
dbPortal.Insert()
|
||||||
}
|
}
|
||||||
portal = user.NewPortal(dbPortal)
|
portal = bridge.NewPortal(dbPortal)
|
||||||
user.portalsByJID[portal.JID] = portal
|
bridge.portalsByJID[portal.Key] = portal
|
||||||
if len(portal.MXID) > 0 {
|
if len(portal.MXID) > 0 {
|
||||||
user.portalsByMXID[portal.MXID] = portal
|
bridge.portalsByMXID[portal.MXID] = portal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return portal
|
return portal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) GetAllPortals() []*Portal {
|
func (bridge *Bridge) GetAllPortals() []*Portal {
|
||||||
user.portalsLock.Lock()
|
bridge.portalsLock.Lock()
|
||||||
defer user.portalsLock.Unlock()
|
defer bridge.portalsLock.Unlock()
|
||||||
dbPortals := user.bridge.DB.Portal.GetAll(user.ID)
|
dbPortals := bridge.DB.Portal.GetAll()
|
||||||
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 := bridge.portalsByJID[dbPortal.Key]
|
||||||
if !ok {
|
if !ok {
|
||||||
portal = user.NewPortal(dbPortal)
|
portal = bridge.NewPortal(dbPortal)
|
||||||
user.portalsByJID[dbPortal.JID] = portal
|
bridge.portalsByJID[portal.Key] = portal
|
||||||
if len(dbPortal.MXID) > 0 {
|
if len(dbPortal.MXID) > 0 {
|
||||||
user.portalsByMXID[dbPortal.MXID] = portal
|
bridge.portalsByMXID[dbPortal.MXID] = portal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output[index] = portal
|
output[index] = portal
|
||||||
|
@ -99,23 +98,115 @@ func (user *User) GetAllPortals() []*Portal {
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) NewPortal(dbPortal *database.Portal) *Portal {
|
func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
|
||||||
return &Portal{
|
return &Portal{
|
||||||
Portal: dbPortal,
|
Portal: dbPortal,
|
||||||
user: user,
|
bridge: bridge,
|
||||||
bridge: user.bridge,
|
log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)),
|
||||||
log: user.log.Sub(fmt.Sprintf("Portal/%s", dbPortal.JID)),
|
|
||||||
|
messageLocks: make(map[types.WhatsAppMessageID]sync.Mutex),
|
||||||
|
recentlyHandled: [20]types.WhatsAppMessageID{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Portal struct {
|
type Portal struct {
|
||||||
*database.Portal
|
*database.Portal
|
||||||
|
|
||||||
user *User
|
|
||||||
bridge *Bridge
|
bridge *Bridge
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
roomCreateLock sync.Mutex
|
roomCreateLock sync.Mutex
|
||||||
|
messageLocksLock sync.Mutex
|
||||||
|
messageLocks map[types.WhatsAppMessageID]sync.Mutex
|
||||||
|
|
||||||
|
recentlyHandled [20]types.WhatsAppMessageID
|
||||||
|
recentlyHandledLock sync.Mutex
|
||||||
|
recentlyHandledIndex uint8
|
||||||
|
|
||||||
|
isPrivate *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) getMessageLock(messageID types.WhatsAppMessageID) sync.Mutex {
|
||||||
|
portal.messageLocksLock.Lock()
|
||||||
|
defer portal.messageLocksLock.Unlock()
|
||||||
|
lock, ok := portal.messageLocks[messageID]
|
||||||
|
if !ok {
|
||||||
|
portal.messageLocks[messageID] = lock
|
||||||
|
}
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) deleteMessageLock(messageID types.WhatsAppMessageID) {
|
||||||
|
portal.messageLocksLock.Lock()
|
||||||
|
delete(portal.messageLocks, messageID)
|
||||||
|
portal.messageLocksLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) isRecentlyHandled(id types.WhatsAppMessageID) bool {
|
||||||
|
start := portal.recentlyHandledIndex
|
||||||
|
for i := start; i != start; i = (i - 1) % 20 {
|
||||||
|
if portal.recentlyHandled[i] == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) isDuplicate(id types.WhatsAppMessageID) bool {
|
||||||
|
msg := portal.bridge.DB.Message.GetByJID(portal.Key, id)
|
||||||
|
if msg != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(&waProto.Message{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) {
|
||||||
|
msg := portal.bridge.DB.Message.New()
|
||||||
|
msg.Chat = portal.Key
|
||||||
|
msg.JID = message.GetKey().GetId()
|
||||||
|
msg.MXID = mxid
|
||||||
|
if message.GetKey().GetFromMe() {
|
||||||
|
msg.Sender = source.JID
|
||||||
|
} else if portal.IsPrivateChat() {
|
||||||
|
msg.Sender = portal.Key.JID
|
||||||
|
} else {
|
||||||
|
msg.Sender = message.GetKey().GetParticipant()
|
||||||
|
if len(msg.Sender) == 0 {
|
||||||
|
msg.Sender = message.GetParticipant()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.Content = message.Message
|
||||||
|
msg.Insert()
|
||||||
|
|
||||||
|
portal.recentlyHandledLock.Lock()
|
||||||
|
index := portal.recentlyHandledIndex
|
||||||
|
portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % 20
|
||||||
|
portal.recentlyHandledLock.Unlock()
|
||||||
|
portal.recentlyHandled[index] = msg.JID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bool) {
|
||||||
|
if portal.isRecentlyHandled(id) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
lock := portal.getMessageLock(id)
|
||||||
|
lock.Lock()
|
||||||
|
if portal.isDuplicate(id) {
|
||||||
|
lock.Unlock()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &lock, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) {
|
||||||
|
portal.markHandled(source, message, mxid)
|
||||||
|
id := message.GetKey().GetId()
|
||||||
|
portal.deleteMessageLock(id)
|
||||||
|
portal.log.Debugln("Handled message", id, "->", mxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
|
func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
|
||||||
|
@ -126,9 +217,16 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
for _, participant := range metadata.Participants {
|
for _, participant := range metadata.Participants {
|
||||||
puppet := portal.user.GetPuppetByJID(participant.JID)
|
puppet := portal.bridge.GetPuppetByJID(participant.JID)
|
||||||
puppet.Intent().EnsureJoined(portal.MXID)
|
puppet.Intent().EnsureJoined(portal.MXID)
|
||||||
|
|
||||||
|
user := portal.bridge.GetUserByJID(participant.JID)
|
||||||
|
if user != nil && !portal.bridge.AS.StateStore.IsInvited(portal.MXID, user.MXID) {
|
||||||
|
portal.MainIntent().InviteUser(portal.MXID, &gomatrix.ReqInviteUser{
|
||||||
|
UserID: user.MXID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
expectedLevel := 0
|
expectedLevel := 0
|
||||||
if participant.IsSuperAdmin {
|
if participant.IsSuperAdmin {
|
||||||
expectedLevel = 95
|
expectedLevel = 95
|
||||||
|
@ -136,20 +234,22 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
|
||||||
expectedLevel = 50
|
expectedLevel = 50
|
||||||
}
|
}
|
||||||
changed = levels.EnsureUserLevel(puppet.MXID, expectedLevel) || changed
|
changed = levels.EnsureUserLevel(puppet.MXID, expectedLevel) || changed
|
||||||
|
if user != nil {
|
||||||
if participant.JID == portal.user.JID() {
|
changed = levels.EnsureUserLevel(user.MXID, expectedLevel) || changed
|
||||||
changed = levels.EnsureUserLevel(portal.user.ID, expectedLevel) || changed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to change power levels:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool {
|
func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInfo) bool {
|
||||||
if avatar == nil {
|
if avatar == nil {
|
||||||
var err error
|
var err error
|
||||||
avatar, err = portal.user.Conn.GetProfilePicThumb(portal.JID)
|
avatar, err = user.Conn.GetProfilePicThumb(portal.Key.JID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Errorln(err)
|
portal.log.Errorln(err)
|
||||||
return false
|
return false
|
||||||
|
@ -184,7 +284,7 @@ func (portal *Portal) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool {
|
||||||
|
|
||||||
func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool {
|
func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool {
|
||||||
if portal.Name != name {
|
if portal.Name != name {
|
||||||
intent := portal.user.GetPuppetByJID(setBy).Intent()
|
intent := portal.bridge.GetPuppetByJID(setBy).Intent()
|
||||||
_, err := intent.SetRoomName(portal.MXID, name)
|
_, err := intent.SetRoomName(portal.MXID, name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
portal.Name = name
|
portal.Name = name
|
||||||
|
@ -197,7 +297,7 @@ func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool {
|
||||||
|
|
||||||
func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool {
|
func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool {
|
||||||
if portal.Topic != topic {
|
if portal.Topic != topic {
|
||||||
intent := portal.user.GetPuppetByJID(setBy).Intent()
|
intent := portal.bridge.GetPuppetByJID(setBy).Intent()
|
||||||
_, err := intent.SetRoomTopic(portal.MXID, topic)
|
_, err := intent.SetRoomTopic(portal.MXID, topic)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
portal.Topic = topic
|
portal.Topic = topic
|
||||||
|
@ -208,8 +308,8 @@ func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) UpdateMetadata() bool {
|
func (portal *Portal) UpdateMetadata(user *User) bool {
|
||||||
metadata, err := portal.user.Conn.GetGroupMetaData(portal.JID)
|
metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Errorln(err)
|
portal.log.Errorln(err)
|
||||||
return false
|
return false
|
||||||
|
@ -221,25 +321,25 @@ func (portal *Portal) UpdateMetadata() bool {
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) Sync(contact whatsapp.Contact) {
|
func (portal *Portal) Sync(user *User, contact whatsapp.Contact) {
|
||||||
if len(portal.MXID) == 0 {
|
|
||||||
if !portal.IsPrivateChat() {
|
|
||||||
portal.Name = contact.Name
|
|
||||||
}
|
|
||||||
err := portal.CreateMatrixRoom()
|
|
||||||
if err != nil {
|
|
||||||
portal.log.Errorln("Failed to create portal room:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if portal.IsPrivateChat() {
|
if portal.IsPrivateChat() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
portal.Name = contact.Name
|
||||||
|
err := portal.CreateMatrixRoom([]string{user.MXID})
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to create portal room:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
portal.MainIntent().EnsureInvited(portal.MXID, user.MXID)
|
||||||
|
}
|
||||||
|
|
||||||
update := false
|
update := false
|
||||||
update = portal.UpdateMetadata() || update
|
update = portal.UpdateMetadata(user) || update
|
||||||
update = portal.UpdateAvatar(nil) || update
|
update = portal.UpdateAvatar(user, nil) || update
|
||||||
if update {
|
if update {
|
||||||
portal.Update()
|
portal.Update()
|
||||||
}
|
}
|
||||||
|
@ -258,10 +358,10 @@ func (portal *Portal) GetBasePowerLevels() *gomatrix.PowerLevels {
|
||||||
Users: map[string]int{
|
Users: map[string]int{
|
||||||
portal.MainIntent().UserID: 100,
|
portal.MainIntent().UserID: 100,
|
||||||
},
|
},
|
||||||
Events: map[gomatrix.EventType]int{
|
Events: map[string]int{
|
||||||
gomatrix.StateRoomName: anyone,
|
gomatrix.StateRoomName.Type: anyone,
|
||||||
gomatrix.StateRoomAvatar: anyone,
|
gomatrix.StateRoomAvatar.Type: anyone,
|
||||||
gomatrix.StateTopic: anyone,
|
gomatrix.StateTopic.Type: anyone,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,15 +377,19 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) {
|
||||||
}
|
}
|
||||||
changed := false
|
changed := false
|
||||||
for _, jid := range jids {
|
for _, jid := range jids {
|
||||||
puppet := portal.user.GetPuppetByJID(jid)
|
puppet := portal.bridge.GetPuppetByJID(jid)
|
||||||
changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed
|
changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed
|
||||||
|
|
||||||
if jid == portal.user.JID() {
|
user := portal.bridge.GetUserByJID(jid)
|
||||||
changed = levels.EnsureUserLevel(portal.user.ID, newLevel) || changed
|
if user != nil {
|
||||||
|
changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to change power levels:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +403,10 @@ func (portal *Portal) RestrictMessageSending(restrict bool) {
|
||||||
} else {
|
} else {
|
||||||
levels.EventsDefault = 0
|
levels.EventsDefault = 0
|
||||||
}
|
}
|
||||||
portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to change power levels:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) RestrictMetadataChanges(restrict bool) {
|
func (portal *Portal) RestrictMetadataChanges(restrict bool) {
|
||||||
|
@ -312,15 +419,18 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) {
|
||||||
newLevel = 50
|
newLevel = 50
|
||||||
}
|
}
|
||||||
changed := false
|
changed := false
|
||||||
changed = levels.EnsureEventLevel(gomatrix.StateRoomName, true, newLevel) || changed
|
changed = levels.EnsureEventLevel(gomatrix.StateRoomName, newLevel) || changed
|
||||||
changed = levels.EnsureEventLevel(gomatrix.StateRoomAvatar, true, newLevel) || changed
|
changed = levels.EnsureEventLevel(gomatrix.StateRoomAvatar, newLevel) || changed
|
||||||
changed = levels.EnsureEventLevel(gomatrix.StateTopic, true, newLevel) || changed
|
changed = levels.EnsureEventLevel(gomatrix.StateTopic, newLevel) || changed
|
||||||
if changed {
|
if changed {
|
||||||
portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to change power levels:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) CreateMatrixRoom() error {
|
func (portal *Portal) CreateMatrixRoom(invite []string) error {
|
||||||
portal.roomCreateLock.Lock()
|
portal.roomCreateLock.Lock()
|
||||||
defer portal.roomCreateLock.Unlock()
|
defer portal.roomCreateLock.Unlock()
|
||||||
if len(portal.MXID) > 0 {
|
if len(portal.MXID) > 0 {
|
||||||
|
@ -330,7 +440,6 @@ func (portal *Portal) CreateMatrixRoom() error {
|
||||||
name := portal.Name
|
name := portal.Name
|
||||||
topic := portal.Topic
|
topic := portal.Topic
|
||||||
isPrivateChat := false
|
isPrivateChat := false
|
||||||
invite := []string{portal.user.ID}
|
|
||||||
if portal.IsPrivateChat() {
|
if portal.IsPrivateChat() {
|
||||||
name = ""
|
name = ""
|
||||||
topic = "WhatsApp private chat"
|
topic = "WhatsApp private chat"
|
||||||
|
@ -360,39 +469,27 @@ func (portal *Portal) CreateMatrixRoom() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) IsPrivateChat() bool {
|
func (portal *Portal) IsPrivateChat() bool {
|
||||||
return strings.HasSuffix(portal.JID, whatsappExt.NewUserSuffix)
|
if portal.isPrivate == nil {
|
||||||
|
val := strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix)
|
||||||
|
portal.isPrivate = &val
|
||||||
|
}
|
||||||
|
return *portal.isPrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) MainIntent() *appservice.IntentAPI {
|
func (portal *Portal) MainIntent() *appservice.IntentAPI {
|
||||||
if portal.IsPrivateChat() {
|
if portal.IsPrivateChat() {
|
||||||
return portal.user.GetPuppetByJID(portal.JID).Intent()
|
return portal.bridge.GetPuppetByJID(portal.Key.JID).Intent()
|
||||||
}
|
}
|
||||||
return portal.bridge.AppService.BotIntent()
|
return portal.bridge.Bot
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool {
|
func (portal *Portal) GetMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI {
|
||||||
msg := portal.bridge.DB.Message.GetByJID(portal.Owner, id)
|
|
||||||
if msg != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) {
|
|
||||||
msg := portal.bridge.DB.Message.New()
|
|
||||||
msg.Owner = portal.Owner
|
|
||||||
msg.JID = jid
|
|
||||||
msg.MXID = mxid
|
|
||||||
msg.Insert()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.IntentAPI {
|
|
||||||
if info.FromMe {
|
if info.FromMe {
|
||||||
if portal.IsPrivateChat() {
|
if portal.IsPrivateChat() {
|
||||||
// TODO handle own messages in private chats properly
|
// TODO handle own messages in private chats properly
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return portal.user.GetPuppetByJID(portal.user.JID()).Intent()
|
return portal.bridge.GetPuppetByJID(user.JID).Intent()
|
||||||
} else if portal.IsPrivateChat() {
|
} else if portal.IsPrivateChat() {
|
||||||
return portal.MainIntent()
|
return portal.MainIntent()
|
||||||
} else if len(info.SenderJid) == 0 {
|
} else if len(info.SenderJid) == 0 {
|
||||||
|
@ -402,14 +499,14 @@ func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.In
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return portal.user.GetPuppetByJID(info.SenderJid).Intent()
|
return portal.bridge.GetPuppetByJID(info.SenderJid).Intent()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageInfo) {
|
func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageInfo) {
|
||||||
if len(info.QuotedMessageID) == 0 {
|
if len(info.QuotedMessageID) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message := portal.bridge.DB.Message.GetByJID(portal.Owner, info.QuotedMessageID)
|
message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID)
|
||||||
if message != nil {
|
if message != nil {
|
||||||
event, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
|
event, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -421,35 +518,20 @@ func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageI
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) FormatWhatsAppMessage(content *gomatrix.Content) {
|
func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) {
|
||||||
output := html.EscapeString(content.Body)
|
lock, ok := portal.startHandling(message.Info.Id)
|
||||||
for regex, replacement := range portal.user.waReplString {
|
if !ok {
|
||||||
output = regex.ReplaceAllString(output, replacement)
|
|
||||||
}
|
|
||||||
for regex, replacer := range portal.user.waReplFunc {
|
|
||||||
output = regex.ReplaceAllStringFunc(output, replacer)
|
|
||||||
}
|
|
||||||
if output != content.Body {
|
|
||||||
content.FormattedBody = output
|
|
||||||
content.Format = gomatrix.FormatHTML
|
|
||||||
for regex, replacer := range portal.user.waReplFuncText {
|
|
||||||
content.Body = regex.ReplaceAllStringFunc(content.Body, replacer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
|
|
||||||
if portal.IsDuplicate(message.Info.Id) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
err := portal.CreateMatrixRoom()
|
err := portal.CreateMatrixRoom([]string{source.MXID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Errorln("Failed to create portal room:", err)
|
portal.log.Errorln("Failed to create portal room:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
intent := portal.GetMessageIntent(message.Info)
|
intent := portal.GetMessageIntent(source, message.Info)
|
||||||
if intent == nil {
|
if intent == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -459,7 +541,7 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
|
||||||
MsgType: gomatrix.MsgText,
|
MsgType: gomatrix.MsgText,
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.FormatWhatsAppMessage(content)
|
portal.bridge.Formatter.ParseWhatsApp(content)
|
||||||
portal.SetReply(content, message.Info)
|
portal.SetReply(content, message.Info)
|
||||||
|
|
||||||
intent.UserTyping(portal.MXID, false, 0)
|
intent.UserTyping(portal.MXID, false, 0)
|
||||||
|
@ -468,22 +550,23 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
|
||||||
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
|
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
portal.MarkHandled(message.Info.Id, resp.EventID)
|
portal.finishHandling(source, message.Info.Source, resp.EventID)
|
||||||
portal.log.Debugln("Handled message", message.Info.Id, "->", resp.EventID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) {
|
func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) {
|
||||||
if portal.IsDuplicate(info.Id) {
|
lock, ok := portal.startHandling(info.Id)
|
||||||
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
err := portal.CreateMatrixRoom()
|
err := portal.CreateMatrixRoom([]string{source.MXID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Errorln("Failed to create portal room:", err)
|
portal.log.Errorln("Failed to create portal room:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
intent := portal.GetMessageIntent(info)
|
intent := portal.GetMessageIntent(source, info)
|
||||||
if intent == nil {
|
if intent == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -559,7 +642,7 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
|
||||||
MsgType: gomatrix.MsgNotice,
|
MsgType: gomatrix.MsgNotice,
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.FormatWhatsAppMessage(captionContent)
|
portal.bridge.Formatter.ParseWhatsApp(captionContent)
|
||||||
|
|
||||||
_, err := intent.SendMassagedMessageEvent(portal.MXID, gomatrix.EventMessage, captionContent, ts)
|
_, err := intent.SendMassagedMessageEvent(portal.MXID, gomatrix.EventMessage, captionContent, ts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -568,8 +651,7 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
|
||||||
// TODO store caption mxid?
|
// TODO store caption mxid?
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.MarkHandled(info.Id, resp.EventID)
|
portal.finishHandling(source, info.Source, resp.EventID)
|
||||||
portal.log.Debugln("Handled message", info.Id, "->", resp.EventID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMessageID() *string {
|
func makeMessageID() *string {
|
||||||
|
@ -612,7 +694,7 @@ func (portal *Portal) downloadThumbnail(evt *gomatrix.Event) []byte {
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) preprocessMatrixMedia(evt *gomatrix.Event, mediaType whatsapp.MediaType) *MediaUpload {
|
func (portal *Portal) preprocessMatrixMedia(sender *User, evt *gomatrix.Event, mediaType whatsapp.MediaType) *MediaUpload {
|
||||||
if evt.Content.Info == nil {
|
if evt.Content.Info == nil {
|
||||||
evt.Content.Info = &gomatrix.FileInfo{}
|
evt.Content.Info = &gomatrix.FileInfo{}
|
||||||
}
|
}
|
||||||
|
@ -630,7 +712,7 @@ func (portal *Portal) preprocessMatrixMedia(evt *gomatrix.Event, mediaType whats
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := portal.user.Conn.Upload(bytes.NewReader(content), mediaType)
|
url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(content), mediaType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Errorfln("Failed to upload media in %s: %v", evt.ID, err)
|
portal.log.Errorfln("Failed to upload media in %s: %v", evt.ID, err)
|
||||||
return nil
|
return nil
|
||||||
|
@ -657,32 +739,11 @@ type MediaUpload struct {
|
||||||
Thumbnail []byte
|
Thumbnail []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) GetMessage(jid types.WhatsAppMessageID) *waProto.WebMessageInfo {
|
func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) {
|
||||||
node, err := portal.user.Conn.LoadMessagesBefore(portal.JID, jid, 1)
|
if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver {
|
||||||
if err != nil {
|
return
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
msgs, ok := node.Content.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msg, ok := msgs[0].(*waProto.WebMessageInfo)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
node, err = portal.user.Conn.LoadMessagesAfter(portal.JID, msg.GetKey().GetId(), 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msgs, ok = node.Content.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msg, _ = msgs[0].(*waProto.WebMessageInfo)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
|
||||||
ts := uint64(evt.Timestamp / 1000)
|
ts := uint64(evt.Timestamp / 1000)
|
||||||
status := waProto.WebMessageInfo_ERROR
|
status := waProto.WebMessageInfo_ERROR
|
||||||
fromMe := true
|
fromMe := true
|
||||||
|
@ -690,7 +751,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
Key: &waProto.MessageKey{
|
Key: &waProto.MessageKey{
|
||||||
FromMe: &fromMe,
|
FromMe: &fromMe,
|
||||||
Id: makeMessageID(),
|
Id: makeMessageID(),
|
||||||
RemoteJid: &portal.JID,
|
RemoteJid: &portal.Key.JID,
|
||||||
},
|
},
|
||||||
MessageTimestamp: &ts,
|
MessageTimestamp: &ts,
|
||||||
Message: &waProto.Message{},
|
Message: &waProto.Message{},
|
||||||
|
@ -701,17 +762,10 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
if len(replyToID) > 0 {
|
if len(replyToID) > 0 {
|
||||||
evt.Content.RemoveReplyFallback()
|
evt.Content.RemoveReplyFallback()
|
||||||
msg := portal.bridge.DB.Message.GetByMXID(replyToID)
|
msg := portal.bridge.DB.Message.GetByMXID(replyToID)
|
||||||
if msg != nil {
|
if msg != nil && msg.Content != nil {
|
||||||
origMsg := portal.GetMessage(msg.JID)
|
|
||||||
if origMsg != nil {
|
|
||||||
ctxInfo.StanzaId = &msg.JID
|
ctxInfo.StanzaId = &msg.JID
|
||||||
replyMsgSender := origMsg.GetParticipant()
|
ctxInfo.Participant = &msg.Sender
|
||||||
if origMsg.GetKey().GetFromMe() {
|
ctxInfo.QuotedMessage = []*waProto.Message{msg.Content}
|
||||||
replyMsgSender = portal.user.JID()
|
|
||||||
}
|
|
||||||
ctxInfo.Participant = &replyMsgSender
|
|
||||||
ctxInfo.QuotedMessage = []*waProto.Message{origMsg.Message}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
|
@ -719,7 +773,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
case gomatrix.MsgText, gomatrix.MsgEmote:
|
case gomatrix.MsgText, gomatrix.MsgEmote:
|
||||||
text := evt.Content.Body
|
text := evt.Content.Body
|
||||||
if evt.Content.Format == gomatrix.FormatHTML {
|
if evt.Content.Format == gomatrix.FormatHTML {
|
||||||
text = portal.user.htmlParser.Parse(evt.Content.FormattedBody)
|
text = portal.bridge.Formatter.ParseMatrix(evt.Content.FormattedBody)
|
||||||
}
|
}
|
||||||
if evt.Content.MsgType == gomatrix.MsgEmote {
|
if evt.Content.MsgType == gomatrix.MsgEmote {
|
||||||
text = "/me " + text
|
text = "/me " + text
|
||||||
|
@ -737,7 +791,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
info.Message.Conversation = &text
|
info.Message.Conversation = &text
|
||||||
}
|
}
|
||||||
case gomatrix.MsgImage:
|
case gomatrix.MsgImage:
|
||||||
media := portal.preprocessMatrixMedia(evt, whatsapp.MediaImage)
|
media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaImage)
|
||||||
if media == nil {
|
if media == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -752,7 +806,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
FileLength: &media.FileLength,
|
FileLength: &media.FileLength,
|
||||||
}
|
}
|
||||||
case gomatrix.MsgVideo:
|
case gomatrix.MsgVideo:
|
||||||
media := portal.preprocessMatrixMedia(evt, whatsapp.MediaVideo)
|
media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaVideo)
|
||||||
if media == nil {
|
if media == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -769,7 +823,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
FileLength: &media.FileLength,
|
FileLength: &media.FileLength,
|
||||||
}
|
}
|
||||||
case gomatrix.MsgAudio:
|
case gomatrix.MsgAudio:
|
||||||
media := portal.preprocessMatrixMedia(evt, whatsapp.MediaAudio)
|
media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaAudio)
|
||||||
if media == nil {
|
if media == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -784,7 +838,7 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
FileLength: &media.FileLength,
|
FileLength: &media.FileLength,
|
||||||
}
|
}
|
||||||
case gomatrix.MsgFile:
|
case gomatrix.MsgFile:
|
||||||
media := portal.preprocessMatrixMedia(evt, whatsapp.MediaDocument)
|
media := portal.preprocessMatrixMedia(sender, evt, whatsapp.MediaDocument)
|
||||||
if media == nil {
|
if media == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -800,8 +854,8 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
|
||||||
portal.log.Debugln("Unhandled Matrix event:", evt)
|
portal.log.Debugln("Unhandled Matrix event:", evt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = portal.user.Conn.Send(info)
|
portal.markHandled(sender, info, evt.ID)
|
||||||
portal.MarkHandled(info.GetKey().GetId(), evt.ID)
|
err = sender.Conn.Send(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
|
portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
|
||||||
} else {
|
} else {
|
||||||
|
|
99
puppet.go
99
puppet.go
|
@ -30,105 +30,83 @@ import (
|
||||||
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
|
func (bridge *Bridge) ParsePuppetMXID(mxid 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]+)"),
|
bridge.Config.Bridge.FormatUsername("([0-9]+)"),
|
||||||
bridge.Config.Homeserver.Domain))
|
bridge.Config.Homeserver.Domain))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Log.Warnln("Failed to compile puppet user ID regex:", err)
|
bridge.Log.Warnln("Failed to compile puppet user ID regex:", err)
|
||||||
return "", "", false
|
return "", false
|
||||||
}
|
}
|
||||||
match := userIDRegex.FindStringSubmatch(string(mxid))
|
match := userIDRegex.FindStringSubmatch(string(mxid))
|
||||||
if match == nil || len(match) != 3 {
|
if match == nil || len(match) != 2 {
|
||||||
return "", "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
receiver := types.MatrixUserID(match[1])
|
jid := types.WhatsAppID(match[1] + whatsappExt.NewUserSuffix)
|
||||||
receiver = strings.Replace(receiver, "=40", "@", 1)
|
return jid, true
|
||||||
colonIndex := strings.LastIndex(receiver, "=3")
|
|
||||||
receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):]
|
|
||||||
jid := types.WhatsAppID(match[2] + whatsappExt.NewUserSuffix)
|
|
||||||
return receiver, jid, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
|
func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
|
||||||
receiver, jid, ok := bridge.ParsePuppetMXID(mxid)
|
jid, ok := bridge.ParsePuppetMXID(mxid)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user := bridge.GetUser(receiver)
|
return bridge.GetPuppetByJID(jid)
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.GetPuppetByJID(jid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
|
func (bridge *Bridge) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
|
||||||
receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid)
|
bridge.puppetsLock.Lock()
|
||||||
if !ok || receiver != user.ID {
|
defer bridge.puppetsLock.Unlock()
|
||||||
return nil
|
puppet, ok := bridge.puppets[jid]
|
||||||
}
|
|
||||||
|
|
||||||
return user.GetPuppetByJID(jid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
|
|
||||||
user.puppetsLock.Lock()
|
|
||||||
defer user.puppetsLock.Unlock()
|
|
||||||
puppet, ok := user.puppets[jid]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID)
|
dbPuppet := bridge.DB.Puppet.Get(jid)
|
||||||
if dbPuppet == nil {
|
if dbPuppet == nil {
|
||||||
dbPuppet = user.bridge.DB.Puppet.New()
|
dbPuppet = bridge.DB.Puppet.New()
|
||||||
dbPuppet.JID = jid
|
dbPuppet.JID = jid
|
||||||
dbPuppet.Receiver = user.ID
|
|
||||||
dbPuppet.Insert()
|
dbPuppet.Insert()
|
||||||
}
|
}
|
||||||
puppet = user.NewPuppet(dbPuppet)
|
puppet = bridge.NewPuppet(dbPuppet)
|
||||||
user.puppets[puppet.JID] = puppet
|
bridge.puppets[puppet.JID] = puppet
|
||||||
}
|
}
|
||||||
return puppet
|
return puppet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) GetAllPuppets() []*Puppet {
|
func (bridge *Bridge) GetAllPuppets() []*Puppet {
|
||||||
user.puppetsLock.Lock()
|
bridge.puppetsLock.Lock()
|
||||||
defer user.puppetsLock.Unlock()
|
defer bridge.puppetsLock.Unlock()
|
||||||
dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID)
|
dbPuppets := bridge.DB.Puppet.GetAll()
|
||||||
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 := bridge.puppets[dbPuppet.JID]
|
||||||
if !ok {
|
if !ok {
|
||||||
puppet = user.NewPuppet(dbPuppet)
|
puppet = bridge.NewPuppet(dbPuppet)
|
||||||
user.puppets[dbPuppet.JID] = puppet
|
bridge.puppets[dbPuppet.JID] = puppet
|
||||||
}
|
}
|
||||||
output[index] = puppet
|
output[index] = puppet
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
|
func (bridge *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
|
||||||
return &Puppet{
|
return &Puppet{
|
||||||
Puppet: dbPuppet,
|
Puppet: dbPuppet,
|
||||||
user: user,
|
bridge: bridge,
|
||||||
bridge: user.bridge,
|
log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)),
|
||||||
log: user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)),
|
|
||||||
|
|
||||||
MXID: fmt.Sprintf("@%s:%s",
|
MXID: fmt.Sprintf("@%s:%s",
|
||||||
user.bridge.Config.Bridge.FormatUsername(
|
bridge.Config.Bridge.FormatUsername(
|
||||||
dbPuppet.Receiver,
|
|
||||||
strings.Replace(
|
strings.Replace(
|
||||||
dbPuppet.JID,
|
dbPuppet.JID,
|
||||||
whatsappExt.NewUserSuffix, "", 1)),
|
whatsappExt.NewUserSuffix, "", 1)),
|
||||||
user.bridge.Config.Homeserver.Domain),
|
bridge.Config.Homeserver.Domain),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Puppet struct {
|
type Puppet struct {
|
||||||
*database.Puppet
|
*database.Puppet
|
||||||
|
|
||||||
user *User
|
|
||||||
bridge *Bridge
|
bridge *Bridge
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
|
@ -143,13 +121,13 @@ func (puppet *Puppet) PhoneNumber() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (puppet *Puppet) Intent() *appservice.IntentAPI {
|
func (puppet *Puppet) Intent() *appservice.IntentAPI {
|
||||||
return puppet.bridge.AppService.Intent(puppet.MXID)
|
return puppet.bridge.AS.Intent(puppet.MXID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (puppet *Puppet) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool {
|
func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicInfo) bool {
|
||||||
if avatar == nil {
|
if avatar == nil {
|
||||||
var err error
|
var err error
|
||||||
avatar, err = puppet.user.Conn.GetProfilePicThumb(puppet.JID)
|
avatar, err = source.Conn.GetProfilePicThumb(puppet.JID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Errorln(err)
|
puppet.log.Errorln(err)
|
||||||
return false
|
return false
|
||||||
|
@ -184,24 +162,25 @@ func (puppet *Puppet) UpdateAvatar(avatar *whatsappExt.ProfilePicInfo) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (puppet *Puppet) Sync(contact whatsapp.Contact) {
|
func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
|
||||||
puppet.Intent().EnsureRegistered()
|
puppet.Intent().EnsureRegistered()
|
||||||
|
|
||||||
if contact.Jid == puppet.user.JID() {
|
if contact.Jid == source.JID {
|
||||||
contact.Notify = puppet.user.Conn.Info.Pushname
|
contact.Notify = source.Conn.Info.Pushname
|
||||||
}
|
}
|
||||||
newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
|
newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
|
||||||
if puppet.Displayname != newName {
|
if puppet.Displayname != newName && quality >= puppet.NameQuality {
|
||||||
err := puppet.Intent().SetDisplayName(newName)
|
err := puppet.Intent().SetDisplayName(newName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
puppet.Displayname = newName
|
puppet.Displayname = newName
|
||||||
|
puppet.NameQuality = quality
|
||||||
puppet.Update()
|
puppet.Update()
|
||||||
} else {
|
} else {
|
||||||
puppet.log.Warnln("Failed to set display name:", err)
|
puppet.log.Warnln("Failed to set display name:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if puppet.UpdateAvatar(nil) {
|
if puppet.UpdateAvatar(source, nil) {
|
||||||
puppet.Update()
|
puppet.Update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
141
user.go
141
user.go
|
@ -17,14 +17,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rhymen/go-whatsapp"
|
"github.com/Rhymen/go-whatsapp"
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
"maunium.net/go/gomatrix/format"
|
|
||||||
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/mautrix-whatsapp/types"
|
"maunium.net/go/mautrix-whatsapp/types"
|
||||||
|
@ -40,32 +37,44 @@ type User struct {
|
||||||
|
|
||||||
Admin bool
|
Admin bool
|
||||||
Whitelisted bool
|
Whitelisted bool
|
||||||
jid string
|
|
||||||
|
|
||||||
portalsByMXID map[types.MatrixRoomID]*Portal
|
|
||||||
portalsByJID map[types.WhatsAppID]*Portal
|
|
||||||
portalsLock sync.Mutex
|
|
||||||
puppets map[types.WhatsAppID]*Puppet
|
|
||||||
puppetsLock sync.Mutex
|
|
||||||
|
|
||||||
htmlParser *format.HTMLParser
|
|
||||||
|
|
||||||
waReplString map[*regexp.Regexp]string
|
|
||||||
waReplFunc map[*regexp.Regexp]func(string) string
|
|
||||||
waReplFuncText map[*regexp.Regexp]func(string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
|
func (bridge *Bridge) GetUserByMXID(userID types.MatrixUserID) *User {
|
||||||
user, ok := bridge.users[userID]
|
bridge.usersLock.Lock()
|
||||||
|
defer bridge.usersLock.Unlock()
|
||||||
|
user, ok := bridge.usersByMXID[userID]
|
||||||
if !ok {
|
if !ok {
|
||||||
dbUser := bridge.DB.User.Get(userID)
|
dbUser := bridge.DB.User.GetByMXID(userID)
|
||||||
if dbUser == nil {
|
if dbUser == nil {
|
||||||
dbUser = bridge.DB.User.New()
|
dbUser = bridge.DB.User.New()
|
||||||
dbUser.ID = userID
|
dbUser.MXID = userID
|
||||||
dbUser.Insert()
|
dbUser.Insert()
|
||||||
}
|
}
|
||||||
user = bridge.NewUser(dbUser)
|
user = bridge.NewUser(dbUser)
|
||||||
bridge.users[user.ID] = user
|
bridge.usersByMXID[user.MXID] = user
|
||||||
|
if len(user.JID) > 0 {
|
||||||
|
bridge.usersByJID[user.JID] = user
|
||||||
|
}
|
||||||
|
if len(user.ManagementRoom) > 0 {
|
||||||
|
bridge.managementRooms[user.ManagementRoom] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetUserByJID(userID types.WhatsAppID) *User {
|
||||||
|
bridge.usersLock.Lock()
|
||||||
|
defer bridge.usersLock.Unlock()
|
||||||
|
user, ok := bridge.usersByJID[userID]
|
||||||
|
if !ok {
|
||||||
|
dbUser := bridge.DB.User.GetByJID(userID)
|
||||||
|
if dbUser == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
user = bridge.NewUser(dbUser)
|
||||||
|
bridge.usersByMXID[user.MXID] = user
|
||||||
|
bridge.usersByJID[user.JID] = user
|
||||||
if len(user.ManagementRoom) > 0 {
|
if len(user.ManagementRoom) > 0 {
|
||||||
bridge.managementRooms[user.ManagementRoom] = user
|
bridge.managementRooms[user.ManagementRoom] = user
|
||||||
}
|
}
|
||||||
|
@ -74,13 +83,18 @@ func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetAllUsers() []*User {
|
func (bridge *Bridge) GetAllUsers() []*User {
|
||||||
|
bridge.usersLock.Lock()
|
||||||
|
defer bridge.usersLock.Unlock()
|
||||||
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.ID]
|
user, ok := bridge.usersByMXID[dbUser.MXID]
|
||||||
if !ok {
|
if !ok {
|
||||||
user = bridge.NewUser(dbUser)
|
user = bridge.NewUser(dbUser)
|
||||||
bridge.users[user.ID] = user
|
bridge.usersByMXID[user.MXID] = user
|
||||||
|
if len(user.JID) > 0 {
|
||||||
|
bridge.usersByJID[user.JID] = user
|
||||||
|
}
|
||||||
if len(user.ManagementRoom) > 0 {
|
if len(user.ManagementRoom) > 0 {
|
||||||
bridge.managementRooms[user.ManagementRoom] = user
|
bridge.managementRooms[user.ManagementRoom] = user
|
||||||
}
|
}
|
||||||
|
@ -94,15 +108,10 @@ func (bridge *Bridge) NewUser(dbUser *database.User) *User {
|
||||||
user := &User{
|
user := &User{
|
||||||
User: dbUser,
|
User: dbUser,
|
||||||
bridge: bridge,
|
bridge: bridge,
|
||||||
log: bridge.Log.Sub("User").Sub(string(dbUser.ID)),
|
log: bridge.Log.Sub("User").Sub(string(dbUser.MXID)),
|
||||||
portalsByMXID: make(map[types.MatrixRoomID]*Portal),
|
|
||||||
portalsByJID: make(map[types.WhatsAppID]*Portal),
|
|
||||||
puppets: make(map[types.WhatsAppID]*Puppet),
|
|
||||||
}
|
}
|
||||||
user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.ID)
|
user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID)
|
||||||
user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.ID)
|
user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.MXID)
|
||||||
user.htmlParser = user.newHTMLParser()
|
|
||||||
user.waReplString, user.waReplFunc, user.waReplFuncText = user.newWhatsAppFormatMaps()
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +151,7 @@ func (user *User) Connect(evenIfNoSession bool) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
user.Conn = whatsappExt.ExtendConn(conn)
|
user.Conn = whatsappExt.ExtendConn(conn)
|
||||||
|
user.Conn.SetClientName("Mautrix-WhatsApp bridge", "mx-wa")
|
||||||
user.log.Debugln("WhatsApp connection successful")
|
user.log.Debugln("WhatsApp connection successful")
|
||||||
user.Conn.AddHandler(user)
|
user.Conn.AddHandler(user)
|
||||||
return user.RestoreSession()
|
return user.RestoreSession()
|
||||||
|
@ -152,7 +162,6 @@ func (user *User) RestoreSession() bool {
|
||||||
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.SetSession(nil)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
user.SetSession(&sess)
|
user.SetSession(&sess)
|
||||||
|
@ -162,8 +171,12 @@ func (user *User) RestoreSession() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) IsLoggedIn() bool {
|
||||||
|
return user.Conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) Login(roomID types.MatrixRoomID) {
|
func (user *User) Login(roomID types.MatrixRoomID) {
|
||||||
bot := user.bridge.AppService.BotClient()
|
bot := user.bridge.AS.BotClient()
|
||||||
|
|
||||||
qrChan := make(chan string, 2)
|
qrChan := make(chan string, 2)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -194,38 +207,24 @@ func (user *User) Login(roomID types.MatrixRoomID) {
|
||||||
qrChan <- "error"
|
qrChan <- "error"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1)
|
||||||
user.Session = &session
|
user.Session = &session
|
||||||
user.Update()
|
user.Update()
|
||||||
bot.SendNotice(roomID, "Successfully logged in. Synchronizing chats...")
|
bot.SendNotice(roomID, "Successfully logged in. Synchronizing chats...")
|
||||||
go user.Sync()
|
go user.Sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) JID() string {
|
|
||||||
if user.Conn == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if len(user.jid) == 0 {
|
|
||||||
user.jid = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1)
|
|
||||||
}
|
|
||||||
return user.jid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) Sync() {
|
func (user *User) Sync() {
|
||||||
user.log.Debugln("Syncing...")
|
user.log.Debugln("Syncing...")
|
||||||
user.Conn.Contacts()
|
user.Conn.Contacts()
|
||||||
for jid, contact := range user.Conn.Store.Contacts {
|
for jid, contact := range user.Conn.Store.Contacts {
|
||||||
if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) {
|
if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) {
|
||||||
puppet := user.GetPuppetByJID(contact.Jid)
|
puppet := user.bridge.GetPuppetByJID(contact.Jid)
|
||||||
puppet.Sync(contact)
|
puppet.Sync(user, contact)
|
||||||
|
} else {
|
||||||
|
portal := user.bridge.GetPortalByJID(database.GroupPortalKey(contact.Jid))
|
||||||
|
portal.Sync(user, contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") {
|
|
||||||
// No messages sent -> don't bridge
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
portal := user.GetPortalByJID(contact.Jid)
|
|
||||||
portal.Sync(contact)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,33 +236,41 @@ func (user *User) HandleJSONParseError(err error) {
|
||||||
user.log.Errorln("WhatsApp JSON parse error:", err)
|
user.log.Errorln("WhatsApp JSON parse error:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) PortalKey(jid types.WhatsAppID) database.PortalKey {
|
||||||
|
return database.NewPortalKey(jid, user.JID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
|
||||||
|
return user.bridge.GetPortalByJID(user.PortalKey(jid))
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
|
func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
|
||||||
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
||||||
portal.HandleTextMessage(message)
|
portal.HandleTextMessage(user, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
|
func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
|
||||||
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
||||||
portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Caption)
|
portal.HandleMediaMessage(user, message.Download, message.Thumbnail, message.Info, message.Type, message.Caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
|
func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
|
||||||
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
||||||
portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Caption)
|
portal.HandleMediaMessage(user, message.Download, message.Thumbnail, message.Info, message.Type, message.Caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) {
|
func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) {
|
||||||
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
||||||
portal.HandleMediaMessage(message.Download, nil, message.Info, message.Type, "")
|
portal.HandleMediaMessage(user, message.Download, nil, message.Info, message.Type, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) {
|
func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) {
|
||||||
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
portal := user.GetPortalByJID(message.Info.RemoteJid)
|
||||||
portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Title)
|
portal.HandleMediaMessage(user, message.Download, message.Thumbnail, message.Info, message.Type, message.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) HandlePresence(info whatsappExt.Presence) {
|
func (user *User) HandlePresence(info whatsappExt.Presence) {
|
||||||
puppet := user.GetPuppetByJID(info.SenderJID)
|
puppet := user.bridge.GetPuppetByJID(info.SenderJID)
|
||||||
switch info.Status {
|
switch info.Status {
|
||||||
case whatsappExt.PresenceUnavailable:
|
case whatsappExt.PresenceUnavailable:
|
||||||
puppet.Intent().SetPresence("offline")
|
puppet.Intent().SetPresence("offline")
|
||||||
|
@ -277,6 +284,12 @@ func (user *User) HandlePresence(info whatsappExt.Presence) {
|
||||||
}
|
}
|
||||||
case whatsappExt.PresenceComposing:
|
case whatsappExt.PresenceComposing:
|
||||||
portal := user.GetPortalByJID(info.JID)
|
portal := user.GetPortalByJID(info.JID)
|
||||||
|
if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
|
||||||
|
if puppet.typingIn == portal.MXID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
puppet.Intent().UserTyping(puppet.typingIn, false, 0)
|
||||||
|
}
|
||||||
puppet.typingIn = portal.MXID
|
puppet.typingIn = portal.MXID
|
||||||
puppet.typingAt = time.Now().Unix()
|
puppet.typingAt = time.Now().Unix()
|
||||||
puppet.Intent().UserTyping(portal.MXID, true, 15*1000)
|
puppet.Intent().UserTyping(portal.MXID, true, 15*1000)
|
||||||
|
@ -290,9 +303,9 @@ func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
intent := user.GetPuppetByJID(info.SenderJID).Intent()
|
intent := user.bridge.GetPuppetByJID(info.SenderJID).Intent()
|
||||||
for _, id := range info.IDs {
|
for _, id := range info.IDs {
|
||||||
msg := user.bridge.DB.Message.GetByJID(user.ID, id)
|
msg := user.bridge.DB.Message.GetByJID(portal.Key, id)
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -308,11 +321,11 @@ func (user *User) HandleCommand(cmd whatsappExt.Command) {
|
||||||
switch cmd.Type {
|
switch cmd.Type {
|
||||||
case whatsappExt.CommandPicture:
|
case whatsappExt.CommandPicture:
|
||||||
if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) {
|
if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) {
|
||||||
puppet := user.GetPuppetByJID(cmd.JID)
|
puppet := user.bridge.GetPuppetByJID(cmd.JID)
|
||||||
puppet.UpdateAvatar(cmd.ProfilePicInfo)
|
puppet.UpdateAvatar(user, cmd.ProfilePicInfo)
|
||||||
} else {
|
} else {
|
||||||
portal := user.GetPortalByJID(cmd.JID)
|
portal := user.GetPortalByJID(cmd.JID)
|
||||||
portal.UpdateAvatar(cmd.ProfilePicInfo)
|
portal.UpdateAvatar(user, cmd.ProfilePicInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
vendor/github.com/Rhymen/go-whatsapp/conn.go
generated
vendored
6
vendor/github.com/Rhymen/go-whatsapp/conn.go
generated
vendored
|
@ -89,6 +89,9 @@ type Conn struct {
|
||||||
Info *Info
|
Info *Info
|
||||||
Store *Store
|
Store *Store
|
||||||
ServerLastSeen time.Time
|
ServerLastSeen time.Time
|
||||||
|
|
||||||
|
longClientName string
|
||||||
|
shortClientName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type wsMsg struct {
|
type wsMsg struct {
|
||||||
|
@ -122,6 +125,9 @@ func NewConn(timeout time.Duration) (*Conn, error) {
|
||||||
msgCount: 0,
|
msgCount: 0,
|
||||||
msgTimeout: timeout,
|
msgTimeout: timeout,
|
||||||
Store: newStore(),
|
Store: newStore(),
|
||||||
|
|
||||||
|
longClientName: "github.com/rhymen/go-whatsapp",
|
||||||
|
shortClientName: "go-whatsapp",
|
||||||
}
|
}
|
||||||
|
|
||||||
go wac.readPump()
|
go wac.readPump()
|
||||||
|
|
52
vendor/github.com/Rhymen/go-whatsapp/handler.go
generated
vendored
52
vendor/github.com/Rhymen/go-whatsapp/handler.go
generated
vendored
|
@ -72,7 +72,7 @@ type JsonMessageHandler interface {
|
||||||
/**
|
/**
|
||||||
The RawMessageHandler interface needs to be implemented to receive raw messages dispatched by the dispatcher.
|
The RawMessageHandler interface needs to be implemented to receive raw messages dispatched by the dispatcher.
|
||||||
Raw messages are the raw protobuf structs instead of the easy-to-use structs in TextMessageHandler, ImageMessageHandler, etc..
|
Raw messages are the raw protobuf structs instead of the easy-to-use structs in TextMessageHandler, ImageMessageHandler, etc..
|
||||||
*/
|
*/
|
||||||
type RawMessageHandler interface {
|
type RawMessageHandler interface {
|
||||||
Handler
|
Handler
|
||||||
HandleRawMessage(message *proto.WebMessageInfo)
|
HandleRawMessage(message *proto.WebMessageInfo)
|
||||||
|
@ -96,53 +96,47 @@ func (wac *Conn) handle(message interface{}) {
|
||||||
}
|
}
|
||||||
case string:
|
case string:
|
||||||
for _, h := range wac.handler {
|
for _, h := range wac.handler {
|
||||||
x, ok := h.(JsonMessageHandler)
|
if x, ok := h.(JsonMessageHandler); ok {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleJsonMessage(m)
|
go x.HandleJsonMessage(m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case TextMessage:
|
case TextMessage:
|
||||||
for _, h := range wac.handler {
|
for _, h := range wac.handler {
|
||||||
x, ok := h.(TextMessageHandler)
|
if x, ok := h.(TextMessageHandler); ok {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleTextMessage(m)
|
go x.HandleTextMessage(m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case ImageMessage:
|
case ImageMessage:
|
||||||
for _, h := range wac.handler {
|
for _, h := range wac.handler {
|
||||||
x, ok := h.(ImageMessageHandler)
|
if x, ok := h.(ImageMessageHandler); ok {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleImageMessage(m)
|
go x.HandleImageMessage(m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case VideoMessage:
|
case VideoMessage:
|
||||||
for _, h := range wac.handler {
|
for _, h := range wac.handler {
|
||||||
x, ok := h.(VideoMessageHandler)
|
if x, ok := h.(VideoMessageHandler); ok {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleVideoMessage(m)
|
go x.HandleVideoMessage(m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case AudioMessage:
|
case AudioMessage:
|
||||||
for _, h := range wac.handler {
|
for _, h := range wac.handler {
|
||||||
x, ok := h.(AudioMessageHandler)
|
if x, ok := h.(AudioMessageHandler); ok {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleAudioMessage(m)
|
go x.HandleAudioMessage(m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case DocumentMessage:
|
case DocumentMessage:
|
||||||
for _, h := range wac.handler {
|
for _, h := range wac.handler {
|
||||||
x, ok := h.(DocumentMessageHandler)
|
if x, ok := h.(DocumentMessageHandler); ok {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleDocumentMessage(m)
|
go x.HandleDocumentMessage(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case *proto.WebMessageInfo:
|
||||||
|
for _, h := range wac.handler {
|
||||||
|
if x, ok := h.(RawMessageHandler); ok {
|
||||||
|
go x.HandleRawMessage(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,13 +151,7 @@ func (wac *Conn) dispatch(msg interface{}) {
|
||||||
if con, ok := message.Content.([]interface{}); ok {
|
if con, ok := message.Content.([]interface{}); ok {
|
||||||
for a := range con {
|
for a := range con {
|
||||||
if v, ok := con[a].(*proto.WebMessageInfo); ok {
|
if v, ok := con[a].(*proto.WebMessageInfo); ok {
|
||||||
for _, h := range wac.handler {
|
wac.handle(v)
|
||||||
x, ok := h.(RawMessageHandler)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go x.HandleRawMessage(v)
|
|
||||||
}
|
|
||||||
wac.handle(parseProtoMessage(v))
|
wac.handle(parseProtoMessage(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
vendor/github.com/Rhymen/go-whatsapp/session.go
generated
vendored
16
vendor/github.com/Rhymen/go-whatsapp/session.go
generated
vendored
|
@ -84,6 +84,18 @@ func newInfoFromReq(info map[string]interface{}) *Info {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SetClientName sets the long and short client names that are sent to WhatsApp when logging in and displayed in the
|
||||||
|
WhatsApp Web device list. As the values are only sent when logging in, changing them after logging in is not possible.
|
||||||
|
*/
|
||||||
|
func (wac *Conn) SetClientName(long, short string) error {
|
||||||
|
if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) {
|
||||||
|
return fmt.Errorf("cannot change client name after logging in")
|
||||||
|
}
|
||||||
|
wac.longClientName, wac.shortClientName = long, short
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Login is the function that creates a new whatsapp session and logs you in. If you do not want to scan the qr code
|
Login is the function that creates a new whatsapp session and logs you in. If you do not want to scan the qr code
|
||||||
every time, you should save the returned session and use RestoreSession the next time. Login takes a writable channel
|
every time, you should save the returned session and use RestoreSession the next time. Login takes a writable channel
|
||||||
|
@ -122,7 +134,7 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
|
||||||
|
|
||||||
session.ClientId = base64.StdEncoding.EncodeToString(clientId)
|
session.ClientId = base64.StdEncoding.EncodeToString(clientId)
|
||||||
//oldVersion=8691
|
//oldVersion=8691
|
||||||
login := []interface{}{"admin", "init", []int{0, 3, 225}, []string{"github.com/rhymen/go-whatsapp", "go-whatsapp"}, session.ClientId, true}
|
login := []interface{}{"admin", "init", []int{0, 3, 225}, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true}
|
||||||
loginChan, err := wac.write(login)
|
loginChan, err := wac.write(login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return session, fmt.Errorf("error writing login: %v\n", err)
|
return session, fmt.Errorf("error writing login: %v\n", err)
|
||||||
|
@ -235,7 +247,7 @@ func (wac *Conn) RestoreSession(session Session) (Session, error) {
|
||||||
wac.listener["s1"] = make(chan string, 1)
|
wac.listener["s1"] = make(chan string, 1)
|
||||||
|
|
||||||
//admin init
|
//admin init
|
||||||
init := []interface{}{"admin", "init", []int{0, 3, 225}, []string{"github.com/rhymen/go-whatsapp", "go-whatsapp"}, session.ClientId, true}
|
init := []interface{}{"admin", "init", []int{0, 3, 225}, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true}
|
||||||
initChan, err := wac.write(init)
|
initChan, err := wac.write(init)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wac.session = nil
|
wac.session = nil
|
||||||
|
|
4
vendor/github.com/mattn/go-isatty/.travis.yml
generated
vendored
4
vendor/github.com/mattn/go-isatty/.travis.yml
generated
vendored
|
@ -2,6 +2,10 @@ language: go
|
||||||
go:
|
go:
|
||||||
- tip
|
- tip
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- go get github.com/mattn/goveralls
|
- go get github.com/mattn/goveralls
|
||||||
- go get golang.org/x/tools/cmd/cover
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
|
2
vendor/github.com/mattn/go-isatty/isatty_others.go
generated
vendored
2
vendor/github.com/mattn/go-isatty/isatty_others.go
generated
vendored
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
package isatty
|
package isatty
|
||||||
|
|
||||||
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||||
// terminal. This is also always false on this environment.
|
// terminal. This is also always false on this environment.
|
||||||
func IsCygwinTerminal(fd uintptr) bool {
|
func IsCygwinTerminal(fd uintptr) bool {
|
||||||
return false
|
return false
|
||||||
|
|
18
vendor/maunium.net/go/gomatrix/client.go
generated
vendored
18
vendor/maunium.net/go/gomatrix/client.go
generated
vendored
|
@ -463,7 +463,7 @@ func (cli *Client) SetAvatarURL(url string) (err error) {
|
||||||
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
||||||
func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}) (resp *RespSendEvent, err error) {
|
func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}) (resp *RespSendEvent, err error) {
|
||||||
txnID := txnID()
|
txnID := txnID()
|
||||||
urlPath := cli.BuildURL("rooms", roomID, "send", string(eventType), txnID)
|
urlPath := cli.BuildURL("rooms", roomID, "send", eventType.String(), txnID)
|
||||||
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -472,7 +472,7 @@ func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJ
|
||||||
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
||||||
func (cli *Client) SendMassagedMessageEvent(roomID string, eventType EventType, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
|
func (cli *Client) SendMassagedMessageEvent(roomID string, eventType EventType, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
|
||||||
txnID := txnID()
|
txnID := txnID()
|
||||||
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", string(eventType), txnID}, map[string]string{
|
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", eventType.String(), txnID}, map[string]string{
|
||||||
"ts": strconv.FormatInt(ts, 10),
|
"ts": strconv.FormatInt(ts, 10),
|
||||||
})
|
})
|
||||||
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
||||||
|
@ -482,7 +482,7 @@ func (cli *Client) SendMassagedMessageEvent(roomID string, eventType EventType,
|
||||||
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
|
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
|
||||||
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
||||||
func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
|
func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
|
||||||
urlPath := cli.BuildURL("rooms", roomID, "state", string(eventType), stateKey)
|
urlPath := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey)
|
||||||
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -490,7 +490,7 @@ func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey s
|
||||||
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
|
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
|
||||||
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
|
||||||
func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
|
func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
|
||||||
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", string(eventType), stateKey}, map[string]string{
|
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{
|
||||||
"ts": strconv.FormatInt(ts, 10),
|
"ts": strconv.FormatInt(ts, 10),
|
||||||
})
|
})
|
||||||
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
|
||||||
|
@ -500,7 +500,7 @@ func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, st
|
||||||
// SendText sends an m.room.message event into the given room with a msgtype of m.text
|
// SendText sends an m.room.message event into the given room with a msgtype of m.text
|
||||||
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
|
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
|
||||||
func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
|
func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
|
||||||
return cli.SendMessageEvent(roomID, "m.room.message", Content{
|
return cli.SendMessageEvent(roomID, EventMessage, Content{
|
||||||
MsgType: MsgText,
|
MsgType: MsgText,
|
||||||
Body: text,
|
Body: text,
|
||||||
})
|
})
|
||||||
|
@ -509,7 +509,7 @@ func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
|
||||||
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
|
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
|
||||||
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
|
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
|
||||||
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
|
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
|
||||||
return cli.SendMessageEvent(roomID, "m.room.message", Content{
|
return cli.SendMessageEvent(roomID, EventMessage, Content{
|
||||||
MsgType: MsgImage,
|
MsgType: MsgImage,
|
||||||
Body: body,
|
Body: body,
|
||||||
URL: url,
|
URL: url,
|
||||||
|
@ -519,7 +519,7 @@ func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
|
||||||
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
|
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
|
||||||
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
|
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
|
||||||
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
|
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
|
||||||
return cli.SendMessageEvent(roomID, "m.room.message", Content{
|
return cli.SendMessageEvent(roomID, EventMessage, Content{
|
||||||
MsgType: MsgVideo,
|
MsgType: MsgVideo,
|
||||||
Body: body,
|
Body: body,
|
||||||
URL: url,
|
URL: url,
|
||||||
|
@ -529,7 +529,7 @@ func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
|
||||||
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
|
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
|
||||||
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
|
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
|
||||||
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
|
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
|
||||||
return cli.SendMessageEvent(roomID, "m.room.message", Content{
|
return cli.SendMessageEvent(roomID, EventMessage, Content{
|
||||||
MsgType: MsgNotice,
|
MsgType: MsgNotice,
|
||||||
Body: text,
|
Body: text,
|
||||||
})
|
})
|
||||||
|
@ -622,7 +622,7 @@ func (cli *Client) SetPresence(status string) (err error) {
|
||||||
// the HTTP response body, or return an error.
|
// the HTTP response body, or return an error.
|
||||||
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
|
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
|
||||||
func (cli *Client) StateEvent(roomID string, eventType EventType, stateKey string, outContent interface{}) (err error) {
|
func (cli *Client) StateEvent(roomID string, eventType EventType, stateKey string, outContent interface{}) (err error) {
|
||||||
u := cli.BuildURL("rooms", roomID, "state", string(eventType), stateKey)
|
u := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey)
|
||||||
_, err = cli.MakeRequest("GET", u, nil, outContent)
|
_, err = cli.MakeRequest("GET", u, nil, outContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
82
vendor/maunium.net/go/gomatrix/events.go
generated
vendored
82
vendor/maunium.net/go/gomatrix/events.go
generated
vendored
|
@ -5,28 +5,56 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventType string
|
type EventType struct {
|
||||||
|
Type string
|
||||||
|
IsState bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et *EventType) UnmarshalJSON(data []byte) error {
|
||||||
|
err := json.Unmarshal(data, &et.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch et.Type {
|
||||||
|
case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type,
|
||||||
|
StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateTopic.Type, StatePinnedEvents.Type:
|
||||||
|
et.IsState = true
|
||||||
|
default:
|
||||||
|
et.IsState = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et *EventType) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(&et.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et *EventType) String() string {
|
||||||
|
return et.Type
|
||||||
|
}
|
||||||
|
|
||||||
type MessageType string
|
type MessageType string
|
||||||
|
|
||||||
// State events
|
// State events
|
||||||
const (
|
var (
|
||||||
StateAliases EventType = "m.room.aliases"
|
StateAliases = EventType{"m.room.aliases", true}
|
||||||
StateCanonicalAlias = "m.room.canonical_alias"
|
StateCanonicalAlias = EventType{"m.room.canonical_alias", true}
|
||||||
StateCreate = "m.room.create"
|
StateCreate = EventType{"m.room.create", true}
|
||||||
StateJoinRules = "m.room.join_rules"
|
StateJoinRules = EventType{"m.room.join_rules", true}
|
||||||
StateMember = "m.room.member"
|
StateMember = EventType{"m.room.member", true}
|
||||||
StatePowerLevels = "m.room.power_levels"
|
StatePowerLevels = EventType{"m.room.power_levels", true}
|
||||||
StateRoomName = "m.room.name"
|
StateRoomName = EventType{"m.room.name", true}
|
||||||
StateTopic = "m.room.topic"
|
StateTopic = EventType{"m.room.topic", true}
|
||||||
StateRoomAvatar = "m.room.avatar"
|
StateRoomAvatar = EventType{"m.room.avatar", true}
|
||||||
StatePinnedEvents = "m.room.pinned_events"
|
StatePinnedEvents = EventType{"m.room.pinned_events", true}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message events
|
// Message events
|
||||||
const (
|
var (
|
||||||
EventRedaction EventType = "m.room.redaction"
|
EventRedaction = EventType{"m.room.redaction", false}
|
||||||
EventMessage = "m.room.message"
|
EventMessage = EventType{"m.room.message", false}
|
||||||
EventSticker = "m.sticker"
|
EventSticker = EventType{"m.sticker", false}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Msgtypes
|
// Msgtypes
|
||||||
|
@ -183,7 +211,7 @@ type PowerLevels struct {
|
||||||
UsersDefault int `json:"users_default,omitempty"`
|
UsersDefault int `json:"users_default,omitempty"`
|
||||||
|
|
||||||
eventsLock sync.RWMutex `json:"-"`
|
eventsLock sync.RWMutex `json:"-"`
|
||||||
Events map[EventType]int `json:"events,omitempty"`
|
Events map[string]int `json:"events,omitempty"`
|
||||||
EventsDefault int `json:"events_default,omitempty"`
|
EventsDefault int `json:"events_default,omitempty"`
|
||||||
|
|
||||||
StateDefaultPtr *int `json:"state_default,omitempty"`
|
StateDefaultPtr *int `json:"state_default,omitempty"`
|
||||||
|
@ -258,12 +286,12 @@ func (pl *PowerLevels) EnsureUserLevel(userID string, level int) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pl *PowerLevels) GetEventLevel(eventType EventType, isState bool) int {
|
func (pl *PowerLevels) GetEventLevel(eventType EventType) int {
|
||||||
pl.eventsLock.RLock()
|
pl.eventsLock.RLock()
|
||||||
defer pl.eventsLock.RUnlock()
|
defer pl.eventsLock.RUnlock()
|
||||||
level, ok := pl.Events[eventType]
|
level, ok := pl.Events[eventType.String()]
|
||||||
if !ok {
|
if !ok {
|
||||||
if isState {
|
if eventType.IsState {
|
||||||
return pl.StateDefault()
|
return pl.StateDefault()
|
||||||
}
|
}
|
||||||
return pl.EventsDefault
|
return pl.EventsDefault
|
||||||
|
@ -271,20 +299,20 @@ func (pl *PowerLevels) GetEventLevel(eventType EventType, isState bool) int {
|
||||||
return level
|
return level
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pl *PowerLevels) SetEventLevel(eventType EventType, isState bool, level int) {
|
func (pl *PowerLevels) SetEventLevel(eventType EventType, level int) {
|
||||||
pl.eventsLock.Lock()
|
pl.eventsLock.Lock()
|
||||||
defer pl.eventsLock.Unlock()
|
defer pl.eventsLock.Unlock()
|
||||||
if (isState && level == pl.StateDefault()) || (!isState && level == pl.EventsDefault) {
|
if (eventType.IsState && level == pl.StateDefault()) || (!eventType.IsState && level == pl.EventsDefault) {
|
||||||
delete(pl.Events, eventType)
|
delete(pl.Events, eventType.String())
|
||||||
} else {
|
} else {
|
||||||
pl.Events[eventType] = level
|
pl.Events[eventType.String()] = level
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pl *PowerLevels) EnsureEventLevel(eventType EventType, isState bool, level int) bool {
|
func (pl *PowerLevels) EnsureEventLevel(eventType EventType, level int) bool {
|
||||||
existingLevel := pl.GetEventLevel(eventType, isState)
|
existingLevel := pl.GetEventLevel(eventType)
|
||||||
if existingLevel != level {
|
if existingLevel != level {
|
||||||
pl.SetEventLevel(eventType, isState, level)
|
pl.SetEventLevel(eventType, level)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
27
vendor/maunium.net/go/mautrix-appservice/appservice.go
generated
vendored
27
vendor/maunium.net/go/mautrix-appservice/appservice.go
generated
vendored
|
@ -2,17 +2,19 @@ package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"maunium.net/go/maulogger"
|
|
||||||
"strings"
|
|
||||||
"net/http"
|
|
||||||
"errors"
|
"errors"
|
||||||
"maunium.net/go/gomatrix"
|
"maunium.net/go/gomatrix"
|
||||||
|
"maunium.net/go/maulogger"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventChannelSize is the size for the Events channel in Appservice instances.
|
// EventChannelSize is the size for the Events channel in Appservice instances.
|
||||||
|
@ -263,15 +265,24 @@ func CreateLogConfig() LogConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileFormatData struct {
|
||||||
|
Date string
|
||||||
|
Index int
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileFormat returns a mauLogger-compatible logger file format based on the data in the struct.
|
// GetFileFormat returns a mauLogger-compatible logger file format based on the data in the struct.
|
||||||
func (lc LogConfig) GetFileFormat() maulogger.LoggerFileFormat {
|
func (lc LogConfig) GetFileFormat() maulogger.LoggerFileFormat {
|
||||||
path := lc.FileNameFormat
|
os.MkdirAll(lc.Directory, 0700)
|
||||||
if len(lc.Directory) > 0 {
|
path := filepath.Join(lc.Directory, lc.FileNameFormat)
|
||||||
path = lc.Directory + "/" + path
|
tpl, _ := template.New("fileformat").Parse(path)
|
||||||
}
|
|
||||||
|
|
||||||
return func(now string, i int) string {
|
return func(now string, i int) string {
|
||||||
return fmt.Sprintf(path, now, i)
|
var buf strings.Builder
|
||||||
|
tpl.Execute(&buf, FileFormatData{
|
||||||
|
Date: now,
|
||||||
|
Index: i,
|
||||||
|
})
|
||||||
|
return buf.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
vendor/maunium.net/go/mautrix-appservice/generator.go
generated
vendored
8
vendor/maunium.net/go/mautrix-appservice/generator.go
generated
vendored
|
@ -44,14 +44,16 @@ func GenerateRegistration(asName, botName string, reserveRooms, reserveUsers boo
|
||||||
boldCyan.Println("Generating appservice config and registration.")
|
boldCyan.Println("Generating appservice config and registration.")
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
registration := CreateRegistration()
|
||||||
|
config := Create()
|
||||||
|
registration.RateLimited = false
|
||||||
|
|
||||||
name, err := readString(reader, "Enter name for appservice", asName)
|
name, err := readString(reader, "Enter name for appservice", asName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to read user Input:", err)
|
fmt.Println("Failed to read user Input:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
registration := CreateRegistration(name)
|
registration.ID = name
|
||||||
config := Create()
|
|
||||||
registration.RateLimited = false
|
|
||||||
|
|
||||||
registration.SenderLocalpart, err = readString(reader, "Enter bot username", botName)
|
registration.SenderLocalpart, err = readString(reader, "Enter bot username", botName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
5
vendor/maunium.net/go/mautrix-appservice/http.go
generated
vendored
5
vendor/maunium.net/go/mautrix-appservice/http.go
generated
vendored
|
@ -1,11 +1,11 @@
|
||||||
package appservice
|
package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"context"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -106,7 +106,6 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, event := range eventList.Events {
|
for _, event := range eventList.Events {
|
||||||
as.Log.Debugln("Received event", event.ID)
|
|
||||||
as.UpdateState(event)
|
as.UpdateState(event)
|
||||||
as.Events <- event
|
as.Events <- event
|
||||||
}
|
}
|
||||||
|
|
16
vendor/maunium.net/go/mautrix-appservice/intent.go
generated
vendored
16
vendor/maunium.net/go/mautrix-appservice/intent.go
generated
vendored
|
@ -201,19 +201,19 @@ func (intent *IntentAPI) RedactEvent(roomID, eventID string, req *gomatrix.ReqRe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (intent *IntentAPI) SetRoomName(roomID, roomName string) (*gomatrix.RespSendEvent, error) {
|
func (intent *IntentAPI) SetRoomName(roomID, roomName string) (*gomatrix.RespSendEvent, error) {
|
||||||
return intent.SendStateEvent(roomID, "m.room.name", "", map[string]interface{}{
|
return intent.SendStateEvent(roomID, gomatrix.StateRoomName, "", map[string]interface{}{
|
||||||
"name": roomName,
|
"name": roomName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (intent *IntentAPI) SetRoomAvatar(roomID, avatarURL string) (*gomatrix.RespSendEvent, error) {
|
func (intent *IntentAPI) SetRoomAvatar(roomID, avatarURL string) (*gomatrix.RespSendEvent, error) {
|
||||||
return intent.SendStateEvent(roomID, "m.room.avatar", "", map[string]interface{}{
|
return intent.SendStateEvent(roomID, gomatrix.StateRoomAvatar, "", map[string]interface{}{
|
||||||
"url": avatarURL,
|
"url": avatarURL,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (intent *IntentAPI) SetRoomTopic(roomID, topic string) (*gomatrix.RespSendEvent, error) {
|
func (intent *IntentAPI) SetRoomTopic(roomID, topic string) (*gomatrix.RespSendEvent, error) {
|
||||||
return intent.SendStateEvent(roomID, "m.room.topic", "", map[string]interface{}{
|
return intent.SendStateEvent(roomID, gomatrix.StateTopic, "", map[string]interface{}{
|
||||||
"topic": topic,
|
"topic": topic,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -231,3 +231,13 @@ func (intent *IntentAPI) SetAvatarURL(avatarURL string) error {
|
||||||
}
|
}
|
||||||
return intent.Client.SetAvatarURL(avatarURL)
|
return intent.Client.SetAvatarURL(avatarURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (intent *IntentAPI) EnsureInvited(roomID, userID string) error {
|
||||||
|
if !intent.as.StateStore.IsInvited(roomID, userID) {
|
||||||
|
_, err := intent.Client.InviteUser(roomID, &gomatrix.ReqInviteUser{
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
2
vendor/maunium.net/go/mautrix-appservice/protocol.go
generated
vendored
2
vendor/maunium.net/go/mautrix-appservice/protocol.go
generated
vendored
|
@ -2,8 +2,8 @@ package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
|
||||||
"maunium.net/go/gomatrix"
|
"maunium.net/go/gomatrix"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventList contains a list of events.
|
// EventList contains a list of events.
|
||||||
|
|
2
vendor/maunium.net/go/mautrix-appservice/registration.go
generated
vendored
2
vendor/maunium.net/go/mautrix-appservice/registration.go
generated
vendored
|
@ -21,7 +21,7 @@ type Registration struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRegistration creates a Registration with random appservice and homeserver tokens.
|
// CreateRegistration creates a Registration with random appservice and homeserver tokens.
|
||||||
func CreateRegistration(name string) *Registration {
|
func CreateRegistration() *Registration {
|
||||||
return &Registration{
|
return &Registration{
|
||||||
AppToken: RandomString(64),
|
AppToken: RandomString(64),
|
||||||
ServerToken: RandomString(64),
|
ServerToken: RandomString(64),
|
||||||
|
|
41
vendor/maunium.net/go/mautrix-appservice/statestore.go
generated
vendored
41
vendor/maunium.net/go/mautrix-appservice/statestore.go
generated
vendored
|
@ -15,19 +15,23 @@ type StateStore interface {
|
||||||
SetTyping(roomID, userID string, timeout int64)
|
SetTyping(roomID, userID string, timeout int64)
|
||||||
|
|
||||||
IsInRoom(roomID, userID string) bool
|
IsInRoom(roomID, userID string) bool
|
||||||
|
IsInvited(roomID, userID string) bool
|
||||||
|
IsMembership(roomID, userID string, allowedMemberships ...string) bool
|
||||||
SetMembership(roomID, userID, membership string)
|
SetMembership(roomID, userID, membership string)
|
||||||
|
|
||||||
SetPowerLevels(roomID string, levels *gomatrix.PowerLevels)
|
SetPowerLevels(roomID string, levels *gomatrix.PowerLevels)
|
||||||
GetPowerLevels(roomID string) *gomatrix.PowerLevels
|
GetPowerLevels(roomID string) *gomatrix.PowerLevels
|
||||||
GetPowerLevel(roomID, userID string) int
|
GetPowerLevel(roomID, userID string) int
|
||||||
GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType, isState bool) int
|
GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType) int
|
||||||
HasPowerLevel(roomID, userID string, eventType gomatrix.EventType, isState bool) bool
|
HasPowerLevel(roomID, userID string, eventType gomatrix.EventType) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *AppService) UpdateState(evt *gomatrix.Event) {
|
func (as *AppService) UpdateState(evt *gomatrix.Event) {
|
||||||
switch evt.Type {
|
switch evt.Type {
|
||||||
case gomatrix.StateMember:
|
case gomatrix.StateMember:
|
||||||
as.StateStore.SetMembership(evt.RoomID, evt.GetStateKey(), evt.Content.Membership)
|
as.StateStore.SetMembership(evt.RoomID, evt.GetStateKey(), evt.Content.Membership)
|
||||||
|
case gomatrix.StatePowerLevels:
|
||||||
|
as.StateStore.SetPowerLevels(evt.RoomID, &evt.Content.PowerLevels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +130,21 @@ func (store *BasicStateStore) GetMembership(roomID, userID string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *BasicStateStore) IsInRoom(roomID, userID string) bool {
|
func (store *BasicStateStore) IsInRoom(roomID, userID string) bool {
|
||||||
return store.GetMembership(roomID, userID) == "join"
|
return store.IsMembership(roomID, userID, "join")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *BasicStateStore) IsInvited(roomID, userID string) bool {
|
||||||
|
return store.IsMembership(roomID, userID, "join", "invite")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *BasicStateStore) IsMembership(roomID, userID string, allowedMemberships ...string) bool {
|
||||||
|
membership := store.GetMembership(roomID, userID)
|
||||||
|
for _, allowedMembership := range allowedMemberships {
|
||||||
|
if allowedMembership == membership {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *BasicStateStore) SetMembership(roomID, userID, membership string) {
|
func (store *BasicStateStore) SetMembership(roomID, userID, membership string) {
|
||||||
|
@ -160,19 +178,10 @@ func (store *BasicStateStore) GetPowerLevel(roomID, userID string) int {
|
||||||
return store.GetPowerLevels(roomID).GetUserLevel(userID)
|
return store.GetPowerLevels(roomID).GetUserLevel(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *BasicStateStore) GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType, isState bool) int {
|
func (store *BasicStateStore) GetPowerLevelRequirement(roomID string, eventType gomatrix.EventType) int {
|
||||||
levels := store.GetPowerLevels(roomID)
|
return store.GetPowerLevels(roomID).GetEventLevel(eventType)
|
||||||
switch eventType {
|
|
||||||
case "kick":
|
|
||||||
return levels.Kick()
|
|
||||||
case "invite":
|
|
||||||
return levels.Invite()
|
|
||||||
case "redact":
|
|
||||||
return levels.Redact()
|
|
||||||
}
|
|
||||||
return levels.GetEventLevel(eventType, isState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *BasicStateStore) HasPowerLevel(roomID, userID string, eventType gomatrix.EventType, isState bool) bool {
|
func (store *BasicStateStore) HasPowerLevel(roomID, userID string, eventType gomatrix.EventType) bool {
|
||||||
return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType, isState)
|
return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue