gRPC-клиент на Go#
Установка#
Сгенерированные Go-пакеты публикуются в Buf Schema Registry (BSR) в двух модулях: один содержит типы protobuf-сообщений, второй — gRPC-клиент и связанные типы.
Локальная генерация не требуется. Для подключения пакетов достаточно выполнить:
go get buf.build/gen/go/greenapi/whatsapp-api/protocolbuffers/go@latest
go get buf.build/gen/go/greenapi/whatsapp-api/grpc/go@latest
Если требуется собственная конфигурация генерации, см. раздел Генерация gRPC-клиента.
Соединение и авторизация#
Для подключения необходимы три параметра:
GREENAPI_GRPC_HOST— адрес gRPC-сервера GREEN-API:grpc.green-api.com:443. Адрес указывается в форматеhost:port, без схемыhttps://;GREENAPI_ID_INSTANCE— значениеidInstanceиз личного кабинета;GREENAPI_API_TOKEN_INSTANCE— значениеapiTokenInstanceиз личного кабинета.
Соединение с gRPC-сервером устанавливается с использованием TLS. Данные для авторизации передаются в метаданных каждого RPC-вызова. Подробнее см. раздел Авторизация в gRPC API.
Проверка подключения#
Для проверки подключения можно вызвать метод GetStateInstance, который возвращает текущее состояние инстанса. Если метод вернул состояние INSTANCE_STATE_AUTHORIZED, подключение к gRPC-серверу и авторизация выполнены успешно, а инстанс готов к работе. Описание других состояний инстанса и возможных ошибок приведено в разделе Проверка подключения обзора gRPC API.
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
}
Первый вызов#
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: "привет из gRPC",
})
if err != nil {
log.Fatal(err)
}
log.Println(resp.GetIdMessage())
}
Одно gRPC-соединение можно использовать для работы со всеми сервисами. Клиенты сервисов, например instancev1grpc.NewInstanceServiceClient(conn) и chatv1grpc.NewChatServiceClient(conn), создаются на основе одного и того же соединения conn.
Контекст ctx, содержащий метаданные авторизации, также можно использовать для последующих RPC-вызовов.
Как загрузить файл#
Файл передаётся с использованием клиентского потока. Сначала клиент отправляет сообщение с метаданными файла, затем последовательно передаёт чанки с его содержимым. После отправки последнего чанка клиент завершает отправку данных (half-close) и ожидает ответ сервера.
Общий порядок потоковой загрузки и требования к размеру чанков описаны в разделе Потоковые вызовы 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 можно переиспользовать: Send сериализует сообщение до возврата
if serr := stream.Send(&messagev1.UploadFileRequest{
Payload: &messagev1.UploadFileRequest_Chunk{Chunk: buf[:n]},
}); serr != nil {
break // настоящий статус заберём ниже, из CloseAndRecv
}
}
if rerr != nil {
if errors.Is(rerr, io.EOF) {
break // Read может вернуть n > 0 вместе с io.EOF
}
return "", rerr // ошибка чтения с диска: обрезанный файл не коммитим
}
}
resp, err := stream.CloseAndRecv() // half-close и ожидание ответа
if err != nil {
return "", err
}
return resp.GetUrlFile(), nil
}
Для вызова используется контекст ctx с метаданными авторизации:
url, err := uploadFile(ctx, client, "/path/to/photo.jpg")
Особенность обработки io.EOF#
Метод Send может вернуть io.EOF, если сервер уже завершил поток. В этом случае io.EOF не следует передавать вызывающему коду как итоговую ошибку вызова.
Фактический статус RPC-вызова возвращается методом CloseAndRecv(). Поэтому при получении io.EOF во время отправки необходимо завершить цикл отправки и вызвать CloseAndRecv(), чтобы получить итоговый ответ или ошибку сервера.
Как получить QR-код#
Метод ScanQrCode использует серверный поток: сервер последовательно отправляет данные с QR-кодами до завершения попытки авторизации. Статусы и поведение метода описаны в разделе Получение QR-кода по 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 // попытка закончилась: отсканировали, истёк таймаут или отказ
}
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("аккаунт требует passkey, а не сканирование QR")
case instancev1.QrCodeStatus_QR_CODE_STATUS_ALREADY_LOGGED:
return errors.New("инстанс уже авторизован, нужен Logout")
}
}
}
Поток завершается сервером автоматически. После этого метод Recv() возвращает io.EOF, и цикл чтения можно завершить.
Чтобы прекратить получение QR-кодов раньше, необходимо отменить контекст ctx, переданный в вызов ScanQrCode.
Обработка ошибок#
Код gRPC-ошибки можно получить с помощью status.Code(err) из пакета google.golang.org/grpc/status, а текст ошибки — через status.Convert(err).Message():
if err != nil {
log.Println(status.Code(err), status.Convert(err).Message())
}
Описание возможных кодов ошибок и рекомендации по повторным вызовам приведены в разделе Коды ошибок gRPC API. Перед автоматическим повтором методов отправки ознакомьтесь с разделом Повторная отправка сообщений, поскольку повторный вызов может привести к дублированию сообщения.