Дополнения
Еще был интересен вопрос перевода приложения. Сейчас бот по умолчанию на английском, но планирую сделать и русский интерфейс. Перевод, как оказалось, легко делается через добавление в Visual Studio Resources file со строками, к нему генерируется класс, у которого можно поменять свойство Culture и для разных языков будут выдаваться разные строки.
, которое появилось недавно, 15 мая.
Если у вас есть вопросы, с удовольствием отвечу в комментариях.
Как правильно сгенерировать ssl сертификат для telegram?
C-страна, два символа латиницы, Россия-RU,Украина-UA,Казахстан-KZ и т.д, таблица в тырнете есть
ST-регион. Слово “область” обычно записывается как region
L-город
О-организация. “ООО” обычно передается как LLC в начале названия или Ltd в конце
CN-веб-адрес типа www.nichego.net.
Все это задавать необязательно, если генерите сами себе. Или же можно задать левые значения
Метрики и производительность.
Для сбора метрик есть два хороших сервиса — это
от Яндекс а также
Общие принципы работы api
Telegram API присылает вашему приложению-боту массив в формате JSON — это объект
В нем содержится множество информации — id чата, пользователя, сам текст сообщения, прикрепленные фотографии и другие файлы, может быть местоположение пользователя или карточка контакта из его телефона. Есть два основных способа получения этой информации.
При первом способе ваше приложение каждые 100мс (или реже) соединяется с сервером Telegram и спрашивает, не появилось ли чего нового — это метод getUpdates. Минусы этого подхода в том, что создается большая нагрузка на сервера Telegram, а также иногда при большой нагрузке сервер может отдать 503 ошибку и это нужно обработать в приложении. Зато такой способ проще в реализации.
Второй способ — вы делаете свое приложение в виде «сервера» который слушает определенный порт, и Telegram, при наличии обновлений, отправляет их нашему приложению. Тут минус в том, что нужен SSL-сертификат, хотя бы и самоподписанный, а также желательно наличие доменного имени. Но есть и способы сделать всё очень просто, которые я опишу ниже.
Пишем «hello, world» telegram бота на си
Привет всем, не знаю зачем это надо, но может кому пригодится…
Дисклеймер: Я ни в коем случае не являюсь профессиональным Си программистом.
Что нам понадобится:
1. Любой компьютер на Linux, Ubuntu, Centos, MacOS… с доступом к порту 443 или 8443 из интернета.
2. Любой Си компилятор
3. Бибилиотеки openssl, libssl-dev («apt-get install openssl libssl-dev» в терминале, для Ubuntu)
Итак, приступим…
Первое что нужно сделать — это создать бота у отца всех ботов @BotFather, опустим все подробности и предположим что с этой задачей все справились и получили токен, что-то вроде:
373288854:AAHHT77v5_ZNEMус4bfnРЩo6dxiMeeEwgwJ
Далее создадим ssl сертификат, для установки WebHook. Команда выглядит примерно так:
openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 365 -out public.pem
Упакуем ключ и публичный сертификат в один файл:
cat private.key public.pem > cert.pem
Устанавливаем WebHook:
curl -F"url=https://ВАШ_IP:ПОРТ(либо 443, либо 8443)/ЛЮБОЙ_URI(можно и без него, я буду использовать токен)/" -F"certificate=@public.pem" https://api.telegram.org/botТОКЕН/setWebhook/
Должен прийти JSON ответ что-то типа success:true…, если нет то проверьте все и попробуйте еще раз.
Приступаем к самому интересному:
Создаем файл main.c и открываем его в любом редакторе. Включаем необходимые библиотеки:
#include <stdio.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <unistd.h>
#include <openssl/err.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <resolv.h>
#include <netdb.h>
Функция инициализации сокета:
int InitializeSocket(int port) {
int sd = socket(AF_INET, SOCK_STREAM, 0);
if (sd < 0) exit(-1);
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET;
s_addr.sin_addr.s_addr = INADDR_ANY;
s_addr.sin_port = htons(port);
if (bind(sd, (struct sockaddr *)&s_addr, sizeof(s_addr)) < 0) {
printf("Binding Error!n");
exit(-3);
}
return sd;
}
Включаем SSL/TLS:
SSL_CTX * InitializeSSL(char[] certificate) {
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
SSL_library_init();
SSL_CTX * sslctx = SSL_CTX_new(TLSv1_2_server_method());
if (SSL_CTX_use_certificate_file(sslctx, certificate , SSL_FILETYPE_PEM) <= 0) {
exit(-2);
}
if (SSL_CTX_use_PrivateKey_file(sslctx, certificate, SSL_FILETYPE_PEM) <= 0) {
exit(-2);
}
if (!SSL_CTX_check_private_key(sslctx)) {
exit(-2);
}
return sslctx;
}
Собственно сам main():
int main() {
SSL_CTX * sslctx = InitializeSSL("cert.pem"); //Созданный нами файл из приватного ключа и публичного сертификата
int sd = InitializeSocket(8443); //Порт который вы указали при установке WebHook
listen(sd, 5); //Слушаем подключения на созданном сокете
while (1) { //Запускаем бесконечный цикл
int client = accept(sd, NULL, NULL) //функция accept ждет новое подключение, в качестве параметров принимает сокет, указатель на структуру sockaddr, и указатель на размер этой структуры и записывает туда данные подключения, так как нам необязательно знать подробности подключения отправим NULL, функция возвращает сетевой дескриптор.
SSL * ssl = SSL_new(sslctx); //Cоздаем ssl дескриптор
SSL_set_fd(ssl, client); //Переключаем обычный дескриптор на защищенный
if (SSL_accept(ssl) <= 0) { //Пытаемся принять подключение, если ошибка то закрываем соединение и возвращаемся к началу цикла
SSL_clear(ssl);
close(newsd);
continue;
}
//Для увеличения производительности будем использовать fork() и обрабатывать соединение в дочернем процессе, а родительский процесс вернем к ожиданию новых подключений
int pid = fork();
if (pid != 0) { //Если это родитель, то закрываем подключение и возвращаемся к началу цикла
SSL_clear(ssl);
close(newsd);
continue;
}
//Дальнейшие действия будут происходить в дочернем процессе
//Опишу их дальше после некоторых пояснений....
exit(0); //Завершаем дочерний процесс
}
}
Так как Telegram использует HTTP протокол поясню некоторые моменты:
Любой HTTP запрос состоит из заголовков отделенных между собой “rn”, и тела отделенного от заголовков “rnrn”, может быть пустым, но разделитель “rnrn” присутствует всегда. Запросы от Telegram будут приходить методом POST, тело будет в формате JSON.
Пример запроса похожего на Telegram:
POST /(URI указанный при установке WebHook) HTTP/1.1rn
....Неважные для нас поля заголовков
Content-Type: application/jsonrn (Тип данных в теле)
Content-Length: 256rn (Размер тела в байтах, целое число)
..../r/n/r/n
Json тело
При каждой отправке человеком боту сообщения, сервер телеграма будет посылать подобные запросы нашему серверу. В общем случае на них отвечать не обязательно, но в случае с Telegram — обязательно, иначе он будет циклично посылать один и тот же запрос.
Для этого подготовим короткий HTTP response:
HTTP/1.1 200 OKrn
Connection: closernrn
Этих двух полей достаточно что бы сказать серверу Telegram что все нормально, ответ 200 и можно закрывать соединение
Продолжаем писать программу. Внутри цикла после создания дочернего процесса…
char[] response = "HTTP/1.1 200 OKrnConnection: closernrn"; //Наш HTTP response
char header[1024];
bzero(header,1024); //Выделили массив для записи в него заголовков запроса и на всякий случай занулили там все записи.
int s = 0;
int n = 0;
while (strcmp(header s - strlen("rnrn"), "rnrn") != 0) { //strcmp Сравнивает две строки и если они равны возвращает 0, в нашем случае сравниваем последние strlen("rnrn") байт с "rnrn", то есть ищем конец заголовка
n = SSL_read(ssl,header s,1); //Считываем данные по одному байту в header s, s - общее кол-во считанных байт
s = n; //n - кол-во считанных байт за раз
}
//Все, заголовки считаны, теперь нам надо проверить метод, uri, content-type и вытащить content-length запроса.
if (strstr(header,"POST /(URI указанный при установке WebHook) HTTP/1.1rn") == NULL) { //Ищем вхождение строки POST .... в header, если его нет то возвращается NULL, значит пришел неверный запрос, закрываем подключение и завершаем дочерний процесс
SSL_clear(ssl);
close(client);
exit(0);
}
//Также проверим тип данных, должен быть application/json;
if (strstr(header, "Content-Type: application/json") == NULL) {
SSL_clear(ssl);
close(client);
exit(0);
}
//Если все нормально, то узнаем размер тела
int len = atoi(strstr(header, "Content-Length: ") strlen("Content-Length: ")); //strstr возвращает указатель не первое вхождение указанной строки, то есть на "Content-Length: ", а кол-во байт записано дальше после этой строки, поэтому прибавляем длину строки "Content-Length: " и приводим строку к типу int функцией atoi(char *);
char body[len 2];
bzero(body, len 2); //Создаем массив для тела, на этот раз мы точно знаем сколько байт нам понадобится, но создаем с запасом, дабы не оказалось что в памяти сразу после нашей строки что-то записано
n = 0;
s = 0;
while (len - s > 0) { //Так как мы четко знаем сколько данных нам надо считать просто считываем пока не считаем нужное кол-во
n = SSL_read(ssl, request s, len - s); //Конечно можно было считать целиком все данные, но бывают случаи при плохом соединении, за раз все данные не считываеются, и функция SSL_read возвращает кол-во считанных байт
s = n;
}
//На этом получение данных окончено, отправим наш http response и закроем соединение SSL_write(ssl, response, (int)strlen(response));
SSL_clear(ssl);
SSL_free(ssl);
close(client);
//Так как у нас "Hello, World" бот то мы будем просто отвечать на любое сообщение "Hello, World!", но нам нужно знать кому отправлять сообщение для это из тела запросы надо вытащить параметр chat_id
int chat_id = atoi(strstr(""chat_id":") strlen(""chat_id":")); //То же самое что и с Content-Length
//Осталось только отправить сообщение, для этого лучше создадим отдельную функцию SendMessage
char msg[] = "Hello, World!";
SendMessage(chat_id, msg); //Описание функции далее
Для отправки запросов нам почти так же понадобится инициализировать сокет и ssl, но в отличие от получения запросов, мы не будем ждать подключения а просто сразу будем отправлять данные:
void SendMessage(int chat_id, char[] msg) {
int port = 443;
char host[] = "api.telegram.org"; //Адрес и порт всегда одинаковые
//Создадим шаблон HTTP запроса для отправки сообщения, в виде форматированной строки
char header[] = "POST /bot352115436:AAEAIEPeKdR2-SS7p9jGeksQljkNa9_Smo0/sendMessage HTTP/1.1rnHost: files.ctrl.uzrnContent-Type: application/jsonrnContent-Length: %drnConnection: closernrn%s";
//Шаблон тела для отправки сообщения
char tpl[] = "{"chat_id":%d,"text":"%s"}";
char body[strlen(tpl) strlen(msg) 16];
bzero(body, strlen(tpl) strlen(msg) 16);
sprintf(body,tpl,chat_id,msg); //Как printf, только печатаем в char[]
char request[strlen(header) strlen(body) 4];
bzero(request,strlen(header) strlen(body) 4);
sprintf(request, header, strlen(body), body);
//Подготовили наш запрос, теперь создаем подключение
struct hostent *server;
struct sockaddr_in serv_addr;
int sd;
sd = socket(AF_INET, SOCK_STREAM, 0);
if (sd < 0) exit(-5);
server = gethostbyname(host); //Данная функция получает ip и еще некоторые данные по url
if (server == NULL) exit(-6);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
memcpy(&serv_addr.sin_addr.s_addr,server->h_addr,server->h_length);
if (connect(sd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0) { exit(-6);}
SSL_CTX * sslctx = SSL_CTX_new(TLSv1_client_method());
SSL * cSSL = SSL_new(sslctx);
SSL_set_fd(cSSL, sfd);
SSL_connect(cSSL);
SSL_write(cSSL,request,(int)strlen(request)); //Отправляем наш запрос, в идеале его надо отправлять так же как мы считывали данные, то есть с проверкой на кол-во отправленных байт
char str[1024];
SSL_read(cSSL, str, 1024); //Считываем ответ и закрываем соединение
SSL_clear(cSSL);
SSL_CTX_free(sslctx);
close(sd);
}
На этом, в принципе все. Сохраняем файл в одной папке с сертификатом, компилируем любым компилятором и запускаем:
clang main.c -o bot -lcrypto -lssl
./bot
Конец!
Надеюсь статья будет кому-то полезной.
Получение сертификата и ngrok
Когда логика бота более-менее заработала, я решил переписать его через WebHook. Самый легкий путь сделать это — воспользоваться сервисом
Проблема с сертификатом в telegram
Использую сервер с ISPmanager 5.8, ОС Ubuntu Server 16.04.
Порт 443 открыт, доступ извне есть. Генерирую ключ такой командой:
openssl req -newkey rsa:2048 -sha256 -nodes -keyout tele.key -x509
-days 365 -out tele.pem -subj “/C=RU/ST=Krasnodar Krai/L=Tuapse/O=telegram/CN=tele.zhirov.su”
Далее, что я делаю… Через Sublime открываю tele.key и tele.pem, копирую код в ISPmanager при создании сертификата
В доменном имени сайта прикрепляю этот сертификат – всё успешно. Браузер видит сертификат.
Далее – отправляю телеграму tele.pem ключ:
<form action="https://api.telegram.org/botТОКЕНБОТА/setwebhook" enctype="multipart/form-data">
<input type="hidden" name="url" value="https://tele.zhirov.su/bot.php">
<input type="file" name="certificate">
<input type="submit" value="Отправить данные">
</form>
В ответ приходит:
{“ok”:true,”result”:true,”description”:”Webhook was set”}
Проверяю командой getWebhookInfo, получаю такое на выходе:
{“ok”:true,”result”:{“url”:”https://tele.zhirov.su/bot.php“,”has_custom_certificate”:false,”pending_update_count”:0,”last_error_date”:1485273410,”last_error_message”:”SSL
error {336134278, error:14090086:SSL
routines:ssl3_get_server_certificate:certificate verify
failed}”,”max_connections”:40}}
Так же пробовал отправлять и такой командой через терминал:
curl -F "url=https://tele.zhirov.su/bot.php" -F "certificate=tele.pem" "https://api.telegram.org/botТОКЕНБОТА/setwebhook"
Что делать, я уже не знаю… Может сертификат не правильно добавляю или отправляю?
Реализация
В качестве готовой библиотеки я взял
от MrRoundRobin.
Цель моего бота
У меня была несложная задумка — бот должен принимать от пользователя файлы и загружать их на файлообменник, а также выдавать пользователю хотя бы 10 последних ссылок на закачанные файлы. Также я хотел сделать Inline режим — при упоминании ника бота в чате, он выдает последнюю ссылку на закачанный в файлообменник файл.