Writing a Telegram echo bot in C

· Francesco · 5 min read · 1059 words
Table of Contents

Photo by @ilgmyzin from Unsplash.com

My main programming language is C (even though I’d like to write more Go these days). I’ve been working with it for years, and since I use Telegram daily, I figured: why not write a bot in C? I know there’s already telebot, but I wanted to learn something new.

In this post, we’re going to build a simple echo bot, it replies with whatever you send it. We’ll use libcurl for HTTP and yyjson for parsing the responses.

Requirements

  • libcurl
  • yyjson
  • A Telegram bot token (get one from @BotFather)

Understanding Long Polling

Before writing any code, let’s talk about how we’re going to receive messages. Telegram offers two methods: webhooks and long polling. We’re going with long polling, it’s simpler.

Here’s the idea: we open an HTTP connection to Telegram’s API and keep it open for up to 30 seconds, waiting for new updates. If a message arrives, the server responds immediately with the data. If nothing shows up before the timeout, the connection closes and we open a new one.

This is way more efficient than short polling (you are basically spamming their servers with requests every a few second).

The Bot Structure

Let’s start by defining the data structures we’ll use throughout the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <curl/curl.h>

#define API_LEN 256
#define URL_LEN 512
#define TEXT_LEN 4096

typedef struct tgbot {
  CURL *curl;
  char api[API_LEN];
  int64_t offset;
} tgbot;

typedef struct tgmessage {
  int64_t chat_id;
  const char *text;
} tgmessage;

The tgbot struct holds everything our bot needs: the curl handle for making requests, the api base URL (https://api.telegram.org/bot<TOKEN>/), and the offset.

The offset is very important, it tells Telegram which updates we’ve already handled. Every time we process an update, we bump the offset so we don’t process the same message twice.

The tgmessage struct is just a better way to pass message data around functions.

Handling Updates

Now let’s handle updates. First, we need a callback function that libcurl will use to write the HTTP response into a buffer:

1
2
3
4
size_t write_cb(void *ptr, size_t size, size_t nmemb, char *data) {
  strncat(data, ptr, size * nmemb);
  return size * nmemb;
}

And here’s the function we were waiting for:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int handle_updates(tgbot *bot) {
  char url[URL_LEN] = {0};
  char response[TEXT_LEN] = {0};
  snprintf(url, sizeof(url), "%sgetUpdates?timeout=30&offset=%ld", bot->api,
           bot->offset + 1);

  curl_easy_setopt(bot->curl, CURLOPT_TIMEOUT, 35L);
  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);

  int res = curl_easy_perform(bot->curl);
  if (res != CURLE_OK) {
    return -1;
  }

  fprintf(stdout, "%s\n", response);

  /* ... JSON parsing comes next */
}

Notice the timeout is set to 35 seconds, slightly higher than the 30-second long polling timeout. This gives Telegram enough time to respond before our side gives up.

The JSON Structure

A typical response from the getUpdates endpoint looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "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!"
      }
    }
  ]
}

The result array can contain multiple updates, so we need to iterate through all of them.

Parsing the Response

Let’s pick up where we left off: parsing the JSON and extracting the data we need.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  yyjson_doc *doc = yyjson_read(response, strlen(response), 0);
  yyjson_val *root = yyjson_doc_get_root(doc);

  /* Check if the request was successful */
  yyjson_val *ok = yyjson_obj_get(root, "ok");
  if (yyjson_get_bool(ok) != true) {
    yyjson_doc_free(doc);
    return -1;
  }

  /* Iterate over the result array */
  yyjson_val *result = yyjson_obj_get(root, "result");
  yyjson_val *a_result;
  size_t idx, max;
  yyjson_arr_foreach(result, idx, max, a_result) {
    /* Update the offset to mark this update as handled */
    yyjson_val *update_id = yyjson_obj_get(a_result, "update_id");
    bot->offset = yyjson_get_sint(update_id);

    /* Extract message data */
    yyjson_val *message = yyjson_obj_get(a_result, "message");
    yyjson_val *text = yyjson_obj_get(message, "text");
    yyjson_val *chat = yyjson_obj_get(message, "chat");
    yyjson_val *id = yyjson_obj_get(chat, "id");

    /* Build the reply */
    tgmessage msg = {
        .chat_id = yyjson_get_sint(id),
        .text = yyjson_get_str(text),
    };
    send_message(bot, &msg);
  }

  yyjson_doc_free(doc);
  return 0;
}

The key thing here is the offset update. By setting bot->offset to the current update_id, we tell Telegram “I’ve processed everything up to this point.” Next time we poll, we pass offset + 1 to skip past what we’ve already seen.

Sending Messages

We send a request to the sendMessage endpoint with the chat ID and our text:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void send_message(tgbot *bot, tgmessage *message) {
  char url[512];
  char *encoded_text = curl_easy_escape(bot->curl, message->text, 0);
  snprintf(url, sizeof(url), "%ssendMessage?chat_id=%ld&text=%s", bot->api,
           message->chat_id, encoded_text);
  curl_free(encoded_text);

  curl_easy_setopt(bot->curl, CURLOPT_URL, url);
  curl_easy_perform(bot->curl);
}

Don’t forget to URL-encode the text! curl_easy_escape handles that for us and curl_free releases the allocated memory.

The Main Loop

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(int argc, char **argv) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s <token>\n", argv[0]);
    return -1;
  }

  curl_global_init(CURL_GLOBAL_ALL);

  tgbot bot = {
      .offset = 0,
      .curl = curl_easy_init(),
  };

  snprintf(bot.api, sizeof(bot.api), "https://api.telegram.org/bot%s/",
           argv[1]);

  int err = 0;
  while (!err) {
    err = handle_updates(&bot);
  }

  curl_easy_cleanup(bot.curl);
  curl_global_cleanup();

  return 0;
}

The bot runs in a simple loop: fetch updates, process them, repeat. If handle_updates returns an error, the loop exits and we clean up.

One thing to improve: handle SIGINT (CTRL+C) so you can shut down the bot cleanly.

Compiling

Compile with:

1
gcc main.c -o bot $(pkgconf --libs libcurl yyjson)

Run it:

1
./bot <your_token>

Open Telegram, find your bot, and send it a message. If everything went right, it’ll echo whatever you send back to you.

Conclusion

In my free time, I’m working on a more complete library with better error handling and more API methods. You can follow the progress here.

Further Reading