forked from lug/matterbridge
		
	
		
			
				
	
	
		
			249 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
//Package whatsapp provides a developer API to interact with the WhatsAppWeb-Servers.
 | 
						||
package whatsapp
 | 
						||
 | 
						||
import (
 | 
						||
	"math/rand"
 | 
						||
	"net/http"
 | 
						||
	"net/url"
 | 
						||
	"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
 | 
						||
	clientVersion   string
 | 
						||
 | 
						||
	loginSessionLock sync.RWMutex
 | 
						||
	Proxy            func(*http.Request) (*url.URL, error)
 | 
						||
 | 
						||
	writerLock sync.RWMutex
 | 
						||
}
 | 
						||
 | 
						||
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",
 | 
						||
		clientVersion:   "0.1.0",
 | 
						||
	}
 | 
						||
	return wac, wac.connect()
 | 
						||
}
 | 
						||
 | 
						||
// 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",
 | 
						||
		clientVersion:   "0.1.0",
 | 
						||
		Proxy:           proxy,
 | 
						||
	}
 | 
						||
	return wac, wac.connect()
 | 
						||
}
 | 
						||
 | 
						||
// 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,
 | 
						||
		Proxy:            wac.Proxy,
 | 
						||
	}
 | 
						||
 | 
						||
	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
 | 
						||
}
 | 
						||
 | 
						||
func (wac *Conn) AdminTest() (bool, error) {
 | 
						||
	if !wac.connected {
 | 
						||
		return false, ErrNotConnected
 | 
						||
	}
 | 
						||
 | 
						||
	if !wac.loggedIn {
 | 
						||
		return false, ErrInvalidSession
 | 
						||
	}
 | 
						||
 | 
						||
	result, err := wac.sendAdminTest()
 | 
						||
	return result, err
 | 
						||
}
 | 
						||
 | 
						||
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
 | 
						||
		}
 | 
						||
	}
 | 
						||
}
 |