Как автоматизировать работу продюсера с помощью JavaScript и Telegram-бота
Создаём веб-приложение для быстрого заполнения расписания новостной передачи.


Иллюстрация: Катя Павловская для Skillbox Media
Готовить эфиры бывает утомительно и непросто, поэтому, помимо ведущих, за кадром должна быть сильная команда помощников. И неважно, идёт ли речь о передаче на телевидении или на YouTube, Rutube, в VK и так далее: главные герои тыла — продюсеры, которые ищут экспертов, договариваются с гостями, предлагают темы для обсуждения, согласовывают всё это и составляют для всех расписания.
Миллиарды нервных клеток продюсеров стримов сгорели в попытках находить интересных героев, делать выпуски непохожими друг на друга, возвращать на связь внезапно исчезнувших с радаров гостей и, конечно, вписываться в график. В общем, продюсер эфира — этакий рокетмен, сжигающий свои предохранители. Но как тут может помочь программирование? Давайте попробуем разобраться.
Что здесь автоматизировать
Опыт участия в подготовке новостных эфиров, а также обсуждения темы с друзьями из других медиа заставили задуматься: а вдруг в работе продюсера, помимо творчества, есть шаблонные действия? Ведь, если это правда, часть задач можно поручить компьютеру! А это снижение нагрузки, освобождение части рабочего времени и прочее.
Предположим, что некий алгоритм действительно существует, хотя и различается в разных редакциях. Вот что может учитывать условный продюсер стрима на условную политическую тематику.
Дата эфира
При ежедневных выходах выбор как минимум между сегодня и завтра. Повестка быстро меняется, и планировать дальше может быть сложно — хотя когда как.
Время эфира
Допустим, утро или вечер — для разной ЦА. Кто-то внимательно посмотрит прямую трансляцию, кто-то хочет послушать эфир за рулём, кто-то на кухне за готовкой, некоторые подписчики включат его фоном во время работы, а остальные посмотрят запись на досуге.
Ведущие
Обычно это один или несколько человек из списка постоянных ведущих. Кто-то из них может быть в этот день недоступен по причине занятости, командировки, отпуска, болезни.
Темы, гости и время подключения
Это самый интересный и творческий пункт. Задача в том, чтобы определить самые актуальные темы и найти гостей (экспертов), которые впишутся в передачу и будут согласны ненадолго выйти в эфир с комментарием.
Но это ещё не всё. Хороших экспертов нужно взять на заметку и приглашать снова, а плохих — отсеять.
Согласование
Когда расписание в том или ином виде составлено, его нужно согласовать с ведущими. С точки зрения подачи это означает, что текст должен быть ясным, кратким и выглядеть аккуратно, потому что ведущие — занятые люди и у них нет времени читать полотна текста.
Таким образом, есть несколько плавающих переменных, которые способны измениться в очень короткий срок. Но всё же это одни и те же переменные, и сбор информации действительно можно немножечко автоматизировать. Это мы и сделаем.
Алгоритм автоматизации
Наша программа будет простой и наглядной. Мы создадим локальную веб-страницу с самым необходимым:
- приятным интерфейсом;
- возможностью выбора опций эфира;
- небольшой базой данных экспертов с возможностью добавления новых;
- автоматическим заполнением текста расписания;
- отправкой оформленного поста в рабочий Telegram-чат команды стрима.
В результате продюсер эфира сможет открыть нашу страницу, выбрать нужные детали и гостей (при необходимости — завести карточки новых гостей), составить расписание, почти ничего не печатая, и в один клик переслать структурированный план на согласование ведущим. Для многих этого будет вполне достаточно, хотя при желании можно запросто накрутить дополнительные возможности.
Какие технологии будем использовать:
- HTML и CSS для красивого дизайна;
- язык JavaScript для программирования интерфейса;
- API IndexedDB для создания локальной базы данных прямо в браузере (современными браузерами оно поддерживается);
- Telegram Bot API для отправки расписания.
Готовый код мы разместили на pastebin.com:
Как будет выглядеть наше приложение
Допустим, мы сделаем приложение для продюсеров условной YouTube-передачи Skillbox FM с реальными ведущими и гостями (по мотивам уже вышедших эпизодов подкаста «Люди и код»).

