2019-05-30 10:20:56 +00:00
|
|
|
|
//Package whatsapp provides a developer API to interact with the WhatsAppWeb-Servers.
|
|
|
|
|
package whatsapp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math/rand"
|
|
|
|
|
"net/http"
|
2019-10-26 23:45:57 +00:00
|
|
|
|
"net/url"
|
2019-05-30 10:20:56 +00:00
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type metric byte
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
debugLog metric = iota + 1
|
|
|
|
|
queryResume
|
|
|
|
|
queryReceipt
|
|
|
|
|
queryMedia
|
|
|
|
|
queryChat
|
|
|
|
|
queryContacts
|
|
|
|
|
queryMessages
|
|
|
|
|
presence
|
|
|
|
|
presenceSubscribe
|
|
|
|
|
group
|
|
|
|
|
read
|
|
|
|
|
chat
|
|
|
|
|
received
|
|
|
|
|
pic
|
|
|
|
|
status
|
|
|
|
|
message
|
|
|
|
|
queryActions
|
|
|
|
|
block
|
|
|
|
|
queryGroup
|
|
|
|
|
queryPreview
|
|
|
|
|
queryEmoji
|
|
|
|
|
queryMessageInfo
|
|
|
|
|
spam
|
|
|
|
|
querySearch
|
|
|
|
|
queryIdentity
|
|
|
|
|
queryUrl
|
|
|
|
|
profile
|
|
|
|
|
contact
|
|
|
|
|
queryVcard
|
|
|
|
|
queryStatus
|
|
|
|
|
queryStatusUpdate
|
|
|
|
|
privacyStatus
|
|
|
|
|
queryLiveLocations
|
|
|
|
|
liveLocation
|
|
|
|
|
queryVname
|
|
|
|
|
queryLabels
|
|
|
|
|
call
|
|
|
|
|
queryCall
|
|
|
|
|
queryQuickReplies
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type flag byte
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
ignore flag = 1 << (7 - iota)
|
|
|
|
|
ackRequest
|
|
|
|
|
available
|
|
|
|
|
notAvailable
|
|
|
|
|
expires
|
|
|
|
|
skipOffline
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Conn is created by NewConn. Interacting with the initialized Conn is the main way of interacting with our package.
|
|
|
|
|
It holds all necessary information to make the package work internally.
|
|
|
|
|
*/
|
|
|
|
|
type Conn struct {
|
|
|
|
|
ws *websocketWrapper
|
|
|
|
|
listener *listenerWrapper
|
|
|
|
|
|
|
|
|
|
connected bool
|
|
|
|
|
loggedIn bool
|
|
|
|
|
wg *sync.WaitGroup
|
|
|
|
|
|
|
|
|
|
session *Session
|
|
|
|
|
sessionLock uint32
|
|
|
|
|
handler []Handler
|
|
|
|
|
msgCount int
|
|
|
|
|
msgTimeout time.Duration
|
|
|
|
|
Info *Info
|
|
|
|
|
Store *Store
|
|
|
|
|
ServerLastSeen time.Time
|
|
|
|
|
|
|
|
|
|
longClientName string
|
|
|
|
|
shortClientName string
|
2019-08-26 21:22:34 +00:00
|
|
|
|
|
|
|
|
|
loginSessionLock sync.RWMutex
|
2019-10-26 23:45:57 +00:00
|
|
|
|
Proxy func(*http.Request) (*url.URL, error)
|
2020-01-09 20:02:56 +00:00
|
|
|
|
|
|
|
|
|
writerLock sync.RWMutex
|
2019-05-30 10:20:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type websocketWrapper struct {
|
|
|
|
|
sync.Mutex
|
|
|
|
|
conn *websocket.Conn
|
|
|
|
|
close chan struct{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type listenerWrapper struct {
|
|
|
|
|
sync.RWMutex
|
|
|
|
|
m map[string]chan string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Creates a new connection with a given timeout. The websocket connection to the WhatsAppWeb servers get´s established.
|
|
|
|
|
The goroutine for handling incoming messages is started
|
|
|
|
|
*/
|
|
|
|
|
func NewConn(timeout time.Duration) (*Conn, error) {
|
|
|
|
|
wac := &Conn{
|
|
|
|
|
handler: make([]Handler, 0),
|
|
|
|
|
msgCount: 0,
|
|
|
|
|
msgTimeout: timeout,
|
|
|
|
|
Store: newStore(),
|
|
|
|
|
|
|
|
|
|
longClientName: "github.com/rhymen/go-whatsapp",
|
|
|
|
|
shortClientName: "go-whatsapp",
|
|
|
|
|
}
|
|
|
|
|
return wac, wac.connect()
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-26 23:45:57 +00:00
|
|
|
|
// NewConnWithProxy Create a new connect with a given timeout and a http proxy.
|
|
|
|
|
func NewConnWithProxy(timeout time.Duration, proxy func(*http.Request) (*url.URL, error)) (*Conn, error) {
|
|
|
|
|
wac := &Conn{
|
|
|
|
|
handler: make([]Handler, 0),
|
|
|
|
|
msgCount: 0,
|
|
|
|
|
msgTimeout: timeout,
|
|
|
|
|
Store: newStore(),
|
|
|
|
|
|
|
|
|
|
longClientName: "github.com/rhymen/go-whatsapp",
|
|
|
|
|
shortClientName: "go-whatsapp",
|
|
|
|
|
Proxy: proxy,
|
|
|
|
|
}
|
|
|
|
|
return wac, wac.connect()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 10:20:56 +00:00
|
|
|
|
// connect should be guarded with wsWriteMutex
|
|
|
|
|
func (wac *Conn) connect() (err error) {
|
|
|
|
|
if wac.connected {
|
|
|
|
|
return ErrAlreadyConnected
|
|
|
|
|
}
|
|
|
|
|
wac.connected = true
|
|
|
|
|
defer func() { // set connected to false on error
|
|
|
|
|
if err != nil {
|
|
|
|
|
wac.connected = false
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
dialer := &websocket.Dialer{
|
|
|
|
|
ReadBufferSize: 25 * 1024 * 1024,
|
|
|
|
|
WriteBufferSize: 10 * 1024 * 1024,
|
|
|
|
|
HandshakeTimeout: wac.msgTimeout,
|
2019-10-26 23:45:57 +00:00
|
|
|
|
Proxy: wac.Proxy,
|
2019-05-30 10:20:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headers := http.Header{"Origin": []string{"https://web.whatsapp.com"}}
|
|
|
|
|
wsConn, _, err := dialer.Dial("wss://web.whatsapp.com/ws", headers)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.Wrap(err, "couldn't dial whatsapp web websocket")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wsConn.SetCloseHandler(func(code int, text string) error {
|
|
|
|
|
// from default CloseHandler
|
|
|
|
|
message := websocket.FormatCloseMessage(code, "")
|
|
|
|
|
err := wsConn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
|
|
|
|
|
|
|
|
|
|
// our close handling
|
|
|
|
|
_, _ = wac.Disconnect()
|
|
|
|
|
wac.handle(&ErrConnectionClosed{Code: code, Text: text})
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
wac.ws = &websocketWrapper{
|
|
|
|
|
conn: wsConn,
|
|
|
|
|
close: make(chan struct{}),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wac.listener = &listenerWrapper{
|
|
|
|
|
m: make(map[string]chan string),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wac.wg = &sync.WaitGroup{}
|
|
|
|
|
wac.wg.Add(2)
|
|
|
|
|
go wac.readPump()
|
|
|
|
|
go wac.keepAlive(20000, 60000)
|
|
|
|
|
|
|
|
|
|
wac.loggedIn = false
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wac *Conn) Disconnect() (Session, error) {
|
|
|
|
|
if !wac.connected {
|
|
|
|
|
return Session{}, ErrNotConnected
|
|
|
|
|
}
|
|
|
|
|
wac.connected = false
|
|
|
|
|
wac.loggedIn = false
|
|
|
|
|
|
|
|
|
|
close(wac.ws.close) //signal close
|
|
|
|
|
wac.wg.Wait() //wait for close
|
|
|
|
|
|
|
|
|
|
err := wac.ws.conn.Close()
|
|
|
|
|
wac.ws = nil
|
|
|
|
|
|
|
|
|
|
if wac.session == nil {
|
|
|
|
|
return Session{}, err
|
|
|
|
|
}
|
|
|
|
|
return *wac.session, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-26 21:22:34 +00:00
|
|
|
|
func (wac *Conn) AdminTest() (bool, error) {
|
|
|
|
|
if !wac.connected {
|
|
|
|
|
return false, ErrNotConnected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !wac.loggedIn {
|
|
|
|
|
return false, ErrInvalidSession
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-26 23:45:57 +00:00
|
|
|
|
result, err := wac.sendAdminTest()
|
2019-08-26 21:22:34 +00:00
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 10:20:56 +00:00
|
|
|
|
func (wac *Conn) keepAlive(minIntervalMs int, maxIntervalMs int) {
|
|
|
|
|
defer wac.wg.Done()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
err := wac.sendKeepAlive()
|
|
|
|
|
if err != nil {
|
|
|
|
|
wac.handle(errors.Wrap(err, "keepAlive failed"))
|
|
|
|
|
//TODO: Consequences?
|
|
|
|
|
}
|
|
|
|
|
interval := rand.Intn(maxIntervalMs-minIntervalMs) + minIntervalMs
|
|
|
|
|
select {
|
|
|
|
|
case <-time.After(time.Duration(interval) * time.Millisecond):
|
|
|
|
|
case <-wac.ws.close:
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|