Ведение Telegram-канала с помощью GitHub Actions

от автора

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

Для меня эта идея стала основой многих собственных разработок, начиная с программ для решения Судоку, подсчёта времени нахождения за компьютером, имитации работы пользователя ПК с помощью самописных скриптов (всё это ещё в давние времена), и заканчивая более сложными проектами.

И вот, среди прочих родилась простая идея: "А почему бы не автоматизировать отслеживание новых выпусков ИТ-подкастов с помощью Telegram-бота и GitHub Actions? Чтобы просто подписаться на telegram-канал и получать актуальные выпуски подкастов по мере их выхода.

Конечно, можно скачать специализированные приложения, типа "Poket Casts", либо подписаться на RSS, но лично для меня использование Telegram-канала является самым удобным, простым и привычным.

Так был создан telegram-канал @awesome_russian_podcasts, куда в автоматическом режиме публикуются новые выпуски множества ИТ-подкастов, собранных в моём репозитории. Собственно, о процессе создания этого канала (его техническую часть) я и хочу рассказать далее.

Используемые инструменты

Для решения задачи я использовал следующие инструменты:

  1. GitHub Actions;
  2. .NET Core Console App;
  3. Luandersonn.iTunesPodcastFinder — для получения данных о подкастах из iTunes;
  4. Telegram.Bot — для взаимодействия с API Telegram-бота;
  5. Git.

Пункты 2-4 можно легко заменить, чтобы реализовать описанный сценарий на удобном вам языке программирования. Кроме того, с Telegram API можно взаимодействовать напрямую через HTTP-запросы, как и с API iTunes.

GitHub Actions


Описание функционала GitHub Actions

С появлением GitHub Actions для разработчиков открылось новое поле для исследований и экспериментов, ведь этот инструмент позволяет автоматизировать вещи, которые ранее приходилось делать вручную или с помощью сторонних не всегда простых в освоении и интеграции сервисов.

Подробная документация (только на английском языке) будет понятна даже тем, кто никогда не работал с подобными инструментами. И времени на её изучение понадобится не очень много.

Для решения своей задачи я выбрал GitHub Actions по нескольким причинам:

  • Он бесплатный для публичных репозиториев;
  • Позволяет запускать "события" по таймеру;
  • Его легко изучить и адаптировать под новые задачи.

Идея состоит в том, что по заданному таймеру запускается Action-скрипт, сконфигурированный таким образом, что при изменении числа выпусков для каждого подкаста в репозитории, данные о новых выпусках публикуются в Telegram-канале через Telegram-бота, управляемого консольным .NET Core приложением.

Задание секретов репозитория

Сначала необходимо задать "секреты" (API-ключ Telegram-бота и id Telegram-канала, куда будут публиковаться новые выпуски) в репозитории с GitHub Action, как это указано на скриншоте:

На самом деле, id Telegram-канала вы можете указать непосредственно в Action-скрипте или в приложении, но для решения однотипных задач лучше иметь возможность изменять эти данные.

Они будут использоваться для передачи в качестве параметров командной строки в консольное .NET Core приложение.

Получение API-ключа Telegram-бота

Для взаимодействия с Telegram-ботом необходимо его создать и получить специальный API-ключ. Сделать это очень просто.

В строке поиска Telegram необходимо ввести BotFather — это официальный Telegram-бот для создания своих ботов:

Для создания нового бота нужно в чате BotFather ввести команду /newbot, на что он попросит указать название нового бота:

Вы можете дать любое название вашему боту.

Далее BotFather попросит ввести username для нового бота. По сути, это будет его уникальный идентификатор в Telegram.

Здесь есть небольшие ограничения: username должен заканчиваться на bot (без учёта регистра) и он должен быть уникальным (если такой username уже существует, то вам не удастся создать нового бота):

В итоге ваш бот будет создан, а вам отобразится API-ключ для управления им извне (первая чать API-ключа — это идентификатор бота).

P.S. Если вы забыли API-ключ, его всегда можно посмотреть, введя команду /mybots в чате BotFather, выбрать нужного бота из списка и нажать кнопку "API Token". Там же можно обновить ключ в случае его компрометации.

Далее необходимо найти в строке поиска Telegram созданного бота по его username, перейти в него и нажать кнопку "Запустить". Теперь бот готов к работе:

Получение id Telegram-канала

Чтобы Telegram-бот знал, куда ему отправлять сообщения, необходимо узнать id Telegram-канала для публикаций.

