После открытия API ботов для Telegram их популяция начала стремительно расти. Я решил не отставать и обзавестись собственным для возможности удаленно включать компьютер. Для разработки был выбран язык C#, а в качестве хостинга выбор пал на Azure.

Задача

Написать бота, который сможет отправлять «магические пакеты» для включения компьютера на указанный адрес и порт.

Примечание: в статье не рассматриваются получение токена для бота и деплой сайта на Azure. На эти вопросы без труда находится ответ на хабре или Google.

Немного теории и сетевые настройки


Для включения компьютера будет использоваться технология Wake on Lan. Поскольку бот будет находиться в облаке, нам нужно отправлять пакет на внешний адрес. В связи с этим, нужно позаботиться о том, чтобы пакет в итоге попал в локальную сеть и достиг нужной сетевой платы. Для этого нужно пробросить порт на маршрутизаторе:



Почему 7 порт и такой адрес?
Обычно, чтобы включить компьютер в локальной сети с помощью Wake on Lan, требуется отправить «волшебный пакет» на широковещательный адрес локальной сети, на порт 7/UDP (в некоторых случаях могут использоваться и другие порты). Когда сетевая карта получает “свой” пакет, она подает сигнал на включение компьютера.

Пакет состоит из набора байт в следующем порядке: первые 6 байтов — нулевые, затем идет последовательность байт из мак-адреса сетевой карты, который повторяется 16 раз. Собственно, именно по MAC адресу сетевая карта и понимает, что нужно включить именно ее компьютер.

Мы будем формировать такой пакет и отправлять его на наш внешний адрес (например, на адрес домашнего роутера), после чего пакет должен будет маршрутизироваться на broadcast адрес локальной сети.

В моем случае пробрасывается 7 порт (доступный из интернета) на шировещательный адрес сети (10.10.10.255).


Пишем код


Для работы с API бота была выбрана библиотека Telegram.Bot. Для получения обновлений будем использовать вариант с вебхуком. В этом случае каждое сообщение или событие, которые будет получать бот, будут отправлены на указанный URL.

В качестве хостинга было решено использовать Azure, так как он подходит по всем параметрам:
  • всегда доступен
  • на бесплатном тарифе есть сертификат, который позволяет обращаться к сайту по HTTPS (боты могут отправлять обновления только по зашифрованным соединениям)
  • быстрая и удобная публикация сайта прямо из Visual Studio

Для начала создаем класс, который будет возвращать нам объект для работы с ботом:

public static class Bot
{
    private static Api _bot;

    /// <summary>
    /// Получаем бота, а если он еще
    /// не инициализирован - инициализируем
    /// и возвращаем
    /// </summary>
    public static Api Get()
    {
        if (_bot != null) return _bot;
        _bot = new Api(Config.BotApiKey);
        _bot.SetWebhook(Config.WebHookUrl);
        return _bot;
    }
}

Настройки достаем из класса Config
Config.cs
public static class Config
{
    /// <summary>
    /// Настройки для бота храним в настройках приложения
    /// </summary>
    private static readonly NameValueCollection Appsettings = ConfigurationManager.AppSettings;

    /// <summary>
    /// Полученный токен для бота
    /// </summary>
    public static string BotApiKey
    {
        get { return Appsettings["BotApiKey"]; }
    }

    /// <summary>
    /// URL, на который должны приходить все обновления от бота
    /// </summary>
    public static string WebHookUrl
    {
        get { return Appsettings["WebHookUrl"]; }
    }
}


Теперь нам нужно формировать и отправлять пакет. За это будет отвечать класс WakeOnLan

/// <summary>
/// Может отправлять "волшебные" пакеты для включения удаленного компьютера
/// </summary>
public static class WakeOnLan
{
    public static void Up(string ip, string mac, int? port = null)
    {
        var client = new UdpClient();
        var data = new byte[102];

        for (var i = 0; i <= 5; i++) // первые шесть байт - нулевые
            data[i] = 0xff;

        var macDigits = GetMacDigits(mac);
        if (macDigits.Length != 6)
            throw new ArgumentException("Incorrect MAC address supplied!");

        const int start = 6;
        for (var i = 0; i < 16; i++) // создаем нужную последовательность байт для пакета
            for (var x = 0; x < 6; x++)
                data[start + i * 6 + x] = (byte)Convert.ToInt32(macDigits[x], 16);

        client.Send(data, data.Length, ip, port ?? 7); // отправляем пакет
    }

    private static string[] GetMacDigits(string mac) // парсим MAC
    {
        return mac.Split(mac.Contains("-") ? '-' : ':');
    }

    public static bool ValidateMac(string mac) // простая проверка на валидность MAC адреса
    {
        return GetMacDigits(mac).Length == 6;
    }
}


Пакеты формируем и умеем отправлять. Осталось научить бота отвечать на команду, к примеру, /wol.
Для простоты реализована команда с параметрами, т.е. пользователь должен будет ввести примерно следующее
/wol 1.2.3.4 01:02:03:04:05:06 7
для того, чтобы отправить пакет на адрес 1.2.3.4, на 7 порт и разбудить компьютер с MAC адресом 01:02:03:04:05:06

public async void Handle(Message message)
{
    var text = message.Text.Split(' ');
    if (text.First() != "/wol") return;
    switch (text.Count())
    {
        case 1:
        case 2:
            await _bot.SendTextMessage(message.Chat.Id, "Пример использования: /wol 1.2.3.4 01:02:03:04:05:06 7");
            break;
        default:
            if (!WakeOnLan.ValidateMac(text[2]))
                await _bot.SendTextMessage(message.Chat.Id, "Неверный MAC адрес");
            else
            {
                try
                {
                    WakeOnLan.Up(text[1], text[2], GetPort(text));
                    await _bot.SendTextMessage(message.Chat.Id, "Пакет отправлен!");
                }
                catch (Exception)
                {
                    await _bot.SendTextMessage(message.Chat.Id, "Произошла ошибка :(");
                }
            }
            break;
    }
}

/// <summary>
/// Получаем порт из параметров
/// </summary>
private static int? GetPort(IReadOnlyList<string> text)
{
    int port;
    if (text.Count == 4 && int.TryParse(text[3], out port))
        return port;
    return null;
}

Отлично, осталось лишь создать контроллер, который будет принимать обновления от бота:
public class MessageController : ApiController
{
    [Route(@"api/message/wol")]
    public OkResult Post([FromBody]Update value)
    {
        Task.Run(() => new Handler().Handle(value.Message));
        return Ok();
    }
}

После «заливки» приложения в Azure проверяем бота:


Ссылки:
WoL бот в Telegram
Проект на GitHub
API ботов Telegram
Библиотека Telegram.Bot (GitHub)

Комментарии (15)


  1. MaximAL
    24.08.2015 11:49
    +2

    Спасибо за статью.

    Вообще, бот-платформа Телеграма — это очень круто.
    Написал несколько ботов для интеграции рабочего процесса команды разработчиков с Телеграмом, всё достаточно удобно.


    1. Spoofi
      24.08.2015 12:19

      Согласен, бот-платформа очень хороша. Позволяет делать многое, причем не затрачивая при этом много времени.

      А какие задачи выполняет Ваш бот для интеграции рабочего процесса, если не секрет? Мы, пока что, лишь пробовали использовать GithubBot и Integram для наших репозиториев.


      1. MaximAL
        24.08.2015 12:21

        Интеграция с Трелло, Меркуриалом (а по сути — с чем угодно): сообщения о задачах и коммитах в рабочий чат, сообщения о новых заказах менеджерам.


    1. Surzhikov
      24.08.2015 16:43
      +1

      Очень согласен, что эта платформа — прямо очень крутая вещь.

      Когда-то люди придумали командную строку и это было круто.
      Ведь Telegram-бот — такая же командная строка с возможностью отправлять фото, видео, аудио, координаты помимо текстовых команд и всё это в смартфоне.

      Кажется что скоро боты для телеграма будут чем-то таким привычным и обыденным, как например веб-сайты!


      1. Spoofi
        24.08.2015 17:48

        будут чем-то таким привычным и обыденным, как например веб-сайты!

        Возможно, не настолько обыденным, но отличным инструментом для выполнения команд и рассылки уведомлений эта платформа уже сейчас является :)


  1. arelay
    24.08.2015 14:01

    Простите за оффтоп:
    Навязчиво сверлит мысль о идиотах, которые угробили icq.
    И база пользователей, и боты… Возьми прикрути номера, возьми усовершенствуй уже имеющуюся бот-платформу… И все бы забыли, что такое whatsapp, все бы тыкали пальцем на телеграмм, который все «копирует».
    Показательный жизненный пример, что на старом велосипеде далеко не уедешь.

    По сабжу:
    Спасибо за интересное применение ботов. Действительно может оказаться полезной вещью.


  1. a553
    24.08.2015 14:07

    Если у вас уже стоит dd-wrt (судя по картинке, так и есть) или другая нормальная прошивка, и у роутера белый адрес, то всё гораздо проще.


    1. Spoofi
      24.08.2015 14:40

      Да, я знаю, что есть такая фича в некоторых пошивках роутеров. Но это не совсем то, что нужно. Использую телефон я могу разбудить компьютер, например, в офисе или дома, где такой возможности не предусмотрено прошивкой (а вот проброс портов сейчас даже на самых простых моделях имеется по умолчанию).

      Для удобства можно доработать бота так, чтобы можно было создавать «алиасы» команд. К примеру /wake1 — включить компьютер дома, а /wake2 — разбудить компьютер на работе.


      1. Dronishe
        24.08.2015 17:00

        Телеграм боты поддерживают кастомные клавиатуры (https://core.telegram.org/bots#keyboards)
        Я бы сделал боту несколько команд:
        /bind name ip mac — чтобы «запомнить» настройки
        /wol для отображения списка запомненных кнопочек для включения нужного компа.


        1. Spoofi
          24.08.2015 17:43

          Ну да, можно делать и так, хорошая идея. Имеется у меня другой, «боевой» бот, — вот на нем что-то подобное и сделаю :)


  1. Rhaps107
    24.08.2015 14:16

    Господа знающие, подскажите, может ли бот видеть сообщения, которые написаны в чате, где бот участник. При этом сообщение написано не лично боту (с указанием ника) и не в качестве ответа на сообщение бота. Т.е. просто сообщения из чата, не личные для бота.


    1. Spoofi
      24.08.2015 14:45

      Может. Посмотрите в документации про privacy mode (отключить этот режим можно для конкретного бота через BotFather)


      1. Rhaps107
        27.08.2015 13:07

        Благодарю. То, что нужно.


    1. MaximAL
      25.08.2015 18:04

      Вкратце: бот по умолчанию видит только сообщения-команды (начинающиеся со слеша), сообщения с его упоминанием, ответы на свои сообщения, сервисные сообщения (кто-то добавлен в чат, удалён из него и т. п.).

      Командой /setprivacy к боту-отцу @BotFather можно изменить это поведение: если отключить режим приватности, бот будет видеть все сообщения чата. Это рекомендуется делать только при реальной необходимости, в большинстве случаев достаточно команд, упоминаний, ответов и принудительных ответов.


      1. Rhaps107
        27.08.2015 13:08

        Благодарю. То, что нужно.