4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-07-11 17:46:27 +00:00

Add whatsappmulti buildflag for whatsapp with multidevice support (whatsapp)

This commit is contained in:
Wim
2022-03-20 02:20:54 +01:00
parent 2623a412c4
commit 496d5b4ec7
53 changed files with 26084 additions and 344 deletions

3
vendor/github.com/Rhymen/go-whatsapp/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
docs/
build/

21
vendor/github.com/Rhymen/go-whatsapp/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

139
vendor/github.com/Rhymen/go-whatsapp/README.md generated vendored Normal file
View File

@ -0,0 +1,139 @@
# go-whatsapp
Package rhymen/go-whatsapp implements the WhatsApp Web API to provide a clean interface for developers. Big thanks to all contributors of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project. The official WhatsApp Business API was released in August 2018. You can check it out [here](https://www.whatsapp.com/business/api).
## Installation
```sh
go get github.com/Rhymen/go-whatsapp
```
## Usage
### Creating a connection
```go
import (
whatsapp "github.com/Rhymen/go-whatsapp"
)
wac, err := whatsapp.NewConn(20 * time.Second)
```
The duration passed to the NewConn function is used to timeout login requests. If you have a bad internet connection use a higher timeout value. This function only creates a websocket connection, it does not handle authentication.
### Login
```go
qrChan := make(chan string)
go func() {
fmt.Printf("qr code: %v\n", <-qrChan)
//show qr code or save it somewhere to scan
}()
sess, err := wac.Login(qrChan)
```
The authentication process requires you to scan the qr code, that is send through the channel, with the device you are using whatsapp on. The session struct that is returned can be saved and used to restore the login without scanning the qr code again. The qr code has a ttl of 20 seconds and the login function throws a timeout err if the time has passed or any other request fails.
### Restore
```go
newSess, err := wac.RestoreWithSession(sess)
```
The restore function needs a valid session and returns the new session that was created.
### Add message handlers
```go
type myHandler struct{}
func (myHandler) HandleError(err error) {
fmt.Fprintf(os.Stderr, "%v", err)
}
func (myHandler) HandleTextMessage(message whatsapp.TextMessage) {
fmt.Println(message)
}
func (myHandler) HandleImageMessage(message whatsapp.ImageMessage) {
fmt.Println(message)
}
func (myHandler) HandleDocumentMessage(message whatsapp.DocumentMessage) {
fmt.Println(message)
}
func (myHandler) HandleVideoMessage(message whatsapp.VideoMessage) {
fmt.Println(message)
}
func (myHandler) HandleAudioMessage(message whatsapp.AudioMessage){
fmt.Println(message)
}
func (myHandler) HandleJsonMessage(message string) {
fmt.Println(message)
}
func (myHandler) HandleContactMessage(message whatsapp.ContactMessage) {
fmt.Println(message)
}
func (myHandler) HandleBatteryMessage(message whatsapp.BatteryMessage) {
fmt.Println(message)
}
func (myHandler) HandleNewContact(contact whatsapp.Contact) {
fmt.Println(contact)
}
wac.AddHandler(myHandler{})
```
The message handlers are all optional, you don't need to implement anything but the error handler to implement the interface. The ImageMessage, VideoMessage, AudioMessage and DocumentMessage provide a Download function to get the media data.
### Sending text messages
```go
text := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{
RemoteJid: "0123456789@s.whatsapp.net",
},
Text: "Hello Whatsapp",
}
err := wac.Send(text)
```
### Sending Contact Messages
```go
contactMessage := whatsapp.ContactMessage{
Info: whatsapp.MessageInfo{
RemoteJid: "0123456789@s.whatsapp.net",
},
DisplayName: "Luke Skylwallker",
Vcard: "BEGIN:VCARD\nVERSION:3.0\nN:Skyllwalker;Luke;;\nFN:Luke Skywallker\nitem1.TEL;waid=0123456789:+1 23 456789789\nitem1.X-ABLabel:Mobile\nEND:VCARD",
}
id, error := client.WaConn.Send(contactMessage)
```
The message will be send over the websocket. The attributes seen above are the required ones. All other relevant attributes (id, timestamp, fromMe, status) are set if they are missing in the struct. For the time being we only support text messages, but other types are planned for the near future.
## Legal
This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by WhatsApp or any of its
affiliates or subsidiaries. This is an independent and unofficial software. Use at your own risk.
## License
The MIT License (MIT)
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

388
vendor/github.com/Rhymen/go-whatsapp/binary/decoder.go generated vendored Normal file
View File

@ -0,0 +1,388 @@
package binary
import (
"fmt"
"github.com/Rhymen/go-whatsapp/binary/token"
"io"
"strconv"
)
type binaryDecoder struct {
data []byte
index int
}
func NewDecoder(data []byte) *binaryDecoder {
return &binaryDecoder{data, 0}
}
func (r *binaryDecoder) checkEOS(length int) error {
if r.index+length > len(r.data) {
return io.EOF
}
return nil
}
func (r *binaryDecoder) readByte() (byte, error) {
if err := r.checkEOS(1); err != nil {
return 0, err
}
b := r.data[r.index]
r.index++
return b, nil
}
func (r *binaryDecoder) readIntN(n int, littleEndian bool) (int, error) {
if err := r.checkEOS(n); err != nil {
return 0, err
}
var ret int
for i := 0; i < n; i++ {
var curShift int
if littleEndian {
curShift = i
} else {
curShift = n - i - 1
}
ret |= int(r.data[r.index+i]) << uint(curShift*8)
}
r.index += n
return ret, nil
}
func (r *binaryDecoder) readInt8(littleEndian bool) (int, error) {
return r.readIntN(1, littleEndian)
}
func (r *binaryDecoder) readInt16(littleEndian bool) (int, error) {
return r.readIntN(2, littleEndian)
}
func (r *binaryDecoder) readInt20() (int, error) {
if err := r.checkEOS(3); err != nil {
return 0, err
}
ret := ((int(r.data[r.index]) & 15) << 16) + (int(r.data[r.index+1]) << 8) + int(r.data[r.index+2])
r.index += 3
return ret, nil
}
func (r *binaryDecoder) readInt32(littleEndian bool) (int, error) {
return r.readIntN(4, littleEndian)
}
func (r *binaryDecoder) readInt64(littleEndian bool) (int, error) {
return r.readIntN(8, littleEndian)
}
func (r *binaryDecoder) readPacked8(tag int) (string, error) {
startByte, err := r.readByte()
if err != nil {
return "", err
}
ret := ""
for i := 0; i < int(startByte&127); i++ {
currByte, err := r.readByte()
if err != nil {
return "", err
}
lower, err := unpackByte(tag, currByte&0xF0>>4)
if err != nil {
return "", err
}
upper, err := unpackByte(tag, currByte&0x0F)
if err != nil {
return "", err
}
ret += lower + upper
}
if startByte>>7 != 0 {
ret = ret[:len(ret)-1]
}
return ret, nil
}
func unpackByte(tag int, value byte) (string, error) {
switch tag {
case token.NIBBLE_8:
return unpackNibble(value)
case token.HEX_8:
return unpackHex(value)
default:
return "", fmt.Errorf("unpackByte with unknown tag %d", tag)
}
}
func unpackNibble(value byte) (string, error) {
switch {
case value < 0 || value > 15:
return "", fmt.Errorf("unpackNibble with value %d", value)
case value == 10:
return "-", nil
case value == 11:
return ".", nil
case value == 15:
return "\x00", nil
default:
return strconv.Itoa(int(value)), nil
}
}
func unpackHex(value byte) (string, error) {
switch {
case value < 0 || value > 15:
return "", fmt.Errorf("unpackHex with value %d", value)
case value < 10:
return strconv.Itoa(int(value)), nil
default:
return string('A' + value - 10), nil
}
}
func (r *binaryDecoder) readListSize(tag int) (int, error) {
switch tag {
case token.LIST_EMPTY:
return 0, nil
case token.LIST_8:
return r.readInt8(false)
case token.LIST_16:
return r.readInt16(false)
default:
return 0, fmt.Errorf("readListSize with unknown tag %d at position %d", tag, r.index)
}
}
func (r *binaryDecoder) readString(tag int) (string, error) {
switch {
case tag >= 3 && tag <= len(token.SingleByteTokens):
tok, err := token.GetSingleToken(tag)
if err != nil {
return "", err
}
if tok == "s.whatsapp.net" {
tok = "c.us"
}
return tok, nil
case tag == token.DICTIONARY_0 || tag == token.DICTIONARY_1 || tag == token.DICTIONARY_2 || tag == token.DICTIONARY_3:
i, err := r.readInt8(false)
if err != nil {
return "", err
}
return token.GetDoubleToken(tag-token.DICTIONARY_0, i)
case tag == token.LIST_EMPTY:
return "", nil
case tag == token.BINARY_8:
length, err := r.readInt8(false)
if err != nil {
return "", err
}
return r.readStringFromChars(length)
case tag == token.BINARY_20:
length, err := r.readInt20()
if err != nil {
return "", err
}
return r.readStringFromChars(length)
case tag == token.BINARY_32:
length, err := r.readInt32(false)
if err != nil {
return "", err
}
return r.readStringFromChars(length)
case tag == token.JID_PAIR:
b, err := r.readByte()
if err != nil {
return "", err
}
i, err := r.readString(int(b))
if err != nil {
return "", err
}
b, err = r.readByte()
if err != nil {
return "", err
}
j, err := r.readString(int(b))
if err != nil {
return "", err
}
if i == "" || j == "" {
return "", fmt.Errorf("invalid jid pair: %s - %s", i, j)
}
return i + "@" + j, nil
case tag == token.NIBBLE_8 || tag == token.HEX_8:
return r.readPacked8(tag)
default:
return "", fmt.Errorf("invalid string with tag %d", tag)
}
}
func (r *binaryDecoder) readStringFromChars(length int) (string, error) {
if err := r.checkEOS(length); err != nil {
return "", err
}
ret := r.data[r.index : r.index+length]
r.index += length
return string(ret), nil
}
func (r *binaryDecoder) readAttributes(n int) (map[string]string, error) {
if n == 0 {
return nil, nil
}
ret := make(map[string]string)
for i := 0; i < n; i++ {
idx, err := r.readInt8(false)
if err != nil {
return nil, err
}
index, err := r.readString(idx)
if err != nil {
return nil, err
}
idx, err = r.readInt8(false)
if err != nil {
return nil, err
}
ret[index], err = r.readString(idx)
if err != nil {
return nil, err
}
}
return ret, nil
}
func (r *binaryDecoder) readList(tag int) ([]Node, error) {
size, err := r.readListSize(tag)
if err != nil {
return nil, err
}
ret := make([]Node, size)
for i := 0; i < size; i++ {
n, err := r.ReadNode()
if err != nil {
return nil, err
}
ret[i] = *n
}
return ret, nil
}
func (r *binaryDecoder) ReadNode() (*Node, error) {
ret := &Node{}
size, err := r.readInt8(false)
if err != nil {
return nil, err
}
listSize, err := r.readListSize(size)
if err != nil {
return nil, err
}
descrTag, err := r.readInt8(false)
if descrTag == token.STREAM_END {
return nil, fmt.Errorf("unexpected stream end")
}
ret.Description, err = r.readString(descrTag)
if err != nil {
return nil, err
}
if listSize == 0 || ret.Description == "" {
return nil, fmt.Errorf("invalid Node")
}
ret.Attributes, err = r.readAttributes((listSize - 1) >> 1)
if err != nil {
return nil, err
}
if listSize%2 == 1 {
return ret, nil
}
tag, err := r.readInt8(false)
if err != nil {
return nil, err
}
switch tag {
case token.LIST_EMPTY, token.LIST_8, token.LIST_16:
ret.Content, err = r.readList(tag)
case token.BINARY_8:
size, err = r.readInt8(false)
if err != nil {
return nil, err
}
ret.Content, err = r.readBytes(size)
case token.BINARY_20:
size, err = r.readInt20()
if err != nil {
return nil, err
}
ret.Content, err = r.readBytes(size)
case token.BINARY_32:
size, err = r.readInt32(false)
if err != nil {
return nil, err
}
ret.Content, err = r.readBytes(size)
default:
ret.Content, err = r.readString(tag)
}
if err != nil {
return nil, err
}
return ret, nil
}
func (r *binaryDecoder) readBytes(n int) ([]byte, error) {
ret := make([]byte, n)
var err error
for i := range ret {
ret[i], err = r.readByte()
if err != nil {
return nil, err
}
}
return ret, nil
}

351
vendor/github.com/Rhymen/go-whatsapp/binary/encoder.go generated vendored Normal file
View File

@ -0,0 +1,351 @@
package binary
import (
"fmt"
"github.com/Rhymen/go-whatsapp/binary/token"
"math"
"strconv"
"strings"
)
type binaryEncoder struct {
data []byte
}
func NewEncoder() *binaryEncoder {
return &binaryEncoder{make([]byte, 0)}
}
func (w *binaryEncoder) GetData() []byte {
return w.data
}
func (w *binaryEncoder) pushByte(b byte) {
w.data = append(w.data, b)
}
func (w *binaryEncoder) pushBytes(bytes []byte) {
w.data = append(w.data, bytes...)
}
func (w *binaryEncoder) pushIntN(value, n int, littleEndian bool) {
for i := 0; i < n; i++ {
var curShift int
if littleEndian {
curShift = i
} else {
curShift = n - i - 1
}
w.pushByte(byte((value >> uint(curShift*8)) & 0xFF))
}
}
func (w *binaryEncoder) pushInt20(value int) {
w.pushBytes([]byte{byte((value >> 16) & 0x0F), byte((value >> 8) & 0xFF), byte(value & 0xFF)})
}
func (w *binaryEncoder) pushInt8(value int) {
w.pushIntN(value, 1, false)
}
func (w *binaryEncoder) pushInt16(value int) {
w.pushIntN(value, 2, false)
}
func (w *binaryEncoder) pushInt32(value int) {
w.pushIntN(value, 4, false)
}
func (w *binaryEncoder) pushInt64(value int) {
w.pushIntN(value, 8, false)
}
func (w *binaryEncoder) pushString(value string) {
w.pushBytes([]byte(value))
}
func (w *binaryEncoder) writeByteLength(length int) error {
if length > math.MaxInt32 {
return fmt.Errorf("length is too large: %d", length)
} else if length >= (1 << 20) {
w.pushByte(token.BINARY_32)
w.pushInt32(length)
} else if length >= 256 {
w.pushByte(token.BINARY_20)
w.pushInt20(length)
} else {
w.pushByte(token.BINARY_8)
w.pushInt8(length)
}
return nil
}
func (w *binaryEncoder) WriteNode(n Node) error {
numAttributes := 0
if n.Attributes != nil {
numAttributes = len(n.Attributes)
}
hasContent := 0
if n.Content != nil {
hasContent = 1
}
w.writeListStart(2*numAttributes + 1 + hasContent)
if err := w.writeString(n.Description, false); err != nil {
return err
}
if err := w.writeAttributes(n.Attributes); err != nil {
return err
}
if err := w.writeChildren(n.Content); err != nil {
return err
}
return nil
}
func (w *binaryEncoder) writeString(tok string, i bool) error {
if !i && tok == "c.us" {
if err := w.writeToken(token.IndexOfSingleToken("s.whatsapp.net")); err != nil {
return err
}
return nil
}
tokenIndex := token.IndexOfSingleToken(tok)
if tokenIndex == -1 {
jidSepIndex := strings.Index(tok, "@")
if jidSepIndex < 1 {
w.writeStringRaw(tok)
} else {
w.writeJid(tok[:jidSepIndex], tok[jidSepIndex+1:])
}
} else {
if tokenIndex < token.SINGLE_BYTE_MAX {
if err := w.writeToken(tokenIndex); err != nil {
return err
}
} else {
singleByteOverflow := tokenIndex - token.SINGLE_BYTE_MAX
dictionaryIndex := singleByteOverflow >> 8
if dictionaryIndex < 0 || dictionaryIndex > 3 {
return fmt.Errorf("double byte dictionary token out of range: %v", tok)
}
if err := w.writeToken(token.DICTIONARY_0 + dictionaryIndex); err != nil {
return err
}
if err := w.writeToken(singleByteOverflow % 256); err != nil {
return err
}
}
}
return nil
}
func (w *binaryEncoder) writeStringRaw(value string) error {
if err := w.writeByteLength(len(value)); err != nil {
return err
}
w.pushString(value)
return nil
}
func (w *binaryEncoder) writeJid(jidLeft, jidRight string) error {
w.pushByte(token.JID_PAIR)
if jidLeft != "" {
if err := w.writePackedBytes(jidLeft); err != nil {
return err
}
} else {
if err := w.writeToken(token.LIST_EMPTY); err != nil {
return err
}
}
if err := w.writeString(jidRight, false); err != nil {
return err
}
return nil
}
func (w *binaryEncoder) writeToken(tok int) error {
if tok < len(token.SingleByteTokens) {
w.pushByte(byte(tok))
} else if tok <= 500 {
return fmt.Errorf("invalid token: %d", tok)
}
return nil
}
func (w *binaryEncoder) writeAttributes(attributes map[string]string) error {
if attributes == nil {
return nil
}
for key, val := range attributes {
if val == "" {
continue
}
if err := w.writeString(key, false); err != nil {
return err
}
if err := w.writeString(val, false); err != nil {
return err
}
}
return nil
}
func (w *binaryEncoder) writeChildren(children interface{}) error {
if children == nil {
return nil
}
switch childs := children.(type) {
case string:
if err := w.writeString(childs, true); err != nil {
return err
}
case []byte:
if err := w.writeByteLength(len(childs)); err != nil {
return err
}
w.pushBytes(childs)
case []Node:
w.writeListStart(len(childs))
for _, n := range childs {
if err := w.WriteNode(n); err != nil {
return err
}
}
default:
return fmt.Errorf("cannot write child of type: %T", children)
}
return nil
}
func (w *binaryEncoder) writeListStart(listSize int) {
if listSize == 0 {
w.pushByte(byte(token.LIST_EMPTY))
} else if listSize < 256 {
w.pushByte(byte(token.LIST_8))
w.pushInt8(listSize)
} else {
w.pushByte(byte(token.LIST_16))
w.pushInt16(listSize)
}
}
func (w *binaryEncoder) writePackedBytes(value string) error {
if err := w.writePackedBytesImpl(value, token.NIBBLE_8); err != nil {
if err := w.writePackedBytesImpl(value, token.HEX_8); err != nil {
return err
}
}
return nil
}
func (w *binaryEncoder) writePackedBytesImpl(value string, dataType int) error {
numBytes := len(value)
if numBytes > token.PACKED_MAX {
return fmt.Errorf("too many bytes to pack: %d", numBytes)
}
w.pushByte(byte(dataType))
x := 0
if numBytes%2 != 0 {
x = 128
}
w.pushByte(byte(x | int(math.Ceil(float64(numBytes)/2.0))))
for i, l := 0, numBytes/2; i < l; i++ {
b, err := w.packBytePair(dataType, value[2*i:2*i+1], value[2*i+1:2*i+2])
if err != nil {
return err
}
w.pushByte(byte(b))
}
if (numBytes % 2) != 0 {
b, err := w.packBytePair(dataType, value[numBytes-1:], "\x00")
if err != nil {
return err
}
w.pushByte(byte(b))
}
return nil
}
func (w *binaryEncoder) packBytePair(packType int, part1, part2 string) (int, error) {
if packType == token.NIBBLE_8 {
n1, err := packNibble(part1)
if err != nil {
return 0, err
}
n2, err := packNibble(part2)
if err != nil {
return 0, err
}
return (n1 << 4) | n2, nil
} else if packType == token.HEX_8 {
n1, err := packHex(part1)
if err != nil {
return 0, err
}
n2, err := packHex(part2)
if err != nil {
return 0, err
}
return (n1 << 4) | n2, nil
} else {
return 0, fmt.Errorf("invalid pack type (%d) for byte pair: %s / %s", packType, part1, part2)
}
}
func packNibble(value string) (int, error) {
if value >= "0" && value <= "9" {
return strconv.Atoi(value)
} else if value == "-" {
return 10, nil
} else if value == "." {
return 11, nil
} else if value == "\x00" {
return 15, nil
}
return 0, fmt.Errorf("invalid string to pack as nibble: %v", value)
}
func packHex(value string) (int, error) {
if (value >= "0" && value <= "9") || (value >= "A" && value <= "F") || (value >= "a" && value <= "f") {
d, err := strconv.ParseInt(value, 16, 0)
return int(d), err
} else if value == "\x00" {
return 15, nil
}
return 0, fmt.Errorf("invalid string to pack as hex: %v", value)
}

106
vendor/github.com/Rhymen/go-whatsapp/binary/node.go generated vendored Normal file
View File

@ -0,0 +1,106 @@
package binary
import (
"fmt"
pb "github.com/Rhymen/go-whatsapp/binary/proto"
"github.com/golang/protobuf/proto"
)
type Node struct {
Description string
Attributes map[string]string
Content interface{}
}
func Marshal(n Node) ([]byte, error) {
if n.Attributes != nil && n.Content != nil {
a, err := marshalMessageArray(n.Content.([]interface{}))
if err != nil {
return nil, err
}
n.Content = a
}
w := NewEncoder()
if err := w.WriteNode(n); err != nil {
return nil, err
}
return w.GetData(), nil
}
func marshalMessageArray(messages []interface{}) ([]Node, error) {
ret := make([]Node, len(messages))
for i, m := range messages {
if wmi, ok := m.(*pb.WebMessageInfo); ok {
b, err := marshalWebMessageInfo(wmi)
if err != nil {
return nil, nil
}
ret[i] = Node{"message", nil, b}
} else {
ret[i], ok = m.(Node)
if !ok {
return nil, fmt.Errorf("invalid Node")
}
}
}
return ret, nil
}
func marshalWebMessageInfo(p *pb.WebMessageInfo) ([]byte, error) {
b, err := proto.Marshal(p)
if err != nil {
return nil, err
}
return b, nil
}
func Unmarshal(data []byte) (*Node, error) {
r := NewDecoder(data)
n, err := r.ReadNode()
if err != nil {
return nil, err
}
if n != nil && n.Attributes != nil && n.Content != nil {
nContent, ok := n.Content.([]Node)
if ok {
n.Content, err = unmarshalMessageArray(nContent)
if err != nil {
return nil, err
}
}
}
return n, nil
}
func unmarshalMessageArray(messages []Node) ([]interface{}, error) {
ret := make([]interface{}, len(messages))
for i, msg := range messages {
if msg.Description == "message" {
info, err := unmarshalWebMessageInfo(msg.Content.([]byte))
if err != nil {
return nil, err
}
ret[i] = info
} else {
ret[i] = msg
}
}
return ret, nil
}
func unmarshalWebMessageInfo(msg []byte) (*pb.WebMessageInfo, error) {
message := &pb.WebMessageInfo{}
err := proto.Unmarshal(msg, message)
if err != nil {
return nil, err
}
return message, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,81 @@
package token
import "fmt"
var SingleByteTokens = [...]string{"", "", "", "200", "400", "404", "500", "501", "502", "action", "add",
"after", "archive", "author", "available", "battery", "before", "body",
"broadcast", "chat", "clear", "code", "composing", "contacts", "count",
"create", "debug", "delete", "demote", "duplicate", "encoding", "error",
"false", "filehash", "from", "g.us", "group", "groups_v2", "height", "id",
"image", "in", "index", "invis", "item", "jid", "kind", "last", "leave",
"live", "log", "media", "message", "mimetype", "missing", "modify", "name",
"notification", "notify", "out", "owner", "participant", "paused",
"picture", "played", "presence", "preview", "promote", "query", "raw",
"read", "receipt", "received", "recipient", "recording", "relay",
"remove", "response", "resume", "retry", "s.whatsapp.net", "seconds",
"set", "size", "status", "subject", "subscribe", "t", "text", "to", "true",
"type", "unarchive", "unavailable", "url", "user", "value", "web", "width",
"mute", "read_only", "admin", "creator", "short", "update", "powersave",
"checksum", "epoch", "block", "previous", "409", "replaced", "reason",
"spam", "modify_tag", "message_info", "delivery", "emoji", "title",
"description", "canonical-url", "matched-text", "star", "unstar",
"media_key", "filename", "identity", "unread", "page", "page_count",
"search", "media_message", "security", "call_log", "profile", "ciphertext",
"invite", "gif", "vcard", "frequent", "privacy", "blacklist", "whitelist",
"verify", "location", "document", "elapsed", "revoke_invite", "expiration",
"unsubscribe", "disable", "vname", "old_jid", "new_jid", "announcement",
"locked", "prop", "label", "color", "call", "offer", "call-id",
"quick_reply", "sticker", "pay_t", "accept", "reject", "sticker_pack",
"invalid", "canceled", "missed", "connected", "result", "audio",
"video", "recent"}
var doubleByteTokens = [...]string{}
func GetSingleToken(i int) (string, error) {
if i < 3 || i >= len(SingleByteTokens) {
return "", fmt.Errorf("index out of single byte token bounds %d", i)
}
return SingleByteTokens[i], nil
}
func GetDoubleToken(index1 int, index2 int) (string, error) {
n := 256*index1 + index2
if n < 0 || n >= len(doubleByteTokens) {
return "", fmt.Errorf("index out of double byte token bounds %d", n)
}
return doubleByteTokens[n], nil
}
func IndexOfSingleToken(token string) int {
for i, t := range SingleByteTokens {
if t == token {
return i
}
}
return -1
}
const (
LIST_EMPTY = 0
STREAM_END = 2
DICTIONARY_0 = 236
DICTIONARY_1 = 237
DICTIONARY_2 = 238
DICTIONARY_3 = 239
LIST_8 = 248
LIST_16 = 249
JID_PAIR = 250
HEX_8 = 251
BINARY_8 = 252
BINARY_20 = 253
BINARY_32 = 254
NIBBLE_8 = 255
)
const (
PACKED_MAX = 254
SINGLE_BYTE_MAX = 256
)

183
vendor/github.com/Rhymen/go-whatsapp/chat_history.go generated vendored Normal file
View File

@ -0,0 +1,183 @@
package whatsapp
import (
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/binary/proto"
"log"
"strconv"
"time"
)
type MessageOffsetInfo struct {
FirstMessageId string
FirstMessageOwner bool
}
func decodeMessages(n *binary.Node) []*proto.WebMessageInfo {
var messages = make([]*proto.WebMessageInfo, 0)
if n == nil || n.Attributes == nil || n.Content == nil {
return messages
}
for _, msg := range n.Content.([]interface{}) {
switch msg.(type) {
case *proto.WebMessageInfo:
messages = append(messages, msg.(*proto.WebMessageInfo))
default:
log.Println("decodeMessages: Non WebMessage encountered")
}
}
return messages
}
// LoadChatMessages is useful to "scroll" messages, loading by count at a time
// if handlers == nil the func will use default handlers
// if after == true LoadChatMessages will load messages after the specified messageId, otherwise it will return
// message before the messageId
func (wac *Conn) LoadChatMessages(jid string, count int, messageId string, owner bool, after bool, handlers ...Handler) error {
if count <= 0 {
return nil
}
if handlers == nil {
handlers = wac.handler
}
kind := "before"
if after {
kind = "after"
}
node, err := wac.query("message", jid, messageId, kind,
strconv.FormatBool(owner), "", count, 0)
if err != nil {
wac.handleWithCustomHandlers(err, handlers)
return err
}
for _, msg := range decodeMessages(node) {
wac.handleWithCustomHandlers(ParseProtoMessage(msg), handlers)
wac.handleWithCustomHandlers(msg, handlers)
}
return nil
}
// LoadFullChatHistory loads full chat history for the given jid
// chunkSize = how many messages to load with one query; if handlers == nil the func will use default handlers;
// pauseBetweenQueries = how much time to sleep between queries
func (wac *Conn) LoadFullChatHistory(jid string, chunkSize int,
pauseBetweenQueries time.Duration, handlers ...Handler) {
if chunkSize <= 0 {
return
}
if handlers == nil {
handlers = wac.handler
}
beforeMsg := ""
beforeMsgIsOwner := true
for {
node, err := wac.query("message", jid, beforeMsg, "before",
strconv.FormatBool(beforeMsgIsOwner), "", chunkSize, 0)
if err != nil {
wac.handleWithCustomHandlers(err, handlers)
} else {
msgs := decodeMessages(node)
for _, msg := range msgs {
wac.handleWithCustomHandlers(ParseProtoMessage(msg), handlers)
wac.handleWithCustomHandlers(msg, handlers)
}
if len(msgs) == 0 {
break
}
beforeMsg = *msgs[0].Key.Id
beforeMsgIsOwner = msgs[0].Key.FromMe != nil && *msgs[0].Key.FromMe
}
<-time.After(pauseBetweenQueries)
}
}
// LoadFullChatHistoryAfter loads all messages after the specified messageId
// useful to "catch up" with the message history after some specified message
func (wac *Conn) LoadFullChatHistoryAfter(jid string, messageId string, chunkSize int,
pauseBetweenQueries time.Duration, handlers ...Handler) {
if chunkSize <= 0 {
return
}
if handlers == nil {
handlers = wac.handler
}
msgOwner := true
prevNotFound := false
for {
node, err := wac.query("message", jid, messageId, "after",
strconv.FormatBool(msgOwner), "", chunkSize, 0)
if err != nil {
// Whatsapp will return 404 status when there is wrong owner flag on the requested message id
if err == ErrServerRespondedWith404 {
// this will detect two consecutive "not found" errors.
// this is done to prevent infinite loop when wrong message id supplied
if prevNotFound {
log.Println("LoadFullChatHistoryAfter: could not retrieve any messages, wrong message id?")
return
}
prevNotFound = true
// try to reverse the owner flag and retry
if msgOwner {
// reverse initial msgOwner value and retry
msgOwner = false
<-time.After(time.Second)
continue
}
}
// if the error isn't a 404 error, pass it to the error handler
wac.handleWithCustomHandlers(err, handlers)
} else {
msgs := decodeMessages(node)
for _, msg := range msgs {
wac.handleWithCustomHandlers(ParseProtoMessage(msg), handlers)
wac.handleWithCustomHandlers(msg, handlers)
}
if len(msgs) != chunkSize {
break
}
messageId = *msgs[0].Key.Id
msgOwner = msgs[0].Key.FromMe != nil && *msgs[0].Key.FromMe
}
// message was found
prevNotFound = false
<-time.After(pauseBetweenQueries)
}
}

302
vendor/github.com/Rhymen/go-whatsapp/conn.go generated vendored Normal file
View File

@ -0,0 +1,302 @@
//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
timeTag string // last 3 digits obtained after a successful login takeover
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) {
return NewConnWithOptions(&Options{
Timeout: timeout,
})
}
// 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) {
return NewConnWithOptions(&Options{
Timeout: timeout,
Proxy: proxy,
})
}
// NewConnWithOptions Create a new connect with a given options.
type Options struct {
Proxy func(*http.Request) (*url.URL, error)
Timeout time.Duration
Handler []Handler
ShortClientName string
LongClientName string
ClientVersion string
Store *Store
}
func NewConnWithOptions(opt *Options) (*Conn, error) {
if opt == nil {
return nil, ErrOptionsNotProvided
}
wac := &Conn{
handler: make([]Handler, 0),
msgCount: 0,
msgTimeout: opt.Timeout,
Store: newStore(),
longClientName: "github.com/Rhymen/go-whatsapp",
shortClientName: "go-whatsapp",
clientVersion: "0.1.0",
}
if opt.Handler != nil {
wac.handler = opt.Handler
}
if opt.Store != nil {
wac.Store = opt.Store
}
if opt.Proxy != nil {
wac.Proxy = opt.Proxy
}
if len(opt.ShortClientName) != 0 {
wac.shortClientName = opt.ShortClientName
}
if len(opt.LongClientName) != 0 {
wac.longClientName = opt.LongClientName
}
if len(opt.ClientVersion) != 0 {
wac.clientVersion = opt.ClientVersion
}
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: 0,
WriteBufferSize: 0,
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
}
}
}
// IsConnected returns whether the server connection is established or not
func (wac *Conn) IsConnected() bool {
return wac.connected
}
// GetConnected returns whether the server connection is established or not
//
// Deprecated: function name is not go idiomatic, use IsConnected instead
func (wac *Conn) GetConnected() bool {
return wac.connected
}
//IsLoggedIn returns whether the you are logged in or not
func (wac *Conn) IsLoggedIn() bool {
return wac.loggedIn
}
// GetLoggedIn returns whether the you are logged in or not
//
// Deprecated: function name is not go idiomatic, use IsLoggedIn instead.
func (wac *Conn) GetLoggedIn() bool {
return wac.loggedIn
}

