Skip to content

Instantly share code, notes, and snippets.

@Gvozd
Last active March 27, 2024 10:44
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gvozd/84f9c5ee011fc1344f21ac16db3c58b6 to your computer and use it in GitHub Desktop.
Save Gvozd/84f9c5ee011fc1344f21ac16db3c58b6 to your computer and use it in GitHub Desktop.
Birthday Notifications in google calendar
function createTrigger() {
ScriptApp.newTrigger('main')
.timeBased()
.everyDays(1)
.create();
}
function main() {
const {tmp, from, to} = getCalendars();
const syncedEvents = getEvents(to)
.map(function(evt) {
return {
event: evt,
eventSeries: run(() => evt.getEventSeries()),
fromId: run(() => evt.getDescription())
};
});
getEvents(from).forEach(function(fromEvent) {
createEvent(tmp, to, fromEvent, syncedEvents);
});
syncedEvents
.filter(function({synced}) {return !synced;})
.forEach(function({eventSeries}) {
Logger.log('Delete "%s"', run(() => eventSeries.getTitle()));
run(() => eventSeries.deleteEventSeries());
});
tmp.deleteCalendar();
deleteTempCals();
}
function getEvents(calendar) {
const currentYear = new Date().getFullYear();
const fromDate = new Date(currentYear, 0, 1, 1);
const toDate = new Date(currentYear + 1, 0, 1, -1);
return run(() => calendar.getEvents(fromDate, toDate));
}
function createEvent(tmp, cal, evt, syncedEvents) {
const evtId = run(() => evt.getEventSeries().getId())
.replace(/^\d{4}_/, '2022_');
const evtTitle = run(() => evt.getTitle());
const evtStartTime = run(() => evt.getAllDayStartDate());
const eventData = syncedEvents.find(function({fromId}) {
return fromId === evtId;
}) || {};
let {eventSeries, event} = eventData;
if(
!eventSeries ||
run(() => eventSeries.getTitle()) !== evtTitle ||
run(() => event.getStartTime().getTime()) !== run(() => evtStartTime.getTime())
) {
Logger.log('%sCreate "%s" %s', eventSeries ? 'Re-' : '', evtTitle, evtStartTime);
eventSeries = run(() => tmp.createAllDayEventSeries(evtTitle, evtStartTime, CalendarApp.newRecurrence().addYearlyRule(), {
description: evtId
}));
run(
() => eventSeries.setGuestsCanInviteOthers(false)
.setGuestsCanModify(false)
.setGuestsCanSeeGuests(false)
);
run(() =>
Calendar.Events.move(tmp.getId(), eventSeries.getId().split('@')[0], cal.getId())
);
} else {
Logger.log('Up to date "%s"', evtTitle);
eventData.synced = true;
}
}
function getCalendars() {
const fromCalendarId = 'addressbook#contacts@group.v.calendar.google.com';
const toCalendarName = 'Birthday Notifications';
const toCalendarIdPropKey = 'toCalendarId';
const scriptProperties = run(() => PropertiesService.getScriptProperties());
Logger.log('scriptProperties: %s', run(() => scriptProperties.getProperties()));
let fromCalendar = run(() => CalendarApp.getCalendarById(fromCalendarId));
if (!fromCalendar) {
Logger.log('Exported calendar not founded');
return;
}
let toCalendar = run(() => CalendarApp.getCalendarById(scriptProperties.getProperty(toCalendarIdPropKey)));
// Этот календарь нужен дял того чтобы подхватывались дефолтовые напоминания из конечного календаря
// у Calendar API куча проблем с заданием напоминаний для all-day событий, особенно в дату самого события(а не до)
// Эти события при попытке resetRemindersToDefault() получают напоминания от обычных событий, а не полнодневных.
// И в итоге не в календаре не удается задать полнодневное напоминание, и уж тем более в день самого ДР. Только обычные напоминания, и только заранее
// Но я обнаружил ХАК. Если созданное через API полнодневное событие передвинуть в другой календарь - оно подхватит его дефолтовые полнодневные напоминания
// К сожалению при возвращении в исходный календарь - все равно возвращается итоговая бага
// Поэтому создаем событие во временном календаре, а потом двигаем в основной. Все буде ОК
// При изменении напоминаний в конечном календаре - они подхвататся для всех событий, и это то, что нужно!
let tmpCalendar = run(() => CalendarApp.createCalendar('Temp calendar').setHidden(true));
if (!toCalendar) {
Logger.log('Import calendar not founded - create it');
toCalendar = run(() => CalendarApp.createCalendar(toCalendarName));
run(() => scriptProperties.setProperty(toCalendarIdPropKey, toCalendar.getId()));
}
return {tmp: tmpCalendar, from: fromCalendar, to: toCalendar};
}
function run(func) {
const timeouts = [10, 50, 100, 250, 500, 1000, 2000];
let error;
for(const timeout of timeouts) {
let start, end;
try {
start = Date.now();
return func();
} catch(e) {
error = e;
} finally {
end = Date.now();
Utilities.sleep(
Math.max(0, timeout - (end - start))
);
}
}
throw error;
}
// @author Winand
function deleteTempCals() {
// Удаление календарей Temp calendar
// CalendarApp.getAllOwnedCalendars() не возвращает скрытые календари,
// поэтому используется Calendar.CalendarList https://stackoverflow.com/a/32384340
var cals = Calendar.CalendarList.list(
{showHidden:true, minAccessRole:'owner', fields:'items(id,summary)'}
).items;
for(const i of cals) {
if(i.summary == 'Temp calendar') {
Logger.log('del cal ' + i.id);
// Calendar.Calendars.remove(i.id);
CalendarApp.getOwnedCalendarById(i.id).deleteCalendar();
}
}
}
@fuhuspel
Copy link

