tulir / whatsmeow

Go library for the WhatsApp web multidevice API
https://go.mau.fi/whatsmeow
Mozilla Public License 2.0
2.25k stars 418 forks source link

Pushing QR to client #165

Closed hajsf closed 2 years ago

hajsf commented 2 years ago

Trying to make login screen as WhatsApp, where the QR is pushed to the client for scanning, so I wrote the below server code:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"

    _ "github.com/mattn/go-sqlite3"
    "github.com/skip2/go-qrcode"
    "google.golang.org/protobuf/proto"

    "go.mau.fi/whatsmeow"
    "go.mau.fi/whatsmeow/store"
    "go.mau.fi/whatsmeow/store/sqlstore"
    "go.mau.fi/whatsmeow/types/events"
    waLog "go.mau.fi/whatsmeow/util/log"
)

func eventHandler(evt interface{}) {
    switch v := evt.(type) {
    case *events.Message:
        fmt.Println("Received a message!", v.Message.GetConversation())
    }
}

func main() {
    passer := &DataPasser{logs: make(chan string)}

    http.HandleFunc("/", passer.handleHello)
    go http.ListenAndServe(":9999", nil)

    store.DeviceProps.Os = proto.String("WhatsApp GO")
    dbLog := waLog.Stdout("Database", "DEBUG", true)
    // Make sure you add appropriate DB connector imports, e.g. github.com/mattn/go-sqlite3 for SQLite
    container, err := sqlstore.New("sqlite3", "file:datastore.db?_foreign_keys=on", dbLog)
    if err != nil {
        panic(err)
    }
    // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
    deviceStore, err := container.GetFirstDevice()
    if err != nil {
        panic(err)
    }
    clientLog := waLog.Stdout("Client", "DEBUG", true)
    client := whatsmeow.NewClient(deviceStore, clientLog)
    client.AddEventHandler(eventHandler)

    if client.Store.ID == nil {
        // No ID stored, new login
        qrChan, _ := client.GetQRChannel(context.Background())
        err = client.Connect()
        if err != nil {
            panic(err)
        }

        for evt := range qrChan {
            switch evt.Event {
            case "success":
                {
                    passer.logs <- "success"
                    fmt.Println("Login event: success")
                }
            case "timeout":
                {
                    passer.logs <- "timeout"
                    fmt.Println("Login event: timeout")
                }
            case "code":
                {
                    passer.logs <- "new code"
                    fmt.Println("new code recieved")
                    img, err := qrcode.Encode(evt.Code, qrcode.Medium, 200) // evt.Code
                    if err != nil {
                        fmt.Println("error when write qrImage", err.Error())
                    }
                    passer.logs <- string(img)
                }
            }
        }
    } else {
        // Already logged in, just connect
        passer.logs <- "Already logged"
        fmt.Println("Already logged")
        err = client.Connect()
        if err != nil {
            panic(err)
        }
    }

    // Listen to Ctrl+C (you can also do something else that prevents the program from exiting)
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    <-c

    client.Disconnect()
}

With the below api:

package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "sync"
)

var (
    mux sync.Mutex
)

type Result struct {
    ResultType, Result string
}

type DataPasser struct {
    logs chan string
}

func (p *DataPasser) handleHello(w http.ResponseWriter, r *http.Request) {
    setupCORS(&w, r)
    w.Header().Set("Content-Type", "text/event-stream")
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Internal error", 500)
        return
    }
    flusher.Flush()
    done := r.Context().Done()
    defer fmt.Println("EXIT")
    for {
        select {
        case <-done:
            // the client disconnected
            return
        case m := <-p.logs:
            fmt.Println(m)
            if m == "new code" || m == "We are logging in" || m == "Already logged" || m == "timeout" || m == "success" {
                if _, err := fmt.Fprintf(w, "data: %s\n\n", m); err != nil {
                    // Write to connection failed. Subsequent writes will probably fail.
                    return
                }
                flusher.Flush()
            } else {
                mux.Lock()

                buffer := bytes.NewBuffer([]byte(m))

                /*  img, _, err := image.Decode(buffer)
                    if err != nil {
                        fmt.Println("err: ", err)
                    }

                    if err := jpeg.Encode(buffer, img, nil); err != nil {
                        log.Println("unable to encode image.")
                    } */

                w.Header().Set("Content-Type", "image/jpeg")
                w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes())))
                if _, err := w.Write(buffer.Bytes()); err != nil {
                    log.Println("unable to write image.")
                }
                mux.Unlock()
                flusher.Flush()
            }
        }
    }
}

