Note

Currently, you cannot sign-up to the community. We're fixing the issue. Sorry for the inconvenience. Please use AIDA (check below) for any queries.

The Exalate team will be on holiday for the coming days - returning Jan 4
Enjoy & stay safe


Out-of-the-box syncing of User mentions cannot be done because the user ID on the remote site does not match the ID used by Salesforce.

To sync User mentions, you need to map the ID from the remote site to the user's email address, which allows Salesforce to retrieve the User ID using that email.


I have two examples for Jira Cloud and Azure DevOps (they are quite similar, so this should work on other connectors too).

First, we will work on the outgoing script to transform the User ID into the linked email.


Jira Cloud Outgoing script
// This function will accept a String and will replace & return the email linked to the User ID
def changeString(String comment){
  // Regex to find all user ID's
  def matcher = comment =~ /\[~accountid:(.*?)]/ 
  // If no match is found (The comment has no user mention in it) then return the comment.
  if (!matcher.find()) return comment
  
  // Iterate over all found matches and replace the ID with the found Email -> if no email is found but there is an ID then the email will be set to "johnDoe@tmp.com" by default
  matcher.each{
    comment = comment.replace(matcher.group(1), nodeHelper.getUser(matcher.group(1))?.email ?: "johnDoe@tmp.com")
  }
  return comment
}

// We collect all comments and change the mentions in the comment body.
replica.comments = issue.comments.collect{
  c ->
  c.body = changeString(c.body)
  c 
}


Azure DevOps Outgoing script
// This function will accept a String and will replace & return the email linked to the User ID
def changeString(String comment){
  // Regex to find all user ID's
  def matcher = comment =~ /<a href="#" data-vss-mention="version:[0-9.]+,([a-f0-9-]+)">[^<]+<\/a>/
  // If no match is found (The comment has no user mention in it) then return the comment.
  if (!matcher.find()) return comment

  // Iterate over all found matches and replace the ID with the found Email -> if no email is found but there is an ID then the email will be set to "johnDoe@tmp.com" by default
  matcher.each{
  def userEmail = nodeHelper.getUser(matcher.group(1), workItem.projectKey)?.email ?: "johnDoe@tmp.com"
  comment = comment.replace(matcher.group(0), "[~accountid:${userEmail}]")
  }
  return comment
}

// We collect all comments and change the mentions in the comment body.
replica.comments = issue.comments.collect{
  c ->
  c.body = changeString(c.body)
  c 
}



As you can see in the scripts above the only changes are the regex to find the userID and the getUser() function

getUser() Explanation
// The nodeHelper has a function called getUser()
// On jira the function expects a String, On ADO it expects a String and the project name

// Usage Jira Cloud
nodeHelper.getUser(String userID)

// Usage Azure DevOps
nodeHelper.getUser(String userID, String projectName)




Now we have the email addres in the comment instead of the ID we can implement the script to add the comment in Salesforce.

This is the full script you need in your Salesfroce incoming script.


Things you need:

import groovy.json.JsonOutput

// function to get the right Segment
def getMessageSegment(String type){
  def mentionMatch = type =~ /\[~accountid:(.*?)]/
  if(mentionMatch.find()){
    // if no id was found add the email address as text not as mention.
    def userId = nodeHelper.getUserByEmail(mentionMatch.group(1))?.key
    if (!userId) return [type:"Text",text:mentionMatch.group(1)]
    return [type:"Mention",id:userId]  
  }
  return [type:"Text",text:type]    
}

// Function that created the Segment body (needed in the API POST)
def createSegmentBody(String comment){
  def regex = /\[~accountid:.*?]/
  // Use findAll to capture all user mentions
  def matches = comment.findAll(regex)
  def splitString = comment.split(regex)

  // Merge the split strings and matches
  def splittedComment = []
  for (int i = 0; i < splitString.size(); i++) {
    splittedComment << splitString[i]
    if (i < matches.size()) {
      splittedComment << matches[i]
    }
  }
  def segmentBody = []
    // iterate over the list and add the segment body
  for (int i = 0; i < splittedComment.size(); i++){
    if (splittedComment[i] == "") continue;
    segmentBody.add(getMessageSegment(splittedComment[i]))
  }
  // Return json object
  return JsonOutput.toJson([subjectId: entity.key, body: [ messageSegments: segmentBody]])
}