а в настройках календарей новый не появился

@alexander-yu-shamin
Copy link

Предположу, что ты не то запускал. Нужно запустить createTrigger

@NataTimos
Copy link

а в настройках календарей новый не появился

так и было. потом психанула и запустила все функции и в конце опять createTrigger и, о чудо!, все сработало :)

@Gvozd
Copy link
Author

Gvozd commented May 18, 2021

image
Если у кого сломался скрипт, подпишитесь на исходный календарь

@armen1313
Copy link

Добрый день!
Что я делаю не правильно?
Как мне заставить работать скрипт?
Спасибо.

code
triggers
calendar

@Gvozd
Copy link
Author

Gvozd commented Jul 30, 2021

@armen1313
Полная инструкция(хотя и для старого интерфейса) находится на https://blog.themarfa.name/kak-dobavit-napominaniia-o-dniakh-rozhdieniiakh-v-google-kaliendar/
Похоже вам нужно подключить Calendar API в проект скрипта
image

@armen1313
Copy link

@Gvozd
Инструкцию читал, нервничал, очень сложно было в первый раз работать со скриптами в гугле, так ещё и инструкция от старого гугла. Но с ней действительно было проще, чем вообще без неё.
Да, Вы правы не был подключен модуль. Теперь всё заработало. Единственное модуль называется Google Calendar API (по началу аж расстроился, не найдя ничего на "Ca".
Ещё раз спасибо за ответ и за скрипт.

@Winand
Copy link

Winand commented Oct 29, 2021

В июне сделал, но напоминания так и не приходили. И тут сегодня обнаруживаю следующее🥲
temp_cal

Upd: удолил.

function deleteTempCals() {
  // Удаление календарей Temp calendar
  // CalendarApp.getAllOwnedCalendars() не возвращает скрытые календари,
  // поэтому используется Calendar.CalendarList https://stackoverflow.com/a/32384340
  var cals = Calendar.CalendarList.list(
    {showHidden:true, minAccessRole:'owner', fields:'items(id,summary)'}
  ).items;
  for(const i of cals) {
    if(i.summary == 'Temp calendar') {
      Logger.log('del cal ' + i.id);
      // Calendar.Calendars.remove(i.id);
      CalendarApp.getOwnedCalendarById(i.id).deleteCalendar();
    }
  }
}

@Gvozd
Copy link
Author

Gvozd commented Oct 29, 2021

@Winand в июне этого года, использовали актуальную на тот момент версию скрипта?
Похоже это еще одна неизвестная пока мне проблема

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

Пожалуйста проверьте что у вас точно используется актуальная версия скрипта
Можете прямо скопировать его еще раз, и запустить функцию main, чтобы выполнить его сразу, а не отправить в планировщик
После этого, посмотрите, и пришлите мне с какой ошибкой упал скрипт
Это можно узнать в левой панели, в пункте "Количество выполнений"

PS повторно запускать сreateTrigger не нужно
На всякий случай убедитесь в левой панели, в "Триггеры", что у вас запланирован только один триггер

@Winand
Copy link

Winand commented Oct 30, 2021

Скрипт был актуальный. Сейчас перезапустил main. В первый раз получил ошибку (см. ниже, у меня ещё вверху файла 3 строки с комментариями были), а второй раз, похоже, нормально отработал: создал события, потом удалил старые. Но появилось 5 "Temp calendar", и я не посмотрел, после 1го или 2го запуска🤦‍♂️

Exception: Service invoked too many times in a short time: calendar. Try Utilities.sleep(1000) between calls.
    at [unknown function](Код:17:36)
    at run(Код:118:14)
    at [unknown function](Код:17:22)
    at main(Код:14:6)

upd: Ещё обнаружил порядка 8 триггеров, все удалил и выполнил сreateTrigger заново

@Gvozd
Copy link
Author

Gvozd commented Oct 30, 2021

@Winand
Одно выполнение скрипта создает только один Temp calendar
Но у вас было много триггеров, так что полагаю это ночное выполнение вам создало дополнительные календари, после предыдущей очистки

Попробуйте отредактировать const timeouts = [10, 50, 100, 250, 500, 1000, 2000];
Задайте первую цифру в 20 или 30, и попробуйте скрипт еще раз
Это увеличит время между запросами, и должно помочь избежать этой ошибки
Перед выполнением скрипта, почистите TempCalendar-и, чтобы проверить что успешное выполнение скрипта подчищает их за собой

Предположу что у вас достаточно много контактов (больше 130), и поэтому гугл в определенный момент начинает отсекать такое большое количество запросов

@SergBl
Copy link

SergBl commented May 24, 2022

Здравствуйте,
Пару лет назад сделал себе эти уведомления, все отлично работало, спасибо. И вот неделю назад добавил для одного существующего контакта день рождения, он как раз сегодня. И сегодня никакого уведомления не получил. Полез в календарь в гугле, и там нет этого события. Что могло сломаться?
Нашел ошибки, каждый день вот такое повторяется:
calendar

@Gvozd
Copy link
Author

Gvozd commented May 25, 2022

@SergBl полагаю у вас не самая последняя версия скрипта
В октябре 20-го года Google убрал старый способ получения календаря контактов, и скрипт был исправлен
обновите скрипт на текущую версию

@SergBl
Copy link

SergBl commented May 27, 2022

@Gvozd Точно, версия скрипта оказалась не последняя. Спасибо!

@SVK050
Copy link

SVK050 commented Aug 2, 2022

Здраствуйте, можно ссылку за последнюю версию скрипта?

@armen1313
Copy link

Добрый день.
У меня частота появления ошибок 100% и все они по времени. Как я понял предельное время выполнения всего скрипта 360 секунд. По всей видимости этих 6 минут не хватает на 1916 контактов.
Есть ли путь это исправить?
image

@Gvozd
Copy link
Author

Gvozd commented Aug 8, 2022

@SVK050 я обновляю этот gist, когда вношу изменения в скрипт
вы видите последнюю версию скрипта

@armen1313 у меня сейчас нет идей как это исправить наверняка
можете попробовать уменьшить в 109-ой строчке значения таймаутов, начиная с первых
Эти таймауты используются когда GoogleAPI отклоняет вызовы по слишком частому обращению, и возможно они завышены
Они были подобраны мной примерно наугад, для того, чтобы гарантировано получить все-таки значение при повторных попытках

@KirillChernobrivchenko
Copy link

@Gvozd
Добрый день!
Тоже некоторое время назад перестали обновляться календари, у тригера 100% ошибок.
Удалил наплодившиеся временные календари, запустил вручную main. Он почему-то очень медленно обрабатывал контакты: в среднем 1 контакт за 20 секунд. В итоге за 6 минут - 18 контактов.
Попытки уменьшить таймаут к результату не привели, вылетает ошибка по таймауту.
Попробовал глубокой ночью запустить руками - результат тот же.
А ограничение на работу в 6 минут установлено гуглом? Почему скрипт не дорабатывает до конца, пусть и с большими задержками.

@Gvozd
Copy link
Author

Gvozd commented Oct 1, 2022

@KirillChernobrivchenko не знаю почему у вас так долго выполняется
у меня 130 записей обрабатывается за минуту-две

вы используете последнюю версию скрипта?
у вас только один активный тригер?

6 минут - это встроенное ограничение гугла, оно не настраивается

@Winand
Copy link

Winand commented Oct 1, 2022

У меня тоже наплодились календари и ничего не работает. Случайно увидел, что сам же писал скрипт для их удаления.)) Большое облегчение