func setupCORS(w *http.ResponseWriter, req *http.Request) {
    (*w).Header().Set("Cache-Control", "no-cache")
    (*w).Header().Set("Connection", "keep-alive")
    (*w).Header().Set("Access-Control-Allow-Origin", "*")
    (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
    (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}

But stuck to read it in the client, I tried something as below, but got lost:

<html>
<head></head>
<body>
    note: <span id="content"></span><br>
    <img id="photo" style="display: block;-webkit-user-select: none;">
</body>
    <script>
      /*  (function(){
                document.getElementById("content").innerHTML='<object type="text/html" data="http://localhost:9999/" ></object>';
        })(); 
*/
        const myRequest = new Request('http://127.0.0.1:9999/', {
        method: 'GET',
        headers: new Headers(),
        type: "arraybuffer",
        mode: 'cors',
        cache: 'default',
        });  

        var source = new EventSource("http://127.0.0.1:9999/");
        source.onmessage = function (event) {
            console.log(event)
            var counter = event.data; // JSON.parse(event.data);
            document.getElementById("content").innerHTML = counter;
        }

    fetch(myRequest).then(response => {
        console.log(response)
        console.log(response.headers)
    const contentType = response.headers.get("content-type");
        if (contentType && contentType.indexOf("application/json") !== -1) {
            return response.json().then(data => {
            var obj = JSON.parse(str);
            console.log(obj)
            // Process your data as a JavaScript object
            });
        }  if (contentType && contentType.indexOf("image/jpeg") !== -1) {
            console.log("Image received")
            return response.blob().then(data => {
                var reader = new FileReader();
                reader.readAsDataURL(blob); 
                reader.onloadend = function() {
                    var imageUrl = reader.result;                
                    var img = document.querySelector("#photo");
                    img.src = imageUrl;
                }
            });
        } else if (contentType && contentType.indexOf("text/event-stream") !== -1) {
            return response.text().then(text => {
            console.log(text)
            var source = new EventSource("http://localhost:9999/");
            source.onmessage = function (event) {
                var response = event.data // JSON.parse(event.data);
                document.getElementById("content").innerHTML = counter;
            } 
            // Process your text as a String
            });
        } else if (contentType && contentType.indexOf("text/html") !== -1) {
            return response.text().then(text => {
            console.log(text)
            var source = new EventSource("http://localhost:9999/");
            source.onmessage = function (event) {
                var response = event.data // JSON.parse(event.data);
                document.getElementById("content").innerHTML = counter;
            } 
            // Process your text as a String
            });
        } 
    });

    </script>
</html>

If I run and see the displayed page, I see:

enter image description here

Any help?

tulir commented 2 years ago

Generally it's easier to only send the data, not a full image.

hajsf commented 2 years ago

it's easier to only send the data

Do you know any JS function to convert the data to QR?

hajsf commented 2 years ago

I solved it buy pushing the QR string t o the server, and convert it to QR using qrcodejs. My full code if any is interested is:

// main.go
package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"

    _ "github.com/mattn/go-sqlite3"
    "google.golang.org/protobuf/proto"

    "go.mau.fi/whatsmeow"
    "go.mau.fi/whatsmeow/store"
    "go.mau.fi/whatsmeow/store/sqlstore"
    "go.mau.fi/whatsmeow/types/events"
    waLog "go.mau.fi/whatsmeow/util/log"
)

func eventHandler(evt interface{}) {
    switch v := evt.(type) {
    case *events.Message:
        fmt.Println("Received a message!", v.Message.GetConversation())
    }
}

func main() {
    passer := &DataPasser{logs: make(chan string)}

    http.HandleFunc("/sse/dashboard", passer.handleHello)
    go http.ListenAndServe(":1234", nil)
    /*
        ticker := time.NewTicker(500 * time.Millisecond)
        defer ticker.Stop()
        done := make(chan bool)
        go func() {
            for {
                select {
                case <-done:
                    return
                case <-ticker.C:
                    //  fmt.Println("Tick at", t)
                    // passer.logs <- buffer.String()
                }
            }
        }()
    */
    store.DeviceProps.Os = proto.String("WhatsApp GO")
    dbLog := waLog.Stdout("Database", "DEBUG", true)
    // Make sure you add appropriate DB connector imports, e.g. github.com/mattn/go-sqlite3 for SQLite
    container, err := sqlstore.New("sqlite3", "file:datastore.db?_foreign_keys=on", dbLog)
    if err != nil {
        panic(err)
    }
    // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
    deviceStore, err := container.GetFirstDevice()
    if err != nil {
        panic(err)
    }
    clientLog := waLog.Stdout("Client", "DEBUG", true)
    client := whatsmeow.NewClient(deviceStore, clientLog)
    client.AddEventHandler(eventHandler)

    if client.Store.ID == nil {
        // No ID stored, new login
        qrChan, _ := client.GetQRChannel(context.Background())
        err = client.Connect()
        if err != nil {
            panic(err)
        }

        for evt := range qrChan {
            switch evt.Event {
            case "success":
                {
                    passer.logs <- "success"
                    fmt.Println("Login event: success")
                }
            case "timeout":
                {
                    passer.logs <- "timeout"
                    fmt.Println("Login event: timeout")
                }
            case "code":
                {
                    fmt.Println("new code recieved")
                    fmt.Println(evt.Code)
                    passer.logs <- evt.Code
                }
            }
        }
    } else {
        // Already logged in, just connect
        passer.logs <- "Already logged"
        fmt.Println("Already logged")
        err = client.Connect()
        if err != nil {
            panic(err)
        }
    }

    // Listen to Ctrl+C (you can also do something else that prevents the program from exiting)
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    <-c

    client.Disconnect()
}

And

// api.go
package main

import (
    "fmt"
    "net/http"
    "sync"
)

var (
    mux sync.Mutex
)

type DataPasser struct {
    logs chan string
}

func (p *DataPasser) handleHello(w http.ResponseWriter, r *http.Request) {
    fmt.Println("from here")
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Internal error", 500)
        return
    }
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")
    setupCORS(&w, r)

    for {
        select {
        case c := <-p.logs:
            fmt.Println("recieved")
            mux.Lock()
            //counter++
            //c := counter
            mux.Unlock()
            fmt.Fprintf(w, "data: %v\n\n", c)
            flusher.Flush()
        case <-r.Context().Done():
            fmt.Println("Connection closed")
            return
        }
    }
}

