Add whatsappmulti buildflag for whatsapp with multidevice support (whatsapp)

pull/1773/head
Wim 2 years ago
parent 2623a412c4
commit 496d5b4ec7

@ -4,104 +4,126 @@ import (
"fmt"
"mime"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"github.com/Rhymen/go-whatsapp"
"github.com/jpillora/backoff"
)
// nolint:gocritic
func (b *Bwhatsapp) eventHandler(evt interface{}) {
switch e := evt.(type) {
case *events.Message:
b.handleMessage(e)
/*
Implement handling messages coming from WhatsApp
Check:
- https://github.com/Rhymen/go-whatsapp#add-message-handlers
- https://github.com/Rhymen/go-whatsapp/blob/master/handler.go
- https://github.com/tulir/mautrix-whatsapp/tree/master/whatsapp-ext for more advanced command handling
*/
// HandleError received from WhatsApp
func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
if strings.Contains(err.Error(), "error processing data: received invalid data") ||
strings.Contains(err.Error(), "invalid string with tag 174") {
return
}
switch err.(type) {
case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed:
b.reconnect(err)
default:
switch err {
case whatsapp.ErrConnectionTimeout:
b.reconnect(err)
default:
b.Log.Errorf("%v", err)
}
}
}
func (b *Bwhatsapp) handleMessage(message *events.Message) {
msg := message.Message
switch {
case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt):
return
func (b *Bwhatsapp) reconnect(err error) {
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
b.Log.Infof("Receiving message %#v", msg)
for {
d := bf.Duration()
switch {
case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
b.handleTextMessage(message.Info, msg)
case msg.VideoMessage != nil:
b.handleVideoMessage(message)
case msg.AudioMessage != nil:
b.handleAudioMessage(message)
case msg.DocumentMessage != nil:
b.handleDocumentMessage(message)
case msg.ImageMessage != nil:
b.handleImageMessage(message)
}
}
b.Log.Errorf("Connection failed, underlying error: %v", err)
b.Log.Infof("Waiting %s...", d)
// nolint:funlen
func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) {
senderJID := messageInfo.Sender
channel := messageInfo.Chat
time.Sleep(d)
senderName := b.getSenderName(messageInfo.Sender)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
b.Log.Info("Reconnecting...")
err := b.conn.Restore()
if err == nil {
bf.Reset()
b.startedAt = uint64(time.Now().Unix())
return
}
}
}
if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" {
b.Log.Debugf("message without text content? %#v", msg)
// HandleTextMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if message.Info.FromMe {
return
}
// whatsapp sends last messages to show context , cut them
if message.Info.Timestamp < b.startedAt {
return
}
var text string
// nolint:nestif
if msg.GetExtendedTextMessage() == nil {
text = msg.GetConversation()
} else {
text = msg.GetExtendedTextMessage().GetText()
ci := msg.GetExtendedTextMessage().GetContextInfo()
groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
if len(senderJID) == 0 {
if message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
}
if ci.MentionedJid != nil {
// handle user mentions
for _, mentionedJID := range ci.MentionedJid {
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
// translate sender's JID to the nicest username we can get
senderName := b.getSenderName(senderJID)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
// mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others
mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer))
if mention == "" {
mention = "someone"
}
extText := message.Info.Source.Message.ExtendedTextMessage
if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
// handle user mentions
for _, mentionedJID := range extText.ContextInfo.MentionedJid {
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1)
// mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others
mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net")
if mention == "" {
mention = "someone"
}
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
rmsg := config.Message{
UserID: senderJID.String(),
UserID: senderJID,
Username: senderName,
Text: text,
Channel: channel.String(),
Text: message.Text,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
ID: messageInfo.ID,
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
@ -112,32 +134,36 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
}
// HandleImageMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
imsg := msg.Message.GetImageMessage()
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID.String(),
UserID: senderJID,
Username: senderName,
Channel: msg.Info.Chat.String(),
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
@ -154,11 +180,11 @@ func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
fileExt[0] = ".jpg"
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype())
b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
data, err := b.wc.Download(imsg)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download image failed: %s", err)
@ -166,7 +192,7 @@ func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
@ -175,32 +201,36 @@ func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
}
// HandleVideoMessage downloads video messages
func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
imsg := msg.Message.GetVideoMessage()
func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID.String(),
UserID: senderJID,
Username: senderName,
Channel: msg.Info.Chat.String(),
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
@ -211,11 +241,11 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
fileExt = append(fileExt, ".mp4")
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := b.wc.Download(imsg)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
@ -223,7 +253,7 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
@ -232,32 +262,36 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
}
// HandleAudioMessage downloads audio messages
func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
imsg := msg.Message.GetAudioMessage()
func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID.String(),
UserID: senderJID,
Username: senderName,
Channel: msg.Info.Chat.String(),
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
@ -268,13 +302,13 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
fileExt = append(fileExt, ".ogg")
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := b.wc.Download(imsg)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
b.Log.Errorf("Download audio failed: %s", err)
return
}
@ -289,43 +323,47 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
}
// HandleDocumentMessage downloads documents
func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
imsg := msg.Message.GetDocumentMessage()
func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID.String(),
UserID: senderJID,
Username: senderName,
Channel: msg.Info.Chat.String(),
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v", imsg.GetFileName())
filename := fmt.Sprintf("%v", message.FileName)
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype())
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
data, err := b.wc.Download(imsg)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download document message failed: %s", err)

@ -1,12 +1,15 @@
package bwhatsapp
import (
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Rhymen/go-whatsapp"
)
type ProfilePicInfo struct {
@ -15,71 +18,141 @@ type ProfilePicInfo struct {
Status int16 `json:"status"`
}
func (b *Bwhatsapp) getSenderName(senderJid types.JID) string {
if sender, exists := b.contacts[senderJid]; exists {
if sender.FullName != "" {
return sender.FullName
}
// if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
// users can change it in their WhatsApp settings -> profile -> click on Avatar
if sender.PushName != "" {
return sender.PushName
}
func qrFromTerminal(invert bool) chan string {
qr := make(chan string)
go func() {
terminal := qrcodeTerminal.New()
if sender.FirstName != "" {
return sender.FirstName
if invert {
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
}
terminal.Get(<-qr).Print()
}()
return qr
}
func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
session := whatsapp.Session{}
sessionFile := b.Config.GetString(sessionFile)
if sessionFile == "" {
return session, errors.New("if you won't set SessionFile then you will need to scan QR code on every restart")
}
// try to reload this contact
if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
file, err := os.Open(sessionFile)
if err != nil {
return session, err
}
allcontacts, err := b.wc.Store.Contacts.GetAllContacts()
defer file.Close()
decoder := gob.NewDecoder(file)
return session, decoder.Decode(&session)
}
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
sessionFile := b.Config.GetString(sessionFile)
if sessionFile == "" {
// we already sent a warning while starting the bridge, so let's be quiet here
return nil
}
file, err := os.Create(sessionFile)
if err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
return encoder.Encode(session)
}
func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
session, err := b.readSession()
if err != nil {
b.Log.Warn(err.Error())
}
if len(allcontacts) > 0 {
b.contacts = allcontacts
b.Log.Debugln("Restoring WhatsApp session..")
session, err = b.conn.RestoreWithSession(session)
if err != nil {
// restore session connection timed out (I couldn't get over it without logging in again)
return nil, errors.New("failed to restore session: " + err.Error())
}
if sender, exists := b.contacts[senderJid]; exists {
if sender.FullName != "" {
return sender.FullName
b.Log.Debugln("Session restored successfully!")
return &session, nil
}
func (b *Bwhatsapp) getSenderName(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
if sender.Name != "" {
return sender.Name
}
// if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
// users can change it in their WhatsApp settings -> profile -> click on Avatar
if sender.PushName != "" {
return sender.PushName
if sender.Notify != "" {
return sender.Notify
}
if sender.FirstName != "" {
return sender.FirstName
if sender.Short != "" {
return sender.Short
}
}
return "Someone"
// try to reload this contact
_, err := b.conn.Contacts()
if err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
if contact, exists := b.conn.Store.Contacts[senderJid]; exists {
// Add it to the user map
b.users[senderJid] = contact
if contact.Name != "" {
return contact.Name
}
// if user is not in phone contacts
// same as above
return contact.Notify
}
return ""
}
func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
if sender, exists := b.contacts[senderJid]; exists {
return sender.PushName
func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
return sender.Notify
}
return ""
}
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) {
pjid, _ := types.ParseJID(jid)
info, err := b.wc.GetProfilePictureInfo(pjid, true)
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
data, err := b.conn.GetProfilePicThumb(jid)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}
@ -88,19 +161,3 @@ func isGroupJid(identifier string) bool {
strings.HasSuffix(identifier, "@temp") ||
strings.HasSuffix(identifier, "@broadcast")
}
func (b *Bwhatsapp) getDevice() (*store.Device, error) {
device := &store.Device{}
storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_foreign_keys=on&_pragma=busy_timeout=10000", nil)
if err != nil {
return device, fmt.Errorf("failed to connect to database: %v", err)
}
device, err = storeContainer.GetFirstDevice()
if err != nil {
return device, fmt.Errorf("failed to get device: %v", err)
}
return device, nil
}

@ -1,41 +1,38 @@
package bwhatsapp
import (
"context"
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"mime"
"os"
"path/filepath"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/mdp/qrterminal"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log"
goproto "google.golang.org/protobuf/proto"
_ "modernc.org/sqlite" // needed for sqlite
"github.com/Rhymen/go-whatsapp"
)
const (
// Account config parameters
cfgNumber = "Number"
cfgNumber = "Number"
qrOnWhiteTerminal = "QrOnWhiteTerminal"
sessionFile = "SessionFile"
)
// Bwhatsapp Bridge structure keeping all the information needed for relying
type Bwhatsapp struct {
*bridge.Config
startedAt time.Time
wc *whatsmeow.Client
contacts map[types.JID]types.ContactInfo
users map[string]types.ContactInfo
session *whatsapp.Session
conn *whatsapp.Conn
startedAt uint64
users map[string]whatsapp.Contact
userAvatars map[string]string
}
@ -50,7 +47,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
b := &Bwhatsapp{
Config: cfg,
users: make(map[string]types.ContactInfo),
users: make(map[string]whatsapp.Contact),
userAvatars: make(map[string]string),
}
@ -59,92 +56,101 @@ func New(cfg *bridge.Config) bridge.Bridger {
// Connect to WhatsApp. Required implementation of the Bridger interface
func (b *Bwhatsapp) Connect() error {
device, err := b.getDevice()
if err != nil {
return err
}
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("whatsapp's telephone number need to be configured")
}
b.Log.Debugln("Connecting to WhatsApp..")
conn, err := whatsapp.NewConn(20 * time.Second)
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
b.wc.AddEventHandler(b.eventHandler)
b.conn = conn
firstlogin := false
var qrChan <-chan whatsmeow.QRChannelItem
if b.wc.Store.ID == nil {
firstlogin = true
qrChan, err = b.wc.GetQRChannel(context.Background())
if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
return errors.New("failed to to get QR channel:" + err.Error())
}
}
b.conn.AddHandler(b)
b.Log.Debugln("WhatsApp connection successful")
err = b.wc.Connect()
// load existing session in order to keep it between restarts
b.session, err = b.restoreSession()
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
b.Log.Warn(err.Error())
}
if b.wc.Store.ID == nil {
for evt := range qrChan {
if evt.Event == "code" {
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
} else {
b.Log.Infof("QR channel result: %s", evt.Event)
}
// login to a new session
if b.session == nil {
if err = b.Login(); err != nil {
return err
}
}
// disconnect and reconnect on our first login/pairing
// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login
if firstlogin {
b.wc.Disconnect()
time.Sleep(time.Second)
b.startedAt = uint64(time.Now().Unix())
err = b.wc.Connect()
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
_, err = b.conn.Contacts()
if err != nil {
return fmt.Errorf("error on update of contacts: %v", err)
}
b.Log.Infoln("WhatsApp connection successful")
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck
b.contacts, err = b.wc.Store.Contacts.GetAllContacts()
if err != nil {
return errors.New("failed to get contacts: " + err.Error())
<-time.After(1 * time.Second)
}
b.startedAt = time.Now()
// map all the users
for id, contact := range b.contacts {
if !isGroupJid(id.String()) && id.String() != "status@broadcast" {
for id, contact := range b.conn.Store.Contacts {
if !isGroupJid(id) && id != "status@broadcast" {
// it is user
b.users[id.String()] = contact
b.users[id] = contact
}
}
// get user avatar asynchronously
b.Log.Info("Getting user avatars..")
go func() {
b.Log.Debug("Getting user avatars..")
for jid := range b.users {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
b.Lock()
if info != nil {
for jid := range b.users {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
b.Lock()
b.userAvatars[jid] = info.URL
b.Unlock()
}
b.Unlock()
}
b.Log.Debug("Finished getting avatars..")
}()
return nil
}
// Login to WhatsApp creating a new session. This will require to scan a QR code on your mobile device
func (b *Bwhatsapp) Login() error {
b.Log.Debugln("Logging in..")
invert := b.GetBool(qrOnWhiteTerminal) // false is the default
qrChan := qrFromTerminal(invert)
session, err := b.conn.Login(qrChan)
if err != nil {
b.Log.Warnln("Failed to log in:", err)
return err
}
b.Log.Info("Finished getting avatars..")
b.session = &session
b.Log.Infof("Logged into session: %#v", session)
b.Log.Infof("Connection: %#v", b.conn)
err = b.writeSession(session)
if err != nil {
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
}
return nil
}
@ -152,8 +158,8 @@ func (b *Bwhatsapp) Connect() error {
// Disconnect is called while reconnecting to the bridge
// Required implementation of the Bridger interface
func (b *Bwhatsapp) Disconnect() error {
b.wc.Disconnect()
// We could Logout, but that would close the session completely and would require a new QR code scan
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
return nil
}
@ -163,118 +169,111 @@ func (b *Bwhatsapp) Disconnect() error {
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
groups, err := b.wc.GetJoinedGroups()
if err != nil {
return err
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck
<-time.After(1 * time.Second)
}
// verify if we are member of the given group
if byJid {
gJID, err := types.ParseJID(channel.Name)
if err != nil {
return err
// channel.Name specifies static group jID, not the name
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
}
for _, group := range groups {
if group.JID == gJID {
return nil
}
}
return nil
}
foundGroups := []string{}
for _, group := range groups {
if group.Name == channel.Name {
foundGroups = append(foundGroups, group.Name)
// channel.Name specifies group name that might change, warn about it
var jids []string
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) && contact.Name == channel.Name {
jids = append(jids, id)
}
}
switch len(foundGroups) {
switch len(jids) {
case 0:
// didn't match any group - print out possibilites
for _, group := range groups {
b.Log.Infof("%s %s", group.JID, group.Name)
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) {
b.Log.Infof("%s %s", contact.Jid, contact.Name)
}
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name)
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups)
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
}
}
// Post a document message from the bridge to WhatsApp
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument)
if err != nil {
return "", err
}
// Post document message
var message proto.Message
message.DocumentMessage = &proto.DocumentMessage{
Title: &fi.Name,
FileName: &fi.Name,
Mimetype: &filetype,
MediaKey: resp.MediaKey,
FileEncSha256: resp.FileEncSHA256,
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
message := whatsapp.DocumentMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel,
},
Title: fi.Name,
FileName: fi.Name,
Type: filetype,
Content: bytes.NewReader(*fi.Data),
}
b.Log.Debugf("=> Sending %#v", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(groupJID, ID, &message)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return ID, err
return message.Info.Id, err
}
// Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage)
if err != nil {
return "", err
// Post image message
message := whatsapp.ImageMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel,
},
Type: filetype,
Caption: msg.Username + fi.Comment,
Content: bytes.NewReader(*fi.Data),
}
var message proto.Message
b.Log.Debugf("=> Sending %#v", msg)
message.ImageMessage = &proto.ImageMessage{
Mimetype: &filetype,
Caption: &caption,
MediaKey: resp.MediaKey,
FileEncSha256: resp.FileEncSHA256,
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
b.Log.Debugf("=> Sending %#v", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(groupJID, ID, &message)
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return ID, err
return message.Info.Id, err
}
// Send a message from the bridge to WhatsApp
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
@ -285,7 +284,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
return "", nil
}
_, err := b.wc.RevokeMessage(groupJID, msg.ID)
_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
return "", err
}
@ -318,14 +317,20 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
}
}
text := msg.Username + msg.Text
var message proto.Message
message.Conversation = &text
// Post text message
message := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel, // which equals to group id
},
Text: msg.Username + msg.Text,
}
ID := whatsmeow.GenerateMessageID()
_, err := b.wc.SendMessage(groupJID, ID, &message)
b.Log.Debugf("=> Sending %#v", msg)
return ID, err
return b.conn.Send(message)
}
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
//func (b *Bwhatsapp) Command(cmd string) string {
// return ""
//}

