How to send photos to Telegram in Ruby

Robert Woolley
4 min readNov 29, 2020

One of the nice things about APIs is that you can bolt disperate systems together to do quite complex things simply.

Photo by Sandra Tan on Unsplash

Well, that’s the theory. On a project I’m working on currently I want to be able to send photos to Telegram. The project uses Rails 6. Photos are JPEGs and are retrieved from an external server over a SOAP interface (not recommended) in base64 format and then need to be sent to Telegram.

Ruby has lots of plug-ins which extend functionality — ‘Gems’. For this project I’ve used telegram-bot-ruby. Other Gems are available, but this one had the clearest documentation.

So, let’s step through the process:

a) First, you’ll need an API key to access the system.

b) You will do this by setting up a Bot in Telegram — and using that key.

https://www.sitepoint.com/quickly-create-a-telegram-bot-in-ruby/ is an excellent article which steps through these points.

Now, we want to setup a class to control Telegram. This is simple. In the class we want:

a) A client which will set up a Telegram connection when called.

b) A method which will will receive a base64 string and a message string, process them and send them to Telegram.

For the client, it’s probably best to set it up when a new instance of our controller is called.

Also, for our bot we are going to need to specify where we are going to send the photo message to.

Specifying Our Destination

In my application, I set up a group for messages. In order to send messages to that group I need the group ID. This is a negative number.

Stack Overflow here: https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id explains how to find it.

To summarise:

a) Add the bot to the Group — by going to the group. Click ‘add members’ — in the dialogue box, type in the Bot’s name. Then click ‘add’.

b) Send a dummy message to the bot from your own account. You MUST do this or the next step won’t work. The example given is the message /my_id @my_bot

c) Go to the following URL; replace xxxx with the bot token you got when you created the bot: https://api.telegram.org/xxx/getUpdates

d) You’ll see a JSON object with “chat”:{“id”:-zzzzzz

The -zzzzz will be Group ID.

Building Telegram Controller Class

In our class we are going to:

require: ‘telegram/bot’ 

so that we can use Telegram

require: ‘base64’ 

so that we can convert our base64 text to binary.

For our constructor method we need to:

a) Check we have a API key present on our machine — and raise an error if it isn’t.

b) Set up a client with the key

In my example, I’ve set the API key as an environmental variable TELEGRAM_KEY :

def initialize(chat_id = xxxxx)
@chat_id = chat_id
bot = Telegram::Bot::Api
raise ‘API key for Telegram not set. export TELEGRAM_KEY=[key]’ if ENV[‘TELEGRAM_KEY’].nil?
token = ENV[‘TELEGRAM_KEY’]
@client = bot.new(token)
end

In the code above:

a) We assume that the chat id for our group is the xxxxx above. The dependency injection in there allows this to be overriden if necessary.

b) We throw an error if an API hasn’t been set.

Receiving and processing text and base64 strings

I’ve assumed that any other class which calls TelegramController does so using a hash. The hash has two keys:

text — This contains the text of anything we want to send with the photo. It will appear as a caption to the photo and can be a maximum of 1024 characters.

photo — This is the base 64 encoded JPEG string we want to send.

Here’s the code for our method — or at least the top level method:

def send_photo_message(hash)
caption = hash[:text]
photo = hash[:photo]
photo_stream = create_image_stream(photo)
photo_message(caption, photo_stream)
end

We assign the text and photo strings to local variables. To process the photo string we call create_image_stream

def create_image_stream(photo)
binary = Base64.strict_decode64(photo)
StringIO.new(binary)
end

We do two things in this method:

i) Convert our base64 encoded stream to a binary.

ii) Convert our binary string to StringIO object. This is necessary as Faraday (see below) can’t process it otherwise.

Our method returns the binary StringIO object to the send_photo_message method. We know need to send the binary and text to Telegram. We do this with the photo_message method:

def photo_message(caption, photo_stream)
@client.send_photo(chat_id: @chat_id, caption: caption,
photo: Faraday::FilePart.new(photo_stream,'image/jpeg'))
end

We defined @client when we called a new instance of TelegramControllersend_photois a method inside Telegram. We specify the parameters for the api as symbols. These are passed as snake_case.

For the photo it’s a little more complex. In order to send a photo to Telegram we need to ensure that it’s sent in a multipart format, and has the file type for Telegram.

Faraday is an http client which manages a whole bunch of things. In this case, it’s managing the wrapping. Incidentally if you want to send a file instead of a StringIO object the photo_stream variable should have filename and path of the file to be sent.

Finally, our caption parameter. This will be the text we received in our initial hash.

Final Note

I decided to write this piece, as I struggled to find documentation on how to send a photo as a variable. This missing piece was StringIO which allows a string to be treated as a file.

--

--

Robert Woolley

Early 50s something ex-manager crossed with mid-level developer. I’m proof that you can change careers in your late 40s — and survive and thrive.