func setupCORS(w *http.ResponseWriter, req *http.Request) {
    (*w).Header().Set("Cache-Control", "no-cache")
    (*w).Header().Set("Access-Control-Allow-Origin", "*")
    (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
    (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}

And the client side is:

<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
</head>

<body>
  <h5>Server message/Code: <span id="message"></span></h5>
  <div id="qr"></div>
  <script>
    var source = new EventSource("http://localhost:1234/sse/dashboard");
    source.onmessage = function (event) {
        var message = event.data
        document.querySelector('#message').innerHTML = message;
        if (new String(message).valueOf() == "success" || new String(message).valueOf() == "timeout"
            || new String(message).valueOf() == "Already logged") {
            document.querySelector('#qr').innerHTML = "";
        } else {
            var qrcode = new QRCode("qr", {
                text: message,
                width: 128,
                height: 128,
                colorDark : "#000000",
                colorLight : "#ffffff",
                correctLevel : QRCode.CorrectLevel.M
            });
        }
    }

/*
    var qrcode = new QRCode(
    "qr",
        [
            "BEGIN:VCARD",
            "VERSION:2.1",
            "N:Doe;John;;Dr;",
            "FN:Dr. John Doe",
            "EMAIL:johndoe@hotmail.com",
            "TEL;TYPE=cell:(123) 555-5832",
            "END:VCARD"
        ].join("\r\n")
    ); */

  </script>
</body>
</html>
prakasa1904 commented 3 months ago

is it works ? I cannot scan the qrcode output.. Always return error "Can't link new devices at this time". Any clue ?