17:50:31	Примечание	Выполнение начато
17:50:36	Информация	scriptProperties: {toCalendarId=vs3qkisvg6olo6epbk98bdm4lo@group.calendar.google.com}
17:53:50	Информация	Re-Create "***" Mon Feb 28 00:00:00 GMT+03:00 2022
17:54:12	Информация	Re-Create "***" Tue Mar 01 00:00:00 GMT+03:00 2022
17:54:31	Информация	Re-Create "***" Fri Mar 04 00:00:00 GMT+03:00 2022
17:54:52	Информация	Re-Create "***" Sat Mar 26 00:00:00 GMT+03:00 2022
17:55:11	Информация	Re-Create "***" Thu Apr 07 00:00:00 GMT+03:00 2022
17:55:29	Информация	Re-Create "***" Sat Apr 16 00:00:00 GMT+03:00 2022
17:55:47	Информация	Re-Create "***" Sat Apr 16 00:00:00 GMT+03:00 2022
17:56:04	Информация	Re-Create "***" Tue Apr 19 00:00:00 GMT+03:00 2022
17:56:19	Информация	Re-Create "***" Tue Apr 26 00:00:00 GMT+03:00 2022
17:56:31	Ошибка	
Exceeded maximum execution time

@KirillChernobrivchenko
Copy link

