Привет, пишу тг бота на C++. Недавно мне требовалось подключить выгрузку данных из бд в гугл таблицы, но годного гайда как это сделать я не нашел. Поэтому будет простенький гайд. Расписываю как читать и записывать данные в гугл таблицы. Использовал библиотеки rapidjson, libcurl и jwt-cpp.
1. Создание сервисного аккаунта
Переходим в Google cloud console открываем выпадающее меню слева и переходим наводимся на вкладку APIs & Services и переходим в Credentials.

Дальше нажимаем Create credentials выбираем Service account. После этого переходим в настройки этого аккаунта на вкладу Keys. Нажимаем Add key -> Create new key -> JSON. JSON файл загрузиться на компьютер.
Этот файлик выглядит примерно так:
{
"type": "service_account",
"project_id": "inxbot",
"private_key_id": "60c19ce53e578c63a679a300024ae29678053a2a",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBPiM67EIHEV+p\nnxSn2cfIUUocbeptKzaF+6HWjIer1+ijhdwEPnYXOAJFJoSQlAJkQeYdTHAHrWHo\n/r+M/QV3hnBk/EljkpEHYTezROFETG02x4LR1PWy0JZceRUYs8EKx0Z1IMXO7JLL\n2n3ePagX3QKMhYn3YDVvenMCvTQZswg1sRxQzZdIIu6Zc/8x8wIp7AZraSxBokPU\nfjACOr9OgyO2reft8fSU2rPUFAfhVXkeW+tbbGqasDXao08koV7Ng0B4sO5PHFik\nA6PMgFftXpSAybBiyVg205UauUhQ6GX8tlegfxSMvK4HITcWuLCQF1N1XFKrsihJ\nUqay+AJ5AgMBAAECggEAEmXXldMLgD/Ds1L9dPOThTtUCnvef1FHah8oa9RKUG6I\nbBUg+th1hsVi1fGSWm91ANWTUoPGAL0dnsCuPP6T6CrJBZxNYmkAXMsBMhSjO8zo\n8vewuUiEsecRAbHXc9gdT2jkiN87ZfV5DmZ5aFDHcaothXJrTb1/qOLoE0EvNij+\nJISdNV4VvCKyh44SYRAeUCUx6Tk071QNLapWxURCVuJZxLAZdatVztuxgSyMjqVc\n/u6ZMDdBwrJX7POPZItQZAYjNiz+5ytK/+a3pb3A4sij7cwfkqrEjy9Vnt6K567c\n7aZ77Zf7bMbPAQaEqs7sc1GgSZidt8hT+lpGSIO6WQKBgQD/ROQ+aQEBNYEY+Iza\nh9w2QvZzl3pS6dohLXeq1tSS4Pn7hddhaMniY2gBUC/UdLxaUSHncYneCWZj17wL\nqUxp8tNuYbmFx7SwN0hf4gRVRs9VHliot0ShJehatXIUdQtZGem6ZeBCMiU6Ahl6\nw0DoeSmXmXKMjA/0nqa7V8uEbQKBgQDBy8gZIaiPAbskfwzE+rtEGysSrWbp5YmL\novaAQyIU6ZQ1QcSRuwPyLsK1mQokpVLp/D3tuX5bf7QEywXGZpqQkNAkTIAszfd1\nFu5dZYjD3/U8nh2vXSOhoWLV3cs+MFn/1pOE6okd22RxVszKKPAMGwdFHLjqhTgm\n4PaGX0F2vQKBgQD7QdkTIRHPGdXvOT6THuT/N9wTHMdXczy0KWe/bWOhCvajvzKn\ne5gzUDsgB8h8a0a182PWxme4eDIjFkbOh55iLpQz8tbEOjS8uj1vuy0erusG0i7c\nKj/QClrxZeVbA3XfBuWLBhm9rlP8C/L+Zc+uwwvQX0Zj1r0hv0NH5Xu55QKBgGEy\nuguQzB/duGUXq+nn76C7F6FogyNw0snMVGIEScW/WWxUriiBafs/5Lx8I5A5sxpU\nPyNKU8vEgx+cTCs0LvWvDBYIoIjYTPLXK3qAgFXtr+xVSYI47rQUY6PseBGa4qjk\nkFyr3VWyQfBunowOeEc6Dy1kuII3ivBFCKBgik7pAoGAUnhxLBrSuBh4dlwKgTZp\n9qsnRnVRyrHvXipHpeizZAyvEGn1+5jHHFa6OYgX/7k90hRGmY0v27HcFgKwiqZX\nGJ4LovYF5kYuZ6EYs1kdLP5Ha8f6snMr3N1f9aKawBE5Lg3OhDJeNJAxaOkOcJTR\nUoZfBrgvw6u6H/Kr0KMo/Ws=\n-----END PRIVATE KEY-----\n",
"client_email": "ura-ia-na-habre@inxbot.iam.gserviceaccount.com",
"client_id": "116600111704528514606",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ura-ia-na-habre%40inxbot.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
Далее переходим в свою гугл таблицу -> Настройки доступа -> Вписываем email нашего аккаунта (поле client_email в json) -> Права редактор или читатель в зависимости от ваших целей. За гугл таблицу можно не бояться, гугл автоматически сохраняет все версии таблицы во вкладке Файл -> История версий -> Смотреть историю версий.
2. C++ код
Код пояснять практически не буду, если что-то непонятно chat про всё спокойно расскажет.
Авторизация по протоколу OAuth 2.0 с помощью JWT
Метод для парсинга JSON файла. В SERVICE_ACCOUNT_FILE лежит путь к файлу.
rapidjson::Document loadServiceAccountKey() {
std::ifstream file(SERVICE_ACCOUNT_FILE);
std::stringstream buffer;
buffer << file.rdbuf();
rapidjson::Document doc;
doc.Parse(buffer.str().c_str());
return doc;
}
Метод для получения jwt токена. jwt::create не будет работать без трэитов, я взял их из репозитория библиотеки на гитхабе. Так же тут очень важная строчка ".set_payload_claim("scope", jwt::claim(std::string("https://www.googleapis.com/auth/spreadsheets")))" - указывает сервис в который мы получаем аутентификацию.
std::string getJwtToken() {
rapidjson::Document doc = loadServiceAccountKey();
std::string privateKey = doc["private_key"].GetString();
std::string clientEmail = doc["client_email"].GetString();
auto now = std::chrono::system_clock::now();
auto exp = now + std::chrono::minutes(60);
auto rsa = jwt::algorithm::rs256("", privateKey, "", "RS256");
std::string jwt = jwt::create()
.set_issuer(clientEmail)
.set_audience("https://oauth2.googleapis.com/token")
.set_issued_at(now)
.set_expires_at(exp)
.set_payload_claim("scope", jwt::claim(std::string("https://www.googleapis.com/auth/spreadsheets")))
.set_type("JWT")
.set_algorithm("RS256")
.sign(rsa);
return jwt;
}
Пример трэйта. Я использовал этот трейт для библиотеки nlohmann-json. Свой писать на rapid времени не было.
#ifndef JWT_CPP_NLOHMANN_JSON_TRAITS_H
#define JWT_CPP_NLOHMANN_JSON_TRAITS_H
#include "jwt-cpp/jwt.h"
#include "nlohmann/json.hpp"
namespace jwt {
/**
* \brief Namespace containing all the json_trait implementations for a jwt::basic_claim.
*/
namespace traits {
/// basic_claim's JSON trait implementation for Modern C++ JSON
struct nlohmann_json {
using json = nlohmann::json;
using value_type = json;
using object_type = json::object_t;
using array_type = json::array_t;
using string_type = std::string; // current limitation of traits implementation
using number_type = json::number_float_t;
using integer_type = json::number_integer_t;
using boolean_type = json::boolean_t;
static jwt::json::type get_type(const json& val) {
using jwt::json::type;
if (val.type() == json::value_t::boolean) return type::boolean;
// nlohmann internally tracks two types of integers
if (val.type() == json::value_t::number_integer) return type::integer;
if (val.type() == json::value_t::number_unsigned) return type::integer;
if (val.type() == json::value_t::number_float) return type::number;
if (val.type() == json::value_t::string) return type::string;
if (val.type() == json::value_t::array) return type::array;
if (val.type() == json::value_t::object) return type::object;
throw std::logic_error("invalid type");
}
static json::object_t as_object(const json& val) {
if (val.type() != json::value_t::object) throw std::bad_cast();
return val.get<json::object_t>();
}
static std::string as_string(const json& val) {
if (val.type() != json::value_t::string) throw std::bad_cast();
return val.get<std::string>();
}
static json::array_t as_array(const json& val) {
if (val.type() != json::value_t::array) throw std::bad_cast();
return val.get<json::array_t>();
}
static int64_t as_integer(const json& val) {
switch (val.type()) {
case json::value_t::number_integer:
case json::value_t::number_unsigned: return val.get<int64_t>();
default: throw std::bad_cast();
}
}
static bool as_boolean(const json& val) {
if (val.type() != json::value_t::boolean) throw std::bad_cast();
return val.get<bool>();
}
static double as_number(const json& val) {
if (val.type() != json::value_t::number_float) throw std::bad_cast();
return val.get<double>();
}
static bool parse(json& val, std::string str) {
val = json::parse(str.begin(), str.end());
return true;
}
static std::string serialize(const json& val) { return val.dump(); }
};
} // namespace traits
} // namespace jwt
#endif
Метод для получения Acess токена. При написании запросов на curl обращаю внимание на строчку "curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);" логирование очень помогает, если есть какие-то проблемы с запросом. Acess токен нужно всегда получают через post запросы.
std::string getAcessToken() {
CURL* curl = curl_easy_init();
//TODO: Возможно нужно как-то обработать этот случай
if(!curl) return "";
std::string postData = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + getJwtToken();
std::string readBuffer;
curl_easy_setopt(curl, CURLOPT_URL, "https://oauth2.googleapis.com/token");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());
curl_easy_setopt(curl, CURLOPT_POST, 1L);
//curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); // для логирования
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
std::cerr << "Ошибка CURL: " << curl_easy_strerror(res) << std::endl;
curl_easy_cleanup(curl);
return "";
}
curl_easy_cleanup(curl);
rapidjson::Document doc_res;
doc_res.Parse(readBuffer.c_str());
if (!(doc_res.HasMember("access_token") && doc_res["access_token"].IsString())) {
std::cerr << "Ошибка: Не найден access_token в ответе!" << std::endl;
return "";
}
return doc_res["access_token"].GetString();
}
Запись данных.
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) {
output->append((char*)contents, size * nmemb);
return size * nmemb;
}
Получение данных из таблицы. Простой пример в этом случае получим данные из D столбца.
void updateDbWithSheets() {
std::string accessToken = getAcessToken();
std::string range = "clients!D:D";
std::string url = "https://sheets.googleapis.com/v4/spreadsheets/" + SPREADSHEET_ID + "/values:batchGet?ranges=" + range;
// Настройка cURL
CURL* curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, ("Authorization: Bearer " + accessToken).c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
std::string readBuffer;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
std::cerr << "Ошибка CURL: " << curl_easy_strerror(res) << std::endl;
} else {
std::cout << "Ответ от Google Sheets API:\n" << readBuffer << std::endl;
}
curl_easy_cleanup(curl);
curl_slist_free_all(headers);
}
}
Запись данных в таблицу. В гугл таблицы данные отправляются put запросом. Передаете range , если нужно несколько ячеек из разных участков таблицы, спросите у гпт или посмотрите как прописывается в google sheets api. Это не сложно изменить.
void updateSheetsWithDb() {
CURL* curl = curl_easy_init();
if(curl) {
const std::string accessToken = getAcessToken();
const std::string range = "clients!A3:AD3";
std::string url = "https://sheets.googleapis.com/v4/spreadsheets/" + SPREADSHEET_ID + "/values/" + range + "?valueInputOption=RAW";
const std::string jsonData = R"({"range":"clients!A3:AD3","majorDimension":"ROWS","values":[["01.03.25","Артем Дадерко", "Tg", "id", "90", null, null, null, "80"]]})";
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, ("Authorization: Bearer " + accessToken).c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Accept: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str());
//curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK) {
std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
}
curl_easy_cleanup(curl);
curl_slist_free_all(headers);
}
}
3. Заключение
По сути этих методов хватает чтобы получить и отправить данные без обработки. Этот гайд написал лишь для быстрого старта. Я несколько дней не мог понять в чем моя ошибка при аутентификации и надеюсь с помощью этих наработок вам не придется тратить время на решение таких проблем. Спасибо.