diff options
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | internal/config/secrets.go | 3 | ||||
| -rw-r--r-- | internal/platforms/linkedin/linkedin.go | 12 | ||||
| -rw-r--r-- | internal/platforms/linkedin/oauth2.go | 142 | ||||
| -rw-r--r-- | internal/run.go | 13 |
6 files changed, 178 insertions, 1 deletions
@@ -2,4 +2,7 @@ module codeberg.org/snonux/gos go 1.22.2 -require golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 +require ( + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 + golang.org/x/oauth2 v0.23.0 +) @@ -1,2 +1,6 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= diff --git a/internal/config/secrets.go b/internal/config/secrets.go index bd35fb7..e480e67 100644 --- a/internal/config/secrets.go +++ b/internal/config/secrets.go @@ -10,6 +10,9 @@ import ( type Secrets struct { MastodonURL string MastodonAccessToken string + LinkedInClientID string + LinkedInSecret string + LinkedInRedirectURL string } func NewSecrets(configPath string) (Secrets, error) { diff --git a/internal/platforms/linkedin/linkedin.go b/internal/platforms/linkedin/linkedin.go new file mode 100644 index 0000000..0f6ef2c --- /dev/null +++ b/internal/platforms/linkedin/linkedin.go @@ -0,0 +1,12 @@ +package linkedin + +import ( + "context" + + "codeberg.org/snonux/gos/internal/config" + "codeberg.org/snonux/gos/internal/entry" +) + +func Post(ctx context.Context, args config.Args, ent entry.Entry) error { + return oauth(args) +} diff --git a/internal/platforms/linkedin/oauth2.go b/internal/platforms/linkedin/oauth2.go new file mode 100644 index 0000000..abd6164 --- /dev/null +++ b/internal/platforms/linkedin/oauth2.go @@ -0,0 +1,142 @@ +package linkedin + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "codeberg.org/snonux/gos/internal/config" + "golang.org/x/oauth2" + "golang.org/x/oauth2/linkedin" +) + +var oauthConfig *oauth2.Config + +func getLinkedInID(token *oauth2.Token) (string, error) { + const url = "https://api.linkedin.com/v2/userinfo" + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("Error creating request:%w", err) + } + + req.Header.Set("Authorization", "Bearer "+token.AccessToken) + req.Header.Set("X-RestLi-Protocol-Version", "2.0.0") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("Error making the request:%w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Failed to retrieve user profile. Status: %s\n%s\n", resp.Status, string(body)) + } + + type User struct { + Sub string `json:"sub"` + } + var user User + if err := json.Unmarshal(body, &user); err != nil { + return "", fmt.Errorf("Error unmarshalling JSON: %w", err) + } + + return user.Sub, nil +} + +func postMessage(token *oauth2.Token, linkedInID, message string) error { + const url = "https://api.linkedin.com/v2/posts" + + post := map[string]interface{}{ + "author": fmt.Sprintf("urn:li:person:%s", linkedInID), + "commentary": message, + "visibility": "PUBLIC", + "distribution": map[string]interface{}{ + "feedDistribution": "MAIN_FEED", + "targetEntities": []string{}, + "thirdPartyDistributionChannels": []string{}, + }, + "lifecycleState": "PUBLISHED", + "isReshareDisabledByAuthor": false, + } + + payload, err := json.Marshal(post) + if err != nil { + return fmt.Errorf("Error encoding JSON:%w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("Error creating request: %w", err) + } + + req.Header.Add("Authorization", "Bearer "+token.AccessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Add("X-RestLi-Protocol-Version", "2.0.0") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Failed to post to LinkedIn. Status: %s\n%s\n\n", resp.Status, body) + } + return nil +} + +func oauthIndexHandler(w http.ResponseWriter, r *http.Request) { + url := oauthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + + token, err := oauthConfig.Exchange(context.Background(), code) + if err != nil { + http.Error(w, "Failed to exchange token", http.StatusInternalServerError) + return + } + + linkedInID, err := getLinkedInID(token) + if err != nil { + fmt.Println(err) + _, _ = w.Write([]byte(err.Error())) + return + } + _, _ = w.Write([]byte("Successfully fetched the LinkedInID\n")) + + if err := postMessage(token, linkedInID, "test"); err != nil { + fmt.Println(err) + _, _ = w.Write([]byte(err.Error())) + return + } + _, _ = w.Write([]byte("Successfully posted a message to LinkedIn!\n")) +} + +func oauth(args config.Args) error { + // Configure the OAuth2 client + oauthConfig = &oauth2.Config{ + ClientID: args.Secrets.LinkedInClientID, + ClientSecret: args.Secrets.LinkedInSecret, + RedirectURL: args.Secrets.LinkedInRedirectURL, + Scopes: []string{"profile", "openid", "w_member_social"}, + Endpoint: linkedin.Endpoint, + } + + http.HandleFunc("/", oauthIndexHandler) + http.HandleFunc("/callback", oauthCallbackHandler) + + log.Println("Listening on http://localhost:8080 for LinkedIn oauth2") + return http.ListenAndServe(":8080", nil) +} diff --git a/internal/run.go b/internal/run.go index 3beea29..b628d9c 100644 --- a/internal/run.go +++ b/internal/run.go @@ -7,6 +7,7 @@ import ( "strings" "codeberg.org/snonux/gos/internal/config" + "codeberg.org/snonux/gos/internal/platforms/linkedin" "codeberg.org/snonux/gos/internal/platforms/mastodon" "codeberg.org/snonux/gos/internal/queue" "codeberg.org/snonux/gos/internal/schedule" @@ -44,6 +45,18 @@ func Run(ctx context.Context, args config.Args) error { return err } log.Println("Posted", ent, "to", platform) + case "linkedin": + if args.DryRun { + log.Println("Not posting", ent, "to", platform, "as dry-run enabled") + continue + } + if err := linkedin.Post(ctx, args, ent); err != nil { + return err + } + if err := ent.MarkPosted(); err != nil { + return err + } + log.Println("Posted", ent, "to", platform) default: // TODO: Once we have LinkedIn implemented, make the above code // more generic so that it can be used with LinkedIn as well. |