322
vendor/github.com/Rhymen/go-whatsapp/contact.go generated vendored Normal file
View File

@ -0,0 +1,322 @@
package whatsapp
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/Rhymen/go-whatsapp/binary"
)
type Presence string
const (
PresenceAvailable Presence = "available"
PresenceUnavailable Presence = "unavailable"
PresenceComposing Presence = "composing"
PresenceRecording Presence = "recording"
PresencePaused Presence = "paused"
)
//TODO: filename? WhatsApp uses Store.Contacts for these functions
// functions probably shouldn't return a string, maybe build a struct / return json
// check for further queries
func (wac *Conn) GetProfilePicThumb(jid string) (<-chan string, error) {
data := []interface{}{"query", "ProfilePicThumb", jid}
return wac.writeJson(data)
}
func (wac *Conn) GetStatus(jid string) (<-chan string, error) {
data := []interface{}{"query", "Status", jid}
return wac.writeJson(data)
}
func (wac *Conn) SubscribePresence(jid string) (<-chan string, error) {
data := []interface{}{"action", "presence", "subscribe", jid}
return wac.writeJson(data)
}
func (wac *Conn) Search(search string, count, page int) (*binary.Node, error) {
return wac.query("search", "", "", "", "", search, count, page)
}
func (wac *Conn) LoadMessages(jid, messageId string, count int) (*binary.Node, error) {
return wac.query("message", jid, "", "before", "true", "", count, 0)
}
func (wac *Conn) LoadMessagesBefore(jid, messageId string, count int) (*binary.Node, error) {
return wac.query("message", jid, messageId, "before", "true", "", count, 0)
}
func (wac *Conn) LoadMessagesAfter(jid, messageId string, count int) (*binary.Node, error) {
return wac.query("message", jid, messageId, "after", "true", "", count, 0)
}
func (wac *Conn) LoadMediaInfo(jid, messageId, owner string) (*binary.Node, error) {
return wac.query("media", jid, messageId, "", owner, "", 0, 0)
}
func (wac *Conn) Presence(jid string, presence Presence) (<-chan string, error) {
ts := time.Now().Unix()
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
content := binary.Node{
Description: "presence",
Attributes: map[string]string{
"type": string(presence),
},
}
switch presence {
case PresenceComposing:
fallthrough
case PresenceRecording:
fallthrough
case PresencePaused:
content.Attributes["to"] = jid
}
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{content},
}
return wac.writeBinary(n, group, ignore, tag)
}
func (wac *Conn) Exist(jid string) (<-chan string, error) {
data := []interface{}{"query", "exist", jid}
return wac.writeJson(data)
}
func (wac *Conn) Emoji() (*binary.Node, error) {
return wac.query("emoji", "", "", "", "", "", 0, 0)
}
func (wac *Conn) Contacts() (*binary.Node, error) {
return wac.query("contacts", "", "", "", "", "", 0, 0)
}
func (wac *Conn) Chats() (*binary.Node, error) {
return wac.query("chat", "", "", "", "", "", 0, 0)
}
func (wac *Conn) Read(jid, id string) (<-chan string, error) {
ts := time.Now().Unix()
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{binary.Node{
Description: "read",
Attributes: map[string]string{
"count": "1",
"index": id,
"jid": jid,
"owner": "false",
},
}},
}
return wac.writeBinary(n, group, ignore, tag)
}
func (wac *Conn) query(t, jid, messageId, kind, owner, search string, count, page int) (*binary.Node, error) {
ts := time.Now().Unix()
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
n := binary.Node{
Description: "query",
Attributes: map[string]string{
"type": t,
"epoch": strconv.Itoa(wac.msgCount),
},
}
if jid != "" {
n.Attributes["jid"] = jid
}
if messageId != "" {
n.Attributes["index"] = messageId
}
if kind != "" {
n.Attributes["kind"] = kind
}
if owner != "" {
n.Attributes["owner"] = owner
}
if search != "" {
n.Attributes["search"] = search
}
if count != 0 {
n.Attributes["count"] = strconv.Itoa(count)
}
if page != 0 {
n.Attributes["page"] = strconv.Itoa(page)
}
metric := group
if t == "media" {
metric = queryMedia
}
ch, err := wac.writeBinary(n, metric, ignore, tag)
if err != nil {
return nil, err
}
msg, err := wac.decryptBinaryMessage([]byte(<-ch))
if err != nil {
return nil, err
}
//TODO: use parseProtoMessage
return msg, nil
}
func (wac *Conn) setGroup(t, jid, subject string, participants []string) (<-chan string, error) {
ts := time.Now().Unix()
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
//TODO: get proto or improve encoder to handle []interface{}
p := buildParticipantNodes(participants)
g := binary.Node{
Description: "group",
Attributes: map[string]string{
"author": wac.session.Wid,
"id": tag,
"type": t,
},
Content: p,
}
if jid != "" {
g.Attributes["jid"] = jid
}
if subject != "" {
g.Attributes["subject"] = subject
}
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{g},
}
return wac.writeBinary(n, group, ignore, tag)
}
func buildParticipantNodes(participants []string) []binary.Node {
l := len(participants)
if participants == nil || l == 0 {
return nil
}
p := make([]binary.Node, len(participants))
for i, participant := range participants {
p[i] = binary.Node{
Description: "participant",
Attributes: map[string]string{
"jid": participant,
},
}
}
return p
}
func (wac *Conn) BlockContact(jid string) (<-chan string, error) {
return wac.handleBlockContact("add", jid)
}
func (wac *Conn) UnblockContact(jid string) (<-chan string, error) {
return wac.handleBlockContact("remove", jid)
}
func (wac *Conn) handleBlockContact(action, jid string) (<-chan string, error) {
ts := time.Now().Unix()
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
netsplit := strings.Split(jid, "@")
cusjid := netsplit[0] + "@c.us"
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{
binary.Node{
Description: "block",
Attributes: map[string]string{
"type": action,
},
Content: []binary.Node{
{
Description: "user",
Attributes: map[string]string{
"jid": cusjid,
},
Content: nil,
},
},
},
},
}
return wac.writeBinary(n, contact, ignore, tag)
}
// Search product details on order
func (wac *Conn) SearchProductDetails(id, orderId, token string) (<-chan string, error) {
data := []interface{}{"query", "order", map[string]string{
"id": id,
"orderId": orderId,
"imageHeight": strconv.Itoa(80),
"imageWidth": strconv.Itoa(80),
"token": token,
}}
return wac.writeJson(data)
}
// Order search and get product catalog reh
func (wac *Conn) SearchOrder(catalogWid, stanzaId string) (<-chan string, error) {
data := []interface{}{"query", "bizCatalog", map[string]string{
"catalogWid": catalogWid,
"limit": strconv.Itoa(10),
"height": strconv.Itoa(100),
"width": strconv.Itoa(100),
"stanza_id": stanzaId,
"type": "get_product_catalog_reh",
}}
return wac.writeJson(data)
}
// Company details for Whatsapp Business
func (wac *Conn) BusinessProfile(wid string) (<-chan string, error) {
query := map[string]string{
"wid": wid,
}
data := []interface{}{"query", "businessProfile", []map[string]string{query}}
return wac.writeJson(data)
}

