UNPKG

gcal-sync

Version:

๐Ÿ”„ add an one way synchronization from github commits to google calendar and track your progress effortlessly.

2 lines (1 loc) โ€ข 19.4 kB
!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",i="2.0.0";function o(e,t){const i=e.filter((e=>!t.includes(e))),o=t.filter((t=>!e.includes(t)));return i.concat(o)}const n=(e,t,i)=>e.reduce(((e,o)=>{const n=o[t],s=o[i];return e[n]=s,e}),{});var s;const r=2500,a=3,c="object"==typeof process&&(null===(s=null===process||void 0===process?void 0:process.env)||void 0===s?void 0:s.NODE_ENV),d=[{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",initial_value:[]},{key:"github_commits_tracked_to_be_deleted",initial_value:[]},{key:"github_commit_changes_count",initial_value:""}],m=n(d,"key","initial_value"),l=n(d,"key","key"),_="schema invalid",u="This method cannot run in non-production environments",g="You provided an invalid github token",p="You provided an invalid github username",h="github_sync";const f={tableStyle:'style="border: 1px solid #333; width: 90%"',tableRowStyle:'style="width: 100%"',tableRowColumnStyle:'style="border: 1px solid #333"'},b=e=>"date"in e?e.date:e.dateTime;function y(e){return e.commits_added.length+e.commits_deleted.length}function v(i){let o="";return o=`Hi!<br/><br/>there were ${y(i)} changes made to your google calendar:<br/>\n`,o+=function(e){const t=e.commits_added,i=e.commits_deleted,o=e=>0===e.length?"":`${e.map((e=>{const{repositoryLink:t,commitMessage:i,repositoryName:o}=e.extendedProperties.private,n=[b(e.start).split("T")[0],`<a href="${t}">${o}</a>`,`<a href="${e.htmlLink}">${i}</a>`].map((e=>`<td ${f.tableRowColumnStyle}>&nbsp;&nbsp;${e}</td>`)).join("\n");return`<tr ${f.tableRowStyle}">\n${n}\n</tr>`})).join("\n")}`,n=`<tr ${f.tableRowStyle}">\n<th ${f.tableRowColumnStyle} width="80px">date</th><th ${f.tableRowColumnStyle} width="130px">repository</th><th ${f.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 ${f.tableStyle}>\n${n}\n${o(t)}\n</table>\n</center>\n`:"",s+=i.length>0?`<br/>removed commits events : ${i.length}<br/><br/> \n <center>\n<table ${f.tableStyle}>\n${n}\n${o(i)}\n</table>\n</center>\n`:"",s}(i),o+=`<br/>Regards,<br/>your <a href='https://github.com/${t}'>${e}</a> bot`,o}function $(e){return{shouldSyncGithub:e.configs[h].commits_configs.should_sync}}function k(e){const t=PropertiesService.getScriptProperties().getProperty(e);let i;try{i=JSON.parse(t)}catch(e){i=t}return i}function w(e,t){const i="string"==typeof t?t:JSON.stringify(t);PropertiesService.getScriptProperties().setProperty(e,i)}function x(e){const t=ScriptApp.getProjectTriggers().find((t=>t.getHandlerFunction()===e));t&&ScriptApp.deleteTrigger(t)}function S(e){MailApp.sendEmail(e)}const C=new class{constructor(){this.logs=[]}info(e,...t){c||(console.log(e,...t),this.logs.push(e))}error(e,...t){c||(console.error(e,...t),this.logs.push(e))}};function P(e,t){const i=function(e){const t=new Date;return t.setHours(t.getHours()+e),t}(t),o=60*Number(i.getHours())+Number(i.getMinutes()),n=e.split(":");return o>=60*Number(n[0])+Number(n[1])}function D(o,n){const{shouldSyncGithub:s}=$(o),r=n.commits_added.length+n.commits_deleted.length;if(s&&r>0){const e=k(l.today_github_added_commits),t=k(l.today_github_deleted_commits);w(l.today_github_added_commits,[...e,...n.commits_added]),w(l.today_github_deleted_commits,[...t,...n.commits_deleted]),C.info(`added ${r} new github items to today's stats`)}!function(o,n,s){var r;const a=o.user_email;if(o.configs.settings.per_sync_emails.email_session&&s>0){S(function(t,i){const o=v(i);return{to:t,name:`${e}`,subject:`session report - ${y(i)} modifications - ${e}`,htmlBody:o}}(a,n))}const c=P(o.configs.settings.per_day_emails.time_to_send,o.timezone_offset),d=o.today_date===k(l.last_daily_email_sent_date);if(c&&o.configs.settings.per_day_emails.email_daily_summary&&!d){w(l.last_daily_email_sent_date,o.today_date);S(function(t,i,o){const n=v(i);return{to:t,name:`${e}`,subject:`daily report for ${o} - ${y(i)} modifications - ${e}`,htmlBody:n}}(a,{commits_added:k(l.today_github_added_commits),commits_deleted:k(l.today_github_deleted_commits)},o.today_date)),w(l.today_github_added_commits,[]),w(l.today_github_deleted_commits,[]),C.info("today stats were reseted!")}const m=o.today_date===k(l.last_released_version_sent_date),_=e=>Number(e.replace("v","").split(".").join("")),u=()=>{var e;const o=UrlFetchApp.fetch(`https://api.github.com/repos/${t}/releases?per_page=1`);return null!==(e=JSON.parse(o.getContentText())[0])&&void 0!==e?e:{tag_name:i}};if(c&&o.configs.settings.per_day_emails.email_new_gcal_sync_release&&!m){w(l.last_released_version_sent_date,o.today_date);const n=u(),s=_(n.tag_name),c=_(i),d=null!==(r=k(l.last_released_version_alerted))&&void 0!==r?r:"";if(s>c&&s.toString()!=d){S(function(i,o){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: ${o.tag_name}</li>\n <li>published at: ${o.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: ${o.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:i,name:`${e}`,subject:`new version [${o.tag_name}] was released - ${e}`,htmlBody:n}}(a,n)),w(l.last_released_version_alerted,s.toString())}}}(o,n,r);const{commits_added:a,commits_deleted:c,commits_tracked_to_be_added:d,commits_tracked_to_be_deleted:m}=n;return{commits_added:a.length,commits_deleted:c.length,commits_tracked_to_be_added:d.length,commits_tracked_to_be_deleted:m.length}}function j(e,t){var i;const o=[];let n=1,s=!1;for(;!1===s;){const r=`https://api.github.com/search/commits?q=author:${e}&page=${n}&sort=committer-date&per_page=100`;let a;a=""!==t?UrlFetchApp.fetch(r,{muteHttpExceptions:!0,headers:{Authorization:`Bearer ${t}`}}):UrlFetchApp.fetch(r,{muteHttpExceptions:!0});const c=null!==(i=JSON.parse(a.getContentText()))&&void 0!==i?i:{};if(200!==a.getResponseCode()){if("Validation Failed"===c.message)throw new Error(p);if("Bad credentials"===c.message)throw new Error(g);throw new Error(c.message)}const d=c.items;if(0===d.length){s=!0;break}if(o.push(...d),n++,n>10){s=!0;break}}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 E(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 i=e;for(const[e,o]of Object.entries(t))i=i.replace(e,o);return i}const N=()=>{var e;return null!==(e=Calendar.CalendarList.list({showHidden:!0}).items)&&void 0!==e?e:[]},G=e=>N().find((t=>t.summary===e)),M=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 i=t.newCalendar();i.summary=e,i.timeZone=t.Settings.get("timezone").value;return t.Calendars.insert(i)};function R(e){return N().find((t=>t.summary===e))}function T(e){const t=e.reduce(((e,t)=>{const i=function(e){const t=Calendar.Events.list(e.id,{maxResults:r}).items.map((e=>function(e){var t,i,o,n,s;return{id:e.id,summary:e.summary,description:null!==(t=e.description)&&void 0!==t?t:"",htmlLink:e.htmlLink,attendees:null!==(i=e.attendees)&&void 0!==i?i:[],reminders:null!==(o=e.reminders)&&void 0!==o?o:{},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 t}(R(t));return[...e,...i]}),[]);return t}function I(e,t){return Calendar.Events.insert(t,e.id)}function A(e,t){Calendar.Events.remove(e.id,t.id)}function O(){w("github_commit_changes_count","0"),w("github_commits_tracked_to_be_added",[]),w("github_commits_tracked_to_be_deleted",[])}function z(e,t){return t.sort(((e,t)=>Number(new Date(t.commitDate))-Number(new Date(e.commitDate)))).filter((t=>t.repository.includes(e[h].username))).filter((t=>!1===e[h].commits_configs.ignored_repos.includes(t.repositoryName)))}function H(e){C.info("syncing github commits");const t={githubCommits:j(e[h].username,e[h].personal_token),githubGcalCommits:T([e[h].commits_configs.commits_calendar])},i=k("github_commit_changes_count"),n=Number(i)+1;null===i&&O(),w("github_commit_changes_count",n.toString()),1===n?C.info(`checking commit changes: ${n}/${a}`):n>1&&n<a?C.info(`confirming commit changes: ${n}/${a}`):n===a&&C.info(`making commit changes if succeed: ${n}/${a}`);const s=z(e,t.githubCommits),r=R(e[h].commits_configs.commits_calendar),c=Object.assign(Object.assign({},function({filteredRepos:e,currentGithubSyncIndex:t,githubCalendar:i,githubGcalCommits:n,parseCommitEmojis:s}){const r={commits_tracked_to_be_added:[],commits_added:[]};for(const t of e){if(!n.filter((e=>e.extendedProperties.private.repository===t.repository)).find((e=>e.extendedProperties.private.commitDate===t.commitDate&&E(e.extendedProperties.private.commitMessage)===E(t.commitMessage)))){const e=s?E(t.commitMessage):t.commitMessage,i={private:{commitMessage:e,commitDate:t.commitDate,repository:t.repository,repositoryName:t.repositoryName,repositoryLink:t.repositoryLink,commitId:t.commitId}},o={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:i};r.commits_tracked_to_be_added.push(o)}}if(1===t)return w("github_commits_tracked_to_be_added",r.commits_tracked_to_be_added.map((e=>e))),r;const c=k("github_commits_tracked_to_be_added").map((e=>e.extendedProperties.private.commitId)),d=r.commits_tracked_to_be_added.map((e=>e.extendedProperties.private.commitId));if(o(c,d).length>0)return C.info("reset github commit properties due differences in added commits"),O(),r;if(t===a&&r.commits_tracked_to_be_added.length>0){C.info(`adding ${r.commits_tracked_to_be_added.length} commits to gcal`);for(let e=0;e<r.commits_tracked_to_be_added.length;e++)try{const t=r.commits_tracked_to_be_added[e],o=I(i,t);r.commits_added.push(o),C.info(`${e+1}/${r.commits_tracked_to_be_added.length} add new commit to gcal: ${t.extendedProperties.private.commitDate} - ${o.extendedProperties.private.repositoryName} - ${o.extendedProperties.private.commitMessage}`)}catch(e){throw new Error(e.message)}finally{O()}}return r}({currentGithubSyncIndex:n,githubCalendar:r,githubGcalCommits:t.githubGcalCommits,filteredRepos:s,parseCommitEmojis:e[h].commits_configs.parse_commit_emojis})),function({githubGcalCommits:e,githubCalendar:t,currentGithubSyncIndex:i,filteredRepos:n}){const s={commits_deleted:[],commits_tracked_to_be_deleted:[]};if(e.forEach((e=>{const t=e.extendedProperties.private;n.filter((e=>e.repository===t.repository)).find((e=>e.commitDate===t.commitDate&&E(e.commitMessage)===E(t.commitMessage)))||s.commits_tracked_to_be_deleted.push(e)})),1===i)return w("github_commits_tracked_to_be_deleted",s.commits_tracked_to_be_deleted),s;const r=k("github_commits_tracked_to_be_deleted").map((e=>e.extendedProperties.private.commitId)),c=s.commits_tracked_to_be_deleted.map((e=>e.extendedProperties.private.commitId));if(o(r,c).length>0)return C.info("reset github commit properties due differences in deleted commits"),O(),s;if(i===a&&s.commits_tracked_to_be_deleted.length>0){C.info(`deleting ${s.commits_tracked_to_be_deleted.length} commits on gcal`);for(let e=0;e<s.commits_tracked_to_be_deleted.length;e++)try{const i=s.commits_tracked_to_be_deleted[e];A(t,i),s.commits_deleted.push(i),C.info(`${e+1}/${s.commits_tracked_to_be_deleted.length} deleted commit on gcal: ${i.extendedProperties.private.commitDate} - ${i.extendedProperties.private.repositoryName} - ${i.extendedProperties.private.commitMessage}`)}catch(e){throw new Error(e.message)}finally{O()}}return s}({currentGithubSyncIndex:n,githubCalendar:r,githubGcalCommits:t.githubGcalCommits,filteredRepos:s}));return 0===c.commits_tracked_to_be_added.length&&0===c.commits_tracked_to_be_deleted.length&&(C.info("reset github commit properties due found no commits tracked"),O()),c}function L(e){return"object"==typeof e&&null!==e}function U(e,t){if(!L(e))return!1;for(const i in t){if(!(i in e))return C.error(`Missing key: ${i}`),!1;const o=typeof t[i],n=typeof e[i];if(L(t[i])){if(!L(e[i])||!U(e[i],t[i]))return C.error(`Invalid nested structure or type mismatch at key: ${i}`),!1}else if(o!==n)return C.error(`Type mismatch at key: ${i}. Expected ${o}, found ${n}`),!1}return!0}function F(e,t){return U(e,t)}const B={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}}},J={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(!L(e))return!1;const t={basic:!0,github:!0,githubIgnoredRepos:!0};if(t.basic=F(e,B),t.github=F(e[h],J),"object"==typeof e[h]&&"ignored_repos"in e[h]&&Array.isArray(e[h].ignored_repos)){const i=e[h].ignored_repos.map((e=>"string"==typeof e));t.githubIgnoredRepos=i.every((e=>!0===e))}return Object.values(t).every((e=>!0===e))}(t))throw new Error(_);if("undefined"==typeof Calendar)throw new Error(u);const o=CalendarApp.getDefaultCalendar().getTimeZone();this.extended_configs.timezone=o,this.extended_configs.timezone_offset=function(e){const t=new Date,i=new Date(Date.UTC(t.getFullYear(),t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds())),o=new Date(t.toLocaleString("en-US",{timeZone:e}));return(Number(o)-Number(i))/36e5}(o)+-1*t.settings.timezone_offset_correction;const n=function(e){const t=new Date,i=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),o=e=>i.find((t=>t.type===e)).value;return`${o("year")}-${o("month")}-${o("day")}T${o("hour")}:${o("minute")}:${o("second")}.000`}(o);this.extended_configs.today_date=n.split("T")[0],this.extended_configs.user_email=Session.getActiveUser().getEmail(),this.extended_configs.configs=t,C.info(`${e} is running at version ${i}!`)}createMissingGASProperties(){const e=PropertiesService.getScriptProperties().getProperties();Object.keys(l).forEach((t=>{Object.keys(e).includes(t)||w(l[t],m[t])}))}createMissingGcalCalendars(){const{shouldSyncGithub:e}=$(this.extended_configs);(e=>{let t=!1;e.forEach((e=>{G(e)||(M(e),C.info(`created google calendar: [${e}]`),t=!0)})),t&&Utilities.sleep(2e3)})([...new Set([].concat(e?[this.extended_configs.configs[h].commits_configs.commits_calendar]:[]))])}handleError(i){if(this.extended_configs.configs.settings.per_sync_emails.email_errors){const o="string"==typeof i?i:i instanceof Error?i.message:JSON.stringify(i);S({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>${o}</b>\n <br /><br />\n Regards,\n your <a href='https://github.com/${t}'>${e}</a> bot\n `})}else C.error(i)}getSessionLogs(){return C.logs}getGithubCommits(){const e=j(this.extended_configs.configs[h].username,this.extended_configs.configs[h].personal_token);return z(this.extended_configs.configs,e)}install(){var t,i;x(this.extended_configs.configs.settings.sync_function),t=this.extended_configs.configs.settings.sync_function,i=this.extended_configs.configs.settings.update_frequency,ScriptApp.newTrigger(t).timeBased().everyMinutes(i).create(),this.createMissingGASProperties(),C.info(`${e} was set to run function "${this.extended_configs.configs.settings.sync_function}" every ${this.extended_configs.configs.settings.update_frequency} minutes`)}uninstall(){x(this.extended_configs.configs.settings.sync_function),Object.keys(l).forEach((e=>{var t;t=l[e],PropertiesService.getScriptProperties().deleteProperty(t)})),C.info(`${e} automation was removed from appscript!`)}sync(){if(this.extended_configs.configs.settings.skip_mode)return C.info("skip_mode is set to true, skipping sync"),{};const{shouldSyncGithub:e}=$(this.extended_configs);if(!e)return C.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&&H(this.extended_configs.configs));return D(this.extended_configs,t)}}}));