Note

We've packed up and moved from Confluence to Discourse to bring you a better, more interactive space. Out of courtesy we didn't migrate your user account so - you will have to signup again

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

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Generate the Access Token under Tempo settings - API integration tab in your Jira Cloud settings.




The Scripts

Jira

...

Cloud 


Outgoing Sync Jira Cloud 

...

Code Block
languagegroovy
titleOutgoing Sync
linenumberstrue
replica.workLogstimeSpent          = issue.workLogs
TempoWorkLogSync.send(
	"dJWtvBhJUkoHroDcEd8iYyYfnd0Bm",  // replace the "token" with the previously generated access token
	replica, 
	issue, 
	httpClient, 
	nodeHelper
) timeSpent
replica.originalEstimate   = issue.originalEstimate
replica.remainingEstimate  = issue.remainingEstimate

replica.workLogs = issue.workLogs

//change the tempo worklog integration token
TempoWorkLogSync.send(
    "vykOUXzZvZo5fufy3TX7gAJKrs3ORx-eu")



Incoming Sync Jira Cloud 

...

Code Block
languagegroovy
titleIncoming Sync
linenumberstruecollapsetrue
import com.exalate.api.domain.webhook.WebhookEntityType
import com.exalate.basic.domain.hubobject.v1.BasicHubIssue
import com.exalate.basic.domain.hubobject.v1.BasicHubUser
import com.exalate.basic.domain.hubobject.v1.BasicHubWorkLog
import com.exalate.replication.services.replication.impersonation.AuditLogService
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import play.libs.Json
import scala.concurrent.Await$;
import scala.concurrent.duration.Duration$;
import java.text.SimpleDateFormat
import java.time.Instant

//Your normal Incoming Sync code
//Add these functions at the end

Worklogs.receive(
    "B1KNo5oCn2kkLXHBzPW7GyV1mXmtjc-eu", // replace the "token" with the previously generated access token
    replica,
    issue,
    httpClient,
    traces,
    nodeHelper
)

