From 8613c788633c85ad95b5b354c73914f7ec312c4a Mon Sep 17 00:00:00 2001 From: tan Date: Thu, 30 May 2019 12:23:41 +0530 Subject: [PATCH] update LinkedIn connector to use v2 APIs This updates LinkedIn connector to use the more recent v2 APIs. Necessary because v1 APIs are not able to retrieve email ids any more with the default permissions. The API URLs are now different. Fetching the email address is now a separate call, made after fetching the profile details. The `r_basicprofile` permission is not needed any more, and `r_liteprofile` (which seems to be the one assigned by default) is sufficient. The relevant API specifications are at: - https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api - https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/primary-contact-api - https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/migration-faq#how-do-i-retrieve-the-members-email-address --- connector/linkedin/linkedin.go | 67 ++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/connector/linkedin/linkedin.go b/connector/linkedin/linkedin.go index df5725b3..1c8312c1 100644 --- a/connector/linkedin/linkedin.go +++ b/connector/linkedin/linkedin.go @@ -16,7 +16,7 @@ import ( ) const ( - apiURL = "https://api.linkedin.com/v1" + apiURL = "https://api.linkedin.com/v2" authURL = "https://www.linkedin.com/oauth/v2/authorization" tokenURL = "https://www.linkedin.com/oauth/v2/accessToken" ) @@ -38,7 +38,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) AuthURL: authURL, TokenURL: tokenURL, }, - Scopes: []string{"r_basicprofile", "r_emailaddress"}, + Scopes: []string{"r_liteprofile", "r_emailaddress"}, RedirectURL: c.RedirectURI, }, logger: logger, @@ -133,11 +133,19 @@ func (c *linkedInConnector) Refresh(ctx context.Context, s connector.Scopes, ide type profile struct { ID string `json:"id"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` + FirstName string `json:"localizedFirstName"` + LastName string `json:"localizedLastName"` Email string `json:"emailAddress"` } +type emailresp struct { + Elements []struct { + Handle struct { + EmailAddress string `json:"emailAddress"` + } `json:"handle~"` + } `json:"elements"` +} + // fullname returns a full name of a person, or email if the resulting name is // empty func (p profile) fullname() string { @@ -149,15 +157,50 @@ func (p profile) fullname() string { return fname } +func (c *linkedInConnector) primaryEmail(ctx context.Context, client *http.Client) (email string, err error) { + req, err := http.NewRequest("GET", apiURL+"/emailAddress?q=members&projection=(elements*(handle~))", nil) + if err != nil { + return email, fmt.Errorf("new req: %v", err) + } + + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return email, fmt.Errorf("get URL %v", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return email, fmt.Errorf("read body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return email, fmt.Errorf("%s: %s", resp.Status, body) + } + + var parsedResp emailresp + err = json.Unmarshal(body, &parsedResp) + if err == nil { + for _, elem := range parsedResp.Elements { + email = elem.Handle.EmailAddress + } + } + + if email == "" { + err = fmt.Errorf("email is not set") + } + + return email, err +} + func (c *linkedInConnector) profile(ctx context.Context, client *http.Client) (p profile, err error) { - // https://developer.linkedin.com/docs/fields/basic-profile - req, err := http.NewRequest("GET", apiURL+"/people/~:(id,first-name,last-name,email-address)", nil) + // https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api + // https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/primary-contact-api + // https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/migration-faq#how-do-i-retrieve-the-members-email-address + req, err := http.NewRequest("GET", apiURL+"/me", nil) if err != nil { return p, fmt.Errorf("new req: %v", err) } - q := req.URL.Query() - q.Add("format", "json") - req.URL.RawQuery = q.Encode() resp, err := client.Do(req.WithContext(ctx)) if err != nil { @@ -177,9 +220,11 @@ func (c *linkedInConnector) profile(ctx context.Context, client *http.Client) (p return p, fmt.Errorf("JSON decode: %v", err) } - if p.Email == "" { - return p, fmt.Errorf("email is not set") + email, err := c.primaryEmail(ctx, client) + if err != nil { + return p, fmt.Errorf("fetching email: %v", err) } + p.Email = email return p, err }