Outline VPN + Telegram: bot hosted on DigitalOcean Functions (serverless)

Search for a command to run...

Turned a messy Google Payments export into a reusable Codex skill. The main bug was duplicate month rows like Jan 27 vs Jan 27, 2025, which can make you export the wrong year if you click too fast. The skill now reuses the logged-in Chrome session, targets explicit-year rows only, resets the details pane, and verifies every image by date + transaction ID.
You want to generate HideMail aliases from Bitwarden app or browser extension? Easy. 1) In Bitwarden options for username generation select “Forwarded email alias”; 2) Select as service “Addy" 3) Enter as domain hidemail.app (in future you can add cu...
How to solve "Error: Google Sign-In failed: [16] Account reauth failed" issue
Hetzner's S3-compatible Object Storage provides a scalable and cost-effective solution for storing and retrieving large amounts of data. In this guide, we'll walk through the process of integrating Hetzner S3 Object Storage with a Laravel 11 applicat...

Or how to remember what social login I used on websites for "sign in" Managing social logins efficiently is crucial in today's digital world. But you can simplify this process using Bitwarden or any other password manager like 1password. Create a de...
TLDR: if you want to try my free VPN bot, click this link @hidemail_vpn_bot
I decided to share what I have for my own Outline VPN Server - it simple bot where you can generate a new personal key to access this VPN server.
In the end, you will have Telegram chat id and @nickname of a user (if exist) inside a list of your outline VPN keys, like on cover for this post
Here is all code with a few comments, in general, you only need to create a telegram bot and put a token there, your chat id and outline server URL and Port.
<?php
const TELEGRAM_BOT_TOKEN = 'XXXXX:XXXXX'; // you can create new bot and generate this token with @BotFather bot - write there /newbot command
const TELEGRAM_BOT_ADMIN_CHAT_ID = 'XXXXX'; // your chat id, you can find it with @cid_bot bot
const API_PORT = 'PORT';
const API_URL = 'https://XXXXX:PORT/SOMEKEYFROMOUTLINE/';
function sendToTelegram(array $args, $url): void
{
$curl = curl_init();
$urlWithName = $url . urlencode('Name of your server for Outline VPN Client');
$id = $args['message']['chat']['id'];
$lang = $args['message']['from']['language_code'] ?? 'en';
//if ($lang === 'en') {
$message = 'Hello! I invite you to connect to my VPN (Outline) serverm so you will get access to the free Internet, wherever you are. Follow the instructions on the link provided in the invitation to download the app and set up your connection.
https://s3.amazonaws.com/outline-vpn/invite.html#' . $url . '
Have problems accessing the invitation link? Write to: @flatroy
Click and copy an access key: `' . $urlWithName . '`
';
// } else {
// with this IF you can add some message in different language if you want
// }
$message = urlencode($message);
curl_setopt_array($curl, [
CURLOPT_URL => 'https://api.telegram.org/bot'.TELEGRAM_BOT_TOKEN.'/sendMessage?chat_id=' . $id . '&parse_mode=Markdown&text=' . $message,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_POSTFIELDS => "",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
// send message to admin
$curl = curl_init();
$id = $args['message']['chat']['id'];
// notify admin of bot
$message = 'new user message: ' . $args['message']['text'] . " from @" . $args['message']['chat']['username'] . ' ' . $args['message']['chat']['id'] . ' ' . $args['message']['chat']['first_name'] . ' - lang: ' . $args['message']['from']['language_code'];
curl_setopt_array($curl, [
CURLOPT_URL => 'https://api.telegram.org/bot'.TELEGRAM_BOT_TOKEN.'/sendMessage?chat_id='.TELEGRAM_BOT_ADMIN_CHAT_ID.'&text=' . $message,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_POSTFIELDS => "",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
}
function main(array $args): array
{
$API_PORT = API_PORT;
$API_URL = API_URL;
if ($args['message']['text'] === '/create' || $args['message']['text'] === '/start') {
//check that if we already have key
$name = "@" . $args['message']['chat']['username'] . ' ' . $args['message']['chat']['id'] . ' ' . $args['message']['chat']['first_name'] . ' - from My Bot';
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => $API_PORT,
CURLOPT_URL => $API_URL . '/access-keys/',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_POSTFIELDS => "",
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
return ["body" => "cURL Error #:" . $err];
} else {
$keys = json_decode($response, true);
foreach ($keys['accessKeys'] as $key) {
if ($key['name'] === $name) {
//return ["body" => 'ok exists'];
sendToTelegram($args, $key['accessUrl']);
return ["body" => 'ok'];
}
}
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => $API_PORT,
CURLOPT_URL => $API_URL . '/access-keys/',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "",
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
return ["body" => "cURL Error #:" . $err];
}
echo $response;
$data = json_decode($response, true);
sendToTelegram($args, $data['accessUrl']);
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => $API_PORT,
CURLOPT_URL => $API_URL . '/access-keys/' . $data['id'] . '/name',
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => "name=" . $name,
CURLOPT_HTTPHEADER => [
"Content-Type: application/x-www-form-urlencoded"
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 30,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
// limit traffic
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => $API_PORT,
CURLOPT_URL => $API_URL . '/access-keys/' . $data['id'] . '/data-limit',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => "{\"limit\":{\n\t\"bytes\": 200000000\n}}",
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
}
return ["body" => 'ok'];
}
This code is done only to use on DigitalOcean (serverless) functions: you can read about it here - https://www.digitalocean.com/products/functions
Hope it will help anyone, cheers!