gcal-sync
Version:
🔄 add an one way synchronization from github commits to google calendar and track your progress effortlessly.
2 lines (1 loc) • 28.5 kB
JavaScript
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).GcalSync=t()}(this,(function(){"use strict";const e="gcal-sync",t="lucasvtiradentes/gcal-sync",o="2.1.2",i=(e,t,o)=>e.reduce(((e,i)=>{const n=i[t],s=i[o];return e[n]=s,e}),{});var n;const s=3,r=15,a=2e3,c=10,d=6,m=2100,l=1e3,_="object"==typeof process&&(null===(n=null===process||void 0===process?void 0:process.env)||void 0===n?void 0:n.NODE_ENV),g=[{key:"today_github_added_commits",initial_value:[]},{key:"today_github_deleted_commits",initial_value:[]},{key:"last_released_version_alerted",initial_value:""},{key:"last_released_version_sent_date",initial_value:""},{key:"last_daily_email_sent_date",initial_value:""},{key:"github_commits_tracked_to_be_added_hash",initial_value:""},{key:"github_commits_tracked_to_be_deleted_hash",initial_value:""},{key:"github_commit_changes_count",initial_value:""}],h=i(g,"key","initial_value"),u=i(g,"key","key"),p="schema invalid",f="This method cannot run in non-production environments",b="github_sync";const y={tableStyle:'style="border: 1px solid #333; width: 90%"',tableRowStyle:'style="width: 100%"',tableRowColumnStyle:'style="border: 1px solid #333"'},$=e=>"date"in e?e.date:e.dateTime;function D(e){return e.commits_added.length+e.commits_deleted.length}function v(o){let i="";return i=`Hi!<br/><br/>there were ${D(o)} changes made to your google calendar:<br/>\n`,i+=function(e){const t=e.commits_added,o=e.commits_deleted,i=e=>0===e.length?"":`${e.map((e=>{const{repositoryLink:t,commitMessage:o,repositoryName:i}=e.extendedProperties.private,n=[$(e.start).split("T")[0],`<a href="${t}">${i}</a>`,`<a href="${e.htmlLink}">${o}</a>`].map((e=>`<td ${y.tableRowColumnStyle}> ${e}</td>`)).join("\n");return`<tr ${y.tableRowStyle}">\n${n}\n</tr>`})).join("\n")}`,n=`<tr ${y.tableRowStyle}">\n<th ${y.tableRowColumnStyle} width="80px">date</th><th ${y.tableRowColumnStyle} width="130px">repository</th><th ${y.tableRowColumnStyle} width="auto">commit</th>\n</tr>`;let s="";return s+=t.length>0?`<br/>added commits events : ${t.length}<br/><br/> \n <center>\n<table ${y.tableStyle}>\n${n}\n${i(t)}\n</table>\n</center>\n`:"",s+=o.length>0?`<br/>removed commits events : ${o.length}<br/><br/> \n <center>\n<table ${y.tableStyle}>\n${n}\n${i(o)}\n</table>\n</center>\n`:"",s}(o),i+=`<br/>Regards,<br/>your <a href='https://github.com/${t}'>${e}</a> bot`,i}function k(e){return{shouldSyncGithub:e.configs[b].commits_configs.should_sync}}function S(e){const t=PropertiesService.getScriptProperties().getProperty(e);let o;try{o=JSON.parse(t)}catch(e){o=t}return o}function E(e,t){const o="string"==typeof t?t:JSON.stringify(t),i=o.length;console.log(`updating property "${e}" with size: ${i} chars`);try{PropertiesService.getScriptProperties().setProperty(e,o)}catch(t){throw console.log(`error updating property "${e}": ${t}`),t}}function w(e){const t=ScriptApp.getProjectTriggers().find((t=>t.getHandlerFunction()===e));t&&ScriptApp.deleteTrigger(t)}function G(e){MailApp.sendEmail(e)}const x=new class{constructor(){this.logs=[]}info(e,...t){_||(console.log(e,...t),this.logs.push(e))}error(e,...t){_||(console.error(e,...t),this.logs.push(e))}};function M(e,t){const o=function(e){const t=new Date;return t.setHours(t.getHours()+e),t}(t),i=60*Number(o.getHours())+Number(o.getMinutes()),n=e.split(":");return i>=60*Number(n[0])+Number(n[1])}function U(e){return{htmlLink:e.htmlLink,start:e.start,extendedProperties:{private:{repositoryLink:e.extendedProperties.private.repositoryLink,repositoryName:e.extendedProperties.private.repositoryName,commitMessage:e.extendedProperties.private.commitMessage}}}}function C(i,n){var s,r;const{shouldSyncGithub:a}=k(i),c=n.commits_added.length+n.commits_deleted.length;if(x.info(`[DEBUG][SESSION] shouldSyncGithub: ${a}, githubNewItems: ${c}`),a&&c>0){const e=null!==(s=S(u.today_github_added_commits))&&void 0!==s?s:[],t=null!==(r=S(u.today_github_deleted_commits))&&void 0!==r?r:[];x.info(`[DEBUG][SESSION] current todayAddedCommits: ${e.length}, todayDeletedCommits: ${t.length}`);const o=n.commits_added.map(U),i=n.commits_deleted.map(U);x.info(`[DEBUG][SESSION] adding ${o.length} commits, deleting ${i.length} commits`);const a=[...e,...o],d=[...t,...i],m=JSON.stringify(a).length,l=JSON.stringify(d).length;x.info(`[DEBUG][SESSION] new added commits size: ${m} chars, deleted size: ${l} chars`);const _=45e4;if(m>_){x.info(`[WARN][SESSION] added commits size (${m}) exceeds limit (${_}), keeping only recent ${Math.min(100,a.length)} commits`);const e=a.slice(-100);try{E(u.today_github_added_commits,e)}catch(e){x.info(`[ERROR][SESSION] failed to store added commits even after truncation: ${e}`),E(u.today_github_added_commits,[])}}else try{E(u.today_github_added_commits,a)}catch(e){x.info(`[ERROR][SESSION] failed to store added commits: ${e}`)}if(l>_){x.info(`[WARN][SESSION] deleted commits size (${l}) exceeds limit (${_}), keeping only recent ${Math.min(100,d.length)} commits`);const e=d.slice(-100);try{E(u.today_github_deleted_commits,e)}catch(e){x.info(`[ERROR][SESSION] failed to store deleted commits even after truncation: ${e}`),E(u.today_github_deleted_commits,[])}}else try{E(u.today_github_deleted_commits,d)}catch(e){x.info(`[ERROR][SESSION] failed to store deleted commits: ${e}`)}x.info(`added ${c} new github items to today's stats`)}!function(i,n,s){var r;const a=i.user_email;if(i.configs.settings.per_sync_emails.email_session&&s>0){G(function(t,o){const i=v(o);return{to:t,name:`${e}`,subject:`session report - ${D(o)} modifications - ${e}`,htmlBody:i}}(a,n))}const c=M(i.configs.settings.per_day_emails.time_to_send,i.timezone_offset),d=i.today_date===S(u.last_daily_email_sent_date);if(c&&i.configs.settings.per_day_emails.email_daily_summary&&!d){E(u.last_daily_email_sent_date,i.today_date);G(function(t,o,i){const n=v(o);return{to:t,name:`${e}`,subject:`daily report for ${i} - ${D(o)} modifications - ${e}`,htmlBody:n}}(a,{commits_added:S(u.today_github_added_commits),commits_deleted:S(u.today_github_deleted_commits)},i.today_date)),E(u.today_github_added_commits,[]),E(u.today_github_deleted_commits,[]),x.info("today stats were reseted!")}const m=i.today_date===S(u.last_released_version_sent_date),l=e=>Number(e.replace("v","").split(".").join("")),_=()=>{var e;const i=UrlFetchApp.fetch(`https://api.github.com/repos/${t}/releases?per_page=1`);return null!==(e=JSON.parse(i.getContentText())[0])&&void 0!==e?e:{tag_name:o}};if(c&&i.configs.settings.per_day_emails.email_new_gcal_sync_release&&!m){E(u.last_released_version_sent_date,i.today_date);const n=_(),s=l(n.tag_name),c=l(o),d=null!==(r=S(u.last_released_version_alerted))&&void 0!==r?r:"";if(s>c&&s.toString()!=d){G(function(o,i){const n=`Hi!\n <br/><br/>\n a new <a href="https://github.com/${t}">${e}</a> version is available: <br/>\n <ul>\n <li>new version: ${i.tag_name}</li>\n <li>published at: ${i.published_at}</li>\n <li>details: <a href="https://github.com/${t}/releases">here</a></li>\n </ul>\n to update, replace the old version number in your apps scripts <a href="https://script.google.com/">gcal sync project</a> to the new version: ${i.tag_name.replace("v","")}<br/>\n and also check if you need to change the setup code in the <a href='https://github.com/${t}#installation'>installation section</a>.\n <br /><br />\n Regards,\n your <a href='https://github.com/${t}'>${e}</a> bot\n `;return{to:o,name:`${e}`,subject:`new version [${i.tag_name}] was released - ${e}`,htmlBody:n}}(a,n)),E(u.last_released_version_alerted,s.toString())}}}(i,n,c);const{commits_added:d,commits_deleted:m,commits_tracked_to_be_added:l,commits_tracked_to_be_deleted:_}=n;return{commits_added:d.length,commits_deleted:m.length,commits_tracked_to_be_added:l.length,commits_tracked_to_be_deleted:_.length}}function B(e,t,o,i){var n,s,r;const a=[];let d=1;for(;d<=c;){const c=`https://api.github.com/search/commits?q=${`author:${e}+committer-date:${o}..${i}`}&page=${d}&sort=committer-date&per_page=100`,l={muteHttpExceptions:!0,headers:t?{Authorization:`Bearer ${t}`}:{}};let _;try{_=UrlFetchApp.fetch(c,l)}catch(e){console.log(`network error during ${o}..${i} page ${d}, returning partial results`);break}const g=null!==(n=JSON.parse(_.getContentText()))&&void 0!==n?n:{};if(200!==_.getResponseCode()){if(403===_.getResponseCode()&&(null===(s=g.message)||void 0===s?void 0:s.includes("rate limit"))){console.log(`GitHub rate limit hit during ${o}..${i}`);break}break}const h=null!==(r=g.items)&&void 0!==r?r:[];if(0===h.length)break;if(a.push(...h),h.length<100)break;Utilities.sleep(m),d++}return a}function R(e,t){const o=[],i=function(e=d){const t=[],o=new Date;for(let i=0;i<e;i++){const e=new Date(o.getFullYear(),o.getMonth()-i+1,1),n=new Date(o.getFullYear(),o.getMonth()-i,1);t.push({start:n.toISOString().split("T")[0],end:e.toISOString().split("T")[0]})}return t}();console.log(`fetching commits for ${i.length} date ranges (${d} months)`);for(const n of i){const i=B(e,t,n.start,n.end);i.length>0&&(o.push(...i),console.log(`${n.start}..${n.end}: ${i.length} commits (total: ${o.length})`)),Utilities.sleep(l)}return o.map((e=>({commitDate:e.commit.author.date,commitMessage:e.commit.message.split("\n")[0],commitId:e.html_url.split("commit/")[1],commitUrl:e.html_url,repository:e.repository.full_name,repositoryLink:`https://github.com/${e.repository.full_name}`,repositoryId:e.repository.id,repositoryName:e.repository.name,repositoryOwner:e.repository.owner.login,repositoryDescription:e.repository.description,isRepositoryPrivate:e.repository.private,isRepositoryFork:e.repository.fork})))}function N(e){const t={":art:":"🎨",":zap:":"⚡️",":fire:":"🔥",":bug:":"🐛",":ambulance:":"🚑️",":sparkles:":"✨",":memo:":"📝",":rocket:":"🚀",":lipstick:":"💄",":tada:":"🎉",":white_check_mark:":"✅",":lock:":"🔒️",":closed_lock_with_key:":"🔐",":bookmark:":"🔖",":rotating_light:":"🚨",":construction:":"🚧",":green_heart:":"💚",":arrow_down:":"⬇️",":arrow_up:":"⬆️",":pushpin:":"📌",":construction_worker:":"👷",":chart_with_upwards_trend:":"📈",":recycle:":"♻️",":heavy_plus_sign:":"➕",":heavy_minus_sign:":"➖",":wrench:":"🔧",":hammer:":"🔨",":globe_with_meridians:":"🌐",":pencil2:":"✏️",":poop:":"💩",":rewind:":"⏪️",":twisted_rightwards_arrows:":"🔀",":package:":"📦️",":alien:":"👽️",":truck:":"🚚",":page_facing_up:":"📄",":boom:":"💥",":bento:":"🍱",":wheelchair:":"♿️",":bulb:":"💡",":beers:":"🍻",":speech_balloon:":"💬",":card_file_box:":"🗃️",":loud_sound:":"🔊",":mute:":"🔇",":busts_in_silhouette:":"👥",":children_crossing:":"🚸",":building_construction:":"🏗️",":iphone:":"📱",":clown_face:":"🤡",":egg:":"🥚",":see_no_evil:":"🙈",":camera_flash:":"📸",":alembic:":"⚗️",":mag:":"🔍️",":label:":"🏷️",":seedling:":"🌱",":triangular_flag_on_post:":"🚩",":goal_net:":"🥅",":dizzy:":"💫",":wastebasket:":"🗑️",":passport_control:":"🛂",":adhesive_bandage:":"🩹",":monocle_face:":"🧐",":coffin:":"⚰️",":test_tube:":"🧪",":necktie:":"👔",":stethoscope:":"🩺",":bricks:":"🧱",":technologist:":"🧑💻",":money_with_wings:":"💸",":thread:":"🧵",":safety_vest:":"🦺"};let o=e;for(const[e,i]of Object.entries(t))o=o.replace(e,i);return o}const A=()=>{var e;return null!==(e=Calendar.CalendarList.list({showHidden:!0}).items)&&void 0!==e?e:[]},O=e=>A().find((t=>t.summary===e)),I=e=>{const t=Calendar;if(t.CalendarList.list({showHidden:!0}).items.filter((e=>"owner"===e.accessRole)).map((e=>e.summary)).includes(e))throw new Error(`calendar ${e} already exists!`);const o=t.newCalendar();o.summary=e,o.timeZone=t.Settings.get("timezone").value;return t.Calendars.insert(o)};function P(e){return A().find((t=>t.summary===e))}function T(e,t,o){x.info(`[DEBUG][GCAL] getTasksFromGoogleCalendarsWithDateRange called for ${e.length} calendars`);const i=e.reduce(((e,i)=>{const n=i,s=P(n);if(!s)return x.info(`[DEBUG][GCAL] calendar "${n}" not found`),e;const r=function(e,t,o){var i;x.info(`[DEBUG][GCAL] fetching events from ${e.id} between ${t} and ${o}`);const n=[];let s,r=0;do{const a=Calendar.Events.list(e.id,{maxResults:2500,timeMin:new Date(t).toISOString(),timeMax:new Date(o).toISOString(),singleEvents:!0,orderBy:"startTime",pageToken:s}),c=null!==(i=a.items)&&void 0!==i?i:[];n.push(...c),s=a.nextPageToken,r++,x.info(`[DEBUG][GCAL] page ${r}: fetched ${c.length} events (total: ${n.length})`)}while(s);x.info(`[DEBUG][GCAL] fetched ${n.length} total events from calendar in ${r} pages`);const a=n.map((e=>function(e){var t,o,i,n,s;return{id:e.id,summary:e.summary,description:null!==(t=e.description)&&void 0!==t?t:"",htmlLink:e.htmlLink,attendees:null!==(o=e.attendees)&&void 0!==o?o:[],reminders:null!==(i=e.reminders)&&void 0!==i?i:{},visibility:null!==(n=e.visibility)&&void 0!==n?n:"default",start:e.start,end:e.end,created:e.created,updated:e.updated,colorId:e.colorId,extendedProperties:null!==(s=e.extendedProperties)&&void 0!==s?s:{}}}(e)));return a}(s,t,o);return[...e,...r]}),[]);return x.info(`[DEBUG][GCAL] total tasks from all calendars: ${i.length}`),i}function L(e,t){if(0===t.length)return[];const o=ScriptApp.getOAuthToken(),i=`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(e.id)}/events`,n=t.map((e=>({url:i,method:"post",contentType:"application/json",headers:{Authorization:`Bearer ${o}`},payload:JSON.stringify(e),muteHttpExceptions:!0})));return UrlFetchApp.fetchAll(n).map(((e,t)=>200===e.getResponseCode()?JSON.parse(e.getContentText()):(x.info(`failed to add event ${t}: ${e.getContentText()}`),null))).filter((e=>null!==e))}function j(e,t){Calendar.Events.remove(e.id,t.id)}function z(e){const t=[...e].sort().join(",");return Utilities.computeDigest(Utilities.DigestAlgorithm.MD5,t).map((e=>(e<0?e+256:e).toString(16).padStart(2,"0"))).join("")}function H(){E("github_commit_changes_count","0"),E("github_commits_tracked_to_be_added_hash",""),E("github_commits_tracked_to_be_deleted_hash","")}function F(e,t){return t.sort(((e,t)=>Number(new Date(t.commitDate))-Number(new Date(e.commitDate)))).filter((t=>t.repository.includes(e[b].username))).filter((t=>!1===e[b].commits_configs.ignored_repos.includes(t.repositoryName)))}function J(e){var t;x.info("syncing github commits");const o=function(){const e=new Date,t=new Date(e.getFullYear(),e.getMonth()+1,1);return{startDate:new Date(e.getFullYear(),e.getMonth()-d,1).toISOString().split("T")[0],endDate:t.toISOString().split("T")[0]}}();x.info(`[DEBUG] github date range: ${o.startDate} to ${o.endDate}`);const i=R(e[b].username,e[b].personal_token);x.info(`[DEBUG] fetched ${i.length} total commits from github`);const n=T([e[b].commits_configs.commits_calendar],o.startDate,o.endDate);x.info(`[DEBUG] fetched ${n.length} events from gcal within date range`);const c={githubCommits:i,githubGcalCommits:n},m=S("github_commit_changes_count"),l=Number(m)+1;x.info(`[DEBUG] sync index: ${m} -> ${l}`),null===m&&(x.info("[DEBUG] oldGithubSyncIndex is null, resetting properties"),H()),E("github_commit_changes_count",l.toString()),1===l?x.info(`checking commit changes: ${l}/${s}`):l>1&&l<s?x.info(`confirming commit changes: ${l}/${s}`):l===s&&x.info(`making commit changes if succeed: ${l}/${s}`);const _=F(e,c.githubCommits);x.info(`found ${_.length} commits after filtering`),x.info(`[DEBUG] filtering removed ${c.githubCommits.length-_.length} commits`);const g=P(e[b].commits_configs.commits_calendar);x.info(`github calendar "${e[b].commits_configs.commits_calendar}" found: ${!!g}, id: ${null!==(t=null==g?void 0:g.id)&&void 0!==t?t:"N/A"}`);const h=Object.assign(Object.assign({},function({filteredRepos:e,currentGithubSyncIndex:t,githubCalendar:o,githubGcalCommits:i,parseCommitEmojis:n}){var c,d,m,l;x.info("[DEBUG][ADD] starting syncGithubCommitsToAdd"),x.info(`[DEBUG][ADD] filteredRepos: ${e.length}, gcalCommits: ${i.length}, parseEmojis: ${n}`);const _={commits_tracked_to_be_added:[],commits_added:[]},g=new Set(e.map((e=>e.repository))),h=new Set(i.map((e=>{var t,o;return null===(o=null===(t=e.extendedProperties)||void 0===t?void 0:t.private)||void 0===o?void 0:o.repository})).filter(Boolean));x.info(`[DEBUG][ADD] unique github repos: ${g.size}, unique gcal repos: ${h.size}`);const u=[...g].filter((e=>!h.has(e)));u.length>0&&x.info(`[DEBUG][ADD] repos in github but NOT in gcal: ${u.join(", ")}`);const p=new Map;for(const e of i){const t=null===(d=null===(c=e.extendedProperties)||void 0===c?void 0:c.private)||void 0===d?void 0:d.repository;t&&(p.has(t)||p.set(t,[]),p.get(t).push(e))}let f=0,b={noSameRepo:0,noDateMatch:0,noMessageMatch:0};const y=[];for(const t of e){const e=null!==(m=p.get(t.repository))&&void 0!==m?m:[];if(0===e.length){b.noSameRepo++;const e=n?N(t.commitMessage):t.commitMessage,o={private:{commitMessage:e,commitDate:t.commitDate,repository:t.repository,repositoryName:t.repositoryName,repositoryLink:t.repositoryLink,commitId:t.commitId}},i={summary:`${t.repositoryName} - ${e}`,description:`repository: https://github.com/${t.repository}\ncommit: ${t.commitUrl}`,start:{dateTime:t.commitDate},end:{dateTime:t.commitDate},reminders:{useDefault:!1,overrides:[]},extendedProperties:o};_.commits_tracked_to_be_added.push(i),y.length<5&&y.push({repo:t.repository,ghDate:t.commitDate,ghMsg:t.commitMessage.slice(0,50)});continue}let o=!1,i=!1,s=!1;for(const n of e){const e=null===(l=n.extendedProperties)||void 0===l?void 0:l.private;if(!e)continue;const r=e.commitDate===t.commitDate,a=N(e.commitMessage||"")===N(t.commitMessage);if(r&&a){o=!0;break}r||(i=!0),r&&!a&&(s=!0)}if(o)f++;else{s?b.noMessageMatch++:i&&b.noDateMatch++,y.length<5&&y.push({repo:t.repository,ghDate:t.commitDate,ghMsg:t.commitMessage.slice(0,50)});const e=n?N(t.commitMessage):t.commitMessage,o={private:{commitMessage:e,commitDate:t.commitDate,repository:t.repository,repositoryName:t.repositoryName,repositoryLink:t.repositoryLink,commitId:t.commitId}},r={summary:`${t.repositoryName} - ${e}`,description:`repository: https://github.com/${t.repository}\ncommit: ${t.commitUrl}`,start:{dateTime:t.commitDate},end:{dateTime:t.commitDate},reminders:{useDefault:!1,overrides:[]},extendedProperties:o};_.commits_tracked_to_be_added.push(r)}}x.info(`[DEBUG][ADD] matched ${f}/${e.length} commits`),x.info(`[DEBUG][ADD] commits to add: ${_.commits_tracked_to_be_added.length}`),x.info(`[DEBUG][ADD] no match reasons: noSameRepo=${b.noSameRepo}, noDateMatch=${b.noDateMatch}, noMessageMatch=${b.noMessageMatch}`),y.length>0&&(x.info(`[DEBUG][ADD] sample commits that didn't match (first ${y.length}):`),y.forEach(((e,t)=>{x.info(`[DEBUG][ADD] ${t+1}. repo=${e.repo} date=${e.ghDate} msg=${e.ghMsg}`)})));_.commits_tracked_to_be_added.length>0&&_.commits_tracked_to_be_added.length<=10&&(x.info("[DEBUG][ADD] commits to add details:"),_.commits_tracked_to_be_added.forEach(((e,t)=>{x.info(`[DEBUG][ADD] ${t+1}. ${e.extendedProperties.private.repository} - ${e.extendedProperties.private.commitDate.slice(0,10)} - ${e.extendedProperties.private.commitMessage.slice(0,50)}`)})));const $=_.commits_tracked_to_be_added.map((e=>e.extendedProperties.private.commitId)),D=z($);if(x.info(`[DEBUG][ADD] computed hash for ${$.length} commitIds: ${D.slice(0,16)}...`),1===t)return x.info(`storing hash for ${$.length} commits to track for addition`),E("github_commits_tracked_to_be_added_hash",D),_;const v=S("github_commits_tracked_to_be_added_hash");if(x.info("[DEBUG][ADD] lastHash from storage: "+(v?v.slice(0,16)+"...":"null")),!v)return x.info("no stored hash found, resetting to step 1"),H(),E("github_commits_tracked_to_be_added_hash",D),_;if(x.info(`comparing hashes: stored=${v.slice(0,8)}... current=${D.slice(0,8)}...`),v!==D)return x.info("reset github commit properties due hash mismatch in added commits"),x.info(`[DEBUG][ADD] hash mismatch: stored=${v}, current=${D}`),H(),_;if(t===s&&_.commits_tracked_to_be_added.length>0){const e=_.commits_tracked_to_be_added.length,t=Math.ceil(e/r);x.info(`adding ${e} commits to gcal in ${t} batches of ${r}`);for(let i=0;i<e;i+=r){const n=L(o,_.commits_tracked_to_be_added.slice(i,i+r));_.commits_added.push(...n),x.info(`batch ${Math.floor(i/r)+1}/${t}: added ${n.length} commits`),i+r<e&&Utilities.sleep(a)}x.info("[DEBUG][ADD] finished adding commits, resetting properties"),H()}return _}({currentGithubSyncIndex:l,githubCalendar:g,githubGcalCommits:c.githubGcalCommits,filteredRepos:_,parseCommitEmojis:e[b].commits_configs.parse_commit_emojis})),function({githubGcalCommits:e,githubCalendar:t,currentGithubSyncIndex:o,filteredRepos:i}){x.info("[DEBUG][DEL] starting syncGithubCommitsToDelete"),x.info(`[DEBUG][DEL] gcalCommits: ${e.length}, filteredRepos: ${i.length}`);const n={commits_deleted:[],commits_tracked_to_be_deleted:[]};let r=0,a={noSameRepo:0,noDateMatch:0,noMessageMatch:0};e.forEach((e=>{var t;const o=null===(t=e.extendedProperties)||void 0===t?void 0:t.private;if(!o)return void x.info(`[DEBUG][DEL] skipping gcal item without private properties: ${e.id}`);const s=i.filter((e=>e.repository===o.repository));0===s.length&&a.noSameRepo++;s.find((e=>{const t=e.commitDate===o.commitDate,i=N(e.commitMessage)===N(o.commitMessage||"");return!t&&s.length>0&&a.noDateMatch++,t&&!i&&a.noMessageMatch++,t&&i}))?r++:n.commits_tracked_to_be_deleted.push(e)})),x.info(`[DEBUG][DEL] matched ${r}/${e.length} gcal commits still exist on github`),x.info(`[DEBUG][DEL] commits to delete: ${n.commits_tracked_to_be_deleted.length}`),x.info(`[DEBUG][DEL] no match reasons: noSameRepo=${a.noSameRepo}, noDateMatch=${a.noDateMatch}, noMessageMatch=${a.noMessageMatch}`),n.commits_tracked_to_be_deleted.length>0&&n.commits_tracked_to_be_deleted.length<=10&&(x.info("[DEBUG][DEL] commits to delete details:"),n.commits_tracked_to_be_deleted.forEach(((e,t)=>{var o,i,n;const s=null===(o=e.extendedProperties)||void 0===o?void 0:o.private;x.info(`[DEBUG][DEL] ${t+1}. ${null==s?void 0:s.repository} - ${null===(i=null==s?void 0:s.commitDate)||void 0===i?void 0:i.slice(0,10)} - ${null===(n=null==s?void 0:s.commitMessage)||void 0===n?void 0:n.slice(0,50)}`)})));const c=n.commits_tracked_to_be_deleted.map((e=>e.extendedProperties.private.commitId)),d=z(c);if(x.info(`[DEBUG][DEL] computed hash for ${c.length} commitIds: ${d.slice(0,16)}...`),1===o)return x.info(`storing hash for ${c.length} commits to track for deletion`),E("github_commits_tracked_to_be_deleted_hash",d),n;const m=S("github_commits_tracked_to_be_deleted_hash");if(x.info("[DEBUG][DEL] lastHash from storage: "+(m?m.slice(0,16)+"...":"null")),!m)return x.info("no stored delete hash found, resetting to step 1"),H(),E("github_commits_tracked_to_be_deleted_hash",d),n;if(x.info(`comparing delete hashes: stored=${m.slice(0,8)}... current=${d.slice(0,8)}...`),m!==d)return x.info("reset github commit properties due hash mismatch in deleted commits"),x.info(`[DEBUG][DEL] hash mismatch: stored=${m}, current=${d}`),H(),n;if(o===s&&n.commits_tracked_to_be_deleted.length>0){x.info(`deleting ${n.commits_tracked_to_be_deleted.length} commits on gcal`);for(let e=0;e<n.commits_tracked_to_be_deleted.length;e++){const o=n.commits_tracked_to_be_deleted[e];j(t,o),n.commits_deleted.push(o),e%50!=0&&e!==n.commits_tracked_to_be_deleted.length-1||x.info(`${e+1}/${n.commits_tracked_to_be_deleted.length} commits deleted from gcal`)}x.info("[DEBUG][DEL] finished deleting commits, resetting properties"),H()}return n}({currentGithubSyncIndex:l,githubCalendar:g,githubGcalCommits:c.githubGcalCommits,filteredRepos:_}));return 0===h.commits_tracked_to_be_added.length&&0===h.commits_tracked_to_be_deleted.length&&(x.info("reset github commit properties due found no commits tracked"),H()),h}function q(e){return"object"==typeof e&&null!==e}function Y(e,t){if(!q(e))return!1;for(const o in t){if(!(o in e))return x.error(`Missing key: ${o}`),!1;const i=typeof t[o],n=typeof e[o];if(q(t[o])){if(!q(e[o])||!Y(e[o],t[o]))return x.error(`Invalid nested structure or type mismatch at key: ${o}`),!1}else if(i!==n)return x.error(`Type mismatch at key: ${o}. Expected ${i}, found ${n}`),!1}return!0}function Z(e,t){return Y(e,t)}const W={settings:{sync_function:"",skip_mode:!1,timezone_offset_correction:0,update_frequency:4,per_day_emails:{time_to_send:"15:00",email_new_gcal_sync_release:!1,email_daily_summary:!1},per_sync_emails:{email_errors:!1,email_session:!1}}},V={username:"",commits_configs:{should_sync:!1,commits_calendar:"",ignored_repos:[],parse_commit_emojis:!1},personal_token:""};return class{constructor(t){if(this.extended_configs={timezone:"",timezone_offset:0,today_date:"",user_email:"",configs:{}},!function(e){if(!q(e))return!1;const t={basic:!0,github:!0,githubIgnoredRepos:!0};if(t.basic=Z(e,W),t.github=Z(e[b],V),"object"==typeof e[b]&&"ignored_repos"in e[b]&&Array.isArray(e[b].ignored_repos)){const o=e[b].ignored_repos.map((e=>"string"==typeof e));t.githubIgnoredRepos=o.every((e=>!0===e))}return Object.values(t).every((e=>!0===e))}(t))throw new Error(p);if("undefined"==typeof Calendar)throw new Error(f);const i=CalendarApp.getDefaultCalendar().getTimeZone();this.extended_configs.timezone=i,this.extended_configs.timezone_offset=function(e){const t=new Date,o=new Date(Date.UTC(t.getFullYear(),t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds())),i=new Date(t.toLocaleString("en-US",{timeZone:e}));return(Number(i)-Number(o))/36e5}(i)+-1*t.settings.timezone_offset_correction;const n=function(e){const t=new Date,o=new Intl.DateTimeFormat("en-CA",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).formatToParts(t),i=e=>o.find((t=>t.type===e)).value;return`${i("year")}-${i("month")}-${i("day")}T${i("hour")}:${i("minute")}:${i("second")}.000`}(i);this.extended_configs.today_date=n.split("T")[0],this.extended_configs.user_email=Session.getActiveUser().getEmail(),this.extended_configs.configs=t,x.info(`${e} is running at version ${o}!`)}createMissingGASProperties(){const e=PropertiesService.getScriptProperties().getProperties();Object.keys(u).forEach((t=>{Object.keys(e).includes(t)||E(u[t],h[t])}))}createMissingGcalCalendars(){const{shouldSyncGithub:e}=k(this.extended_configs);(e=>{let t=!1;x.info(`checking calendars to create: ${JSON.stringify(e)}`),e.forEach((e=>{const o=O(e);x.info(`calendar "${e}" exists: ${!!o}`),o||(I(e),x.info(`created google calendar: [${e}]`),t=!0)})),t&&Utilities.sleep(2e3)})([...new Set([].concat(e?[this.extended_configs.configs[b].commits_configs.commits_calendar]:[]))])}handleError(o){if(this.extended_configs.configs.settings.per_sync_emails.email_errors){const i="string"==typeof o?o:o instanceof Error?o.message:JSON.stringify(o);G({to:this.extended_configs.user_email,name:`${e}`,subject:`an error occurred - ${e}`,htmlBody:`Hi!\n <br/><br/>\n an error recently occurred: <br/><br/>\n <b>${i}</b>\n <br /><br />\n Regards,\n your <a href='https://github.com/${t}'>${e}</a> bot\n `})}else x.error(o)}getSessionLogs(){return x.logs}getGithubCommits(){const e=R(this.extended_configs.configs[b].username,this.extended_configs.configs[b].personal_token);return F(this.extended_configs.configs,e)}install(){var t,o;w(this.extended_configs.configs.settings.sync_function),t=this.extended_configs.configs.settings.sync_function,o=this.extended_configs.configs.settings.update_frequency,ScriptApp.newTrigger(t).timeBased().everyMinutes(o).create(),this.createMissingGASProperties(),x.info(`${e} was set to run function "${this.extended_configs.configs.settings.sync_function}" every ${this.extended_configs.configs.settings.update_frequency} minutes`)}uninstall(){w(this.extended_configs.configs.settings.sync_function),Object.keys(u).forEach((e=>{var t;t=u[e],PropertiesService.getScriptProperties().deleteProperty(t)})),x.info(`${e} automation was removed from appscript!`)}sync(){if(this.extended_configs.configs.settings.skip_mode)return x.info("skip_mode is set to true, skipping sync"),{};const{shouldSyncGithub:e}=k(this.extended_configs);if(!e)return x.info("nothing to sync"),{};this.createMissingGcalCalendars(),this.createMissingGASProperties();const t=Object.assign(Object.assign({},{commits_added:[],commits_deleted:[],commits_tracked_to_be_added:[],commits_tracked_to_be_deleted:[]}),e&&J(this.extended_configs.configs));return C(this.extended_configs,t)}}}));