Skip to main content

Chat Notification Bot

The point of this script, and several versions of it, was to notify the group chat of a new form submission. Initially the various forms were all combined into one script, but we had several problems with this - maybe it was due to the transition Google Workspace was going through at the time, or my crappy coding skills? Either way, I opted to split the various forms and their notification scripts into their own file/project, which worked out better in the end.

/* 
 * This function adds a 'TSFormBot' menu to the active form when opened
 */
function onOpen() {
   FormApp.getUi().createMenu('SERBot')
          .addItem('🕜 Enable Bot', 'TSFormBot.enableBot')
          .addToUi();
};

/* 
 * TSFormBot Class - Google Form Notifications
 */
class TSFormBot {

  /* 
   * Constructor function
   */
  constructor() {
    const self = this;
    self.form = FormApp.getActiveForm();
  }
  
  /* 
   * This static method sets the Hangouts Chat Room Webhook URL and configures the form submit trigger
   * @param {string} triggerFunction - name of trigger function to execute on form submission
   */
  static enableBot(triggerFunction = 'TSFormBot.postToRoom') {
    const ui = FormApp.getUi(),
          url = ui.prompt('Webhook URL', 'Enter Chat Room Webhook URL', FormApp.getUi().ButtonSet.OK).getResponseText();
    let submitTriggers;
    if (url !== '' && url.match(/^https:\/\/chat\.googleapis\.com/)) {
      submitTriggers = ScriptApp.getProjectTriggers()
        .filter(trigger => trigger.getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT && trigger.getHandlerFunction() === triggerFunction);   
      if (submitTriggers.length < 1) {
        ScriptApp.newTrigger(triggerFunction).forForm(FormApp.getActiveForm()).onFormSubmit().create();
      }
      PropertiesService.getScriptProperties().setProperty('WEBHOOK_URL', url);
      ui.alert('TSFormBot Configuration COMPLETE.\n\nClick "Ok" to continue.');
    } else {
      ui.alert('TSFormBot Configuration FAILED!.\n\nInvalid Chat Room Webhook URL.\n\nPlease enter a valid url.');
    }  
  }
  
  /* 
   * This static method processes the form submission and posts a form notification to the Hangouts Chat Room
   * @param {Object} e - event object passed to the form submit function
   */
   static postToRoom(e) {
     const tsfb = new TSFormBot(),
           url = PropertiesService.getScriptProperties().getProperty('WEBHOOK_URL');
     if (url) {
       try {
         const payload = tsfb.getResponse_(e);
         const options = {
           'method' : 'post',
           'contentType': 'application/json; charset=UTF-8',
           'payload' : JSON.stringify(payload)
         };
         UrlFetchApp.fetch(url, options); 
       } catch(err) {
         console.log('TSFormBot: Error processing Bot. ' + err.message);
       }
     } else {
       console.log('TSFormBot: No Webhook URL specified for Bot');
     }
   }

  /* 
   * This method creates a 'key/value' response widget
   * @param {string} top - widget top label
   * @param {string} content - widget content
   * @param {boolean} multiline - indicates whether content can span multiple lines
   * @param {string} bottom - (optional) widget bottom label
   * @return {Object} 'key/value' widget
   */
  createKeyValueWidget_(top, content, multiline=false,icon, bottom) {
    const keyValue = {},
          widget = {};
    keyValue.topLabel = top;
    keyValue.content = content;
    keyValue.contentMultiline = multiline;
    if (bottom) {
      keyValue.bottomLabel = bottom;
    }
    if (icon) {
      keyValue.icon = icon;
    }
    widget.keyValue = keyValue;
    return widget;
  }

  /* 
   * This method creates a 'text button' response widget
   * @param {string} text - widget button text
   * @param {string} url - widget button URL
   * @return {Object} 'text button' widget
   */
  createTextBtnWidget_(text, url){
    const widget = {},
          textBtn = {},
          click = {};
    click.openLink = {url: url};
    textBtn.text = text;
    textBtn.onClick = click;
    widget.textButton = textBtn;
    return widget;
  }

  /* 
   * This method creates a 'text' response widget
   * @param {string} text - widget text
   * @return {Object} 'text' widget
   */
  createTextWidget_(text) {
    const widget = {},
          textParagraph = {};
    textParagraph.text = text;
    widget.textParagraph = textParagraph;
    return widget;
  }

  /* 
   * This method creates a response card button footer widget
   * @param {FormResponse} formResponse - form submission response
   * @return {Object} card button footer widget
   */
  getCardFooter_(formResponse) {
    const self = this,
          btns = {buttons:[]},
          widget = {widgets:[]};
    btns.buttons.push(self.createTextBtnWidget_("OPEN SHEET", "https://docs.google.com/spreadsheets/d/blahblahblahblah/edit?resourcekey#gid=blahblahblah"));
    btns.buttons.push(self.createTextBtnWidget_("GO TO FORM", self.form.getEditUrl()));
    widget.widgets.push(btns);
    return widget;
  }