101
vendor/github.com/Rhymen/go-whatsapp/crypto/cbc/cbc.go generated vendored Normal file
View File

@ -0,0 +1,101 @@
/*
CBC describes a block cipher mode. In cryptography, a block cipher mode of operation is an algorithm that uses a
block cipher to provide an information service such as confidentiality or authenticity. A block cipher by itself
is only suitable for the secure cryptographic transformation (encryption or decryption) of one fixed-length group of
bits called a block. A mode of operation describes how to repeatedly apply a cipher's single-block operation to
securely transform amounts of data larger than a block.
This package simplifies the usage of AES-256-CBC.
*/
package cbc
/*
Some code is provided by the GitHub user locked (github.com/locked):
https://gist.github.com/locked/b066aa1ddeb2b28e855e
Thanks!
*/
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
/*
Decrypt is a function that decrypts a given cipher text with a provided key and initialization vector(iv).
*/
func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("ciphertext is shorter then block size: %d / %d", len(ciphertext), aes.BlockSize)
}
if iv == nil {
iv = ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
}
cbc := cipher.NewCBCDecrypter(block, iv)
cbc.CryptBlocks(ciphertext, ciphertext)
return unpad(ciphertext)
}
/*
Encrypt is a function that encrypts plaintext with a given key and an optional initialization vector(iv).
*/
func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
plaintext = pad(plaintext, aes.BlockSize)
if len(plaintext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("plaintext is not a multiple of the block size: %d / %d", len(plaintext), aes.BlockSize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
var ciphertext []byte
if iv == nil {
ciphertext = make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
} else {
ciphertext = make([]byte, len(plaintext))
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(ciphertext, plaintext)
}
return ciphertext, nil
}
func pad(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func unpad(src []byte) ([]byte, error) {
length := len(src)
padLen := int(src[length-1])
if padLen > length {
return nil, fmt.Errorf("padding is greater then the length: %d / %d", padLen, length)
}
return src[:(length - padLen)], nil
}

View File

@ -0,0 +1,44 @@
/*
In cryptography, Curve25519 is an elliptic curve offering 128 bits of security and designed for use with the elliptic
curve DiffieHellman (ECDH) key agreement scheme. It is one of the fastest ECC curves and is not covered by any known
patents. The reference implementation is public domain software. The original Curve25519 paper defined it
as a DiffieHellman (DH) function.
*/
package curve25519
import (
"crypto/rand"
"golang.org/x/crypto/curve25519"
"io"
)
/*
GenerateKey generates a public private key pair using Curve25519.
*/
func GenerateKey() (privateKey *[32]byte, publicKey *[32]byte, err error) {
var pub, priv [32]byte
_, err = io.ReadFull(rand.Reader, priv[:])
if err != nil {
return nil, nil, err
}
priv[0] &= 248
priv[31] &= 127
priv[31] |= 64
curve25519.ScalarBaseMult(&pub, &priv)
return &priv, &pub, nil
}
/*
GenerateSharedSecret generates the shared secret with a given public private key pair.
*/
func GenerateSharedSecret(priv, pub [32]byte) []byte {
var secret [32]byte
curve25519.ScalarMult(&secret, &priv, &pub)
return secret[:]
}

View File

@ -0,0 +1,47 @@
/*
HKDF is a simple key derivation function (KDF) based on
a hash-based message authentication code (HMAC). It was initially proposed by its authors as a building block in
various protocols and applications, as well as to discourage the proliferation of multiple KDF mechanisms.
The main approach HKDF follows is the "extract-then-expand" paradigm, where the KDF logically consists of two modules:
the first stage takes the input keying material and "extracts" from it a fixed-length pseudorandom key, and then the
second stage "expands" this key into several additional pseudorandom keys (the output of the KDF).
*/
package hkdf
import (
"crypto/sha256"
"fmt"
"golang.org/x/crypto/hkdf"
"io"
)
/*
Expand expands a given key with the HKDF algorithm.
*/
func Expand(key []byte, length int, info string) ([]byte, error) {
var h io.Reader
if info == "" {
/*
Only used during initial login
Pseudorandom Key is provided by server and has not to be created
*/
h = hkdf.Expand(sha256.New, key, []byte(info))
} else {
/*
Used every other time
Pseudorandom Key is created during kdf.New
This is the normal that crypto/hkdf is used
*/
h = hkdf.New(sha256.New, key, nil, []byte(info))
}
out := make([]byte, length)
n, err := io.ReadAtLeast(h, out, length)
if err != nil {
return nil, err
}
if n != length {
return nil, fmt.Errorf("new key to short")
}
return out, nil
}

42
vendor/github.com/Rhymen/go-whatsapp/errors.go generated vendored Normal file
View File

@ -0,0 +1,42 @@
package whatsapp
import (
"fmt"
"github.com/pkg/errors"
)
var (
ErrAlreadyConnected = errors.New("already connected")
ErrAlreadyLoggedIn = errors.New("already logged in")
ErrInvalidSession = errors.New("invalid session")
ErrLoginInProgress = errors.New("login or restore already running")
ErrNotConnected = errors.New("not connected")
ErrInvalidWsData = errors.New("received invalid data")
ErrInvalidWsState = errors.New("can't handle binary data when not logged in")
ErrConnectionTimeout = errors.New("connection timed out")
ErrMissingMessageTag = errors.New("no messageTag specified or to short")
ErrInvalidHmac = errors.New("invalid hmac")
ErrInvalidServerResponse = errors.New("invalid response received from server")
ErrServerRespondedWith404 = errors.New("server responded with status 404")
ErrInvalidWebsocket = errors.New("invalid websocket")
ErrMessageTypeNotImplemented = errors.New("message type not implemented")
ErrOptionsNotProvided = errors.New("new conn options not provided")
)
type ErrConnectionFailed struct {
Err error
}
func (e *ErrConnectionFailed) Error() string {
return fmt.Sprintf("connection to WhatsApp servers failed: %v", e.Err)
}
type ErrConnectionClosed struct {
Code int
Text string
}
func (e *ErrConnectionClosed) Error() string {
return fmt.Sprintf("server closed connection,code: %d,text: %s", e.Code, e.Text)
}

90
vendor/github.com/Rhymen/go-whatsapp/group.go generated vendored Normal file
View File

@ -0,0 +1,90 @@
package whatsapp
import (
"encoding/json"
"fmt"
"time"
)
func (wac *Conn) GetGroupMetaData(jid string) (<-chan string, error) {
data := []interface{}{"query", "GroupMetadata", jid}
return wac.writeJson(data)
}
func (wac *Conn) CreateGroup(subject string, participants []string) (<-chan string, error) {
return wac.setGroup("create", "", subject, participants)
}
func (wac *Conn) UpdateGroupSubject(subject string, jid string) (<-chan string, error) {
return wac.setGroup("subject", jid, subject, nil)
}
func (wac *Conn) SetAdmin(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("promote", jid, "", participants)
}
func (wac *Conn) RemoveAdmin(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("demote", jid, "", participants)
}
func (wac *Conn) AddMember(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("add", jid, "", participants)
}
func (wac *Conn) RemoveMember(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("remove", jid, "", participants)
}
func (wac *Conn) LeaveGroup(jid string) (<-chan string, error) {
return wac.setGroup("leave", jid, "", nil)
}
func (wac *Conn) GroupInviteLink(jid string) (string, error) {
request := []interface{}{"query", "inviteCode", jid}
ch, err := wac.writeJson(request)
if err != nil {
return "", err
}
var response map[string]interface{}
select {
case r := <-ch:
if err := json.Unmarshal([]byte(r), &response); err != nil {
return "", fmt.Errorf("error decoding response message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
return "", fmt.Errorf("request timed out")
}
if int(response["status"].(float64)) != 200 {
return "", fmt.Errorf("request responded with %d", response["status"])
}
return response["code"].(string), nil
}
func (wac *Conn) GroupAcceptInviteCode(code string) (jid string, err error) {
request := []interface{}{"action", "invite", code}
ch, err := wac.writeJson(request)
if err != nil {
return "", err
}
var response map[string]interface{}
select {
case r := <-ch:
if err := json.Unmarshal([]byte(r), &response); err != nil {
return "", fmt.Errorf("error decoding response message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
return "", fmt.Errorf("request timed out")
}
if int(response["status"].(float64)) != 200 {
return "", fmt.Errorf("request responded with %d", response["status"])
}
return response["gid"].(string), nil
}

481
vendor/github.com/Rhymen/go-whatsapp/handler.go generated vendored Normal file
View File

@ -0,0 +1,481 @@
package whatsapp
import (
"fmt"
"os"
"strings"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/binary/proto"
)
/*
The Handler interface is the minimal interface that needs to be implemented
to be accepted as a valid handler for our dispatching system.
The minimal handler is used to dispatch error messages. These errors occur on unexpected behavior by the websocket
connection or if we are unable to handle or interpret an incoming message. Error produced by user actions are not
dispatched through this handler. They are returned as an error on the specific function call.
*/
type Handler interface {
HandleError(err error)
}
type SyncHandler interface {
Handler
ShouldCallSynchronously() bool
}
/*
The TextMessageHandler interface needs to be implemented to receive text messages dispatched by the dispatcher.
*/
type TextMessageHandler interface {
Handler
HandleTextMessage(message TextMessage)
}
/*
The ImageMessageHandler interface needs to be implemented to receive image messages dispatched by the dispatcher.
*/
type ImageMessageHandler interface {
Handler
HandleImageMessage(message ImageMessage)
}
/*
The VideoMessageHandler interface needs to be implemented to receive video messages dispatched by the dispatcher.
*/
type VideoMessageHandler interface {
Handler
HandleVideoMessage(message VideoMessage)
}
/*
The AudioMessageHandler interface needs to be implemented to receive audio messages dispatched by the dispatcher.
*/
type AudioMessageHandler interface {
Handler
HandleAudioMessage(message AudioMessage)
}
/*
The DocumentMessageHandler interface needs to be implemented to receive document messages dispatched by the dispatcher.
*/
type DocumentMessageHandler interface {
Handler
HandleDocumentMessage(message DocumentMessage)
}
/*
The LiveLocationMessageHandler interface needs to be implemented to receive live location messages dispatched by the dispatcher.
*/
type LiveLocationMessageHandler interface {
Handler
HandleLiveLocationMessage(message LiveLocationMessage)
}
/*
The LocationMessageHandler interface needs to be implemented to receive location messages dispatched by the dispatcher.
*/
type LocationMessageHandler interface {
Handler
HandleLocationMessage(message LocationMessage)
}
/*
The StickerMessageHandler interface needs to be implemented to receive sticker messages dispatched by the dispatcher.
*/
type StickerMessageHandler interface {
Handler
HandleStickerMessage(message StickerMessage)
}
/*
The ContactMessageHandler interface needs to be implemented to receive contact messages dispatched by the dispatcher.
*/
type ContactMessageHandler interface {
Handler
HandleContactMessage(message ContactMessage)
}
/*
The ProductMessageHandler interface needs to be implemented to receive product messages dispatched by the dispatcher.
*/
type ProductMessageHandler interface {
Handler
HandleProductMessage(message ProductMessage)
}
/*
The OrderMessageHandler interface needs to be implemented to receive order messages dispatched by the dispatcher.
*/
type OrderMessageHandler interface {
Handler
HandleOrderMessage(message OrderMessage)
}
/*
The JsonMessageHandler interface needs to be implemented to receive json messages dispatched by the dispatcher.
These json messages contain status updates of every kind sent by WhatsAppWeb servers. WhatsAppWeb uses these messages
to built a Store, which is used to save these "secondary" information. These messages may contain
presence (available, last see) information, or just the battery status of your phone.
*/
type JsonMessageHandler interface {
Handler
HandleJsonMessage(message string)
}
/**
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..
*/
type RawMessageHandler interface {
Handler
HandleRawMessage(message *proto.WebMessageInfo)
}
/**
The ContactListHandler interface needs to be implemented to applky custom actions to contact lists dispatched by the dispatcher.
*/
type ContactListHandler interface {
Handler
HandleContactList(contacts []Contact)
}
/**
The ChatListHandler interface needs to be implemented to apply custom actions to chat lists dispatched by the dispatcher.
*/
type ChatListHandler interface {
Handler
HandleChatList(contacts []Chat)
}
/**
The BatteryMessageHandler interface needs to be implemented to receive percentage the device connected dispatched by the dispatcher.
*/
type BatteryMessageHandler interface {
Handler
HandleBatteryMessage(battery BatteryMessage)
}
/**
The NewContactHandler interface needs to be implemented to receive the contact's name for the first time.
*/
type NewContactHandler interface {
Handler
HandleNewContact(contact Contact)
}
/*
AddHandler adds an handler to the list of handler that receive dispatched messages.
The provided handler must at least implement the Handler interface. Additionally implemented
handlers(TextMessageHandler, ImageMessageHandler) are optional. At runtime it is checked if they are implemented
and they are called if so and needed.
*/
func (wac *Conn) AddHandler(handler Handler) {
wac.handler = append(wac.handler, handler)
}
// RemoveHandler removes a handler from the list of handlers that receive dispatched messages.
func (wac *Conn) RemoveHandler(handler Handler) bool {
i := -1
for k, v := range wac.handler {
if v == handler {
i = k
break
}
}
if i > -1 {
wac.handler = append(wac.handler[:i], wac.handler[i+1:]...)
return true
}
return false
}
// RemoveHandlers empties the list of handlers that receive dispatched messages.
func (wac *Conn) RemoveHandlers() {
wac.handler = make([]Handler, 0)
}
func (wac *Conn) shouldCallSynchronously(handler Handler) bool {
sh, ok := handler.(SyncHandler)
return ok && sh.ShouldCallSynchronously()
}
func (wac *Conn) handle(message interface{}) {
wac.handleWithCustomHandlers(message, wac.handler)
}
func (wac *Conn) handleWithCustomHandlers(message interface{}, handlers []Handler) {
switch m := message.(type) {
case error:
for _, h := range handlers {
if wac.shouldCallSynchronously(h) {
h.HandleError(m)
} else {
go h.HandleError(m)
}
}
case string:
for _, h := range handlers {
if x, ok := h.(JsonMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleJsonMessage(m)
} else {
go x.HandleJsonMessage(m)
}
}
}
case TextMessage:
for _, h := range handlers {
if x, ok := h.(TextMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleTextMessage(m)
} else {
go x.HandleTextMessage(m)
}
}
}
case ImageMessage:
for _, h := range handlers {
if x, ok := h.(ImageMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleImageMessage(m)
} else {
go x.HandleImageMessage(m)
}
}
}
case VideoMessage:
for _, h := range handlers {
if x, ok := h.(VideoMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleVideoMessage(m)
} else {
go x.HandleVideoMessage(m)
}
}
}
case AudioMessage:
for _, h := range handlers {
if x, ok := h.(AudioMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleAudioMessage(m)
} else {
go x.HandleAudioMessage(m)
}
}
}
case DocumentMessage:
for _, h := range handlers {
if x, ok := h.(DocumentMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleDocumentMessage(m)
} else {
go x.HandleDocumentMessage(m)
}
}
}
case LocationMessage:
for _, h := range handlers {
if x, ok := h.(LocationMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleLocationMessage(m)
} else {
go x.HandleLocationMessage(m)
}
}
}
case LiveLocationMessage:
for _, h := range handlers {
if x, ok := h.(LiveLocationMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleLiveLocationMessage(m)
} else {
go x.HandleLiveLocationMessage(m)
}
}
}
case StickerMessage:
for _, h := range handlers {
if x, ok := h.(StickerMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleStickerMessage(m)
} else {
go x.HandleStickerMessage(m)
}
}
}
case ContactMessage:
for _, h := range handlers {
if x, ok := h.(ContactMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleContactMessage(m)
} else {
go x.HandleContactMessage(m)
}
}
}
case BatteryMessage:
for _, h := range handlers {
if x, ok := h.(BatteryMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleBatteryMessage(m)
} else {
go x.HandleBatteryMessage(m)
}
}
}
case Contact:
for _, h := range handlers {
if x, ok := h.(NewContactHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleNewContact(m)
} else {
go x.HandleNewContact(m)
}
}
}
case ProductMessage:
for _, h := range handlers {
if x, ok := h.(ProductMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleProductMessage(m)
} else {
go x.HandleProductMessage(m)
}
}
}
case OrderMessage:
for _, h := range handlers {
if x, ok := h.(OrderMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleOrderMessage(m)
} else {
go x.HandleOrderMessage(m)
}
}
}
case *proto.WebMessageInfo:
for _, h := range handlers {
if x, ok := h.(RawMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleRawMessage(m)
} else {
go x.HandleRawMessage(m)
}
}
}
}
}
func (wac *Conn) handleContacts(contacts interface{}) {
var contactList []Contact
c, ok := contacts.([]interface{})
if !ok {
return
}
for _, contact := range c {
contactNode, ok := contact.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(contactNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
contactList = append(contactList, Contact{
jid,
contactNode.Attributes["notify"],
contactNode.Attributes["name"],
contactNode.Attributes["short"],
})
}
for _, h := range wac.handler {
if x, ok := h.(ContactListHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleContactList(contactList)
} else {
go x.HandleContactList(contactList)
}
}
}
}
func (wac *Conn) handleChats(chats interface{}) {
var chatList []Chat
c, ok := chats.([]interface{})
if !ok {
return
}
for _, chat := range c {
chatNode, ok := chat.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(chatNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
chatList = append(chatList, Chat{
jid,
chatNode.Attributes["name"],
chatNode.Attributes["count"],
chatNode.Attributes["t"],
chatNode.Attributes["mute"],
chatNode.Attributes["spam"],
})
}
for _, h := range wac.handler {
if x, ok := h.(ChatListHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleChatList(chatList)
} else {
go x.HandleChatList(chatList)
}
}
}
}
func (wac *Conn) dispatch(msg interface{}) {
if msg == nil {
return
}
switch message := msg.(type) {
case *binary.Node:
if message.Description == "action" {
if con, ok := message.Content.([]interface{}); ok {
for a := range con {
if v, ok := con[a].(*proto.WebMessageInfo); ok {
wac.handle(v)
wac.handle(ParseProtoMessage(v))
}
if v, ok := con[a].(binary.Node); ok {
wac.handle(ParseNodeMessage(v))
}
}
} else if con, ok := message.Content.([]binary.Node); ok {
for a := range con {
wac.handle(ParseNodeMessage(con[a]))
}
}
} else if message.Description == "response" && message.Attributes["type"] == "contacts" {
wac.updateContacts(message.Content)
wac.handleContacts(message.Content)
} else if message.Description == "response" && message.Attributes["type"] == "chat" {
wac.updateChats(message.Content)
wac.handleChats(message.Content)
}
case error:
wac.handle(message)
case string:
wac.handle(message)
default:
fmt.Fprintf(os.Stderr, "unknown type in dipatcher chan: %T", msg)
}
}

221
vendor/github.com/Rhymen/go-whatsapp/media.go generated vendored Normal file
View File

@ -0,0 +1,221 @@
package whatsapp
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/Rhymen/go-whatsapp/crypto/hkdf"
)
func Download(url string, mediaKey []byte, appInfo MediaType, fileLength int) ([]byte, error) {
if url == "" {
return nil, fmt.Errorf("no url present")
}
file, mac, err := downloadMedia(url)
if err != nil {
return nil, err
}
iv, cipherKey, macKey, _, err := getMediaKeys(mediaKey, appInfo)
if err != nil {
return nil, err
}
if err = validateMedia(iv, file, macKey, mac); err != nil {
return nil, err
}
data, err := cbc.Decrypt(cipherKey, iv, file)
if err != nil {
return nil, err
}
if len(data) != fileLength {
return nil, fmt.Errorf("file length does not match. Expected: %v, got: %v", fileLength, len(data))
}
return data, nil
}
func validateMedia(iv []byte, file []byte, macKey []byte, mac []byte) error {
h := hmac.New(sha256.New, macKey)
n, err := h.Write(append(iv, file...))
if err != nil {
return err
}
if n < 10 {
return fmt.Errorf("hash to short")
}
if !hmac.Equal(h.Sum(nil)[:10], mac) {
return fmt.Errorf("invalid media hmac")
}
return nil
}
func getMediaKeys(mediaKey []byte, appInfo MediaType) (iv, cipherKey, macKey, refKey []byte, err error) {
mediaKeyExpanded, err := hkdf.Expand(mediaKey, 112, string(appInfo))
if err != nil {
return nil, nil, nil, nil, err
}
return mediaKeyExpanded[:16], mediaKeyExpanded[16:48], mediaKeyExpanded[48:80], mediaKeyExpanded[80:], nil
}
func downloadMedia(url string) (file []byte, mac []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("download failed with status code %d", resp.StatusCode)
}
if resp.ContentLength <= 10 {
return nil, nil, fmt.Errorf("file to short")
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
n := len(data)
return data[:n-10], data[n-10 : n], nil
}
type MediaConn struct {
Status int `json:"status"`
MediaConn struct {
Auth string `json:"auth"`
TTL int `json:"ttl"`
Hosts []struct {
Hostname string `json:"hostname"`
IPs []struct {
IP4 net.IP `json:"ip4"`
IP6 net.IP `json:"ip6"`
} `json:"ips"`
} `json:"hosts"`
} `json:"media_conn"`
}
func (wac *Conn) queryMediaConn() (hostname, auth string, ttl int, err error) {
queryReq := []interface{}{"query", "mediaConn"}
ch, err := wac.writeJson(queryReq)
if err != nil {
return "", "", 0, err
}
var resp MediaConn
select {
case r := <-ch:
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return "", "", 0, fmt.Errorf("error decoding query media conn response: %v", err)
}
case <-time.After(wac.msgTimeout):
return "", "", 0, fmt.Errorf("query media conn timed out")
}
if resp.Status != http.StatusOK {
return "", "", 0, fmt.Errorf("query media conn responded with %d", resp.Status)
}
for _, h := range resp.MediaConn.Hosts {
if h.Hostname != "" {
return h.Hostname, resp.MediaConn.Auth, resp.MediaConn.TTL, nil
}
}
return "", "", 0, fmt.Errorf("query media conn responded with no host")
}
var mediaTypeMap = map[MediaType]string{
MediaImage: "/mms/image",
MediaVideo: "/mms/video",
MediaDocument: "/mms/document",
MediaAudio: "/mms/audio",
}
func (wac *Conn) Upload(reader io.Reader, appInfo MediaType) (downloadURL string, mediaKey []byte, fileEncSha256 []byte, fileSha256 []byte, fileLength uint64, err error) {
data, err := ioutil.ReadAll(reader)
if err != nil {
return "", nil, nil, nil, 0, err
}
mediaKey = make([]byte, 32)
rand.Read(mediaKey)
iv, cipherKey, macKey, _, err := getMediaKeys(mediaKey, appInfo)
if err != nil {
return "", nil, nil, nil, 0, err
}
enc, err := cbc.Encrypt(cipherKey, iv, data)
if err != nil {
return "", nil, nil, nil, 0, err
}
fileLength = uint64(len(data))
h := hmac.New(sha256.New, macKey)
h.Write(append(iv, enc...))
mac := h.Sum(nil)[:10]
sha := sha256.New()
sha.Write(data)
fileSha256 = sha.Sum(nil)
sha.Reset()
sha.Write(append(enc, mac...))
fileEncSha256 = sha.Sum(nil)
hostname, auth, _, err := wac.queryMediaConn()
if err != nil {
return "", nil, nil, nil, 0, err
}
token := base64.URLEncoding.EncodeToString(fileEncSha256)
q := url.Values{
"auth": []string{auth},
"token": []string{token},
}
path := mediaTypeMap[appInfo]
uploadURL := url.URL{
Scheme: "https",
Host: hostname,
Path: fmt.Sprintf("%s/%s", path, token),
RawQuery: q.Encode(),
}
body := bytes.NewReader(append(enc, mac...))
req, err := http.NewRequest(http.MethodPost, uploadURL.String(), body)
if err != nil {
return "", nil, nil, nil, 0, err
}
req.Header.Set("Origin", "https://web.whatsapp.com")
req.Header.Set("Referer", "https://web.whatsapp.com/")
client := &http.Client{}
// Submit the request
res, err := client.Do(req)
if err != nil {
return "", nil, nil, nil, 0, err
}
if res.StatusCode != http.StatusOK {
return "", nil, nil, nil, 0, fmt.Errorf("upload failed with status code %d", res.StatusCode)
}
var jsonRes map[string]string
if err := json.NewDecoder(res.Body).Decode(&jsonRes); err != nil {
return "", nil, nil, nil, 0, err
}
return jsonRes["url"], mediaKey, fileEncSha256, fileSha256, fileLength, nil
}

1005
vendor/github.com/Rhymen/go-whatsapp/message.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

65
vendor/github.com/Rhymen/go-whatsapp/profile.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
package whatsapp
import (
"fmt"
"strconv"
"time"
"github.com/Rhymen/go-whatsapp/binary"
)
// Pictures must be JPG 640x640 and 96x96, respectively
func (wac *Conn) UploadProfilePic(image, preview []byte) (<-chan string, error) {
tag := fmt.Sprintf("%d.--%d", time.Now().Unix(), wac.msgCount*19)
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{
binary.Node{
Description: "picture",
Attributes: map[string]string{
"id": tag,
"jid": wac.Info.Wid,
"type": "set",
},
Content: []binary.Node{
{
Description: "image",
Attributes: nil,
Content: image,
},
{
Description: "preview",
Attributes: nil,
Content: preview,
},
},
},
},
}
return wac.writeBinary(n, profile, 136, tag)
}
func (wac *Conn) UpdateProfileName(name string) (<-chan string, error) {
tag := fmt.Sprintf("%d.--%d", time.Now().Unix(), wac.msgCount*19)
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{
binary.Node{
Description: "profile",
Attributes: map[string]string{
"name": name,
},
Content: []binary.Node{},
},
},
}
return wac.writeBinary(n, profile, ignore, tag)
}