Скриншот: Skillbox Media
Приложение будет следовать логике продюсера новостного эфира: определять дату, время и ведущих. А после этого сопоставлять экспертов и время выхода. В конце приложение покажет расписание всем ведущим.
Взглянем на всё это как программисты:
- по сути, нам надо сделать набор чекбоксов, которые администратор проставляет в нужном порядке;
- выбор времени подключения привязан к блоку «Вид эфира» и устанавливается, когда отмечаем утро или вечер;
- для добавления экспертов в расписание к конкретному времени нужно по очереди щёлкнуть на время подключения, а затем на имя гостя — и они встанут на свои места;
- все опции сконцентрированы вокруг требуемого результата (текста поста). Поэтому блок «Предпросмотр поста» должен располагаться по центру.
Вот как отреагирует программа, если мы выберем случайные опции.

Скриншот: Skillbox Media

Скриншот: Skillbox Media
Нажав на голубую кнопку вверху, мы отправим в Telegram-чат вот такой пост.

Скриншот: Skillbox Media

Скриншот: Skillbox Media
Теперь разберём, как написать такую программу.
Реализация
Код будет упакован в два файла: Air Constructor.html и speakersDB.js. Первый — сама страница (HTML, CSS и немного JavaScript). Второй — всё, что связано с базой данных экспертов (JavaScript-код, который мы подключим к веб-странице).
Посмотрим на применение указанных инструментов.
Вёрстка интерфейса — HTML и CSS
Наш интерфейс должен быть не только приятным, но и привычным для продюсера, поэтому его нужно оформить в фирменном стиле медиа. В нашем случае ведущий цвет — синий (код #3D3BFF).
С вёрсткой мудрить не будем:
- элементарная сетка из трёх <div>-блоков для разделения страницы на три вертикальных уровня (<div id="firstBlock">, <div id="secondBlock">, <div id="thirdBlock">);
- первый уровень (см. скриншот) — заголовок «Расписание эфира» и кнопка отправки на синем фоне;
- второй уровень — блоки «Дата эфира», «Вид эфира», «Ведущие»;
- третий уровень — блоки «Время подключения», «Предпросмотр поста», «Спикеры»;
- CSS-правил будет немного, поэтому разместим их не в отдельном файле, а внутри страницы с помощью элемента <style>.
С опциями интерфейса в целом всё тоже просто: чтобы дать пользователю возможность выбора пунктов, вставим в наши <div>-блоки шесть HTML-форм (элементы <form>), внутри которых будут связанные элементы <input>/<label>, <fieldset>/<legend> или <textarea>.
Взаимодействие форм с инпутами и подписями поначалу может показаться запутанным, поэтому разберём его подробнее:
- форма (элемент <form>) — это секция документа, содержащая интерактивные элементы для отправки информации. Содержит элементы формы (см. ниже);
- инпут (элемент <input>) — это как раз элемент управления одного из нескольких возможных типов. Мы будем использовать инпуты типа radio (выбор только одного варианта) и checkbox (выбор любого количества вариантов);
- элемент <label> — это подпись для связанного инпута. Связь указывается с помощью атрибутов id и for;
- элемент <fieldset> группирует несколько управляющих элементов формы, а связанный с ним <legend> буквально добавляет над ними «легенду» (красивый заголовок).
Мы нарушим эту схему только в двух случаях: для кнопки отправки в Telegram укажем тип submit («отправка», без подписи с помощью <label>), а форма предпросмотра поста будет содержать только текстовый блок <textarea> для расписания.
Давайте теперь настроим отображение этих элементов. Для начала сбросим дефолтные стили браузера, чтобы самостоятельно задать отступы и шрифт.
* {
font-family: Intro Light, sans-serif;
margin: 0;
padding: 0;
}
Далее сделаем ширину трёх главных блоков сетки равной ширине страницы (заодно установим и другие опции).
#firstBlock {
width: 100%;
position: relative;
}
#secondBlock {
width: 100%;
margin-left: 5%;
}
#thirdBlock {
width: 100%;
margin-left: 5%;
clear: both;
}
Позаботимся о стиле заголовка страницы:
h1 {
background-color: #3D3BFF;
color: #fff;
width: 100%;
font-family: Intro Black, sans-serif;
text-align: center;
padding: 5px;
}
И стиле кнопки отправки в Telegram:
#sendButton {
width: 170px;
height: 30px;
position: absolute;
top: 10%;
right: 4%;
background-color: #179cde;
border: 1px dashed white;
color: #fff;
font-family: Intro Black, sans-serif;
cursor: pointer;
}
А этот стиль поможет правильно выстроить блоки меню относительно друг друга — чтобы они не съезжали со строк и не наезжали друг на друга. Первые три блока — одинаковые.
#first_form, #second_form, #third_form {
width: 30%;
margin-top: 1%;
margin-bottom: 1%;
margin-right: 5px;
float: left;
}
Формы 4–6 должны быть разными по ширине: больше места под расписание и базу гостей и меньше — для времени подключения. Обратите внимание, что в коде фактически три разных варианта четвёртой формы (выбор времени подключения) — просто отображаться должен только один (об этом ниже).
#fourth_form_filler, #fourth_form_morning, #fourth_form_evening {
width: 20%;
margin-bottom: 1%;
margin-right: 5px;
float: left;
}
#fifth_form, #sixth_form {
width: 35%;
margin-bottom: 1%;
margin-right: 5px;
float: left;
}
Отдельные настройки области для текста (элемент <textarea> в пятой форме). В частности, убираем возможность менять её размер (resize: none) и добавляем возможность прокрутки на случай, если текста будет много (ну мало ли).
#fifth_form > fieldset {
overflow: scroll;
}
textarea {
width: 95%;
height: 92%;
padding: 2%;
font-size: 18px;
resize: none;
border: 0;
}
Оставшиеся CSS-правила не так важны — и вы сможете увидеть их в финальном варианте. А мы пойдём к самому интересному — программированию поведения элементов.
Пока ещё не касаясь базы данных, отметим менее очевидные задачи: необходимо сбросить дефолтный выбор пунктов меню и вовремя добавить функции — обработчики событий на клики по различным пунктам.
Этот блок мы добавим ближе к началу кода HTML-страницы — в элемент <head>:
<script>
// Функция отправки расписания в Telegram-чат (привязывается к кнопке отправки).
function sendSchedule() {
// Конструктор ссылки для отправки новостей в Telegram: токен бота, ID чата, способ кодировки, заголовок + текст дайджеста, предупреждение.
let token = '12345abcd';
let chat = '-10012345';
let text = encodeURIComponent(document.getElementsByTagName('textarea')[0].value);
let sendURL = 'https://api.telegram.org/bot' + token + '/sendMessage?chat_id=' + chat + '&parse_mode=HTML&text=' + text;
fetch(sendURL);
alert('Расписание отправлено.');
console.log('Отправка расписания в Telegram-чат.');
};
// Функция отмены предварительного выбора пунктов в меню.
function uncheckInputs() {
var inputs = document.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
inputs[i].checked = false;
};
};
</script>
Остановимся ненадолго на реализации отправки поста в Telegram (функция sendSchedule). Нужно всего лишь сделать GET-запрос методом sendMessage() из Telegram Bot API с помощью JavaScript-метода fetch(). Требуется только подставить в нужные переменные токен вашего бота (его можно получить при создании бота у BotFather) и ID чата.
Далее конструктор собирает из переменных ссылку для запроса, а чтобы функция срабатывала по клику на кнопку отправки, мы добавляем кнопке обработчик с названием функции:
<input type="submit" value="Отправить расписание" id="sendButton" onclick="sendSchedule()">
Ещё один скрипт — после отрисовки первой формы «Дата эфира», для которой требуется получить сегодняшнюю и завтрашнюю даты.
<script>
// Определяем и вставляем актуальные даты (сегодня и завтра).
let now = new Date();
let now2 = new Date();
now2.setDate(now2.getDate() + 1);
let month = [
'января',
'февраля',
'марта',
'апреля',
'мая',
'июня',
'июля',
'августа',
'сентября',
'октября',
'ноября',
'декабря'
];
let today = now.getDate().toString() + ' ' + month[now.getMonth()];
let tomorrow = now2.getDate().toString() + ' ' + month[now2.getMonth()];
document.getElementById('replace1').innerHTML = 'Сегодня, ' + today;
document.getElementById('replace2').innerHTML = 'Завтра, ' + tomorrow;
</script>
В самый конец тела страницы вставляем вызовы функции отмены предварительного выбора пунктов меню uncheckInputs() и функций — обработчиков кликов по пунктам меню. Приведём часть этого блока — остальное будет по тому же принципу.
// Если выбрано время "утро".
function checkMorning() {
if (document.getElementById('morning').checked = true) {
document.getElementById('fourth_form_filler').setAttribute('style', 'display: none;');
document.getElementById('fourth_form_evening').setAttribute('style', 'display: none;');
document.getElementById('fourth_form_morning').setAttribute('style', 'display: inherit;');
document.getElementsByTagName('textarea')[0].value += 'УТРО' + '\n\n' + 'Ведущие: ';
};
};
// Если выбрано время "вечер".
function checkEvening() {
if (document.getElementById('evening').checked = true) {
document.getElementById('fourth_form_filler').setAttribute('style', 'display: none;');
document.getElementById('fourth_form_morning').setAttribute('style', 'display: none;');
document.getElementById('fourth_form_evening').setAttribute('style', 'display: inherit;');
document.getElementsByTagName('textarea')[0].value += 'ВЕЧЕР' + '\n\n' + 'Ведущие: ';
};
};
document.getElementById('morning').addEventListener('click', checkMorning);
document.getElementById('evening').addEventListener('click', checkEvening);
Теперь самое сложное — прикрутить базу данных на IndexedDB. Грубо говоря, мы создадим в браузере пользователя локальное хранилище.
Чтобы было удобнее, реализуем эту фичу отдельным модулем — и для начала в самый низ <body> вставляем обращение к файлу speakersDB.js:
<script src="speakersDB.js"></script>
Далее работаем в этом файле.
Вкратце алгоритм работы хранилища на IndexedDB выглядит следующим образом: открыть базу, создать или открыть хранилище объектов (object store) с ключом, совершать с ним транзакции.
Открываем базу данных
const dbName = 'База данных гостей эфира'; // Название базы данных.
let openRequest = indexedDB.open(dbName, 1); // Открытие базы данных.
console.log('Открытие базы данных...');
Где «1» — это версия базы.
У попытки открытия может быть три возможных результата: либо базы ещё нет (и её нужно создать), либо ошибка, либо успех. Отсюда — три разных обработчика.
Если хранилище объектов ещё не создано:
openRequest.onupgradeneeded = function() {
let db = openRequest.result;
if (!db.objectStoreNames.contains('Гости эфира')) { // Если хранилища 'Гости эфира' не существует...
db.createObjectStore('Гости эфира', {keyPath: 'Name'}); // ...создаём хранилище.
};
};
Если у нас ошибка:
openRequest.onerror = function() {
console.error("Ошибка открытия базы данных.", openRequest.error);
};
Самая длинная часть — если всё идёт по плану. База данных открыта — сначала ещё немного формальностей.
openRequest.onsuccess = function() {
let db = openRequest.result;
console.log('База данных успешно открыта.');
// Защита от повторного открытия вкладки.
db.onversionchange = function() {
db.close();
alert("База данных устарела, пожалуйста, перезагрузите страницу.")
};
Дальше нужно записать в базу гостей «по умолчанию», сделать возможность ручного добавления и вывести данные из базы на страницу в блок «Спикеры» (шестая форма).
Начнём с автоматического добавления стартового списка.
// Запись в базу данных стартовых значений.
// Объявление массива с гостями по умолчанию.
let initialGuests = [
{
Name: 'Никита Дубко',
Post: 'Senior Frontend Developer, Google Developer Expert по Web',
Telegram: '@dev_tip'
},
{
Name: 'Светлана Вронская',
Post: 'Эксперт департамента аналитических решений ГК «КОРУС Консалтинг»',
Telegram: '@analyticsnow'
},
{
Name: 'Евгений Некрасов',
Post: 'DevOps-инженер кластеров и нейронных сетей',
Telegram: '@ravino_doul_channel'
},
{
Name: 'Роман Душкин',
Post: 'Автор и ведущий просветительского YouTube-канала «Душкин объяснит»',
Telegram: '@drv_official'
}
];
// Добавление гостей по умолчанию в базу, если их ещё там нет.
for (let i = 0; i < initialGuests.length; i++) {
let transactionWrite = db.transaction('Гости эфира', 'readwrite'); // Создание транзакции.
let guests = transactionWrite.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним.
let addInitialGuest = guests.add(initialGuests[i]); // Добавление записи гостя в хранилище объектов.
addInitialGuest.onsuccess = function() {
console.log('Добавление в базу записи гостя по умолчанию: ' + initialGuests[i].Name);
};
addInitialGuest.onerror = function() {
console.log('В базе найдены записи по умолчанию.');
};
};
Теперь позаботимся о возможности вручную добавить гостя в базу, чтобы продюсер мог ввести ФИО, место работы и Telegram-канал и одним кликом добавить данные в хранилище.
Для этого понадобятся три формы ввода данных, кнопка добавления и привязанная к ней функция сбора введённых значений. Всё перечисленное нужно предварительно создать в памяти, а потом вставить в нужное место страницы.
// Вывод формы добавления в базу нового гостя.
// Объявление функции добавления нового гостя нажатием на кнопку.
function addingGuest() {
let transactionWrite2 = db.transaction('Гости эфира', 'readwrite'); // Создание транзакции.
let guests2 = transactionWrite2.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним.
let getUserInputName = document.getElementById('newGuestName').value; // Получение введённого пользователем имени нового гостя.
let getUserInputPost = document.getElementById('newGuestPost').value; // Получение введённых пользователем должности и места работы нового гостя.
let getUserInputTelegram = document.getElementById('newGuestTelegram').value; // Получение введённой пользователем ссылки на Telegram-канал нового гостя.
let newGuestFromUser = {
Name: getUserInputName,
Post: getUserInputPost,
Telegram: getUserInputTelegram
};
// Добавление записи нового гостя в хранилище объектов.
let addNewGuest = guests2.add(newGuestFromUser);
addNewGuest.onsuccess = function() {
console.log('Добавление в базу нового гостя: ' + newGuestFromUser.Name);
};
addNewGuest.onerror = function() {
console.log('Ошибка добавления в базу нового гостя.');
};
};
let newGuestNameInput = '<input type="text" id="newGuestName" name="newName" placeholder="Имя гостя" required minlength="5" size="30">';
let newGuestPostInput = '<input type="text" id="newGuestPost" name="newName" placeholder="Место работы/должность гостя" required minlength="5" size="30">';
let newGuestTelegramInput = '<input type="text" id="newGuestTelegram" name="newName" placeholder="Telegram-канал гостя (если есть)" minlength="5" size="30"><br>';
let newGuestNameLabel = '<label for="newGuestName">Добавить гостя в базу:</label><br>';
let newGuestButton = document.createElement('input');
newGuestButton.setAttribute('type', 'button');
newGuestButton.setAttribute('value', 'Добавить');
newGuestButton.addEventListener('click', addingGuest);
let path2 = document.getElementById('sixth_form').getElementsByTagName('fieldset')[0];
path2.insertAdjacentHTML('beforeend', newGuestNameLabel);
path2.insertAdjacentHTML('beforeend', newGuestNameInput);
path2.insertAdjacentHTML('beforeend', newGuestPostInput);
path2.insertAdjacentHTML('beforeend', newGuestTelegramInput);
path2.append(newGuestButton);
path2.insertAdjacentHTML('beforeend', '<br><br>Гости в базе:<br>');
Наконец, уже существующих в базе спикеров нужно вывести на страницу и предложить для выбора. Это другая транзакция, и делается она следующим образом.
// Вывод на страницу гостей из базы данных.
let transactionRead = db.transaction('Гости эфира', 'readonly'); // Создание транзакции.
let existingGuests = transactionRead.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним.
let readRequest = existingGuests.getAll();
readRequest.onsuccess = function(e) {
console.log('ЧТЕНИЕ БАЗЫ ДАННЫХ');
console.log('В базе найдено ' + readRequest.result.length + ' гостей.');
// Перебор гостей в базе для уведомления в консоль и вывода на страницу.
for (let eg = 0; eg < readRequest.result.length; eg++) {
let printName = readRequest.result[eg].Name; // Получение ФИО.
let printPost = readRequest.result[eg].Post; // Получение должности и места работы.
let printTelegram = readRequest.result[eg].Telegram; // Получение ссылки на Telegram-канал.
// Финальная строка с данными гостя (для вывода пользователю).
let printGuest = printName + ', ' + printPost + ', ' + printTelegram;
// Уведомление в консоль о найденном в базе пользователе.
console.log('В базе найден гость ' + printGuest);
// Вставка списка на страницу.
let guestInput = document.createElement('input');
guestInput.type = 'checkbox';
guestInput.id = 'guest' + eg;
guestInput.name = 'air_guest';
let guestLabel = document.createElement('label');
guestLabel.setAttribute('for', ['guest' + eg]);
guestLabel.innerHTML = printGuest;
// Добавление переноса строки после каждого гостя.
let newLine = document.createElement('br');
let path = document.getElementById('sixth_form').getElementsByTagName('fieldset')[0];
path.append(guestInput);
path.append(guestLabel);
guestLabel.after(newLine);
// Вставка функции выбора гостей к каждому пункту с именами гостей.
for (g = 0; g < document.getElementsByName('air_guest').length; g++) {
document.getElementsByName('air_guest')[g].addEventListener('click', checkGuest);
};
};
};
};
На этом реализация закончена.
Убедиться в правильности работы хранилища можно двумя способами.
Во-первых, для этой цели у нас предусмотрены консольные уведомления о работе локального хранилища.

Скриншот: Skillbox Media
Во-вторых, на него можно взглянуть в браузерных инструментах разработчика на вкладке Приложение → Хранилище → IndexedDB (в Google Chrome) или Хранилище → IndexedDB (в Mozilla Firefox).

Скриншот: Skillbox Media

Скриншот: личный архив Евгения Колесникова
Заключение
Наш эксперимент увенчался успехом, но это лишь первая бета-версия с самыми важными функциями. И к ней всегда можно добавить что-то ещё. Однако важнее другое: мы не использовали никаких фреймворков и сложных «продвинутых» языков. То есть для такого приложения достаточно относительно простых и известных каждому веб-разработчику инструментов (если не считать IndexedDB). А экономия времени получается колоссальная.