One of the most exciting features of Puppeteer Sharp v0.4 is the ability to connect to remote browsers, like So, when I decided to write a post about using Chrome in Azure, I knew I had two options:

The first option would be something like this:

“To connect to a remote browser you need to use the ConnectAsync function:

var options = new ConnectOptions()
    BrowserWSEndpoint = $"wss://{apikey}"

var browser = await Puppeteer.ConnectAsync(options);
var page = await browser.NewPageAsync();

The end”

The second option was a bit more interesting: A Telegram WebPhotographer.

A tele what?


We need to implement an Azure Function which receives a URL and returns a screenshot of that URL. This Azure function would help not only the Telegram Photographer but also any other service we want to implement.

We also want to implement a Telegram bot in .NET Core and deploy it using a Docker container. This bot would listen to screenshot requests, call our Azure Function and return that image to the client.

Let’s get started

The Azure Function

As we won’t be able to execute Chrome inside an Azure Function, we will need to use a SaaS Chrome such as Once we know the URL of a Chrome instance, we can connect to this external Chrome process using the ConnectAsync function.

var options = new ConnectOptions()
    BrowserWSEndpoint = $"wss://{apikey}"

var browser = await Puppeteer.ConnectAsync(options);

Our Azure function will look something like this:

namespace ScreenshotFunction
    public static class TakeScreenshot
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequest req, 
            TraceWriter log,
            Microsoft.Azure.WebJobs.ExecutionContext context)
            var config = new ConfigurationBuilder()
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)

            string url = req.Query["url"];
            if (url == null)
                return new BadRequestObjectResult("Please pass a name in the query string");
                string apikey = config["browserlessApiKey"];
                var options = new ConnectOptions()
                    BrowserWSEndpoint = $"wss://{apikey}"

                var browser = await Puppeteer.ConnectAsync(options);
                var page = await browser.NewPageAsync();
                await page.GoToAsync(url);
                var stream = await page.ScreenshotStreamAsync(new ScreenshotOptions
                    FullPage = true

                byte[] bytesInStream = new byte[stream.Length];
                stream.Read(bytesInStream, 0, bytesInStream.Length);

                await page.CloseAsync();

                return new FileContentResult(bytesInStream, "image/png");

A piece of cake!

Creating a bot on Telegram

First, we need to create our Telegram bot. Let’s chat with BotFather:


The console app

Implementing a Telegram bot is quite easy thanks to Telegram.Bot. Let’s connect with Telegram:

private static ManualResetEvent Wait = new ManualResetEvent(false);

static void Main(string[] args)
    string telegramApiKey = Environment.GetEnvironmentVariable("WEBPHOTOGRAPHER_APIKEY");
    var botClient = new TelegramBotClient(telegramApiKey);

    botClient.OnMessage += BotClient_OnMessage;

    Console.WriteLine("Telegram Bot Started\npress any key to exit");

If you are wondering about the ManualResetEvent class; It’s just a recipe to get this process alive inside a Docker container. A simple Console.ReadLine won’t work there.

Then, on the Message event, we need to listen to URLs, call our Azure Function and send the image back to the client.

private static async void BotClient_OnMessage(object sender, Telegram.Bot.Args.MessageEventArgs e)
    var linkParser = new Regex(@"\b(?:https?://|www\.)\S+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    var bot = (TelegramBotClient)sender;
    if (!string.IsNullOrEmpty(e.Message.Text))
        foreach (Match m in linkParser.Matches(e.Message.Text))
            await bot.SendTextMessageAsync(e.Message.Chat.Id, "Prepping a screenshot for you my friend");

            var url = (m.Value.StartsWith("http") ? string.Empty : "https://") + m.Value;
            MemoryStream stream = null;

                var data = await new WebClient().DownloadDataTaskAsync(azureFunction + url);
                stream = new MemoryStream(data);

                await bot.SendPhotoAsync(
                    new Telegram.Bot.Types.FileToSend("url", stream),
            catch(Exception ex)
                await bot.SendTextMessageAsync(e.Message.Chat.Id, "Unable to get a screenshot for you");

Deploy time!

There is not much to say about deploying an Azure Function: Right click, Publish, next, next, next and it will be up in Azure.

In order to deploy our Console App in Docker we’ll need these two files:

A Dockerfile file:

FROM microsoft/dotnet:2.0-sdk

# copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# copy and build everything else
COPY . ./
RUN dotnet publish -c Release -o out
ENTRYPOINT ["dotnet", "out/WebPhotographerBot.dll"]

And a docker-compose.yml file:

version: '2.1'

    image: telegramwebphotographer
    build: .

We have include the real Telegram API Key and Azure Function endpoint.

Finally, we run docker-compose up and we’ll have our bot up and running. Let’s check this out!


Final Words

First of all, this is a proof of concept. Don’t use this code in production! Second, I know this solution was a little bit over-engineered. The idea was showing off how to connect to a remote Chrome instance and also playing a little bit with .NET Core and Docker. Wrapping up, you can find this code on Github as telegram-webphotographer, feel free to fork it and play with it.

Don’t stop coding!