├── Code.gs ├── LICENSE ├── README.md ├── img ├── step1.png ├── step10-1.png ├── step10-2.png ├── step10-3.png ├── step2.png ├── step3.png ├── step4.png ├── step5.png ├── step6.png ├── step7.png ├── step8-1.png ├── step8-2.png ├── step9-1.png ├── step9-2.png ├── tsformbot.jpg └── tsformbotedu.jpg └── notifications.png /Code.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Laura Taylor 3 | * (https://github.com/techstreams/TSFormBot) 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 | */ 23 | 24 | 25 | /* 26 | * This function adds a 'TSFormBot' menu to the active form when opened 27 | */ 28 | function onOpen() { 29 | FormApp.getUi().createMenu('TSFormBot') 30 | .addItem('🕜 Enable Bot', 'TSFormBot.enableBot') 31 | .addToUi(); 32 | }; 33 | 34 | /* 35 | * TSFormBot Class - Google Form Notifications 36 | */ 37 | class TSFormBot { 38 | 39 | /* 40 | * Constructor function 41 | */ 42 | constructor() { 43 | const self = this; 44 | self.form = FormApp.getActiveForm(); 45 | } 46 | 47 | /* 48 | * This static method sets the Hangouts Chat Room Webhook URL and configures the form submit trigger 49 | * @param {string} triggerFunction - name of trigger function to execute on form submission 50 | */ 51 | static enableBot(triggerFunction = 'TSFormBot.postToRoom') { 52 | const ui = FormApp.getUi(), 53 | url = ui.prompt('Webhook URL', 'Enter Chat Room Webhook URL', FormApp.getUi().ButtonSet.OK).getResponseText(); 54 | let submitTriggers; 55 | if (url !== '' && url.match(/^https:\/\/chat\.googleapis\.com/)) { 56 | submitTriggers = ScriptApp.getProjectTriggers() 57 | .filter(trigger => trigger.getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT && trigger.getHandlerFunction() === triggerFunction); 58 | if (submitTriggers.length < 1) { 59 | ScriptApp.newTrigger(triggerFunction).forForm(FormApp.getActiveForm()).onFormSubmit().create(); 60 | } 61 | PropertiesService.getScriptProperties().setProperty('WEBHOOK_URL', url); 62 | ui.alert('TSFormBot Configuration COMPLETE.\n\nClick "Ok" to continue.'); 63 | } else { 64 | ui.alert('TSFormBot Configuration FAILED!.\n\nInvalid Chat Room Webhook URL.\n\nPlease enter a valid url.'); 65 | } 66 | } 67 | 68 | /* 69 | * This static method processes the form submission and posts a form notification to the Hangouts Chat Room 70 | * @param {Object} e - event object passed to the form submit function 71 | */ 72 | static postToRoom(e) { 73 | const tsfb = new TSFormBot(), 74 | url = PropertiesService.getScriptProperties().getProperty('WEBHOOK_URL'); 75 | if (url) { 76 | try { 77 | const payload = tsfb.getResponse_(e); 78 | const options = { 79 | 'method' : 'post', 80 | 'contentType': 'application/json; charset=UTF-8', 81 | 'payload' : JSON.stringify(payload) 82 | }; 83 | UrlFetchApp.fetch(url, options); 84 | } catch(err) { 85 | console.log('TSFormBot: Error processing Bot. ' + err.message); 86 | } 87 | } else { 88 | console.log('TSFormBot: No Webhook URL specified for Bot'); 89 | } 90 | } 91 | 92 | /* 93 | * This method creates a 'key/value' response widget 94 | * @param {string} top - widget top label 95 | * @param {string} content - widget content 96 | * @param {boolean} multiline - indicates whether content can span multiple lines 97 | * @param {string} bottom - (optional) widget bottom label 98 | * @return {Object} 'key/value' widget 99 | */ 100 | createKeyValueWidget_(top, content, multiline=false,icon, bottom) { 101 | const keyValue = {}, 102 | widget = {}; 103 | keyValue.topLabel = top; 104 | keyValue.content = content; 105 | keyValue.contentMultiline = multiline; 106 | if (bottom) { 107 | keyValue.bottomLabel = bottom; 108 | } 109 | if (icon) { 110 | keyValue.icon = icon; 111 | } 112 | widget.keyValue = keyValue; 113 | return widget; 114 | } 115 | 116 | /* 117 | * This method creates a 'text button' response widget 118 | * @param {string} text - widget button text 119 | * @param {string} url - widget button URL 120 | * @return {Object} 'text button' widget 121 | */ 122 | createTextBtnWidget_(text, url){ 123 | const widget = {}, 124 | textBtn = {}, 125 | click = {}; 126 | click.openLink = {url: url}; 127 | textBtn.text = text; 128 | textBtn.onClick = click; 129 | widget.textButton = textBtn; 130 | return widget; 131 | } 132 | 133 | /* 134 | * This method creates a 'text' response widget 135 | * @param {string} text - widget text 136 | * @return {Object} 'text' widget 137 | */ 138 | createTextWidget_(text) { 139 | const widget = {}, 140 | textParagraph = {}; 141 | textParagraph.text = text; 142 | widget.textParagraph = textParagraph; 143 | return widget; 144 | } 145 | 146 | /* 147 | * This method creates a response card button footer widget 148 | * @param {FormResponse} formResponse - form submission response 149 | * @return {Object} card button footer widget 150 | */ 151 | getCardFooter_(formResponse) { 152 | const self = this, 153 | btns = {buttons:[]}, 154 | widget = {widgets:[]}; 155 | btns.buttons.push(self.createTextBtnWidget_("GO TO RESPONSE", formResponse.getEditResponseUrl())); 156 | btns.buttons.push(self.createTextBtnWidget_("GO TO FORM", self.form.getEditUrl())); 157 | widget.widgets.push(btns); 158 | return widget; 159 | } 160 | 161 | /* 162 | * This method creates a form item response widget 163 | * @param {ItemResponse} ir - form item response 164 | * @return {Object} form item response widget 165 | */ 166 | getCardFormWidget_(ir) { 167 | const self = this, 168 | item = ir.getItem(), 169 | title = item.getTitle(), 170 | itemtype = item.getType(), 171 | widget = {widgets:[]}; 172 | let content, date, day, footer = null, hour, minute, month, pattern, year; 173 | 174 | switch (itemtype) { 175 | case FormApp.ItemType.CHECKBOX: 176 | content = ir.getResponse().map(i => i).join('\n'); 177 | break; 178 | case FormApp.ItemType.GRID: 179 | content = item.asGridItem().getRows() 180 | .map((r,i) => { 181 | const resp = ir.getResponse()[i]; 182 | return `${r}: ${resp && resp !== '' ? resp : ' '}`; 183 | }).join('\n'); 184 | break; 185 | case FormApp.ItemType.CHECKBOX_GRID: 186 | content = item.asCheckboxGridItem().getRows() 187 | .map((row,i) => ({name:row,val:ir.getResponse()[i]})) 188 | .filter(row => row.val) 189 | .map(row => `${row.name}:\n${row.val.filter(v => v !== '').map(v => ` - ${v}`).join('\n')}`) 190 | .join('\n'); 191 | break; 192 | case FormApp.ItemType.DATETIME: 193 | pattern = /^(\d{4})-(\d{2})-(\d{2})\s(\d{1,2}):(\d{2})$/; 194 | [, year, month, day, hour, minute] = pattern.exec(ir.getResponse()); 195 | date = new Date(`${year}-${month}-${day}T${('0' + hour).slice(-2)}:${minute}:00`); 196 | content = Utilities.formatDate(date,Session.getTimeZone(),"yyyy-MM-dd h:mm aaa"); 197 | break; 198 | case FormApp.ItemType.TIME: 199 | pattern = /^(\d{1,2}):(\d{2})$/; 200 | [, hour, minute] = pattern.exec(ir.getResponse()); 201 | date = new Date(); 202 | date.setHours(parseInt(hour,10), parseInt(minute,10)); 203 | content = Utilities.formatDate(date,Session.getTimeZone(),"h:mm aaa"); 204 | break; 205 | case FormApp.ItemType.DURATION: 206 | content = `${ir.getResponse()}`; 207 | footer = 'Hrs : Min : Sec'; 208 | break; 209 | case FormApp.ItemType.SCALE: 210 | const scale = item.asScaleItem(); 211 | content = ir.getResponse(); 212 | footer = `${scale.getLeftLabel()}(${scale.getLowerBound()}) ... ${scale.getRightLabel()}(${scale.getUpperBound()})`; 213 | break; 214 | case FormApp.ItemType.MULTIPLE_CHOICE: 215 | case FormApp.ItemType.LIST: 216 | case FormApp.ItemType.DATE: 217 | case FormApp.ItemType.PARAGRAPH_TEXT: 218 | case FormApp.ItemType.TEXT: 219 | content = ir.getResponse(); 220 | break; 221 | default: 222 | content = "Unsupported form element"; 223 | } 224 | if (footer) { 225 | widget.widgets.push(self.createKeyValueWidget_(title, content, true, "STAR", footer)); 226 | } else { 227 | widget.widgets.push(self.createKeyValueWidget_(title, content, true, "STAR")); 228 | } 229 | return widget; 230 | } 231 | 232 | /* 233 | * This method creates a response card header widget 234 | * @return {Object} card header widget 235 | */ 236 | getCardHeader_() { 237 | const widget = {}; 238 | widget.title = "TSFormBot"; 239 | widget.subtitle = "Form Notifications Bot"; 240 | widget.imageUrl = "https://raw.githubusercontent.com/techstreams/TSFormBot/master/notifications.png"; 241 | widget.imageStyle = "IMAGE"; 242 | return widget; 243 | } 244 | 245 | /* 246 | * This method creates a response card information widget 247 | * @return {Object} card information widget 248 | */ 249 | getCardIntro_() { 250 | const self = this, 251 | date = new Date(), 252 | title = `${self.form.getTitle()}<\/b> has a new submission!<\/font><\/b>`, 253 | timestamp = `${Utilities.formatDate(date,Session.getTimeZone(),"MM/dd/yyyy hh:mm:ss a (z)")}`, 254 | widgets = [], 255 | widget = {}; 256 | widgets.push(self.createTextWidget_(title)); 257 | widgets.push(self.createTextWidget_(timestamp)); 258 | widget.widgets = widgets; 259 | return widget; 260 | } 261 | 262 | /* 263 | * This method creates a response card 264 | * @param {Object} e - event object passed to the form submit function 265 | * @return {Object} card response 266 | */ 267 | getResponse_(e) { 268 | const self = this, 269 | formResponse = e.response, 270 | itemResponses = formResponse.getItemResponses(), 271 | sections = [], 272 | cards = [], 273 | card = {}, 274 | response = {}; 275 | card.header = self.getCardHeader_(); 276 | sections.push(self.getCardIntro_()); 277 | if (self.form.collectsEmail()) { 278 | sections.push({"widgets": [self.createKeyValueWidget_('Submitted By', formResponse.getRespondentEmail(), true, "STAR")]}); 279 | } 280 | itemResponses.forEach(ir => { 281 | if(ir.getResponse()){ 282 | sections.push(self.getCardFormWidget_(ir)); 283 | } 284 | }); 285 | sections.push(self.getCardFooter_(formResponse)); 286 | card.sections = sections; 287 | cards.push(card); 288 | response.cards = cards; 289 | return response; 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Laura Taylor 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## TSFormBot 2 | 3 | *If you enjoy my [Google Workspace Apps Script](https://developers.google.com/apps-script) work, please consider buying me a cup of coffee!* 4 | 5 | 6 | [![](https://techstreams.github.io/images/bmac.svg)](https://www.buymeacoffee.com/techstreams) 7 | 8 | --- 9 | 10 | **TSFormBot** is an [Apps Script](https://www.google.com/script/start/) powered [Google Form](https://www.google.com/forms/about/) **Hangouts Chat Bot** which creates form submission webhook notifications for [Hangouts Chat Rooms](https://gsuite.google.com/products/chat/). 11 | 12 | *:point_right: For an overview, see this [blog post](https://medium.com/@techstreams/its-time-to-chat-with-a-little-help-from-our-friends-caf99b9969f8).* 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Use with G Suite for BusinessUse with G Suite for Education
26 | 27 | 28 |
29 | 30 | --- 31 | 32 | TSFormBot has been upgraded to include form responses. 33 | 34 | | SUPPORTED | GOOGLE FORM ELEMENT | 35 | | :-------: | :----------------- | 36 | | :heavy_check_mark: | Short Answer | 37 | | :heavy_check_mark: | Paragraph | 38 | | :heavy_check_mark: | Multiple Choice | 39 | | :heavy_check_mark: | Checkboxes | 40 | | :heavy_check_mark: | Dropdown | 41 | | :heavy_check_mark: | Linear Scale | 42 | | :heavy_check_mark: | Multiple Choice Grid | 43 | | :heavy_check_mark: | Checkbox Grid | 44 | | :heavy_check_mark: | Date | 45 | | :heavy_check_mark: | Date/Time | 46 | | :heavy_check_mark: | Time | 47 | | :heavy_check_mark: | Duration | 48 | | :x: | File Upload | 49 | 50 |
51 | 52 | **Important:** 53 | 54 | *:point_right: Forms with the ['collect respondents' email addresses'](https://support.google.com/docs/answer/139706) option enabled will display the form submitter email in the bot notification.* 55 | 56 | *:point_right: Form responses for elements containing images will only display associated text in the bot notification.* 57 | 58 |
59 | 60 | **Looking for the previous version of TSFormBot?** See [v1](https://github.com/techstreams/TSFormBot/tree/v1). 61 | 62 | 63 | --- 64 | 65 | ## Getting Started 66 | 67 | ### Installation 68 | 69 | **New Install** 70 | 71 | * Create a new [Google Form](form.new) 72 | * From within the new form, select the **More _(three dots)_ > Script editor** menu 73 | * Delete the code in `Code.gs` from the script editor 74 | * Click on the `Untitled project` project name, and rename to `TSFormBot` 75 | * Copy and paste the [code](Code.gs) into the script editor 76 | * Select the menu **File > Save all** 77 | * Close the Script editor window 78 | * Reload the Form 79 | 80 |
81 | 82 | **Upgrade an Existing TSFormBot** 83 | 84 | * Open existing TSFormBot enabled Google Form 85 | * From within the form, select the **More _(three dots)_ > Script editor** menu 86 | * Delete the code in `Code.gs` from the script editor 87 | * Copy and paste the [code](Code.gs) into the script editor 88 | * Select the menu **File > Save all** 89 | * Select the **Run > Enable new Apps Script runtime powered by V8** *(if V8 is not already enabled)* 90 | * Select the **Edit > Current Project's Triggers** 91 | * Delete any existing project `From form - On form submit` triggers *(click the __More > Delete trigger__ menu to the right of each trigger to delete)* 92 | * Close the Project Triggers window 93 | * Close the Script editor window 94 | * Reload the Form 95 | 96 |
97 | 98 | 99 | > *:grey_question: Prefer to install the __TSFormBot - Kitchen Sink Demo__?* 100 | > * *Login to [Google Drive](https://drive.google.com/)* 101 | > * *Access the __[TSFormBot - Kitchen Sink Demo](https://docs.google.com/forms/d/1274HULOEGv9xVAhAVlwqmmwxPwsV8S5rBqAVzBQEj7I/template/preview)__* 102 | > * *Click the __Use Template__ button. This will copy the form to Google Drive.* 103 | 104 |
105 | 106 | 107 | ### Create Hangouts Chat Room Webhook 108 | 109 | **Access the Hangouts Chat Room Webhooks** 110 | 111 | * Go to [Hangouts Chat](https://chat.google.com) 112 | * Select the desired **Hangouts Chat room** *(or create a new one)* 113 | * Select the **dropdown** for the room 114 | * Select **Configure webhooks** 115 | 116 | ![](img/step2.png) 117 | 118 |
119 | 120 | **Add a Webhook** 121 | 122 | * Select **+ ADD WEBHOOK** 123 | 124 | ![](img/step3.png) 125 | 126 |
127 | 128 | **Enter a Webhook Name** 129 | 130 | * Enter a **webhook name** 131 | * *(Optional) Enter an Avatar URL* 132 | * Click **Save** 133 | 134 | ![](img/step4.png) 135 | 136 | **Important:** 137 | 138 | *:point_right: Multiple TSFormBot enabled Google Forms can be used to provide notifications to a single [Google Hangouts Chat Room](https://gsuite.google.com/products/chat/). Be sure to use descriptive webhook names to disguish multiple bots.* 139 | 140 |
141 | 142 | **Make a Copy of the URL** 143 | 144 | * Click the **Copy** button to make a copy of the webhook URL 145 | 146 | ![](img/step5.png) 147 | 148 | 149 |
150 | 151 | ### Configure TSFormBot 152 | 153 | **Enable TSFormBot** 154 | 155 | * Open the form and wait for the menu to fully load 156 | * Select the **TSFormBot** menu 157 | * Select the **Enable Bot** menu 158 | * *Complete the Google authoriation prompts if enabling TSFormBot for the first time and then re-run the __Enable Bot__ menu.* 159 | 160 | ![](img/step6.png) 161 | 162 | ![](img/step7.png) 163 | 164 |
165 | 166 | **Enter the Webhook URL** 167 | 168 | * Enter the full **Webhook URL** copied in **Make a Copy of the URL** step above 169 | * Click **Ok** 170 | * Click **Ok** when configuration complete 171 | 172 | ![](img/step9-1.png) 173 | 174 | ![](img/step9-2.png) 175 | 176 |
177 | 178 | 179 | ### Submit to the Google Form 180 | 181 | * Submit an entry to the form 182 | * A new form submission notification post will be made to the Hangouts Chat room 183 | 184 | **Important:** 185 | 186 | *:point_right: If the notification timestamp does not accurately reflect the correct timezone, ensure the Apps Script timezone is configured appropriately. The Apps Script timezone can be changed by opening the Form’s Apps Script editor (__More menu _(three dots)_ > Script editor__) and altering the timezone under the Apps Script editor __File > Project properties__ menu.* 187 | 188 | 189 |
190 | 191 | 192 | ## Important Notes 193 | 194 | * Multiple TSFormBot enabled Google Forms can be used to provide notifications to a single [Google Hangouts Chat Room](https://gsuite.google.com/products/chat/). 195 | 196 | * Check the [Apps Script Dashboard](https://script.google.com) for execution errors if TSFormBot does not work as expected. 197 | 198 | * TSFormBot may not be appropriate for all Google Forms. 199 | 200 | --- 201 | 202 | ## License 203 | 204 | **TSFormBot License** 205 | 206 | © Laura Taylor ([github.com/techstreams](https://github.com/techstreams)). Licensed under an MIT license. 207 | 208 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 209 | 210 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 211 | 212 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 213 | -------------------------------------------------------------------------------- /img/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step1.png -------------------------------------------------------------------------------- /img/step10-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step10-1.png -------------------------------------------------------------------------------- /img/step10-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step10-2.png -------------------------------------------------------------------------------- /img/step10-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step10-3.png -------------------------------------------------------------------------------- /img/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step2.png -------------------------------------------------------------------------------- /img/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step3.png -------------------------------------------------------------------------------- /img/step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step4.png -------------------------------------------------------------------------------- /img/step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step5.png -------------------------------------------------------------------------------- /img/step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step6.png -------------------------------------------------------------------------------- /img/step7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step7.png -------------------------------------------------------------------------------- /img/step8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step8-1.png -------------------------------------------------------------------------------- /img/step8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step8-2.png -------------------------------------------------------------------------------- /img/step9-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step9-1.png -------------------------------------------------------------------------------- /img/step9-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/step9-2.png -------------------------------------------------------------------------------- /img/tsformbot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/tsformbot.jpg -------------------------------------------------------------------------------- /img/tsformbotedu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/img/tsformbotedu.jpg -------------------------------------------------------------------------------- /notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSFormBot/dbb9e6807556bf0cefd392fd48e05b11e13a2c28/notifications.png --------------------------------------------------------------------------------