142
vendor/github.com/Rhymen/go-whatsapp/read.go generated vendored Normal file
View File

@ -0,0 +1,142 @@
package whatsapp
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
func (wac *Conn) readPump() {
defer func() {
wac.wg.Done()
_, _ = wac.Disconnect()
}()
var readErr error
var msgType int
var reader io.Reader
for {
readerFound := make(chan struct{})
go func() {
if wac.ws != nil {
msgType, reader, readErr = wac.ws.conn.NextReader()
}
close(readerFound)
}()
select {
case <-readerFound:
if readErr != nil {
wac.handle(&ErrConnectionFailed{Err: readErr})
return
}
msg, err := ioutil.ReadAll(reader)
if err != nil {
wac.handle(errors.Wrap(err, "error reading message from Reader"))
continue
}
err = wac.processReadData(msgType, msg)
if err != nil {
wac.handle(errors.Wrap(err, "error processing data"))
}
case <-wac.ws.close:
return
}
}
}
func (wac *Conn) processReadData(msgType int, msg []byte) error {
data := strings.SplitN(string(msg), ",", 2)
if data[0][0] == '!' { //Keep-Alive Timestamp
data = append(data, data[0][1:]) //data[1]
data[0] = "!"
}
if len(data) == 2 && len(data[1]) == 0 {
return nil
}
if len(data) != 2 || len(data[1]) == 0 {
return ErrInvalidWsData
}
wac.listener.RLock()
listener, hasListener := wac.listener.m[data[0]]
wac.listener.RUnlock()
if hasListener {
// listener only exists for TextMessages query messages out of contact.go
// If these binary query messages can be handled another way,
// then the TextMessages, which are all JSON encoded, can directly
// be unmarshalled. The listener chan could then be changed from type
// chan string to something like chan map[string]interface{}. The unmarshalling
// in several places, especially in session.go, would then be gone.
listener <- data[1]
close(listener)
wac.removeListener(data[0])
} else if msgType == websocket.BinaryMessage {
wac.loginSessionLock.RLock()
sess := wac.session
wac.loginSessionLock.RUnlock()
if sess == nil || sess.MacKey == nil || sess.EncKey == nil {
return ErrInvalidWsState
}
message, err := wac.decryptBinaryMessage([]byte(data[1]))
if err != nil {
return errors.Wrap(err, "error decoding binary")
}
wac.dispatch(message)
} else { //RAW json status updates
wac.handle(string(data[1]))
}
return nil
}
func (wac *Conn) decryptBinaryMessage(msg []byte) (*binary.Node, error) {
//message validation
h2 := hmac.New(sha256.New, wac.session.MacKey)
if len(msg) < 33 {
var response struct {
Status int `json:"status"`
}
if err := json.Unmarshal(msg, &response); err == nil {
if response.Status == http.StatusNotFound {
return nil, ErrServerRespondedWith404
}
return nil, errors.New(fmt.Sprintf("server responded with %d", response.Status))
}
return nil, ErrInvalidServerResponse
}
h2.Write([]byte(msg[32:]))
if !hmac.Equal(h2.Sum(nil), msg[:32]) {
return nil, ErrInvalidHmac
}
// message decrypt
d, err := cbc.Decrypt(wac.session.EncKey, nil, msg[32:])
if err != nil {
return nil, errors.Wrap(err, "decrypting message with AES-CBC failed")
}
// message unmarshal
message, err := binary.Unmarshal(d)
if err != nil {
return nil, errors.Wrap(err, "could not decode binary")
}
return message, nil
}

