Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling persistent user-specific values in Gmail Add-ons

I have created simple Gmail addon, now I'm struggling with below strategies.

After installing the addon, when first inbox is opened, we ask basic info input from users, when he opens second mail we won't ask basic info details again.

How I can handle this?

I have tried property service but no luck.

Update

var MAX_THREADS = 5;

/**
 * Returns the array of cards that should be rendered for the current
 * e-mail thread. The name of this function is specified in the
 * manifest 'onTriggerFunction' field, indicating that this function
 * runs every time the add-on is started.
 *
 * @param {Object} e data provided by the Gmail UI.
 * @returns {Card[]}
 */
function buildAddOn(e) {
  // Activate temporary Gmail add-on scopes.
  //Logger.log('E', Session.getActiveUser());
  var accessToken = e.messageMetadata.accessToken;
  GmailApp.setCurrentMessageAccessToken(accessToken);
  var userProperties = PropertiesService.getUserProperties();
  var Token = userProperties.getProperty('Token');
  Logger.log('Token value:',typeof Token);
  if(Token != null ){
    var messageId = e.messageMetadata.messageId;
    var senderData = extractSenderData(messageId);
    var cards = [];

    // Build a card for each recent thread from this email's sender.
    if (senderData.recents.length > 0) {
      senderData.recents.forEach(function(threadData) {
        cards.push(buildRecentThreadCard(senderData.email, threadData));
      });
    } else {
      // Present a blank card if there are no recent threads from
      // this sender.
      cards.push(CardService.newCardBuilder()
        .setHeader(CardService.newCardHeader()
        .setTitle('No recent threads from this sender')).build());
    }
    return cards;
  } 
  else{
    var cards = []
    var login_card = build_login_card()
    cards.push(login_card);
    return cards;
  }
}

/**
 *  This function builds a set of data about this sender's presence in your
 *  inbox.
 *
 *  @param {String} messageId The message ID of the open message.
 *  @return {Object} a collection of sender information to display in cards.
 */
function extractSenderData(messageId) {
  // Use the Gmail service to access information about this message.
  var mail = GmailApp.getMessageById(messageId);
  var threadId = mail.getThread().getId();
  var senderEmail = extractEmailAddress(mail.getFrom());

  var recentThreads = GmailApp.search('from:' + senderEmail);
  var recents = [];

  // Retrieve information about up to 5 recent threads from the same sender.
  recentThreads.slice(0,MAX_THREADS).forEach(function(thread) {
    if (thread.getId() != threadId && ! thread.isInChats()) {
      recents.push({
        'subject': thread.getFirstMessageSubject(),
        'count': thread.getMessageCount(),
        'link': 'https://mail.google.com/mail/u/0/#inbox/' + thread.getId(),
        'lastDate': thread.getLastMessageDate().toDateString()
      });
    }
  });

  var senderData = {
    "email": senderEmail,
    'recents': recents
  };

  return senderData;
}

/**
 *  Given the result of GmailMessage.getFrom(), extract only the email address.
 *  getFrom() can return just the email address or a string in the form
 *  "Name <myemail@domain>".
 *
 *  @param {String} sender The results returned from getFrom().
 *  @return {String} Only the email address.
 */
function extractEmailAddress(sender) {
  var regex = /\<([^\@]+\@[^\>]+)\>/;
  var email = sender;  // Default to using the whole string.
  var match = regex.exec(sender);
  if (match) {
    email = match[1];
  }
  return email;
}

/**
 *  Builds a card to display information about a recent thread from this sender.
 *
 *  @param {String} senderEmail The sender email.
 *  @param {Object} threadData Infomation about the thread to display.
 *  @return {Card} a card that displays thread information.
 */
function buildRecentThreadCard(senderEmail, threadData) {
  var card = CardService.newCardBuilder();
  card.setHeader(CardService.newCardHeader().setTitle(threadData.subject));
  var section = CardService.newCardSection()
    .setHeader("<font color=\"#1257e0\">Recent thread</font>");
  section.addWidget(CardService.newTextParagraph().setText(threadData.subject));
  section.addWidget(CardService.newKeyValue()
    .setTopLabel('Sender')
    .setContent(senderEmail));
  section.addWidget(CardService.newKeyValue()
    .setTopLabel('Number of messages')
    .setContent(threadData.count.toString()));
  section.addWidget(CardService.newKeyValue()
    .setTopLabel('Last updated')
    .setContent(threadData.lastDate.toString()));

  var threadLink = CardService.newOpenLink()
    .setUrl(threadData.link)
    .setOpenAs(CardService.OpenAs.FULL_SIZE);
  var button = CardService.newTextButton()
    .setText('Open Thread')
    .setOpenLink(threadLink);
  section.addWidget(CardService.newButtonSet().addButton(button));

  card.addSection(section);
  return card.build();
}
function build_login_card(){
  var card = CardService.newCardBuilder();
  card.setHeader(CardService.newCardHeader().setTitle("Login Here"));
  var userProperties = PropertiesService.getUserProperties();
  var Token = userProperties.setProperty('Token',"Token");
  return card.build()
}
like image 673
Robert Avatar asked Oct 16 '22 23:10

Robert


1 Answers

According to comments, the primary issue here is that uninstalling the add-on does not remove bits from the UserProperties datastore, and thus if the add-on is re-installed, the add-on configuration steps cannot be performed again.

Generic Add-on Solution

If this were not a Gmail add-on, this could be simply solved with a time-based trigger, since per documentation:

Add-on triggers will stop firing in any of the following situations:
- If the add-on is uninstalled by the user
- If the add-on is disabled in a document (if it is re-enabled, the trigger will become operational again)
- If the developer unpublishes the add-on or submits a broken version to the add-on store

I.e., you would simply write the day's date to a key (e.g. LAST_SEEN) in PropertiesService#UserProperties every day, and then check both TOKEN and LAST_SEEN when deciding which card to display.


Gmail Add-on Solution:

Per Gmail add-on documentation, you cannot create or use Apps Script simple / installable triggers in a Gmail add-on. So, the easiest implementation of the solution is not available. However, we can still use this approach, by moving the region in which that LAST_SEEN key is updated.

Right now (2018 March), the only available trigger for a Gmail add-on is the contextual trigger unconditional:

Currently, the only contextual trigger type available is unconditional, which triggers for all emails regardless of content.

So, if you bind to this contextual trigger, while your add-on is installed it will run a function whenever the user opens an email. The solution is then for your contextually triggered function to include the snippet

PropertiesService.getUserProperties().setProperty("LAST_SEEN", String(new Date().getTime()));

If you have other stuff to do in your contextually triggered function, that code will be uninfluenced by this addition. If you don't have a contextually triggered function, then you'd want to return [] (an empty card stack) in order to avoid showing any UI.

To use this new property, in your buildAddon(e) method you want to query for this value in addition to the TOKEN property you are using:

var userProps = PropertiesService.getUserProperties().getAll();
var Token = userProps["Token"];
var lastSeen = userProps["LAST_SEEN"] || 0; // If found, will be milliseconds since epoch.
var absence = new Date().getTime() - lastSeen; // Time in ms since last use of add-on.
if (Token == null || absence > /* some duration you choose */ ) {
  // New install, or user has started using app again.
  return [build_login_card()];
} else {
  // User is still using add-on, so do normal stuff.
}

  • This is obviously not a perfect solution (i.e. an uninstall contextual trigger would be much better), but can help detect lack-of-use situations
  • There are rate limits on how often you can write to PropertiesService. If you have speedy/"power" users, they might trip quotas.
    • Could combine CacheService and PropertiesService to handle frequent reads in a "short" session (of up to 6hr since last storage into cache).
like image 116
tehhowch Avatar answered Oct 21 '22 01:10

tehhowch