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 }