Add avatars, room topics and message deduplication

This commit is contained in:
Tulir Asokan 2018-08-23 01:12:26 +03:00
parent c1f8d264f4
commit 329bc9d8ce
8 changed files with 481 additions and 44 deletions

View File

@ -26,9 +26,10 @@ type Database struct {
*sql.DB *sql.DB
log log.Logger log log.Logger
User *UserQuery User *UserQuery
Portal *PortalQuery Portal *PortalQuery
Puppet *PuppetQuery Puppet *PuppetQuery
Message *MessageQuery
} }
func New(file string) (*Database, error) { func New(file string) (*Database, error) {
@ -53,6 +54,10 @@ func New(file string) (*Database, error) {
db: db, db: db,
log: db.log.Sub("Puppet"), log: db.log.Sub("Puppet"),
} }
db.Message = &MessageQuery{
db: db,
log: db.log.Sub("Message"),
}
return db, nil return db, nil
} }
@ -69,6 +74,10 @@ func (db *Database) CreateTables() error {
if err != nil { if err != nil {
return err return err
} }
err = db.Message.CreateTable()
if err != nil {
return err
}
return nil return nil
} }

111
database/message.go Normal file
View File

@ -0,0 +1,111 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types"
"database/sql"
)
type MessageQuery struct {
db *Database
log log.Logger
}
func (mq *MessageQuery) CreateTable() error {
_, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message (
owner VARCHAR(255),
jid VARCHAR(255),
mxid VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (owner, jid),
FOREIGN KEY (owner) REFERENCES user(mxid)
)`)
return err
}
func (mq *MessageQuery) New() *Message {
return &Message{
db: mq.db,
log: mq.log,
}
}
func (mq *MessageQuery) GetAll(owner types.MatrixUserID) (messages []*Message) {
rows, err := mq.db.Query("SELECT * FROM message WHERE owner=?", owner)
if err != nil || rows == nil {
return nil
}
defer rows.Close()
for rows.Next() {
messages = append(messages, mq.New().Scan(rows))
}
return
}
func (mq *MessageQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppMessageID) *Message {
return mq.get("SELECT * FROM message WHERE jid=?", jid)
}
func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message {
return mq.get("SELECT * FROM message WHERE mxid=?", mxid)
}
func (mq *MessageQuery) get(query string, args ...interface{}) *Message {
row := mq.db.QueryRow(query, args...)
if row == nil {
return nil
}
return mq.New().Scan(row)
}
type Message struct {
db *Database
log log.Logger
Owner types.MatrixUserID
JID types.WhatsAppMessageID
MXID types.MatrixEventID
}
func (msg *Message) Scan(row Scannable) *Message {
err := row.Scan(&msg.Owner, &msg.JID, &msg.MXID)
if err != nil {
if err != sql.ErrNoRows {
msg.log.Fatalln("Database scan failed:", err)
}
return nil
}
return msg
}
func (msg *Message) Insert() error {
_, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?)", msg.Owner, msg.JID, msg.MXID)
if err != nil {
msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err)
}
return err
}
func (msg *Message) Update() error {
_, err := msg.db.Exec("UPDATE portal SET mxid=? WHERE owner=? AND jid=?", msg.MXID, msg.Owner, msg.JID)
if err != nil {
msg.log.Warnfln("Failed to update %s->%s: %v", msg.Owner, msg.JID, err)
}
return err
}

View File

@ -31,7 +31,11 @@ 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(255),
owner VARCHAR(255), owner VARCHAR(255),
mxid VARCHAR(255) NOT NULL UNIQUE, mxid VARCHAR(255) UNIQUE,
name VARCHAR(255),
topic VARCHAR(255),
avatar VARCHAR(255),
PRIMARY KEY (jid, owner), PRIMARY KEY (jid, owner),
FOREIGN KEY (owner) REFERENCES user(mxid) FOREIGN KEY (owner) REFERENCES user(mxid)
@ -83,11 +87,12 @@ type Portal struct {
Owner types.MatrixUserID Owner types.MatrixUserID
Name string Name string
Topic string
Avatar string Avatar string
} }
func (portal *Portal) Scan(row Scannable) *Portal { func (portal *Portal) Scan(row Scannable) *Portal {
err := row.Scan(&portal.JID, &portal.Owner, &portal.MXID) err := row.Scan(&portal.JID, &portal.Owner, &portal.MXID, &portal.Name, &portal.Topic, &portal.Avatar)
if err != nil { if err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
portal.log.Fatalln("Database scan failed:", err) portal.log.Fatalln("Database scan failed:", err)
@ -98,15 +103,25 @@ func (portal *Portal) Scan(row Scannable) *Portal {
} }
func (portal *Portal) Insert() error { func (portal *Portal) Insert() error {
_, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?)", portal.JID, portal.Owner, portal.MXID) var mxid *string
if len(portal.MXID) > 0 {
mxid = &portal.MXID
}
_, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?, ?, ?, ?)",
portal.JID, portal.Owner, mxid, portal.Name, portal.Topic, portal.Avatar)
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 insert %s->%s: %v", portal.JID, portal.Owner, err)
} }
return err return err
} }
func (portal *Portal) Update() error { func (portal *Portal) Update() error {
_, err := portal.db.Exec("UPDATE portal SET mxid=? WHERE jid=? AND owner=?", portal.MXID, portal.JID, portal.Owner) var mxid *string
if len(portal.MXID) > 0 {
mxid = &portal.MXID
}
_, err := portal.db.Exec("UPDATE portal SET mxid=?, name=?, topic=?, avatar=? WHERE jid=? AND owner=?",
mxid, portal.Name, portal.Topic, portal.Avatar, portal.JID, portal.Owner)
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->%s: %v", portal.JID, portal.Owner, err)
} }

186
portal.go
View File

@ -25,9 +25,14 @@ import (
"strings" "strings"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"sync"
"net/http"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
user.portalsLock.Lock()
defer user.portalsLock.Unlock()
portal, ok := user.portalsByMXID[mxid] portal, ok := user.portalsByMXID[mxid]
if !ok { if !ok {
dbPortal := user.bridge.DB.Portal.GetByMXID(mxid) dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
@ -44,6 +49,8 @@ func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
} }
func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal { func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
user.portalsLock.Lock()
defer user.portalsLock.Unlock()
portal, ok := user.portalsByJID[jid] portal, ok := user.portalsByJID[jid]
if !ok { if !ok {
dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid) dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid)
@ -63,6 +70,8 @@ func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
} }
func (user *User) GetAllPortals() []*Portal { func (user *User) GetAllPortals() []*Portal {
user.portalsLock.Lock()
defer user.portalsLock.Unlock()
dbPortals := user.bridge.DB.Portal.GetAll(user.ID) dbPortals := user.bridge.DB.Portal.GetAll(user.ID)
output := make([]*Portal, len(dbPortals)) output := make([]*Portal, len(dbPortals))
for index, dbPortal := range dbPortals { for index, dbPortal := range dbPortals {
@ -94,10 +103,87 @@ type Portal struct {
user *User user *User
bridge *Bridge bridge *Bridge
log log.Logger log log.Logger
roomCreateLock sync.Mutex
}
func (portal *Portal) SyncParticipants(metadata *whatsapp_ext.GroupInfo) {
for _, participant := range metadata.Participants {
intent := portal.user.GetPuppetByJID(participant.JID).Intent()
intent.EnsureJoined(portal.MXID)
}
}
func (portal *Portal) UpdateAvatar() bool {
avatar, err := portal.user.Conn.GetProfilePicThumb(portal.JID)
if err != nil {
portal.log.Errorln(err)
return false
}
if portal.Avatar == avatar.Tag {
return false
}
data, err := avatar.DownloadBytes()
if err != nil {
portal.log.Errorln("Failed to download avatar:", err)
return false
}
mime := http.DetectContentType(data)
resp, err := portal.MainIntent().UploadBytes(data, mime)
if err != nil {
portal.log.Errorln("Failed to upload avatar:", err)
return false
}
_, err = portal.MainIntent().SetRoomAvatar(portal.MXID, resp.ContentURI)
if err != nil {
portal.log.Warnln("Failed to set room topic:", err)
return false
}
portal.Avatar = avatar.Tag
return true
}
func (portal *Portal) UpdateName(metadata *whatsapp_ext.GroupInfo) bool {
if portal.Name != metadata.Name {
_, err := portal.MainIntent().SetRoomName(portal.MXID, metadata.Name)
if err == nil {
portal.Name = metadata.Name
return true
}
portal.log.Warnln("Failed to set room name:", err)
}
return false
}
func (portal *Portal) UpdateTopic(metadata *whatsapp_ext.GroupInfo) bool {
if portal.Topic != metadata.Topic {
_, err := portal.MainIntent().SetRoomTopic(portal.MXID, metadata.Topic)
if err == nil {
portal.Topic = metadata.Topic
return true
}
portal.log.Warnln("Failed to set room topic:", err)
}
return false
}
func (portal *Portal) UpdateMetadata() bool {
metadata, err := portal.user.Conn.GetGroupMetaData(portal.JID)
if err != nil {
portal.log.Errorln(err)
return false
}
portal.SyncParticipants(metadata)
update := false
update = portal.UpdateName(metadata) || update
update = portal.UpdateTopic(metadata) || update
return update
} }
func (portal *Portal) Sync(contact whatsapp.Contact) { func (portal *Portal) Sync(contact whatsapp.Contact) {
if len(portal.MXID) == 0 { if len(portal.MXID) == 0 {
if !portal.IsPrivateChat() { if !portal.IsPrivateChat() {
portal.Name = contact.Name portal.Name = contact.Name
@ -109,23 +195,27 @@ func (portal *Portal) Sync(contact whatsapp.Contact) {
} }
} }
if !portal.IsPrivateChat() && portal.Name != contact.Name { if portal.IsPrivateChat() {
portal.Name = contact.Name return
}
update := false
update = portal.UpdateMetadata() || update
update = portal.UpdateAvatar() || update
if update {
portal.Update() portal.Update()
// TODO add SetRoomName function to intent API
portal.MainIntent().SendStateEvent(portal.MXID, "m.room.name", "", map[string]interface{}{
"name": portal.Name,
})
} }
} }
func (portal *Portal) CreateMatrixRoom() error { func (portal *Portal) CreateMatrixRoom() error {
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
if len(portal.MXID) > 0 { if len(portal.MXID) > 0 {
return nil return nil
} }
name := portal.Name name := portal.Name
topic := "" topic := portal.Topic
isPrivateChat := false isPrivateChat := false
if strings.HasSuffix(portal.JID, "s.whatsapp.net") { if strings.HasSuffix(portal.JID, "s.whatsapp.net") {
puppet := portal.user.GetPuppetByJID(portal.JID) puppet := portal.user.GetPuppetByJID(portal.JID)
@ -160,28 +250,67 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI {
return portal.bridge.AppService.BotIntent() return portal.bridge.AppService.BotIntent()
} }
func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) { func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool {
portal.CreateMatrixRoom() msg := portal.bridge.DB.Message.GetByJID(portal.Owner, id)
var intent *appservice.IntentAPI if msg != nil {
if portal.IsPrivateChat() { portal.log.Debugln("Ignoring duplicate message", id)
intent = portal.MainIntent() return true
} else {
portal.log.Debugln("Received group text message:", message)
return
} }
resp, err := intent.SendText(portal.MXID, message.Text) return false
portal.log.Debugln("Handled message ", message, "->", resp, err)
} }
func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), msgID, mime, caption string) { func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) {
portal.CreateMatrixRoom() msg := portal.bridge.DB.Message.New()
var intent *appservice.IntentAPI msg.Owner = portal.Owner
if portal.IsPrivateChat() { msg.JID = jid
intent = portal.MainIntent() msg.MXID = mxid
} else { msg.Insert()
portal.log.Debugln("Received group media message:", msgID) }
func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.IntentAPI {
if info.FromMe {
portal.log.Debugln("Unhandled message from me:", info.Id)
return nil
} else if portal.IsPrivateChat() {
return portal.MainIntent()
}
puppet := portal.user.GetPuppetByJID(info.SenderJid)
return puppet.Intent()
}
func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
if portal.IsDuplicate(message.Info.Id) {
return return
} }
portal.CreateMatrixRoom()
intent := portal.GetMessageIntent(message.Info)
if intent == nil {
return
}
resp, err := intent.SendText(portal.MXID, message.Text)
if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
return
}
portal.MarkHandled(message.Info.Id, resp.EventID)
portal.log.Debugln("Handled message", message.Info.Id, "->", resp.EventID)
}
func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), info whatsapp.MessageInfo, mime, caption string) {
if portal.IsDuplicate(info.Id) {
return
}
portal.CreateMatrixRoom()
intent := portal.GetMessageIntent(info)
if intent == nil {
return
}
img, err := download() img, err := download()
if err != nil { if err != nil {
portal.log.Errorln("Failed to download media:", err) portal.log.Errorln("Failed to download media:", err)
@ -193,7 +322,12 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), msgID,
return return
} }
resp, err := intent.SendImage(portal.MXID, caption, uploaded.ContentURI) resp, err := intent.SendImage(portal.MXID, caption, uploaded.ContentURI)
portal.log.Debugln("Handled message ", msgID, "->", resp, err) if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err)
return
}
portal.MarkHandled(info.Id, resp.EventID)
portal.log.Debugln("Handled message", info.Id, "->", resp.EventID)
} }
func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {

View File

@ -25,6 +25,7 @@ import (
"strings" "strings"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"net/http"
) )
const puppetJIDStrippedSuffix = "@s.whatsapp.net" const puppetJIDStrippedSuffix = "@s.whatsapp.net"
@ -74,6 +75,8 @@ func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet {
} }
func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet { func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
user.puppetsLock.Lock()
defer user.puppetsLock.Unlock()
puppet, ok := user.puppets[jid] puppet, ok := user.puppets[jid]
if !ok { if !ok {
dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID) dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID)
@ -90,6 +93,8 @@ func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
} }
func (user *User) GetAllPuppets() []*Puppet { func (user *User) GetAllPuppets() []*Puppet {
user.puppetsLock.Lock()
defer user.puppetsLock.Unlock()
dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID) dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID)
output := make([]*Puppet, len(dbPuppets)) output := make([]*Puppet, len(dbPuppets))
for index, dbPuppet := range dbPuppets { for index, dbPuppet := range dbPuppets {
@ -134,13 +139,50 @@ func (puppet *Puppet) Intent() *appservice.IntentAPI {
return puppet.bridge.AppService.Intent(puppet.MXID) return puppet.bridge.AppService.Intent(puppet.MXID)
} }
func (puppet *Puppet) UpdateAvatar() bool {
avatar, err := puppet.user.Conn.GetProfilePicThumb(puppet.JID)
if err != nil {
puppet.log.Errorln(err)
return false
}
if avatar.Tag == puppet.Avatar {
return false
}
data, err := avatar.DownloadBytes()
if err != nil {
puppet.log.Errorln("Failed to download avatar:", err)
return false
}
mime := http.DetectContentType(data)
resp, err := puppet.Intent().UploadBytes(data, mime)
if err != nil {
puppet.log.Errorln("Failed to upload avatar:", err)
return false
}
puppet.Intent().SetAvatarURL(resp.ContentURI)
puppet.Avatar = avatar.Tag
return true
}
func (puppet *Puppet) Sync(contact whatsapp.Contact) { func (puppet *Puppet) Sync(contact whatsapp.Contact) {
puppet.Intent().EnsureRegistered() puppet.Intent().EnsureRegistered()
newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact) newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
if puppet.Displayname != newName { if puppet.Displayname != newName {
puppet.Displayname = newName err := puppet.Intent().SetDisplayName(newName)
if err == nil {
puppet.Displayname = newName
puppet.Update()
} else {
puppet.log.Warnln("Failed to set display name:", err)
}
}
if puppet.UpdateAvatar() {
puppet.Update() puppet.Update()
puppet.Intent().SetDisplayName(puppet.Displayname)
} }
} }

View File

@ -19,8 +19,14 @@ package types
// WhatsAppID is a WhatsApp JID. // WhatsAppID is a WhatsApp JID.
type WhatsAppID = string type WhatsAppID = string
// WhatsAppMessageID is the internal ID of a WhatsApp message.
type WhatsAppMessageID = string
// MatrixUserID is the ID of a Matrix user. // MatrixUserID is the ID of a Matrix user.
type MatrixUserID = string type MatrixUserID = string
// MatrixRoomID is the internal room ID of a Matrix room. // MatrixRoomID is the internal room ID of a Matrix room.
type MatrixRoomID = string type MatrixRoomID = string
// MatrixEventID is the internal ID of a Matrix event.
type MatrixEventID = string

20
user.go
View File

@ -25,18 +25,22 @@ import (
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"strings" "strings"
"encoding/json" "encoding/json"
"sync"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
type User struct { type User struct {
*database.User *database.User
Conn *whatsapp.Conn Conn *whatsapp_ext.ExtendedConn
bridge *Bridge bridge *Bridge
log log.Logger log log.Logger
portalsByMXID map[types.MatrixRoomID]*Portal portalsByMXID map[types.MatrixRoomID]*Portal
portalsByJID map[types.WhatsAppID]*Portal portalsByJID map[types.WhatsAppID]*Portal
portalsLock sync.Mutex
puppets map[types.WhatsAppID]*Puppet puppets map[types.WhatsAppID]*Puppet
puppetsLock sync.Mutex
} }
func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User { func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
@ -115,12 +119,12 @@ func (user *User) Connect(evenIfNoSession bool) bool {
return false return false
} }
user.log.Debugln("Connecting to WhatsApp") user.log.Debugln("Connecting to WhatsApp")
var err error conn, err := whatsapp.NewConn(20 * time.Second)
user.Conn, err = whatsapp.NewConn(20 * time.Second)
if err != nil { if err != nil {
user.log.Errorln("Failed to connect to WhatsApp:", err) user.log.Errorln("Failed to connect to WhatsApp:", err)
return false return false
} }
user.Conn = whatsapp_ext.ExtendConn(conn)
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()
@ -192,7 +196,7 @@ func (user *User) Sync() {
} }
if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") { if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") {
// Don't bridge yet // No messages sent -> don't bridge
continue continue
} }
@ -212,15 +216,15 @@ func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
} }
func (user *User) HandleImageMessage(message whatsapp.ImageMessage) { func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
user.log.Debugln("Received image message:", message) // user.log.Debugln("Received image message:", message)
portal := user.GetPortalByJID(message.Info.RemoteJid) portal := user.GetPortalByJID(message.Info.RemoteJid)
portal.HandleMediaMessage(message.Download, message.Info.Id, message.Type, message.Caption) portal.HandleMediaMessage(message.Download, message.Info, message.Type, message.Caption)
} }
func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
user.log.Debugln("Received video message:", message) // user.log.Debugln("Received video message:", message)
portal := user.GetPortalByJID(message.Info.RemoteJid) portal := user.GetPortalByJID(message.Info.RemoteJid)
portal.HandleMediaMessage(message.Download, message.Info.Id, message.Type, message.Caption) portal.HandleMediaMessage(message.Download, message.Info, message.Type, message.Caption)
} }
func (user *User) HandleJsonMessage(message string) { func (user *User) HandleJsonMessage(message string) {

116
whatsapp-ext/whatsapp.go Normal file
View File

@ -0,0 +1,116 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package whatsapp_ext
import (
"fmt"
"encoding/json"
"github.com/Rhymen/go-whatsapp"
"net/http"
"io/ioutil"
"io"
"strings"
)
type ExtendedConn struct {
*whatsapp.Conn
}
func ExtendConn(conn *whatsapp.Conn) *ExtendedConn {
return &ExtendedConn{
Conn: conn,
}
}
type GroupInfo struct {
JID string `json:"jid"`
OwnerJID string `json:"owner"`
Name string `json:"subject"`
NameSetTime int64 `json:"subjectTime"`
NameSetBy string `json:"subjectOwner"`
Topic string `json:"desc"`
TopicID string `json:"descId"`
TopicSetAt int64 `json:"descTime"`
TopicSetBy string `json:"descOwner"`
GroupCreated int64 `json:"creation"`
Participants []struct {
JID string `json:"id"`
IsAdmin bool `json:"isAdmin"`
IsSuperAdmin bool `json:"isSuperAdmin"`
} `json:"participants"`
}
func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) {
data, err := ext.Conn.GetGroupMetaData(jid)
if err != nil {
return nil, fmt.Errorf("failed to get group metadata: %v", err)
}
content := <-data
fmt.Println("GROUP METADATA", content)
info := &GroupInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal group metadata: %v", err)
}
for index, participant := range info.Participants {
info.Participants[index].JID = strings.Replace(participant.JID, "@c.us", "@s.whatsapp.net", 1)
}
return info, nil
}
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
}
func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) {
resp, err := http.Get(ppi.URL)
if err != nil {
return nil, err
}
return resp.Body, nil
}
func (ppi *ProfilePicInfo) DownloadBytes() ([]byte, error) {
body, err := ppi.Download()
if err != nil {
return nil, err
}
defer body.Close()
data, err := ioutil.ReadAll(body)
return data, err
}
func (ext *ExtendedConn) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
data, err := ext.Conn.GetProfilePicThumb(jid)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}