Интро

Некоторое время назад я столкнулась с проблемой ведения двух календарей: личного и рабочего. Точнее само ведение (планирование встреч, переносы, редактирование и т.п.) проблемы не вызывало, но я осознала, что в ежедневной суете у меня совершенно нет возможности / желания несколько раз залезать в календарь и проверять предстоящие встречи.

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

Но все равно я периодически пропускала уведомления о начале какого-либо события.

В общем, я пришла к тому, что мне лично очень удобно все делать через телеграм и было бы неплохо иметь бота, который присылал бы мне сообщение со ссылкой на встречу за 5-10 минут. Для меня лично это стало лучшим решением, которым я пользуюсь несколько месяцев и до сих пор не вижу дроубеков вообще.

Таким образом, я продолжаю пользоваться календарем: в личный календарь я вношу конфы и митинги по личным интересам, плюс онлайн-уроки; в рабочем планы строят обычно коллеги, что позволяет мне вообще его не открывать. Телеграм бот в свою очередь присылает полный план встреч на день и каждую отдельную встречу непосредственно перед началом из обоих календарей.

Реализация

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

Далее выбираем Подписаться на календарь и вводим почту, на кого подписываемся. Я не буду подробно здесь останавливаться, возможно нужны дополнительные пермишены и подтверждения, но я уверена, что вы справитесь.

Как только видимость из обоих календарей настроена можно переходить к написанию кода. Моменты с созданием бота и проектом в App Script уже были обговорены здесь, поэтому сразу перейду к ключевым функциям.

 Получение всех событий из календаря

В App Script есть класс CalendarApp, в котором мы используем метод getCalendarById(calendar_id), calendar_id - это ваш gmail login.

Таким образом я сохраняю оба календаря в соответствующие переменные:

const calendar_ct = CalendarApp.getCalendarById(gmail_login_1)
const calendar_ns = CalendarApp.getCalendarById(gmail_login_2)

Далее отфильтрую все события по дате, я хочу получить планы только на сегодня и использую метод getEventsForDay(date). Также записываю их в переменные

const now = new Date();
const events_ct = calendar_ct.getEventsForDay(now);
const events_ns = calendar_ns.getEventsForDay(now);

Полученные массивы просто объединяем в один методом concat(). При обращении к элементу массива мы увидим в консоли CalendarEvent. Чтобы вычленить подробности из CalendarEvent, нужно обратиться к каждому элементу и забрать нужную инфу, например, название, время начала, описание.

Для данного набора запрашиваемой информации мне нужно использовать методы getDescription(), getTitle(), getStartTime().

Помимо получения деталей события я их преобразую в нужный мне вид. Например, getStartTime() возвращает мне дату целиком и возвращаемое время не соответствует моему часовому поясу. Учитывая, что я хочу получить только время вида чч:мм, я преобразую время следующим образом

start_time = new Date(start_time.getTime() + (6 * 60 * 60 * 1000))
//6 - разница во времени между NY и Berlin

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

То есть описание из календаря как на картинке слева, в консоли может выглядеть как: <html-blob>Professor&nbsp;<b>Sabine Lüder</b><br><a href="https://tu-ilmenau.webex.com/tu-ilmenau/j.php?MTID=m4372a53e03a381435" referrerpolicy="origin" id="ow792" __is_owner="true">Link</a><br><br><br><dl><dt><b>Gastgeber-Kennnummer:&nbsp;</b>348984</dt></dl><br><u></u><br><u></u></html-blob>

И проблема заключается в отправке в телеграм. Чтобы все обработалось гладко и сообщение выглядело примерно следующим образом

я применила замену (я просто остановилась на решении, которое работает, не вдаваясь в детали) :

const regExp = /<(?!\/?a>|\/?a href|\/?br)[^>]+>/g;
descr = descr.replace(regExp,'').replace(/<br>/g,`\n`).replace(/&nbsp;/g,' ');

Все выбранные и преобразованные данные я вставила в новый массив для дальнейшей работы уже с ним. Вся функция целиком ниже.

/**
 * The function gets all events for today from 2 calendars and sorts them
 * 
 * @return {array} events_details_arr all sorted events for today
 */
function getEvents() {
  const calendar_ct = CalendarApp.getCalendarById(gmail_login_1)
  const calendar_ns = CalendarApp.getCalendarById(gmail_login_2)
  const now = new Date();
  const events_ct = calendar_ct.getEventsForDay(now);
  const events_ns = calendar_ns.getEventsForDay(now);
  const events = events_ct.concat(events_ns);
  const regExp = /<(?!\/?a>|\/?a href|\/?br)[^>]+>/g;
  const events_details_arr = new Array();
 
  events.forEach((el) => {
    let descr = el.getDescription().toString();
    const title = el.getTitle();
    let start_time = el.getStartTime();
    start_time = new Date(start_time.getTime() + (6 * 60 * 60 * 1000)) //6 - разница во времени между NY и Berlin
    if (descr === '') {
      events_details_arr.push([title,start_time]);
    } else {
      descr = descr.replace(regExp,'').replace(/<br>/g,`\n`).replace(/&nbsp;/g,' ');
      events_details_arr.push([title,start_time,`\n${descr}`]);
    }
  })
 
  bubble_sort(events_details_arr)
 
  return events_details_arr
}

Да, здесь же я сортирую события по возрастанию времени их старта.

Отдельно я написала функцию, которая преобразует время в удобоваримый вид

/**
 * The function transforms the date into a string
 * 
 * @param {date} date The date to be transformed
 * @return {array} time_str the time in the string form
 */
function time_to_string(date) {
  let h = new String(date.getHours());
  let m = new String(date.getMinutes());
  if (m.length == 1) {
    m = `0${m}`
  }
  const time_str = ` ${h}:${m}`;
 
  return time_str
}

Отправка в телеграм

Наконец, функции отправки в чат с ботом. Я разделила отправку на 2 отдельные функции: 1-я отправляет одно событие в чат за 5-8 минут до начала, 2-я отправляет все события списком в одном сообщении по порядку следования.

function send_next_event() {
  const today_events_arr = getEvents();
  const cur_time = new Date(new Date().getTime() + (6 * 60 * 60 * 1000)) //6 - разница во времени между NY и Berlin
  let msg = new String();
 
  for (i=0; i<today_events_arr.length; i++) {
    let substract = today_events_arr[i][1].getTime() - cur_time.getTime();
    if (substract/36000000 < 0.02 && substract > 0.1) {
      today_events_arr[i][1] = time_to_string(today_events_arr[i][1]);
 
    if (msg == "") {
        msg = `${today_events_arr[i].flat()}`
      } else {
        msg = `${msg}\n${today_events_arr[i].flat()}`
      }
      send(msg,chat_id_root,API);
    }
  }
}

Функция send_next_event() проверяет разницу между текущим временем и стартовым временем каждого из событий в массиве и отправляет сообщение в чат (send(msg,chat_id_root,API)) примерно за 5 минут.

Понимаю, здесь очень странные манипуляции со значением времени, но вот так это выглядит в js. Я сама еще в этом плаваю, потому не хочу объяснять все детали, т.к. сама искала ответы онлайн касаемо этой части. В целом, идея была преобразовать время из 1.65286722566E12 во время вида 13:00.

Функция для отправки всех событий представлена ниже.

function send_all_events() {
  const today_events_arr = getEvents();
  let msg = new String();
 
  if (today_events_arr.length !== 0) {
    today_events_arr.forEach((el,ind) => {
      el[1] = time_to_string(el[1])
      if (msg == "") {
        msg = `${ind+1} ${el.flat()}`
      } else {
        msg = `${msg}\n${ind+1} ${el.flat()}`
      }
    })
 
    send(msg, chat_id_root, API);
 
  } else {
    send('В календаре нет планов', chat_id_root, API);
  }
}

Функция активируется по триггеру каждое утро, а также я могу запустить ее по команде из бота.

Как выглядят планы в чате
Как выглядят планы в чате
Как выглядят календари
Как выглядят календари

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

Заключение

Вот такой Reminder bot уровня минимум спасает меня каждый день. Как обычно буду рада вашим комментам и вопросам. Весь код целиком есть на гите, который я вот только начала вести (https://github.com/Nadezhda95).

Всем добра и нет войне.

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


  1. den_golub
    18.05.2022 15:18
    +1

    Все ок, но только предвижу, что у многих кто захочет использовать Вашу наработку но не шарит за код, появятся вопросы, а где указывать API ключ бота, а где указывать и как логинится к календарю в боте. Я бы вынес подобное в реадми или комментариями прям в коде. С другой стороны, а надо ли)


    1. Nadjuscha Автор
      18.05.2022 17:10

      Потому и прикрепила ссылку на первый пост про создание ботов в целом. Переписывать в каждом посте одно и то же как-то не ок, по-моему)


      1. den_golub
        18.05.2022 18:52
        -1

        Ой сорри, я 3 раза просмотрел все статью прежде чем увидел)) ссылочная слепота)


  1. LeshaRB
    18.05.2022 16:12
    +1

    В чем отличие от

    iCalendarBot

    Google Calendar Bot


    1. Nadjuscha Автор
      18.05.2022 17:13

      Я не искала готовых решений и не сравнивала с моим, поэтому ответить на вопрос не могу. Бота сделала просто по фану и в посте показала методы класса CalendarApp для тех, кому могут быть интересны эти инструменты в другом контексте


    1. zaitsevio
      19.05.2022 15:35

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

      Google Calendar Bot - это было бы интересно глянуть, но не смог нагуглить, дайте ссылку, пожалуйста.


  1. nronnie
    19.05.2022 09:45
    -1

    не вижу дроубеков

    "О, мои глазоньки..." (с)

    Я сам абсолютно не против англицизмов в тексте (английский текст практически всегда короче и, кроме того, далеко не всякий термин можно нормально на русский перевести). Но в данном-то случае неужели нельзя либо написать "не вижу недостатков", либо если уж хочется английский, то так и написать "не вижу drawbacks"?


  1. zaitsevio
    19.05.2022 15:27

    Я тоже столкнулся с такой потребностью и сделал похожего бота: https://epical.app

    На самом деле, в таком боте гораздо больше подводных камней, чем это может показаться:

    1. Нужно отправлять сообщение ровно за то время, за которое хочет пользователь, мне, например, удобнее всего получать уведомления за минуту. Тут можно использовать очередь, поддерживающую отправку в заданное время.

    2. Событие в календаре может быть создано совсем незадолго до настоящего события, значит проверять нужно либо чаще чем раз в 5 минут, либо можно прозевать событие. Я решил эту проблему используя вебхуки.

    3. Нужно проверять, что одно и то же событие не будет отправлено несколько раз. У меня для этого сохраняется state.

    4. Если событие из календаря удаляется незадолго до назначенного времени, это тоже нужно учитывать и не слать уведомление.

    В общем, у вас еще большой потенциал для улучшений :)