class Worklogs {

static receive(String token, BasicHubIssue replica, BasicHubIssue issue, httpClient, traces, nodeHelper){
  receive(
     token,
     replica,
     issue,
     httpClient,
     traces,
     nodeHelper,
     { BasicHubWorkLog w ->
         def getUser = { String key ->
            def localAuthor = nodeHelper.getUser(key)
            if (localAuthor == null) {
                 localAuthor = new BasicHubUser()
                 localAuthor.key = "557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec"
            }
            localAuthor
         }
         w.author = w.author ? getUser(w.author.key) : null
         w.updateAuthor = w.updateAuthor ? getUser(w.updateAuthor.key) : null
         w
     }
   )
}

static receive(String token, BasicHubIssue replica, BasicHubIssue issue, httpClient, traces, nodeHelper, Closure<?> onWorklogFn){

    def http = { String method, String path, Map<String, List<String>> queryParams, String body, Map<String, List<String>> headers ->
        def await = { future -> Await$.MODULE$.result(future, Duration$.MODULE$.apply(60, java.util.concurrent.TimeUnit.SECONDS)) }
        def orNull = { scala.Option<?> opt -> opt.isDefined() ? opt.get() : null }
        def pair = { l, r -> scala.Tuple2$.MODULE$.apply(l, r) }
        def none = { scala.Option$.MODULE$.empty() }

        def getGeneralSettings = {
            def classLoader = this.getClassLoader()
            def gsp
            try {
               gsp = InjectorGetter.getInjector().instanceOf(classLoader.loadClass("com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsRepository"))
            } catch(ClassNotFoundException exception) {
               gsp = InjectorGetter.getInjector().instanceOf(classLoader.loadClass("com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence"))
            }
            def gsOpt = await(gsp.get())
            def gs = orNull(gsOpt)
            gs
        }
        final def gs = getGeneralSettings()

        def removeTailingSlash = { String str -> str.trim().replace("/+\$", "") }
        final def tempoRestApiUrl = "https://api.tempo.io/core/3"

         def parseQueryString = { String string ->
             string.split('&').collectEntries{ param ->
                  param.split('=', 2).collect{ URLDecoder.decode(it, 'UTF-8') }
             }
         }

         def parseUri
         parseUri = { String uri ->
            def parsedUri
            try {
               parsedUri = new URI(uri)
               if (parsedUri.scheme == 'mailto') {
                    def schemeSpecificPartList = parsedUri.schemeSpecificPart.split('\\?', 2)
                    def tempMailMap = parseQueryString(schemeSpecificPartList[1])
                    //noinspection GrUnresolvedAccess
                    parsedUri.metaClass.mailMap = [
                        recipient: schemeSpecificPartList[0],
                        cc : tempMailMap.find { //noinspection GrUnresolvedAccess
                              it.key.toLowerCase() == 'cc' }.value,
                        bcc : tempMailMap.find { //noinspection GrUnresolvedAccess
                              it.key.toLowerCase() == 'bcc' }.value,
                        subject : tempMailMap.find { //noinspection GrUnresolvedAccess
                              it.key.toLowerCase() == 'subject' }.value,
                        body : tempMailMap.find { //noinspection GrUnresolvedAccess
                               it.key.toLowerCase() == 'body' }.value
                    ]
               }
               if (parsedUri.fragment?.contains('?')) {
                     //noinspection GrUnresolvedAccess
                     parsedUri.metaClass.rawQuery = parsedUri.rawFragment.split('\\?')[1]
                     //noinspection GrUnresolvedAccess
                     parsedUri.metaClass.query = parsedUri.fragment.split('\\?')[1]
                     //noinspection GrUnresolvedAccess
                     parsedUri.metaClass.rawFragment = parsedUri.rawFragment.split('\\?')[0]
                     //noinspection GrUnresolvedAccess
                     parsedUri.metaClass.fragment = parsedUri.fragment.split('\\?')[0]
               }
               if (parsedUri.rawQuery) {
                    //noinspection GrUnresolvedAccess
                    parsedUri.metaClass.queryMap = parseQueryString(parsedUri.rawQuery)
               } else {
                   //noinspection GrUnresolvedAccess
                   parsedUri.metaClass.queryMap = null
               }

               //noinspection GrUnresolvedAccess
               if (parsedUri.queryMap) {
                    //noinspection GrUnresolvedAccess
                    parsedUri.queryMap.keySet().each { key ->
                      def value = parsedUri.queryMap[key]
                      //noinspection GrUnresolvedAccess
                      if (value.startsWith('http') || value.startsWith('/')) {
                           parsedUri.queryMap[key] = parseUri(value)
                      }
                    }
               }
           } catch (e) {
                throw new com.exalate.api.exception.IssueTrackerException("Parsing of URI failed: $uri\n$e", e)
           }
           parsedUri
      }

      def unsanitizedUrl = tempoRestApiUrl + path
      def parsedUri = parseUri(unsanitizedUrl)

      def embeddedQueryParams = parsedUri.queryMap

      def allQueryParams = embeddedQueryParams instanceof Map ?
      ({
          def m = [:] as Map<String, List<String>>;
          m.putAll(embeddedQueryParams as Map<String, List<String>>)
          m.putAll(queryParams) 
      })()
      : (queryParams ?: [:] as Map<String, List<String>>)

      def urlWithoutQueryParams = { String url ->
           URI uri = new URI(url)
           new URI(uri.getScheme(),
               uri.getUserInfo(), uri.getHost(), uri.getPort(),
               uri.getPath(),
               null,
               uri.getFragment()).toString()
      }
      def sanitizedUrl = urlWithoutQueryParams(unsanitizedUrl)

      def response
      try {
        def request = httpClient
            .ws()
            .url(sanitizedUrl)
            .withMethod(method)

        if (headers != null && !headers.isEmpty()) {
             def scalaHeaders = scala.collection.JavaConverters.asScalaBuffer(
                  headers.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
                      kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
                      result
                  }
             )
             request = request.withHeaders(scalaHeaders.toSeq())
         }

         if (!allQueryParams.isEmpty()) {
            def scalaQueryParams = scala.collection.JavaConverters.asScalaBuffer(queryParams.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
                kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
                result
            })
            request = request.withQueryString(scalaQueryParams.toSeq())
         }

         if (body != null) {
            def writable = play.api.libs.ws.WSBodyWritables$.MODULE$.writeableOf_String()
            request = request.withBody(body, writable)
         }
         response = await(request.execute())


      } catch (Exception e) {
            throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $path, please contact Exalate Support: ".toString() + e.message, e)
      }
      if (response.status() >= 300) {
             throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $path ${body ? "with body `$body`".toString() : ""}(status ${response.status()}), please contact Exalate Support: ".toString() + response.body())
      }
      response.body() as String
   }

   def gsp = InjectorGetter.getInjector().instanceOf(AuditLogService.class)

   def js = new groovy.json.JsonSlurper()
   def jo = new groovy.json.JsonOutput()

   def listAdditionalParams = replica.customKeys."tempoWorklogParams" as Map<String, Map<String, Object>>;

   replica.workLogs.findAll{it.id == null}.each{ BasicHubWorkLog worklog ->
   def transformedWorklog


   try {
        transformedWorklog = onWorklogFn(worklog)
   } catch (com.exalate.api.exception.IssueTrackerException ite) {
        throw ite
   } catch (Exception e) {
        throw new com.exalate.api.exception.IssueTrackerException(e)
   }
   if (transformedWorklog instanceof BasicHubWorkLog) {
         worklog = transformedWorklog as BasicHubWorkLog
   } else if (transformedWorklog == null) {
         return
}

   def auditLogOpt = gsp.createAuditLog(scala.Option$.MODULE$.<String>apply(issue.id as String),
       WebhookEntityType.WORKLOG_CREATED,
       worklog.getAuthor().getKey()
   )

   def attributes = ((listAdditionalParams?.get(worklog.remoteId.toString())?.get("attributes") as Map<String, Object>)?.get("values") as List<Map<String, String>>)?.inject([]){
      List<Map<String, String>>result, Map<String, String> attribute ->
         result.add(
            [
               key: attribute.get("key"),
               value: attribute.get("value")
            ]
         )
         result
       } ?: []
      def properties = [
         issueKey : issue.key,
         timeSpentSeconds : worklog.getTimeSpent(),
         billableSeconds: listAdditionalParams?.get(worklog.remoteId.toString())?.get("billableSeconds") ?: worklog.getTimeSpent(),
         startDate : new java.text.SimpleDateFormat("yyyy-MM-dd").format(worklog.startDate),
         startTime : new java.text.SimpleDateFormat("hh:mm:ss").format(worklog.startDate) ?: listAdditionalParams?.get(worklog.remoteId.toString())?.get("startTime"),//strDateSplitted[1].split("\\.")[0],
         description : worklog.getComment(),
         authorAccountId : worklog.getAuthor().getKey(),
         remainingEstimateSeconds: replica.remainingEstimate ?: listAdditionalParams?.get(worklog.remoteId.toString())?.get("remainingEstimateSeconds"),
         attributes : attributes
       ]
       def jsonTempoPost = jo.toJson(properties)


       def response = js.parseText(http(
           "POST",
           "/worklogs",
           null,
           jsonTempoPost,
           [
               "Authorization":["Bearer ${token}".toString()],
               "Content-Type":["application/json"],
           ]
       ))
       println(response)

       gsp.updateAuditLog(scala.Option$.MODULE$.apply(auditLogOpt), issue.id as String, response["jiraWorklogId"] as String, Json.stringify(Json.toJson(response)))


       String localIdStr = response["tempoWorklogId"]
       String remoteIdStr = worklog.remoteId.toString()

       com.exalate.api.domain.twintrace.INonPersistentTrace trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
            .setLocalId(localIdStr)
            .setRemoteId(remoteIdStr)
            .setType(com.exalate.api.domain.twintrace.TraceType.WORKLOG)
            .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
            .setToSynchronize(true)
       traces.add(trace)
       }
   }
}



.originalEstimate   = replica.originalEstimate
issue.remainingEstimate  = replica.remainingEstimate

if(firstSync){
	//change the project Key
    issue.projectKey  = "MAT"
    issue.typeName    = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
    issue.summary      = replica.summary
    store(issue)
}

issue.summary      = replica.summary
issue.description  = replica.description
issue.comments     = commentHelper.mergeComments(issue, replica)
issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
issue.labels       = replica.labels

//change the tempo integration token
TempoWorkLogSync.receive("4E5Gha3JVKspLkjueXh2CIAq1Xs4G-eu",replica,issue,httpClient,traces,nodeHelper)






Please be sure to change the tokens, to the correct TEMPO tokens.



Video


Disclaimer: Please use the above updated scripts instead of the scripts shown in the video. Thank you.




Widget Connector
urlhttp://youtube.com/watch?v=r1QW0nnihAY

...