Для это необходимо добавить бота в созданный вами Telegram-канал (бота в канал можно добавить только в качестве администратора), написать какое-нибудь сообщение в этом канале, и вызвать HTTP GET-запрос: https://api.telegram.org/bot<api_key>/getUpdates, где <api_key> — это API-ключ бота, полученный ранее:

В поле id внутри chat получите необходимый id Telegram-канала.

P.S. Так как Telegram, к сожалению, всё ещё заблокирован на территории Российской Федерации, то указанный выше HTTP GET-запрос необходимо отправить через прокси. Например, можно воспользоваться браузером Opera и его встроенным прокси, как это сделал я.

Структура скрипта

Далее перейдём непосредственно к Action-скрипту.

О том, как создавать и редактировать скрипты подробно описано в документации. Разобраться в этом не составит труда. Здесь я лишь приведу свой скрипт с комментариями:

name: Update_Podcasts_Data      # название скрипта, которое будет отображаться во вкладке Actions репозитория  on:                             # действие, по которому запускается скрипт   schedule:                     # в данном случае, это выполнение по таймеру     - cron: '0 5-20/2 * * *'    # 'каждый день каждые 2 часа в часы с 5 по 20 по UTC+0', то есть в 5, 7, 9, 11, 13, 15, 17, 19 по UTC+0  jobs:                           # выполняемые в рамках скрипта работы   build:      runs-on: ubuntu-latest      # запускаем на образе последней версии ubuntu      steps:                      # шаги, выполняемые после запуска образа     - uses: actions/checkout@v2 # переходим в актуальную ветку     - name: Setup .NET Core                                 # имя 1-ой работы       uses: actions/setup-dotnet@v1                         # устанавливаем компоненты, необходимые для запуска .NET приложений       with:                                                 # с параметрами:         dotnet-version: 3.1.101                             # указываем конкретную версию устанавливаемых компонент .NET Core     - name: Set chmod to Unchase.HtmlTagsUpdater            # имя 2-ой работы       run: chmod 777 ./utils/Unchase.HtmlTagsUpdater        # выдаем права, необходимые для запуска и выполнения .NET Core Console App - "Unchase.HtmlTagsUpdater"     - name: Run Unchase.HtmlTagsUpdater (with Telegram Bot) # имя 3-ей работы       env:                                                  # задаём переменные среды для текущей работы         TG_KEY: ${{ secrets.TgKey }}                        # TG_KEY берется из "секрета" репозитория с именем "TgKey" - это API-ключ для управления Telegram-ботом          TG_CHANNEL_ID: ${{ secrets.TgChannelId }}           # TG_CHANNEL_ID берется из "секрета" репозитория с именем "TgChannelId" - это id канала, куда будут публиковаться сообщения от бота        # далее запускается консольное .NET Core приложение с передачей параметров командной строки:       # -f - обрабатываемый файл ("Podcasts.md" - в нем содержится список ИТ-подкастов с указанием количества выпусков на момент предыдущей проверки)       # -t - тип обрабатываемых данных (в моем приложении есть 2 типа: для подкастов - "iTunesPodcast", и для YouTube-каналов)       # -a - API-ключ для Telegram-бота, который берётся из переменной среды TG_KEY       # -c - id канала для публикации из переменной среды TG_CHANNEL_ID       # -i - таймаут запроса Telegram.Bot в секундах       # есть еще несколько дополнительных параметров, которые нет необходимости сейчас рассматривать       run: ./utils/Unchase.HtmlTagsUpdater -f "Podcasts.md" -t "iTunesPodcast" -a "$TG_KEY" -c "$TG_CHANNEL_ID" -i "90"      # следующие работы не относятся к взаимодействию с telegram-каналом, но необходимы для сохранения изменений файла "Podcasts.md" в исходном репозитории     - name: Git set author (email)  # имя 4-ой работы       run: /usr/bin/git config --global user.name "GitHub Action Unchase"   # задаем имя пользователя, от которого будет сделан commit      - name: Git set author (email)  # имя 5-ой работы       run: /usr/bin/git config --global user.email "spiritkola@hotmail.com" # задаем email пользователя, от которого будет сделан commit      - name: Git add                 # имя 6-ой работы       run: /usr/bin/git add Podcasts.md # добавляем (индексируем) изменённый файл для последующего commit                       - name: Git commit              # имя 7-ой работы       run: /usr/bin/git commit -m "Update podcasts data"    # делаем commit      - name: Git push       run: /usr/bin/git push origin master  # делаем push с изменениями в исходный репозиторий