532
vendor/github.com/Rhymen/go-whatsapp/session.go generated vendored Normal file
View File

@ -0,0 +1,532 @@
package whatsapp
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/Rhymen/go-whatsapp/crypto/curve25519"
"github.com/Rhymen/go-whatsapp/crypto/hkdf"
)
//represents the WhatsAppWeb client version
var waVersion = []int{2, 2142, 12}
/*
Session contains session individual information. To be able to resume the connection without scanning the qr code
every time you should save the Session returned by Login and use RestoreWithSession the next time you want to login.
Every successful created connection returns a new Session. The Session(ClientToken, ServerToken) is altered after
every re-login and should be saved every time.
*/
type Session struct {
ClientId string
ClientToken string
ServerToken string
EncKey []byte
MacKey []byte
Wid string
}
type Info struct {
Battery int
Platform string
Connected bool
Pushname string
Wid string
Lc string
Phone *PhoneInfo
Plugged bool
Tos int
Lg string
Is24h bool
}
type PhoneInfo struct {
Mcc string
Mnc string
OsVersion string
DeviceManufacturer string
DeviceModel string
OsBuildNumber string
WaVersion string
}
func newInfoFromReq(info map[string]interface{}) *Info {
phoneInfo := info["phone"].(map[string]interface{})
ret := &Info{
Battery: int(info["battery"].(float64)),
Platform: info["platform"].(string),
Connected: info["connected"].(bool),
Pushname: info["pushname"].(string),
Wid: info["wid"].(string),
Lc: info["lc"].(string),
Phone: &PhoneInfo{
phoneInfo["mcc"].(string),
phoneInfo["mnc"].(string),
phoneInfo["os_version"].(string),
phoneInfo["device_manufacturer"].(string),
phoneInfo["device_model"].(string),
phoneInfo["os_build_number"].(string),
phoneInfo["wa_version"].(string),
},
Plugged: info["plugged"].(bool),
Lg: info["lg"].(string),
Tos: int(info["tos"].(float64)),
}
if is24h, ok := info["is24h"]; ok {
ret.Is24h = is24h.(bool)
}
return ret
}
/*
CheckCurrentServerVersion is based on the login method logic in order to establish the websocket connection and get
the current version from the server with the `admin init` command. This can be very useful for automations in which
you need to quickly perceive new versions (mostly patches) and update your application so it suddenly stops working.
*/
func CheckCurrentServerVersion() ([]int, error) {
wac, err := NewConn(5 * time.Second)
if err != nil {
return nil, fmt.Errorf("fail to create connection")
}
clientId := make([]byte, 16)
if _, err = rand.Read(clientId); err != nil {
return nil, fmt.Errorf("error creating random ClientId: %v", err)
}
b64ClientId := base64.StdEncoding.EncodeToString(clientId)
login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, b64ClientId, true}
loginChan, err := wac.writeJson(login)
if err != nil {
return nil, fmt.Errorf("error writing login: %s", err.Error())
}
// Retrieve an answer from the websocket
var r string
select {
case r = <-loginChan:
case <-time.After(wac.msgTimeout):
return nil, fmt.Errorf("login connection timed out")
}
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return nil, fmt.Errorf("error decoding login: %s", err.Error())
}
// Take the curr property as X.Y.Z and split it into as int slice
curr := resp["curr"].(string)
currArray := strings.Split(curr, ".")
version := make([]int, len(currArray))
for i := range version {
version[i], _ = strconv.Atoi(currArray[i])
}
return version, nil
}
/*
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, version 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, wac.clientVersion = long, short, version
return nil
}
/*
SetClientVersion sets WhatsApp client version
Default value is 0.4.2080
*/
func (wac *Conn) SetClientVersion(major int, minor int, patch int) {
waVersion = []int{major, minor, patch}
}
// GetClientVersion returns WhatsApp client version
func (wac *Conn) GetClientVersion() []int {
return waVersion
}
/*
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 RestoreWithSession the next time. Login takes a writable channel
as an parameter. This channel is used to push the data represented by the qr code back to the user. The received data
should be displayed as an qr code in a way you prefer. To print a qr code to console you can use:
github.com/Baozisoftware/qrcode-terminal-go Example login procedure:
wac, err := whatsapp.NewConn(5 * time.Second)
if err != nil {
panic(err)
}
qr := make(chan string)
go func() {
terminal := qrcodeTerminal.New()
terminal.Get(<-qr).Print()
}()
session, err := wac.Login(qr)
if err != nil {
fmt.Fprintf(os.Stderr, "error during login: %v\n", err)
}
fmt.Printf("login successful, session: %v\n", session)
*/
func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
session := Session{}
//Makes sure that only a single Login or Restore can happen at the same time
if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) {
return session, ErrLoginInProgress
}
defer atomic.StoreUint32(&wac.sessionLock, 0)
if wac.loggedIn {
return session, ErrAlreadyLoggedIn
}
if err := wac.connect(); err != nil && err != ErrAlreadyConnected {
return session, err
}
//logged in?!?
if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) {
return session, fmt.Errorf("already logged in")
}
clientId := make([]byte, 16)
_, err := rand.Read(clientId)
if err != nil {
return session, fmt.Errorf("error creating random ClientId: %v", err)
}
session.ClientId = base64.StdEncoding.EncodeToString(clientId)
login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, session.ClientId, true}
loginChan, err := wac.writeJson(login)
if err != nil {
return session, fmt.Errorf("error writing login: %v\n", err)
}
var r string
select {
case r = <-loginChan:
case <-time.After(wac.msgTimeout):
return session, fmt.Errorf("login connection timed out")
}
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return session, fmt.Errorf("error decoding login resp: %v\n", err)
}
var ref string
if rref, ok := resp["ref"].(string); ok {
ref = rref
} else {
return session, fmt.Errorf("error decoding login resp: invalid resp['ref']\n")
}
priv, pub, err := curve25519.GenerateKey()
if err != nil {
return session, fmt.Errorf("error generating keys: %v\n", err)
}
//listener for Login response
s1 := make(chan string, 1)
wac.listener.Lock()
wac.listener.m["s1"] = s1
wac.listener.Unlock()
qrChan <- fmt.Sprintf("%v,%v,%v", ref, base64.StdEncoding.EncodeToString(pub[:]), session.ClientId)
var resp2 []interface{}
select {
case r1 := <-s1:
wac.loginSessionLock.Lock()
defer wac.loginSessionLock.Unlock()
if err := json.Unmarshal([]byte(r1), &resp2); err != nil {
return session, fmt.Errorf("error decoding qr code resp: %v", err)
}
case <-time.After(time.Duration(resp["ttl"].(float64)) * time.Millisecond):
return session, fmt.Errorf("qr code scan timed out")
}
info := resp2[1].(map[string]interface{})
wac.Info = newInfoFromReq(info)
session.ClientToken = info["clientToken"].(string)
session.ServerToken = info["serverToken"].(string)
session.Wid = info["wid"].(string)
s := info["secret"].(string)
decodedSecret, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return session, fmt.Errorf("error decoding secret: %v", err)
}
var pubKey [32]byte
copy(pubKey[:], decodedSecret[:32])
sharedSecret := curve25519.GenerateSharedSecret(*priv, pubKey)
hash := sha256.New
nullKey := make([]byte, 32)
h := hmac.New(hash, nullKey)
h.Write(sharedSecret)
sharedSecretExtended, err := hkdf.Expand(h.Sum(nil), 80, "")
if err != nil {
return session, fmt.Errorf("hkdf error: %v", err)
}
//login validation
checkSecret := make([]byte, 112)
copy(checkSecret[:32], decodedSecret[:32])
copy(checkSecret[32:], decodedSecret[64:])
h2 := hmac.New(hash, sharedSecretExtended[32:64])
h2.Write(checkSecret)
if !hmac.Equal(h2.Sum(nil), decodedSecret[32:64]) {
return session, fmt.Errorf("abort login")
}
keysEncrypted := make([]byte, 96)
copy(keysEncrypted[:16], sharedSecretExtended[64:])
copy(keysEncrypted[16:], decodedSecret[64:])
keyDecrypted, err := cbc.Decrypt(sharedSecretExtended[:32], nil, keysEncrypted)
if err != nil {
return session, fmt.Errorf("error decryptAes: %v", err)
}
session.EncKey = keyDecrypted[:32]
session.MacKey = keyDecrypted[32:64]
wac.session = &session
wac.loggedIn = true
return session, nil
}
//TODO: GoDoc
/*
Basically the old RestoreSession functionality
*/
func (wac *Conn) RestoreWithSession(session Session) (_ Session, err error) {
if wac.loggedIn {
return Session{}, ErrAlreadyLoggedIn
}
old := wac.session
defer func() {
if err != nil {
wac.session = old
}
}()
wac.session = &session
if err = wac.Restore(); err != nil {
wac.session = nil
return Session{}, err
}
return *wac.session, nil
}
/*//TODO: GoDoc
RestoreWithSession is the function that restores a given session. It will try to reestablish the connection to the
WhatsAppWeb servers with the provided session. If it succeeds it will return a new session. This new session has to be
saved because the Client and Server-Token will change after every login. Logging in with old tokens is possible, but not
suggested. If so, a challenge has to be resolved which is just another possible point of failure.
*/
func (wac *Conn) Restore() error {
//Makes sure that only a single Login or Restore can happen at the same time
if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) {
return ErrLoginInProgress
}
defer atomic.StoreUint32(&wac.sessionLock, 0)
if wac.session == nil {
return ErrInvalidSession
}
if err := wac.connect(); err != nil && err != ErrAlreadyConnected {
return err
}
if wac.loggedIn {
return ErrAlreadyLoggedIn
}
//listener for Conn or challenge; s1 is not allowed to drop
s1 := make(chan string, 1)
wac.listener.Lock()
wac.listener.m["s1"] = s1
wac.listener.Unlock()
//admin init
init := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, wac.session.ClientId, true}
initChan, err := wac.writeJson(init)
if err != nil {
return fmt.Errorf("error writing admin init: %v\n", err)
}
//admin login with takeover
login := []interface{}{"admin", "login", wac.session.ClientToken, wac.session.ServerToken, wac.session.ClientId, "takeover"}
loginChan, err := wac.writeJson(login)
if err != nil {
return fmt.Errorf("error writing admin login: %v\n", err)
}
select {
case r := <-initChan:
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
wac.timeTag = ""
return fmt.Errorf("init responded with %d", resp["status"])
}
case <-time.After(wac.msgTimeout):
wac.timeTag = ""
return fmt.Errorf("restore session init timed out")
}
//wait for s1
var connResp []interface{}
select {
case r1 := <-s1:
if err := json.Unmarshal([]byte(r1), &connResp); err != nil {
wac.timeTag = ""
return fmt.Errorf("error decoding s1 message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
wac.timeTag = ""
//check for an error message
select {
case r := <-loginChan:
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("admin login responded with %d", int(resp["status"].(float64)))
}
default:
// not even an error message assume timeout
return fmt.Errorf("restore session connection timed out")
}
}
//check if challenge is present
if len(connResp) == 2 && connResp[0] == "Cmd" && connResp[1].(map[string]interface{})["type"] == "challenge" {
s2 := make(chan string, 1)
wac.listener.Lock()
wac.listener.m["s2"] = s2
wac.listener.Unlock()
if err := wac.resolveChallenge(connResp[1].(map[string]interface{})["challenge"].(string)); err != nil {
wac.timeTag = ""
return fmt.Errorf("error resolving challenge: %v\n", err)
}
select {
case r := <-s2:
if err := json.Unmarshal([]byte(r), &connResp); err != nil {
wac.timeTag = ""
return fmt.Errorf("error decoding s2 message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
wac.timeTag = ""
return fmt.Errorf("restore session challenge timed out")
}
}
//check for login 200 --> login success
select {
case r := <-loginChan:
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
wac.timeTag = ""
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
wac.timeTag = ""
return fmt.Errorf("admin login responded with %d", resp["status"])
}
case <-time.After(wac.msgTimeout):
wac.timeTag = ""
return fmt.Errorf("restore session login timed out")
}
info := connResp[1].(map[string]interface{})
wac.Info = newInfoFromReq(info)
//set new tokens
wac.session.ClientToken = info["clientToken"].(string)
wac.session.ServerToken = info["serverToken"].(string)
wac.session.Wid = info["wid"].(string)
wac.loggedIn = true
return nil
}
func (wac *Conn) resolveChallenge(challenge string) error {
decoded, err := base64.StdEncoding.DecodeString(challenge)
if err != nil {
return err
}
h2 := hmac.New(sha256.New, wac.session.MacKey)
h2.Write([]byte(decoded))
ch := []interface{}{"admin", "challenge", base64.StdEncoding.EncodeToString(h2.Sum(nil)), wac.session.ServerToken, wac.session.ClientId}
challengeChan, err := wac.writeJson(ch)
if err != nil {
return fmt.Errorf("error writing challenge: %v\n", err)
}
select {
case r := <-challengeChan:
var resp map[string]interface{}
if err := json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login resp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("challenge responded with %d\n", resp["status"])
}
case <-time.After(wac.msgTimeout):
return fmt.Errorf("connection timed out")
}
return nil
}
/*
Logout is the function to logout from a WhatsApp session. Logging out means invalidating the current session.
The session can not be resumed and will disappear on your phone in the WhatsAppWeb client list.
*/
func (wac *Conn) Logout() error {
login := []interface{}{"admin", "Conn", "disconnect"}
_, err := wac.writeJson(login)
if err != nil {
return fmt.Errorf("error writing logout: %v\n", err)
}
wac.loggedIn = false
return nil
}

