summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-16 23:50:00 +0300
committerPaul Buetow <paul@buetow.org>2025-09-16 23:50:00 +0300
commit650bfe958f84cbb3045c33044b46562c51720e0a (patch)
tree469721d25f6d362c15f3aa153e0da7e966417272
parent2c076a24fd94a64b4da8b3e7682a27c99d24cc0d (diff)
make linkedin version API configurable
-rw-r--r--README.md12
-rw-r--r--examples/gos.json1
-rw-r--r--internal/config/config.go4
-rw-r--r--internal/platforms/linkedin/linkedin.go41
4 files changed, 43 insertions, 15 deletions
diff --git a/README.md b/README.md
index ef9051a..82dbe16 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,8 @@ Example Configuration File (`~/.config/gos/gos.json`):
"MastodonAccessToken": "your-mastodon-access-token",
"LinkedInClientID": "your-linkedin-client-id",
"LinkedInSecret": "your-linkedin-client-secret",
- "LinkedInRedirectURL": "http://localhost:8080/callback"
+ "LinkedInRedirectURL": "http://localhost:8080/callback",
+ "LinkedInVersion": ""
}
```
@@ -86,11 +87,20 @@ Example with pause period configured:
* `LinkedInClientID`: The client ID for your LinkedIn app, which is needed for OAuth2 authentication.
* `LinkedInSecret`: The client secret for your LinkedIn app.
* `LinkedInRedirectURL`: The redirect URL configured for handling OAuth2 responses.
+* `LinkedInVersion`: (Optional) LinkedIn API version header value (e.g., `202502`). Set this if you receive 426 Upgrade Required with `NONEXISTENT_VERSION`. Leave empty to omit the header and use LinkedIn's default.
* `LinkedInAccessToken`: Gos will automatically update this after successful OAuth2 authentication with LinkedIn.
* `LinkedInPersonID`: Gos will automatically update this after successful OAuth2 authentication with LinkedIn.
* `PauseStart`: (Optional) Start date for pausing all posts in YYYY-MM-DD format.
* `PauseEnd`: (Optional) End date for pausing all posts in YYYY-MM-DD format.
+### LinkedIn API versioning
+
+LinkedIn requires an active API version via the `LinkedIn-Version` header for some endpoints. If you see an error like:
+
+`Status: 426 Upgrade Required {"code":"NONEXISTENT_VERSION","message":"Requested version <date> is not active"}`
+
+set an active version string (e.g., `202502`) in the `LinkedInVersion` field of your config. You can also clear the field to omit the header and fall back to LinkedIn's default version.
+
### Automatically managed fields
Once you finish the OAuth2 setup (after the initial run of `gos`), some fields—like `LinkedInAccessToken` and `LinkedInPersonID` will get filled in automatically. To check if everything's working without actually posting anything, you can run the app in dry run mode with the `--dry` option. After OAuth2 is successful, the file will be updated with `LinkedInClientID` and `LinkedInAccessToken`. If the access token expires, it will go through the OAuth2 process again.
diff --git a/examples/gos.json b/examples/gos.json
index 2b033c6..d8e9082 100644
--- a/examples/gos.json
+++ b/examples/gos.json
@@ -4,6 +4,7 @@
"LinkedInClientID": "CENSORED",
"LinkedInSecret": "CENSORED",
"LinkedInRedirectURL": "http://localhost:8080/callback",
+ "LinkedInVersion": "",
"LinkedInAccessToken": "CENSORED",
"LinkedInPersonID": "CENSORED"
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 1504b95..8b101be 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -19,6 +19,10 @@ type Config struct {
LinkedInClientID string
LinkedInSecret string
LinkedInRedirectURL string
+ // Optional LinkedIn API version header value (e.g., "202502").
+ // When set, requests include `LinkedIn-Version: <value>`.
+ // Leave empty to omit the header and use LinkedIn's default.
+ LinkedInVersion string `json:"LinkedInVersion,omitempty"`
// Will be updated by gos automatically, after successful oauth2
LinkedInAccessToken string `json:"LinkedInAccessToken,omitempty"`
// Will be updated by gos automatically, after successful oauth2
diff --git a/internal/platforms/linkedin/linkedin.go b/internal/platforms/linkedin/linkedin.go
index 4c63f9a..11291fb 100644
--- a/internal/platforms/linkedin/linkedin.go
+++ b/internal/platforms/linkedin/linkedin.go
@@ -22,6 +22,16 @@ var errUnauthorized = errors.New("unauthorized access, refresh or create token?"
const linkedInTimeout = 10 * time.Second
+// addCommonHeaders applies required headers and optional LinkedIn versioning.
+func addCommonHeaders(req *http.Request, accessToken, liVersion string) {
+ req.Header.Set("Authorization", "Bearer "+accessToken)
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-RestLi-Protocol-Version", "2.0.0")
+ if liVersion != "" {
+ req.Header.Set("LinkedIn-Version", liVersion)
+ }
+}
+
func Post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry) error {
err := post(ctx, args, sizeLimit, en)
if errors.Is(err, errUnauthorized) {
@@ -68,11 +78,11 @@ func post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry)
newCtx, cancel = context.WithTimeout(ctx, linkedInTimeout)
defer cancel()
- return postMessageToLinkedInAPI(newCtx, personID, accessToken, content, prev)
+ return postMessageToLinkedInAPI(newCtx, personID, accessToken, content, prev, args.Config.LinkedInVersion)
}
// https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api
-func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, content string, prev preview) error {
+func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, content string, prev preview, liVersion string) error {
const linkedInPostsURL = "https://api.linkedin.com/rest/posts"
personURN := fmt.Sprintf("urn:li:person:%s", personID)
@@ -91,7 +101,7 @@ func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, conten
article := map[string]interface{}{}
if thumbnailPath, ok := prev.Thumbnail(); ok {
- thumbnailURN, err := postImageToLinkedInAPI(ctx, personURN, accessToken, thumbnailPath)
+ thumbnailURN, err := postImageToLinkedInAPI(ctx, personURN, accessToken, thumbnailPath, liVersion)
if err != nil {
return err
}
@@ -112,10 +122,11 @@ func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, conten
return fmt.Errorf("Error creating request: %w", err)
}
- req.Header.Add("Authorization", "Bearer "+accessToken)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Add("X-RestLi-Protocol-Version", "2.0.0")
- req.Header.Add("LinkedIn-Version", "202409")
+ // Use configured LinkedIn version if available
+ addCommonHeaders(req, accessToken, liVersion)
+ if liVersion != "" {
+ colour.Infoln("Using LinkedIn-Version", liVersion)
+ }
client := &http.Client{}
resp, err := client.Do(req)
@@ -133,21 +144,25 @@ func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, conten
resp.Status, string(body))
if resp.StatusCode == http.StatusUnauthorized {
err = errors.Join(err, errUnauthorized)
+ } else if resp.StatusCode == http.StatusUpgradeRequired {
+ // 426 often indicates a non-active LinkedIn-Version header.
+ // Provide a clear hint to configure a valid version.
+ err = fmt.Errorf("%w; LinkedIn API version likely inactive. Set an active 'LinkedInVersion' in config (e.g. 202502) or remove to use default. Response: %s", err, string(body))
}
}
return err
}
// https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/images-api
-func postImageToLinkedInAPI(ctx context.Context, personURN, accessToken, imagePath string) (string, error) {
- uploadURL, imageURN, err := initializeImageUpload(ctx, personURN, accessToken)
+func postImageToLinkedInAPI(ctx context.Context, personURN, accessToken, imagePath string, liVersion string) (string, error) {
+ uploadURL, imageURN, err := initializeImageUpload(ctx, personURN, accessToken, liVersion)
if err != nil {
return imageURN, err
}
return imageURN, performImageUpload(ctx, imagePath, uploadURL, accessToken)
}
-func initializeImageUpload(ctx context.Context, personURN, accessToken string) (string, string, error) {
+func initializeImageUpload(ctx context.Context, personURN, accessToken string, liVersion string) (string, string, error) {
const linkedInAPIURL = "https://api.linkedin.com/rest/images?action=initializeUpload"
type InitializeUploadRequest struct {
@@ -156,7 +171,6 @@ func initializeImageUpload(ctx context.Context, personURN, accessToken string) (
requestBody, err := json.Marshal(map[string]interface{}{
"initializeUploadRequest": InitializeUploadRequest{Owner: personURN},
})
-
if err != nil {
return "", "", fmt.Errorf("error creating request body: %w", err)
}
@@ -167,9 +181,8 @@ func initializeImageUpload(ctx context.Context, personURN, accessToken string) (
return "", "", fmt.Errorf("error creating request: %w", err)
}
- req.Header.Set("Authorization", "Bearer "+accessToken)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Add("LinkedIn-Version", "202409")
+ // Version header is optional and configurable
+ addCommonHeaders(req, accessToken, liVersion)
client := &http.Client{}
resp, err := client.Do(req)