KirillChernobrivchenko commented Oct 1, 2022

@Gvozd скрипт последней версии, да. Специально обновлял, на всякий случай, позавчера. Триггер один.
Такое ощущение, что на аккаунте дневную квоту по созданию событий порезали до 0...
Вы территориально где живете? Может это в России квоты порезали
@Winand Вашим скриптом для чистки временных календарей и пользуюсь, спасибо)))
И процесс у меня ровно такой же, как у Вас: тоже примерно раз в 20 секунд напоминание по контакту создается.

@Gvozd
Copy link
Author

Gvozd commented Oct 12, 2022

Re-Create на каждой записи вероятнее всего означает что где-то сбито время
Потому что в штатном режиме он должен вызываться только при изменении имени или дня рождения в исходном контакте

  1. Проверьте что в созданном календаре "Birthday Notifications" такой же часовой пояс, что и в стандартном календаре "Дни рождения"
    если нет, то поменяйте у созданного календаря
    у встроенного календаря часовой пояс не настраивается - наверно берется из вашего гугл-профиля
  2. затем в настройках самого скрипта также замените часовой пояс на такой же

также, так как скрипт падал по таймауту, то он только добавлял несколько записей, а до процедуры очистки лишних записей дело не доходило
есть подозрение что ваш "Birthday Notifications" переполнен дубликатами, и по этой причине может быть медленно добавляются новые записи
попробуйте удалить его, запустить скрипт, и посмотреть что будет
потом проверьте настройки часового пояса в новом календаре, и если они неверные, поправьте и запустите скрипт еще раз

@Winand
Copy link

Winand commented Oct 12, 2022

Установка часового пояса, соответствующего основному календарю, помогла. Изначально стояло время GMT+0.
Но скрипт всё равно не успел за 6 минут отработать, поскольку после сообщений "up to date" он стал удалять по ~50 раз каждое уведомление. Возможно, это последствия неудачных запусков ранее.
UPD: Второй запуск отработал уже успешно.

@KirillChernobrivchenko
Copy link