80
vendor/github.com/Rhymen/go-whatsapp/store.go generated vendored Normal file
View File

@ -0,0 +1,80 @@
package whatsapp
import (
"github.com/Rhymen/go-whatsapp/binary"
"strings"
)
type Store struct {
Contacts map[string]Contact
Chats map[string]Chat
}
type Contact struct {
Jid string
Notify string
Name string
Short string
}
type Chat struct {
Jid string
Name string
Unread string
LastMessageTime string
IsMuted string
IsMarkedSpam string
}
func newStore() *Store {
return &Store{
make(map[string]Contact),
make(map[string]Chat),
}
}
func (wac *Conn) updateContacts(contacts interface{}) {
c, ok := contacts.([]interface{})
if !ok {
return
}
for _, contact := range c {
contactNode, ok := contact.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(contactNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
wac.Store.Contacts[jid] = Contact{
jid,
contactNode.Attributes["notify"],
contactNode.Attributes["name"],
contactNode.Attributes["short"],
}
}
}
func (wac *Conn) updateChats(chats interface{}) {
c, ok := chats.([]interface{})
if !ok {
return
}
for _, chat := range c {
chatNode, ok := chat.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(chatNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
wac.Store.Chats[jid] = Chat{
jid,
chatNode.Attributes["name"],
chatNode.Attributes["count"],
chatNode.Attributes["t"],
chatNode.Attributes["mute"],
chatNode.Attributes["spam"],
}
}
}

195
vendor/github.com/Rhymen/go-whatsapp/write.go generated vendored Normal file
View File

@ -0,0 +1,195 @@
package whatsapp
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
func (wac *Conn) addListener(ch chan string, messageTag string) {
wac.listener.Lock()
wac.listener.m[messageTag] = ch
wac.listener.Unlock()
}
func (wac *Conn) removeListener(answerMessageTag string) {
wac.listener.Lock()
delete(wac.listener.m, answerMessageTag)
wac.listener.Unlock()
}
//writeJson enqueues a json message into the writeChan
func (wac *Conn) writeJson(data []interface{}) (<-chan string, error) {
ch := make(chan string, 1)
wac.writerLock.Lock()
defer wac.writerLock.Unlock()
d, err := json.Marshal(data)
if err != nil {
close(ch)
return ch, err
}
ts := time.Now().Unix()
messageTag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
bytes := []byte(fmt.Sprintf("%s,%s", messageTag, d))
if wac.timeTag == "" {
tss := fmt.Sprintf("%d", ts)
wac.timeTag = tss[len(tss)-3:]
}
wac.addListener(ch, messageTag)
err = wac.write(websocket.TextMessage, bytes)
if err != nil {
close(ch)
wac.removeListener(messageTag)
return ch, err
}
wac.msgCount++
return ch, nil
}
func (wac *Conn) writeBinary(node binary.Node, metric metric, flag flag, messageTag string) (<-chan string, error) {
ch := make(chan string, 1)
if len(messageTag) < 2 {
close(ch)
return ch, ErrMissingMessageTag
}
wac.writerLock.Lock()
defer wac.writerLock.Unlock()
data, err := wac.encryptBinaryMessage(node)
if err != nil {
close(ch)
return ch, errors.Wrap(err, "encryptBinaryMessage(node) failed")
}
bytes := []byte(messageTag + ",")
bytes = append(bytes, byte(metric), byte(flag))
bytes = append(bytes, data...)
wac.addListener(ch, messageTag)
err = wac.write(websocket.BinaryMessage, bytes)
if err != nil {
close(ch)
wac.removeListener(messageTag)
return ch, errors.Wrap(err, "failed to write message")
}
wac.msgCount++
return ch, nil
}
func (wac *Conn) sendKeepAlive() error {
respChan := make(chan string, 1)
wac.addListener(respChan, "!")
bytes := []byte("?,,")
err := wac.write(websocket.TextMessage, bytes)
if err != nil {
close(respChan)
wac.removeListener("!")
return errors.Wrap(err, "error sending keepAlive")
}
select {
case resp := <-respChan:
msecs, err := strconv.ParseInt(resp, 10, 64)
if err != nil {
return errors.Wrap(err, "Error converting time string to uint")
}
wac.ServerLastSeen = time.Unix(msecs/1000, (msecs%1000)*int64(time.Millisecond))
case <-time.After(wac.msgTimeout):
return ErrConnectionTimeout
}
return nil
}
/*
When phone is unreachable, WhatsAppWeb sends ["admin","test"] time after time to try a successful contact.
Tested with Airplane mode and no connection at all.
*/
func (wac *Conn) sendAdminTest() (bool, error) {
data := []interface{}{"admin", "test"}
r, err := wac.writeJson(data)
if err != nil {
return false, errors.Wrap(err, "error sending admin test")
}
var response []interface{}
select {
case resp := <-r:
if err := json.Unmarshal([]byte(resp), &response); err != nil {
return false, fmt.Errorf("error decoding response message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
return false, ErrConnectionTimeout
}
if len(response) == 2 && response[0].(string) == "Pong" && response[1].(bool) == true {
return true, nil
} else {
return false, nil
}
}
func (wac *Conn) write(messageType int, data []byte) error {
if wac == nil || wac.ws == nil {
return ErrInvalidWebsocket
}
wac.ws.Lock()
err := wac.ws.conn.WriteMessage(messageType, data)
wac.ws.Unlock()
if err != nil {
return errors.Wrap(err, "error writing to websocket")
}
return nil
}
func (wac *Conn) encryptBinaryMessage(node binary.Node) (data []byte, err error) {
b, err := binary.Marshal(node)
if err != nil {
return nil, errors.Wrap(err, "binary node marshal failed")
}
cipher, err := cbc.Encrypt(wac.session.EncKey, nil, b)
if err != nil {
return nil, errors.Wrap(err, "encrypt failed")
}
h := hmac.New(sha256.New, wac.session.MacKey)
h.Write(cipher)
hash := h.Sum(nil)
data = append(data, hash[:32]...)
data = append(data, cipher...)
return data, nil
}