Skip to content

Instantly share code, notes, and snippets.

@Gvozd
Last active April 30, 2023 09:03
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
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 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, только последней ошибки нет, скрипт запускается. Думал я один такой с сумасшедшим количеством записей в книжке.
Перенести удаление в начало хорошая идея. Я же сделал отдельный триггер для удаления.
Выручайте.

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