Как можно убедиться, сам Action-скрипт довольно прост. Его можно улучшить, например, добавив возможность не делать commit, если изменений не было. Но пока в этом нет необходимости. Вся полезная работа кроется в консольном .NET Core приложении "Unchase.HtmlTagsUpdater". Давайте посмотрим, что у него внутри.

.NET Core Console App

"Unchase.HtmlTagsUpdater" — это обычное консольное .NET Core приложение, в которое передаются заданные параметры командной строки. Здесь я приведу упрощенный код: без допоплнительных проверок, обработок, промежуточных частей и частей, не относящихся к задаче.

Для разбора параметров командной строки удобно использовать nuget-пакет CommandLineParser. Он позволяет поместить входные параметры приложения в заданный класс:

using System.Collections.Generic; using System.IO; using CommandLine; using Telegram.Bot.Types; using File = System.IO.File;  public enum UtilType {     YouTube,      iTunesPodcast }  public class Options {     [Option('f', "file", Required = true)]     public string InputFile { get; set; }      [Option('t', "type", Required = true)]     public UtilType Type { get; set; }      [Option('a', "tgapi", Required = false)]     public string TelegramBotApiKey { get; set; }      [Option('c', "tgchannel", Required = false)]     public ChatId TelegramChannelId { get; set; }      [Option('i', "tgtimeout", Required = false)]     public int TelegramTimeout { get; set; }      public string ReadAllTextFromInputFile()     {         if (!File.Exists(InputFile))         {             throw new FileNotFoundException("Input file does not exist!", InputFile);         }         return File.ReadAllText(InputFile);     }      public void WriteAllTextToInputFile(string text)     {         if (!File.Exists(InputFile))         {             throw new FileNotFoundException("Input file does not exist!", InputFile);         }         File.WriteAllText(InputFile, text);     } }

В основном методе Main приложения необходимо вызвать Parser.Default.ParseArguments:

internal static Options Options { get; private set; }  static void Main(string[] args) {     Console.WriteLine("Start!");      // разбираем входные параметры командной строки, поместив их в Options     var parseResult = Parser.Default.ParseArguments<Options>(args)         .WithParsed(o =>         {             Options = o;         });      // если разбор параметров не был успешен, то завершаем работу программы     if (Options == null || parseResult.Tag != ParserResultType.Parsed)     {         // сообщение об ошибке будет выведено в консоли GitHub Action         Console.WriteLine("Error: Options was not parsed!");         return;     }      //...      // считываем текстовые данные из входного файла     var text = Options.ReadAllTextFromInputFile();      switch (Options.Type)     {         // ...         case UtilType.iTunesPodcast:             // обрабатываем данные iTunes-подкастов             text = ProcessPodcasts(text);             break;     }      // записываем изменённые текстовые данные в выходной файл     Options.WriteAllTextToInputFile(text);      Console.WriteLine("Done!"); }

Дальнейшаая обработка происходит в методах ProcessPodcasts и SendPodcastData:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using iTunesPodcastFinder; using iTunesPodcastFinder.Models; using Telegram.Bot; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups;  // клиент для работы с API Telegram-бота private static ITelegramBotClient _telegramBotClient; internal static ITelegramBotClient TelegramBotClient {     get     {         if (_telegramBotClient != null)         {             return _telegramBotClient;         }          if (!string.IsNullOrWhiteSpace(Options.TelegramBotApiKey) && !string.IsNullOrWhiteSpace(Options.TelegramChannelId))         {             // создаём клиента для обращения к API Telegram-бота             // если соединения с ботом нет, то, скорей всего необходимо передать в TelegramBotClient в качестве второго параметра какой-нибудь прокси             // например, такой прокси можно задать с помощью nuget-пакета 'HttpToSocks5Proxy'             // для работы из GitHub Actions прокси, к счастью, не требуется             _telegramBotClient = new TelegramBotClient(Options.TelegramBotApiKey) { Timeout = new TimeSpan(0, 0, Options.TelegramTimeout) };         }          return _telegramBotClient;     } }  private static string ProcessPodcasts(string text) {     // получаем все span'ы из входного файла, в которых хранится количество эпизодов для каждого подкаста     // сам метод возвращает коллекцию строк вида: '<span itunes-id="1120110650" class="episodes" hashtag="Ивент_Кухня">35 (<font color="red">0</font>)</span>'     foreach (var span in GetSpans(text, "episodes"))     {         // метод возвращает значение id подкаста в iTunes. Например, '1120110650'         var iTunesPodcastId = GetAttributeValue(span, "itunes-id");         if (string.IsNullOrWhiteSpace(iTunesPodcastId))             continue;          try         {             // получаем данные о подкасте по его id в iTunes             Podcast podcast = PodcastFinder.GetPodcastAsync(iTunesPodcastId).GetAwaiter().GetResult();             if (podcast == null)                 continue;              // получаем новое значение (с актуальным количеством эпизодов подкаста) для span'а             var newValue = podcast.EpisodesCount.ToString();              // отправляем данные о новых эпизодах в Telegram-канал             SendPodcastData(podcast, span, newValue);              // ...              // заменяем количество эпизодов подкаста на новое             // измененные данные будут записаны во входной файл             text = text.Replace(span, SetSpanValue(span, newValue));         }         catch (Exception ex)         {             Console.WriteLine($"Error: {ex.Message}{Environment.NewLine}Podcast id = {iTunesPodcastId}");         }     }      // возвращаем текстовые данные о подкастах с изменёнными новыми значениями     return text; }  // ...  private static void SendPodcastData(Podcast podcast, string span, string newValue) {     // получаем текущее значение количества эпизодов подкаста     var currentSpanValue = GetSpanValue(span);     if (long.TryParse(currentSpanValue, out var currentSpanLongValue) && long.TryParse(newValue, out var newSpanLongValue))     {         var diff = newSpanLongValue - currentSpanLongValue;          // если количество эпизодов выросло...         if (diff > 0)         {             try             {                 // получаем список эпизодов подкаста                 var episodes = PodcastFinder.GetPodcastEpisodesAsync(podcast.FeedUrl).GetAwaiter().GetResult()                     ?.Episodes?.OrderByDescending(e => e.PublishedDate)?.ToList();                  if (episodes?.Any() == true && episodes.Count >= diff)                 {                     for (int i = (int)diff - 1; i >= 0; i--)                     {                         // формируем текст сообщения, публикуемого в Telegram-канале                         var message = new StringBuilder();                          // ...                              message.AppendLine("@awesome\\_russian\\_podcasts");                          if (!string.IsNullOrWhiteSpace(Options.TelegramBotApiKey) && TelegramBotClient != null)                         {                             // отправляем сообщение в Telegram-канал через Telegram-бота                             TelegramBotClient.SendPhotoAsync(Options.TelegramChannelId,    // id Telegram-канала                                 podcast.ArtWork,    // изображение подкаста в iTunes                                 $"{message.ToString()}",    // текст (данные о подкасте)                                 ParseMode.Markdown, // указываем, что текст передаётся в Markdown                                 true,   // отправлять push-уведомление о новом сообщении                                 // добавляем кнопки под сообщением                                 replyMarkup: new InlineKeyboardMarkup(new List<InlineKeyboardButton> {                                     InlineKeyboardButton.WithUrl(                                         "iTunes",                                         podcast.ItunesLink),                                     InlineKeyboardButton.WithUrl(                                         "Episode",                                         episodes[i].FileUrl.ToString()),                                     InlineKeyboardButton.WithUrl(                                         "Feed URL",                                         podcast.FeedUrl)                                 })).GetAwaiter().GetResult();                              // ...                         }                     }                 }             }             catch (Exception e)             {                 var errorMessage = new StringBuilder();                 // формируем информативный текст ошибки для вывода в консоль                 // ...                  Console.WriteLine(errorMessage.ToString());             }         }     } }

Это всё, что минимально необходимо выполнить для решения поставленной задачи.

Вывод

GitHub Actions можно использовать как для работы с Telegram-каналами: публикация сообщений, модерация, интерактивное взаимодействие с участниками канала… Так и для множества других задач: CI/CD для ваших проектов, обновление статических страниц для GitHub Pages, взаимодействие с любыми сторонними сервисами по описанному сценарию и т.д.

Применений этому действительно полезному и удобному инструменту можно найти много. Я лишь постарался описать один из возможных вариантов.

Я убеждён, что каналы должны приносить пользу не только его создателям, поэтому если вам нравится слушать ИТ-подкасты, или вы в поисках новых знаний, — присоединяйтесь к каналу @awesome_russian_podcasts и добавляйте свои любимые русскоязычные ИТ-подкасты в репозиторий, чтобы и о них могли услышать другие люди.

Если же вы адепт youtub’а и просмотра видео, то и для вас есть аналогичный канал — @awesome_russian_youtube.

Спасибо, что дошли до конца. Будьте здоровы и не сходите с пути познания. Удачи!

ссылка на оригинал статьи https://habr.com/ru/post/499180/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *