github.com/uvalib/orcid-access-ws@v0.0.0-20250612130209-7d062dbabf9d/orcidaccessws/orcid/client.go (about) 1 package orcid 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/parnurzeal/gorequest" 13 "github.com/pkg/errors" 14 "github.com/uvalib/orcid-access-ws/orcidaccessws/api" 15 "github.com/uvalib/orcid-access-ws/orcidaccessws/config" 16 "github.com/uvalib/orcid-access-ws/orcidaccessws/logger" 17 ) 18 19 var emptyUpdateCode = "" 20 var emptyAccessToken = "" 21 var currentAccessToken = "" 22 23 // we want to handle this differently from other requests because it is used as part of healthchecking 24 var authTimeout = 5 * time.Second 25 26 // UpdateOrcidActivity -- update the user activity 27 func UpdateOrcidActivity(orcid string, oauthToken string, activity api.ActivityUpdate) (string, int, error) { 28 29 logActivityUpdateRequest(activity) 30 31 // determine if we are creating a new activity or updating an existing one 32 existingActivity := len(activity.UpdateCode) != 0 33 34 // construct target URL 35 url := fmt.Sprintf("%s/%s/work", config.Configuration.OrcidSecureURL, orcid) 36 if existingActivity == true { 37 url = fmt.Sprintf("%s/%s", url, activity.UpdateCode) 38 } 39 //fmt.Printf( "%s\n", url ) 40 41 // build the request body 42 requestBody, err := makeUpdateActivityBody(activity) 43 44 // check for errors 45 if err != nil { 46 logger.Log(fmt.Sprintf("ERROR: creating service payload %s", err)) 47 return emptyUpdateCode, http.StatusBadRequest, err 48 } 49 50 // construct the auth field 51 auth := fmt.Sprintf("Bearer %s", oauthToken) 52 53 // issue the request 54 start := time.Now() 55 var resp gorequest.Response 56 var body string 57 var errs []error 58 if existingActivity == true { 59 resp, body, errs = gorequest.New(). 60 SetDebug(config.Configuration.Debug). 61 Put(url). 62 Set("Accept", "application/json"). 63 Set("Content-Type", "application/xml"). 64 Set("Authorization", auth). 65 Send(requestBody). 66 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 67 End() 68 } else { 69 resp, body, errs = gorequest.New(). 70 SetDebug(config.Configuration.Debug). 71 Post(url). 72 Set("Accept", "application/json"). 73 Set("Content-Type", "application/xml"). 74 Set("Authorization", auth). 75 Send(requestBody). 76 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 77 End() 78 } 79 duration := time.Since(start) 80 81 // check for errors 82 if errs != nil { 83 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 84 httpStatus := mapErrorResponseToStatus(errs[0]) 85 return emptyUpdateCode, httpStatus, errs[0] 86 } 87 88 defer io.Copy(ioutil.Discard, resp.Body) 89 defer resp.Body.Close() 90 91 logger.Log(fmt.Sprintf("Service (%s) returns http %d in %s", url, resp.StatusCode, duration)) 92 93 // the happy update path; just return the original update code 94 if existingActivity == true && resp.StatusCode == http.StatusOK { 95 return activity.UpdateCode, http.StatusOK, nil 96 } 97 98 // the happy create path, pull the push code from the location header 99 if existingActivity == false && resp.StatusCode == http.StatusCreated { 100 tokens := strings.Split(resp.Header.Get("Location"), "/") 101 if len(tokens) != 0 { 102 return tokens[len(tokens)-1], http.StatusOK, nil 103 } 104 105 // unexpected, return an error 106 return emptyUpdateCode, http.StatusInternalServerError, errors.New("Unexpected/missing location header in response") 107 } 108 109 // 110 // something unexpected happened and we did not get an error report (handled above) 111 // 112 113 // check for a 500 error and return if we find it 114 if resp.StatusCode == http.StatusInternalServerError { 115 return emptyUpdateCode, http.StatusInternalServerError, errors.New("Server reports Internal Server Error") 116 } 117 118 // otherwise, attempt to decode the response 119 aur := activityUpdateResponse{} 120 err = json.Unmarshal([]byte(body), &aur) 121 if err != nil { 122 logger.Log(fmt.Sprintf("ERROR: json unmarshal: %s", err)) 123 httpStatus := mapErrorResponseToStatus(err) 124 return emptyUpdateCode, httpStatus, err 125 } 126 127 // 128 // ORCID reported an error 129 // 130 if len(aur.Error) != 0 { 131 logger.Log(fmt.Sprintf("ERROR: service reports %s (%s)", aur.Error, aur.ErrorDescription)) 132 return emptyUpdateCode, resp.StatusCode, errors.New(aur.ErrorDescription) 133 } 134 135 if aur.ResponseCode != http.StatusOK { 136 logger.Log(fmt.Sprintf("ERROR: service reports: %d (%s)", aur.ResponseCode, aur.DeveloperMessage)) 137 return emptyUpdateCode, aur.ResponseCode, errors.New(aur.DeveloperMessage) 138 } 139 140 // unclear why we are here but an error occurred 141 return emptyUpdateCode, http.StatusInternalServerError, errors.New("Unhandled error case") 142 } 143 144 var employmentXML = `<?xml version="1.0" encoding="UTF-8"?> 145 <employment:employment 146 xmlns:employment="http://www.orcid.org/ns/employment" xmlns:common="http://www.orcid.org/ns/common" 147 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 148 xsi:schemaLocation="http://www.orcid.org/ns/employment ../employment-3.0.xsd "> 149 <common:organization> 150 <common:name>University of Virginia</common:name> 151 <common:address> 152 <common:city>Charlottesville</common:city> 153 <common:region>VA</common:region> 154 <common:country>US</common:country> 155 </common:address> 156 <common:disambiguated-organization> 157 <common:disambiguated-organization-identifier>https://ror.org/0153tk833</common:disambiguated-organization-identifier> 158 <common:disambiguation-source>ROR</common:disambiguation-source> 159 </common:disambiguated-organization> 160 </common:organization> 161 </employment:employment>` 162 163 // SendEmployment updates the user's ORCID with UVA Employent info 164 func SendEmployment(attributes api.OrcidAttributes) { 165 166 // First check for existing UVA Employment 167 if hasEmp, err := hasExistingEmployment(attributes); err != nil || hasEmp { 168 return 169 } 170 171 url := fmt.Sprintf("%s/%s/employment", config.Configuration.OrcidSecureURL, attributes.Orcid) 172 // construct the auth field 173 auth := fmt.Sprintf("Bearer %s", attributes.OauthAccessToken) 174 start := time.Now() 175 resp, _, errs := gorequest.New(). 176 SetDebug(config.Configuration.Debug). 177 Post(url). 178 Set("Accept", "application/xml"). 179 Set("Content-Type", "application/xml"). 180 Set("Authorization", auth). 181 Send(employmentXML). 182 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 183 End() 184 duration := time.Since(start) 185 186 defer io.Copy(ioutil.Discard, resp.Body) 187 defer resp.Body.Close() 188 // check for errors 189 if errs != nil || resp.StatusCode != http.StatusCreated { 190 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s %s in %s \n %s", url, resp.Status, errs, duration, resp.Body)) 191 return 192 } 193 logger.Log(fmt.Sprintf("Employment created for %s", attributes.Orcid)) 194 195 return 196 } 197 198 // hasExistingEmployment checks for existing UVA Employment matching our OrcidClientID 199 func hasExistingEmployment(attributes api.OrcidAttributes) (bool, []error) { 200 hasEmployment := false 201 // Only need to know employment here 202 var employmentStruct struct { 203 AffiliationGroup []struct { 204 Summaries []struct { 205 Employment struct { 206 Source struct { 207 SourceClientID struct { 208 Path string `json:"path"` 209 } `json:"source-client-id"` 210 } `json:"source"` 211 } `json:"employment-summary"` 212 } `json:"summaries"` 213 } `json:"affiliation-group"` 214 } 215 url := fmt.Sprintf("%s/%s/employments", config.Configuration.OrcidSecureURL, attributes.Orcid) 216 // construct the auth field 217 auth := fmt.Sprintf("Bearer %s", attributes.OauthAccessToken) 218 start := time.Now() 219 resp, body, errs := gorequest.New(). 220 SetDebug(config.Configuration.Debug). 221 Get(url). 222 Set("Accept", "application/json"). 223 Set("Content-Type", "application/json"). 224 Set("Authorization", auth). 225 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 226 End() 227 duration := time.Since(start) 228 229 defer io.Copy(ioutil.Discard, resp.Body) 230 defer resp.Body.Close() 231 // check for errors 232 if errs != nil || resp.StatusCode != http.StatusOK { 233 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s %s in %s \n %s", url, resp.Status, errs, duration, resp.Body)) 234 return false, errs 235 } 236 237 if err := json.Unmarshal([]byte(body), &employmentStruct); err != nil { 238 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 239 return false, errs 240 } 241 242 // Check the nested json 243 for _, g := range employmentStruct.AffiliationGroup { 244 for _, s := range g.Summaries { 245 // Check if existing employments match our client ID 246 if s.Employment.Source.SourceClientID.Path == config.Configuration.OrcidClientID { 247 hasEmployment = true 248 } 249 } 250 } 251 logger.Log(fmt.Sprintf("INFO: %s has UVA Employment: %t", attributes.Orcid, hasEmployment)) 252 return hasEmployment, nil 253 } 254 255 var educationXML = `<?xml version="1.0" encoding="UTF-8"?> 256 <education:education 257 xmlns:common="http://www.orcid.org/ns/common" xmlns:education="http://www.orcid.org/ns/education" 258 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 259 xsi:schemaLocation="http://www.orcid.org/ns/education ../education-3.0.xsd "> 260 <common:organization> 261 <common:name>University of Virginia</common:name> 262 <common:address> 263 <common:city>Charlottesville</common:city> 264 <common:region>VA</common:region> 265 <common:country>US</common:country> 266 </common:address> 267 <common:disambiguated-organization> 268 <common:disambiguated-organization-identifier>https://ror.org/0153tk833</common:disambiguated-organization-identifier> 269 <common:disambiguation-source>ROR</common:disambiguation-source> 270 </common:disambiguated-organization> 271 </common:organization> 272 </education:education>` 273 274 // SendEmployment updates the user's ORCID with UVA Employent info 275 func SendEducation(attributes api.OrcidAttributes) { 276 277 // First check for existing UVA Employment 278 if hasEmp, err := hasExistingEducation(attributes); err != nil || hasEmp { 279 return 280 } 281 282 url := fmt.Sprintf("%s/%s/education", config.Configuration.OrcidSecureURL, attributes.Orcid) 283 // construct the auth field 284 auth := fmt.Sprintf("Bearer %s", attributes.OauthAccessToken) 285 start := time.Now() 286 resp, _, errs := gorequest.New(). 287 SetDebug(config.Configuration.Debug). 288 Post(url). 289 Set("Accept", "application/xml"). 290 Set("Content-Type", "application/xml"). 291 Set("Authorization", auth). 292 Send(educationXML). 293 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 294 End() 295 duration := time.Since(start) 296 297 defer io.Copy(ioutil.Discard, resp.Body) 298 defer resp.Body.Close() 299 // check for errors 300 if errs != nil || resp.StatusCode != http.StatusCreated { 301 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s %s in %s \n %s", url, resp.Status, errs, duration, resp.Body)) 302 return 303 } 304 logger.Log(fmt.Sprintf("INFO: UVA Education created for %s", attributes.Orcid)) 305 306 return 307 } 308 309 // hasExistingEmployment checks for existing UVA Employment matching our OrcidClientID 310 func hasExistingEducation(attributes api.OrcidAttributes) (bool, error) { 311 hasUVAEducation := false 312 // Only need to know employment here 313 var educationStruct struct { 314 AffiliationGroup []struct { 315 Summaries []struct { 316 Education struct { 317 Source struct { 318 SourceClientID struct { 319 Path string `json:"path"` 320 } `json:"source-client-id"` 321 } `json:"source"` 322 } `json:"education-summary"` 323 } `json:"summaries"` 324 } `json:"affiliation-group"` 325 } 326 url := fmt.Sprintf("%s/%s/educations", config.Configuration.OrcidSecureURL, attributes.Orcid) 327 // construct the auth field 328 auth := fmt.Sprintf("Bearer %s", attributes.OauthAccessToken) 329 start := time.Now() 330 resp, body, errs := gorequest.New(). 331 SetDebug(config.Configuration.Debug). 332 Get(url). 333 Set("Accept", "application/json"). 334 Set("Content-Type", "application/json"). 335 Set("Authorization", auth). 336 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 337 End() 338 duration := time.Since(start) 339 340 defer io.Copy(ioutil.Discard, resp.Body) 341 defer resp.Body.Close() 342 // check for errors 343 if errs != nil { 344 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 345 return false, errs[0] 346 } 347 348 if err := json.Unmarshal([]byte(body), &educationStruct); err != nil { 349 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 350 return false, errs[0] 351 } 352 353 // Check the nested json 354 for _, g := range educationStruct.AffiliationGroup { 355 for _, s := range g.Summaries { 356 // Check if existing employments match our client ID 357 if s.Education.Source.SourceClientID.Path == config.Configuration.OrcidClientID { 358 hasUVAEducation = true 359 } 360 } 361 } 362 //logger.Log(fmt.Sprintf("hasEducation: %s ", resp.Body)) 363 logger.Log(fmt.Sprintf("INFO: %s has UVA education: %t", attributes.Orcid, hasUVAEducation)) 364 return hasUVAEducation, nil 365 } 366 367 func getOauthToken() (string, int, error) { 368 369 // construct target URL 370 url := fmt.Sprintf("%s/oauth/token", config.Configuration.OrcidOauthURL) 371 //fmt.Printf("%s\n", url) 372 373 // create the request payload 374 pl := oauthRequest{ 375 ClientID: config.Configuration.OrcidClientID, 376 ClientSecret: config.Configuration.OrcidClientSecret, 377 Scope: "/read-public", 378 GrantType: "client_credentials"} 379 380 // issue the request 381 start := time.Now() 382 resp, body, errs := gorequest.New(). 383 SetDebug(config.Configuration.Debug). 384 Post(url). 385 Send(pl). 386 Set("Accept", "application/json"). 387 Set("Content-Type", "application/x-www-form-urlencoded"). 388 Timeout(authTimeout). 389 End() 390 duration := time.Since(start) 391 392 // check for errors 393 if errs != nil { 394 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 395 httpStatus := mapErrorResponseToStatus(errs[0]) 396 return emptyAccessToken, httpStatus, errs[0] 397 } 398 399 defer io.Copy(ioutil.Discard, resp.Body) 400 defer resp.Body.Close() 401 402 logger.Log(fmt.Sprintf("Service (%s) returns http %d in %s", url, resp.StatusCode, duration)) 403 404 // check for a non-success status and return if we find it 405 if resp.StatusCode != http.StatusOK { 406 return emptyAccessToken, resp.StatusCode, fmt.Errorf("Service (%s) returns http %d", url, resp.StatusCode) 407 } 408 409 // otherwise, attempt to decode the response 410 oar := oauthResponse{} 411 err := json.Unmarshal([]byte(body), &oar) 412 if err != nil { 413 logger.Log(fmt.Sprintf("ERROR: json unmarshal: %s", err)) 414 httpStatus := mapErrorResponseToStatus(err) 415 return emptyAccessToken, httpStatus, err 416 } 417 418 return oar.AccessToken, http.StatusOK, nil 419 } 420 421 // RenewAccessToken -- renew the access token 422 func RenewAccessToken(staleToken string) (string, string, int, error) { 423 424 // construct target URL 425 url := fmt.Sprintf("%s/oauth/token", config.Configuration.OrcidOauthURL) 426 //fmt.Printf("%s\n", url) 427 428 // issue the request 429 start := time.Now() 430 resp, _, errs := gorequest.New(). 431 SetDebug(config.Configuration.Debug). 432 Get(url). 433 Set("Accept", "application/json"). 434 Set("refresh_token", staleToken). 435 Set("grant_type", "refresh_token"). 436 Set("client_id", config.Configuration.OrcidClientID). 437 Set("client_secret", config.Configuration.OrcidClientSecret). 438 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 439 End() 440 duration := time.Since(start) 441 442 // check for errors 443 if errs != nil { 444 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 445 httpStatus := mapErrorResponseToStatus(errs[0]) 446 return emptyUpdateCode, emptyUpdateCode, httpStatus, errs[0] 447 } 448 449 defer io.Copy(ioutil.Discard, resp.Body) 450 defer resp.Body.Close() 451 452 logger.Log(fmt.Sprintf("Service (%s) returns http %d in %s", url, resp.StatusCode, duration)) 453 454 //logger.Log(fmt.Sprintf("BODY [%s]", body ) ) 455 456 return emptyUpdateCode, emptyUpdateCode, http.StatusInternalServerError, errors.New("Not implemented") 457 } 458 459 // GetOrcidDetails -- get details for the specified ORCID 460 func GetOrcidDetails(orcid string) (*api.OrcidDetails, int, error) { 461 462 // construct target URL 463 url := fmt.Sprintf("%s/%s/person", config.Configuration.OrcidPublicURL, orcid) 464 //fmt.Printf( "%s\n", url ) 465 466 // get an access token 467 token, err := getAccessToken() 468 if err != nil { 469 return nil, http.StatusInternalServerError, err 470 } 471 472 // issue the request 473 start := time.Now() 474 resp, body, errs := gorequest.New(). 475 SetDebug(config.Configuration.Debug). 476 Get(url). 477 Set("Authorization", token). 478 Set("Accept", "application/json"). 479 Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 480 End() 481 duration := time.Since(start) 482 483 // check for errors 484 if errs != nil { 485 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 486 httpStatus := mapErrorResponseToStatus(errs[0]) 487 return nil, httpStatus, errs[0] 488 } 489 490 defer io.Copy(ioutil.Discard, resp.Body) 491 defer resp.Body.Close() 492 493 logger.Log(fmt.Sprintf("Service (%s) returns http %d in %s", url, resp.StatusCode, duration)) 494 495 // check for an http status 496 if resp.StatusCode != http.StatusOK { 497 return nil, resp.StatusCode, errors.New(orcid) 498 } 499 500 pr := orcidPersonResponse{} 501 err = json.Unmarshal([]byte(body), &pr) 502 if err != nil { 503 logger.Log(fmt.Sprintf("ERROR: json unmarshal: %s", err)) 504 return nil, http.StatusInternalServerError, err 505 } 506 507 return transformDetailsResponse(&pr), http.StatusOK, nil 508 } 509 510 // 511 // SearchOrcid -- search ORCID given the supplied parameters and return the set of ORCID details that match 512 // 513 //func SearchOrcid(search string, startIx string, maxResults string) ([]*api.OrcidDetails, int, int, error) { 514 // 515 // // construct target URL 516 // url := fmt.Sprintf("%s/search?q=%s&start=%s&rows=%s", config.Configuration.OrcidPublicURL, 517 // htmlEncodeString(search), startIx, maxResults) 518 // fmt.Printf("%s\n", url) 519 // 520 // // issue the request 521 // start := time.Now() 522 // resp, body, errs := gorequest.New(). 523 // SetDebug(config.Configuration.Debug). 524 // Get(url). 525 // Set("Accept", "application/json"). 526 // Timeout(time.Duration(config.Configuration.ServiceTimeout) * time.Second). 527 // End() 528 // duration := time.Since(start) 529 // 530 // // check for errors 531 // if errs != nil { 532 // logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 533 // httpStatus := mapErrorResponseToStatus(errs[0]) 534 // return nil, 0, httpStatus, errs[0] 535 // } 536 // 537 // defer io.Copy(ioutil.Discard, resp.Body) 538 // defer resp.Body.Close() 539 // 540 // logger.Log(fmt.Sprintf("INFO: Service (%s) returns http %d in %s", url, resp.StatusCode, duration)) 541 // 542 // // check the common response elements 543 // status, err := checkCommonResponse(body) 544 // if err != nil { 545 // httpStatus := mapErrorResponseToStatus(err) 546 // return nil, 0, httpStatus, err 547 // } 548 // 549 // if status != http.StatusOK { 550 // return nil, 0, status, err 551 // } 552 // 553 // sr := orcidSearchResponse{} 554 // err = json.Unmarshal([]byte(body), &sr) 555 // if err != nil { 556 // logger.Log(fmt.Sprintf("ERROR: json unmarshal: %s", err)) 557 // httpStatus := mapErrorResponseToStatus(err) 558 // return nil, 0, httpStatus, err 559 // } 560 // 561 // //if sr.SearchResults.Results == nil || len( sr.SearchResults.Results ) == 0 { 562 // // return nil, sr.SearchResults.TotalFound, http.StatusNotFound, nil 563 // //} 564 // 565 // return transformSearchResponse(sr.SearchResults), sr.SearchResults.TotalFound, http.StatusOK, nil 566 //} 567 568 // GetPublicEndpointStatus -- get the public endpoint status 569 func GetPublicEndpointStatus() error { 570 571 // construct target URL 572 url := fmt.Sprintf("%s/status", config.Configuration.OrcidPublicURL) 573 //fmt.Printf( "%s\n", url ) 574 575 // get an access token 576 token, err := getAccessToken() 577 if err != nil { 578 return err 579 } 580 581 return issueAuthorizedGet(url, "application/json", token) 582 } 583 584 // GetSecureEndpointStatus -- get the secure endpoint status 585 func GetSecureEndpointStatus() error { 586 587 // construct target URL 588 url := fmt.Sprintf("%s/status", config.Configuration.OrcidSecureURL) 589 //fmt.Printf( "%s\n", url ) 590 591 // get the access token 592 token, err := getAccessToken() 593 if err != nil { 594 return err 595 } 596 return issueAuthorizedGet(url, "application/json", token) 597 } 598 599 // issue a GET to the specified URL and use a bearer token for aurthorization 600 // ignore the response payload and return an error if we get a non-200 response 601 func issueAuthorizedGet(url string, accept string, authToken string) error { 602 603 // construct the auth field 604 auth := fmt.Sprintf("Bearer %s", authToken) 605 606 // issue the request 607 start := time.Now() 608 resp, _, errs := gorequest.New(). 609 SetDebug(config.Configuration.Debug). 610 Get(url). 611 Set("Accept", accept). 612 Set("Authorization", auth). 613 Timeout(authTimeout). 614 End() 615 duration := time.Since(start) 616 617 // check for errors 618 if errs != nil { 619 logger.Log(fmt.Sprintf("ERROR: service (%s) returns %s in %s", url, errs, duration)) 620 return errs[0] 621 } 622 623 defer io.Copy(ioutil.Discard, resp.Body) 624 defer resp.Body.Close() 625 626 logger.Log(fmt.Sprintf("Service (%s) returns http %d in %s", url, resp.StatusCode, duration)) 627 628 // check for a non-success status and return if we find it 629 if resp.StatusCode != http.StatusOK { 630 return fmt.Errorf("Service (%s) returns http %d", url, resp.StatusCode) 631 } 632 633 return nil 634 } 635 636 // singleton type access to the access token 637 func getAccessToken() (string, error) { 638 639 // do we need an access token 640 if len(currentAccessToken) == 0 { 641 token, status, err := getOauthToken() 642 if status != http.StatusOK { 643 return emptyAccessToken, err 644 } 645 currentAccessToken = token 646 } 647 return currentAccessToken, nil 648 } 649 650 // 651 // end of file 652 //