Telegex - The perfect Telegram bot API client, and an easy-to-use framework for building bots

Background

About three years ago, I created a library called Telegex. Although its implementation also involves using data to generate functions that call the API, it still appears unable to keep up with the Bot API’s frequent updates.

Recently, I completely redesigned this library. Its speed of adaptation to new APIs is unparalleled, as it only requires one command. The secret lies in generating code based on the official documentation “data”, and I only need to update the document to adapt to all the latest changes, including API alterations, comment changes, changes to any type or field.

Why is it called documentation “data”?

Because the documents are really parsed into data. I have converted almost all of the valid content from the official document page (Telegram Bot API) into JSON format and uploaded it to a separate repository (telegex/api_doc.json). This includes all types, methods, and comments.

Generating the JSON file from the documents is a Mix task located in the telegex/lib/mix/tasks/gen.doc_json.ex file.

For types

  • I converted the types to JSON, for example:
{
  "name": "WebhookInfo",
  "description": "Describes the current status of a webhook.",
  "fields": [
	{
	  "name": "url",
	  "type": "String",
	  "description": "Webhook URL, may be empty if webhook is not set up",
	  "optional": false
	},
	{
	  "name": "has_custom_certificate",
	  "type": "Boolean",
	  "description": "True, if a custom certificate was provided for webhook certificate checks",
	  "optional": false
	},
	{
	  "name": "pending_update_count",
	  "type": "Integer",
	  "description": "Number of updates awaiting delivery",
	  "optional": false
	},
	{
	  "name": "ip_address",
	  "type": "String",
	  "description": "Optional. Currently used webhook IP address",
	  "optional": true
	},
	{
	  "name": "last_error_date",
	  "type": "Integer",
	  "description": "Optional. Unix time for the most recent error that happened when trying to deliver an update via webhook",
	  "optional": true
	},
	{
	  "name": "last_error_message",
	  "type": "String",
	  "description": "Optional. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook",
	  "optional": true
	},
	{
	  "name": "last_synchronization_error_date",
	  "type": "Integer",
	  "description": "Optional. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters",
	  "optional": true
	},
	{
	  "name": "max_connections",
	  "type": "Integer",
	  "description": "Optional. The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery",
	  "optional": true
	},
	{
	  "name": "allowed_updates",
	  "type": "Array of String",
	  "description": "Optional. A list of update types the bot is subscribed to. Defaults to all update types except chat_member",
	  "optional": true
	}
  ]
}
  • I saved types with common fields as union types, such as:
{
  "name": "ChatMember",
  "description": "This object contains information about one member of a chat. Currently, the following 6 types of chat members are supported:",
  "types": [
	"ChatMemberOwner",
	"ChatMemberAdministrator",
	"ChatMemberMember",
	"ChatMemberRestricted",
	"ChatMemberLeft",
	"ChatMemberBanned"
  ]
}

Union types are return values in some API documentation. They are a group of specific types with some common fields.

  • Usually, the value of one field in the return value of a union type is fixed, and this fixed value is used to point to a specific type. Such as:
{
  "name": "ChatMemberLeft",
  "description": "Represents a chat member that isn't currently a member of the chat, but may join it themselves.",
  "fields": [
	{
	  "name": "status",
	  "type": "String",
	  "description": "The member's status in the chat, always “left”",
	  "optional": false
	},
	{
	  "name": "user",
	  "type": "User",
	  "description": "Information about the user",
	  "optional": false
	}
  ],
  "fixed": {
	"value": "left",
	"field": "status"
  }
}

The fixed field above describes this referencing relationship. When the status field in the return data of an API that returns ChatMember is left, you should convert it to ChatMemberLeft.

For methods

  • I converted the methods to JSON, for example:
{
  "name": "getUpdates",
  "description": "Use this method to receive incoming updates using long polling (wiki). Returns an Array of Update objects.",
  "parameters": [
	{
	  "name": "offset",
	  "type": "Integer",
	  "description": "Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as getUpdates is called with an offset higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset update from the end of the updates queue. All previous updates will be forgotten.",
	  "required": false
	},
	{
	  "name": "limit",
	  "type": "Integer",
	  "description": "Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100.",
	  "required": false
	},
	{
	  "name": "timeout",
	  "type": "Integer",
	  "description": "Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.",
	  "required": false
	},
	{
	  "name": "allowed_updates",
	  "type": "Array of String",
	  "description": "A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member (default). If not specified, the previous setting will be used.\n\nPlease note that this parameter doesn't affect updates created before the call to the getUpdates, so unwanted updates may be received for a short period of time.",
	  "required": false
	}
  ],
  "result_type": "Array of Update"
}

The result_type of methods varies and mainly includes structure types, basic types, union types, and array types. Among them, union types can be ChatMember or Message or True. These types may be nested in each other.

This is why I called it “documentation data” because I actually converted the documentation into data, creating conditions for code generation. Of course, the introduction here is not a complete presentation of document data. In fact, it has more complex details.

You can find them from telegex/api_doc.json, and you may also be able to make use of them, after all, they are independent of the implementation language.

Building Types and API Calls Functions from Document Data

If generating the call function directly from the JSON data, it is very difficult because defining the function cannot be completed by data. So I first created some macros that can generate function and type code by inputting “data”.

Generate types using data:

deftype(WebhookInfo, "Describes the current status of a webhook.", [
  %{
    name: :url,
    type: :string,
    description: "Webhook URL, may be empty if webhook is not set up",
    optional: false
  },
  %{
    name: :has_custom_certificate,
    type: :boolean,
    description: "True, if a custom certificate was provided for webhook certificate checks",
    optional: false
  },
  %{
    name: :pending_update_count,
    type: :integer,
    description: "Number of updates awaiting delivery",
    optional: false
  },
  %{
    name: :ip_address,
    type: :string,
    description: "Optional. Currently used webhook IP address",
    optional: true
  },
  %{
    name: :last_error_date,
    type: :integer,
    description:
      "Optional. Unix time for the most recent error that happened when trying to deliver an update via webhook",
    optional: true
  },
  %{
    name: :last_error_message,
    type: :string,
    description:
      "Optional. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook",
    optional: true
  },
  %{
    name: :last_synchronization_error_date,
    type: :integer,
    description:
      "Optional. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters",
    optional: true
  },
  %{
    name: :max_connections,
    type: :integer,
    description:
      "Optional. The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery",
    optional: true
  },
  %{
    name: :allowed_updates,
    type: %{__struct__: Telegex.TypeDefiner.ArrayType, elem_type: :string},
    description:
      "Optional. A list of update types the bot is subscribed to. Defaults to all update types except chat_member",
    optional: true
  }
])

Generate API call functions using data:

defmethod(
  "getUpdates",
  "Use this method to receive incoming updates using long polling (wiki). Returns an Array of Update objects.",
  [
    %{
      name: :offset,
      type: :integer,
      description:
        "Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as getUpdates is called with an offset higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset update from the end of the updates queue. All previous updates will be forgotten.",
      required: false
    },
    %{
      name: :limit,
      type: :integer,
      description:
        "Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100.",
      required: false
    },
    %{
      name: :timeout,
      type: :integer,
      description:
        "Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.",
      required: false
    },
    %{
      name: :allowed_updates,
      type: %{__struct__: Telegex.TypeDefiner.ArrayType, elem_type: :string},
      description:
        "A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member (default). If not specified, the previous setting will be used.\n\nPlease note that this parameter doesn't affect updates created before the call to the getUpdates, so unwanted updates may be received for a short period of time.",
      required: false
    }
  ],
  %{__struct__: Telegex.TypeDefiner.ArrayType, elem_type: Telegex.Type.Update}
)

The deftype and defmethod above are macros that generate structs and functions respectively using data. They will construct perfect function and struct modules, including complete type specifications and comments.

Extracting Macro Invocation Code from JSON Input

Just use the eex template. Parse the JSON data and inject it into the template to easily generate all macro invocation code. Generating code files from templates is a Mix task located in the telegex/lib/mix/tasks/gen.code.ex file.

Usage

As a client of the Bot API, the call is very simple, and all call functions for Bot methods are in the Telegex module. For more advanced usage, refer to the README.md and documentation.

In fact, Telegex can also be called a framework rather than a simple API client library because it provides a convenient updates processing module and a chain-based updates processing model. For details, please refer to the README.md of the Telegex repository and the documentation of some modules.

As for the part about the “chain”, I have not written any documentation or tutorials. When I have time, I will share with you how it is designed and used.

Conclusion

Code generation based on document data can reduce the adaptation burden of upstream API version changes to almost zero, making my library more correct, reliable, and always up to date. For developers using my library, there will no longer be headaches due to slow updates of the library. This is something to be happy about, so I wrote this article to promote it.

7 Likes

First of all, awesome work! I am really interested in the methodology you follow with this package and really thankfully by your explanations.

I am testing the bot with an idea I have in mind since many time and your package mand your work motivate me to start it!

I will probably have questions, so maybe soon I will post more replies here with doubts :smiling_face:

1 Like