gRPC client in Go#
Installation#
The generated Go packages are published to the Buf Schema Registry (BSR) in two modules: one contains the protobuf message types, the other contains the gRPC client and related types.
Local generation is not required. To add the packages, run:
go get buf.build/gen/go/greenapi/whatsapp-api/protocolbuffers/go@latest
go get buf.build/gen/go/greenapi/whatsapp-api/grpc/go@latest
If you need your own generation configuration, see Generating a gRPC client.
Connection and authorization#
Three parameters are required to connect:
GREENAPI_GRPC_HOSTβ the address of the GREEN-API gRPC server:grpc.green-api.com:443. The address is specified in thehost:portformat, without thehttps://scheme;GREENAPI_ID_INSTANCEβ theidInstancevalue from your personal account;GREENAPI_API_TOKEN_INSTANCEβ theapiTokenInstancevalue from your personal account.
The connection to the gRPC server is established using TLS. The authorization data is passed in the metadata of each RPC call. For details, see Authorization in the gRPC API.
Checking the connection#
To check the connection, you can call the GetStateInstance method, which returns the current state of the instance. If the method returns the INSTANCE_STATE_AUTHORIZED state, the connection to the gRPC server and authorization were successful, and the instance is ready to work. The other instance states and possible errors are described in the Checking the connection section of the gRPC API overview.
package main
import (
"context"
"crypto/tls"
"log"
"os"
"buf.build/gen/go/greenapi/whatsapp-api/grpc/go/greenapi/instance/v1/instancev1grpc"
instancev1 "buf.build/gen/go/greenapi/whatsapp-api/protocolbuffers/go/greenapi/instance/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func main() {
conn, err := grpc.NewClient(
os.Getenv("GREENAPI_GRPC_HOST"), // "grpc.green-api.com:443"
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := instancev1grpc.NewInstanceServiceClient(conn)
ctx := metadata.AppendToOutgoingContext(context.Background(),
"x-instance-id", os.Getenv("GREENAPI_ID_INSTANCE"),
"authorization", "Bearer "+os.Getenv("GREENAPI_API_TOKEN_INSTANCE"),
)
resp, err := client.GetStateInstance(ctx, &instancev1.GetStateInstanceRequest{})
if err != nil {
log.Fatalf("%s: %s", status.Code(err), status.Convert(err).Message())
}
log.Println(resp.GetStateInstance()) // INSTANCE_STATE_AUTHORIZED
}
The first call#
package main
import (
"context"
"crypto/tls"
"log"
"os"
"buf.build/gen/go/greenapi/whatsapp-api/grpc/go/greenapi/message/v1/messagev1grpc"
messagev1 "buf.build/gen/go/greenapi/whatsapp-api/protocolbuffers/go/greenapi/message/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
func main() {
conn, err := grpc.NewClient(
os.Getenv("GREENAPI_GRPC_HOST"), // "grpc.green-api.com:443"
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := messagev1grpc.NewMessageServiceClient(conn)
ctx := metadata.AppendToOutgoingContext(context.Background(),
"x-instance-id", os.Getenv("GREENAPI_ID_INSTANCE"),
"authorization", "Bearer "+os.Getenv("GREENAPI_API_TOKEN_INSTANCE"),
)
resp, err := client.SendMessage(ctx, &messagev1.SendMessageRequest{
ChatId: "11001234567@c.us",
Message: "hello from gRPC",
})
if err != nil {
log.Fatal(err)
}
log.Println(resp.GetIdMessage())
}
A single gRPC connection can be used to work with all services. Service clients, such as instancev1grpc.NewInstanceServiceClient(conn) and chatv1grpc.NewChatServiceClient(conn), are created from the same conn connection.
The ctx context containing the authorization metadata can also be used for subsequent RPC calls.
How to upload a file#
The file is transferred using a client stream. First, the client sends a message with the file metadata, then it sequentially sends chunks with the file contents. After sending the last chunk, the client finishes sending data (half-close) and waits for the server response.
The general order of a streaming upload and the chunk size requirements are described in Streaming calls in the gRPC API.
func uploadFile(ctx context.Context, client messagev1grpc.MessageServiceClient, path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
stream, err := client.UploadFile(ctx)
if err != nil {
return "", err
}
if err := stream.Send(&messagev1.UploadFileRequest{
Payload: &messagev1.UploadFileRequest_Metadata{
Metadata: &messagev1.UploadFileMetadata{
FileName: filepath.Base(path),
ContentType: "image/jpeg",
},
},
}); err != nil && !errors.Is(err, io.EOF) {
return "", err
}
buf := make([]byte, 64*1024)
for {
n, rerr := f.Read(buf)
if n > 0 {
// buf can be reused: Send serializes the message before returning
if serr := stream.Send(&messagev1.UploadFileRequest{
Payload: &messagev1.UploadFileRequest_Chunk{Chunk: buf[:n]},
}); serr != nil {
break // we will pick up the real status below, from CloseAndRecv
}
}
if rerr != nil {
if errors.Is(rerr, io.EOF) {
break // Read may return n > 0 together with io.EOF
}
return "", rerr // a disk read error: we do not commit a truncated file
}
}
resp, err := stream.CloseAndRecv() // half-close and wait for the response
if err != nil {
return "", err
}
return resp.GetUrlFile(), nil
}
The call uses the ctx context with the authorization metadata:
url, err := uploadFile(ctx, client, "/path/to/photo.jpg")
Handling io.EOF#
The Send method may return io.EOF if the server has already closed the stream. In this case, io.EOF should not be passed to the calling code as the final error of the call.
The actual status of the RPC call is returned by the CloseAndRecv() method. Therefore, if io.EOF is received while sending, exit the send loop and call CloseAndRecv() to get the final response or the server error.
How to get a QR code#
The ScanQrCode method uses a server stream: the server sequentially sends data with QR codes until the authorization attempt is complete. The statuses and the behaviour of the method are described in Getting a QR code over gRPC.
func scanQrCode(ctx context.Context, client instancev1grpc.InstanceServiceClient) error {
stream, err := client.ScanQrCode(ctx, &instancev1.ScanQrCodeRequest{})
if err != nil {
return err
}
for {
resp, err := stream.Recv()
if errors.Is(err, io.EOF) {
return nil // the attempt is over: scanned, timed out or rejected
}
if err != nil {
return err
}
switch resp.GetStatus() {
case instancev1.QrCodeStatus_QR_CODE_STATUS_QR_CODE:
showQr("data:image/png;base64," + base64.StdEncoding.EncodeToString(resp.GetQrPng()))
case instancev1.QrCodeStatus_QR_CODE_STATUS_PASSKEY_REQUIRED:
return errors.New("the account requires a passkey, not a QR scan")
case instancev1.QrCodeStatus_QR_CODE_STATUS_ALREADY_LOGGED:
return errors.New("the instance is already authorized, Logout is required")
}
}
}
The server closes the stream automatically. After that, the Recv() method returns io.EOF, and the read loop can be exited.
To stop receiving QR codes earlier, cancel the ctx context passed to the ScanQrCode call.
Error handling#
You can get the gRPC error code with status.Code(err) from the google.golang.org/grpc/status package, and the error text with status.Convert(err).Message():
if err != nil {
log.Println(status.Code(err), status.Convert(err).Message())
}
The possible error codes and recommendations on retries are described in gRPC API error codes. Before automatically retrying sending methods, read Resending messages, since a repeated call may result in a duplicate message.