├── BigQuery ├── README.md └── webhook │ ├── README.md │ └── code.gs ├── Classroom ├── README.md └── Record Attendance │ └── Code.gs ├── Firebase ├── Authentication │ ├── Index.html │ └── code.gs ├── Dynamic Links │ └── newLink │ │ └── urlShortenerFB.gs └── README.md ├── Ghost ├── README.md └── Subscribers │ ├── README.md │ └── code.gs ├── Gmail ├── Gmail API │ └── Code.gs └── README.md ├── Google Chat ├── Chat App │ ├── README.md │ └── Twitter │ │ ├── App.gs │ │ ├── Cards.gs │ │ ├── Code.gs │ │ ├── README.md │ │ ├── Twitter.gs │ │ └── appsscript.json └── README.md ├── Hangouts Chat ├── Bot │ ├── README.md │ └── getThreadID.gs └── README.md ├── LICENSE ├── Library ├── Exotel │ ├── ExoAPI.gs │ ├── README.md │ └── sample │ │ ├── callDetails.gs │ │ └── connect2Num.gs └── README.md ├── Login Dashboard ├── Dashboard.html ├── Login.html ├── README.md └── code.gs ├── Materialize CSS ├── Autocomplete │ ├── Code.gs │ ├── Index.html │ └── README.md └── README.md ├── Meeting Reminder └── Code.gs ├── Metrics (GAS) ├── Code.gs ├── README.md ├── appscript.json └── md5.gs ├── README.md ├── Random ├── Employee certificate │ └── code.gs ├── Icons │ ├── ACCEPTED.png │ ├── AUDIO.png │ ├── CANCELLED.png │ ├── CHAT.png │ ├── DRAFT.png │ ├── DRAWING.png │ ├── FILE.png │ ├── FOLDER.png │ ├── IMAGE.png │ ├── IMPORTANT.png │ ├── INBOX.png │ ├── README.md │ ├── SCRIPT.png │ ├── SELF.png │ ├── SENT.png │ ├── SHARED.png │ ├── SITES.png │ ├── SPAM.png │ ├── STARRED.png │ ├── TENTATIVE.png │ ├── TRASH.png │ ├── TWITTER.png │ ├── UNREAD.png │ └── VIDEO.png ├── Meetings Heatmap │ ├── Chart.html │ ├── Index.html │ ├── Processing.html │ ├── README.md │ └── code.gs ├── NewMonthNewSheet.gs ├── README.md └── Revue <> Ghost │ ├── Config.gs │ ├── Ghost.gs │ └── Revue.gs ├── Real-time Dashboard ├── Index.html ├── README.md └── code.gs ├── Rebrandly ├── README.md └── newLink │ └── urlShortenerRB.gs ├── Sheets ├── Custom Functions │ └── GET_REDIRECT_LOCATION.gs ├── Find Precedents │ └── Code.gs ├── JsDoc2JSON │ └── Code.gs ├── Project BoldX │ ├── Code.gs │ └── Index.html ├── README.md └── Webhooks │ ├── GET.gs │ ├── POST.gs │ └── appsscript.json ├── Twilio ├── Authy │ ├── Code.gs │ ├── Dashboard.html │ ├── Index.html │ └── README.md └── README.md └── Workspace Add-on └── More than 100 widgets ├── Data.gs ├── UI.gs └── appsscript.json /BigQuery/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [BigQuery](https://cloud.google.com/bigquery/) 2 | 3 | - [webhook](webhook/): This script uses a doGet() function as a webhook to capture data and store it in your BigQuery table 4 | -------------------------------------------------------------------------------- /BigQuery/webhook/README.md: -------------------------------------------------------------------------------- 1 | # Store data directly on BigQuery using Apps Script 2 | 3 | The [official reference guide](https://developers.google.com/apps-script/advanced/bigquery) doesn't talk of this but the following gave me a hard time to crack down 4 | 5 | ```javascript 6 | 'useLegacySql': false 7 | ``` 8 | -------------------------------------------------------------------------------- /BigQuery/webhook/code.gs: -------------------------------------------------------------------------------- 1 | var projectId = 'XXXXXXXX'; 2 | var datasetId = 'YYYYYYYYY'; 3 | var tableId = 'ZZZZZZZZ'; 4 | 5 | function doGet(e) { 6 | var params = JSON.stringify(e.parameters); 7 | var jsonMapping = JSON.parse(params) 8 | var param1 = jsonMapping["param1"][0] 9 | var param2 = jsonMapping["param2"][0] 10 | var request = { 11 | 'query': "INSERT INTO `" + projectId + "."+ datasetId + "." + tableId + "` VALUES ('" + param1 + "','" + param2 + "')", 12 | 'useLegacySql': false 13 | } 14 | var queryResults = BigQuery.Jobs.query(request, projectId); 15 | var jobId = queryResults.jobReference.jobId; 16 | return ContentService.createTextOutput('Successful') 17 | } 18 | -------------------------------------------------------------------------------- /Classroom/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Google Classroom](https://edu.google.com/products/classroom/) 2 | -------------------------------------------------------------------------------- /Classroom/Record Attendance/Code.gs: -------------------------------------------------------------------------------- 1 | var courseId = 'XXXXXXXXXXXX'; // https://developers.google.com/classroom/reference/rest/v1/courses/list 2 | var topicID = 'YYYYYYYYYYY'; // https://developers.google.com/classroom/reference/rest/v1/courses.topics/list 3 | 4 | var startDate = new Date(); // new Date("dd-MMM-yyyy") 5 | var scheduleForDays = 5; // Number of days to schedule the attendace from 'startDate' 6 | 7 | var scheduledTimeHour = 9; // the number 9 (integer value) for 9 AM 8 | var scheduledTimeMinutes = 0; // the number 0 (integer value) for exactly at the 'scheduledTimeHour' 9 | var dueByHour = 15; // 24-hour; the number 15 (integer value) for 3 PM 10 | var dueByMinutes = 30; // the number 30 (integer value) for the 30th minute from 'dueByHour' 11 | 12 | function scheduleAttendance() { 13 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 14 | var questionDate = startDate; 15 | for (var i = 0; i < scheduleForDays; i++) { 16 | var newDate = new Date(questionDate.getTime() + MILLIS_PER_DAY); 17 | createQuestion(questionDate); 18 | questionDate = newDate; 19 | } 20 | } 21 | 22 | function createQuestion(date) { 23 | var title = "Attendance for " + Utilities.formatDate(date, Session.getScriptTimeZone(), "dd-MMMM-yyyy"); 24 | var scheduledTime = Utilities.formatDate(new Date(date.getFullYear(), date.getMonth(), date.getDate(), scheduledTimeHour, scheduledTimeMinutes, 0), "UTC", "yyyy-MM-dd'T'HH:mm:ss'Z'"); 25 | var dueByTime = Utilities.formatDate(new Date(date.getFullYear(), date.getMonth(), date.getDate(), dueByHour, dueByMinutes, 0), "UTC", "yyyy-MM-dd HH:mm:ss"); 26 | var payload = { 27 | "workType": "MULTIPLE_CHOICE_QUESTION", 28 | "multipleChoiceQuestion": { 29 | "choices": [ 30 | "Yes" 31 | ] 32 | }, 33 | "title": title, 34 | "description": "Are you working online in Google Classroom?", 35 | "scheduledTime": scheduledTime, 36 | "topicId": topicID, 37 | "dueDate": { 38 | "day": date.getDate(), 39 | "month": date.getMonth() + 1, 40 | "year": date.getFullYear() 41 | }, 42 | "dueTime": { 43 | "hours": new Date(dueByTime).getHours(), 44 | "minutes": new Date(dueByTime).getMinutes(), 45 | "seconds": 0 46 | } 47 | }; 48 | Classroom.Courses.CourseWork.create(payload, courseId); // https://developers.google.com/classroom/reference/rest/v1/courses.courseWork/create 49 | } 50 | -------------------------------------------------------------------------------- /Firebase/Authentication/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 26 | 44 | 69 | 70 | 71 | 72 |
73 |

Firebase and Google Apps Script


Login using your Google account via Firebase Auth



74 |
75 |
76 |
77 |

 78 |             
79 |
80 | 84 | 85 | 96 | 97 |
98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Firebase/Authentication/code.gs: -------------------------------------------------------------------------------- 1 | function doGet(e) { 2 | return HtmlService.createHtmlOutputFromFile('Index') 3 | .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) 4 | .setTitle('FirebaseUI | Firebase Authentication'); 5 | } 6 | 7 | function webAppUrl() { 8 | return ScriptApp.getService().getUrl(); 9 | } 10 | -------------------------------------------------------------------------------- /Firebase/Dynamic Links/newLink/urlShortenerFB.gs: -------------------------------------------------------------------------------- 1 | function URLShortener() { 2 | var body = { 3 | "dynamicLinkInfo": { 4 | "domainUriPrefix": "https://example.page.link", 5 | "link": "https://example.com/?utm_source=email&utm_medium=mobile&utm_content=1234567890" 6 | }, 7 | "suffix": { 8 | "option": "SHORT" 9 | } 10 | }; 11 | var key = 'XXXXXXXXXXXXXXXXXXXXXXX' 12 | var url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=" + key; 13 | var options = { 14 | 'method': 'POST', 15 | "contentType": "application/json", 16 | 'payload': JSON.stringify(body), 17 | }; 18 | var response = UrlFetchApp.fetch(url, options); 19 | var json = response.getContentText(); 20 | var data = JSON.parse(json); 21 | var obj = data["shortLink"]; 22 | Logger.log(obj) 23 | } 24 | -------------------------------------------------------------------------------- /Firebase/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Firebase Dynamic Links](https://firebase.google.com/docs/reference/dynamic-links/link-shortener) 2 | 3 | - [newLink](Dynamic%20Links/newLink/): This script allows you to create a new short link with the help of Firebase Dynamic Links Short Links API 4 | -------------------------------------------------------------------------------- /Ghost/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Ghost.org](https://ghost.org/) 2 | 3 | - [Subscribers](Subscribers/code.gs): [How does the Subscribers feature work?](https://ghost.org/faq/enable-subscribers-feature/) 4 | -------------------------------------------------------------------------------- /Ghost/Subscribers/README.md: -------------------------------------------------------------------------------- 1 | ## Manage email subscriber list on a spreadsheet 2 | 3 | This script handles both - folks who subscribe and unsubscribe. 4 | 5 | An additional identifier is introduced as `state` in the last column where - 6 | - TRUE denotes that its a subscriber 7 | - FALSE denotes that its an unsubscriber 8 | -------------------------------------------------------------------------------- /Ghost/Subscribers/code.gs: -------------------------------------------------------------------------------- 1 | /**//* ======================================================== *//**/ 2 | /**/ /**/ 3 | /**/ // Make changes only to this segment /**/ 4 | /**/ /**/ 5 | /**/ var sheetID = "Enter-Your-Sheet-ID"; /**/ 6 | /**/ var timeZone = "IST"; /**/ 7 | /**/ /**/ 8 | /**//* ======================================================== *//**/ 9 | 10 | 11 | /* ==================== DO NOT CHANGE ANYTHING BELOW THIS LINE ======================== */ 12 | 13 | 14 | var ss = SpreadsheetApp.openById(sheetID); 15 | var subscribers = "subscribers" 16 | var sheetName; 17 | 18 | function doPost(e) { 19 | if (JSON.parse(e.postData.contents).subscriber.previous.id == undefined) { 20 | return addSubscriber(e) 21 | } else if (JSON.parse(e.postData.contents).subscriber.current.id == undefined) { 22 | return deleteSubscriber(e) 23 | } 24 | } 25 | 26 | function addSubscriber(e) { 27 | var params = JSON.parse(e.postData.contents).subscriber.current 28 | var id = params.id 29 | var email = params.email 30 | var status = params.status 31 | var subscribed_url = params.subscribed_url 32 | var subscribed_referrer = params.subscribed_referrer 33 | var created_at = params.created_at 34 | created_at = Utilities.formatDate(new Date(created_at), timeZone, "dd MMM, yyyy HH:mm:ss"); 35 | var updated_at = params.updated_at 36 | updated_at = Utilities.formatDate(new Date(updated_at), timeZone, "dd MMM, yyyy HH:mm:ss"); 37 | var name = params.name 38 | var post_id = params.post_id 39 | var unsubscribed_url = params.unsubscribed_url 40 | var unsubscribed_at = params.unsubscribed_at 41 | if (unsubscribed_at == null) { 42 | unsubscribed_at = null 43 | } else { 44 | unsubscribed_at = Utilities.formatDate(new Date(unsubscribed_at), timeZone, "dd MMM, yyyy HH:mm:ss"); 45 | } 46 | var state = true 47 | sheetName = subscribers; 48 | var activeSheet = ss.getSheetByName(sheetName); 49 | if (activeSheet == null) { 50 | activeSheet = ss.insertSheet().setName(sheetName); 51 | activeSheet.appendRow ( 52 | [ 53 | "id", 54 | "email", 55 | "status", 56 | "subscribed_url", 57 | "subscribed_referrer", 58 | "created_at", 59 | "updated_at", 60 | "name", 61 | "post_id", 62 | "unsubscribed_url", 63 | "unsubscribed_at", 64 | "state" 65 | ] 66 | ) 67 | activeSheet.setFrozenRows(1) 68 | activeSheet.appendRow ( 69 | [ 70 | id, 71 | email, 72 | status, 73 | subscribed_url, 74 | subscribed_referrer, 75 | created_at, 76 | updated_at, 77 | name, 78 | post_id, 79 | unsubscribed_url, 80 | unsubscribed_at, 81 | state 82 | ] 83 | ) 84 | removeEmptyColumns(sheetName); 85 | ss.deleteSheet(ss.getSheetByName('Sheet1')) 86 | } else { 87 | activeSheet.appendRow ( 88 | [ 89 | id, 90 | email, 91 | status, 92 | subscribed_url, 93 | subscribed_referrer, 94 | created_at, 95 | updated_at, 96 | name, 97 | post_id, 98 | unsubscribed_url, 99 | unsubscribed_at, 100 | state 101 | ] 102 | ) 103 | } 104 | removeDuplicateRows(sheetName) 105 | return ContentService.createTextOutput('"addSubscriber":"Successful"') 106 | } 107 | 108 | function deleteSubscriber(e) { 109 | var params = JSON.parse(e.postData.contents).subscriber.previous 110 | var id = params.id 111 | var email = params.email 112 | var status = params.status 113 | var subscribed_url = params.subscribed_url 114 | var subscribed_referrer = params.subscribed_referrer 115 | var created_at = params.created_at 116 | created_at = Utilities.formatDate(new Date(created_at), timeZone, "dd MMM, yyyy HH:mm:ss"); 117 | var updated_at = params.updated_at 118 | updated_at = Utilities.formatDate(new Date(updated_at), timeZone, "dd MMM, yyyy HH:mm:ss"); 119 | var name = params.name 120 | var post_id = params.post_id 121 | var unsubscribed_url = params.unsubscribed_url 122 | var unsubscribed_at = params.unsubscribed_at 123 | if (unsubscribed_at == null) { 124 | unsubscribed_at = null 125 | } else { 126 | unsubscribed_at = Utilities.formatDate(new Date(unsubscribed_at), timeZone, "dd MMM, yyyy HH:mm:ss"); 127 | } 128 | sheetName = subscribers; 129 | var activeSheet = ss.getSheetByName(sheetName); 130 | if (activeSheet == null) { 131 | activeSheet = ss.insertSheet().setName(sheetName); 132 | activeSheet.appendRow ( 133 | [ 134 | "id", 135 | "email", 136 | "status", 137 | "subscribed_url", 138 | "subscribed_referrer", 139 | "created_at", 140 | "updated_at", 141 | "name", 142 | "post_id", 143 | "unsubscribed_url", 144 | "unsubscribed_at", 145 | "state" 146 | ] 147 | ) 148 | activeSheet.setFrozenRows(1) 149 | var state = false 150 | activeSheet.appendRow ( 151 | [ 152 | id, 153 | email, 154 | status, 155 | subscribed_url, 156 | subscribed_referrer, 157 | created_at, 158 | updated_at, 159 | name, 160 | post_id, 161 | unsubscribed_url, 162 | unsubscribed_at, 163 | state 164 | ] 165 | ) 166 | removeEmptyColumns(sheetName); 167 | ss.deleteSheet(ss.getSheetByName('Sheet1')) 168 | } else { 169 | var values = activeSheet.getDataRange().getValues(); 170 | var headers = values[0] 171 | var idIndex = headers.indexOf('id'); 172 | var stateIndex = headers.indexOf('state'); 173 | var statusIndex = headers.indexOf('status'); 174 | var updated_atIndex = headers.indexOf('updated_at'); 175 | var unsubscribed_urlIndex = headers.indexOf('unsubscribed_url'); 176 | var unsubscribed_atIndex = headers.indexOf('unsubscribed_at'); 177 | var sheetRow; 178 | var subscriberID = true 179 | for(var i=0, iLen=values.length; iThis iš suppösed to be in HTML 👩🏽‍💻` 15 | } 16 | }; 17 | 18 | const boundaryId = Utilities.getUuid(); 19 | // Email message as per RFC 2822 format 20 | const message = 21 | `From: =?UTF-8?B?${Utilities.base64Encode(input.from.name, Utilities.Charset.UTF_8)}?= <${input.from.email}>` + `\r\n` + 22 | `To: =?UTF-8?B?${Utilities.base64Encode(input.to.name, Utilities.Charset.UTF_8)}?= <${input.to.email}>` + `\r\n` + 23 | `Subject: =?UTF-8?B?${Utilities.base64Encode(input.subject, Utilities.Charset.UTF_8)}?=` + `\r\n` + 24 | `Content-Type: multipart/alternative; boundary=${boundaryId}` + `\r\n\r\n` + 25 | `--${boundaryId}` + `\r\n` + 26 | `Content-Type: text/plain; charset="UTF-8"` + `\r\n` + 27 | `Content-Transfer-Encoding: base64` + `\r\n\r\n` + 28 | `${Utilities.base64Encode(input.body.plainText, Utilities.Charset.UTF_8)}` + `\r\n\r\n` + 29 | `--${boundaryId}` + `\r\n` + 30 | `Content-Type: text/html; charset="UTF-8"` + `\r\n` + 31 | `Content-Transfer-Encoding: base64` + `\r\n\r\n` + 32 | `${Utilities.base64Encode(input.body.html, Utilities.Charset.UTF_8)}` + `\r\n\r\n` + 33 | `--${boundaryId}--`; 34 | 35 | const newMsg = Gmail.newMessage(); 36 | newMsg.raw = Utilities.base64EncodeWebSafe(message, Utilities.Charset.UTF_8); 37 | 38 | try { 39 | Gmail.Users.Messages.send(newMsg, "me"); 40 | console.log("Email sent."); 41 | } catch (error) { 42 | console.log("Error: " + error); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Gmail/README.md: -------------------------------------------------------------------------------- 1 | Snippets related to Gmail and Apps Script 2 | -------------------------------------------------------------------------------- /Google Chat/Chat App/README.md: -------------------------------------------------------------------------------- 1 | # Chat App 2 | -------------------------------------------------------------------------------- /Google Chat/Chat App/Twitter/App.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Responds to a MESSAGE event in Google Chat. 3 | * 4 | * @param {Object} event the event object from Google Chat 5 | */ 6 | function onMessage(event) { 7 | const uid = event.user.name.split("/")[1]; 8 | if (event.message.slashCommand && event.message.slashCommand.commandId === 6) { 9 | return helpCard(event.space.singleUserBotDm ? "NEW_MESSAGE" : "UPDATE_USER_MESSAGE_CARDS"); 10 | } 11 | 12 | if (!getService().hasAccess()) { 13 | return requestConfig(event.configCompleteRedirectUrl); 14 | } 15 | 16 | if (event.message.slashCommand) { 17 | switch (event.message.slashCommand.commandId) { 18 | case 1: // /twitter_connect 19 | if (!getService().hasAccess()) { 20 | return requestConfig(event.configCompleteRedirectUrl); 21 | } else { 22 | return connectCard(event.space.singleUserBotDm ? "NEW_MESSAGE" : "UPDATE_USER_MESSAGE_CARDS"); 23 | } 24 | case 2: // /twitter_me 25 | return meCard(uid, event, event.space.singleUserBotDm ? "NEW_MESSAGE" : "UPDATE_USER_MESSAGE_CARDS"); 26 | case 3: // /twitter_me_public 27 | return meCard(uid, event, "NEW_MESSAGE"); 28 | case 4: // /twitter_logout 29 | getService().reset(); 30 | return logoutCard(event.space.singleUserBotDm ? "NEW_MESSAGE" : "UPDATE_USER_MESSAGE_CARDS"); 31 | } 32 | } else if (event.message.matchedUrl) { 33 | const matchedUrl = event.message.matchedUrl.url.split("?")[0]; 34 | const matchedUrlEntities = checkMatchedUrl(matchedUrl); 35 | return matchedUrlEntities.entity === "USER" ? 36 | userCard(uid, matchedUrlEntities.username, event) : 37 | ( 38 | matchedUrlEntities.entity === "TWEET" ? 39 | tweetCard(uid, matchedUrlEntities.tweetId, event) : 40 | { text: "Invalid URL" } 41 | ); 42 | } else { 43 | const message = (event.space.singleUserBotDm ? "P" : "<" + event.user.name + ">, p") + "lease use our _slash commands_ by typing */twitter...* or check previews of tweets & users by sharing a direct link."; 44 | return { "text": message }; 45 | } 46 | } 47 | 48 | /** 49 | * Updates a card that was attached to a message with a previewed link. 50 | * 51 | * @param {Object} event The event object from Chat API. 52 | * @return {Object} Response from the Chat app. Either a new card attached to 53 | * the message with the previewed link, or an update to an existing card. 54 | */ 55 | function onCardClick(event) { 56 | const uid = event.user.name.split("/")[1]; 57 | if (!getService().hasAccess()) { 58 | return { 59 | text: (event.space.singleUserBotDm ? "T" : ", t") + 60 | "o take any action from the cards, please connect your Twitter account by running */twitter_connect*" + 61 | (event.space.singleUserBotDm ? "." : " or DMing the bot directly.") 62 | }; 63 | } 64 | let actionName = event.action.actionMethodName; 65 | 66 | switch (actionName) { 67 | case "LIKE_TWEET": { 68 | const tweetId = event.action.parameters.filter(params => params.key == "tweetId")[0].value; 69 | return likeTweet(uid, tweetId, event); 70 | }; 71 | case "FOLLOW_USER": { 72 | const userIdToFollow = event.action.parameters.filter(params => params.key == "userIdToFollow")[0].value; 73 | const userNameToFollow = event.action.parameters.filter(params => params.key == "userNameToFollow")[0].value; 74 | return followUser(uid, userIdToFollow, userNameToFollow, event); 75 | }; 76 | } 77 | } 78 | 79 | /** 80 | * Responds to an ADDED_TO_SPACE event in Google Chat. 81 | * 82 | * @param {Object} event the event object from Google Chat 83 | */ 84 | function onAddToSpace(event) { 85 | const message = (event.space.singleUserBotDm ? "T" : "<" + event.user.name + ">, t") + "hank you for adding me to " + (event.space.singleUserBotDm ? ("a DM, " + event.user.displayName) : (event.space.displayName ? event.space.displayName + '!' : "this chat!")) + 86 | "\n\n➡️ Next, connect your Twitter account by running the */twitter_connect* slash command.\nAnyone who wants to either *like* a tweet or *follow* another user will need to authorize their own Twitter account, which can be done either by running */twitter_connect* or DMing the bot directly.\n\n⚠️ Notes on connecting and authorization:\nYou will need to click on that *Configure* button *twice* during the auth process!\n— First would prepare your Google account to store your Twitter credentials — this way, no one else can access them\n\t— Needs to be done only once, unless you manually revoke the apps' permissions\n— And the second, would then allow you to connect & interact with your Twitter account"; 87 | 88 | return { "text": message }; 89 | } 90 | 91 | /** 92 | * Responds to a REMOVED_FROM_SPACE event in Google Chat. 93 | * 94 | * @param {Object} event the event object from Google Chat 95 | */ 96 | const onRemoveFromSpace = (event) => console.info("Bot removed from ", (event.space.name ? event.space.name : "this chat by " + event.user.email)); 97 | -------------------------------------------------------------------------------- /Google Chat/Chat App/Twitter/Cards.gs: -------------------------------------------------------------------------------- 1 | const tweetCard = (uid, tweetId, event) => { 2 | const tweetData = TWITTER_API.getTweet(uid, tweetId); 3 | if (!tweetData) { 4 | return requestConfig(event.configCompleteRedirectUrl); 5 | } else { 6 | const userData = tweetData.includes.users.filter(user => user.id == tweetData.data.author_id)[0]; 7 | const card = { 8 | "actionResponse": { 9 | "type": "UPDATE_USER_MESSAGE_CARDS" 10 | }, 11 | "cards": [ 12 | { 13 | "header": { 14 | "title": userData.name, 15 | "subtitle": `@${userData.username}`, 16 | "imageUrl": userData.profile_image_url.replace("_normal.jpg", "_400x400.jpg"), 17 | "imageStyle": "AVATAR", 18 | }, 19 | "sections": [ 20 | { 21 | "header": `Created at (GMT): ${Utilities.formatDate(new Date(tweetData.data.created_at), "GMT", "h:mm a · MMM d, yyyy")}`, 22 | "widgets": [ 23 | { 24 | "keyValue": { 25 | "content": tweetData.data.text, 26 | "contentMultiline": true 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | "widgets": [ 33 | { 34 | "keyValue": { 35 | "content": `Likes: ${tweetData.data.public_metrics.like_count}`, 36 | "bottomLabel": `RTs: ${tweetData.data.public_metrics.retweet_count} | QTs: ${tweetData.data.public_metrics.quote_count} | Replies: ${tweetData.data.public_metrics.reply_count}`, 37 | "icon": "STAR", 38 | "button": { 39 | "textButton": { 40 | "text": "LIKE", 41 | "onClick": { 42 | "action": { 43 | "actionMethodName": `LIKE_TWEET`, 44 | "parameters": [ 45 | { 46 | "key": "tweetId", 47 | "value": tweetId 48 | }, 49 | ] 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | ] 57 | }, 58 | { 59 | "widgets": [ 60 | { 61 | "keyValue": { 62 | "topLabel": `"Twitter is unable to process your request"?`, 63 | "content": "Connect your Twitter account by running /twitter_connect or DM the bot directly.", 64 | "contentMultiline": true, 65 | "iconUrl": "https://script.gs/content/images/2022/07/warning.png", 66 | } 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | ] 73 | }; 74 | 75 | if (tweetData.includes.media) { 76 | const cardImageUrl = tweetData.includes.media[0].type === "photo" ? tweetData.includes.media[0].url : tweetData.includes.media[0].preview_image_url 77 | const imageWidget = { 78 | "image": { 79 | "imageUrl": cardImageUrl 80 | } 81 | } 82 | card.cards[0].sections[0].widgets.push(imageWidget); 83 | } 84 | 85 | return card; 86 | } 87 | } 88 | 89 | const userCard = (uid, username, event) => { 90 | const userData = TWITTER_API.getUser(uid, username); 91 | if (!userData) { 92 | return requestConfig(event.configCompleteRedirectUrl); 93 | } else { 94 | const card = { 95 | "actionResponse": { 96 | "type": "UPDATE_USER_MESSAGE_CARDS" 97 | }, 98 | "cards": [ 99 | { 100 | "header": { 101 | "title": userData.data.name, 102 | "subtitle": `@${userData.data.username}`, 103 | "imageUrl": userData.data.profile_image_url.replace("_normal.jpg", "_400x400.jpg"), 104 | "imageStyle": "AVATAR", 105 | }, 106 | "sections": [ 107 | { 108 | "widgets": [ 109 | { 110 | "keyValue": { 111 | "content": `Followers: ${userData.data.public_metrics.followers_count}`, 112 | "contentMultiline": "true", 113 | "bottomLabel": `Following: ${userData.data.public_metrics.following_count}`, 114 | "iconUrl": `https://script.gs/content/images/2022/07/twitter-verified-${userData.data.verified ? "blue" : "grey"}.png`, 115 | "button": { 116 | "textButton": { 117 | "text": "FOLLOW", 118 | "onClick": { 119 | "action": { 120 | "actionMethodName": `FOLLOW_USER`, 121 | "parameters": [ 122 | { 123 | "key": "userIdToFollow", 124 | "value": userData.data.id 125 | }, 126 | { 127 | "key": "userNameToFollow", 128 | "value": userData.data.username 129 | }, 130 | ] 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | ] 138 | }, 139 | { 140 | "widgets": [ 141 | { 142 | "textParagraph": { 143 | "text": `Profile description:
${userData.data.description}` 144 | } 145 | } 146 | ] 147 | }, 148 | { 149 | "widgets": [ 150 | { 151 | "keyValue": { 152 | "topLabel": `"Twitter is unable to process your request"?`, 153 | "content": "Connect your Twitter account by running /twitter_connect or DM the bot directly.", 154 | "contentMultiline": true, 155 | "iconUrl": "https://script.gs/content/images/2022/07/warning.png", 156 | } 157 | } 158 | ] 159 | } 160 | ] 161 | } 162 | ] 163 | }; 164 | return card; 165 | } 166 | } 167 | 168 | const meCard = (uid, event, type) => { 169 | let twitterUserData = getService().getStorage().getValue(`${uid}_twitter_data`); 170 | let username; 171 | if (twitterUserData) { 172 | username = JSON.parse(twitterUserData).username; 173 | } else { 174 | twitterUserData = TWITTER_API.getMe(uid); 175 | if (!twitterUserData) { 176 | return requestConfig(event.configCompleteRedirectUrl); 177 | } 178 | username = twitterUserData.data.username; 179 | } 180 | const userData = TWITTER_API.getUser(uid, username); 181 | if (!userData) { 182 | return requestConfig(event.configCompleteRedirectUrl); 183 | } else { 184 | const card = { 185 | "actionResponse": { 186 | "type": type 187 | }, 188 | "cards": [ 189 | { 190 | "header": { 191 | "title": userData.data.name, 192 | "subtitle": `@${userData.data.username}`, 193 | "imageUrl": userData.data.profile_image_url.replace("_normal.jpg", "_400x400.jpg"), 194 | "imageStyle": "AVATAR", 195 | }, 196 | "sections": [ 197 | { 198 | "widgets": [ 199 | { 200 | "keyValue": { 201 | "content": `Followers: ${userData.data.public_metrics.followers_count}`, 202 | "contentMultiline": "true", 203 | "bottomLabel": `Following: ${userData.data.public_metrics.following_count}`, 204 | "onClick": { 205 | "openLink": { 206 | "url": `https://twitter.com/${userData.data.username}` 207 | } 208 | }, 209 | "iconUrl": `https://script.gs/content/images/2022/07/twitter-verified-${userData.data.verified ? "blue" : "grey"}.png`, 210 | } 211 | } 212 | ] 213 | }, 214 | { 215 | "widgets": [ 216 | { 217 | "textParagraph": { 218 | "text": `Profile description:
${userData.data.description}` 219 | } 220 | } 221 | ] 222 | }, 223 | { 224 | "widgets": [ 225 | { 226 | "keyValue": { 227 | "topLabel": "User Id", 228 | "content": userData.data.id, 229 | "onClick": { 230 | "openLink": { 231 | "url": `https://twitter.com/${userData.data.username}` 232 | } 233 | }, 234 | "icon": "PERSON", 235 | } 236 | } 237 | ] 238 | } 239 | ] 240 | } 241 | ] 242 | }; 243 | 244 | const followButton = { 245 | "textButton": { 246 | "text": "FOLLOW", 247 | "onClick": { 248 | "action": { 249 | "actionMethodName": `FOLLOW_USER`, 250 | "parameters": [ 251 | { 252 | "key": "userIdToFollow", 253 | "value": userData.data.id 254 | }, 255 | { 256 | "key": "userNameToFollow", 257 | "value": userData.data.username 258 | }, 259 | ] 260 | } 261 | } 262 | } 263 | }; 264 | event.space.singleUserBotDm ? card : card.cards[0].sections[0].widgets[0].keyValue["button"] = followButton; 265 | 266 | const noticeWidget = { 267 | "widgets": [ 268 | { 269 | "keyValue": { 270 | "topLabel": `"Twitter is unable to process your request"?`, 271 | "content": "Connect your Twitter account by running /twitter_connect or DM the bot directly.", 272 | "contentMultiline": true, 273 | "iconUrl": "https://script.gs/content/images/2022/07/warning.png", 274 | } 275 | } 276 | ] 277 | }; 278 | event.space.singleUserBotDm ? card : card.cards[0].sections.push(noticeWidget); 279 | return card; 280 | } 281 | } 282 | 283 | const connectCard = (type) => { 284 | return { 285 | "actionResponse": { 286 | "type": type 287 | }, 288 | "cards": [ 289 | { 290 | "sections": [ 291 | { 292 | "widgets": [ 293 | { 294 | "keyValue": { 295 | "content": 'Your Twitter account has been connected.
To check your profile, try /twitter_me', 296 | "contentMultiline": true, 297 | "icon": "STAR", 298 | } 299 | } 300 | ] 301 | }, 302 | ] 303 | } 304 | ] 305 | }; 306 | } 307 | 308 | const feedbackCard = (type) => { 309 | return { 310 | "actionResponse": { 311 | "type": type 312 | }, 313 | "cards": [ 314 | { 315 | "sections": [ 316 | { 317 | "widgets": [ 318 | { 319 | "keyValue": { 320 | "content": `Thanks for your feedback!
It's shared with Sourabh (code@script.gs).
DM @choraria on Twitter for quicker response.`, 321 | "contentMultiline": true, 322 | "icon": "BOOKMARK", 323 | } 324 | } 325 | ] 326 | }, 327 | ] 328 | } 329 | ] 330 | }; 331 | } 332 | 333 | const logoutCard = (type) => { 334 | return { 335 | "actionResponse": { 336 | "type": type 337 | }, 338 | "cards": [ 339 | { 340 | "sections": [ 341 | { 342 | "widgets": [ 343 | { 344 | "keyValue": { 345 | "content": `You've now logged-out of your Twitter account!`, 346 | "contentMultiline": true, 347 | "icon": "PERSON", 348 | } 349 | } 350 | ] 351 | }, 352 | ] 353 | } 354 | ] 355 | }; 356 | } 357 | 358 | const helpCard = (type) => { 359 | return { 360 | "actionResponse": { 361 | "type": type 362 | }, 363 | "cards": [ 364 | { 365 | "header": { 366 | "title": "Help", 367 | "imageUrl": "https://script.gs/content/images/2022/07/help-icon.png", 368 | "imageStyle": "AVATAR", 369 | }, 370 | "sections": [ 371 | { 372 | "header": "Getting started", 373 | "widgets": [ 374 | { 375 | "keyValue": { 376 | "content": "Connect your Twitter account by running the /twitter_connect slash command.", 377 | "contentMultiline": true, 378 | "icon": "DESCRIPTION", 379 | } 380 | }, 381 | { 382 | "keyValue": { 383 | "content": "To like a tweet or follow another user, you will need to authorize your own Twitter account.", 384 | "contentMultiline": true, 385 | } 386 | } 387 | ] 388 | }, 389 | { 390 | "header": "Connecting and authorization", 391 | "widgets": [ 392 | { 393 | "keyValue": { 394 | "content": "You will need to click on that Configure button twice during the auth process!", 395 | "contentMultiline": true, 396 | "iconUrl": "https://script.gs/content/images/2022/07/warning.png", 397 | } 398 | }, 399 | { 400 | "keyValue": { 401 | "content": "— First would prepare your Google account to store your Twitter credentials
— And the second, would then allow you to connect & interact with your Twitter account", 402 | "contentMultiline": true, 403 | } 404 | } 405 | ] 406 | }, 407 | { 408 | "header": "Usage", 409 | "widgets": [ 410 | { 411 | "keyValue": { 412 | "content": "Preview twitter.com links and invoke Slash commands using /twitter...", 413 | "contentMultiline": true, 414 | "icon": "STAR", 415 | } 416 | } 417 | ] 418 | }, 419 | ] 420 | } 421 | ] 422 | }; 423 | } 424 | -------------------------------------------------------------------------------- /Google Chat/Chat App/Twitter/Code.gs: -------------------------------------------------------------------------------- 1 | function requestConfig(configCompleteRedirectUrl) { 2 | const service = getService(); 3 | const codeVerifier = generateCodeVerifier(); 4 | let codeChallenge = encodeChallenge(codeVerifier); 5 | service.setParam('code_challenge_method', 'S256') 6 | service.setParam('code_challenge', codeChallenge) 7 | 8 | const authorizationUrl = service.getAuthorizationUrl({ 9 | codeVerifier: codeVerifier, 10 | }); 11 | service.getStorage().setValue('configCompleteRedirectUrl', configCompleteRedirectUrl); 12 | return { 13 | "actionResponse": { 14 | "type": "REQUEST_CONFIG", 15 | "url": authorizationUrl 16 | } 17 | } 18 | } 19 | 20 | const TWITTER_API = { 21 | getMe: function (uid) { // https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me 22 | const url = `https://api.twitter.com/2/users/me`; 23 | const accessToken = getService().getAccessToken(); 24 | const options = { 25 | "method": 'GET', 26 | "headers": { 27 | Authorization: "Bearer " + accessToken, 28 | }, 29 | "muteHttpExceptions": true, 30 | }; 31 | const res = UrlFetchApp.fetch(url, options); 32 | if (res.getResponseCode() === 200) { 33 | const twitterData = JSON.parse(res); 34 | getService().getStorage().setValue(`${uid}_twitter_data`, JSON.stringify({ 35 | id: twitterData.data.id, 36 | username: twitterData.data.username, 37 | name: twitterData.data.name 38 | })); 39 | return twitterData; 40 | } else { 41 | console.log({ 42 | message: `An error occurred getting user ${uid}'s Twitter user data using token ${accessToken}`, 43 | responseCode: res.getResponseCode(), 44 | responseMessage: res.getContentText(), 45 | response: res 46 | }); 47 | return false; 48 | } 49 | }, 50 | getTweet: function (uid, tweetId) { // https://developer.twitter.com/en/docs/twitter-api/tweets/lookup/api-reference/get-tweets-id 51 | const url = `https://api.twitter.com/2/tweets/${tweetId}?expansions=attachments.poll_ids,attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id&media.fields=duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics,alt_text,variants&place.fields=contained_within,country,country_code,full_name,geo,id,name,place_type&poll.fields=duration_minutes,end_datetime,id,options,voting_status&tweet.fields=attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,public_metrics,possibly_sensitive,referenced_tweets,reply_settings,source,text,withheld&user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld`; 52 | const accessToken = getService().getAccessToken(); 53 | const options = { 54 | "method": 'GET', 55 | "headers": { 56 | Authorization: "Bearer " + accessToken, 57 | }, 58 | "muteHttpExceptions": true, 59 | }; 60 | const res = UrlFetchApp.fetch(url, options); 61 | if (res.getResponseCode() === 200) { 62 | return JSON.parse(res); 63 | } else { 64 | console.log({ 65 | message: `An error occurred getting tweet via Id ${tweetId} from user ${uid} using token ${accessToken}`, 66 | responseCode: res.getResponseCode(), 67 | responseMessage: res.getContentText(), 68 | response: res 69 | }); 70 | return false; 71 | } 72 | }, 73 | getUser: function (uid, username) { // https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-by-username-username 74 | const url = `https://api.twitter.com/2/users/by/username/${username}?expansions=pinned_tweet_id&tweet.fields=attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,public_metrics,possibly_sensitive,referenced_tweets,reply_settings,source,text,withheld&user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld`; 75 | const accessToken = getService().getAccessToken(); 76 | const options = { 77 | "method": 'GET', 78 | "headers": { 79 | Authorization: "Bearer " + accessToken, 80 | }, 81 | "muteHttpExceptions": true, 82 | }; 83 | const res = UrlFetchApp.fetch(url, options); 84 | if (res.getResponseCode() === 200) { 85 | return JSON.parse(res); 86 | } else { 87 | console.log({ 88 | message: `An error occurred getting user via username ${username} from user ${uid} using token ${accessToken}`, 89 | responseCode: res.getResponseCode(), 90 | responseMessage: res.getContentText(), 91 | response: res 92 | }); 93 | return false; 94 | } 95 | }, 96 | likeTweet: function (uid, tweetId) { // https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/post-users-id-likes 97 | let twitterUserData = getService().getStorage().getValue(`${uid}_twitter_data`); 98 | let userId; 99 | if (twitterUserData) { 100 | userId = JSON.parse(twitterUserData).id; 101 | } else { 102 | twitterUserData = TWITTER_API.getMe(uid); 103 | userId = twitterUserData.data.id; 104 | } 105 | if (userId) { 106 | const url = `https://api.twitter.com/2/users/${userId}/likes`; 107 | const accessToken = getService().getAccessToken(); 108 | const options = { 109 | "method": 'POST', 110 | "payload": JSON.stringify({ tweet_id: tweetId }), 111 | "headers": { 112 | Authorization: "Bearer " + accessToken, 113 | "Content-Type": "application/json", 114 | }, 115 | "muteHttpExceptions": true, 116 | }; 117 | const res = UrlFetchApp.fetch(url, options); 118 | if (res.getResponseCode() === 200) { 119 | return JSON.parse(res); 120 | } else { 121 | console.log({ 122 | message: `An error occurred when liking tweet via Id ${tweetId} from user ${uid} using token ${accessToken}`, 123 | responseCode: res.getResponseCode(), 124 | responseMessage: res.getContentText(), 125 | response: res 126 | }); 127 | return false; 128 | } 129 | } else { 130 | return false; 131 | } 132 | }, 133 | followUser: function (uid, userIdToFollow, userNameToFollow) { // https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/post-users-id-likes 134 | let twitterUserData = getService().getStorage().getValue(`${uid}_twitter_data`); 135 | let userId; 136 | if (twitterUserData) { 137 | userId = JSON.parse(twitterUserData).id; 138 | } else { 139 | twitterUserData = TWITTER_API.getMe(uid); 140 | userId = twitterUserData.data.id; 141 | } 142 | if (userId) { 143 | const url = `https://api.twitter.com/2/users/${userId}/following`; 144 | const accessToken = getService().getAccessToken(); 145 | const options = { 146 | "method": 'POST', 147 | "payload": JSON.stringify({ target_user_id: userIdToFollow }), 148 | "headers": { 149 | Authorization: "Bearer " + accessToken, 150 | "Content-Type": "application/json", 151 | }, 152 | "muteHttpExceptions": true, 153 | }; 154 | const res = UrlFetchApp.fetch(url, options); 155 | if (res.getResponseCode() === 200) { 156 | return JSON.parse(res); 157 | } else { 158 | console.log({ 159 | message: `An error occurred when following user @${userNameToFollow} via Id ${userIdToFollow} from user ${uid} using token ${accessToken}`, 160 | responseCode: res.getResponseCode(), 161 | responseMessage: res.getContentText(), 162 | response: res 163 | }); 164 | return false; 165 | } 166 | } else { 167 | return false; 168 | } 169 | }, 170 | } 171 | 172 | function checkMatchedUrl(url) { 173 | const urlPattern = new RegExp(/^https:\/\/twitter\.com\/([A-Za-z0-9_]{1,15})(?:\/status\/([0-9]{19}))?$/); 174 | const [matchedUrl, username, tweetId] = urlPattern.test(url) ? urlPattern.exec(url) : new Array(3).fill(null); 175 | const entity = !matchedUrl ? "UNDEFINED" : (matchedUrl.length > 40 ? "TWEET" : "USER"); 176 | return { 177 | entity: entity, 178 | username: username, 179 | tweetId: tweetId 180 | } 181 | } 182 | 183 | function likeTweet(uid, tweetId, event) { 184 | const like = TWITTER_API.likeTweet(uid, tweetId); 185 | if (like) { 186 | return { text: `*${event.space.singleUserBotDm ? "You" : event.user.displayName}* liked the Tweet with Id ${tweetId}` } 187 | } else { 188 | return { 189 | text: (event.space.singleUserBotDm ? "S" : ", s") + 190 | "omething went wrong.\nPlease log-out of your Twitter account by using */twitter_logout* and re-connect by mentioning */twitter_connect*" + 191 | (event.space.singleUserBotDm ? "." : " or DMing the bot directly.") 192 | } 193 | } 194 | } 195 | 196 | function followUser(uid, userIdToFollow, userNameToFollow, event) { 197 | const follow = TWITTER_API.followUser(uid, userIdToFollow, userNameToFollow); 198 | if (follow) { 199 | return { 200 | text: `*${event.space.singleUserBotDm ? "You" : event.user.displayName}* ${follow.data.following ? "followed" : (follow.data.pending_follow ? "requested to follow" : "tried following") 201 | } *@${userNameToFollow}* (userId: ${userIdToFollow})` 202 | } 203 | } else { 204 | return { 205 | text: (event.space.singleUserBotDm ? "S" : ", s") + 206 | "omething went wrong.\nPlease log-out of your Twitter account by using */twitter_logout* and re-connect by mentioning */twitter_connect*" + 207 | (event.space.singleUserBotDm ? "." : " or DMing the bot directly.") 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Google Chat/Chat App/Twitter/README.md: -------------------------------------------------------------------------------- 1 | # Twitter for Google Chat 2 | -------------------------------------------------------------------------------- /Google Chat/Chat App/Twitter/Twitter.gs: -------------------------------------------------------------------------------- 1 | const CLIENT_ID = '...'; 2 | const CLIENT_SECRET = '...'; 3 | 4 | function generateCodeVerifier() { 5 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; 6 | let verifier = ""; 7 | for (let i = 0; i < 32; ++i) { 8 | const r = Math.floor(Math.random() * charset.length); 9 | verifier += charset[r]; 10 | } 11 | return verifier; 12 | } 13 | 14 | function encodeChallenge(codeVerifier) { 15 | const hashedValue = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, codeVerifier, Utilities.Charset.US_ASCII); 16 | let encodedValue = Utilities.base64EncodeWebSafe(hashedValue); 17 | encodedValue = encodedValue.slice(0, encodedValue.indexOf('=')); // Strip padding 18 | return encodedValue; 19 | } 20 | 21 | /** 22 | * Configures the service. 23 | */ 24 | const getService = () => { 25 | return OAuth2.createService('Twitter') 26 | // Set the endpoint URLs. 27 | .setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize') 28 | .setTokenUrl('https://api.twitter.com/2/oauth2/token') 29 | 30 | // Set the client ID and secret. 31 | .setClientId(CLIENT_ID) 32 | .setClientSecret(CLIENT_SECRET) 33 | 34 | // Set the name of the callback function that should be invoked to 35 | // complete the OAuth flow. 36 | .setCallbackFunction('authCallback') 37 | 38 | // Set the property store where authorized tokens should be persisted. 39 | .setPropertyStore(PropertiesService.getUserProperties()) 40 | 41 | // Set the scopes to request (space-separated for Twitter services). 42 | .setScope('users.read tweet.read follows.write like.write offline.access') 43 | 44 | .setTokenHeaders({ 45 | 'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET), 46 | 'Content-Type': 'application/x-www-form-urlencoded' 47 | }) 48 | } 49 | 50 | /** 51 | * Handles the OAuth callback. 52 | */ 53 | function authCallback(request) { 54 | const service = getService(); 55 | service.setTokenPayloadHandler(payload => { 56 | payload['code_verifier'] = request.parameter.codeVerifier; 57 | return payload; 58 | }); 59 | const authorized = service.handleCallback(request); 60 | if (authorized) { 61 | const configCompleteRedirectUrl = service.getStorage().getValue('configCompleteRedirectUrl'); 62 | return HtmlService 63 | .createHtmlOutput(``); 64 | } else { 65 | return HtmlService.createHtmlOutput('Denied.'); 66 | } 67 | } 68 | 69 | /** 70 | * Resets OAuth config. 71 | */ 72 | const reset = () => getService().reset(); 73 | -------------------------------------------------------------------------------- /Google Chat/Chat App/Twitter/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "libraries": [ 4 | { 5 | "userSymbol": "OAuth2", 6 | "version": "41", 7 | "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" 8 | } 9 | ] 10 | }, 11 | "exceptionLogging": "STACKDRIVER", 12 | "runtimeVersion": "V8", 13 | "chat": { 14 | "addToSpaceFallbackMessage": "Thanks for adding me this space!\n\n➡️ Next, connect your Twitter account by running the */twitter_connect* slash command.\nAnyone who wants to either *like* a tweet or *follow* another user will need to authorize their own Twitter account, which can be done either by running */twitter_connect* or DMing the bot directly.\n\n⚠️ Notes on connecting and authorization:\nYou will need to click on that *Configure* button *twice* during the auth process!\n— First would prepare your Google account to store your Twitter credentials — this way, no one else can access them\n\t— Needs to be done only once, unless you manually revoke the apps' permissions\n— And the second, would then allow you to connect & interact with your Twitter account" 15 | }, 16 | "oauthScopes": [ 17 | "https://www.googleapis.com/auth/script.external_request" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Google Chat/README.md: -------------------------------------------------------------------------------- 1 | # Google Chat 2 | -------------------------------------------------------------------------------- /Hangouts Chat/Bot/README.md: -------------------------------------------------------------------------------- 1 | # List of bots hosted using Google Apps Script 2 | 3 | - [getThreadID](getThreadID.gs): Link to a specific conversation thread on Google Hangouts Chat 4 | - As reported on [stackexchange](https://webapps.stackexchange.com/questions/117392/get-link-to-specific-conversation-thread-and-or-message-in-a-chat-room-in-google) 5 | -------------------------------------------------------------------------------- /Hangouts Chat/Bot/getThreadID.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Responds to a MESSAGE event in Hangouts Chat. 3 | * 4 | * @param {Object} event the event object from Hangouts Chat 5 | */ 6 | function onMessage(event) { 7 | var thread = event.message.thread.name; 8 | var threadRegex = /(spaces\/)(.*)(\/threads\/)(.*)/; 9 | var spaceID = threadRegex.exec(thread)[2] 10 | var threadID = threadRegex.exec(thread)[4] 11 | var message = "Thread ID: " + threadID + "\nThread URL: https://chat.google.com/room/" + spaceID + "/" + threadID; 12 | return { "text": message }; 13 | } 14 | 15 | /** 16 | * Responds to an ADDED_TO_SPACE event in Hangouts Chat. 17 | * 18 | * @param {Object} event the event object from Hangouts Chat 19 | */ 20 | function onAddToSpace(event) { 21 | var message = ""; 22 | message = "Thank you for adding me to *" + event.space.displayName + "*. \nYou can now use `@getThreadID` command to get the URL of a specific conversation."; 23 | if (event.message) { 24 | var thread = event.message.thread.name; 25 | var threadRegex = /(spaces\/)(.*)(\/threads\/)(.*)/; 26 | var spaceID = threadRegex.exec(thread)[2] 27 | var threadID = threadRegex.exec(thread)[4] 28 | message = "Thank you for adding me to " + event.space.displayName + "\n" + "Thread ID: " + threadID + "\nThread URL: https://chat.google.com/room/" + spaceID + "/" + threadID; 29 | } 30 | return { "text": message }; 31 | } 32 | -------------------------------------------------------------------------------- /Hangouts Chat/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Hangouts Chat](https://developers.google.com/hangouts/chat/) 2 | 3 | - [Bots](Bot/): [Chatbot Concepts](https://developers.google.com/hangouts/chat/concepts/bots) 4 | - Webhooks: [Using incoming webhooks](https://developers.google.com/hangouts/chat/how-tos/webhooks) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sourabh Choraria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Library/Exotel/ExoAPI.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Google Apps Script library for Exotel APIs - https://developer.exotel.com/api/ 3 | * 4 | * @version v1 5 | * @author Sourabh Choraria 6 | * NOT officially maintained by Exotel 7 | * Inspired by Coda.io 8 | */ 9 | 10 | /** Private Exotel helper functions. */ 11 | var _ = { 12 | authToken: null, 13 | tenant: null, 14 | subdomain: null, 15 | 16 | host: 'exotel.com/v1/Accounts/', 17 | 18 | ensure: function(value, message) { 19 | if (!value) { 20 | throw new Error(message); 21 | } 22 | }, 23 | 24 | ensureAuthenticated: function() { 25 | _.ensure(_.authToken, 'Call ExoAPI.authenticate() first to set your token.') 26 | }, 27 | 28 | getBaseOpts: function() { 29 | return { 30 | headers: { 31 | Authorization: 'Basic ' + _.authToken, 32 | }, 33 | }; 34 | }, 35 | 36 | getQueryString: function(params) { 37 | var output = ''; 38 | for (var param in params) { 39 | if (typeof params[param] === 'undefined') { 40 | continue; 41 | } else if (output) { 42 | output += '&'; 43 | } 44 | output += encodeURIComponent(param); 45 | output += '='; 46 | output += encodeURIComponent(params[param]); 47 | } 48 | return output ? '?' + output : ''; 49 | } 50 | } 51 | 52 | /** 53 | * Sets the required authentication credentials for use by the library. 54 | * 55 | * @param {String} apiKey The API key as per your Exotel dashboard. 56 | * @param {String} apiToken The Token key as per your Exotel dashboard. 57 | * @param {String} accSid The Account Sid of your Exotel account. 58 | * @param {String} cluster The subdomain with the region of your Exotel account. 59 | */ 60 | function authenticate(apiKey, apiToken, accSid, cluster) { 61 | _.authToken = Utilities.base64Encode(apiKey + ":" + apiToken); 62 | _.tenant = accSid; 63 | _.subdomain = cluster; 64 | } 65 | 66 | /** 67 | * Returns metadata for the specified telephone number. 68 | * 69 | * @param {String} number The number for which you require metadata. 70 | * @returns {Object} Basic Telephone number metadata. 71 | */ 72 | function metaData(number) { 73 | _.ensureAuthenticated(); 74 | var opts = _.getBaseOpts(); 75 | if (_.subdomain == '' || _.subdomain == 'undefined' || _.subdomain == null){ 76 | var url = 'https://api.' + _.host + _.tenant + '/Numbers/' + number + '.json'; 77 | } else { 78 | var url = 'https://api.' + _.subdomain + '.' + _.host + _.tenant + '/Numbers/' + number + '.json'; 79 | } 80 | url += _.getQueryString({}); 81 | var response = UrlFetchApp.fetch(url, opts); 82 | return JSON.parse(response.getContentText()); 83 | } 84 | 85 | /** 86 | * Outgoing call to connect two numbers 87 | * 88 | * @param {String} from The 'From' number which would be connected first. 89 | * @param {String} to The 'To' number which would be connected after the 'From' number has answered the call. 90 | * @param {String} callerID The 'ExoPhone' with which the call would be connected. 91 | * @returns {Object} The response for connecting call for two numbers 92 | */ 93 | function connectTwoNumbers(from, to, callerID) { 94 | _.ensureAuthenticated(); 95 | var opts = _.getBaseOpts(); 96 | opts.method = 'POST'; 97 | if (_.subdomain == '' || _.subdomain == 'undefined' || _.subdomain == null){ 98 | var url = 'https://api.' + _.host + _.tenant + '/Calls/connect.json?From=' + from + '&To=' + to + '&CallerID=' + callerID; 99 | } else { 100 | var url = 'https://api.' + _.subdomain + '.' + _.host + _.tenant + '/Calls/connect.json?From=' + from + '&To=' + to + '&CallerID=' + callerID; 101 | } 102 | url += _.getQueryString({}); 103 | var response = UrlFetchApp.fetch(url, opts); 104 | return JSON.parse(response.getContentText()); 105 | } 106 | 107 | /** 108 | * Outgoing call to connect number to a call flow 109 | * 110 | * @param {String} from The 'From' number which would be connected first. 111 | * @param {String} callerID The 'ExoPhone' with which the call would be connected. 112 | * @param {String} flowID The 'APP ID' as per Exotel dashboard. 113 | * @returns {Object} The response for connecting call with a call flow 114 | */ 115 | function connectNumberToFlow(from, callerID, flowID) { 116 | _.ensureAuthenticated(); 117 | var opts = _.getBaseOpts(); 118 | opts.method = 'POST'; 119 | if (_.subdomain == '' || _.subdomain == 'undefined' || _.subdomain == null){ 120 | var url = 'https://api.' + _.host + _.tenant + '/Calls/connect.json?From=' + from + '&CallerID=' + callerID + '&Url=http://my.exotel.com/' + _.tenant + '/exoml/start_voice/' + flowID; 121 | } else { 122 | var url = 'https://api.' + _.subdomain + '.' + _.host + _.tenant + '/Calls/connect.json?From=' + from + '&CallerID=' + callerID + '&Url=http://my.exotel.com/' + _.tenant + '/exoml/start_voice/' + flowID; 123 | } 124 | url += _.getQueryString({}); 125 | var response = UrlFetchApp.fetch(url, opts); 126 | return JSON.parse(response.getContentText()); 127 | } 128 | 129 | /** 130 | * Returns call details for the specified call SID. 131 | * 132 | * @param {String} callSid The SID for which you require call data. 133 | * @returns {Object} Basic CDR of the call. 134 | */ 135 | function callDetails(callSid) { 136 | _.ensureAuthenticated(); 137 | var opts = _.getBaseOpts(); 138 | if (_.subdomain == '' || _.subdomain == 'undefined' || _.subdomain == null){ 139 | var url = 'https://api.' + _.host + _.tenant + '/Calls/' + callSid + '.json'; 140 | } else { 141 | var url = 'https://api.' + _.subdomain + '.' + _.host + _.tenant + '/Calls/' + callSid + '.json'; 142 | } 143 | url += _.getQueryString({}); 144 | var response = UrlFetchApp.fetch(url, opts); 145 | return JSON.parse(response.getContentText()); 146 | } 147 | 148 | /** 149 | * Sending an SMS 150 | * 151 | * @param {String} callerID The 'ExoPhone' associated with the 'SenderID' via which you intend to send the SMS. 152 | * @param {String} to The 'To' number to which you intend to send an SMS. 153 | * @param {String} body The 'Approved' SMS template with it's substituted values. 154 | * @returns {Object} The response for sending an SMS. 155 | */ 156 | function sendSMS(callerID, to, body) { 157 | _.ensureAuthenticated(); 158 | var opts = _.getBaseOpts(); 159 | opts.method = 'POST'; 160 | if (_.subdomain == '' || _.subdomain == 'undefined' || _.subdomain == null){ 161 | var url = 'https://api.' + _.host + _.tenant + '/Sms/send.json?From=' + callerID + '&To=' + to + '&Body=' + body; 162 | } else { 163 | var url = 'https://api.' + _.subdomain + '.' + _.host + _.tenant + '/Sms/send.json?From=' + callerID + '&To=' + to + '&Body=' + body; 164 | } 165 | url += _.getQueryString({}); 166 | var response = UrlFetchApp.fetch(url, opts); 167 | return JSON.parse(response.getContentText()); 168 | } 169 | 170 | /** 171 | * Returns SMS details for the specified SMS SID. 172 | * 173 | * @param {String} smsSid The SID for which you require SMS data. 174 | * @returns {Object} Basic data of that SMS. 175 | */ 176 | function smsDetails(smsSid) { 177 | _.ensureAuthenticated(); 178 | var opts = _.getBaseOpts(); 179 | if (_.subdomain == '' || _.subdomain == 'undefined' || _.subdomain == null){ 180 | var url = 'https://api.' + _.host + _.tenant + '/SMS/Messages/' + smsSid + '.json'; 181 | } else { 182 | var url = 'https://api.' + _.subdomain + '.' + _.host + _.tenant + '/SMS/Messages/' + smsSid + '.json'; 183 | } 184 | url += _.getQueryString({}); 185 | var response = UrlFetchApp.fetch(url, opts); 186 | return JSON.parse(response.getContentText()); 187 | } 188 | -------------------------------------------------------------------------------- /Library/Exotel/README.md: -------------------------------------------------------------------------------- 1 | # Google Apps Script Library for [Exotel APIs](https://developer.exotel.com/api/) 2 | 3 | ### Host it yourself 4 | 5 | Refer to Google's guide on [creating a library](https://developers.google.com/apps-script/guides/libraries#creating_a_library) and copy-paste the code available in this repository - [ExoAPI.gs](ExoAPI.gs) 6 | 7 | ### List of functions 8 | 9 | - ExoAPI.metaData(`number`) // [Number metadata](https://developer.exotel.com/api/#metadata-phone) 10 | - ExoAPI.connectTwoNumbers(`from`, `to`, `callerID`) // [Outgoing call to connect two numbers](https://developer.exotel.com/api/#call-agent) 11 | - [sample](sample/connect2Num.gs) 12 | - ExoAPI.connectNumberToFlow(`from`, `callerID`, `flowID`) // [Outgoing call to connect number to a call flow](https://developer.exotel.com/api/#call-customer) 13 | - ExoAPI.callDetails(`callSid`) // [Call details](https://developer.exotel.com/api/#call-details) 14 | - [sample](sample/callDetails.gs) 15 | - ExoAPI.sendSMS(`callerID`, `to`, `body`) // [Send SMS](https://developer.exotel.com/api/#send-sms) 16 | - ExoAPI.smsDetails(`smsSid`) // [SMS details](https://developer.exotel.com/api/#sms-details) 17 | - ExoAPI.authenticate(`apiKey`, `apiToken`, `accSid`, `cluster`) 18 | - this is a default function that needs to be called before invoking any/all other functions - if this is skipped, an error message would request to invoke the same 19 | - *cluster* parameter is optional 20 | 21 | **Caveat**: Only the mandatory parameters have been considered for the purposes of this library 22 | 23 | ### Use mine 24 | 25 | You're also free to use the library hosted on my personal ID [here](https://script.google.com/d/1V9cn0CSU9GnSyCebBRZ5vS-jSn2z3U6s1KkaHe4Aml2x-CAsmTNU4bp4/edit) & *this* is how to [use an existing library](https://developers.google.com/apps-script/guides/libraries#using_a_library) 26 | 27 | ## Disclaimer 28 | 29 | This library is neither endoresed nor offically approved by [Exotel](https://exotel.com/). 30 | -------------------------------------------------------------------------------- /Library/Exotel/sample/callDetails.gs: -------------------------------------------------------------------------------- 1 | var Key = 'XXXXXXXXXXX' 2 | var Token = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' 3 | var aSID = 'ZZZZZZZZZZZZZZZZZZZZZZZZZZ' 4 | 5 | function callDetails() { 6 | ExoAPI.authenticate(Key, Token, aSID) 7 | var SID = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 8 | var calldetail = ExoAPI.callDetails(SID) 9 | var Uri = calldetail.Call.Uri 10 | Logger.log(Uri) 11 | } 12 | -------------------------------------------------------------------------------- /Library/Exotel/sample/connect2Num.gs: -------------------------------------------------------------------------------- 1 | var Key = 'XXXXXXXXXXX' 2 | var Token = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' 3 | var aSID = 'ZZZZZZZZZZZZZZZZZZZZZZZZZZ' 4 | 5 | function Connect2Nos() { 6 | ExoAPI.authenticate(Key, Token, aSID) 7 | var From = 'AAAAAAAAAA' 8 | var To = 'BBBBBBBBBB' 9 | var CalerId = 'CCCCCCCCCC' 10 | var dial = ExoAPI.connectTwoNumbers(From, To, CalerId) 11 | var Sid = dial.Call.Sid 12 | Logger.log(Sid) 13 | } 14 | -------------------------------------------------------------------------------- /Library/README.md: -------------------------------------------------------------------------------- 1 | # A collection of Apps Script libraries I've created over time 2 | 3 | - [Exotel](Exotel/) ([official site](https://exotel.com/), [dev portal](https://developer.exotel.com/api/)): Cloud Communication APIs for Calls & SMS 4 | -------------------------------------------------------------------------------- /Login Dashboard/Dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Exotel 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |



29 |

Dashboard

30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Login Dashboard/Login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Exotel 9 | 10 | 11 | 16 | 17 | 18 | 19 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |



49 |
50 |
51 |
52 |
53 |

Login Dashboard

54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 |
62 |
63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Login Dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Password protected Web App 2 | With this setup, the [Google Apps Script Web App](https://developers.google.com/apps-script/guides/web) would require a passcode (setup as a "lock") before accessing the actual dashboard 3 | 4 | ## Demo 5 | You can view the final output of this setup [here](https://script.google.com/macros/s/AKfycbyC2rvc5YjeqTSnOrmGjL0qaYv5IG5H_wGqoomhjvHGju_YlfXB/exec) (Passcode: `admin`) 6 | -------------------------------------------------------------------------------- /Login Dashboard/code.gs: -------------------------------------------------------------------------------- 1 | /**//* ======================================================== *//**/ 2 | /**/ /**/ 3 | /**/ // Make changes only to this segment /**/ 4 | /**/ /**/ 5 | /**/ var ID = "Your-SpreadsheetID-goes-here"; /**/ 6 | /**/ var lock = 'admin' /**/ 7 | /**/ /**/ 8 | /**//* ======================================================== *//**/ 9 | 10 | /* ==================== DO NOT CHANGE ANYTHING BELOW THIS LINE ======================== */ 11 | 12 | var conf = 'config' 13 | var ss = SpreadsheetApp.openById(ID) 14 | 15 | function doGet(e) { 16 | if (Object.keys(e.parameter).length === 0) { 17 | var htmlFile 18 | var sheetName = conf 19 | var activeSheet = ss.getSheetByName(sheetName) 20 | if (activeSheet !== null) { 21 | var values = activeSheet.getDataRange().getValues(); 22 | for(var i=0, iLen=values.length; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 48 | 49 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 |
75 |

Autocomplete

76 |
77 |
78 | 79 |
80 | 84 |
85 | 86 |
87 |
88 |


89 |
90 |
91 | 92 |
93 |
94 |
95 |
96 | 97 |
98 |
99 |
100 | 101 |
102 |
103 |
104 | 107 | 108 |
109 |
110 |
111 | 112 |
113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Materialize CSS/Autocomplete/README.md: -------------------------------------------------------------------------------- 1 | # Autocomplete from Spreadsheet Data using Apps Script 2 | 3 | This script covers the usage for the following components of [Materialize CSS](https://materializecss.com): 4 | - [Chips](https://materializecss.com/chips.html) 5 | - [Select](https://materializecss.com/select.html) 6 | 7 | ## Demo 8 | 9 | You could view the inuput data sheet [here](https://docs.google.com/spreadsheets/d/1I3nhVn_YnyfZHjbjKPIkS_k72Qn8IfXt9mpF26k8bUg/edit?usp=sharing) and the web app deployed on my account [here](https://script.google.com/macros/s/AKfycbzH6s2OGoqA9EW2kKjgoTjMxCPrRlXjjECEdIzgXTZixdey2rHG/exec). 10 | -------------------------------------------------------------------------------- /Materialize CSS/README.md: -------------------------------------------------------------------------------- 1 | # Connecting [Materialize CSS](https://materializecss.com) and Google Apps Script 2 | 3 | ## Attributes covered 4 | - [Autocomplete](https://materializecss.com/autocomplete.html): This feature allows for the user to create form components that could be automatically filled in, as and when the user typer in their option or clicks on the drop-down to view available ones. 5 | -------------------------------------------------------------------------------- /Meeting Reminder/Code.gs: -------------------------------------------------------------------------------- 1 | function reminders() { 2 | var events = CalendarApp.getDefaultCalendar().getEventsForDay(new Date()); 3 | for (var i = 0; i < events.length; i++) { 4 | var event = events[i]; 5 | if (event.isOwnedByMe()) { 6 | var guests = event.getGuestList(false); 7 | for (var j = 0; j < guests.length; j++) { 8 | var guest = guests[j]; 9 | if (guest.getGuestStatus() == 'INVITED') { 10 | var meetingName = event.getTitle(); 11 | var meetingDescription = event.getDescription(); 12 | var meetingID = event.getId().split('@')[0]; 13 | var guestMail = guest.getEmail(); 14 | var eventID = Utilities.base64Encode(meetingID + ' ' + guestMail); 15 | var meetingLink = 'https://www.google.com/calendar/render?action=VIEW&eid=' + eventID; 16 | var meetingTime = Utilities.formatDate( 17 | new Date(event.getStartTime()), 18 | Session.getScriptTimeZone(), 19 | 'HHmm' 20 | ); 21 | var msgSubject = 'Reminder for ' + meetingName + ' | ⏰: ' + meetingTime + ' hrs.'; 22 | var msgBody = 'Hi there!\n\n' 23 | + 'You\'ve not responded to my invite for today\'s conversation on -\n⚡ ' + meetingName 24 | + '.\n\nPlease either ✅ or ❌ the invite so we could plan accordingly -\n' + meetingLink; 25 | if (meetingDescription.length > 0) { 26 | msgBody = msgBody + '\n\nHere\'s the agenda:\n\n' + meetingDescription; 27 | } 28 | msgBody = msgBody + '\n\nHope to see you there.\n\nCiao!' 29 | GmailApp.sendEmail( 30 | guestMail, 31 | msgSubject, 32 | msgBody, 33 | { 34 | name: 'Reminder BOT', 35 | cc: Session.getEffectiveUser().getEmail() 36 | } 37 | ); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | function cronSetup() { 45 | ScriptApp.newTrigger('reminders') 46 | .timeBased() 47 | .everyDays(1) 48 | .atHour(10) 49 | .nearMinute(1) 50 | .create(); 51 | } 52 | -------------------------------------------------------------------------------- /Metrics (GAS)/Code.gs: -------------------------------------------------------------------------------- 1 | var sheetID = 'Your-Sheet-ID-Goes-Here'; 2 | 3 | /* ========== DO NOT EDIT BELOW THIS LINE ========== */ 4 | 5 | var token = ScriptApp.getOAuthToken(); 6 | var userProperties = PropertiesService.getUserProperties(); 7 | var endPoint = 'https://script.googleapis.com/v1/processes'; 8 | var pageSize = 200; // default = 50 9 | var url = endPoint + '?pageSize=' + pageSize; 10 | var headers = { 11 | 'Accept':'application/json', 12 | 'Authorization': 'Bearer ' + token 13 | }; 14 | var options = { 15 | 'method': 'GET', 16 | 'headers': headers, 17 | 'muteHttpExceptions': true 18 | } 19 | 20 | var ss = SpreadsheetApp.openById(sheetID); 21 | var scriptLog = 'ScriptLog'; 22 | 23 | function fetchMetrics() { 24 | var today = new Date(); 25 | var response = getScriptsData(url); 26 | if (response.turnPage) { 27 | var nextPageToken = response.data.nextPageToken; 28 | do { 29 | if (isTimeUp(today)) { 30 | var newTimeTrigger = ScriptApp.newTrigger("fetchOldMetrics") 31 | .timeBased() 32 | .everyMinutes(10) 33 | .create(); 34 | userProperties.setProperty('newTimeTrigger', newTimeTrigger.getUniqueId()); 35 | break; 36 | } else { 37 | var newURL = url + '&pageToken=' + encodeURIComponent(nextPageToken); 38 | var newResponse = getScriptsData(newURL); 39 | var turnPage = newResponse.turnPage; 40 | nextPageToken = newResponse.data.nextPageToken; 41 | } 42 | } 43 | while (turnPage); 44 | } 45 | } 46 | 47 | function fetchOldMetrics() { 48 | var today = new Date(); 49 | var nextPageToken = userProperties.getProperty('nextPageToken'); 50 | var newURL = url + '&pageToken=' + encodeURIComponent(nextPageToken); 51 | var response = getScriptsData(newURL); 52 | if (response.turnPage) { 53 | var nextPageToken = response.data.nextPageToken; 54 | do { 55 | if (isTimeUp(today)) { 56 | break; 57 | } else { 58 | newURL = url + '&pageToken=' + encodeURIComponent(nextPageToken); 59 | var newResponse = getScriptsData(newURL); 60 | var turnPage = newResponse.turnPage; 61 | nextPageToken = newResponse.data.nextPageToken; 62 | } 63 | } 64 | while (turnPage); 65 | } 66 | } 67 | 68 | function getScriptsData(url) { 69 | var response = UrlFetchApp.fetch(url, options); 70 | if (response.getResponseCode() !== 200 ) { 71 | Logger.log(response); 72 | } 73 | 74 | var data = JSON.parse(response); 75 | var processes = data.processes; 76 | var turnPage = true; 77 | if (processes !== undefined) { 78 | var nextPageToken = data.nextPageToken; 79 | userProperties.setProperty('nextPageToken', nextPageToken); 80 | logData(processes); 81 | turnPage = true; 82 | } else if (JSON.stringify(response) == '{}') { 83 | turnPage = false; 84 | var triggers = ScriptApp.getProjectTriggers(); 85 | for (var i = 0; i < triggers.length; i++) { 86 | var currentTimeTrigger = userProperties.getProperty('newTimeTrigger'); 87 | if (triggers[i].getUniqueId() == currentTimeTrigger) { 88 | ScriptApp.deleteTrigger(triggers[i]); 89 | } 90 | } 91 | schedule(); 92 | } else { 93 | turnPage = false; 94 | } 95 | return { 96 | "data": data, 97 | "turnPage": turnPage 98 | } 99 | } 100 | 101 | function logData(processes) { 102 | var sheetName = scriptLog; 103 | var activeSheet = ss.getSheetByName(sheetName); 104 | if (activeSheet == null) { 105 | activeSheet = ss.insertSheet(sheetName); 106 | activeSheet.appendRow( 107 | [ 108 | "TriggerHash", 109 | "ProjectName", 110 | "FunctionName", 111 | "ProcessType", 112 | "ProcessStatus", 113 | "UserAccessLevel", 114 | "StartTime", 115 | "Duration" 116 | ] 117 | ); 118 | activeSheet.setFrozenRows(1); 119 | try { 120 | ss.deleteSheet(ss.getSheetByName('Sheet1')) 121 | } catch (sheetErr) { 122 | Logger.log(sheetErr); 123 | } 124 | removeEmptyColumns(sheetName); 125 | logDataHelper(processes); 126 | } else { 127 | logDataHelper(processes); 128 | } 129 | } 130 | 131 | function logDataHelper(processes) { 132 | var sheetName = scriptLog; 133 | var activeSheet = ss.getSheetByName(sheetName); 134 | for (var i = 0; i < processes.length; i++) { 135 | var process = processes[i]; 136 | var projectName = process.projectName; 137 | var functionName = process.functionName; 138 | var processType = process.processType; 139 | var processStatus = process.processStatus; 140 | var userAccessLevel = process.userAccessLevel; 141 | var startTime = process.startTime; 142 | var duration = process.duration; 143 | var hashInput = projectName + functionName + processType + processStatus + userAccessLevel + startTime + duration; 144 | var triggerHash = MD5(hashInput); 145 | if (projectName !== 'undefined' && functionName !== 'undefined' && processType !== 'undefined' && userAccessLevel !== 'undefined') { 146 | activeSheet.appendRow( 147 | [ 148 | triggerHash, 149 | projectName, 150 | functionName, 151 | processType, 152 | processStatus, 153 | userAccessLevel, 154 | Utilities.formatDate(new Date(startTime), Session.getScriptTimeZone(), "MM/dd/yyyy HH:mm:ss"), 155 | duration.replace("s","") 156 | ] 157 | ); 158 | } 159 | } 160 | } 161 | 162 | function schedule() { 163 | ScriptApp.newTrigger("fetchFreshMetrics") 164 | .timeBased() 165 | .everyMinutes(10) 166 | .create(); 167 | } 168 | 169 | function fetchFreshMetrics() { 170 | var today = new Date(); 171 | var response = getNewScriptsData(url); 172 | if (response.turnPage) { 173 | var nextPageToken = response.data.nextPageToken; 174 | do { 175 | if (isTimeUp(today)) { 176 | var newTimeTriggerFresh = ScriptApp.newTrigger("fetchFreshMetricsCont") 177 | .timeBased() 178 | .everyMinutes(10) 179 | .create(); 180 | userProperties.setProperty('newTimeTriggerFresh', newTimeTriggerFresh.getUniqueId()); 181 | break; 182 | } else { 183 | var newURL = url + '&pageToken=' + encodeURIComponent(nextPageToken); 184 | var newResponse = getNewScriptsData(newURL); 185 | var turnPage = newResponse.turnPage; 186 | nextPageToken = newResponse.data.nextPageToken; 187 | } 188 | } while (turnPage); 189 | } 190 | } 191 | 192 | function fetchFreshMetricsCont() { 193 | var today = new Date(); 194 | var nextPageToken = userProperties.getProperty('nextTempPageToken'); 195 | var newURL = url + '&pageToken=' + encodeURIComponent(nextPageToken); 196 | var response = getNewScriptsData(newURL); 197 | if (response.turnPage) { 198 | var nextPageToken = response.data.nextPageToken; 199 | do { 200 | if (isTimeUp(today)) { 201 | break; 202 | } else { 203 | newURL = url + '&pageToken=' + encodeURIComponent(nextPageToken); 204 | var newResponse = getNewScriptsData(newURL); 205 | var turnPage = newResponse.turnPage; 206 | nextPageToken = newResponse.data.nextPageToken; 207 | } 208 | } while (turnPage); 209 | } 210 | } 211 | 212 | function getNewScriptsData(url) { 213 | var response = UrlFetchApp.fetch(url, options); 214 | if (response.getResponseCode() !== 200 ) { 215 | Logger.log(response); 216 | } 217 | var data = JSON.parse(response); 218 | var processes = data.processes; 219 | var turnPage = true; 220 | if (processes !== undefined) { 221 | var nextPageToken = data.nextPageToken; 222 | userProperties.setProperty('nextTempPageToken', nextPageToken); 223 | if (logFreshData(processes)) { 224 | turnPage = true; 225 | } else { 226 | turnPage = false; 227 | } 228 | } else { 229 | turnPage = false; 230 | } 231 | return { 232 | "data": data, 233 | "turnPage": turnPage 234 | } 235 | } 236 | 237 | function logFreshData(processes) { 238 | var sheetName = scriptLog; 239 | var activeSheet = ss.getSheetByName(sheetName); 240 | var hashValues = activeSheet.getRange(2,1,activeSheet.getLastRow()-1,1).getValues().toString(); 241 | for (var i = 0; i < processes.length; i++) { 242 | var process = processes[i]; 243 | var projectName = process.projectName; 244 | var functionName = process.functionName; 245 | var processType = process.processType; 246 | var processStatus = process.processStatus; 247 | var userAccessLevel = process.userAccessLevel; 248 | var startTime = process.startTime; 249 | var duration = process.duration; 250 | var hashInput = projectName + functionName + processType + processStatus + userAccessLevel + startTime + duration; 251 | var triggerHash = MD5(hashInput); 252 | var terminateFunction = true; 253 | if (hashValues.indexOf(triggerHash) == -1) { 254 | if (projectName !== 'undefined' && functionName !== 'undefined' && processType !== 'undefined' && userAccessLevel !== 'undefined') { 255 | activeSheet.appendRow( 256 | [ 257 | triggerHash, 258 | projectName, 259 | functionName, 260 | processType, 261 | processStatus, 262 | userAccessLevel, 263 | Utilities.formatDate(new Date(startTime), Session.getScriptTimeZone(), "MM/dd/yyyy HH:mm:ss"), 264 | duration.replace("s","") 265 | ] 266 | ); 267 | terminateFunction = false; 268 | } 269 | } else { 270 | var triggers = ScriptApp.getProjectTriggers(); 271 | for (var j = 0; j < triggers.length; j++) { 272 | var timeTriggerCont = userProperties.getProperty('newTimeTriggerFresh'); 273 | if (triggers[j].getUniqueId() == timeTriggerCont) { 274 | ScriptApp.deleteTrigger(triggers[j]); 275 | } 276 | } 277 | terminateFunction = true; 278 | break; 279 | } 280 | } 281 | if (terminateFunction) { 282 | return false; 283 | } else { 284 | return true; 285 | } 286 | } 287 | 288 | function removeEmptyColumns(sheetName) { 289 | var activeSheet = ss.getSheetByName(sheetName) 290 | var maxColumns = activeSheet.getMaxColumns(); 291 | var lastColumn = activeSheet.getLastColumn(); 292 | if (maxColumns-lastColumn != 0){ 293 | activeSheet.deleteColumns(lastColumn+1, maxColumns-lastColumn); 294 | } 295 | } 296 | 297 | function isTimeUp(today) { 298 | var now = new Date(); 299 | return now.getTime() - today.getTime() > 240000; // timeout at 4 mins. 300 | } 301 | -------------------------------------------------------------------------------- /Metrics (GAS)/README.md: -------------------------------------------------------------------------------- 1 | # Capture Google Apps Script Metrics Using REST APIs 2 | 3 | Using [Method: processes.list](https://developers.google.com/apps-script/api/reference/rest/v1/processes/list), we could now log and analyse our google apps script execution stats. 4 | -------------------------------------------------------------------------------- /Metrics (GAS)/appscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Asia/Kolkata", 3 | "dependencies": { 4 | }, 5 | "oauthScopes": [ 6 | "https://www.googleapis.com/auth/script.external_request", 7 | "https://www.googleapis.com/auth/script.processes", 8 | "https://www.googleapis.com/auth/spreadsheets", 9 | "https://www.googleapis.com/auth/script.scriptapp" 10 | ], 11 | "exceptionLogging": "STACKDRIVER" 12 | } -------------------------------------------------------------------------------- /Metrics (GAS)/md5.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * ------------------------------------------ 3 | * MD5 function for GAS(GoogleAppsScript) 4 | * 5 | * You can get a MD5 hash value and even a 4digit short Hash value of a string. 6 | * ------------------------------------------ 7 | * Usage1: 8 | * `=MD5("YourStringToHash")` 9 | * or 10 | * `=MD5( A1 )` with the same string at A1 cell 11 | * result: 12 | * `FCE7453B7462D9DE0C56AFCCFB756193`. 13 | * For your sure-ness you can verify it in your terminal as below. 14 | * `$ md5 -s "YourStringToHash"` 15 | * Usage2: 16 | * `=MD5("YourStringToHash", true)` for short Hash 17 | * result: 18 | * `6MQH` 19 | * Note that it has more conflict probability. 20 | * 21 | * How to install: 22 | * Copy the scipt, pase it at [Tools]-[Script Editor]-[] 23 | * or go https://script.google.com and paste it. 24 | * For more details go: 25 | * https://developers.google.com/apps-script/articles/ 26 | * Latest version: 27 | * https://gist.github.com/KEINOS/78cc23f37e55e848905fc4224483763d 28 | * Author: 29 | * KEINOS @ https://github.com/keinos 30 | * Reference and thanks to: 31 | * https://stackoverflow.com/questions/7994410/hash-of-a-cell-text-in-google-spreadsheet 32 | * ------------------------------------------ 33 | * 34 | * @param {string} input The value to hash. 35 | * @param {boolean} isShortMode Set true for 4 digit shortend hash, else returns usual MD5 hash. 36 | * @return {string} The hashed input 37 | * @customfunction 38 | * 39 | */ 40 | function MD5( input, isShortMode ) 41 | { 42 | var txtHash = ''; 43 | var rawHash = Utilities.computeDigest( 44 | Utilities.DigestAlgorithm.MD5, 45 | input, 46 | Utilities.Charset.UTF_8 ); 47 | 48 | var isShortMode = ( isShortMode == true ) ? true : false; 49 | 50 | if ( ! isShortMode ) { 51 | for ( i = 0; i < rawHash.length; i++ ) { 52 | 53 | var hashVal = rawHash[i]; 54 | 55 | if ( hashVal < 0 ) { 56 | hashVal += 256; 57 | }; 58 | if ( hashVal.toString( 16 ).length == 1 ) { 59 | txtHash += '0'; 60 | }; 61 | txtHash += hashVal.toString( 16 ); 62 | }; 63 | } else { 64 | for ( j = 0; j < 16; j += 8 ) { 65 | 66 | hashVal = ( rawHash[j] + rawHash[j+1] + rawHash[j+2] + rawHash[j+3] ) 67 | ^ ( rawHash[j+4] + rawHash[j+5] + rawHash[j+6] + rawHash[j+7] ); 68 | 69 | if ( hashVal < 0 ) { 70 | hashVal += 1024; 71 | }; 72 | if ( hashVal.toString( 36 ).length == 1 ) { 73 | txtHash += "0"; 74 | }; 75 | 76 | txtHash += hashVal.toString( 36 ); 77 | }; 78 | }; 79 | 80 | // change below to "txtHash.toLowerCase()" for lower case result. 81 | return txtHash.toLowerCase(); 82 | 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Apps Script 2 | A collection of Google Apps Script that I've worked on over time. 3 | -------------------------------------------------------------------------------- /Random/Employee certificate/code.gs: -------------------------------------------------------------------------------- 1 | var slideTemplateId = "SLIDE-ID-GOES-HERE"; // Sample: https://docs.google.com/spreadsheets/d/1cgK1UETpMF5HWaXfRE6c0iphWHhl7v-dQ81ikFtkIVk 2 | var tempFolderId = "TEMPORARY-FOLDER-ID-GOES-HERE"; // Create an empty folder in Google Drive 3 | 4 | /** 5 | * Creates a custom menu "Appreciation" in the spreadsheet 6 | * with drop-down options to create and send certificates 7 | */ 8 | function onOpen(e) { 9 | var ui = SpreadsheetApp.getUi(); 10 | ui.createMenu('Appreciation') 11 | .addItem('Create certificates', 'createCertificates') 12 | .addSeparator() 13 | .addItem('Send certificates', 'sendCertificates') 14 | .addToUi(); 15 | } 16 | 17 | /** 18 | * Creates a personalized certificate for each employee 19 | * and stores every individual Slides doc on Google Drive 20 | */ 21 | function createCertificates() { 22 | 23 | // Load the Google Slide template file 24 | var template = DriveApp.getFileById(slideTemplateId); 25 | 26 | // Get all employee data from the spreadsheet and identify the headers 27 | var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); 28 | var values = sheet.getDataRange().getValues(); 29 | var headers = values[0]; 30 | var empNameIndex = headers.indexOf("Employee Name"); 31 | var dateIndex = headers.indexOf("Date"); 32 | var managerNameIndex = headers.indexOf("Manager Name"); 33 | var titleIndex = headers.indexOf("Title"); 34 | var compNameIndex = headers.indexOf("Company Name"); 35 | var empEmailIndex = headers.indexOf("Employee Email"); 36 | var empSlideIndex = headers.indexOf("Employee Slide"); 37 | var statusIndex = headers.indexOf("Status"); 38 | 39 | // Iterate through each row to capture individual details 40 | for (var i = 1; i < values.length; i++) { 41 | var rowData = values[i]; 42 | var empName = rowData[empNameIndex]; 43 | var date = rowData[dateIndex]; 44 | var managerName = rowData[managerNameIndex]; 45 | var title = rowData[titleIndex]; 46 | var compName = rowData[compNameIndex]; 47 | 48 | // Make a copy of the Slide template and rename it with employee name 49 | var tempFolder = DriveApp.getFolderById(tempFolderId); 50 | var empSlideId = template.makeCopy(tempFolder).setName(empName).getId(); 51 | var empSlide = SlidesApp.openById(empSlideId).getSlides()[0]; 52 | 53 | // Replace placeholder values with actual employee related details 54 | empSlide.replaceAllText("Employee Name", empName); 55 | empSlide.replaceAllText("Date", "Date: " + Utilities.formatDate(date, Session.getScriptTimeZone(), "MMMM dd, yyyy")); 56 | empSlide.replaceAllText("Your Name", managerName); 57 | empSlide.replaceAllText("Title", title); 58 | empSlide.replaceAllText("Company Name", compName); 59 | 60 | // Update the spreadsheet with the new Slide Id and status 61 | sheet.getRange(i + 1, empSlideIndex + 1).setValue(empSlideId); 62 | sheet.getRange(i + 1, statusIndex + 1).setValue("CREATED"); 63 | SpreadsheetApp.flush(); 64 | } 65 | } 66 | 67 | /** 68 | * Send an email to each individual employee 69 | * with a PDF attachment of their appreciation certificate 70 | */ 71 | function sendCertificates() { 72 | 73 | // Get all employee data from the spreadsheet and identify the headers 74 | var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); 75 | var values = sheet.getDataRange().getValues(); 76 | var headers = values[0]; 77 | var empNameIndex = headers.indexOf("Employee Name"); 78 | var dateIndex = headers.indexOf("Date"); 79 | var managerNameIndex = headers.indexOf("Manager Name"); 80 | var titleIndex = headers.indexOf("Title"); 81 | var compNameIndex = headers.indexOf("Company Name"); 82 | var empEmailIndex = headers.indexOf("Employee Email"); 83 | var empSlideIndex = headers.indexOf("Employee Slide"); 84 | var statusIndex = headers.indexOf("Status"); 85 | 86 | // Iterate through each row to capture individual details 87 | for (var i = 1; i < values.length; i++) { 88 | var rowData = values[i]; 89 | var empName = rowData[empNameIndex]; 90 | var date = rowData[dateIndex]; 91 | var managerName = rowData[managerNameIndex]; 92 | var title = rowData[titleIndex]; 93 | var compName = rowData[compNameIndex]; 94 | var empSlideId = rowData[empSlideIndex]; 95 | var empEmail = rowData[empEmailIndex]; 96 | 97 | // Load the employee's personalized Google Slide file 98 | var attachment = DriveApp.getFileById(empSlideId); 99 | 100 | // Setup the required parameters and send them the email 101 | var senderName = "CertBot"; 102 | var subject = empName + ", you're awesome!"; 103 | var body = "Please find your employee appreciation certificate attached." 104 | + "\n\n" + compName + " team"; 105 | GmailApp.sendEmail(empEmail, subject, body, { 106 | attachments: [attachment.getAs(MimeType.PDF)], 107 | name: senderName 108 | }); 109 | 110 | // Update the spreadsheet with email status 111 | sheet.getRange(i + 1, statusIndex + 1).setValue("SENT"); 112 | SpreadsheetApp.flush(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Random/Icons/ACCEPTED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/ACCEPTED.png -------------------------------------------------------------------------------- /Random/Icons/AUDIO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/AUDIO.png -------------------------------------------------------------------------------- /Random/Icons/CANCELLED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/CANCELLED.png -------------------------------------------------------------------------------- /Random/Icons/CHAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/CHAT.png -------------------------------------------------------------------------------- /Random/Icons/DRAFT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/DRAFT.png -------------------------------------------------------------------------------- /Random/Icons/DRAWING.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/DRAWING.png -------------------------------------------------------------------------------- /Random/Icons/FILE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/FILE.png -------------------------------------------------------------------------------- /Random/Icons/FOLDER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/FOLDER.png -------------------------------------------------------------------------------- /Random/Icons/IMAGE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/IMAGE.png -------------------------------------------------------------------------------- /Random/Icons/IMPORTANT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/IMPORTANT.png -------------------------------------------------------------------------------- /Random/Icons/INBOX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/INBOX.png -------------------------------------------------------------------------------- /Random/Icons/README.md: -------------------------------------------------------------------------------- 1 | Repo for random icon images 2 | -------------------------------------------------------------------------------- /Random/Icons/SCRIPT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/SCRIPT.png -------------------------------------------------------------------------------- /Random/Icons/SELF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/SELF.png -------------------------------------------------------------------------------- /Random/Icons/SENT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/SENT.png -------------------------------------------------------------------------------- /Random/Icons/SHARED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/SHARED.png -------------------------------------------------------------------------------- /Random/Icons/SITES.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/SITES.png -------------------------------------------------------------------------------- /Random/Icons/SPAM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/SPAM.png -------------------------------------------------------------------------------- /Random/Icons/STARRED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/STARRED.png -------------------------------------------------------------------------------- /Random/Icons/TENTATIVE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/TENTATIVE.png -------------------------------------------------------------------------------- /Random/Icons/TRASH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/TRASH.png -------------------------------------------------------------------------------- /Random/Icons/TWITTER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/TWITTER.png -------------------------------------------------------------------------------- /Random/Icons/UNREAD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/UNREAD.png -------------------------------------------------------------------------------- /Random/Icons/VIDEO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choraria/google-apps-script/38d0be9a005fe5d074c24224a9bdf699d9d440b1/Random/Icons/VIDEO.png -------------------------------------------------------------------------------- /Random/Meetings Heatmap/Chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Random/Meetings Heatmap/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 33 | 34 | 60 | 61 | 62 | 63 | 64 |
65 | 66 |
67 |
68 |


69 |

Meetings Heatmap.

70 |


71 |
Please choose the 'Start Date':
72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | Mandatory 80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 | 88 |
89 | 90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Random/Meetings Heatmap/Processing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 45 | 46 | 47 | 48 | 49 |
50 | 51 |
52 |
53 |


54 |

Meetings Heatmap.

55 |


56 |
All events are being logged from your calendar, from the specified 'Start Date'.

So far, it has been logged till...
57 |

58 |


59 |
The script automatically pauses and resumes to avoid execution timeouts.

This page would reload automatically and
will also notifiy you via Email, soon as this is done!

60 |
You may close the browser and resume your other tasks.
...or feel free to stick around and see the dates jump.
61 |
62 |
63 | 64 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Random/Meetings Heatmap/README.md: -------------------------------------------------------------------------------- 1 | # Meetings Heatmap 2 | A Github-like visualization (heatmap) of all the meetings you've been invited to. 3 | 4 | ## Demo 5 | You can 'Make a copy' of the script already hosted on my account, [here](https://script.google.com/d/16uqhVFAsEhQLBTySW8AYmtggHqih2YkDXzR7BZXHvxdm9FZiH3IOXdgG/edit?usp=sharing) 6 | -------------------------------------------------------------------------------- /Random/Meetings Heatmap/code.gs: -------------------------------------------------------------------------------- 1 | var sessionUser = Session.getEffectiveUser(); 2 | var spreadsheetName = '[DND] Meetings Heatmap - ' + sessionUser; 3 | var sheetName = 'Meetings' 4 | var spreadsheetID; 5 | if(DriveApp.getFilesByName(spreadsheetName).hasNext()) { 6 | spreadsheetID = DriveApp.getFilesByName(spreadsheetName).next().getId(); 7 | } else { 8 | spreadsheetID = null; 9 | } 10 | var activeSheet; 11 | if(spreadsheetID !== null) { 12 | activeSheet = SpreadsheetApp.openById(spreadsheetID).getSheetByName(sheetName); 13 | } else { 14 | activeSheet = null; 15 | } 16 | 17 | function doGet(e) { 18 | if (Object.keys(e.parameter).length === 0) { 19 | var htmlFile; 20 | var pageTitle; 21 | if(spreadsheetID !== null && activeSheet !== null) { 22 | var lastDate = activeSheet.getRange(activeSheet.getLastRow(), 1).getValue(); 23 | var today = new Date(); 24 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 25 | var MILLIS_PER_TWO_DAY = 1000 * 60 * 60 * 24 * 2; 26 | var yesterday = new Date(new Date().getTime() - MILLIS_PER_DAY) 27 | var dayBefore = new Date(new Date().getTime() - MILLIS_PER_TWO_DAY) 28 | if ((lastDate.getDate() == today.getDate() && lastDate.getMonth() == today.getMonth() && lastDate.getFullYear() == today.getFullYear()) || (lastDate.getDate() == yesterday.getDate() && lastDate.getMonth() == yesterday.getMonth() && lastDate.getFullYear() == yesterday.getFullYear()) || (lastDate.getDate() == dayBefore.getDate() && lastDate.getMonth() == dayBefore.getMonth() && lastDate.getFullYear() == dayBefore.getFullYear())) { 29 | htmlFile = 'Chart'; 30 | pageTitle = 'Meetings Heatmap'; 31 | } else { 32 | htmlFile = 'Processing'; 33 | pageTitle = 'Data Processing...'; 34 | } 35 | } else { 36 | htmlFile = 'Index'; 37 | pageTitle = 'Meetings Heatmap | Index'; 38 | } 39 | } 40 | return HtmlService.createHtmlOutputFromFile(htmlFile).setTitle(pageTitle); 41 | } 42 | 43 | function getUserInput(formData) { 44 | var startDate = new Date(formData.startDate); 45 | calEventsOriginal(startDate); 46 | } 47 | 48 | function calEventsOriginal(startDate) { 49 | if (spreadsheetID == null) { 50 | var createSpreadsheet = SpreadsheetApp.create(spreadsheetName) 51 | spreadsheetID = createSpreadsheet.getId(); 52 | activeSheet = SpreadsheetApp.openById(spreadsheetID).insertSheet().setName(sheetName); 53 | activeSheet.appendRow(['Date', 'Meetings']); 54 | SpreadsheetApp.openById(spreadsheetID).deleteSheet(SpreadsheetApp.openById(spreadsheetID).getSheetByName('Sheet1')) 55 | removeEmptyColumns(); 56 | var startDate = new Date(startDate); 57 | var today = new Date(); 58 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 59 | var nextDay = new Date(startDate.getTime() + MILLIS_PER_DAY) 60 | var totalDays = Math.floor((new Date().getTime() - startDate.getTime())/(24*3600*1000)) 61 | for (var i = 0; i < totalDays; i++) { 62 | if (isTimeUpOriginal_(today)) { 63 | ScriptApp.newTrigger("getLastDate") 64 | .timeBased() 65 | .everyMinutes(1) 66 | .create(); 67 | break; 68 | } else { 69 | if (nextDay < today) { 70 | var events = CalendarApp.getDefaultCalendar().getEventsForDay(nextDay).length 71 | activeSheet.appendRow([nextDay, events]) 72 | nextDay = new Date(nextDay.getTime() + MILLIS_PER_DAY) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | function isTimeUpOriginal_(today) { 80 | var now = new Date(); 81 | return now.getTime() - today.getTime() > 3000; 82 | } 83 | 84 | 85 | function isTimeUp_(today) { 86 | var now = new Date(); 87 | return now.getTime() - today.getTime() > 30000; 88 | } 89 | 90 | function getLastDate() { 91 | var lastDate = activeSheet.getRange(activeSheet.getLastRow(), 1).getValue(); 92 | calEventsRepeat(lastDate) 93 | } 94 | 95 | function calEventsRepeat(lastDate) { 96 | var lastDate = new Date(lastDate); 97 | var today = new Date(); 98 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 99 | var nextDay = new Date(lastDate.getTime() + MILLIS_PER_DAY) 100 | var totalDays = Math.floor((new Date().getTime() - lastDate.getTime())/(24*3600*1000)) 101 | for (var i = 0; i < totalDays; i++) { 102 | if (isTimeUp_(today)) { 103 | break; 104 | } else { 105 | if (nextDay < today) { 106 | if (nextDay.getDate() == today.getDate() && nextDay.getMonth() == today.getMonth() && nextDay.getFullYear() == today.getFullYear()) { 107 | var triggers = ScriptApp.getProjectTriggers(); 108 | for (var i = 0; i < triggers.length; i++) { 109 | ScriptApp.deleteTrigger(triggers[i]); 110 | } 111 | ScriptApp.newTrigger("getPreviousDate") 112 | .timeBased() 113 | .everyDays(1) 114 | .create(); 115 | var subject = 'Your Meetings Heatmap Is Ready!' 116 | var linkAddr = webAppURL(linkAddr); 117 | var message = 'Please visit the following link to access the visualization -\n' + linkAddr; 118 | MailApp.sendEmail(sessionUser, subject, message); 119 | break; 120 | } else { 121 | var events = CalendarApp.getDefaultCalendar().getEventsForDay(nextDay).length; 122 | activeSheet.appendRow([nextDay, events]); 123 | nextDay = new Date(nextDay.getTime() + MILLIS_PER_DAY); 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | function getPreviousDate() { 131 | var previousDate = activeSheet.getRange(activeSheet.getLastRow(), 1).getValue(); 132 | calEventsDaily(previousDate); 133 | } 134 | 135 | function calEventsDaily(previousDate) { 136 | var previousDate = new Date(previousDate); 137 | var today = new Date(); 138 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 139 | var nextDay = new Date(previousDate.getTime() + MILLIS_PER_DAY) 140 | var totalDays = Math.floor((new Date().getTime() - previousDate.getTime())/(24*3600*1000)) 141 | for (var i = 0; i < totalDays; i++) { 142 | if (isTimeUp_(today)) { 143 | break; 144 | } else { 145 | if (nextDay < today && nextDay.getDate() !== today.getDate()) { 146 | var events = CalendarApp.getDefaultCalendar().getEventsForDay(nextDay).length; 147 | activeSheet.appendRow([nextDay, events]); 148 | nextDay = new Date(nextDay.getTime() + MILLIS_PER_DAY); 149 | } 150 | } 151 | } 152 | } 153 | 154 | function getEvents() { 155 | var range = activeSheet.getRange(2,1,activeSheet.getLastRow()-1,2); 156 | var values = range.getValues(); 157 | for(var i=0;i Ghost/Config.gs: -------------------------------------------------------------------------------- 1 | const REVUE_API_KEY = "..."; 2 | const REVUE_LIST_ID = "..."; // run REVUE_API.listAllLists() 3 | const GHOST_ACCESS_TOKEN = "..."; 4 | const GHOST_ADMIN_DOMAIN = "..."; 5 | 6 | const REVUE_BASE_URL = "https://www.getrevue.co/api"; 7 | const GHOST_BASE_URL = `https://${GHOST_ADMIN_DOMAIN}/ghost/api`; 8 | const REVUE_SHEET_NAME = "Revue"; 9 | const GHOST_SHEET_NAME = "Ghost"; 10 | 11 | const scriptProperties = PropertiesService.getScriptProperties(); 12 | 13 | const startSync = () => importRevueList(); 14 | 15 | const continueSync = () => { 16 | let jwt = importGhostMembers(); 17 | if (jwt) { 18 | if (syncWithGhost(jwt)) { 19 | if (syncWithRevue()) { 20 | console.log("Sync was successful!"); 21 | } else { 22 | console.log("syncWithRevue() falied at startSync()."); 23 | } 24 | } else { 25 | console.log("syncWithGhost(jwt) falied at startSync()."); 26 | } 27 | } else { 28 | console.log("importGhostMembers() falied at startSync()."); 29 | } 30 | } 31 | 32 | const scheduleSync = () => ScriptApp.newTrigger("importRevueList") 33 | .timeBased() 34 | .atHour(0) 35 | .nearMinute(1) 36 | .everyDays(1) 37 | .inTimezone(Session.getScriptTimeZone()) 38 | .create(); 39 | -------------------------------------------------------------------------------- /Random/Revue <> Ghost/Ghost.gs: -------------------------------------------------------------------------------- 1 | let [id, secret] = GHOST_ACCESS_TOKEN.split(':'); 2 | 3 | const GHOST_API = { 4 | getMembers: function (jwt, nextPage) { // https://ghost.org/docs/admin-api/#members 5 | let url = `${GHOST_BASE_URL}/admin/members/?fields=email,name,created_at&page=${nextPage ? nextPage : 1}`; 6 | let options = { 7 | 'method': 'GET', 8 | 'headers': { 9 | 'Authorization': `Ghost ${jwt}`, // https://ghost.org/docs/admin-api/#token-authentication 10 | }, 11 | 'muteHttpExceptions': true, 12 | 'contentType': 'application/json', 13 | }; 14 | let res = UrlFetchApp.fetch(url, options); 15 | if (res.getResponseCode() === 200) { 16 | return JSON.parse(res); 17 | } else { 18 | console.log({ 19 | responseCode: res.getResponseCode(), 20 | responseMessage: res.getContentText(), 21 | }); 22 | return false; 23 | } 24 | }, 25 | addMember: function (jwt, email, name) { 26 | let url = `${GHOST_BASE_URL}/admin/members/`; 27 | let options = { 28 | 'method': 'POST', 29 | 'headers': { 30 | 'Authorization': `Ghost ${jwt}`, // https://ghost.org/docs/admin-api/#token-authentication 31 | }, 32 | 'muteHttpExceptions': true, 33 | 'contentType': 'application/json', 34 | 'payload': JSON.stringify({ 35 | "members": [ 36 | { 37 | "email": email, 38 | "name": name, 39 | } 40 | ] 41 | }), 42 | } 43 | let res = UrlFetchApp.fetch(url, options); 44 | if (res.getResponseCode() === 200 || res.getResponseCode() === 201) { 45 | return JSON.parse(res); 46 | } else { 47 | console.log({ 48 | responseCode: res.getResponseCode(), 49 | responseMessage: res.getContentText(), 50 | }); 51 | return false; 52 | } 53 | } 54 | } 55 | 56 | function importGhostMembers(jwt) { 57 | jwt = jwt ? jwt : createJwt(); 58 | let ghostMembers = GHOST_API.getMembers(jwt); 59 | if (ghostMembers) { 60 | let data = [["email", "first_name", "last_name", "created_at"]]; 61 | while (data.length < ghostMembers.meta.pagination.total) { 62 | ghostMembers.members.forEach(member => { 63 | let name = member.name ? member.name.split(" ") : null; 64 | let first_name = name ? name.shift() : ''; 65 | let last_name = name ? name.join(" ") : ''; 66 | data.push([member.email.toLowerCase(), first_name, last_name, member.created_at.replace("T", " ").replace("Z", "")]); 67 | }); 68 | ghostMembers = GHOST_API.getMembers(jwt, ghostMembers.meta.pagination.next); 69 | } 70 | const ss = SpreadsheetApp.getActiveSpreadsheet(); 71 | let activeSheet = ss.getSheetByName(GHOST_SHEET_NAME); 72 | if (!activeSheet) { 73 | ss.insertSheet().setName(GHOST_SHEET_NAME); 74 | activeSheet = ss.getSheetByName(GHOST_SHEET_NAME); 75 | activeSheet.getRange(1, 1, data.length, data[0].length).setValues(data); 76 | } else { 77 | const headers = activeSheet.getRange("A1:D1").getValues(); 78 | headers.length === 4 ? null : activeSheet.getRange("A1:D1").setValues([["email", "first_name", "last_name", "created_at"]]); 79 | const emailDataRange = activeSheet.getRange("A2:A"); 80 | let sheetData = []; 81 | data.slice(1, data.length).forEach(row => !emailDataRange.createTextFinder(row[0]).matchEntireCell(true).findNext() ? sheetData.push(row) : null) 82 | sheetData.length > 0 ? activeSheet.getRange(activeSheet.getLastRow() + 1, 1, sheetData.length, data[0].length).setValues(sheetData) : null; 83 | activeSheet.getDataRange().sort({ column: 4, ascending: false }); 84 | } 85 | activeSheet.setFrozenRows(1); 86 | activeSheet.getMaxColumns() > 4 ? activeSheet.deleteColumns(5, activeSheet.getMaxColumns() - 5 + 1) : null; 87 | } else { 88 | console.log("GHOST_API.getMembers failed"); 89 | } 90 | return jwt; 91 | } 92 | 93 | function syncWithRevue() { 94 | const ss = SpreadsheetApp.getActiveSpreadsheet(); 95 | const revueSheet = ss.getSheetByName(REVUE_SHEET_NAME); 96 | const ghostSheet = ss.getSheetByName(GHOST_SHEET_NAME); 97 | const revueData = revueSheet.getRange("A2:C").getValues(); 98 | const ghostData = ghostSheet.getRange("A2:C").getValues(); 99 | const revueEmails = revueData.map(cols => cols[0]); 100 | const ghostEmails = ghostData.map(cols => cols[0]); 101 | const freshEmails = ghostEmails.filter(email => !revueEmails.includes(email)); 102 | const dataToSync = ghostData.filter(row => freshEmails.includes(row[0])) 103 | if (dataToSync.length > 0) { 104 | dataToSync.forEach(row => REVUE_API.addSubscriber(row[0], row[1] === '' ? null : row[1], row[2] === '' ? null : row[2])); 105 | importRevueList(); 106 | } else { 107 | console.log("No new emails in Ghost to sync with Revue!"); 108 | } 109 | return true; 110 | } 111 | 112 | function createJwt() { // adopted from https://www.labnol.org/code/json-web-token-201128 113 | const header = Utilities.base64EncodeWebSafe(JSON.stringify({ 114 | alg: 'HS256', 115 | kid: id, 116 | typ: 'JWT' 117 | })).replace(/=+$/, ''); 118 | 119 | const now = Date.now(); 120 | let expires = new Date(now); 121 | expires.setMinutes(expires.getMinutes() + 5); 122 | 123 | const payload = Utilities.base64EncodeWebSafe(JSON.stringify({ 124 | exp: Math.round(expires.getTime() / 1000), 125 | iat: Math.round(now / 1000), 126 | aud: '/v3/admin/' 127 | })).replace(/=+$/, ''); 128 | 129 | // https://gist.github.com/tanaikech/707b2cd2705f665a11b1ceb2febae91e#sample-script 130 | // Convert hex 'secret' to byte array then base64Encode 131 | secret = secret 132 | .match(/.{2}/g) 133 | .map((e) => 134 | parseInt(e[0], 16).toString(2).length == 4 135 | ? parseInt(e, 16) - 256 136 | : parseInt(e, 16) 137 | ); 138 | 139 | const toSign = Utilities.newBlob(`${header}.${payload}`).getBytes(); 140 | const signatureBytes = Utilities.computeHmacSha256Signature(toSign, secret); 141 | const signature = Utilities.base64EncodeWebSafe(signatureBytes).replace(/=+$/, ''); 142 | const jwt = `${header}.${payload}.${signature}`; 143 | return jwt; 144 | }; 145 | -------------------------------------------------------------------------------- /Random/Revue <> Ghost/Revue.gs: -------------------------------------------------------------------------------- 1 | const REVUE_API = { 2 | listAllLists: function () { // https://www.getrevue.co/api#get-/v2/lists 3 | let url = `${REVUE_BASE_URL}/v2/lists`; 4 | let options = { 5 | 'method': 'GET', 6 | 'headers': { 7 | 'Authorization': `Token ${REVUE_API_KEY}`, 8 | }, 9 | 'muteHttpExceptions': true, 10 | 'contentType': 'application/json', 11 | }; 12 | let res = UrlFetchApp.fetch(url, options); 13 | if (res.getResponseCode() === 200) { 14 | return JSON.parse(res); 15 | } else { 16 | console.log({ 17 | responseCode: res.getResponseCode(), 18 | responseMessage: res.getContentText(), 19 | }); 20 | return false; 21 | } 22 | }, 23 | startListExport: function (listId) { // https://www.getrevue.co/api#post-/v2/exports/lists/-id- 24 | let url = `${REVUE_BASE_URL}/v2/exports/lists/${listId}`; 25 | let options = { 26 | 'method': 'POST', 27 | 'headers': { 28 | 'Authorization': `Token ${REVUE_API_KEY}`, 29 | }, 30 | 'muteHttpExceptions': true, 31 | 'contentType': 'application/json', 32 | }; 33 | let res = UrlFetchApp.fetch(url, options); 34 | if (res.getResponseCode() === 200) { 35 | return JSON.parse(res); 36 | } else { 37 | console.log({ 38 | responseCode: res.getResponseCode(), 39 | responseMessage: res.getContentText(), 40 | }); 41 | return false; 42 | } 43 | }, 44 | getExport: function (exportId) { // https://www.getrevue.co/api#get-/v2/exports/-id- 45 | let url = `${REVUE_BASE_URL}/v2/exports/${exportId}`; 46 | let options = { 47 | 'method': 'GET', 48 | 'headers': { 49 | 'Authorization': `Token ${REVUE_API_KEY}`, 50 | }, 51 | 'muteHttpExceptions': true, 52 | 'contentType': 'application/json', 53 | }; 54 | let res = UrlFetchApp.fetch(url, options); 55 | if (res.getResponseCode() === 200) { 56 | return JSON.parse(res); 57 | } else { 58 | console.log({ 59 | responseCode: res.getResponseCode(), 60 | responseMessage: res.getContentText(), 61 | }); 62 | return false; 63 | } 64 | }, 65 | addSubscriber: function (email, firstName, lastName) { // https://www.getrevue.co/api#post-/v2/subscribers 66 | let url = `${REVUE_BASE_URL}/v2/subscribers`; 67 | let options = { 68 | 'method': 'POST', 69 | 'payload': JSON.stringify({ 70 | "email": email, 71 | "first_name": firstName, 72 | "last_name": lastName, 73 | "double_opt_in": false 74 | }), 75 | 'headers': { 76 | 'Authorization': `Token ${REVUE_API_KEY}`, 77 | }, 78 | 'muteHttpExceptions': true, 79 | 'contentType': 'application/json', 80 | }; 81 | let res = UrlFetchApp.fetch(url, options); 82 | if (res.getResponseCode() === 200) { 83 | return JSON.parse(res); 84 | } else { 85 | console.log({ 86 | responseCode: res.getResponseCode(), 87 | responseMessage: res.getContentText(), 88 | }); 89 | return false; 90 | } 91 | } 92 | } 93 | 94 | function importRevueList() { 95 | let exportId = scriptProperties.getProperty("exportId"); 96 | let timeTrigger = scriptProperties.getProperty("timeTriggerId"); 97 | if (!exportId) { 98 | const startExport = REVUE_API.startListExport(REVUE_LIST_ID); 99 | if (startExport) { 100 | exportId = startExport.id; 101 | } else { 102 | console.log("REVUE_API.startListExport failed"); 103 | return false; 104 | } 105 | } 106 | const exportData = REVUE_API.getExport(exportId); 107 | if (exportData) { 108 | const subscribed_url = exportData.subscribed_url; 109 | if (subscribed_url) { 110 | if (importData(subscribed_url)) { 111 | exportId ? scriptProperties.deleteProperty("exportId") : null; 112 | if (timeTrigger) { 113 | ScriptApp.getProjectTriggers().filter(trigger => trigger.getUniqueId() === timeTrigger ? ScriptApp.deleteTrigger(trigger) : null) 114 | scriptProperties.deleteProperty("timeTriggerId"); 115 | } 116 | continueSync(); 117 | } else { 118 | console.log("importData(subscribed_url) failed"); 119 | return false; 120 | } 121 | } else { 122 | scriptProperties.setProperty("exportId", exportId); 123 | if (!timeTrigger) { 124 | timeTrigger = ScriptApp.newTrigger("importRevueList") 125 | .timeBased() 126 | .everyMinutes(1) 127 | .create() 128 | .getUniqueId(); 129 | scriptProperties.setProperty("timeTriggerId", timeTrigger); 130 | } 131 | } 132 | } else { 133 | console.log("REVUE_API.getExport failed"); 134 | return false; 135 | } 136 | } 137 | 138 | function importData(url) { 139 | const res = UrlFetchApp.fetch(url, { 'muteHttpExceptions': true }); 140 | if (res.getResponseCode() === 200) { 141 | let data = []; 142 | res.getContentText().split("\n").forEach(row => data.push(row.split(","))); 143 | data.pop(); 144 | // const json = data.slice(1, data.length).map(row => data[0].reduce((obj, curr, i) => (obj[curr] = row[i], obj), {})); 145 | 146 | const ss = SpreadsheetApp.getActiveSpreadsheet(); 147 | let activeSheet = ss.getSheetByName(REVUE_SHEET_NAME); 148 | if (!activeSheet) { 149 | ss.insertSheet().setName(REVUE_SHEET_NAME); 150 | activeSheet = ss.getSheetByName(REVUE_SHEET_NAME); 151 | activeSheet.getRange(1, 1, data.length, data[0].length).setValues(data); 152 | } else { 153 | const headers = activeSheet.getRange("A1:D1").getValues(); 154 | headers.length === 4 ? null : activeSheet.getRange("A1:D1").setValues([["email", "first_name", "last_name", "created_at"]]); 155 | const emailDataRange = activeSheet.getRange("A2:A"); 156 | let sheetData = []; 157 | data.slice(1, data.length).forEach(row => !emailDataRange.createTextFinder(row[0]).matchEntireCell(true).findNext() ? sheetData.push(row) : null) 158 | sheetData.length > 0 ? activeSheet.getRange(activeSheet.getLastRow() + 1, 1, sheetData.length, data[0].length).setValues(sheetData) : null; 159 | activeSheet.getDataRange().sort({ column: 4, ascending: false }); 160 | } 161 | activeSheet.setFrozenRows(1); 162 | activeSheet.getMaxColumns() > 4 ? activeSheet.deleteColumns(5, activeSheet.getMaxColumns() - 5 + 1) : null; 163 | return true; 164 | } else { 165 | console.log({ 166 | responseCode: res.getResponseCode(), 167 | responseMessage: res.getContentText(), 168 | }); 169 | return false; 170 | } 171 | } 172 | 173 | function syncWithGhost(jwt) { 174 | const ss = SpreadsheetApp.getActiveSpreadsheet(); 175 | const revueSheet = ss.getSheetByName(REVUE_SHEET_NAME); 176 | const ghostSheet = ss.getSheetByName(GHOST_SHEET_NAME); 177 | const revueData = revueSheet.getRange("A2:C").getValues(); 178 | const ghostData = ghostSheet.getRange("A2:C").getValues(); 179 | const revueEmails = revueData.map(cols => cols[0]); 180 | const ghostEmails = ghostData.map(cols => cols[0]); 181 | const freshEmails = revueEmails.filter(email => !ghostEmails.includes(email)); 182 | const dataToSync = revueData.filter(row => freshEmails.includes(row[0])) 183 | if (dataToSync.length > 0) { 184 | jwt = jwt ? jwt : createJwt(); 185 | dataToSync.forEach(row => GHOST_API.addMember(jwt, row[0], `${row[1]} ${row[2]}`.trim())); 186 | importGhostMembers(jwt); 187 | } else { 188 | console.log("No new emails in Revue to sync with Ghost!"); 189 | } 190 | return true; 191 | } 192 | -------------------------------------------------------------------------------- /Real-time Dashboard/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 34 | 35 | 36 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |

62 |
63 | 64 |
65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Real-time Dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Real-time dashboard using google.script.run 2 | Polling [Client-side API](https://developers.google.com/apps-script/guides/html/reference/run) in Google Apps Script to stream live data with some help from `setInterval` 3 | 4 | ## Demo 5 | You can view the final output of this setup [here](https://script.google.com/macros/s/AKfycbzm_yn_6Vsllcn6PTBfpCj0fOPUeVKkg3jWLo8CJ3v797cRgSd6/exec) 6 | -------------------------------------------------------------------------------- /Real-time Dashboard/code.gs: -------------------------------------------------------------------------------- 1 | function doGet(e) { 2 | return HtmlService.createHtmlOutputFromFile('Index').setTitle('Realtime Data'); 3 | } 4 | 5 | function randomQuotes() { 6 | var baseURL = 'https://thesimpsonsquoteapi.glitch.me/quotes'; 7 | var quotesData = UrlFetchApp.fetch(baseURL, { muteHttpExceptions: true }); 8 | var quote; 9 | var imageURL; 10 | if (quotesData.getResponseCode() == 200 || quotesData.getResponseCode() == 201) { 11 | var response = quotesData.getContentText(); 12 | var data = JSON.parse(response)[0]; 13 | quote = data["quote"]; 14 | imageURL = data["image"]; 15 | } else { 16 | quote = 'Random Quote Generator is broken!'; 17 | imageURL = 'https://cdn.shopify.com/s/files/1/1061/1924/products/Sad_Face_Emoji_large.png?v=1480481055'; 18 | } 19 | var randomQuote = { 20 | "quote": quote, 21 | "imageTag": '' 22 | } 23 | return randomQuote; 24 | } 25 | 26 | function getTime() { 27 | var now = new Date(); 28 | return now; 29 | } 30 | -------------------------------------------------------------------------------- /Rebrandly/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Rebrandly](https://rebrandly.com/) 2 | 3 | - [newLink](newLink/): This script allows you to create a new short link with the help of Rebrandly APIs 4 | -------------------------------------------------------------------------------- /Rebrandly/newLink/urlShortenerRB.gs: -------------------------------------------------------------------------------- 1 | function URLShortener() { 2 | var data = { 3 | "title": "Your Title Goes Here", 4 | "slashtag": "1234567890", 5 | "destination": "https://example.com/?utm_source=email&utm_medium=mobile&utm_content=1234567890", 6 | "domain": { 7 | "id": "YYYYYYYYYYYYYYYYYYYYYYYY" 8 | } 9 | }; 10 | var url = "https://api.rebrandly.com/v1/links" 11 | var options = { 12 | 'method': 'POST', 13 | "contentType": "application/json", 14 | 'payload': JSON.stringify(data), 15 | 'headers': { 16 | "apikey":"ZZZZZZZZZZZZZZZZZZZZZZ", 17 | "workspace":"XXXXXXXXXXXXXXXXXXXXX" 18 | }, 19 | }; 20 | var response = UrlFetchApp.fetch(url, options); 21 | var json = response.getContentText(); 22 | var data = JSON.parse(json); 23 | var id = data["id"]; 24 | var shortUrl = data["shortUrl"]; 25 | Logger.log(id,shortUrl) 26 | } 27 | -------------------------------------------------------------------------------- /Sheets/Custom Functions/GET_REDIRECT_LOCATION.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the redirect location of a url. 3 | * 4 | * @param {string} input The source URL being redirected. 5 | * @return The destination location/URL. 6 | * @customfunction 7 | */ 8 | function GET_REDIRECT_LOCATION(input) { 9 | try { // use the validator.gs library => 1OLVhM4V7DKQaPLM0025IO_Url3xr8QnnLqTlC7viE9AtEIIG_-IPVDY0 10 | if (!validator.isURL(input)) return "INVALID_URL"; 11 | } catch (err) { 12 | console.log(err); 13 | } 14 | if (input == null || input == undefined || input.toString().includes("@") || !input.toString().includes(".")) return "INVALID_URL"; 15 | let response; 16 | try { 17 | response = UrlFetchApp.fetch(input, { 18 | muteHttpExceptions: true, 19 | followRedirects: false, 20 | validateHttpsCertificates: false 21 | }); 22 | } catch (error) { 23 | console.log(error); 24 | return error.toString(); 25 | } 26 | const status = response.getResponseCode(); 27 | console.log(status); 28 | if (/3\d\d/.test(status)) { // https://en.wikipedia.org/wiki/URL_redirection#HTTP_status_codes_3xx 29 | const location = response.getAllHeaders().Location; 30 | console.log(location); 31 | return location; 32 | } else { 33 | return "NO_REDIRECTS_FOUND"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sheets/Find Precedents/Code.gs: -------------------------------------------------------------------------------- 1 | const saveToDriveAsJson = false; 2 | 3 | const ss = SpreadsheetApp.getActiveSpreadsheet(); 4 | const activeSheet = ss.getActiveSheet(); 5 | 6 | function getPrecedents() { 7 | const rawCellData = scanCurrentSheet(); 8 | let result = {}; 9 | rawCellData.forEach(cellData => { 10 | let rawRangesR1C1 = cellData.rangesR1C1; 11 | rawRangesR1C1.forEach(range => { 12 | let enumeratedData = enumerateRange(range, cellData.rowIndex, cellData.columnIndex); 13 | result[enumeratedData.cell] = Object.keys(result).includes(enumeratedData.cell) 14 | ? [...new Set(result[enumeratedData.cell].concat(enumeratedData.precedents))] 15 | : enumeratedData.precedents; 16 | }); 17 | let rawCellR1C1 = cellData.cellsR1C1; 18 | rawCellR1C1.forEach(cell => { 19 | let cellA1Notation = convertCellsFromR1C1toA1Notation(cell, cellData.rowIndex, cellData.columnIndex); 20 | result[cellA1Notation.cell] = Object.keys(result).includes(cellA1Notation.cell) 21 | ? [...new Set(result[cellA1Notation.cell].concat(cellA1Notation.precedents))] 22 | : cellA1Notation.precedents; 23 | }); 24 | }); 25 | console.log(JSON.stringify(result, null, 2)); 26 | saveToDriveAsJson ? DriveApp.createFile(`${ss.getName()} — ${activeSheet.getSheetName()} — graph.json`, JSON.stringify(result, null, 2), MimeType.PLAIN_TEXT) : null; 27 | } 28 | 29 | function scanCurrentSheet() { 30 | const formulasR1C1 = activeSheet.getDataRange().getFormulasR1C1(); 31 | let result = []; 32 | formulasR1C1.forEach((row, rowIndex) => { 33 | row.forEach((cell, columnIndex) => { 34 | let dict = {}; 35 | if (cell) { 36 | dict["formula"] = cell; 37 | dict["rowIndex"] = rowIndex; 38 | dict["columnIndex"] = columnIndex; 39 | 40 | let formattedCell = JSON.parse(JSON.stringify(cell)) 41 | .replace(/''/gmi, "'") 42 | .replace(/\\/gmi, "\\") 43 | 44 | dict["rangesR1C1"] = []; 45 | let rangesRegExPattern = `(?:[R|C]\\[-?\\d+\\]){0,2}:(?:[R|C]\\[-?\\d+\\]){0,2}`; 46 | try { 47 | cell.match(/!?(?:[R|C]\[-?\d+\]){0,2}:(?:[R|C]\[-?\d+\]){0,2}/gmi).filter(data => !data.includes("!") && data !== ":" ? dict["rangesR1C1"].push(data) : null); 48 | } catch (e) { } 49 | try { 50 | cell.match(/\b\w+!(?:[R|C]\[-?\d+\]){0,2}:(?:[R|C]\[-?\d+\]){0,2}/gmi).forEach(data => dict["rangesR1C1"].push(data)); 51 | } catch (e) { } 52 | try { 53 | let moreRangeReferences = getMatchingRangeCellRef(formattedCell, rangesRegExPattern); 54 | moreRangeReferences.length > 0 ? moreRangeReferences.forEach(data => dict["rangesR1C1"].push(JSON.parse(JSON.stringify(data)))) : null; 55 | } catch (e) { } 56 | 57 | dict["cellsR1C1"] = []; 58 | let cellsRegExPattern = `(? !data.includes("!") ? dict["cellsR1C1"].push(data) : null); 61 | } catch (e) { } 62 | try { 63 | cell.match(/\b\w+!(? dict["cellsR1C1"].push(data)); 64 | } catch (e) { } 65 | try { 66 | let moreCellReferences = getMatchingRangeCellRef(formattedCell, cellsRegExPattern); 67 | moreCellReferences.length > 0 ? moreCellReferences.forEach(data => dict["cellsR1C1"].push(JSON.parse(JSON.stringify(data)))) : null; 68 | } catch (e) { } 69 | 70 | result.push(dict); 71 | } 72 | }); 73 | }); 74 | return result; 75 | } 76 | 77 | function getMatchingRangeCellRef(cell, regexPattern) { 78 | const sheets = ss.getSheets(); 79 | let result = []; 80 | sheets.forEach(sheet => { 81 | let sheetNameFormat = sheet.getSheetName() 82 | .replace(/\\/gmi, "\\\\") 83 | .replace(/\//gmi, "\\\/") 84 | .replace(/\|/gmi, "\\\|") 85 | .replace(/\./gmi, "\\\.") 86 | .replace(/\+/gmi, "\\\+") 87 | .replace(/\*/gmi, "\\\*") 88 | .replace(/\?/gmi, "\\\?") 89 | .replace(/\^/gmi, "\\\^") 90 | .replace(/\$/gmi, "\\\$") 91 | .replace(/\(/gmi, "\\\(") 92 | .replace(/\)/gmi, "\\\)") 93 | .replace(/\[/gmi, "\\\[") 94 | .replace(/\]/gmi, "\\\]") 95 | .replace(/\{/gmi, "\\\{") 96 | .replace(/\}/gmi, "\\\}") 97 | let finalRegExPattern = new RegExp(`'${sheetNameFormat}'!${regexPattern}`, "gmi"); 98 | let matchedReferences = cell.match(finalRegExPattern); 99 | matchedReferences?.forEach(data => result.push(data)); 100 | }); 101 | return result; 102 | } 103 | 104 | function enumerateRange(range, rowIndex, columnIndex) { 105 | let enumerated = []; 106 | const lastRow = activeSheet.getLastRow(); 107 | const lastColumn = activeSheet.getLastColumn(); 108 | const isDifferentSheet = range.includes("!"); 109 | const rangeData = isDifferentSheet ? range.split("!") : null; 110 | range = isDifferentSheet ? rangeData[1] : range; 111 | let [startCell, endCell] = range.split(":"); 112 | startCell = startCell.includes("R") && startCell.includes("C") ? startCell : 113 | (!startCell.includes("R") ? `R[${0 - rowIndex}]${startCell}` : 114 | (!startCell.includes("C") ? `${startCell}C[${(0 - columnIndex)}]` : 115 | startCell 116 | ) 117 | ); 118 | endCell = endCell.includes("R") && endCell.includes("C") ? endCell : 119 | (!endCell.includes("R") ? `R[${(lastRow - 1) - rowIndex}]${endCell}` : 120 | (!endCell.includes("C") ? `${endCell}C[${(lastColumn - 1) - columnIndex}]` : 121 | endCell 122 | ) 123 | ); 124 | const [, startRowIndex, startColumnIndex] = /R\[(-?\d+)\]C\[(-?\d+)\]/gmi.exec(startCell); 125 | const [, endRowIndex, endColumnIndex] = /R\[(-?\d+)\]C\[(-?\d+)\]/gmi.exec(endCell); 126 | const corrected = { 127 | startRowIndex: +startRowIndex + +rowIndex, 128 | startColumnIndex: +startColumnIndex + +columnIndex, 129 | endRowIndex: +endRowIndex + +rowIndex, 130 | endColumnIndex: +endColumnIndex + +columnIndex 131 | } 132 | for (let j = corrected.startColumnIndex; j <= corrected.endColumnIndex; j++) { 133 | for (let i = corrected.startRowIndex; i <= corrected.endRowIndex; i++) { 134 | let a1Notation = activeSheet.getRange(`R[${i}]C[${j}]`).getA1Notation(); 135 | enumerated.push(isDifferentSheet ? `${rangeData[0]}!${a1Notation}` : a1Notation); 136 | } 137 | } 138 | const cell = activeSheet.getRange(`R[${rowIndex}]C[${columnIndex}]`).getA1Notation(); 139 | return { 140 | cell: cell, 141 | precedents: [...new Set(enumerated)] 142 | }; 143 | } 144 | 145 | function convertCellsFromR1C1toA1Notation(cellR1C1Reference, rowIndex, columnIndex) { 146 | let enumerated = []; 147 | const isDifferentSheet = cellR1C1Reference.includes("!"); 148 | const cellReferenceData = isDifferentSheet ? cellR1C1Reference.split("!") : null; 149 | cellR1C1Reference = isDifferentSheet ? cellReferenceData[1] : cellR1C1Reference; 150 | 151 | const [, startRowIndex, startColumnIndex] = /R\[(-?\d+)\]C\[(-?\d+)\]/gmi.exec(cellR1C1Reference); 152 | const corrected = { 153 | startRowIndex: +startRowIndex + +rowIndex, 154 | startColumnIndex: +startColumnIndex + +columnIndex, 155 | } 156 | 157 | const a1Notation = ss.getRange(`R[${corrected.startRowIndex}]C[${corrected.startColumnIndex}]`).getA1Notation(); 158 | enumerated.push(isDifferentSheet ? `${cellReferenceData[0]}!${a1Notation}` : a1Notation); 159 | const cell = activeSheet.getRange(`R[${rowIndex}]C[${columnIndex}]`).getA1Notation(); 160 | return { 161 | cell: cell, 162 | precedents: [...new Set(enumerated)] 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /Sheets/JsDoc2JSON/Code.gs: -------------------------------------------------------------------------------- 1 | function jsDoc2JSON(input) { 2 | input = UrlFetchApp.fetch("https://raw.githubusercontent.com/custom-functions/google-sheets/main/functions/DOUBLE.gs").getBlob().getDataAsString() 3 | const jsDocJSON = {}; 4 | const jsDocComment = input.match(/\/\*\*.*\*\//s); 5 | const jsDocDescription = jsDocComment ? jsDocComment[0].match(/^[^@]*/s) : false; 6 | const description = jsDocDescription ? jsDocDescription[0].split("*").map(el => el.trim()).filter(el => el !== '' && el !== '/').join(" ") : false; 7 | const jsDocTags = jsDocComment ? jsDocComment[0].match(/@.*(?=\@)/s) : false; 8 | const rawTags = jsDocTags ? jsDocTags[0].split("*").map(el => el.trim()).filter(el => el !== '') : false; 9 | const tags = []; 10 | let components; 11 | rawTags.forEach(el => { 12 | if (el.startsWith("@param ")) { // https://jsdoc.app/tags-param.html 13 | components = el.match(/^\@(param)(?: )\{(.*)\}(?: )(?:(?=\[)(?:\[(.*?)\])|(?!\[)(?:(.*?)))(?:(?= )(?: )(?:\- )?(.*)|(?! )$)/i); 14 | if (components) { 15 | components = components.filter(el => el !== undefined); 16 | tags.push({ 17 | "tag": "param", 18 | "type": components[2] ? components[2] : null, 19 | "name": components[3] ? components[3] : null, 20 | "description": components[4] ? components[4] : null, 21 | }); 22 | } else { 23 | components = el.match(/^\@(param) (?:(?=\[)(?:\[(.*)\]$)|(?!\[)(?:([^\s]+)$))/i); 24 | if (components) { 25 | components = components.filter(el => el !== undefined); 26 | tags.push({ 27 | "tag": "param", 28 | "type": components[2] ? components[2] : null, 29 | "name": components[3] ? components[3] : null, 30 | "description": components[4] ? components[4] : null, 31 | }); 32 | } else { 33 | console.log(`invalid @param tag: ${el}`); 34 | } 35 | } 36 | } else if (el.startsWith("@return ") || el.startsWith("@returns ")) { // https://jsdoc.app/tags-returns.html 37 | components = el.match(/^\@(returns?)(?: )\{(.*)\}(?:(?= )(?: )(?:\- )?(.*)|(?! )$)/i); 38 | if (components) { 39 | components = components.filter(el => el !== undefined); 40 | tags.push({ 41 | "tag": "return", 42 | "type": components[2] ? components[2] : null, 43 | "description": components[3] ? components[3] : null, 44 | }); 45 | } else { 46 | console.log(`invalid @return tag: ${el}`); 47 | } 48 | } else { 49 | console.log(`unknown tag: ${el}`); 50 | } 51 | }); 52 | 53 | jsDocJSON.description = description; 54 | jsDocJSON.tags = tags; 55 | 56 | console.log(JSON.stringify(jsDocJSON, null, 2)); 57 | } 58 | 59 | 60 | // https://jsdoc.app/tags-param.html 61 | 62 | // ^\@(param) (?:(?=\[)(?:\[(.*)\]$)|(?!\[)(?:([^\s]+)$)) 63 | // @param somebody 64 | 65 | // ^\@(param)(?: )\{(.*)\}(?: )(?:(?=\[)(?:\[(.*?)\])|(?!\[)(?:(.*?)))(?:(?= )(?: )(?:\- )?(.*)|(?! )$) 66 | // @param {string} somebody 67 | // @param {string} somebody Somebody's name. 68 | // @param {string} somebody - Somebody's name. 69 | // @param {string} employee.name - The name of the employee. 70 | // @param {Object[]} employees - The employees who are responsible for the project. 71 | // @param {string} employees[].department - The employee's department. 72 | // @param {string=} somebody - Somebody's name. 73 | // @param {*} somebody - Whatever you want. 74 | // @param {string} [somebody=John Doe] - Somebody's name. 75 | // @param {(string|string[])} [somebody=John Doe] - Somebody's name, or an array of names. 76 | // @param {string} [somebody] - Somebody's name. 77 | 78 | // https://jsdoc.app/tags-returns.html 79 | 80 | // ^\@(returns)(?: )\{(.*)\}(?:(?= )(?: )(?:\- )?(.*)|(?! )$) 81 | // @returns {number} 82 | // @returns {number} Sum of a and b 83 | // @returns {(number|Array)} Sum of a and b or an array that contains a, b and the sum of a and b. 84 | // @returns {Promise} Promise object represents the sum of a and b 85 | -------------------------------------------------------------------------------- /Sheets/Project BoldX/Code.gs: -------------------------------------------------------------------------------- 1 | function onOpen() { 2 | SpreadsheetApp.getUi() 3 | .createMenu('BoldX Menu') 4 | .addItem('Show sidebar', 'showSidebar') 5 | .addToUi(); 6 | } 7 | 8 | function showSidebar() { 9 | const html = HtmlService.createHtmlOutputFromFile('Index').setTitle('BoldX Sidebar'); 10 | SpreadsheetApp.getUi().showSidebar(html); 11 | } 12 | 13 | const ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); 14 | 15 | function main(range, csvWords) { 16 | range = ss.getRange(range); 17 | const rIndex = range.getRow(); 18 | const cIndex = range.getColumn(); 19 | const values = range.getValues(); 20 | const rawWords = csvWords.split(","); 21 | const words = []; 22 | rawWords.forEach(word => { 23 | words.push(word.trim()); 24 | }); 25 | 26 | 27 | for (let rowIndex = 0; rowIndex < values.length; rowIndex++) { 28 | const row = values[rowIndex]; 29 | for (let colIndex = 0; colIndex < row.length; colIndex++) { 30 | const value = row[colIndex]; 31 | if (value != '' && value != null && value != undefined) { 32 | if (checkWords(value, words)) { 33 | setBoldFormat(value, words, (rowIndex + rIndex), (colIndex + cIndex)); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | function checkWords(value, words) { 41 | const wordArray = value.match(/\b(\S+)\b/g); 42 | const hasWord = words.some((value) => wordArray.indexOf(value) !== -1); 43 | return hasWord; 44 | } 45 | 46 | function setBoldFormat(value, words, rowIndex, colIndex) { 47 | const range = ss.getRange(rowIndex, colIndex); 48 | const boldX = SpreadsheetApp.newTextStyle().setBold(true).build(); 49 | for (let wordIndex in words) { 50 | let word = words[wordIndex]; 51 | const richTextValue = range.getRichTextValue().copy(); 52 | const startIndex = value.indexOf(word); 53 | if (startIndex > 0) { 54 | const endIndex = startIndex + word.length; 55 | const formattedOutput = richTextValue.setTextStyle(startIndex, endIndex, boldX).build(); 56 | range.setRichTextValue(formattedOutput); 57 | SpreadsheetApp.flush(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sheets/Project BoldX/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sheets/README.md: -------------------------------------------------------------------------------- 1 | # Scripts related to Google Sheets 2 | -------------------------------------------------------------------------------- /Sheets/Webhooks/GET.gs: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/choraria/google-apps-script/blob/master/Sheets/Webhooks/GET.gs 2 | 3 | const documentProperties = PropertiesService.getDocumentProperties(); 4 | let ok200Status = '%200OKSTATUS%'; // replace '%200OKSTATUS%' from the add-on to either `true` or `false` (boolean) 5 | let logTimeStamp = '%LOGTIMESTAMP%'; // replace '%LOGTIMESTAMP%' from the add-on to either `true` or `false` (boolean) 6 | 7 | function onOpen(e) { 8 | if (documentProperties.getProperty('Authorized') !== 'true') { 9 | const ui = SpreadsheetApp.getUi(); 10 | ui.createMenu('Webhooks') 11 | .addItem('Authorize', 'authorizeScript') 12 | .addToUi(); 13 | } 14 | } 15 | 16 | function authorizeScript() { 17 | SpreadsheetApp.getActiveSpreadsheet().toast('Authorization successful.', "🪝 Webhooks for Sheets"); 18 | documentProperties.setProperty('Authorized', 'true'); 19 | } 20 | 21 | function doGet(e) { 22 | const lock = LockService.getScriptLock(); 23 | try { 24 | lock.waitLock(28000); 25 | } catch (e) { 26 | response = { 27 | status: 'error', 28 | message: 'Request throttled' 29 | } 30 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 31 | } 32 | 33 | let params = e.parameters; 34 | 35 | const activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet(); 36 | const allSheets = activeSpreadsheet.getSheets(); 37 | 38 | const activeSheetsAndNewParams = gidHandlerForGet(params, activeSpreadsheet, allSheets); 39 | const activeSheets = activeSheetsAndNewParams.activeSheetNames; 40 | params = activeSheetsAndNewParams.revisedParameters; 41 | 42 | let keys = Object.keys(params); 43 | let response = {}; 44 | 45 | if (keys.length > 0) { 46 | logTimeStamp === true ? params["timestamp_incoming_webhook"] = [new Date()] : null; 47 | keys = Object.keys(params); 48 | const cartesianData = cartesian(params); 49 | 50 | activeSheets.forEach(activeSheetName => { 51 | let activeSheet = activeSpreadsheet.getSheetByName(activeSheetName); 52 | let headers = activeSheet.getDataRange().offset(0, 0, 1).getValues()[0]; 53 | if (headers.length == 0 || (headers.length == 1 && headers[0].length == 0)) { 54 | activeSheet.appendRow(keys); 55 | activeSheet.setFrozenRows(1); 56 | if (logTimeStamp === true) { 57 | activeSheet.moveColumns(activeSheet.getRange(1, keys.indexOf("timestamp_incoming_webhook") + 1), 1); 58 | SpreadsheetApp.flush(); 59 | activeSheet.getRange("A:A").setNumberFormat('dd/MM/yyyy HH:mm:ss'); 60 | headers = activeSheet.getDataRange().offset(0, 0, 1).getValues()[0]; 61 | } else { 62 | headers = keys; 63 | } 64 | } 65 | 66 | let rowData = []; 67 | cartesianData.forEach(rowLevelData => [rowLevelData].map(row => rowData.push(headers.map(key => row[String(key)] || '')))); 68 | activeSheet.getRange(activeSheet.getLastRow() + 1, 1, rowData.length, rowData[0].length).setValues(rowData); 69 | }) 70 | 71 | response = { 72 | status: 'success', 73 | message: 'Data logged successfully' 74 | } 75 | lock.releaseLock(); 76 | return ok200Status === true ? 77 | HtmlService.createHtmlOutput('Data logged successfully').setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) : 78 | ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 79 | } else { 80 | response = { 81 | status: 'success', 82 | message: 'No parameters detected' 83 | } 84 | lock.releaseLock(); 85 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 86 | } 87 | } 88 | 89 | function gidHandlerForGet(params, activeSpreadsheet, allSheets) { 90 | let existingSheetIds = []; 91 | let getDefaultSheet; 92 | let newParameters = {}; 93 | allSheets.forEach(sheet => existingSheetIds.push(sheet.getSheetId().toString())); 94 | 95 | let defaultWebhookGetSheetId = documentProperties.getProperty('defaultWebhookGetSheetId'); 96 | let newDefaultWebhookGetSheetName = `[GET] Webhook — ${new Date().getTime().toString()}`; 97 | 98 | let checkDefaultOrCreateNewGetSheet = false; 99 | 100 | let keys = Object.keys(params); 101 | if (keys.includes('gid')) { 102 | const gidValues = params['gid']; 103 | const matchingGids = existingSheetIds.filter(sheetId => gidValues.includes(sheetId)); 104 | const nonMatchingGids = gidValues.filter(gid => !matchingGids.includes(gid)); 105 | if (matchingGids.length === 0) { 106 | checkDefaultOrCreateNewGetSheet = true; 107 | } else { 108 | newParameters = params; 109 | delete newParameters["gid"]; 110 | if (nonMatchingGids.length > 0) { 111 | newParameters["gid"] = nonMatchingGids; 112 | } 113 | if (matchingGids.length === 1) { 114 | getDefaultSheet = allSheets.filter(sheet => sheet.getSheetId() == matchingGids[0]); 115 | return { 116 | activeSheetNames: [getDefaultSheet[0].getSheetName()], 117 | revisedParameters: newParameters, 118 | }; 119 | } else { 120 | let validSheetNames = []; 121 | matchingGids.forEach(gid => { 122 | getDefaultSheet = allSheets.filter(sheet => sheet.getSheetId() == gid); 123 | if (getDefaultSheet.length !== 0) { 124 | validSheetNames.push(getDefaultSheet[0].getSheetName()) 125 | } 126 | }); 127 | return { 128 | activeSheetNames: validSheetNames, 129 | revisedParameters: newParameters, 130 | } 131 | } 132 | } 133 | } else { 134 | checkDefaultOrCreateNewGetSheet = true; 135 | } 136 | 137 | if (checkDefaultOrCreateNewGetSheet) { 138 | if (!defaultWebhookGetSheetId) { 139 | defaultWebhookGetSheetId = activeSpreadsheet.insertSheet().setName(newDefaultWebhookGetSheetName).getSheetId().toString(); 140 | documentProperties.setProperty('defaultWebhookGetSheetId', defaultWebhookGetSheetId); 141 | return { 142 | activeSheetNames: [newDefaultWebhookGetSheetName], 143 | revisedParameters: params, 144 | }; 145 | } else { 146 | getDefaultSheet = allSheets.filter(sheet => sheet.getSheetId() == defaultWebhookGetSheetId); 147 | if (getDefaultSheet.length !== 0) { 148 | return { 149 | activeSheetNames: [getDefaultSheet[0].getSheetName()], 150 | revisedParameters: params, 151 | }; 152 | } else { 153 | defaultWebhookGetSheetId = activeSpreadsheet.insertSheet().setName(newDefaultWebhookGetSheetName).getSheetId().toString(); 154 | documentProperties.setProperty('defaultWebhookGetSheetId', defaultWebhookGetSheetId); 155 | return { 156 | activeSheetNames: [newDefaultWebhookGetSheetName], 157 | revisedParameters: params, 158 | }; 159 | } 160 | } 161 | } 162 | } 163 | 164 | function cartesian(parameters) { 165 | let keys = Object.keys(parameters); 166 | let depth = Object.values(parameters).reduce((product, { length }) => product * length, 1); 167 | let result = []; 168 | for (let i = 0; i < depth; i++) { 169 | let j = i; 170 | let dict = {}; 171 | for (let key of keys) { 172 | let size = parameters[key].length; 173 | dict[key] = parameters[key][j % size]; 174 | j = Math.floor(j / size); 175 | } 176 | result.push(dict); 177 | } 178 | return result; 179 | } 180 | -------------------------------------------------------------------------------- /Sheets/Webhooks/POST.gs: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/choraria/google-apps-script/blob/master/Sheets/Webhooks/POST.gs 2 | 3 | function doPost(e) { 4 | const lock = LockService.getScriptLock(); 5 | try { 6 | lock.waitLock(28000); 7 | } catch (e) { 8 | response = { 9 | status: 'error', 10 | message: 'Request throttled' 11 | } 12 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 13 | } 14 | 15 | let { parameters, postData: { contents, type } = {} } = e; 16 | let response = {}; 17 | 18 | if (type === 'text/plain' || type === 'text/html' || type === 'application/xml') { 19 | response = { 20 | status: 'error', 21 | message: `Unsupported data-type: ${type}` 22 | } 23 | lock.releaseLock(); 24 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 25 | } 26 | 27 | const activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet(); 28 | const allSheets = activeSpreadsheet.getSheets(); 29 | 30 | const activeSheetsAndNewParams = gidHandlerForPost(parameters, activeSpreadsheet, allSheets); 31 | const activeSheets = activeSheetsAndNewParams.activeSheetNames; 32 | parameters = activeSheetsAndNewParams.revisedParameters; 33 | 34 | let keys = []; 35 | 36 | if (type === 'application/json' || (type === '' && contents.length > 0)) { 37 | let jsonData; 38 | try { 39 | jsonData = JSON.parse(contents); 40 | } catch (e) { 41 | response = { 42 | status: 'error', 43 | message: 'Invalid JSON format' 44 | }; 45 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 46 | }; 47 | 48 | jsonData = Array.isArray(jsonData) ? jsonData.map(data => flatten(data)) : [flatten(jsonData)]; 49 | keys = Array.isArray(jsonData) ? ((jsonData[0].constructor === Object || jsonData[0].constructor === Array) ? Object.keys(jsonData[0]) : jsonData[0]) : Object.keys(jsonData); 50 | if (keys.length > 0) { 51 | activeSheets.forEach(activeSheetName => { 52 | let activeSheet = activeSpreadsheet.getSheetByName(activeSheetName); 53 | let headers = activeSheet.getDataRange().offset(0, 0, 1).getValues()[0]; 54 | if (headers.length == 0 || (headers.length == 1 && headers[0].length == 0)) { 55 | activeSheet.appendRow(keys); 56 | activeSheet.setFrozenRows(1); 57 | if (logTimeStamp === true) { 58 | activeSheet.insertColumnBefore(1); 59 | SpreadsheetApp.flush(); 60 | activeSheet.getRange("A1").setValue("timestamp_incoming_webhook"); 61 | activeSheet.getRange("A:A").setNumberFormat('dd/MM/yyyy HH:mm:ss'); 62 | SpreadsheetApp.flush(); 63 | headers = activeSheet.getDataRange().offset(0, 0, 1).getValues()[0]; 64 | } else { 65 | headers = keys; 66 | } 67 | } 68 | let rowData = []; 69 | const now = new Date(); 70 | jsonData.forEach(rowLevelData => [rowLevelData].map(row => rowData.push(headers.map(key => key === "timestamp_incoming_webhook" ? now : row[String(key)] || '')))); 71 | 72 | activeSheet.getRange(activeSheet.getLastRow() + 1, 1, rowData.length, rowData[0].length).setValues(rowData); 73 | }); 74 | response = { 75 | status: 'success', 76 | message: 'Data logged successfully' 77 | }; 78 | lock.releaseLock(); 79 | return ok200Status === true ? 80 | HtmlService.createHtmlOutput('Data logged successfully').setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) : 81 | ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 82 | } else { 83 | response = { 84 | status: 'success', 85 | message: 'No parameters detected' 86 | }; 87 | lock.releaseLock(); 88 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 89 | } 90 | } else { 91 | if (parameters) { 92 | keys = Object.keys(parameters); 93 | if (keys.length > 0) { 94 | logTimeStamp === true ? parameters["timestamp_incoming_webhook"] = [new Date()] : null; 95 | keys = Object.keys(parameters); 96 | const cartesianData = cartesian(parameters); 97 | activeSheets.forEach(activeSheetName => { 98 | let activeSheet = activeSpreadsheet.getSheetByName(activeSheetName); 99 | let headers = activeSheet.getDataRange().offset(0, 0, 1).getValues()[0]; 100 | if (headers.length == 0 || (headers.length == 1 && headers[0].length == 0)) { 101 | activeSheet.appendRow(keys); 102 | activeSheet.setFrozenRows(1); 103 | if (logTimeStamp === true) { 104 | activeSheet.moveColumns(activeSheet.getRange(1, keys.indexOf("timestamp_incoming_webhook") + 1), 1); 105 | SpreadsheetApp.flush(); 106 | activeSheet.getRange("A:A").setNumberFormat('dd/MM/yyyy HH:mm:ss'); 107 | headers = activeSheet.getDataRange().offset(0, 0, 1).getValues()[0]; 108 | } else { 109 | headers = keys; 110 | } 111 | } 112 | let rowData = []; 113 | cartesianData.forEach(rowLevelData => [rowLevelData].map(row => rowData.push(headers.map(key => row[String(key)] || '')))); 114 | 115 | activeSheet.getRange(activeSheet.getLastRow() + 1, 1, rowData.length, rowData[0].length).setValues(rowData); 116 | }); 117 | response = { 118 | status: 'success', 119 | message: 'Data logged successfully' 120 | }; 121 | lock.releaseLock(); 122 | return ok200Status === true ? 123 | HtmlService.createHtmlOutput('Data logged successfully').setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) : 124 | ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 125 | } else { 126 | response = { 127 | status: 'success', 128 | message: 'No parameters detected' 129 | }; 130 | lock.releaseLock(); 131 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 132 | } 133 | } 134 | } 135 | } 136 | 137 | function gidHandlerForPost(params, activeSpreadsheet, allSheets) { 138 | let existingSheetIds = []; 139 | let postDefaultSheet; 140 | let newParameters = {}; 141 | allSheets.forEach(sheet => existingSheetIds.push(sheet.getSheetId().toString())); 142 | 143 | let defaultWebhookPostSheetId = documentProperties.getProperty('defaultWebhookPostSheetId'); 144 | let newDefaultWebhookPostSheetName = `[POST] Webhook — ${new Date().getTime().toString()}`; 145 | 146 | let checkDefaultOrCreateNewPostSheet = false; 147 | 148 | let keys = Object.keys(params); 149 | if (keys.includes('gid')) { 150 | const gidValues = params['gid']; 151 | const matchingGids = existingSheetIds.filter(sheetId => gidValues.includes(sheetId)); 152 | const nonMatchingGids = gidValues.filter(gid => !matchingGids.includes(gid)); 153 | if (matchingGids.length === 0) { 154 | checkDefaultOrCreateNewPostSheet = true; 155 | } else { 156 | newParameters = params; 157 | delete newParameters["gid"]; 158 | if (nonMatchingGids.length > 0) { 159 | newParameters["gid"] = nonMatchingGids; 160 | } 161 | if (matchingGids.length === 1) { 162 | postDefaultSheet = allSheets.filter(sheet => sheet.getSheetId() == matchingGids[0]); 163 | return { 164 | activeSheetNames: [postDefaultSheet[0].getSheetName()], 165 | revisedParameters: newParameters, 166 | }; 167 | } else { 168 | let validSheetNames = []; 169 | matchingGids.forEach(gid => { 170 | postDefaultSheet = allSheets.filter(sheet => sheet.getSheetId() == gid); 171 | if (postDefaultSheet.length !== 0) { 172 | validSheetNames.push(postDefaultSheet[0].getSheetName()) 173 | } 174 | }); 175 | return { 176 | activeSheetNames: validSheetNames, 177 | revisedParameters: newParameters, 178 | } 179 | } 180 | } 181 | } else { 182 | checkDefaultOrCreateNewPostSheet = true; 183 | } 184 | 185 | if (checkDefaultOrCreateNewPostSheet) { 186 | if (!defaultWebhookPostSheetId) { 187 | defaultWebhookPostSheetId = activeSpreadsheet.insertSheet().setName(newDefaultWebhookPostSheetName).getSheetId().toString(); 188 | documentProperties.setProperty('defaultWebhookPostSheetId', defaultWebhookPostSheetId); 189 | return { 190 | activeSheetNames: [newDefaultWebhookPostSheetName], 191 | revisedParameters: params, 192 | }; 193 | } else { 194 | postDefaultSheet = allSheets.filter(sheet => sheet.getSheetId() == defaultWebhookPostSheetId); 195 | if (postDefaultSheet.length !== 0) { 196 | return { 197 | activeSheetNames: [postDefaultSheet[0].getSheetName()], 198 | revisedParameters: params, 199 | }; 200 | } else { 201 | defaultWebhookPostSheetId = activeSpreadsheet.insertSheet().setName(newDefaultWebhookPostSheetName).getSheetId().toString(); 202 | documentProperties.setProperty('defaultWebhookPostSheetId', defaultWebhookPostSheetId); 203 | return { 204 | activeSheetNames: [newDefaultWebhookPostSheetName], 205 | revisedParameters: params, 206 | }; 207 | } 208 | } 209 | } 210 | } 211 | 212 | // https://hawksey.info/blog/2020/04/google-apps-script-patterns-writing-rows-of-data-to-google-sheets-the-v8-way/#Flattening_nested_JSON_and_template_literals 213 | // Based on https://stackoverflow.com/a/54897035/1027723 214 | const flatten = (obj, prefix = '', res = {}) => 215 | Object.entries(obj).reduce((r, [key, val]) => { 216 | const k = `${prefix}${key}`; 217 | if (typeof val === 'object' && val !== null) { 218 | flatten(val, `${k}_`, r); 219 | } else { 220 | res[k] = val; 221 | } 222 | return r; 223 | }, res); 224 | -------------------------------------------------------------------------------- /Sheets/Webhooks/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "oauthScopes": [ 3 | "https://www.googleapis.com/auth/spreadsheets.currentonly" 4 | ], 5 | "exceptionLogging": "STACKDRIVER", 6 | "runtimeVersion": "V8", 7 | "webapp": { 8 | "executeAs": "USER_DEPLOYING", 9 | "access": "ANYONE_ANONYMOUS" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Twilio/Authy/Code.gs: -------------------------------------------------------------------------------- 1 | var authyKey = 'YourAuthyAPIKeyGoesHere'; 2 | 3 | var userSession = Session.getTemporaryActiveUserKey(); 4 | var userProperties = PropertiesService.getUserProperties(); 5 | 6 | function addNewUser(formData) { 7 | var addUserURL = "https://api.authy.com/protected/json/users/new"; 8 | var userPayload = { 9 | "send_install_link_via_sms": false, 10 | "user[email]" : formData.email, 11 | "user[cellphone]" : formData.phoneNumber, 12 | "user[country_code]" : formData.country 13 | }; 14 | var addUserOptions = { 15 | "method" : "POST", 16 | "payload" : userPayload, 17 | "muteHttpExceptions": true 18 | }; 19 | addUserOptions.headers = { 20 | "X-Authy-API-Key" : authyKey 21 | }; 22 | var newUser = UrlFetchApp.fetch(addUserURL, addUserOptions); 23 | var newUserResponse = newUser.getContentText(); 24 | var userResponse = JSON.parse(newUserResponse); 25 | if (newUser.getResponseCode() == 200) { 26 | if (userResponse["success"] == true) { 27 | var authyID = JSON.stringify(userResponse["user"]["id"]); 28 | userProperties.setProperty(userSession, authyID); 29 | var newProperties = {}; 30 | newProperties[authyID] = JSON.stringify({ 31 | userLoggedIn: '', 32 | pushAuthUuid: '' 33 | }); 34 | userProperties.setProperties(newProperties); 35 | return 'Registered Successfully!'; 36 | } else { 37 | return 'Something went wrong :('; 38 | } 39 | } else { 40 | return 'Something went wrong :('; 41 | } 42 | } 43 | 44 | function verifyTOTP(token) { 45 | var authyID = userProperties.getProperty(userSession); 46 | if (authyID !== null) { 47 | var verifyTOTPURL = "https://api.authy.com/protected/json/verify/" + token + "/" + authyID; 48 | var varifyTOTPOptions = { 49 | "method" : "GET", 50 | "muteHttpExceptions": true 51 | }; 52 | varifyTOTPOptions.headers = { 53 | "X-Authy-API-Key" : authyKey 54 | }; 55 | var verifyTOTP = UrlFetchApp.fetch(verifyTOTPURL, varifyTOTPOptions); 56 | var verifyTOTPResponse = verifyTOTP.getContentText(); 57 | var TOTPResponse = JSON.parse(verifyTOTPResponse); 58 | if (verifyTOTP.getResponseCode() == 200) { 59 | if (TOTPResponse["success"] == "true" && TOTPResponse["token"] == "is valid" && TOTPResponse["message"] == "Token is valid.") { 60 | var updateProperties = JSON.stringify({ 61 | userLoggedIn: true, 62 | pushAuthUuid: '' 63 | }); 64 | userProperties.setProperty(authyID, updateProperties); 65 | return 'Logging you in!'; 66 | } else { 67 | return 'Something went wrong :('; 68 | } 69 | } else { 70 | return 'Something went wrong :('; 71 | } 72 | } else { 73 | return 'Please sign-up first to login.' 74 | } 75 | } 76 | 77 | function pushAuth() { 78 | var authyID = userProperties.getProperty(userSession); 79 | if (authyID !== null) { 80 | var pushAuthURL = "https://api.authy.com/onetouch/json/users/" + authyID + "/approval_requests"; 81 | var pushAuthPayload = { 82 | "message": "Login requested from Google Apps Script." 83 | }; 84 | var pushAuthOptions = { 85 | "method" : "POST", 86 | "payload" : pushAuthPayload, 87 | "muteHttpExceptions": true 88 | }; 89 | pushAuthOptions.headers = { 90 | "X-Authy-API-Key" : authyKey 91 | }; 92 | var newPushAuthReq = UrlFetchApp.fetch(pushAuthURL, pushAuthOptions); 93 | var newPushAuthResponse = newPushAuthReq.getContentText(); 94 | var pushAuthResponse = JSON.parse(newPushAuthResponse); 95 | if (newPushAuthReq.getResponseCode() == 200) { 96 | if (pushAuthResponse["success"] == true) { 97 | var pushAuthUuid = pushAuthResponse["approval_request"]["uuid"]; 98 | var updateProperties = JSON.stringify({ 99 | userLoggedIn: '', 100 | pushAuthUuid: pushAuthUuid 101 | }); 102 | userProperties.setProperty(authyID, updateProperties); 103 | return 'Please check your phone...'; 104 | } else { 105 | return 'Something went wrong :('; 106 | } 107 | } else { 108 | return 'Something went wrong :('; 109 | } 110 | } else { 111 | return 'Please sign-up first to login.' 112 | } 113 | } 114 | 115 | function doGet(e) { 116 | var htmlFile; 117 | var title; 118 | if (loginStatus()) { 119 | htmlFile = 'Dashboard'; 120 | title = 'Login Using Authy!' 121 | } else { 122 | htmlFile = 'Index'; 123 | title = 'Login Using Authy!' 124 | } 125 | return HtmlService.createHtmlOutputFromFile(htmlFile).setTitle(title); 126 | } 127 | 128 | function doPost(e) { 129 | if (JSON.parse(e.postData.contents).callback_action == "approval_request_status") { 130 | var pushAuthParams = JSON.parse(e.postData.contents); 131 | var pushAuthCallbackUuid = pushAuthParams.uuid; 132 | var authyID = pushAuthParams.authy_id; 133 | if (JSON.parse(userProperties.getProperty(authyID)).pushAuthUuid == pushAuthCallbackUuid) { 134 | if (pushAuthParams.status == "approved") { 135 | var updateProperties = JSON.stringify({ 136 | userLoggedIn: true, 137 | pushAuthUuid: pushAuthCallbackUuid 138 | }); 139 | userProperties.setProperty(authyID, updateProperties); 140 | } 141 | } 142 | } 143 | } 144 | 145 | function loginStatus() { 146 | var loginStatus = false; 147 | var authyID = userProperties.getProperty(userSession); 148 | if (authyID !== null) { 149 | var loginStatus = JSON.parse(userProperties.getProperty(authyID)).userLoggedIn; 150 | } 151 | return loginStatus; 152 | } 153 | 154 | function webAppURL(linkAddr) { 155 | var linkAddr = ScriptApp.getService().getUrl(); 156 | return linkAddr; 157 | } 158 | 159 | function userLogout() { 160 | var authyID = userProperties.getProperty(userSession); 161 | if (authyID !== null) { 162 | userProperties.deleteProperty(authyID); 163 | userProperties.deleteProperty(userSession) 164 | return true; 165 | } else { 166 | return false; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Twilio/Authy/Dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 38 | 39 | 40 | 41 | 42 |
43 |


44 |

You've been logged in!

45 |


46 | 47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Twilio/Authy/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 51 | 52 | 89 | 90 | 112 | 113 | 114 | 115 | 116 |
117 |


118 |

Use Authy with Google Apps Script

119 |
Please download the Authy app on your phone to continue
120 |


121 | 122 | 123 | 124 | 149 | 150 |
151 |
152 | 153 | 154 | 155 | 175 | 176 |


177 | 178 |
179 |
180 |
181 | 182 |
183 |
184 |
185 | 186 |
187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /Twilio/Authy/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Authy](https://www.twilio.com) 2 | 3 | ## Currently supporting 4 | 5 | - Adding users using PII 6 | - TOTP 7 | - Push Authentications 8 | 9 | ## Demo 10 | 11 | You can view the final output of this setup [here](https://script.google.com/macros/s/AKfycbyTUIBCvY1aXrEUQoIYghyxExJ3GubdWzfAzCKDYH4EtkkTDPyh/exec) 12 | -------------------------------------------------------------------------------- /Twilio/README.md: -------------------------------------------------------------------------------- 1 | # Connecting Apps Script to [Twilio](https://twilio.com) 2 | -------------------------------------------------------------------------------- /Workspace Add-on/More than 100 widgets/Data.gs: -------------------------------------------------------------------------------- 1 | const functions = [ 2 | { "name": "ABS" }, 3 | { "name": "ACCRINT" }, 4 | { "name": "ACCRINTM" }, 5 | { "name": "ACOS" }, 6 | { "name": "ACOSH" }, 7 | { "name": "ACOT" }, 8 | { "name": "ACOTH" }, 9 | { "name": "ADD" }, 10 | { "name": "ADDRESS" }, 11 | { "name": "AMORLINC" }, 12 | { "name": "AND" }, 13 | { "name": "ARABIC" }, 14 | { "name": "ARRAY_CONSTRAIN" }, 15 | { "name": "ARRAYFORMULA" }, 16 | { "name": "ASC" }, 17 | { "name": "ASIN" }, 18 | { "name": "ASINH" }, 19 | { "name": "ATAN" }, 20 | { "name": "ATAN2" }, 21 | { "name": "ATANH" }, 22 | { "name": "AVEDEV" }, 23 | { "name": "AVERAGE" }, 24 | { "name": "AVERAGE.WEIGHTED" }, 25 | { "name": "AVERAGEA" }, 26 | { "name": "AVERAGEIF" }, 27 | { "name": "AVERAGEIFS" }, 28 | { "name": "BASE" }, 29 | { "name": "BETA.DIST" }, 30 | { "name": "BETA.INV" }, 31 | { "name": "BETADIST" }, 32 | { "name": "BETAINV" }, 33 | { "name": "BIN2DEC" }, 34 | { "name": "BIN2HEX" }, 35 | { "name": "BIN2OCT" }, 36 | { "name": "BINOM.DIST" }, 37 | { "name": "BINOM.INV" }, 38 | { "name": "BINOMDIST" }, 39 | { "name": "BITAND" }, 40 | { "name": "BITLSHIFT" }, 41 | { "name": "BITOR" }, 42 | { "name": "BITRSHIFT" }, 43 | { "name": "BITXOR" }, 44 | { "name": "BYCOL" }, 45 | { "name": "BYROW" }, 46 | { "name": "CEILING" }, 47 | { "name": "CEILING.MATH" }, 48 | { "name": "CEILING.PRECISE" }, 49 | { "name": "CELL" }, 50 | { "name": "CHAR" }, 51 | { "name": "CHIDIST" }, 52 | { "name": "CHIINV" }, 53 | { "name": "CHISQ.DIST" }, 54 | { "name": "CHISQ.DIST.RT" }, 55 | { "name": "CHISQ.INV" }, 56 | { "name": "CHISQ.INV.RT" }, 57 | { "name": "CHISQ.TEST" }, 58 | { "name": "CHITEST" }, 59 | { "name": "CHOOSE" }, 60 | { "name": "CLEAN" }, 61 | { "name": "CODE" }, 62 | { "name": "COLUMN" }, 63 | { "name": "COLUMNS" }, 64 | { "name": "COMBIN" }, 65 | { "name": "COMBINA" }, 66 | { "name": "COMPLEX" }, 67 | { "name": "CONCAT" }, 68 | { "name": "CONCATENATE" }, 69 | { "name": "CONFIDENCE" }, 70 | { "name": "CONFIDENCE.NORM" }, 71 | { "name": "CONFIDENCE.T" }, 72 | { "name": "CONVERT" }, 73 | { "name": "CORREL" }, 74 | { "name": "COS" }, 75 | { "name": "COSH" }, 76 | { "name": "COT" }, 77 | { "name": "COTH" }, 78 | { "name": "COUNT" }, 79 | { "name": "COUNTA" }, 80 | { "name": "COUNTBLANK" }, 81 | { "name": "COUNTIF" }, 82 | { "name": "COUNTIFS" }, 83 | { "name": "COUNTUNIQUE" }, 84 | { "name": "COUPDAYBS" }, 85 | { "name": "COUPDAYS" }, 86 | { "name": "COUPDAYSNC" }, 87 | { "name": "COUPNCD" }, 88 | { "name": "COUPNUM" }, 89 | { "name": "COUPPCD" }, 90 | { "name": "COVAR" }, 91 | { "name": "COVARIANCE.P" }, 92 | { "name": "COVARIANCE.S" }, 93 | { "name": "CRITBINOM" }, 94 | { "name": "CSC" }, 95 | { "name": "CSCH" }, 96 | { "name": "CUMIPMT" }, 97 | { "name": "CUMPRINC" }, 98 | { "name": "DATE" }, 99 | { "name": "DATEDIF" }, 100 | { "name": "DATEVALUE" }, 101 | { "name": "DAVERAGE" }, 102 | { "name": "DAY" }, 103 | { "name": "DAYS" }, 104 | { "name": "DAYS360" }, 105 | { "name": "DB" }, 106 | { "name": "DCOUNT" }, 107 | { "name": "DCOUNTA" }, 108 | { "name": "DDB" }, 109 | { "name": "DEC2BIN" }, 110 | { "name": "DEC2HEX" }, 111 | { "name": "DEC2OCT" }, 112 | { "name": "DECIMAL" }, 113 | { "name": "DEGREES" }, 114 | { "name": "DELTA" }, 115 | { "name": "DETECTLANGUAGE" }, 116 | { "name": "DEVSQ" }, 117 | { "name": "DGET" }, 118 | { "name": "DISC" }, 119 | { "name": "DIVIDE" }, 120 | { "name": "DMAX" }, 121 | { "name": "DMIN" }, 122 | { "name": "DOLLAR" }, 123 | { "name": "DOLLARDE" }, 124 | { "name": "DOLLARFR" }, 125 | { "name": "DPRODUCT" }, 126 | { "name": "DSTDEV" }, 127 | { "name": "DSTDEVP" }, 128 | { "name": "DSUM" }, 129 | { "name": "DURATION" }, 130 | { "name": "DVAR" }, 131 | { "name": "DVARP" }, 132 | { "name": "EDATE" }, 133 | { "name": "EFFECT" }, 134 | { "name": "ENCODEURL" }, 135 | { "name": "EOMONTH" }, 136 | { "name": "EQ" }, 137 | { "name": "ERF" }, 138 | { "name": "ERF.PRECISE" }, 139 | { "name": "ERFC" }, 140 | { "name": "ERFC.PRECISE" }, 141 | { "name": "ERROR.TYPE" }, 142 | { "name": "EVEN" }, 143 | { "name": "EXACT" }, 144 | { "name": "EXP" }, 145 | { "name": "EXPON.DIST" }, 146 | { "name": "EXPONDIST" }, 147 | { "name": "F.DIST" }, 148 | { "name": "F.DIST.RT" }, 149 | { "name": "F.INV" }, 150 | { "name": "F.INV.RT" }, 151 | { "name": "F.TEST" }, 152 | { "name": "FACT" }, 153 | { "name": "FACTDOUBLE" }, 154 | { "name": "FDIST" }, 155 | { "name": "FILTER" }, 156 | { "name": "FIND" }, 157 | { "name": "FINDB" }, 158 | { "name": "FINV" }, 159 | { "name": "FISHER" }, 160 | { "name": "FISHERINV" }, 161 | { "name": "FIXED" }, 162 | { "name": "FLATTEN" }, 163 | { "name": "FLOOR" }, 164 | { "name": "FLOOR.MATH" }, 165 | { "name": "FLOOR.PRECISE" }, 166 | { "name": "FORECAST" }, 167 | { "name": "FORECAST.LINEAR" }, 168 | { "name": "FORMULATEXT" }, 169 | { "name": "FREQUENCY" }, 170 | { "name": "FTEST" }, 171 | { "name": "FV" }, 172 | { "name": "FVSCHEDULE" }, 173 | { "name": "GAMMA" }, 174 | { "name": "GAMMA.DIST" }, 175 | { "name": "GAMMA.INV" }, 176 | { "name": "GAMMADIST" }, 177 | { "name": "GAMMAINV" }, 178 | { "name": "GAMMALN" }, 179 | { "name": "GAMMALN.PRECISE" }, 180 | { "name": "GAUSS" }, 181 | { "name": "GCD" }, 182 | { "name": "GEOMEAN" }, 183 | { "name": "GESTEP" }, 184 | { "name": "GETPIVOTDATA" }, 185 | { "name": "GOOGLEFINANCE" }, 186 | { "name": "GOOGLETRANSLATE" }, 187 | { "name": "GROWTH" }, 188 | { "name": "GT" }, 189 | { "name": "GTE" }, 190 | { "name": "HARMEAN" }, 191 | { "name": "HEX2BIN" }, 192 | { "name": "HEX2DEC" }, 193 | { "name": "HEX2OCT" }, 194 | { "name": "HLOOKUP" }, 195 | { "name": "HOUR" }, 196 | { "name": "HYPERLINK" }, 197 | { "name": "HYPGEOM.DIST" }, 198 | { "name": "HYPGEOMDIST" }, 199 | { "name": "IF" }, 200 | { "name": "IFERROR" }, 201 | { "name": "IFNA" }, 202 | { "name": "IFS" }, 203 | { "name": "IMABS" }, 204 | { "name": "IMAGE" }, 205 | { "name": "IMAGINARY" }, 206 | { "name": "IMARGUMENT" }, 207 | { "name": "IMCONJUGATE" }, 208 | { "name": "IMCOS" }, 209 | { "name": "IMCOSH" }, 210 | { "name": "IMCOT" }, 211 | { "name": "IMCOTH" }, 212 | { "name": "IMCSC" }, 213 | { "name": "IMCSCH" }, 214 | { "name": "IMDIV" }, 215 | { "name": "IMEXP" }, 216 | { "name": "IMLN" }, 217 | { "name": "IMLOG" }, 218 | { "name": "IMLOG10" }, 219 | { "name": "IMLOG2" }, 220 | { "name": "IMPORTDATA" }, 221 | { "name": "IMPORTFEED" }, 222 | { "name": "IMPORTHTML" }, 223 | { "name": "IMPORTRANGE" }, 224 | { "name": "IMPORTXML" }, 225 | { "name": "IMPOWER" }, 226 | { "name": "IMPRODUCT" }, 227 | { "name": "IMREAL" }, 228 | { "name": "IMSEC" }, 229 | { "name": "IMSECH" }, 230 | { "name": "IMSIN" }, 231 | { "name": "IMSINH" }, 232 | { "name": "IMSQRT" }, 233 | { "name": "IMSUB" }, 234 | { "name": "IMSUM" }, 235 | { "name": "IMTAN" }, 236 | { "name": "IMTANH" }, 237 | { "name": "INDEX" }, 238 | { "name": "INDIRECT" }, 239 | { "name": "INT" }, 240 | { "name": "INTERCEPT" }, 241 | { "name": "INTRATE" }, 242 | { "name": "IPMT" }, 243 | { "name": "IRR" }, 244 | { "name": "ISBETWEEN" }, 245 | { "name": "ISBLANK" }, 246 | { "name": "ISDATE" }, 247 | { "name": "ISEMAIL" }, 248 | { "name": "ISERR" }, 249 | { "name": "ISERROR" }, 250 | { "name": "ISEVEN" }, 251 | { "name": "ISFORMULA" }, 252 | { "name": "ISLOGICAL" }, 253 | { "name": "ISNA" }, 254 | { "name": "ISNONTEXT" }, 255 | { "name": "ISNUMBER" }, 256 | { "name": "ISO.CEILING" }, 257 | { "name": "ISODD" }, 258 | { "name": "ISOWEEKNUM" }, 259 | { "name": "ISPMT" }, 260 | { "name": "ISREF" }, 261 | { "name": "ISTEXT" }, 262 | { "name": "ISURL" }, 263 | { "name": "JOIN" }, 264 | { "name": "KURT" }, 265 | { "name": "LAMBDA" }, 266 | { "name": "LARGE" }, 267 | { "name": "LCM" }, 268 | { "name": "LEFT" }, 269 | { "name": "LEFTB" }, 270 | { "name": "LEN" }, 271 | { "name": "LENB" }, 272 | { "name": "LINEST" }, 273 | { "name": "LN" }, 274 | { "name": "LOG" }, 275 | { "name": "LOG10" }, 276 | { "name": "LOGEST" }, 277 | { "name": "LOGINV" }, 278 | { "name": "LOGNORM.DIST" }, 279 | { "name": "LOGNORM.INV" }, 280 | { "name": "LOGNORMDIST" }, 281 | { "name": "LOOKUP" }, 282 | { "name": "LOWER" }, 283 | { "name": "LT" }, 284 | { "name": "LTE" }, 285 | { "name": "MAKEARRAY" }, 286 | { "name": "MAP" }, 287 | { "name": "MATCH" }, 288 | { "name": "MAX" }, 289 | { "name": "MAXA" }, 290 | { "name": "MAXIFS" }, 291 | { "name": "MDETERM" }, 292 | { "name": "MDURATION" }, 293 | { "name": "MEDIAN" }, 294 | { "name": "MID" }, 295 | { "name": "MIDB" }, 296 | { "name": "MIN" }, 297 | { "name": "MINA" }, 298 | { "name": "MINIFS" }, 299 | { "name": "MINUS" }, 300 | { "name": "MINUTE" }, 301 | { "name": "MINVERSE" }, 302 | { "name": "MIRR" }, 303 | { "name": "MMULT" }, 304 | { "name": "MOD" }, 305 | { "name": "MODE" }, 306 | { "name": "MODE.MULT" }, 307 | { "name": "MODE.SNGL" }, 308 | { "name": "MONTH" }, 309 | { "name": "MROUND" }, 310 | { "name": "MULTINOMIAL" }, 311 | { "name": "MULTIPLY" }, 312 | { "name": "MUNIT" }, 313 | { "name": "N" }, 314 | { "name": "NA" }, 315 | { "name": "NE" }, 316 | { "name": "NEGBINOM.DIST" }, 317 | { "name": "NEGBINOMDIST" }, 318 | { "name": "NETWORKDAYS" }, 319 | { "name": "NETWORKDAYS.INTL" }, 320 | { "name": "NOMINAL" }, 321 | { "name": "NORM.DIST" }, 322 | { "name": "NORM.INV" }, 323 | { "name": "NORM.S.DIST" }, 324 | { "name": "NORM.S.INV" }, 325 | { "name": "NORMDIST" }, 326 | { "name": "NORMINV" }, 327 | { "name": "NORMSDIST" }, 328 | { "name": "NORMSINV" }, 329 | { "name": "NOT" }, 330 | { "name": "NOW" }, 331 | { "name": "NPER" }, 332 | { "name": "NPV" }, 333 | { "name": "OCT2BIN" }, 334 | { "name": "OCT2DEC" }, 335 | { "name": "OCT2HEX" }, 336 | { "name": "ODD" }, 337 | { "name": "OFFSET" }, 338 | { "name": "OR" }, 339 | { "name": "PDURATION" }, 340 | { "name": "PEARSON" }, 341 | { "name": "PERCENTILE" }, 342 | { "name": "PERCENTILE.EXC" }, 343 | { "name": "PERCENTILE.INC" }, 344 | { "name": "PERCENTRANK" }, 345 | { "name": "PERCENTRANK.EXC" }, 346 | { "name": "PERCENTRANK.INC" }, 347 | { "name": "PERMUT" }, 348 | { "name": "PERMUTATIONA" }, 349 | { "name": "PHI" }, 350 | { "name": "PI" }, 351 | { "name": "PMT" }, 352 | { "name": "POISSON" }, 353 | { "name": "POISSON.DIST" }, 354 | { "name": "POW" }, 355 | { "name": "POWER" }, 356 | { "name": "PPMT" }, 357 | { "name": "PRICE" }, 358 | { "name": "PRICEDISC" }, 359 | { "name": "PRICEMAT" }, 360 | { "name": "PROB" }, 361 | { "name": "PRODUCT" }, 362 | { "name": "PROPER" }, 363 | { "name": "PV" }, 364 | { "name": "QUARTILE" }, 365 | { "name": "QUARTILE.EXC" }, 366 | { "name": "QUARTILE.INC" }, 367 | { "name": "QUERY" }, 368 | { "name": "QUOTIENT" }, 369 | { "name": "RADIANS" }, 370 | { "name": "RAND" }, 371 | { "name": "RANDARRAY" }, 372 | { "name": "RANDBETWEEN" }, 373 | { "name": "RANK" }, 374 | { "name": "RANK.AVG" }, 375 | { "name": "RANK.EQ" }, 376 | { "name": "RATE" }, 377 | { "name": "RECEIVED" }, 378 | { "name": "REDUCE" }, 379 | { "name": "REGEXEXTRACT" }, 380 | { "name": "REGEXMATCH" }, 381 | { "name": "REGEXREPLACE" }, 382 | { "name": "REPLACE" }, 383 | { "name": "REPLACEB" }, 384 | { "name": "REPT" }, 385 | { "name": "RIGHT" }, 386 | { "name": "RIGHTB" }, 387 | { "name": "ROMAN" }, 388 | { "name": "ROUND" }, 389 | { "name": "ROUNDDOWN" }, 390 | { "name": "ROUNDUP" }, 391 | { "name": "ROW" }, 392 | { "name": "ROWS" }, 393 | { "name": "RRI" }, 394 | { "name": "RSQ" }, 395 | { "name": "SCAN" }, 396 | { "name": "SEARCH" }, 397 | { "name": "SEARCHB" }, 398 | { "name": "SEC" }, 399 | { "name": "SECH" }, 400 | { "name": "SECOND" }, 401 | { "name": "SEQUENCE" }, 402 | { "name": "SERIESSUM" }, 403 | { "name": "SIGN" }, 404 | { "name": "SIN" }, 405 | { "name": "SINH" }, 406 | { "name": "SKEW" }, 407 | { "name": "SKEW.P" }, 408 | { "name": "SLN" }, 409 | { "name": "SLOPE" }, 410 | { "name": "SMALL" }, 411 | { "name": "SORT" }, 412 | { "name": "SORTN" }, 413 | { "name": "SPARKLINE" }, 414 | { "name": "SPLIT" }, 415 | { "name": "SQRT" }, 416 | { "name": "SQRTPI" }, 417 | { "name": "STANDARDIZE" }, 418 | { "name": "STDEV" }, 419 | { "name": "STDEV.P" }, 420 | { "name": "STDEV.S" }, 421 | { "name": "STDEVA" }, 422 | { "name": "STDEVP" }, 423 | { "name": "STDEVPA" }, 424 | { "name": "STEYX" }, 425 | { "name": "SUBSTITUTE" }, 426 | { "name": "SUBTOTAL" }, 427 | { "name": "SUM" }, 428 | { "name": "SUMIF" }, 429 | { "name": "SUMIFS" }, 430 | { "name": "SUMPRODUCT" }, 431 | { "name": "SUMSQ" }, 432 | { "name": "SUMX2MY2" }, 433 | { "name": "SUMX2PY2" }, 434 | { "name": "SUMXMY2" }, 435 | { "name": "SWITCH" }, 436 | { "name": "SYD" }, 437 | { "name": "T" }, 438 | { "name": "T.DIST" }, 439 | { "name": "T.DIST.2T" }, 440 | { "name": "T.DIST.RT" }, 441 | { "name": "T.INV" }, 442 | { "name": "T.INV.2T" }, 443 | { "name": "T.TEST" }, 444 | { "name": "TAN" }, 445 | { "name": "TANH" }, 446 | { "name": "TBILLEQ" }, 447 | { "name": "TBILLPRICE" }, 448 | { "name": "TBILLYIELD" }, 449 | { "name": "TDIST" }, 450 | { "name": "TEXT" }, 451 | { "name": "TEXTJOIN" }, 452 | { "name": "TIME" }, 453 | { "name": "TIMEVALUE" }, 454 | { "name": "TINV" }, 455 | { "name": "TO_DATE" }, 456 | { "name": "TO_DOLLARS" }, 457 | { "name": "TO_PERCENT" }, 458 | { "name": "TO_PURE_NUMBER" }, 459 | { "name": "TO_TEXT" }, 460 | { "name": "TODAY" }, 461 | { "name": "TRANSPOSE" }, 462 | { "name": "TREND" }, 463 | { "name": "TRIM" }, 464 | { "name": "TRIMMEAN" }, 465 | { "name": "TRUNC" }, 466 | { "name": "TTEST" }, 467 | { "name": "TYPE" }, 468 | { "name": "UMINUS" }, 469 | { "name": "UNARY_PERCENT" }, 470 | { "name": "UNICHAR" }, 471 | { "name": "UNICODE" }, 472 | { "name": "UNIQUE" }, 473 | { "name": "UNIQUE" }, 474 | { "name": "UPLUS" }, 475 | { "name": "UPPER" }, 476 | { "name": "VALUE" }, 477 | { "name": "VAR" }, 478 | { "name": "VAR.P" }, 479 | { "name": "VAR.S" }, 480 | { "name": "VARA" }, 481 | { "name": "VARP" }, 482 | { "name": "VARPA" }, 483 | { "name": "VDB" }, 484 | { "name": "VLOOKUP" }, 485 | { "name": "WEEKDAY" }, 486 | { "name": "WEEKNUM" }, 487 | { "name": "WEIBULL" }, 488 | { "name": "WEIBULL.DIST" }, 489 | { "name": "WORKDAY" }, 490 | { "name": "WORKDAY.INTL" }, 491 | { "name": "XIRR" }, 492 | { "name": "XLOOKUP" }, 493 | { "name": "XNPV" }, 494 | { "name": "XOR" }, 495 | { "name": "YEAR" }, 496 | { "name": "YEARFRAC" }, 497 | { "name": "YIELD" }, 498 | { "name": "YIELDDISC" }, 499 | { "name": "YIELDMAT" }, 500 | { "name": "Z.TEST" }, 501 | { "name": "ZTEST" } 502 | ] 503 | -------------------------------------------------------------------------------- /Workspace Add-on/More than 100 widgets/UI.gs: -------------------------------------------------------------------------------- 1 | const headerCard = CardService.newCardHeader() 2 | .setTitle('An alternate to displaying more than 100 widgets/sections.') 3 | .setImageUrl('https://script.gs/content/images/2022/11/custom-functions-logo.jpeg') 4 | .setImageStyle(CardService.ImageStyle.SQUARE); 5 | 6 | const footerCard = CardService.newFixedFooter() 7 | .setPrimaryButton(CardService.newTextButton() 8 | .setText('FOLLOW') 9 | .setBackgroundColor("#f57c00") 10 | .setOpenLink(CardService.newOpenLink() 11 | .setUrl('https://twitter.com/intent/follow?screen_name=choraria'))) 12 | .setSecondaryButton(CardService.newTextButton() 13 | .setText('READ MORE') 14 | .setOpenLink(CardService.newOpenLink() 15 | .setUrl('https://script.gs/'))); 16 | 17 | function sheetsHome(e) { 18 | const userInput = e.formInput && e.formInput !== '' ? e.formInput['searchInput'] : null; 19 | 20 | const rawData = functions; 21 | 22 | let newData = rawData.sort((a, b) => a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)) 23 | let userFiltered 24 | try { 25 | userFiltered = newData.filter(func => userInput !== null && userInput !== '' && userInput !== undefined ? func.name.toString().match(userInput.toUpperCase()) : func); 26 | } catch (e) { 27 | userFiltered = null; 28 | } 29 | 30 | const searchOnChangeAction = CardService.newAction() 31 | .setFunctionName('sheetsHomeOnChange'); 32 | const searchWidget = CardService.newTextInput() 33 | .setFieldName('searchInput') 34 | .setTitle('Search') 35 | .setOnChangeAction(searchOnChangeAction); 36 | userInput !== null && userInput !== '' && userInput !== undefined ? searchWidget.setValue(userInput) : null; 37 | const functionList = CardService.newCardSection(); 38 | functionList.addWidget(searchWidget); 39 | const functionGrid = CardService.newGrid() 40 | .setTitle(" ") 41 | .setBorderStyle(CardService.newBorderStyle() 42 | .setType(CardService.BorderType.STROKE) 43 | .setCornerRadius(8) 44 | .setStrokeColor("#D3D3D3")) 45 | .setNumColumns(1) 46 | .setOnClickAction(CardService.newAction() 47 | .setFunctionName("functionClick_")); 48 | const funcGridItem = (func) => CardService.newGridItem().setTitle(func.name).setIdentifier(JSON.stringify(func)); 49 | if (userFiltered?.length > 0) { 50 | userFiltered.forEach(func => functionGrid.addItem(funcGridItem(func))); 51 | } else { 52 | functionList.addWidget(CardService.newDecoratedText() 53 | .setText(`No function matched: "${userInput}"`) 54 | .setWrapText(true)) 55 | newData.forEach(func => functionGrid.addItem(funcGridItem(func))); 56 | } 57 | functionList.addWidget(functionGrid); 58 | 59 | const card = CardService.newCardBuilder() 60 | .setHeader(headerCard) 61 | .addSection(functionList) 62 | .setFixedFooter(footerCard) 63 | .build(); 64 | 65 | return card; 66 | } 67 | 68 | function sheetsHomeOnChange(e) { 69 | let card = sheetsHome(e); 70 | const actionBuilder = CardService.newActionResponseBuilder(); 71 | actionBuilder.setNavigation(CardService.newNavigation().updateCard(card)) 72 | .setStateChanged(true) 73 | return actionBuilder.build(); 74 | } 75 | 76 | function functionClick_(e) { 77 | const actionBuilder = CardService.newActionResponseBuilder(); 78 | actionBuilder.setNotification(CardService.newNotification() 79 | .setText(`You clicked ${JSON.parse(e.parameters.grid_item_identifier).name}`) 80 | .setType(CardService.NotificationType.INFO)) 81 | return actionBuilder.build(); 82 | } 83 | -------------------------------------------------------------------------------- /Workspace Add-on/More than 100 widgets/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "exceptionLogging": "STACKDRIVER", 3 | "runtimeVersion": "V8", 4 | "addOns": { 5 | "common": { 6 | "name": "More than 100", 7 | "logoUrl": "https://script.gs/content/images/2022/11/custom-functions-logo.jpeg", 8 | "layoutProperties": { 9 | "primaryColor": "#ffb300", 10 | "secondaryColor": "#f57c00" 11 | }, 12 | "homepageTrigger": { 13 | "enabled": true, 14 | "runFunction": "sheetsHome" 15 | }, 16 | "openLinkUrlPrefixes": [ 17 | "https://twitter.com/", 18 | "https://script.gs/", 19 | "https://workspace.google.com/marketplace/app/" 20 | ], 21 | "universalActions": [ 22 | { 23 | "label": "Privacy", 24 | "openLink": "https://script.gs/privacy" 25 | }, 26 | { 27 | "label": "Terms", 28 | "openLink": "https://script.gs/terms/" 29 | } 30 | ] 31 | }, 32 | "sheets": {} 33 | } 34 | } 35 | --------------------------------------------------------------------------------