Перейти к содержанию

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. Перед автоматическим повтором методов отправки ознакомьтесь с разделом Повторная отправка сообщений, поскольку повторный вызов может привести к дублированию сообщения.