Skip to content

Instantly share code, notes, and snippets.

@Gvozd
Last active March 27, 2024 10:44
Show Gist options
  • 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();
}
}
}
@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