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();
}
}
}
@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