Merge pull request #1460 from tanmaykm/tan/linkedin
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
This commit is contained in:
commit
c19ada3236
1 changed files with 56 additions and 11 deletions
|
@ -16,7 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
apiURL = "https://api.linkedin.com/v1"
|
apiURL = "https://api.linkedin.com/v2"
|
||||||
authURL = "https://www.linkedin.com/oauth/v2/authorization"
|
authURL = "https://www.linkedin.com/oauth/v2/authorization"
|
||||||
tokenURL = "https://www.linkedin.com/oauth/v2/accessToken"
|
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,
|
AuthURL: authURL,
|
||||||
TokenURL: tokenURL,
|
TokenURL: tokenURL,
|
||||||
},
|
},
|
||||||
Scopes: []string{"r_basicprofile", "r_emailaddress"},
|
Scopes: []string{"r_liteprofile", "r_emailaddress"},
|
||||||
RedirectURL: c.RedirectURI,
|
RedirectURL: c.RedirectURI,
|
||||||
},
|
},
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
@ -133,11 +133,19 @@ func (c *linkedInConnector) Refresh(ctx context.Context, s connector.Scopes, ide
|
||||||
|
|
||||||
type profile struct {
|
type profile struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"localizedFirstName"`
|
||||||
LastName string `json:"lastName"`
|
LastName string `json:"localizedLastName"`
|
||||||
Email string `json:"emailAddress"`
|
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
|
// fullname returns a full name of a person, or email if the resulting name is
|
||||||
// empty
|
// empty
|
||||||
func (p profile) fullname() string {
|
func (p profile) fullname() string {
|
||||||
|
@ -149,15 +157,50 @@ func (p profile) fullname() string {
|
||||||
return fname
|
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) {
|
func (c *linkedInConnector) profile(ctx context.Context, client *http.Client) (p profile, err error) {
|
||||||
// https://developer.linkedin.com/docs/fields/basic-profile
|
// https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api
|
||||||
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/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 {
|
if err != nil {
|
||||||
return p, fmt.Errorf("new req: %v", err)
|
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))
|
resp, err := client.Do(req.WithContext(ctx))
|
||||||
if err != nil {
|
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)
|
return p, fmt.Errorf("JSON decode: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Email == "" {
|
email, err := c.primaryEmail(ctx, client)
|
||||||
return p, fmt.Errorf("email is not set")
|
if err != nil {
|
||||||
|
return p, fmt.Errorf("fetching email: %v", err)
|
||||||
}
|
}
|
||||||
|
p.Email = email
|
||||||
|
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue