Pull to refresh

Wake on Lan бот для Telegram

Instant Messaging *C# *
Sandbox
После открытия 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)
Tags:
Hubs:
Total votes 15: ↑13 and ↓2 +11
Views 28K
Comments Comments 16