  /* 
   * This method creates a form item response widget
   * @param {ItemResponse} ir - form item response
   * @return {Object} form item response widget
   */
  getCardFormWidget_(ir) {
    const self = this,
          item = ir.getItem(),
          title = item.getTitle(),
          itemtype = item.getType(),
          widget = {widgets:[]};
    let content, date, day, footer = null, hour, minute, month, pattern, year;
    
    switch (itemtype) {
      case FormApp.ItemType.CHECKBOX:
        content = ir.getResponse().map(i => i).join('\n');
        break;
      case FormApp.ItemType.GRID:
        content = item.asGridItem().getRows()
                      .map((r,i) => {
                         const resp = ir.getResponse()[i];
                         return `${r}: ${resp && resp !== '' ? resp : ' '}`;
                      }).join('\n');
        break;
      case FormApp.ItemType.CHECKBOX_GRID:
        content = item.asCheckboxGridItem().getRows()
                      .map((row,i) => ({name:row,val:ir.getResponse()[i]}))
                      .filter(row => row.val)
                      .map(row => `${row.name}:\n${row.val.filter(v => v !== '').map(v => `  - ${v}`).join('\n')}`)
                      .join('\n');
        break;
      case FormApp.ItemType.DATETIME:
        pattern = /^(\d{4})-(\d{2})-(\d{2})\s(\d{1,2}):(\d{2})$/;
        [, year, month, day, hour, minute] = pattern.exec(ir.getResponse());
        date = new Date(`${year}-${month}-${day}T${('0' + hour).slice(-2)}:${minute}:00`);
        content = Utilities.formatDate(date,Session.getTimeZone(),"yyyy-MM-dd   h:mm aaa");
        break;
      case FormApp.ItemType.TIME:
        pattern = /^(\d{1,2}):(\d{2})$/;
        [, hour, minute] = pattern.exec(ir.getResponse());
        date = new Date();
        date.setHours(parseInt(hour,10), parseInt(minute,10));
        content = Utilities.formatDate(date,Session.getTimeZone(),"h:mm aaa");
        break;
      case FormApp.ItemType.DURATION:
        content = `${ir.getResponse()}`;
        footer = 'Hrs : Min : Sec';
        break;
      case FormApp.ItemType.SCALE:
        const scale = item.asScaleItem();
        content = ir.getResponse();
        footer = `${scale.getLeftLabel()}(${scale.getLowerBound()}) ... ${scale.getRightLabel()}(${scale.getUpperBound()})`;
        break;
      case FormApp.ItemType.MULTIPLE_CHOICE:
      case FormApp.ItemType.LIST:
      case FormApp.ItemType.DATE:
      case FormApp.ItemType.PARAGRAPH_TEXT:
      case FormApp.ItemType.TEXT:
        content = ir.getResponse();
        break;
      default:
        content = "Unsupported form element";
    } 
    if (footer) {
      widget.widgets.push(self.createKeyValueWidget_(title, content, true, "STAR", footer));
    } else {
      widget.widgets.push(self.createKeyValueWidget_(title, content, true, "STAR")); 
    }   
    return widget;
  }

  /* 
   * This method creates a response card header widget
   * @return {Object} card header widget   
   * Green Notification Bell ID for imgURL below: 1HfhS_D7iCm5ft97tcGdGKvB9YfcXOBim
   */
  getCardHeader_() {
    const widget = {};
    widget.title = "CHANGE ME!";
    widget.subtitle = "CHANGE ME TOO!";
    widget.imageUrl = "https://drive.google.com/uc?export=view&id=blahblahblah";
    widget.imageStyle = "IMAGE";
    return widget;
  }

  /* 
   * This method creates a response card information widget
   * @return {Object} card information widget
   */
  getCardIntro_() {
    const self = this,
          date = new Date(),
          title = `${self.form.getTitle()}<\/b> has a new submission!<\/b>`,
          timestamp = `${Utilities.formatDate(date,Session.getTimeZone(),"MM/dd/yyyy  hh:mm:ss a (z)")}`,
          widgets = [],
          widget = {};
    widgets.push(self.createTextWidget_(title));
    widgets.push(self.createTextWidget_(timestamp));
    widget.widgets = widgets;
    return widget;
  }

  /* 
   * This method creates a response card
   * @param {Object} e - event object passed to the form submit function
   * @return {Object} card response
   */
  getResponse_(e) {
    const self = this,
          formResponse = e.response,
          itemResponses = formResponse.getItemResponses(),
          sections = [],
          cards = [],
          card = {},
          response = {};
    card.header = self.getCardHeader_();
    sections.push(self.getCardIntro_());
    if (self.form.collectsEmail()) {
      sections.push({"widgets": [self.createKeyValueWidget_('Submitted By', formResponse.getRespondentEmail(), true, "STAR")]});
    }
    itemResponses.forEach(ir => {
      if(ir.getResponse()){
        sections.push(self.getCardFormWidget_(ir));
      }
    });
    sections.push(self.getCardFooter_(formResponse));
    card.sections = sections;
    cards.push(card);
    response.cards = cards;
    return response;
  }

}