github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/linkedin/client.go (about) 1 package linkedin 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 11 "github.com/caarlos0/log" 12 "github.com/goreleaser/goreleaser/pkg/context" 13 "golang.org/x/oauth2" 14 ) 15 16 var ErrLinkedinForbidden = errors.New("forbidden. please check your permissions") 17 18 type oauthClientConfig struct { 19 Context *context.Context 20 AccessToken string 21 } 22 23 type client struct { 24 client *http.Client 25 baseURL string 26 } 27 28 type postShareText struct { 29 Text string `json:"text"` 30 } 31 32 type postShareRequest struct { 33 Text postShareText `json:"text"` 34 Owner string `json:"owner"` 35 } 36 37 func createLinkedInClient(cfg oauthClientConfig) (client, error) { 38 if cfg.Context == nil { 39 return client{}, fmt.Errorf("context is nil") 40 } 41 42 if cfg.AccessToken == "" { 43 return client{}, fmt.Errorf("empty access token") 44 } 45 46 config := oauth2.Config{} 47 48 c := config.Client(cfg.Context, &oauth2.Token{ 49 AccessToken: cfg.AccessToken, 50 }) 51 52 if c == nil { 53 return client{}, fmt.Errorf("client is nil") 54 } 55 56 return client{ 57 client: c, 58 baseURL: "https://api.linkedin.com", 59 }, nil 60 } 61 62 // getProfileIDLegacy returns the Current Member's ID 63 // it's legacy because it uses deprecated v2/me endpoint, that requires old permissions such as r_liteprofile 64 // POST Share API requires a Profile ID in the 'owner' field 65 // Format must be in: 'urn:li:person:PROFILE_ID' 66 // https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api#retrieve-current-members-profile 67 func (c client) getProfileIDLegacy() (string, error) { 68 resp, err := c.client.Get(c.baseURL + "/v2/me") 69 if err != nil { 70 return "", fmt.Errorf("could not GET /v2/me: %w", err) 71 } 72 73 if resp.StatusCode == http.StatusForbidden { 74 return "", ErrLinkedinForbidden 75 } 76 77 value, err := io.ReadAll(resp.Body) 78 if err != nil { 79 return "", fmt.Errorf("could not read response body: %w", err) 80 } 81 defer resp.Body.Close() 82 83 var result map[string]interface{} 84 err = json.Unmarshal(value, &result) 85 if err != nil { 86 return "", fmt.Errorf("could not unmarshal: %w", err) 87 } 88 89 if v, ok := result["id"]; ok { 90 return v.(string), nil 91 } 92 93 return "", fmt.Errorf("could not find 'id' in result: %w", err) 94 } 95 96 // getProfileSub returns the Current Member's sub (formally ID) - requires 'profile' permission 97 // POST Share API requires a Profile ID in the 'owner' field 98 // Format must be in: 'urn:li:person:PROFILE_SUB' 99 // https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#api-request-to-retreive-member-details 100 func (c client) getProfileSub() (string, error) { 101 resp, err := c.client.Get(c.baseURL + "/v2/userinfo") 102 if err != nil { 103 return "", fmt.Errorf("could not GET /v2/userinfo: %w", err) 104 } 105 106 if resp.StatusCode == http.StatusForbidden { 107 return "", ErrLinkedinForbidden 108 } 109 110 value, err := io.ReadAll(resp.Body) 111 if err != nil { 112 return "", fmt.Errorf("could not read response body: %w", err) 113 } 114 defer resp.Body.Close() 115 116 var result map[string]interface{} 117 if err := json.Unmarshal(value, &result); err != nil { 118 return "", fmt.Errorf("could not unmarshal: %w", err) 119 } 120 121 if v, ok := result["sub"]; ok { 122 return v.(string), nil 123 } 124 125 return "", fmt.Errorf("could not find 'sub' in result: %v", result) 126 } 127 128 // Person or Organization URN - urn:li:person:PROFILE_IDENTIFIER 129 // Owner of the share. Required on create. 130 // tries to get the profile sub (formally id) first, if it fails, it tries to get the profile id (legacy) 131 // https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/share-api?tabs=http#schema 132 func (c client) getProfileURN() (string, error) { 133 // To build the URN, we need to get the profile sub (formally id) 134 profileSub, err := c.getProfileSub() 135 if err != nil { 136 if !errors.Is(err, ErrLinkedinForbidden) { 137 return "", fmt.Errorf("could not get profile sub: %w", err) 138 } 139 140 log.Debug("could not get linkedin profile sub due to permission, getting profile id (legacy)") 141 142 profileSub, err = c.getProfileIDLegacy() 143 if err != nil { 144 return "", fmt.Errorf("could not get profile id: %w", err) 145 } 146 } 147 148 return fmt.Sprintf("urn:li:person:%s", profileSub), nil 149 } 150 151 func (c client) Share(message string) (string, error) { 152 // To get Owner of the share, we need to get the profile URN 153 profileURN, err := c.getProfileURN() 154 if err != nil { 155 return "", fmt.Errorf("could not get profile URN: %w", err) 156 } 157 158 req := postShareRequest{ 159 Text: postShareText{ 160 Text: message, 161 }, 162 Owner: profileURN, 163 } 164 165 reqBytes, err := json.Marshal(req) 166 if err != nil { 167 return "", fmt.Errorf("could not marshal request: %w", err) 168 } 169 170 // Filling only required 'owner' and 'text' field is OK 171 // https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/share-api?tabs=http#sample-request-3 172 resp, err := c.client.Post(c.baseURL+"/v2/shares", "application/json", bytes.NewReader(reqBytes)) 173 if err != nil { 174 return "", fmt.Errorf("could not POST /v2/shares: %w", err) 175 } 176 177 body, err := io.ReadAll(resp.Body) 178 if err != nil { 179 return "", fmt.Errorf("could not read from body: %w", err) 180 } 181 defer resp.Body.Close() 182 183 var result map[string]interface{} 184 err = json.Unmarshal(body, &result) 185 if err != nil { 186 return "", fmt.Errorf("could not unmarshal: %w", err) 187 } 188 189 // Activity URN 190 // URN of the activity associated with this share. Activities act as a wrapper around 191 // shares and articles to represent content in the LinkedIn feed. Read only. 192 if v, ok := result["activity"]; ok { 193 return fmt.Sprintf("https://www.linkedin.com/feed/update/%s", v.(string)), nil 194 } 195 196 return "", fmt.Errorf("could not find 'activity' in result: %w", err) 197 }