@Gvozd Спасибо большое!
В календаре "Birthday Notifications" почему-то действительно был часовой пояс GMT+00:00, хотя в остальных календарях аккаунта GMT+03:00. Где именно в скрипте настраивается часовой пояс, я сходу не понял. В меню "Настройки проекта" указан корректный GMT+03:00, тем не менее "Birthday Notifications" создается по Гринвичу
В итоге удалил календарь "Birthday Notifications", запустил main, после создания календаря руками поменял часовой пояс.
Потребовалось несколько запусков, чтобы добавить все ДР, т.к. добавление все равно длится 15-20 секунд. Но в итоге все добавилось, повторные запуски не пересоздают события, все отрабатывает корректно.

@Gvozd
Copy link
Author

Gvozd commented Jan 5, 2023

Новогоднее обновление
У исходных событий в getId() участвовал текущий год
Это привело к пересозданию событий в целевом календаре

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

  1. сделал замену в getId на 2022-ой год
    таким образом при следующем запуске он только почистит новые дубликаты, а старые события останутся
    в таймаут скрипт должен укладываться
  2. добавил в конец вызов deleteTempCals за авторством @Winand, чтобы почистить временные календари

@armen1313
Copy link

С праздниками!

Попробовал новый скрипт.

Первый запуск не избавил от дубликатов и не удаленных временных календарей, остановился так же по таймауту 360сек.

Запустил функцию удаление временных календарей. Пришлось несколько раз запускать... :) так их много у меня оказалось.

Удалил основной календарь, т.к. там было по 20+ дубликатов.

Запустил main после создания основного календаря остановил скрипт и поменял часовой пояс на свой.

Запустил main повторно он остановился отработав январь и не удалив за собой временный календарь и осталась пара дубликатов начала января, видимо те, что до смены часового пояса затясались. Добавил строчку с функцией от @Winand с удалением временных календарей в начало main'a, чтоб сначала удалялись временные календари, а только потом создавались новые.

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

21:17:02 Примечание Выполнение начато
21:17:03 Информация del cal l7sk8ndlххххххх069b404@group.calendar.google.com
21:17:06 Информация scriptProperties: {toCalendarId=cgtххххххххххххххххххххххх00@group.calendar.google.com}
21:22:03 Информация Up to date "Алексей – день рождения"
21:22:03 Информация Up to date " – Антон "
......
21:23:00 Информация Up to date "Даниил – день рождения"
21:23:00 Информация Create "Екатерина – день рождения" Tue Nov 14 00:00:00 GMT+03:00 2023
21:23:02 Ошибка
Exceeded maximum execution time

Всегда заканчивается на Екатерине и она не добавляется по итогу.

upd:
Скрипт перестал вообще отрабатывать вот что пишет:
00:46:05 Примечание Выполнение начато
00:46:06 Информация scriptProperties: {toCalendarId=cgххххххххххх0@group.calendar.google.com}
00:46:06 Информация Exported calendar not founded
00:46:07 Ошибка TypeError: Cannot destructure property 'tmp' of 'getCalendars(...)' as it is undefined.
main @ Notifications.gs:10

У меня 10-ая строчка это:
8 function main() {
9 deleteTempCals();
10 const {tmp, from, to} = getCalendars();
11 const syncedEvents = getEvents(to)

@JohnS08
Copy link

JohnS08 commented Jan 11, 2023

Добрый день.

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

@armen1313
Copy link

Доброго дня!
Решил проверить как поживает скрипт и обнаружил, что он вылетает ошибкой, которая была 4 года назад за секунду.
На всякий случай скопировал текст скрипта из первого сообщения, но это не помогло.
2024-03-23_15-15-10

@Gvozd
Copy link
Author

Gvozd commented Mar 26, 2024

@armen1313 такая ошибка может быть, если не сделать эту часть инструкции
https://gist.github.com/Gvozd/84f9c5ee011fc1344f21ac16db3c58b6?permalink_comment_id=3747641#gistcomment-3747641

@Winand
Copy link

Winand commented Mar 27, 2024

Недавно Гугл стал сам предлагать включать уведомления о днях рождения. Но, как я понимаю, индивидуально для каждого человека? Это не особо удобно

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment