Writing a simple Telegram bot in C
Photo by @ilgmyzin from Unsplash.com
Introduction
In this post, we’ll build a simple echo bot for Telegram using pure C. The bot will receive messages and echo them back to the user. We’ll use libcurl for HTTP requests and json-c for parsing JSON responses.
Requirements
You’ll need:
libcurl- for making HTTP requests to the Telegram APIjson-c- for parsing JSON responses- A Telegram bot token (get one from @BotFather)
- A C compiler (gcc or clang)
Getting Your Bot Token
- Open Telegram and search for @BotFather
- Send
/newbotand follow the instructions - Choose a name and username for your bot
- BotFather will give you an API token that looks like:
123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ - Keep this token secret - anyone with it can control your bot!
The complete code is available on my git server here.
Understanding Long Polling
Telegram offers two methods to receive updates: webhooks and long polling. For simplicity, we’ll use long polling.
Long polling keeps an HTTP connection open for up to 30 seconds, waiting for new updates. When an update arrives, the server responds immediately. If no updates come within the timeout, the connection closes and we make a new request. This is much more efficient than short polling (repeatedly asking “any updates?”).
The Bot Structure
Let’s start by defining our bot structure and a callback for receiving HTTP responses:
#include <curl/curl.h>
#include <json-c/json.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
CURL *curl;
char api[256];
long last_offset;
} bot_s;
size_t write_cb(void *ptr, size_t size, size_t nmemb, char *data) {
strncat(data, ptr, size * nmemb);
return size * nmemb;
}
The bot_s struct holds:
curl- the libcurl handle for making HTTP requestsapi- the base API URL with our bot token (e.g.,https://api.telegram.org/bot<TOKEN>/)last_offset- tracks the last update ID we processed to avoid duplicates
The write_cb function is a libcurl callback that appends received data to our buffer. Libcurl calls this function whenever it receives data from the server.
Sending Messages
To send a message back to the user, we construct a URL with the chat ID and text:
void send_message(bot_s *bot, long chat_id, const char *text) {
char url[512];
char *encoded_text = curl_easy_escape(bot->curl, text, 0);
snprintf(url, sizeof(url), "%ssendMessage?chat_id=%ld&text=%s",
bot->api, chat_id, encoded_text);
curl_free(encoded_text);
curl_easy_setopt(bot->curl, CURLOPT_URL, url);
curl_easy_perform(bot->curl);
}
We use curl_easy_escape to URL-encode the text, preventing issues with special characters. This is essential because URL parameters can’t contain spaces, newlines, or special characters like & or ?. The function converts these into percent-encoded format (e.g., space becomes %20).
Important: Always curl_free() the escaped string to prevent memory leaks, as curl allocates memory for the encoded string.
The sendMessage method requires two parameters:
chat_id- the unique identifier for the chat (user or group)text- the message to send (up to 4096 characters)
Handling Updates
The core of our bot is the handle_updates function, which polls for new messages:
void handle_updates(bot_s *bot) {
char url[512], response[4096] = {0};
snprintf(url, sizeof(url), "%sgetUpdates?timeout=30&offset=%ld",
bot->api, bot->last_offset + 1);
curl_easy_setopt(bot->curl, CURLOPT_URL, url);
curl_easy_setopt(bot->curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(bot->curl, CURLOPT_WRITEDATA, response);
if (curl_easy_perform(bot->curl) == CURLE_OK) {
struct json_object *root = json_tokener_parse(response);
struct json_object *result, *update;
json_object_object_get_ex(root, "result", &result);
if (json_object_array_length(result) > 0) {
update = json_object_array_get_idx(result, 0);
/* Extract Update ID (offset) */
struct json_object *id_obj;
json_object_object_get_ex(update, "update_id", &id_obj);
bot->last_offset = json_object_get_int64(id_obj);
/* Extract Chat ID and Message */
struct json_object *msg, *chat, *chat_id_obj, *text_obj;
json_object_object_get_ex(update, "message", &msg);
json_object_object_get_ex(msg, "chat", &chat);
json_object_object_get_ex(chat, "id", &chat_id_obj);
json_object_object_get_ex(msg, "text", &text_obj);
const char *text = json_object_get_string(text_obj);
long chat_id = json_object_get_int64(chat_id_obj);
/* Send the message back */
if (text) {
send_message(bot, chat_id, text);
}
}
json_object_put(root);
}
}
This function:
- Makes a
getUpdatesrequest with a 30-second timeout and the last offset + 1 - Parses the JSON response using
json-c - Extracts the update ID, chat ID, and message text
- Updates
last_offsetto mark this update as processed - Echoes the message back using
send_message
By using offset = last_offset + 1, we tell Telegram we’ve processed all updates up to that point, preventing duplicates.
Understanding the JSON Structure
A typical Telegram update looks like this:
{
"ok": true,
"result": [
{
"update_id": 123456789,
"message": {
"message_id": 1,
"from": {
"id": 987654321,
"first_name": "John",
"username": "john_doe"
},
"chat": {
"id": 987654321,
"first_name": "John",
"type": "private"
},
"date": 1234567890,
"text": "Hello, bot!"
}
}
]
}
We navigate this structure using json-c functions:
json_object_object_get_ex()- extracts an object by keyjson_object_array_get_idx()- gets an element from an arrayjson_object_get_string()- extracts a string valuejson_object_get_int64()- extracts a numeric value
The json_object_put() call at the end is crucial - it decrements the reference count and frees the JSON object when it reaches zero. Forgetting this causes memory leaks.
The Main Loop
Finally, we initialize everything and start polling:
int main(void) {
bot_s bot = {.last_offset = 0};
curl_global_init(CURL_GLOBAL_ALL);
bot.curl = curl_easy_init();
if (!bot.curl) {
fprintf(stderr, "Failed to initialize curl\n");
return 1;
}
snprintf(bot.api, 256, "https://api.telegram.org/bot%s/",
"YOUR_BOT_TOKEN_HERE");
printf("Bot started. Polling for updates...\n");
while (1) {
handle_updates(&bot);
}
curl_easy_cleanup(bot.curl);
curl_global_cleanup();
return 0;
}
Replace YOUR_BOT_TOKEN_HERE with your actual bot token from BotFather.
Important initialization steps:
curl_global_init()- initializes libcurl globally (call once at program start)curl_easy_init()- creates a curl handle for making requests- Check if initialization succeeded before proceeding
- Clean up with
curl_easy_cleanup()andcurl_global_cleanup()when done
The infinite loop continuously polls for updates. In a production environment, you’d want to add graceful shutdown handling (e.g., catching SIGINT).
Testing
Compile with:
gcc -o bot bot.c -lcurl -ljson-c
Run the bot:
./bot
Open Telegram and send a message to your bot - it should echo back everything you send!
Homework
This is a minimal implementation. Some improvements you could make:
1. Handle Multiple Updates: currently, we only process the first update. Modify the code to iterate through all updates:
int num_updates = json_object_array_length(result);
for (int i = 0; i < num_updates; i++) {
update = json_object_array_get_idx(result, i);
/* Process each update */
}
2. Better Error Handling: check for NULL pointers and API errors.
3. Support Different Message Types: handle photos, documents, commands, etc…
4. Implement Command Handling: parse commands like /start, /help.
5. Add Logging: use a proper logging system instead of printf for production use.
6. Graceful Shutdown: handle SIGINT and SIGTERM to clean up resources properly.
Conclusion
We’ve built a functional Telegram bot in less than 100 lines of C! The code demonstrates the basics of the Telegram Bot API, HTTP requests with libcurl, and JSON parsing with json-c.
For a more complete implementation, check out my minimal bot library here.