
Наверное каждому разработчику хотя бы раз в жизни приходила идея что-нибудь автоматизировать. Ведь если есть возможность избавиться от рутины, то грех ей не воспользоваться.
Для меня эта идея стала основой многих собственных разработок, начиная с программ для решения Судоку, подсчёта времени нахождения за компьютером, имитации работы пользователя ПК с помощью самописных скриптов (всё это ещё в давние времена), и заканчивая более сложными проектами.
И вот, среди прочих родилас�� простая идея: "А почему бы не автоматизировать отслеживание новых выпусков ИТ-подкастов с помощью Telegram-бота и GitHub Actions? Чтобы просто подписаться на telegram-канал и получать актуальные выпуски подкастов по мере их выхода.
Конечно, можно скачать специализированные приложения, типа "Poket Casts", либо подписаться на RSS, но лично для меня использование Telegram-канала является самым удобным, простым и привычным.
Так был создан telegram-канал @awesome_russian_podcasts, куда в автоматическом режиме публикуются новые выпуски множества ИТ-подкастов, собранных в моём репозитории. Собственно, о процессе создания этого канала (его техническую часть) я и хочу рассказать далее.
Используемые инструменты
Для решения задачи я использовал следующие инструменты:
- GitHub Actions;
- .NET Core Console App;
- Luandersonn.iTunesPodcastFinder — для получения данных о подкастах из iTunes;
- Telegram.Bot — для взаимодействия с API Telegram-бота;
- 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.
Спасибо, что дошли до конца. Будьте здоровы и не сходите с пути познания. Удачи!