@ -0,0 +1,344 @@
// +build whatsappmulti
package bwhatsapp
import (
"fmt"
"mime"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
// nolint:gocritic
func (b *Bwhatsapp) eventHandler(evt interface{}) {
switch e := evt.(type) {
case *events.Message:
b.handleMessage(e)
}
}
func (b *Bwhatsapp) handleMessage(message *events.Message) {
msg := message.Message
switch {
case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt):
return
}
b.Log.Infof("Receiving message %#v", msg)
switch {
case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
b.handleTextMessage(message.Info, msg)
case msg.VideoMessage != nil:
b.handleVideoMessage(message)
case msg.AudioMessage != nil:
b.handleAudioMessage(message)
case msg.DocumentMessage != nil:
b.handleDocumentMessage(message)
case msg.ImageMessage != nil:
b.handleImageMessage(message)
}
}
// nolint:funlen
func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) {
senderJID := messageInfo.Sender
channel := messageInfo.Chat
senderName := b.getSenderName(messageInfo.Sender)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" {
b.Log.Debugf("message without text content? %#v", msg)
return
}
var text string
// nolint:nestif
if msg.GetExtendedTextMessage() == nil {
text = msg.GetConversation()
} else {
text = msg.GetExtendedTextMessage().GetText()
ci := msg.GetExtendedTextMessage().GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
if ci.MentionedJid != nil {
// handle user mentions
for _, mentionedJID := range ci.MentionedJid {
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
// mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others
mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer))
if mention == "" {
mention = "someone"
}
text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Text: text,
Channel: channel.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
ID: messageInfo.ID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleImageMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
imsg := msg.Message.GetImageMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
if fileExt[0] == ".jfif" {
fileExt[0] = ".jpg"
}
// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
if fileExt[0] == ".jpe" {
fileExt[0] = ".jpg"
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download image failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleVideoMessage downloads video messages
func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
imsg := msg.Message.GetVideoMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".mp4")
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleAudioMessage downloads audio messages
func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
imsg := msg.Message.GetAudioMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".ogg")
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleDocumentMessage downloads documents
func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
imsg := msg.Message.GetDocumentMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(senderJID)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v", imsg.GetFileName())
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download document message failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

@ -0,0 +1,108 @@
// +build whatsappmulti
package bwhatsapp
import (
"fmt"
"strings"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
)
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
}
func (b *Bwhatsapp) getSenderName(senderJid types.JID) string {
if sender, exists := b.contacts[senderJid]; exists {
if sender.FullName != "" {
return sender.FullName
}
// if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
// users can change it in their WhatsApp settings -> profile -> click on Avatar
if sender.PushName != "" {
return sender.PushName
}
if sender.FirstName != "" {
return sender.FirstName
}
}
// try to reload this contact
if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
allcontacts, err := b.wc.Store.Contacts.GetAllContacts()
if err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
if len(allcontacts) > 0 {
b.contacts = allcontacts
}
if sender, exists := b.contacts[senderJid]; exists {
if sender.FullName != "" {
return sender.FullName
}
// if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
// users can change it in their WhatsApp settings -> profile -> click on Avatar
if sender.PushName != "" {
return sender.PushName
}
if sender.FirstName != "" {
return sender.FirstName
}
}
return "Someone"
}
func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
if sender, exists := b.contacts[senderJid]; exists {
return sender.PushName
}
return ""
}
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) {
pjid, _ := types.ParseJID(jid)
info, err := b.wc.GetProfilePictureInfo(pjid, true)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
return info, nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") ||
strings.HasSuffix(identifier, "@temp") ||
strings.HasSuffix(identifier, "@broadcast")
}
func (b *Bwhatsapp) getDevice() (*store.Device, error) {
device := &store.Device{}
storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_foreign_keys=on&_pragma=busy_timeout=10000", nil)
if err != nil {
return device, fmt.Errorf("failed to connect to database: %v", err)
}
device, err = storeContainer.GetFirstDevice()
if err != nil {
return device, fmt.Errorf("failed to get device: %v", err)
}
return device, nil
}

@ -0,0 +1,333 @@
// +build whatsappmulti
package bwhatsapp
import (
"context"
"errors"
"fmt"
"mime"
"os"
"path/filepath"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/mdp/qrterminal"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log"
goproto "google.golang.org/protobuf/proto"
_ "modernc.org/sqlite" // needed for sqlite
)
const (
// Account config parameters
cfgNumber = "Number"
)
// Bwhatsapp Bridge structure keeping all the information needed for relying
type Bwhatsapp struct {
*bridge.Config
startedAt time.Time
wc *whatsmeow.Client
contacts map[types.JID]types.ContactInfo
users map[string]types.ContactInfo
userAvatars map[string]string
}
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
}
b := &Bwhatsapp{
Config: cfg,
users: make(map[string]types.ContactInfo),
userAvatars: make(map[string]string),
}
return b
}
// Connect to WhatsApp. Required implementation of the Bridger interface
func (b *Bwhatsapp) Connect() error {
device, err := b.getDevice()
if err != nil {
return err
}
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("whatsapp's telephone number need to be configured")
}
b.Log.Debugln("Connecting to WhatsApp..")
b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
b.wc.AddEventHandler(b.eventHandler)
firstlogin := false
var qrChan <-chan whatsmeow.QRChannelItem
if b.wc.Store.ID == nil {
firstlogin = true
qrChan, err = b.wc.GetQRChannel(context.Background())
if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
return errors.New("failed to to get QR channel:" + err.Error())
}
}
err = b.wc.Connect()
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
if b.wc.Store.ID == nil {
for evt := range qrChan {
if evt.Event == "code" {
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
} else {
b.Log.Infof("QR channel result: %s", evt.Event)
}
}
}
// disconnect and reconnect on our first login/pairing
// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login
if firstlogin {
b.wc.Disconnect()
time.Sleep(time.Second)
err = b.wc.Connect()
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
}
b.Log.Infoln("WhatsApp connection successful")
b.contacts, err = b.wc.Store.Contacts.GetAllContacts()
if err != nil {
return errors.New("failed to get contacts: " + err.Error())
}
b.startedAt = time.Now()
// map all the users
for id, contact := range b.contacts {
if !isGroupJid(id.String()) && id.String() != "status@broadcast" {
// it is user
b.users[id.String()] = contact
}
}
// get user avatar asynchronously
b.Log.Info("Getting user avatars..")
for jid := range b.users {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
b.Lock()
if info != nil {
b.userAvatars[jid] = info.URL
}
b.Unlock()
}
}
b.Log.Info("Finished getting avatars..")
return nil
}
// Disconnect is called while reconnecting to the bridge
// Required implementation of the Bridger interface
func (b *Bwhatsapp) Disconnect() error {
b.wc.Disconnect()
return nil
}
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
groups, err := b.wc.GetJoinedGroups()
if err != nil {
return err
}
// verify if we are member of the given group
if byJid {
gJID, err := types.ParseJID(channel.Name)
if err != nil {
return err
}
for _, group := range groups {
if group.JID == gJID {
return nil
}
}
}
foundGroups := []string{}
for _, group := range groups {
if group.Name == channel.Name {
foundGroups = append(foundGroups, group.Name)
}
}
switch len(foundGroups) {
case 0:
// didn't match any group - print out possibilites
for _, group := range groups {
b.Log.Infof("%s %s", group.JID, group.Name)
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups)
}
}
// Post a document message from the bridge to WhatsApp
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument)
if err != nil {
return "", err
}
// Post document message
var message proto.Message
message.DocumentMessage = &proto.DocumentMessage{
Title: &fi.Name,
FileName: &fi.Name,
Mimetype: &filetype,
MediaKey: resp.MediaKey,
FileEncSha256: resp.FileEncSHA256,
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
}
b.Log.Debugf("=> Sending %#v", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(groupJID, ID, &message)
return ID, err
}
// Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage)
if err != nil {
return "", err
}
var message proto.Message
message.ImageMessage = &proto.ImageMessage{
Mimetype: &filetype,
Caption: &caption,
MediaKey: resp.MediaKey,
FileEncSha256: resp.FileEncSHA256,
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
}
b.Log.Debugf("=> Sending %#v", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(groupJID, ID, &message)
return ID, err
}
// Send a message from the bridge to WhatsApp
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
return "", nil
}
_, err := b.wc.RevokeMessage(groupJID, msg.ID)
return "", err
}
// Edit message
if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID)
if b.GetString("editsuffix") != "" {
msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)"
}
}
// Handle Upload a file
if msg.Extra["file"] != nil {
fi := msg.Extra["file"][0].(config.FileInfo)
filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
b.Log.Debugf("Extra file is %#v", filetype)
// TODO: add different types
// TODO: add webp conversion
switch filetype {
case "image/jpeg", "image/png", "image/gif":
return b.PostImageMessage(msg, filetype)
default:
return b.PostDocumentMessage(msg, filetype)
}
}
text := msg.Username + msg.Text
var message proto.Message
message.Conversation = &text
ID := whatsmeow.GenerateMessageID()
_, err := b.wc.SendMessage(groupJID, ID, &message)
return ID, err
}

@ -1,4 +1,5 @@
// +build !nowhatsapp
// +build !whatsappmulti
package bridgemap

@ -0,0 +1,11 @@
// +build whatsappmulti
package bridgemap
import (
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsappmulti"
)
func init() {
FullMap["whatsapp"] = bwhatsapp.New
}

@ -2,8 +2,10 @@ module github.com/42wim/matterbridge
require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
github.com/SevereCloud/vksdk/v2 v2.13.1
github.com/bwmarrin/discordgo v0.24.0
github.com/d5/tengo/v2 v2.10.1
@ -110,6 +112,7 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect

@ -90,6 +90,8 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 h1:+wrfJITuBoQOE6ST4k3c4EortNVQXVhfAbwt0M/j0+Y=
github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989/go.mod h1:aDWSWjsayFyGTvHZH3v4ijGXEBe51xcEkAK+NUWeOeo=
github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f h1:aUkwZDEMJIGRcWlSDifSLoKG37UCOH/DPeG52/xwois=
@ -144,6 +146,8 @@ github.com/PuerkitoBio/goquery v1.7.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBK
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c h1:4mIZQXKYBymQ9coA82nNyG/CjicMNLBZ8cPVrhNUM3g=
github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c/go.mod h1:DNSFRLFDFIqm2+0aJzSOVfn25020vldM4SRqz6YtLgI=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v0.8.0/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
@ -1497,6 +1501,8 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 h1:A7o8tOERTtpD/poS+2VoassCjXpjHn916luXbf5QKD0=
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882/go.mod h1:5IwJoz9Pw7JsrCN4/skkxUtSWT7myuUPLhCgv6Q5vvQ=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/slack-go/slack v0.10.2 h1:KMN/h2sgUninHXvQI8PrR/PHBUuWp2NPvz2Kr66tki4=
github.com/slack-go/slack v0.10.2/go.mod h1:5FLdBRv7VW/d9EBxx/eEktOptWygbA9K2QK/KW7ds1s=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=

@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2017, Baozisoftware
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,39 @@
# qrcode-terminal-go
QRCode terminal for golang.
# Example
```go
package main
import "github.com/Baozisoftware/qrcode-terminal-go"
func main() {
Test1()
Test2()
}
func Test1(){
content := "Hello, 世界"
obj := qrcodeTerminal.New()
obj.Get(content).Print()
}
func Test2(){
content := "https://github.com/Baozisoftware/qrcode-terminal-go"
obj := qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightBlue,qrcodeTerminal.ConsoleColors.BrightGreen,qrcodeTerminal.QRCodeRecoveryLevels.Low)
obj.Get([]byte(content)).Print()
}
```
## Screenshots
### Windows XP
![winxp](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/winxp.png)
### Windows 7
![win7](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/win7.png)
### Windows 10
![win10](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/win10.png)
### Ubuntu
![ubuntu](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/ubuntu.png)
### macOS
![macos](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/macos.png)

@ -0,0 +1,155 @@
package qrcodeTerminal
import (
"fmt"
"github.com/skip2/go-qrcode"
"github.com/mattn/go-colorable"
"image/png"
nbytes "bytes"
)
type consoleColor string
type consoleColors struct {
NormalBlack consoleColor
NormalRed consoleColor
NormalGreen consoleColor
NormalYellow consoleColor
NormalBlue consoleColor
NormalMagenta consoleColor
NormalCyan consoleColor
NormalWhite consoleColor
BrightBlack consoleColor
BrightRed consoleColor
BrightGreen consoleColor
BrightYellow consoleColor
BrightBlue consoleColor
BrightMagenta consoleColor
BrightCyan consoleColor
BrightWhite consoleColor
}
type qrcodeRecoveryLevel qrcode.RecoveryLevel
type qrcodeRecoveryLevels struct {
Low qrcodeRecoveryLevel
Medium qrcodeRecoveryLevel
High qrcodeRecoveryLevel
Highest qrcodeRecoveryLevel
}
var (
ConsoleColors consoleColors = consoleColors{
NormalBlack: "\033[38;5;0m \033[0m",
NormalRed: "\033[38;5;1m \033[0m",
NormalGreen: "\033[38;5;2m \033[0m",
NormalYellow: "\033[38;5;3m \033[0m",
NormalBlue: "\033[38;5;4m \033[0m",
NormalMagenta: "\033[38;5;5m \033[0m",
NormalCyan: "\033[38;5;6m \033[0m",
NormalWhite: "\033[38;5;7m \033[0m",
BrightBlack: "\033[48;5;0m \033[0m",
BrightRed: "\033[48;5;1m \033[0m",
BrightGreen: "\033[48;5;2m \033[0m",
BrightYellow: "\033[48;5;3m \033[0m",
BrightBlue: "\033[48;5;4m \033[0m",
BrightMagenta: "\033[48;5;5m \033[0m",
BrightCyan: "\033[48;5;6m \033[0m",
BrightWhite: "\033[48;5;7m \033[0m"}
QRCodeRecoveryLevels = qrcodeRecoveryLevels{
Low: qrcodeRecoveryLevel(qrcode.Low),
Medium: qrcodeRecoveryLevel(qrcode.Medium),
High: qrcodeRecoveryLevel(qrcode.High),
Highest: qrcodeRecoveryLevel(qrcode.Highest)}
)
type QRCodeString string
func (v *QRCodeString) Print() {
fmt.Fprint(outer, *v)
}
type qrcodeTerminal struct {
front consoleColor
back consoleColor
level qrcodeRecoveryLevel
}
func (v *qrcodeTerminal) Get(content interface{}) (result *QRCodeString) {
var qr *qrcode.QRCode
var err error
if t, ok := content.(string); ok {
qr, err = qrcode.New(t, qrcode.RecoveryLevel(v.level))
} else if t, ok := content.([]byte); ok {
qr, err = qrcode.New(string(t), qrcode.RecoveryLevel(v.level))
}
if qr != nil && err == nil {
data := qr.Bitmap()
result = v.getQRCodeString(data)
}
return
}
func (v *qrcodeTerminal) Get2(bytes []byte) (result *QRCodeString) {
data, err := parseQR(bytes)
if err == nil {
result = v.getQRCodeString(data)
}
return
}
func New2(front, back consoleColor, level qrcodeRecoveryLevel) *qrcodeTerminal {
obj := qrcodeTerminal{front: front, back: back, level: level}
return &obj
}
func New() *qrcodeTerminal {
front, back, level := ConsoleColors.BrightBlack, ConsoleColors.BrightWhite, QRCodeRecoveryLevels.Medium
return New2(front, back, level)
}
func (v *qrcodeTerminal) getQRCodeString(data [][]bool) (result *QRCodeString) {
str := ""
for ir, row := range data {
lr := len(row)
if ir == 0 || ir == 1 || ir == 2 ||
ir == lr-1 || ir == lr-2 || ir == lr-3 {
continue
}
for ic, col := range row {
lc := len(data)
if ic == 0 || ic == 1 || ic == 2 ||
ic == lc-1 || ic == lc-2 || ic == lc-3 {
continue
}
if col {
str += fmt.Sprint(v.front)
} else {
str += fmt.Sprint(v.back)
}
}
str += fmt.Sprintln()
}
obj := QRCodeString(str)
result = &obj
return
}
func parseQR(bytes []byte) (data [][]bool, err error) {
r := nbytes.NewReader(bytes)
img, err := png.Decode(r)
if err == nil {
rect := img.Bounds()
mx, my := rect.Max.X, rect.Max.Y
data = make([][]bool, mx)
for x := 0; x < mx; x++ {
data[x] = make([]bool, my)
for y := 0; y < my; y++ {
c := img.At(x, y)
r, _, _, _ := c.RGBA()
data[x][y] = r == 0
}
}
}
return
}
var outer = colorable.NewColorableStdout()

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

@ -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.

@ -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.

@ -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
}

@ -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)
}

@ -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

@ -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
)

@ -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)
}
}

@ -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
}

@ -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)
}

@ -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
}

@ -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[:]
}

@ -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
}

@ -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)
}

@ -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
}

@ -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)
}
}

@ -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
}

File diff suppressed because it is too large Load Diff

@ -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)
}

@ -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
}

@ -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
}

@ -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"],
}
}
}

@ -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
}

@ -0,0 +1,4 @@
*.sw*
*.png
*.directory
qrcode/qrcode

@ -0,0 +1,8 @@
language: go
go:
- 1.7
script:
- go test -v ./...

@ -0,0 +1,19 @@
Copyright (c) 2014 Tom Harwood
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.

@ -0,0 +1,80 @@
# go-qrcode #
<img src='https://skip.org/img/nyancat-youtube-qr.png' align='right'>
Package qrcode implements a QR Code encoder. [![Build Status](https://travis-ci.org/skip2/go-qrcode.svg?branch=master)](https://travis-ci.org/skip2/go-qrcode)
A QR Code is a matrix (two-dimensional) barcode. Arbitrary content may be encoded, with URLs being a popular choice :)
Each QR Code contains error recovery information to aid reading damaged or obscured codes. There are four levels of error recovery: Low, medium, high and highest. QR Codes with a higher recovery level are more robust to damage, at the cost of being physically larger.
## Install
go get -u github.com/skip2/go-qrcode/...
A command-line tool `qrcode` will be built into `$GOPATH/bin/`.
## Usage
import qrcode "github.com/skip2/go-qrcode"
- **Create a PNG image:**
var png []byte
png, err := qrcode.Encode("https://example.org", qrcode.Medium, 256)
- **Create a PNG image and write to a file:**
err := qrcode.WriteFile("https://example.org", qrcode.Medium, 256, "qr.png")
- **Create a PNG image with custom colors and write to file:**
err := qrcode.WriteColorFile("https://example.org", qrcode.Medium, 256, color.Black, color.White, "qr.png")
All examples use the qrcode.Medium error Recovery Level and create a fixed
256x256px size QR Code. The last function creates a white on black instead of black
on white QR Code.
The maximum capacity of a QR Code varies according to the content encoded and
the error recovery level. The maximum capacity is 2,953 bytes, 4,296
alphanumeric characters, 7,089 numeric digits, or a combination of these.
## Documentation
[![godoc](https://godoc.org/github.com/skip2/go-qrcode?status.png)](https://godoc.org/github.com/skip2/go-qrcode)
## Demoapp
[http://go-qrcode.appspot.com](http://go-qrcode.appspot.com)
## CLI
A command-line tool `qrcode` will be built into `$GOPATH/bin/`.
```
qrcode -- QR Code encoder in Go
https://github.com/skip2/go-qrcode
Flags:
-o string
out PNG file prefix, empty for stdout
-s int
image size (pixel) (default 256)
Usage:
1. Arguments except for flags are joined by " " and used to generate QR code.
Default output is STDOUT, pipe to imagemagick command "display" to display
on any X server.
qrcode hello word | display
2. Save to file if "display" not available:
qrcode "homepage: https://github.com/skip2/go-qrcode" > out.png
```
## Links
- [http://en.wikipedia.org/wiki/QR_code](http://en.wikipedia.org/wiki/QR_code)
- [ISO/IEC 18004:2006](http://www.iso.org/iso/catalogue_detail.htm?csnumber=43655) - Main QR Code specification (approx CHF 198,00)<br>
- [https://github.com/qpliu/qrencode-go/](https://github.com/qpliu/qrencode-go/) - alternative Go QR encoding library based on [ZXing](https://github.com/zxing/zxing)

@ -0,0 +1,273 @@
// go-qrcode
// Copyright 2014 Tom Harwood
// Package bitset implements an append only bit array.
//
// To create a Bitset and append some bits:
// // Bitset Contents
// b := bitset.New() // {}
// b.AppendBools(true, true, false) // {1, 1, 0}
// b.AppendBools(true) // {1, 1, 0, 1}
// b.AppendValue(0x02, 4) // {1, 1, 0, 1, 0, 0, 1, 0}
//
// To read values:
//
// len := b.Len() // 8
// v := b.At(0) // 1
// v = b.At(1) // 1
// v = b.At(2) // 0
// v = b.At(8) // 0
package bitset
import (
"bytes"
"fmt"
"log"
)
const (
b0 = false
b1 = true
)
// Bitset stores an array of bits.
type Bitset struct {
// The number of bits stored.
numBits int
// Storage for individual bits.
bits []byte
}
// New returns an initialised Bitset with optional initial bits v.
func New(v ...bool) *Bitset {
b := &Bitset{numBits: 0, bits: make([]byte, 0)}
b.AppendBools(v...)
return b
}
// Clone returns a copy.
func Clone(from *Bitset) *Bitset {
return &Bitset{numBits: from.numBits, bits: from.bits[:]}
}
// Substr returns a substring, consisting of the bits from indexes start to end.
func (b *Bitset) Substr(start int, end int) *Bitset {
if start > end || end > b.numBits {
log.Panicf("Out of range start=%d end=%d numBits=%d", start, end, b.numBits)
}
result := New()
result.ensureCapacity(end - start)
for i := start; i < end; i++ {
if b.At(i) {
result.bits[result.numBits/8] |= 0x80 >> uint(result.numBits%8)
}
result.numBits++
}
return result
}
// NewFromBase2String constructs and returns a Bitset from a string. The string
// consists of '1', '0' or ' ' characters, e.g. "1010 0101". The '1' and '0'
// characters represent true/false bits respectively, and ' ' characters are
// ignored.
//
// The function panics if the input string contains other characters.
func NewFromBase2String(b2string string) *Bitset {
b := &Bitset{numBits: 0, bits: make([]byte, 0)}
for _, c := range b2string {
switch c {
case '1':
b.AppendBools(true)
case '0':
b.AppendBools(false)
case ' ':
default:
log.Panicf("Invalid char %c in NewFromBase2String", c)
}
}
return b
}
// AppendBytes appends a list of whole bytes.
func (b *Bitset) AppendBytes(data []byte) {
for _, d := range data {
b.AppendByte(d, 8)
}
}
// AppendByte appends the numBits least significant bits from value.
func (b *Bitset) AppendByte(value byte, numBits int) {
b.ensureCapacity(numBits)
if numBits > 8 {
log.Panicf("numBits %d out of range 0-8", numBits)
}
for i := numBits - 1; i >= 0; i-- {
if value&(1<<uint(i)) != 0 {
b.bits[b.numBits/8] |= 0x80 >> uint(b.numBits%8)
}
b.numBits++
}
}
// AppendUint32 appends the numBits least significant bits from value.
func (b *Bitset) AppendUint32(value uint32, numBits int) {
b.ensureCapacity(numBits)
if numBits > 32 {
log.Panicf("numBits %d out of range 0-32", numBits)
}
for i := numBits - 1; i >= 0; i-- {
if value&(1<<uint(i)) != 0 {
b.bits[b.numBits/8] |= 0x80 >> uint(b.numBits%8)
}
b.numBits++
}
}
// ensureCapacity ensures the Bitset can store an additional |numBits|.
//
// The underlying array is expanded if necessary. To prevent frequent
// reallocation, expanding the underlying array at least doubles its capacity.
func (b *Bitset) ensureCapacity(numBits int) {
numBits += b.numBits
newNumBytes := numBits / 8
if numBits%8 != 0 {
newNumBytes++
}
if len(b.bits) >= newNumBytes {
return
}
b.bits = append(b.bits, make([]byte, newNumBytes+2*len(b.bits))...)
}
// Append bits copied from |other|.
//
// The new length is b.Len() + other.Len().
func (b *Bitset) Append(other *Bitset) {
b.ensureCapacity(other.numBits)
for i := 0; i < other.numBits; i++ {
if other.At(i) {
b.bits[b.numBits/8] |= 0x80 >> uint(b.numBits%8)
}
b.numBits++
}
}
// AppendBools appends bits to the Bitset.
func (b *Bitset) AppendBools(bits ...bool) {
b.ensureCapacity(len(bits))
for _, v := range bits {
if v {
b.bits[b.numBits/8] |= 0x80 >> uint(b.numBits%8)
}
b.numBits++
}
}
// AppendNumBools appends num bits of value value.
func (b *Bitset) AppendNumBools(num int, value bool) {
for i := 0; i < num; i++ {
b.AppendBools(value)
}
}
// String returns a human readable representation of the Bitset's contents.
func (b *Bitset) String() string {
var bitString string
for i := 0; i < b.numBits; i++ {
if (i % 8) == 0 {
bitString += " "
}
if (b.bits[i/8] & (0x80 >> byte(i%8))) != 0 {
bitString += "1"
} else {
bitString += "0"
}
}
return fmt.Sprintf("numBits=%d, bits=%s", b.numBits, bitString)
}
// Len returns the length of the Bitset in bits.
func (b *Bitset) Len() int {
return b.numBits
}
// Bits returns the contents of the Bitset.
func (b *Bitset) Bits() []bool {
result := make([]bool, b.numBits)
var i int
for i = 0; i < b.numBits; i++ {
result[i] = (b.bits[i/8] & (0x80 >> byte(i%8))) != 0
}
return result
}
// At returns the value of the bit at |index|.
func (b *Bitset) At(index int) bool {
if index >= b.numBits {
log.Panicf("Index %d out of range", index)
}
return (b.bits[index/8] & (0x80 >> byte(index%8))) != 0
}
// Equals returns true if the Bitset equals other.
func (b *Bitset) Equals(other *Bitset) bool {
if b.numBits != other.numBits {
return false
}
if !bytes.Equal(b.bits[0:b.numBits/8], other.bits[0:b.numBits/8]) {
return false
}
for i := 8 * (b.numBits / 8); i < b.numBits; i++ {
a := (b.bits[i/8] & (0x80 >> byte(i%8)))
b := (other.bits[i/8] & (0x80 >> byte(i%8)))
if a != b {
return false
}
}
return true
}
// ByteAt returns a byte consisting of upto 8 bits starting at index.
func (b *Bitset) ByteAt(index int) byte {
if index < 0 || index >= b.numBits {
log.Panicf("Index %d out of range", index)
}
var result byte
for i := index; i < index+8 && i < b.numBits; i++ {
result <<= 1
if b.At(i) {
result |= 1
}
}
return result
}

@ -0,0 +1,455 @@
// go-qrcode
// Copyright 2014 Tom Harwood
package qrcode
import (
"errors"
"log"
bitset "github.com/skip2/go-qrcode/bitset"
)
// Data encoding.
//
// The main data portion of a QR Code consists of one or more segments of data.
// A segment consists of:
//
// - The segment Data Mode: numeric, alphanumeric, or byte.
// - The length of segment in bits.
// - Encoded data.
//
// For example, the string "123ZZ#!#!" may be represented as:
//
// [numeric, 3, "123"] [alphanumeric, 2, "ZZ"] [byte, 4, "#!#!"]
//
// Multiple data modes exist to minimise the size of encoded data. For example,
// 8-bit bytes require 8 bits to encode each, but base 10 numeric data can be
// encoded at a higher density of 3 numbers (e.g. 123) per 10 bits.
//
// Some data can be represented in multiple modes. Numeric data can be
// represented in all three modes, whereas alphanumeric data (e.g. 'A') can be
// represented in alphanumeric and byte mode.
//
// Starting a new segment (to use a different Data Mode) has a cost, the bits to
// state the new segment Data Mode and length. To minimise each QR Code's symbol
// size, an optimisation routine coalesces segment types where possible, to
// reduce the encoded data length.
//
// There are several other data modes available (e.g. Kanji mode) which are not
// implemented here.
// A segment encoding mode.
type dataMode uint8
const (
// Each dataMode is a subset of the subsequent dataMode:
// dataModeNone < dataModeNumeric < dataModeAlphanumeric < dataModeByte
//
// This ordering is important for determining which data modes a character can
// be encoded with. E.g. 'E' can be encoded in both dataModeAlphanumeric and
// dataModeByte.
dataModeNone dataMode = 1 << iota
dataModeNumeric
dataModeAlphanumeric
dataModeByte
)
// dataModeString returns d as a short printable string.
func dataModeString(d dataMode) string {
switch d {
case dataModeNone:
return "none"
case dataModeNumeric:
return "numeric"
case dataModeAlphanumeric:
return "alphanumeric"
case dataModeByte:
return "byte"
}
return "unknown"
}
type dataEncoderType uint8
const (
dataEncoderType1To9 dataEncoderType = iota
dataEncoderType10To26
dataEncoderType27To40
)
// segment is a single segment of data.
type segment struct {
// Data Mode (e.g. numeric).
dataMode dataMode
// segment data (e.g. "abc").
data []byte
}
// A dataEncoder encodes data for a particular QR Code version.
type dataEncoder struct {
// Minimum & maximum versions supported.
minVersion int
maxVersion int
// Mode indicator bit sequences.
numericModeIndicator *bitset.Bitset
alphanumericModeIndicator *bitset.Bitset
byteModeIndicator *bitset.Bitset
// Character count lengths.
numNumericCharCountBits int
numAlphanumericCharCountBits int
numByteCharCountBits int
// The raw input data.
data []byte
// The data classified into unoptimised segments.
actual []segment
// The data classified into optimised segments.
optimised []segment
}
// newDataEncoder constructs a dataEncoder.
func newDataEncoder(t dataEncoderType) *dataEncoder {
d := &dataEncoder{}
switch t {
case dataEncoderType1To9:
d = &dataEncoder{
minVersion: 1,
maxVersion: 9,
numericModeIndicator: bitset.New(b0, b0, b0, b1),
alphanumericModeIndicator: bitset.New(b0, b0, b1, b0),
byteModeIndicator: bitset.New(b0, b1, b0, b0),
numNumericCharCountBits: 10,
numAlphanumericCharCountBits: 9,
numByteCharCountBits: 8,
}
case dataEncoderType10To26:
d = &dataEncoder{
minVersion: 10,
maxVersion: 26,
numericModeIndicator: bitset.New(b0, b0, b0, b1),
alphanumericModeIndicator: bitset.New(b0, b0, b1, b0),
byteModeIndicator: bitset.New(b0, b1, b0, b0),
numNumericCharCountBits: 12,
numAlphanumericCharCountBits: 11,
numByteCharCountBits: 16,
}
case dataEncoderType27To40:
d = &dataEncoder{
minVersion: 27,
maxVersion: 40,
numericModeIndicator: bitset.New(b0, b0, b0, b1),
alphanumericModeIndicator: bitset.New(b0, b0, b1, b0),
byteModeIndicator: bitset.New(b0, b1, b0, b0),
numNumericCharCountBits: 14,
numAlphanumericCharCountBits: 13,
numByteCharCountBits: 16,
}
default:
log.Panic("Unknown dataEncoderType")
}
return d
}
// encode data as one or more segments and return the encoded data.
//
// The returned data does not include the terminator bit sequence.
func (d *dataEncoder) encode(data []byte) (*bitset.Bitset, error) {
d.data = data
d.actual = nil
d.optimised = nil
if len(data) == 0 {
return nil, errors.New("no data to encode")
}
// Classify data into unoptimised segments.
d.classifyDataModes()
// Optimise segments.
err := d.optimiseDataModes()
if err != nil {
return nil, err
}
// Encode data.
encoded := bitset.New()
for _, s := range d.optimised {
d.encodeDataRaw(s.data, s.dataMode, encoded)
}
return encoded, nil
}
// classifyDataModes classifies the raw data into unoptimised segments.
// e.g. "123ZZ#!#!" =>
// [numeric, 3, "123"] [alphanumeric, 2, "ZZ"] [byte, 4, "#!#!"].
func (d *dataEncoder) classifyDataModes() {
var start int
mode := dataModeNone
for i, v := range d.data {
newMode := dataModeNone
switch {
case v >= 0x30 && v <= 0x39:
newMode = dataModeNumeric
case v == 0x20 || v == 0x24 || v == 0x25 || v == 0x2a || v == 0x2b || v ==
0x2d || v == 0x2e || v == 0x2f || v == 0x3a || (v >= 0x41 && v <= 0x5a):
newMode = dataModeAlphanumeric
default:
newMode = dataModeByte
}
if newMode != mode {
if i > 0 {
d.actual = append(d.actual, segment{dataMode: mode, data: d.data[start:i]})
start = i
}
mode = newMode
}
}
d.actual = append(d.actual, segment{dataMode: mode, data: d.data[start:len(d.data)]})
}
// optimiseDataModes optimises the list of segments to reduce the overall output
// encoded data length.
//
// The algorithm coalesces adjacent segments. segments are only coalesced when
// the Data Modes are compatible, and when the coalesced segment has a shorter
// encoded length than separate segments.
//
// Multiple segments may be coalesced. For example a string of alternating
// alphanumeric/numeric segments ANANANANA can be optimised to just A.
func (d *dataEncoder) optimiseDataModes() error {
for i := 0; i < len(d.actual); {
mode := d.actual[i].dataMode
numChars := len(d.actual[i].data)
j := i + 1
for j < len(d.actual) {
nextNumChars := len(d.actual[j].data)
nextMode := d.actual[j].dataMode
if nextMode > mode {
break
}
coalescedLength, err := d.encodedLength(mode, numChars+nextNumChars)
if err != nil {
return err
}
seperateLength1, err := d.encodedLength(mode, numChars)
if err != nil {
return err
}
seperateLength2, err := d.encodedLength(nextMode, nextNumChars)
if err != nil {
return err
}
if coalescedLength < seperateLength1+seperateLength2 {
j++
numChars += nextNumChars
} else {
break
}
}
optimised := segment{dataMode: mode,
data: make([]byte, 0, numChars)}
for k := i; k < j; k++ {
optimised.data = append(optimised.data, d.actual[k].data...)
}
d.optimised = append(d.optimised, optimised)
i = j
}
return nil
}
// encodeDataRaw encodes data in dataMode. The encoded data is appended to
// encoded.
func (d *dataEncoder) encodeDataRaw(data []byte, dataMode dataMode, encoded *bitset.Bitset) {
modeIndicator := d.modeIndicator(dataMode)
charCountBits := d.charCountBits(dataMode)
// Append mode indicator.
encoded.Append(modeIndicator)
// Append character count.
encoded.AppendUint32(uint32(len(data)), charCountBits)
// Append data.
switch dataMode {
case dataModeNumeric:
for i := 0; i < len(data); i += 3 {
charsRemaining := len(data) - i
var value uint32
bitsUsed := 1
for j := 0; j < charsRemaining && j < 3; j++ {
value *= 10
value += uint32(data[i+j] - 0x30)
bitsUsed += 3
}
encoded.AppendUint32(value, bitsUsed)
}
case dataModeAlphanumeric:
for i := 0; i < len(data); i += 2 {
charsRemaining := len(data) - i
var value uint32
for j := 0; j < charsRemaining && j < 2; j++ {
value *= 45
value += encodeAlphanumericCharacter(data[i+j])
}
bitsUsed := 6
if charsRemaining > 1 {
bitsUsed = 11
}
encoded.AppendUint32(value, bitsUsed)
}
case dataModeByte:
for _, b := range data {
encoded.AppendByte(b, 8)
}
}
}
// modeIndicator returns the segment header bits for a segment of type dataMode.
func (d *dataEncoder) modeIndicator(dataMode dataMode) *bitset.Bitset {
switch dataMode {
case dataModeNumeric:
return d.numericModeIndicator
case dataModeAlphanumeric:
return d.alphanumericModeIndicator
case dataModeByte:
return d.byteModeIndicator
default:
log.Panic("Unknown data mode")
}
return nil
}
// charCountBits returns the number of bits used to encode the length of a data
// segment of type dataMode.
func (d *dataEncoder) charCountBits(dataMode dataMode) int {
switch dataMode {
case dataModeNumeric:
return d.numNumericCharCountBits
case dataModeAlphanumeric:
return d.numAlphanumericCharCountBits
case dataModeByte:
return d.numByteCharCountBits
default:
log.Panic("Unknown data mode")
}
return 0
}
// encodedLength returns the number of bits required to encode n symbols in
// dataMode.
//
// The number of bits required is affected by:
// - QR code type - Mode Indicator length.
// - Data mode - number of bits used to represent data length.
// - Data mode - how the data is encoded.
// - Number of symbols encoded.
//
// An error is returned if the mode is not supported, or the length requested is
// too long to be represented.
func (d *dataEncoder) encodedLength(dataMode dataMode, n int) (int, error) {
modeIndicator := d.modeIndicator(dataMode)
charCountBits := d.charCountBits(dataMode)
if modeIndicator == nil {
return 0, errors.New("mode not supported")
}
maxLength := (1 << uint8(charCountBits)) - 1
if n > maxLength {
return 0, errors.New("length too long to be represented")
}
length := modeIndicator.Len() + charCountBits
switch dataMode {
case dataModeNumeric:
length += 10 * (n / 3)
if n%3 != 0 {
length += 1 + 3*(n%3)
}
case dataModeAlphanumeric:
length += 11 * (n / 2)
length += 6 * (n % 2)
case dataModeByte:
length += 8 * n
}
return length, nil
}
// encodeAlphanumericChar returns the QR Code encoded value of v.
//
// v must be a QR Code defined alphanumeric character: 0-9, A-Z, SP, $%*+-./ or
// :. The characters are mapped to values in the range 0-44 respectively.
func encodeAlphanumericCharacter(v byte) uint32 {
c := uint32(v)
switch {
case c >= '0' && c <= '9':
// 0-9 encoded as 0-9.
return c - '0'
case c >= 'A' && c <= 'Z':
// A-Z encoded as 10-35.
return c - 'A' + 10
case c == ' ':
return 36
case c == '$':
return 37
case c == '%':
return 38
case c == '*':
return 39
case c == '+':
return 40
case c == '-':
return 41
case c == '.':
return 42
case c == '/':
return 43
case c == ':':
return 44
default:
log.Panicf("encodeAlphanumericCharacter() with non alphanumeric char %v.", v)
}
return 0
}

@ -0,0 +1,589 @@
// go-qrcode
// Copyright 2014 Tom Harwood
/*
Package qrcode implements a QR Code encoder.
A QR Code is a matrix (two-dimensional) barcode. Arbitrary content may be
encoded.
A QR Code contains error recovery information to aid reading damaged or
obscured codes. There are four levels of error recovery: qrcode.{Low, Medium,
High, Highest}. QR Codes with a higher recovery level are more robust to damage,
at the cost of being physically larger.
Three functions cover most use cases:
- Create a PNG image:
var png []byte
png, err := qrcode.Encode("https://example.org", qrcode.Medium, 256)
- Create a PNG image and write to a file:
err := qrcode.WriteFile("https://example.org", qrcode.Medium, 256, "qr.png")
- Create a PNG image with custom colors and write to file:
err := qrcode.WriteColorFile("https://example.org", qrcode.Medium, 256, color.Black, color.White, "qr.png")
All examples use the qrcode.Medium error Recovery Level and create a fixed
256x256px size QR Code. The last function creates a white on black instead of black
on white QR Code.
To generate a variable sized image instead, specify a negative size (in place of
the 256 above), such as -4 or -5. Larger negative numbers create larger images:
A size of -5 sets each module (QR Code "pixel") to be 5px wide/high.
- Create a PNG image (variable size, with minimum white padding) and write to a file:
err := qrcode.WriteFile("https://example.org", qrcode.Medium, -5, "qr.png")
The maximum capacity of a QR Code varies according to the content encoded and
the error recovery level. The maximum capacity is 2,953 bytes, 4,296
alphanumeric characters, 7,089 numeric digits, or a combination of these.
This package implements a subset of QR Code 2005, as defined in ISO/IEC
18004:2006.
*/
package qrcode
import (
"bytes"
"errors"
"image"
"image/color"
"image/png"
"io"
"io/ioutil"
"log"
"os"
bitset "github.com/skip2/go-qrcode/bitset"
reedsolomon "github.com/skip2/go-qrcode/reedsolomon"
)
// Encode a QR Code and return a raw PNG image.
//
// size is both the image width and height in pixels. If size is too small then
// a larger image is silently returned. Negative values for size cause a
// variable sized image to be returned: See the documentation for Image().
//
// To serve over HTTP, remember to send a Content-Type: image/png header.
func Encode(content string, level RecoveryLevel, size int) ([]byte, error) {
var q *QRCode
q, err := New(content, level)
if err != nil {
return nil, err
}
return q.PNG(size)
}
// WriteFile encodes, then writes a QR Code to the given filename in PNG format.
//
// size is both the image width and height in pixels. If size is too small then
// a larger image is silently written. Negative values for size cause a variable
// sized image to be written: See the documentation for Image().
func WriteFile(content string, level RecoveryLevel, size int, filename string) error {
var q *QRCode
q, err := New(content, level)
if err != nil {
return err
}
return q.WriteFile(size, filename)
}
// WriteColorFile encodes, then writes a QR Code to the given filename in PNG format.
// With WriteColorFile you can also specify the colors you want to use.
//
// size is both the image width and height in pixels. If size is too small then
// a larger image is silently written. Negative values for size cause a variable
// sized image to be written: See the documentation for Image().
func WriteColorFile(content string, level RecoveryLevel, size int, background,
foreground color.Color, filename string) error {
var q *QRCode
q, err := New(content, level)
q.BackgroundColor = background
q.ForegroundColor = foreground
if err != nil {
return err
}
return q.WriteFile(size, filename)
}
// A QRCode represents a valid encoded QRCode.
type QRCode struct {
// Original content encoded.
Content string
// QR Code type.
Level RecoveryLevel
VersionNumber int
// User settable drawing options.
ForegroundColor color.Color
BackgroundColor color.Color
encoder *dataEncoder
version qrCodeVersion
data *bitset.Bitset
symbol *symbol
mask int
}
// New constructs a QRCode.
//
// var q *qrcode.QRCode
// q, err := qrcode.New("my content", qrcode.Medium)
//
// An error occurs if the content is too long.
func New(content string, level RecoveryLevel) (*QRCode, error) {
encoders := []dataEncoderType{dataEncoderType1To9, dataEncoderType10To26,
dataEncoderType27To40}
var encoder *dataEncoder
var encoded *bitset.Bitset
var chosenVersion *qrCodeVersion
var err error
for _, t := range encoders {
encoder = newDataEncoder(t)
encoded, err = encoder.encode([]byte(content))
if err != nil {
continue
}
chosenVersion = chooseQRCodeVersion(level, encoder, encoded.Len())
if chosenVersion != nil {
break
}
}
if err != nil {
return nil, err
} else if chosenVersion == nil {
return nil, errors.New("content too long to encode")
}
q := &QRCode{
Content: content,
Level: level,
VersionNumber: chosenVersion.version,
ForegroundColor: color.Black,
BackgroundColor: color.White,
encoder: encoder,
data: encoded,
version: *chosenVersion,
}
q.encode(chosenVersion.numTerminatorBitsRequired(encoded.Len()))
return q, nil
}
func newWithForcedVersion(content string, version int, level RecoveryLevel) (*QRCode, error) {
var encoder *dataEncoder
switch {
case version >= 1 && version <= 9:
encoder = newDataEncoder(dataEncoderType1To9)
case version >= 10 && version <= 26:
encoder = newDataEncoder(dataEncoderType10To26)
case version >= 27 && version <= 40:
encoder = newDataEncoder(dataEncoderType27To40)
default:
log.Fatalf("Invalid version %d (expected 1-40 inclusive)", version)
}
var encoded *bitset.Bitset
encoded, err := encoder.encode([]byte(content))
if err != nil {
return nil, err
}
chosenVersion := getQRCodeVersion(level, version)
if chosenVersion == nil {
return nil, errors.New("cannot find QR Code version")
}
q := &QRCode{
Content: content,
Level: level,
VersionNumber: chosenVersion.version,
ForegroundColor: color.Black,
BackgroundColor: color.White,
encoder: encoder,
data: encoded,
version: *chosenVersion,
}
q.encode(chosenVersion.numTerminatorBitsRequired(encoded.Len()))
return q, nil
}
// Bitmap returns the QR Code as a 2D array of 1-bit pixels.
//
// bitmap[y][x] is true if the pixel at (x, y) is set.
//
// The bitmap includes the required "quiet zone" around the QR Code to aid
// decoding.
func (q *QRCode) Bitmap() [][]bool {
return q.symbol.bitmap()
}
// Image returns the QR Code as an image.Image.
//
// A positive size sets a fixed image width and height (e.g. 256 yields an
// 256x256px image).
//
// Depending on the amount of data encoded, fixed size images can have different
// amounts of padding (white space around the QR Code). As an alternative, a
// variable sized image can be generated instead:
//
// A negative size causes a variable sized image to be returned. The image
// returned is the minimum size required for the QR Code. Choose a larger
// negative number to increase the scale of the image. e.g. a size of -5 causes
// each module (QR Code "pixel") to be 5px in size.
func (q *QRCode) Image(size int) image.Image {
// Minimum pixels (both width and height) required.
realSize := q.symbol.size
// Variable size support.
if size < 0 {
size = size * -1 * realSize
}
// Actual pixels available to draw the symbol. Automatically increase the
// image size if it's not large enough.
if size < realSize {
size = realSize
}
// Size of each module drawn.
pixelsPerModule := size / realSize
// Center the symbol within the image.
offset := (size - realSize*pixelsPerModule) / 2
rect := image.Rectangle{Min: image.Point{0, 0}, Max: image.Point{size, size}}
// Saves a few bytes to have them in this order
p := color.Palette([]color.Color{q.BackgroundColor, q.ForegroundColor})
img := image.NewPaletted(rect, p)
fgClr := uint8(img.Palette.Index(q.ForegroundColor))
bitmap := q.symbol.bitmap()
for y, row := range bitmap {
for x, v := range row {
if v {
startX := x*pixelsPerModule + offset
startY := y*pixelsPerModule + offset
for i := startX; i < startX+pixelsPerModule; i++ {
for j := startY; j < startY+pixelsPerModule; j++ {
pos := img.PixOffset(i, j)
img.Pix[pos] = fgClr
}
}
}
}
}
return img
}
// PNG returns the QR Code as a PNG image.
//
// size is both the image width and height in pixels. If size is too small then
// a larger image is silently returned. Negative values for size cause a
// variable sized image to be returned: See the documentation for Image().
func (q *QRCode) PNG(size int) ([]byte, error) {
img := q.Image(size)
encoder := png.Encoder{CompressionLevel: png.BestCompression}
var b bytes.Buffer
err := encoder.Encode(&b, img)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// Write writes the QR Code as a PNG image to io.Writer.
//
// size is both the image width and height in pixels. If size is too small then
// a larger image is silently written. Negative values for size cause a
// variable sized image to be written: See the documentation for Image().
func (q *QRCode) Write(size int, out io.Writer) error {
var png []byte
png, err := q.PNG(size)
if err != nil {
return err
}
_, err = out.Write(png)
return err
}
// WriteFile writes the QR Code as a PNG image to the specified file.
//
// size is both the image width and height in pixels. If size is too small then
// a larger image is silently written. Negative values for size cause a
// variable sized image to be written: See the documentation for Image().
func (q *QRCode) WriteFile(size int, filename string) error {
var png []byte
png, err := q.PNG(size)
if err != nil {
return err
}
return ioutil.WriteFile(filename, png, os.FileMode(0644))
}
// encode completes the steps required to encode the QR Code. These include
// adding the terminator bits and padding, splitting the data into blocks and
// applying the error correction, and selecting the best data mask.
func (q *QRCode) encode(numTerminatorBits int) {
q.addTerminatorBits(numTerminatorBits)
q.addPadding()
encoded := q.encodeBlocks()
const numMasks int = 8
penalty := 0
for mask := 0; mask < numMasks; mask++ {
var s *symbol
var err error
s, err = buildRegularSymbol(q.version, mask, encoded)
if err != nil {
log.Panic(err.Error())
}
numEmptyModules := s.numEmptyModules()
if numEmptyModules != 0 {
log.Panicf("bug: numEmptyModules is %d (expected 0) (version=%d)",
numEmptyModules, q.VersionNumber)
}
p := s.penaltyScore()
//log.Printf("mask=%d p=%3d p1=%3d p2=%3d p3=%3d p4=%d\n", mask, p, s.penalty1(), s.penalty2(), s.penalty3(), s.penalty4())
if q.symbol == nil || p < penalty {
q.symbol = s
q.mask = mask
penalty = p
}
}
}
// addTerminatorBits adds final terminator bits to the encoded data.
//
// The number of terminator bits required is determined when the QR Code version
// is chosen (which itself depends on the length of the data encoded). The
// terminator bits are thus added after the QR Code version
// is chosen, rather than at the data encoding stage.
func (q *QRCode) addTerminatorBits(numTerminatorBits int) {
q.data.AppendNumBools(numTerminatorBits, false)
}
// encodeBlocks takes the completed (terminated & padded) encoded data, splits
// the data into blocks (as specified by the QR Code version), applies error
// correction to each block, then interleaves the blocks together.
//
// The QR Code's final data sequence is returned.
func (q *QRCode) encodeBlocks() *bitset.Bitset {
// Split into blocks.
type dataBlock struct {
data *bitset.Bitset
ecStartOffset int
}
block := make([]dataBlock, q.version.numBlocks())
start := 0
end := 0
blockID := 0
for _, b := range q.version.block {
for j := 0; j < b.numBlocks; j++ {
start = end
end = start + b.numDataCodewords*8
// Apply error correction to each block.
numErrorCodewords := b.numCodewords - b.numDataCodewords
block[blockID].data = reedsolomon.Encode(q.data.Substr(start, end), numErrorCodewords)
block[blockID].ecStartOffset = end - start
blockID++
}
}
// Interleave the blocks.
result := bitset.New()
// Combine data blocks.
working := true
for i := 0; working; i += 8 {
working = false
for j, b := range block {
if i >= block[j].ecStartOffset {
continue
}
result.Append(b.data.Substr(i, i+8))
working = true
}
}
// Combine error correction blocks.
working = true
for i := 0; working; i += 8 {
working = false
for j, b := range block {
offset := i + block[j].ecStartOffset
if offset >= block[j].data.Len() {
continue
}
result.Append(b.data.Substr(offset, offset+8))
working = true
}
}
// Append remainder bits.
result.AppendNumBools(q.version.numRemainderBits, false)
return result
}
// max returns the maximum of a and b.
func max(a int, b int) int {
if a > b {
return a
}
return b
}
// addPadding pads the encoded data upto the full length required.
func (q *QRCode) addPadding() {
numDataBits := q.version.numDataBits()
if q.data.Len() == numDataBits {
return
}
// Pad to the nearest codeword boundary.
q.data.AppendNumBools(q.version.numBitsToPadToCodeword(q.data.Len()), false)
// Pad codewords 0b11101100 and 0b00010001.
padding := [2]*bitset.Bitset{
bitset.New(true, true, true, false, true, true, false, false),
bitset.New(false, false, false, true, false, false, false, true),
}
// Insert pad codewords alternately.
i := 0
for numDataBits-q.data.Len() >= 8 {
q.data.Append(padding[i])
i = 1 - i // Alternate between 0 and 1.
}
if q.data.Len() != numDataBits {
log.Panicf("BUG: got len %d, expected %d", q.data.Len(), numDataBits)
}
}
// ToString produces a multi-line string that forms a QR-code image.
func (q *QRCode) ToString(inverseColor bool) string {
bits := q.Bitmap()
var buf bytes.Buffer
for y := range bits {
for x := range bits[y] {
if bits[y][x] != inverseColor {
buf.WriteString(" ")
} else {
buf.WriteString("██")
}
}
buf.WriteString("\n")
}
return buf.String()
}
// ToSmallString produces a multi-line string that forms a QR-code image, a
// factor two smaller in x and y then ToString.
func (q *QRCode) ToSmallString(inverseColor bool) string {
bits := q.Bitmap()
var buf bytes.Buffer
// if there is an odd number of rows, the last one needs special treatment
for y := 0; y < len(bits)-1; y += 2 {
for x := range bits[y] {
if bits[y][x] == bits[y+1][x] {
if bits[y][x] != inverseColor {
buf.WriteString(" ")
} else {
buf.WriteString("█")
}
} else {
if bits[y][x] != inverseColor {
buf.WriteString("▄")
} else {
buf.WriteString("▀")
}
}
}
buf.WriteString("\n")
}
// special treatment for the last row if odd
if len(bits)%2 == 1 {
y := len(bits) - 1
for x := range bits[y] {
if bits[y][x] != inverseColor {
buf.WriteString(" ")
} else {
buf.WriteString("▀")
}
}
buf.WriteString("\n")
}
return buf.String()
}

@ -0,0 +1,387 @@
// go-qrcode
// Copyright 2014 Tom Harwood
package reedsolomon
// Addition, subtraction, multiplication, and division in GF(2^8).
// Operations are performed modulo x^8 + x^4 + x^3 + x^2 + 1.
// http://en.wikipedia.org/wiki/Finite_field_arithmetic
import "log"
const (
gfZero = gfElement(0)
gfOne = gfElement(1)
)
var (
gfExpTable = [256]gfElement{
/* 0 - 9 */ 1, 2, 4, 8, 16, 32, 64, 128, 29, 58,
/* 10 - 19 */ 116, 232, 205, 135, 19, 38, 76, 152, 45, 90,
/* 20 - 29 */ 180, 117, 234, 201, 143, 3, 6, 12, 24, 48,
/* 30 - 39 */ 96, 192, 157, 39, 78, 156, 37, 74, 148, 53,
/* 40 - 49 */ 106, 212, 181, 119, 238, 193, 159, 35, 70, 140,
/* 50 - 59 */ 5, 10, 20, 40, 80, 160, 93, 186, 105, 210,
/* 60 - 69 */ 185, 111, 222, 161, 95, 190, 97, 194, 153, 47,
/* 70 - 79 */ 94, 188, 101, 202, 137, 15, 30, 60, 120, 240,
/* 80 - 89 */ 253, 231, 211, 187, 107, 214, 177, 127, 254, 225,
/* 90 - 99 */ 223, 163, 91, 182, 113, 226, 217, 175, 67, 134,
/* 100 - 109 */ 17, 34, 68, 136, 13, 26, 52, 104, 208, 189,
/* 110 - 119 */ 103, 206, 129, 31, 62, 124, 248, 237, 199, 147,
/* 120 - 129 */ 59, 118, 236, 197, 151, 51, 102, 204, 133, 23,
/* 130 - 139 */ 46, 92, 184, 109, 218, 169, 79, 158, 33, 66,
/* 140 - 149 */ 132, 21, 42, 84, 168, 77, 154, 41, 82, 164,
/* 150 - 159 */ 85, 170, 73, 146, 57, 114, 228, 213, 183, 115,
/* 160 - 169 */ 230, 209, 191, 99, 198, 145, 63, 126, 252, 229,
/* 170 - 179 */ 215, 179, 123, 246, 241, 255, 227, 219, 171, 75,
/* 180 - 189 */ 150, 49, 98, 196, 149, 55, 110, 220, 165, 87,
/* 190 - 199 */ 174, 65, 130, 25, 50, 100, 200, 141, 7, 14,
/* 200 - 209 */ 28, 56, 112, 224, 221, 167, 83, 166, 81, 162,
/* 210 - 219 */ 89, 178, 121, 242, 249, 239, 195, 155, 43, 86,
/* 220 - 229 */ 172, 69, 138, 9, 18, 36, 72, 144, 61, 122,
/* 230 - 239 */ 244, 245, 247, 243, 251, 235, 203, 139, 11, 22,
/* 240 - 249 */ 44, 88, 176, 125, 250, 233, 207, 131, 27, 54,
/* 250 - 255 */ 108, 216, 173, 71, 142, 1}
gfLogTable = [256]int{
/* 0 - 9 */ -1, 0, 1, 25, 2, 50, 26, 198, 3, 223,
/* 10 - 19 */ 51, 238, 27, 104, 199, 75, 4, 100, 224, 14,
/* 20 - 29 */ 52, 141, 239, 129, 28, 193, 105, 248, 200, 8,
/* 30 - 39 */ 76, 113, 5, 138, 101, 47, 225, 36, 15, 33,
/* 40 - 49 */ 53, 147, 142, 218, 240, 18, 130, 69, 29, 181,
/* 50 - 59 */ 194, 125, 106, 39, 249, 185, 201, 154, 9, 120,
/* 60 - 69 */ 77, 228, 114, 166, 6, 191, 139, 98, 102, 221,
/* 70 - 79 */ 48, 253, 226, 152, 37, 179, 16, 145, 34, 136,
/* 80 - 89 */ 54, 208, 148, 206, 143, 150, 219, 189, 241, 210,
/* 90 - 99 */ 19, 92, 131, 56, 70, 64, 30, 66, 182, 163,
/* 100 - 109 */ 195, 72, 126, 110, 107, 58, 40, 84, 250, 133,
/* 110 - 119 */ 186, 61, 202, 94, 155, 159, 10, 21, 121, 43,
/* 120 - 129 */ 78, 212, 229, 172, 115, 243, 167, 87, 7, 112,
/* 130 - 139 */ 192, 247, 140, 128, 99, 13, 103, 74, 222, 237,
/* 140 - 149 */ 49, 197, 254, 24, 227, 165, 153, 119, 38, 184,
/* 150 - 159 */ 180, 124, 17, 68, 146, 217, 35, 32, 137, 46,
/* 160 - 169 */ 55, 63, 209, 91, 149, 188, 207, 205, 144, 135,
/* 170 - 179 */ 151, 178, 220, 252, 190, 97, 242, 86, 211, 171,
/* 180 - 189 */ 20, 42, 93, 158, 132, 60, 57, 83, 71, 109,
/* 190 - 199 */ 65, 162, 31, 45, 67, 216, 183, 123, 164, 118,
/* 200 - 209 */ 196, 23, 73, 236, 127, 12, 111, 246, 108, 161,
/* 210 - 219 */ 59, 82, 41, 157, 85, 170, 251, 96, 134, 177,
/* 220 - 229 */ 187, 204, 62, 90, 203, 89, 95, 176, 156, 169,
/* 230 - 239 */ 160, 81, 11, 245, 22, 235, 122, 117, 44, 215,
/* 240 - 249 */ 79, 174, 213, 233, 230, 231, 173, 232, 116, 214,
/* 250 - 255 */ 244, 234, 168, 80, 88, 175}
)
// gfElement is an element in GF(2^8).
type gfElement uint8
// newGFElement creates and returns a new gfElement.
func newGFElement(data byte) gfElement {
return gfElement(data)
}
// gfAdd returns a + b.
func gfAdd(a, b gfElement) gfElement {
return a ^ b
}
// gfSub returns a - b.
//
// Note addition is equivalent to subtraction in GF(2).
func gfSub(a, b gfElement) gfElement {
return a ^ b
}
// gfMultiply returns a * b.
func gfMultiply(a, b gfElement) gfElement {
if a == gfZero || b == gfZero {
return gfZero
}
return gfExpTable[(gfLogTable[a]+gfLogTable[b])%255]
}
// gfDivide returns a / b.
//
// Divide by zero results in a panic.
func gfDivide(a, b gfElement) gfElement {
if a == gfZero {
return gfZero
} else if b == gfZero {
log.Panicln("Divide by zero")
}
return gfMultiply(a, gfInverse(b))
}
// gfInverse returns the multiplicative inverse of a, a^-1.
//
// a * a^-1 = 1
func gfInverse(a gfElement) gfElement {
if a == gfZero {
log.Panicln("No multiplicative inverse of 0")
}
return gfExpTable[255-gfLogTable[a]]
}
// a^i | bits | polynomial | decimal
// --------------------------------------------------------------------------
// 0 | 000000000 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 0
// a^0 | 000000001 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 1
// a^1 | 000000010 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 2
// a^2 | 000000100 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 4
// a^3 | 000001000 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 8
// a^4 | 000010000 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 16
// a^5 | 000100000 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 32
// a^6 | 001000000 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 64
// a^7 | 010000000 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 128
// a^8 | 000011101 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 29
// a^9 | 000111010 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 58
// a^10 | 001110100 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 116
// a^11 | 011101000 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 232
// a^12 | 011001101 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 205
// a^13 | 010000111 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 135
// a^14 | 000010011 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 19
// a^15 | 000100110 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 38
// a^16 | 001001100 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 76
// a^17 | 010011000 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 152
// a^18 | 000101101 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 45
// a^19 | 001011010 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 90
// a^20 | 010110100 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 180
// a^21 | 001110101 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 117
// a^22 | 011101010 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 234
// a^23 | 011001001 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 201
// a^24 | 010001111 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 143
// a^25 | 000000011 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 3
// a^26 | 000000110 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 6
// a^27 | 000001100 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 12
// a^28 | 000011000 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 24
// a^29 | 000110000 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 48
// a^30 | 001100000 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 96
// a^31 | 011000000 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 192
// a^32 | 010011101 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 157
// a^33 | 000100111 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 39
// a^34 | 001001110 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 78
// a^35 | 010011100 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 156
// a^36 | 000100101 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 37
// a^37 | 001001010 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 74
// a^38 | 010010100 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 148
// a^39 | 000110101 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 53
// a^40 | 001101010 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 106
// a^41 | 011010100 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 212
// a^42 | 010110101 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 181
// a^43 | 001110111 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 119
// a^44 | 011101110 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 238
// a^45 | 011000001 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 193
// a^46 | 010011111 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 159
// a^47 | 000100011 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 35
// a^48 | 001000110 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 70
// a^49 | 010001100 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 140
// a^50 | 000000101 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 5
// a^51 | 000001010 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 10
// a^52 | 000010100 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 20
// a^53 | 000101000 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 40
// a^54 | 001010000 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 80
// a^55 | 010100000 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 160
// a^56 | 001011101 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 93
// a^57 | 010111010 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 186
// a^58 | 001101001 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 105
// a^59 | 011010010 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 210
// a^60 | 010111001 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 185
// a^61 | 001101111 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 111
// a^62 | 011011110 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 222
// a^63 | 010100001 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 161
// a^64 | 001011111 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 95
// a^65 | 010111110 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 190
// a^66 | 001100001 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 97
// a^67 | 011000010 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 194
// a^68 | 010011001 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 153
// a^69 | 000101111 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 47
// a^70 | 001011110 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 94
// a^71 | 010111100 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 188
// a^72 | 001100101 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 101
// a^73 | 011001010 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 202
// a^74 | 010001001 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 137
// a^75 | 000001111 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 15
// a^76 | 000011110 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 30
// a^77 | 000111100 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 60
// a^78 | 001111000 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 120
// a^79 | 011110000 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 240
// a^80 | 011111101 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 253
// a^81 | 011100111 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 231
// a^82 | 011010011 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 211
// a^83 | 010111011 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 187
// a^84 | 001101011 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 107
// a^85 | 011010110 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 214
// a^86 | 010110001 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 177
// a^87 | 001111111 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 127
// a^88 | 011111110 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 254
// a^89 | 011100001 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 225
// a^90 | 011011111 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 223
// a^91 | 010100011 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 163
// a^92 | 001011011 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 91
// a^93 | 010110110 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 182
// a^94 | 001110001 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 113
// a^95 | 011100010 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 226
// a^96 | 011011001 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 217
// a^97 | 010101111 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 175
// a^98 | 001000011 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 67
// a^99 | 010000110 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 134
// a^100 | 000010001 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 17
// a^101 | 000100010 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 34
// a^102 | 001000100 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 68
// a^103 | 010001000 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 136
// a^104 | 000001101 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 13
// a^105 | 000011010 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 26
// a^106 | 000110100 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 52
// a^107 | 001101000 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 104
// a^108 | 011010000 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 208
// a^109 | 010111101 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 189
// a^110 | 001100111 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 103
// a^111 | 011001110 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 206
// a^112 | 010000001 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 129
// a^113 | 000011111 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 31
// a^114 | 000111110 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 62
// a^115 | 001111100 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 124
// a^116 | 011111000 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 248
// a^117 | 011101101 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 237
// a^118 | 011000111 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 199
// a^119 | 010010011 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 147
// a^120 | 000111011 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 59
// a^121 | 001110110 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 118
// a^122 | 011101100 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 236
// a^123 | 011000101 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 197
// a^124 | 010010111 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 151
// a^125 | 000110011 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 51
// a^126 | 001100110 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 102
// a^127 | 011001100 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 204
// a^128 | 010000101 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 133
// a^129 | 000010111 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 23
// a^130 | 000101110 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 46
// a^131 | 001011100 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 92
// a^132 | 010111000 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 184
// a^133 | 001101101 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 109
// a^134 | 011011010 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 218
// a^135 | 010101001 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 169
// a^136 | 001001111 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 79
// a^137 | 010011110 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 158
// a^138 | 000100001 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 33
// a^139 | 001000010 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 66
// a^140 | 010000100 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 132
// a^141 | 000010101 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 21
// a^142 | 000101010 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 42
// a^143 | 001010100 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 84
// a^144 | 010101000 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 168
// a^145 | 001001101 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 77
// a^146 | 010011010 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 154
// a^147 | 000101001 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 41
// a^148 | 001010010 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 82
// a^149 | 010100100 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 164
// a^150 | 001010101 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 85
// a^151 | 010101010 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 170
// a^152 | 001001001 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 73
// a^153 | 010010010 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 146
// a^154 | 000111001 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 57
// a^155 | 001110010 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 114
// a^156 | 011100100 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 228
// a^157 | 011010101 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 213
// a^158 | 010110111 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 183
// a^159 | 001110011 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 115
// a^160 | 011100110 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 230
// a^161 | 011010001 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 209
// a^162 | 010111111 | 0x^8 1x^7 0x^6 1x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 191
// a^163 | 001100011 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 99
// a^164 | 011000110 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 198
// a^165 | 010010001 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 145
// a^166 | 000111111 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 63
// a^167 | 001111110 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 1x^2 1x^1 0x^0 | 126
// a^168 | 011111100 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 252
// a^169 | 011100101 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 229
// a^170 | 011010111 | 0x^8 1x^7 1x^6 0x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 215
// a^171 | 010110011 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 179
// a^172 | 001111011 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 123
// a^173 | 011110110 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 246
// a^174 | 011110001 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 241
// a^175 | 011111111 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 1x^2 1x^1 1x^0 | 255
// a^176 | 011100011 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 227
// a^177 | 011011011 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 219
// a^178 | 010101011 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 171
// a^179 | 001001011 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 75
// a^180 | 010010110 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 150
// a^181 | 000110001 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 49
// a^182 | 001100010 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 98
// a^183 | 011000100 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 196
// a^184 | 010010101 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 149
// a^185 | 000110111 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 55
// a^186 | 001101110 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 110
// a^187 | 011011100 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 220
// a^188 | 010100101 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 165
// a^189 | 001010111 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 87
// a^190 | 010101110 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 174
// a^191 | 001000001 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 65
// a^192 | 010000010 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 130
// a^193 | 000011001 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 25
// a^194 | 000110010 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 50
// a^195 | 001100100 | 0x^8 0x^7 1x^6 1x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 100
// a^196 | 011001000 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 200
// a^197 | 010001101 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 141
// a^198 | 000000111 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 7
// a^199 | 000001110 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 14
// a^200 | 000011100 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 1x^2 0x^1 0x^0 | 28
// a^201 | 000111000 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 56
// a^202 | 001110000 | 0x^8 0x^7 1x^6 1x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 112
// a^203 | 011100000 | 0x^8 1x^7 1x^6 1x^5 0x^4 0x^3 0x^2 0x^1 0x^0 | 224
// a^204 | 011011101 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 221
// a^205 | 010100111 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 167
// a^206 | 001010011 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 83
// a^207 | 010100110 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 1x^2 1x^1 0x^0 | 166
// a^208 | 001010001 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 0x^2 0x^1 1x^0 | 81
// a^209 | 010100010 | 0x^8 1x^7 0x^6 1x^5 0x^4 0x^3 0x^2 1x^1 0x^0 | 162
// a^210 | 001011001 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 89
// a^211 | 010110010 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 178
// a^212 | 001111001 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 121
// a^213 | 011110010 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 242
// a^214 | 011111001 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 0x^2 0x^1 1x^0 | 249
// a^215 | 011101111 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 239
// a^216 | 011000011 | 0x^8 1x^7 1x^6 0x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 195
// a^217 | 010011011 | 0x^8 1x^7 0x^6 0x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 155
// a^218 | 000101011 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 43
// a^219 | 001010110 | 0x^8 0x^7 1x^6 0x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 86
// a^220 | 010101100 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 172
// a^221 | 001000101 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 1x^2 0x^1 1x^0 | 69
// a^222 | 010001010 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 0x^2 1x^1 0x^0 | 138
// a^223 | 000001001 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 9
// a^224 | 000010010 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 0x^2 1x^1 0x^0 | 18
// a^225 | 000100100 | 0x^8 0x^7 0x^6 1x^5 0x^4 0x^3 1x^2 0x^1 0x^0 | 36
// a^226 | 001001000 | 0x^8 0x^7 1x^6 0x^5 0x^4 1x^3 0x^2 0x^1 0x^0 | 72
// a^227 | 010010000 | 0x^8 1x^7 0x^6 0x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 144
// a^228 | 000111101 | 0x^8 0x^7 0x^6 1x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 61
// a^229 | 001111010 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 122
// a^230 | 011110100 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 1x^2 0x^1 0x^0 | 244
// a^231 | 011110101 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 1x^2 0x^1 1x^0 | 245
// a^232 | 011110111 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 1x^2 1x^1 1x^0 | 247
// a^233 | 011110011 | 0x^8 1x^7 1x^6 1x^5 1x^4 0x^3 0x^2 1x^1 1x^0 | 243
// a^234 | 011111011 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 251
// a^235 | 011101011 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 235
// a^236 | 011001011 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 203
// a^237 | 010001011 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 139
// a^238 | 000001011 | 0x^8 0x^7 0x^6 0x^5 0x^4 1x^3 0x^2 1x^1 1x^0 | 11
// a^239 | 000010110 | 0x^8 0x^7 0x^6 0x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 22
// a^240 | 000101100 | 0x^8 0x^7 0x^6 1x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 44
// a^241 | 001011000 | 0x^8 0x^7 1x^6 0x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 88
// a^242 | 010110000 | 0x^8 1x^7 0x^6 1x^5 1x^4 0x^3 0x^2 0x^1 0x^0 | 176
// a^243 | 001111101 | 0x^8 0x^7 1x^6 1x^5 1x^4 1x^3 1x^2 0x^1 1x^0 | 125
// a^244 | 011111010 | 0x^8 1x^7 1x^6 1x^5 1x^4 1x^3 0x^2 1x^1 0x^0 | 250
// a^245 | 011101001 | 0x^8 1x^7 1x^6 1x^5 0x^4 1x^3 0x^2 0x^1 1x^0 | 233
// a^246 | 011001111 | 0x^8 1x^7 1x^6 0x^5 0x^4 1x^3 1x^2 1x^1 1x^0 | 207
// a^247 | 010000011 | 0x^8 1x^7 0x^6 0x^5 0x^4 0x^3 0x^2 1x^1 1x^0 | 131
// a^248 | 000011011 | 0x^8 0x^7 0x^6 0x^5 1x^4 1x^3 0x^2 1x^1 1x^0 | 27
// a^249 | 000110110 | 0x^8 0x^7 0x^6 1x^5 1x^4 0x^3 1x^2 1x^1 0x^0 | 54
// a^250 | 001101100 | 0x^8 0x^7 1x^6 1x^5 0x^4 1x^3 1x^2 0x^1 0x^0 | 108
// a^251 | 011011000 | 0x^8 1x^7 1x^6 0x^5 1x^4 1x^3 0x^2 0x^1 0x^0 | 216
// a^252 | 010101101 | 0x^8 1x^7 0x^6 1x^5 0x^4 1x^3 1x^2 0x^1 1x^0 | 173
// a^253 | 001000111 | 0x^8 0x^7 1x^6 0x^5 0x^4 0x^3 1x^2 1x^1 1x^0 | 71
// a^254 | 010001110 | 0x^8 1x^7 0x^6 0x^5 0x^4 1x^3 1x^2 1x^1 0x^0 | 142
// a^255 | 000000001 | 0x^8 0x^7 0x^6 0x^5 0x^4 0x^3 0x^2 0x^1 1x^0 | 1

@ -0,0 +1,216 @@
// go-qrcode
// Copyright 2014 Tom Harwood
package reedsolomon
import (
"fmt"
"log"
bitset "github.com/skip2/go-qrcode/bitset"
)
// gfPoly is a polynomial over GF(2^8).
type gfPoly struct {
// The ith value is the coefficient of the ith degree of x.
// term[0]*(x^0) + term[1]*(x^1) + term[2]*(x^2) ...
term []gfElement
}
// newGFPolyFromData returns |data| as a polynomial over GF(2^8).
//
// Each data byte becomes the coefficient of an x term.
//
// For an n byte input the polynomial is:
// data[n-1]*(x^n-1) + data[n-2]*(x^n-2) ... + data[0]*(x^0).
func newGFPolyFromData(data *bitset.Bitset) gfPoly {
numTotalBytes := data.Len() / 8
if data.Len()%8 != 0 {
numTotalBytes++
}
result := gfPoly{term: make([]gfElement, numTotalBytes)}
i := numTotalBytes - 1
for j := 0; j < data.Len(); j += 8 {
result.term[i] = gfElement(data.ByteAt(j))
i--
}
return result
}
// newGFPolyMonomial returns term*(x^degree).
func newGFPolyMonomial(term gfElement, degree int) gfPoly {
if term == gfZero {
return gfPoly{}
}
result := gfPoly{term: make([]gfElement, degree+1)}
result.term[degree] = term
return result
}
func (e gfPoly) data(numTerms int) []byte {
result := make([]byte, numTerms)
i := numTerms - len(e.term)
for j := len(e.term) - 1; j >= 0; j-- {
result[i] = byte(e.term[j])
i++
}
return result
}
// numTerms returns the number of
func (e gfPoly) numTerms() int {
return len(e.term)
}
// gfPolyMultiply returns a * b.
func gfPolyMultiply(a, b gfPoly) gfPoly {
numATerms := a.numTerms()
numBTerms := b.numTerms()
result := gfPoly{term: make([]gfElement, numATerms+numBTerms)}
for i := 0; i < numATerms; i++ {
for j := 0; j < numBTerms; j++ {
if a.term[i] != 0 && b.term[j] != 0 {
monomial := gfPoly{term: make([]gfElement, i+j+1)}
monomial.term[i+j] = gfMultiply(a.term[i], b.term[j])
result = gfPolyAdd(result, monomial)
}
}
}
return result.normalised()
}
// gfPolyRemainder return the remainder of numerator / denominator.
func gfPolyRemainder(numerator, denominator gfPoly) gfPoly {
if denominator.equals(gfPoly{}) {
log.Panicln("Remainder by zero")
}
remainder := numerator
for remainder.numTerms() >= denominator.numTerms() {
degree := remainder.numTerms() - denominator.numTerms()
coefficient := gfDivide(remainder.term[remainder.numTerms()-1],
denominator.term[denominator.numTerms()-1])
divisor := gfPolyMultiply(denominator,
newGFPolyMonomial(coefficient, degree))
remainder = gfPolyAdd(remainder, divisor)
}
return remainder.normalised()
}
// gfPolyAdd returns a + b.
func gfPolyAdd(a, b gfPoly) gfPoly {
numATerms := a.numTerms()
numBTerms := b.numTerms()
numTerms := numATerms
if numBTerms > numTerms {
numTerms = numBTerms
}
result := gfPoly{term: make([]gfElement, numTerms)}
for i := 0; i < numTerms; i++ {
switch {
case numATerms > i && numBTerms > i:
result.term[i] = gfAdd(a.term[i], b.term[i])
case numATerms > i:
result.term[i] = a.term[i]
default:
result.term[i] = b.term[i]
}
}
return result.normalised()
}
func (e gfPoly) normalised() gfPoly {
numTerms := e.numTerms()
maxNonzeroTerm := numTerms - 1
for i := numTerms - 1; i >= 0; i-- {
if e.term[i] != 0 {
break
}
maxNonzeroTerm = i - 1
}
if maxNonzeroTerm < 0 {
return gfPoly{}
} else if maxNonzeroTerm < numTerms-1 {
e.term = e.term[0 : maxNonzeroTerm+1]
}
return e
}
func (e gfPoly) string(useIndexForm bool) string {
var str string
numTerms := e.numTerms()
for i := numTerms - 1; i >= 0; i-- {
if e.term[i] > 0 {
if len(str) > 0 {
str += " + "
}
if !useIndexForm {
str += fmt.Sprintf("%dx^%d", e.term[i], i)
} else {
str += fmt.Sprintf("a^%dx^%d", gfLogTable[e.term[i]], i)
}
}
}
if len(str) == 0 {
str = "0"
}
return str
}
// equals returns true if e == other.
func (e gfPoly) equals(other gfPoly) bool {
var minecPoly *gfPoly
var maxecPoly *gfPoly
if e.numTerms() > other.numTerms() {
minecPoly = &other
maxecPoly = &e
} else {
minecPoly = &e
maxecPoly = &other
}
numMinTerms := minecPoly.numTerms()
numMaxTerms := maxecPoly.numTerms()
for i := 0; i < numMinTerms; i++ {
if e.term[i] != other.term[i] {
return false
}
}
for i := numMinTerms; i < numMaxTerms; i++ {
if maxecPoly.term[i] != 0 {
return false
}
}
return true
}

@ -0,0 +1,73 @@
// go-qrcode
// Copyright 2014 Tom Harwood
// Package reedsolomon provides error correction encoding for QR Code 2005.
//
// QR Code 2005 uses a Reed-Solomon error correcting code to detect and correct
// errors encountered during decoding.
//
// The generated RS codes are systematic, and consist of the input data with
// error correction bytes appended.
package reedsolomon
import (
"log"
bitset "github.com/skip2/go-qrcode/bitset"
)
// Encode data for QR Code 2005 using the appropriate Reed-Solomon code.
//
// numECBytes is the number of error correction bytes to append, and is
// determined by the target QR Code's version and error correction level.
//
// ISO/IEC 18004 table 9 specifies the numECBytes required. e.g. a 1-L code has
// numECBytes=7.
func Encode(data *bitset.Bitset, numECBytes int) *bitset.Bitset {
// Create a polynomial representing |data|.
//
// The bytes are interpreted as the sequence of coefficients of a polynomial.
// The last byte's value becomes the x^0 coefficient, the second to last
// becomes the x^1 coefficient and so on.
ecpoly := newGFPolyFromData(data)
ecpoly = gfPolyMultiply(ecpoly, newGFPolyMonomial(gfOne, numECBytes))
// Pick the generator polynomial.
generator := rsGeneratorPoly(numECBytes)
// Generate the error correction bytes.
remainder := gfPolyRemainder(ecpoly, generator)
// Combine the data & error correcting bytes.
// The mathematically correct answer is:
//
// result := gfPolyAdd(ecpoly, remainder).
//
// The encoding used by QR Code 2005 is slightly different this result: To
// preserve the original |data| bit sequence exactly, the data and remainder
// are combined manually below. This ensures any most significant zero bits
// are preserved (and not optimised away).
result := bitset.Clone(data)
result.AppendBytes(remainder.data(numECBytes))
return result
}
// rsGeneratorPoly returns the Reed-Solomon generator polynomial with |degree|.
//
// The generator polynomial is calculated as:
// (x + a^0)(x + a^1)...(x + a^degree-1)
func rsGeneratorPoly(degree int) gfPoly {
if degree < 2 {
log.Panic("degree < 2")
}
generator := gfPoly{term: []gfElement{1}}
for i := 0; i < degree; i++ {
nextPoly := gfPoly{term: []gfElement{gfExpTable[i], 1}}
generator = gfPolyMultiply(generator, nextPoly)
}
return generator
}

@ -0,0 +1,309 @@
// go-qrcode
// Copyright 2014 Tom Harwood
package qrcode
import (
bitset "github.com/skip2/go-qrcode/bitset"
)
type regularSymbol struct {
version qrCodeVersion
mask int
data *bitset.Bitset
symbol *symbol
size int
}
// Abbreviated true/false.
const (
b0 = false
b1 = true
)
var (
alignmentPatternCenter = [][]int{
{}, // Version 0 doesn't exist.
{}, // Version 1 doesn't use alignment patterns.
{6, 18},
{6, 22},
{6, 26},
{6, 30},
{6, 34},
{6, 22, 38},
{6, 24, 42},
{6, 26, 46},
{6, 28, 50},
{6, 30, 54},
{6, 32, 58},
{6, 34, 62},
{6, 26, 46, 66},
{6, 26, 48, 70},
{6, 26, 50, 74},
{6, 30, 54, 78},
{6, 30, 56, 82},
{6, 30, 58, 86},
{6, 34, 62, 90},
{6, 28, 50, 72, 94},
{6, 26, 50, 74, 98},
{6, 30, 54, 78, 102},
{6, 28, 54, 80, 106},
{6, 32, 58, 84, 110},
{6, 30, 58, 86, 114},
{6, 34, 62, 90, 118},
{6, 26, 50, 74, 98, 122},
{6, 30, 54, 78, 102, 126},
{6, 26, 52, 78, 104, 130},
{6, 30, 56, 82, 108, 134},
{6, 34, 60, 86, 112, 138},
{6, 30, 58, 86, 114, 142},
{6, 34, 62, 90, 118, 146},
{6, 30, 54, 78, 102, 126, 150},
{6, 24, 50, 76, 102, 128, 154},
{6, 28, 54, 80, 106, 132, 158},
{6, 32, 58, 84, 110, 136, 162},
{6, 26, 54, 82, 110, 138, 166},
{6, 30, 58, 86, 114, 142, 170},
}
finderPattern = [][]bool{
{b1, b1, b1, b1, b1, b1, b1},
{b1, b0, b0, b0, b0, b0, b1},
{b1, b0, b1, b1, b1, b0, b1},
{b1, b0, b1, b1, b1, b0, b1},
{b1, b0, b1, b1, b1, b0, b1},
{b1, b0, b0, b0, b0, b0, b1},
{b1, b1, b1, b1, b1, b1, b1},
}
finderPatternSize = 7
finderPatternHorizontalBorder = [][]bool{
{b0, b0, b0, b0, b0, b0, b0, b0},
}
finderPatternVerticalBorder = [][]bool{
{b0},
{b0},
{b0},
{b0},
{b0},
{b0},
{b0},
{b0},
}
alignmentPattern = [][]bool{
{b1, b1, b1, b1, b1},
{b1, b0, b0, b0, b1},
{b1, b0, b1, b0, b1},
{b1, b0, b0, b0, b1},
{b1, b1, b1, b1, b1},
}
)
func buildRegularSymbol(version qrCodeVersion, mask int,
data *bitset.Bitset) (*symbol, error) {
m := &regularSymbol{
version: version,
mask: mask,
data: data,
symbol: newSymbol(version.symbolSize(), version.quietZoneSize()),
size: version.symbolSize(),
}
m.addFinderPatterns()
m.addAlignmentPatterns()
m.addTimingPatterns()
m.addFormatInfo()
m.addVersionInfo()
ok, err := m.addData()
if !ok {
return nil, err
}
return m.symbol, nil
}
func (m *regularSymbol) addFinderPatterns() {
fpSize := finderPatternSize
fp := finderPattern
fpHBorder := finderPatternHorizontalBorder
fpVBorder := finderPatternVerticalBorder
// Top left Finder Pattern.
m.symbol.set2dPattern(0, 0, fp)
m.symbol.set2dPattern(0, fpSize, fpHBorder)
m.symbol.set2dPattern(fpSize, 0, fpVBorder)
// Top right Finder Pattern.
m.symbol.set2dPattern(m.size-fpSize, 0, fp)
m.symbol.set2dPattern(m.size-fpSize-1, fpSize, fpHBorder)
m.symbol.set2dPattern(m.size-fpSize-1, 0, fpVBorder)
// Bottom left Finder Pattern.
m.symbol.set2dPattern(0, m.size-fpSize, fp)
m.symbol.set2dPattern(0, m.size-fpSize-1, fpHBorder)
m.symbol.set2dPattern(fpSize, m.size-fpSize-1, fpVBorder)
}
func (m *regularSymbol) addAlignmentPatterns() {
for _, x := range alignmentPatternCenter[m.version.version] {
for _, y := range alignmentPatternCenter[m.version.version] {
if !m.symbol.empty(x, y) {
continue
}
m.symbol.set2dPattern(x-2, y-2, alignmentPattern)
}
}
}
func (m *regularSymbol) addTimingPatterns() {
value := true
for i := finderPatternSize + 1; i < m.size-finderPatternSize; i++ {
m.symbol.set(i, finderPatternSize-1, value)
m.symbol.set(finderPatternSize-1, i, value)
value = !value
}
}
func (m *regularSymbol) addFormatInfo() {
fpSize := finderPatternSize
l := formatInfoLengthBits - 1
f := m.version.formatInfo(m.mask)
// Bits 0-7, under the top right finder pattern.
for i := 0; i <= 7; i++ {
m.symbol.set(m.size-i-1, fpSize+1, f.At(l-i))
}
// Bits 0-5, right of the top left finder pattern.
for i := 0; i <= 5; i++ {
m.symbol.set(fpSize+1, i, f.At(l-i))
}
// Bits 6-8 on the corner of the top left finder pattern.
m.symbol.set(fpSize+1, fpSize, f.At(l-6))
m.symbol.set(fpSize+1, fpSize+1, f.At(l-7))
m.symbol.set(fpSize, fpSize+1, f.At(l-8))
// Bits 9-14 on the underside of the top left finder pattern.
for i := 9; i <= 14; i++ {
m.symbol.set(14-i, fpSize+1, f.At(l-i))
}
// Bits 8-14 on the right side of the bottom left finder pattern.
for i := 8; i <= 14; i++ {
m.symbol.set(fpSize+1, m.size-fpSize+i-8, f.At(l-i))
}
// Always dark symbol.
m.symbol.set(fpSize+1, m.size-fpSize-1, true)
}
func (m *regularSymbol) addVersionInfo() {
fpSize := finderPatternSize
v := m.version.versionInfo()
l := versionInfoLengthBits - 1
if v == nil {
return
}
for i := 0; i < v.Len(); i++ {
// Above the bottom left finder pattern.
m.symbol.set(i/3, m.size-fpSize-4+i%3, v.At(l-i))
// Left of the top right finder pattern.
m.symbol.set(m.size-fpSize-4+i%3, i/3, v.At(l-i))
}
}
type direction uint8
const (
up direction = iota
down
)
func (m *regularSymbol) addData() (bool, error) {
xOffset := 1
dir := up
x := m.size - 2
y := m.size - 1
for i := 0; i < m.data.Len(); i++ {
var mask bool
switch m.mask {
case 0:
mask = (y+x+xOffset)%2 == 0
case 1:
mask = y%2 == 0
case 2:
mask = (x+xOffset)%3 == 0
case 3:
mask = (y+x+xOffset)%3 == 0
case 4:
mask = (y/2+(x+xOffset)/3)%2 == 0
case 5:
mask = (y*(x+xOffset))%2+(y*(x+xOffset))%3 == 0
case 6:
mask = ((y*(x+xOffset))%2+((y*(x+xOffset))%3))%2 == 0
case 7:
mask = ((y+x+xOffset)%2+((y*(x+xOffset))%3))%2 == 0
}
// != is equivalent to XOR.
m.symbol.set(x+xOffset, y, mask != m.data.At(i))
if i == m.data.Len()-1 {
break
}
// Find next free bit in the symbol.
for {
if xOffset == 1 {
xOffset = 0
} else {
xOffset = 1
if dir == up {
if y > 0 {
y--
} else {
dir = down
x -= 2
}
} else {
if y < m.size-1 {
y++
} else {
dir = up
x -= 2
}
}
}
// Skip over the vertical timing pattern entirely.
if x == 5 {
x--
}
if m.symbol.empty(x+xOffset, y) {
break
}
}
}
return true, nil
}

@ -0,0 +1,309 @@
// go-qrcode
// Copyright 2014 Tom Harwood
package qrcode
// symbol is a 2D array of bits representing a QR Code symbol.
//
// A symbol consists of size*size modules, with each module normally drawn as a
// black or white square. The symbol also has a border of quietZoneSize modules.
//
// A (fictional) size=2, quietZoneSize=1 QR Code looks like:
//
// +----+
// | |
// | ab |
// | cd |
// | |
// +----+
//
// For ease of implementation, the functions to set/get bits ignore the border,
// so (0,0)=a, (0,1)=b, (1,0)=c, and (1,1)=d. The entire symbol (including the
// border) is returned by bitmap().
//
type symbol struct {
// Value of module at [y][x]. True is set.
module [][]bool
// True if the module at [y][x] is used (to either true or false).
// Used to identify unused modules.
isUsed [][]bool
// Combined width/height of the symbol and quiet zones.
//
// size = symbolSize + 2*quietZoneSize.
size int
// Width/height of the symbol only.
symbolSize int
// Width/height of a single quiet zone.
quietZoneSize int
}
// newSymbol constructs a symbol of size size*size, with a border of
// quietZoneSize.
func newSymbol(size int, quietZoneSize int) *symbol {
var m symbol
m.module = make([][]bool, size+2*quietZoneSize)
m.isUsed = make([][]bool, size+2*quietZoneSize)
for i := range m.module {
m.module[i] = make([]bool, size+2*quietZoneSize)
m.isUsed[i] = make([]bool, size+2*quietZoneSize)
}
m.size = size + 2*quietZoneSize
m.symbolSize = size
m.quietZoneSize = quietZoneSize
return &m
}
// get returns the module value at (x, y).
func (m *symbol) get(x int, y int) (v bool) {
v = m.module[y+m.quietZoneSize][x+m.quietZoneSize]
return
}
// empty returns true if the module at (x, y) has not been set (to either true
// or false).
func (m *symbol) empty(x int, y int) bool {
return !m.isUsed[y+m.quietZoneSize][x+m.quietZoneSize]
}
// numEmptyModules returns the number of empty modules.
//
// Initially numEmptyModules is symbolSize * symbolSize. After every module has
// been set (to either true or false), the number of empty modules is zero.
func (m *symbol) numEmptyModules() int {
var count int
for y := 0; y < m.symbolSize; y++ {
for x := 0; x < m.symbolSize; x++ {
if !m.isUsed[y+m.quietZoneSize][x+m.quietZoneSize] {
count++
}
}
}
return count
}
// set sets the module at (x, y) to v.
func (m *symbol) set(x int, y int, v bool) {
m.module[y+m.quietZoneSize][x+m.quietZoneSize] = v
m.isUsed[y+m.quietZoneSize][x+m.quietZoneSize] = true
}
// set2dPattern sets a 2D array of modules, starting at (x, y).
func (m *symbol) set2dPattern(x int, y int, v [][]bool) {
for j, row := range v {
for i, value := range row {
m.set(x+i, y+j, value)
}
}
}
// bitmap returns the entire symbol, including the quiet zone.
func (m *symbol) bitmap() [][]bool {
module := make([][]bool, len(m.module))
for i := range m.module {
module[i] = m.module[i][:]
}
return module
}
// string returns a pictorial representation of the symbol, suitable for
// printing in a TTY.
func (m *symbol) string() string {
var result string
for _, row := range m.module {
for _, value := range row {
switch value {
case true:
result += " "
case false:
// Unicode 'FULL BLOCK' (U+2588).
result += "██"
}
}
result += "\n"
}
return result
}
// Constants used to weight penalty calculations. Specified by ISO/IEC
// 18004:2006.
const (
penaltyWeight1 = 3
penaltyWeight2 = 3
penaltyWeight3 = 40
penaltyWeight4 = 10
)
// penaltyScore returns the penalty score of the symbol. The penalty score
// consists of the sum of the four individual penalty types.
func (m *symbol) penaltyScore() int {
return m.penalty1() + m.penalty2() + m.penalty3() + m.penalty4()
}
// penalty1 returns the penalty score for "adjacent modules in row/column with
// same colour".
//
// The numbers of adjacent matching modules and scores are:
// 0-5: score = 0
// 6+ : score = penaltyWeight1 + (numAdjacentModules - 5)
func (m *symbol) penalty1() int {
penalty := 0
for x := 0; x < m.symbolSize; x++ {
lastValue := m.get(x, 0)
count := 1
for y := 1; y < m.symbolSize; y++ {
v := m.get(x, y)
if v != lastValue {
count = 1
lastValue = v
} else {
count++
if count == 6 {
penalty += penaltyWeight1 + 1
} else if count > 6 {
penalty++
}
}
}
}
for y := 0; y < m.symbolSize; y++ {
lastValue := m.get(0, y)
count := 1
for x := 1; x < m.symbolSize; x++ {
v := m.get(x, y)
if v != lastValue {
count = 1
lastValue = v
} else {
count++
if count == 6 {
penalty += penaltyWeight1 + 1
} else if count > 6 {
penalty++
}
}
}
}
return penalty
}
// penalty2 returns the penalty score for "block of modules in the same colour".
//
// m*n: score = penaltyWeight2 * (m-1) * (n-1).
func (m *symbol) penalty2() int {
penalty := 0
for y := 1; y < m.symbolSize; y++ {
for x := 1; x < m.symbolSize; x++ {
topLeft := m.get(x-1, y-1)
above := m.get(x, y-1)
left := m.get(x-1, y)
current := m.get(x, y)
if current == left && current == above && current == topLeft {
penalty++
}
}
}
return penalty * penaltyWeight2
}
// penalty3 returns the penalty score for "1:1:3:1:1 ratio
// (dark:light:dark:light:dark) pattern in row/column, preceded or followed by
// light area 4 modules wide".
//
// Existence of the pattern scores penaltyWeight3.
func (m *symbol) penalty3() int {
penalty := 0
for y := 0; y < m.symbolSize; y++ {
var bitBuffer int16 = 0x00
for x := 0; x < m.symbolSize; x++ {
bitBuffer <<= 1
if v := m.get(x, y); v {
bitBuffer |= 1
}
switch bitBuffer & 0x7ff {
// 0b000 0101 1101 or 0b10111010000
// 0x05d or 0x5d0
case 0x05d, 0x5d0:
penalty += penaltyWeight3
bitBuffer = 0xFF
default:
if x == m.symbolSize-1 && (bitBuffer&0x7f) == 0x5d {
penalty += penaltyWeight3
bitBuffer = 0xFF
}
}
}
}
for x := 0; x < m.symbolSize; x++ {
var bitBuffer int16 = 0x00
for y := 0; y < m.symbolSize; y++ {
bitBuffer <<= 1
if v := m.get(x, y); v {
bitBuffer |= 1
}
switch bitBuffer & 0x7ff {
// 0b000 0101 1101 or 0b10111010000
// 0x05d or 0x5d0
case 0x05d, 0x5d0:
penalty += penaltyWeight3
bitBuffer = 0xFF
default:
if y == m.symbolSize-1 && (bitBuffer&0x7f) == 0x5d {
penalty += penaltyWeight3
bitBuffer = 0xFF
}
}
}
}
return penalty
}
// penalty4 returns the penalty score...
func (m *symbol) penalty4() int {
numModules := m.symbolSize * m.symbolSize
numDarkModules := 0
for x := 0; x < m.symbolSize; x++ {
for y := 0; y < m.symbolSize; y++ {
if v := m.get(x, y); v {
numDarkModules++
}
}
}
numDarkModuleDeviation := numModules/2 - numDarkModules
if numDarkModuleDeviation < 0 {
numDarkModuleDeviation *= -1
}
return penaltyWeight4 * (numDarkModuleDeviation / (numModules / 20))
}

File diff suppressed because it is too large Load Diff

17
vendor/modules.txt vendored

@ -5,6 +5,9 @@ filippo.io/edwards25519/field
# github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
## explicit
github.com/42wim/go-gitter
# github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
## explicit
github.com/Baozisoftware/qrcode-terminal-go
# github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989
## explicit
github.com/Benau/go_rlottie
@ -26,6 +29,15 @@ github.com/Philipp15b/go-steam/protocol/steamlang
github.com/Philipp15b/go-steam/rwu
github.com/Philipp15b/go-steam/socialcache
github.com/Philipp15b/go-steam/steamid
# github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
## explicit; go 1.13
github.com/Rhymen/go-whatsapp
github.com/Rhymen/go-whatsapp/binary
github.com/Rhymen/go-whatsapp/binary/proto
github.com/Rhymen/go-whatsapp/binary/token
github.com/Rhymen/go-whatsapp/crypto/cbc
github.com/Rhymen/go-whatsapp/crypto/curve25519
github.com/Rhymen/go-whatsapp/crypto/hkdf
# github.com/SevereCloud/vksdk/v2 v2.13.1
## explicit; go 1.16
github.com/SevereCloud/vksdk/v2
@ -357,6 +369,11 @@ github.com/sirupsen/logrus
# github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882
## explicit; go 1.16
github.com/sizeofint/webpanimation
# github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9
## explicit
github.com/skip2/go-qrcode
github.com/skip2/go-qrcode/bitset
github.com/skip2/go-qrcode/reedsolomon
# github.com/slack-go/slack v0.10.2
## explicit; go 1.16
github.com/slack-go/slack

Loading…
Cancel
Save