summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod5
-rw-r--r--go.sum4
-rw-r--r--internal/config/secrets.go3
-rw-r--r--internal/platforms/linkedin/linkedin.go12
-rw-r--r--internal/platforms/linkedin/oauth2.go142
-rw-r--r--internal/run.go13
6 files changed, 178 insertions, 1 deletions
diff --git a/go.mod b/go.mod
index 06a8b5e..1d44043 100644
--- a/go.mod
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
index 32d6375..603da79 100644
--- a/go.sum
+++ b/go.sum
@@ -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.