// This will add the comments in your salesfroce case and will add it to the stack trace.
def addComments(def comments){
  def SFCommentId = ""
  // collect all comments and post the createSegmentBody...
  comments.collect{ c ->
    if (c.body){
      def res = httpClient.post("/services/data/v54.0/chatter/feed-elements/",createSegmentBody(c.body),null, ["Content-Type":["application/json"]]){
        req,
        res ->
        // When the rest api call is successfully fulfilled set the SFCommentId var.
        if (res.code == 201) {
          SFCommentId = res?.body?.id
        } else {
          debug.error("error while creating comment:" + res.code + " message:" + res.body)
        }
      }
      // Add trace
      if ( SFCommentId ) {
        def trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
            .setType(com.exalate.api.domain.twintrace.TraceType.COMMENT)
            .setToSynchronize(true)
            .setLocalId(SFCommentId as String)
            .setRemoteId(c.remoteId as String)
            .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
        traces.add(trace)
      }
    }
  }
}


// Now we need to call the addComments() function, we need to store the case in the first sync so we have the case ID to add the comments to.
if(entity.entityType == "Case"){
  if(firstSync){
	// Add the required fields you need to create a case in the firstSync.
    // Store to create the Case ID so we can add the comment in the right case.
    store(entity)
  }
  // Add the fields you need below..
  
  // Set comments with or without user mentions
  addComments(replica.addedComments)
}



Salesforce Incoming script
import groovy.json.JsonOutput

if(firstSync){
  entity.entityType = "Case"
}

def getMessageSegment(String type){
  def mentionMatch = type =~ /\[~accountid:(.*?)]/
  if(mentionMatch.find()){
    // if no id was found add the email address as text not as mention.
    def userId = nodeHelper.getUserByEmail(mentionMatch.group(1))?.key
    if (!userId) return [type:"Text",text:mentionMatch.group(1)]
    return [type:"Mention",id:userId]  
  }
  return [type:"Text",text:type]    
}

def createSegmentBody(String comment){
  def regex = /\[~accountid:.*?]/
  // Use findAll to capture all user mentions
  def matches = comment.findAll(regex)
  def splitString = comment.split(regex)

  // Merge the split strings and matches
  def splittedComment = []
  for (int i = 0; i < splitString.size(); i++) {
    splittedComment << splitString[i]
    if (i < matches.size()) {
      splittedComment << matches[i]
    }
  }
  def segmentBody = []
    // iterate over the list and add the segment body
  for (int i = 0; i < splittedComment.size(); i++){
    if (splittedComment[i] == "") continue;
    segmentBody.add(getMessageSegment(splittedComment[i]))
  }
  // Return json object
  return JsonOutput.toJson([subjectId: entity.key, body: [ messageSegments: segmentBody]])
}

def addComments(def comments){
  def SFCommentId = ""
  // collect all comments and post the createSegmentBody...
  comments.collect{ c ->
    if (c.body){
      def res = httpClient.post("/services/data/v54.0/chatter/feed-elements/",createSegmentBody(c.body),null, ["Content-Type":["application/json"]]){
        req,
        res ->
        // When the rest api call is successfully fulfilled set the SFCommentId var.
        if (res.code == 201) {
          SFCommentId = res?.body?.id
        } else {
          debug.error("error while creating comment:" + res.code + " message:" + res.body)
        }
      }
      // Add trace
      if ( SFCommentId ) {
        def trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
            .setType(com.exalate.api.domain.twintrace.TraceType.COMMENT)
            .setToSynchronize(true)
            .setLocalId(SFCommentId as String)
            .setRemoteId(c.remoteId as String)
            .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
        traces.add(trace)
      }
    }
  }
}

if(entity.entityType == "Case"){
  // Add the required fields you need to create a case in the firstSync.
  if(firstSync){
    entity.Subject      = replica.summary
    entity.Origin       = "Web"
    entity.Status       = "New"
    // Store to create the Case ID so we can add the comment in the right case.
    store(entity)
  }
  entity.Subject      = replica.summary
  entity.Description  = replica.description
  entity.attachments  = attachmentHelper.mergeAttachments(entity, replica)
  
  // Set comments with or without user mentions
  addComments(replica.addedComments)
}



This is how to set usermentions in Salesforce.

You can find my source code in this GitHub Repo.