summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2024-10-08 10:32:13 +0300
committerPaul Buetow <paul@buetow.org>2024-10-08 10:32:13 +0300
commit2bc4585db96da4000fbf727beadfc25af64bcd4a (patch)
tree2df64b440728e198a6752323f61447aea57f73b2
parentcb38f62ea7fe65732180805a1555bf969e9394ee (diff)
initial restructure for oauth2
-rw-r--r--cmd/gos/main.go19
-rw-r--r--gosdir/db/platforms/linkedin/oauth2/oauth2.go (renamed from internal/platforms/linkedin/oauth2.go)69
-rw-r--r--internal/config/args.go13
-rw-r--r--internal/config/secrets.go24
-rw-r--r--internal/platforms/linkedin/linkedin.go61
5 files changed, 119 insertions, 67 deletions
diff --git a/cmd/gos/main.go b/cmd/gos/main.go
index 752370a..5b98386 100644
--- a/cmd/gos/main.go
+++ b/cmd/gos/main.go
@@ -20,25 +20,26 @@ func main() {
dry := flag.Bool("dry", false, "Dry run")
version := flag.Bool("version", false, "Display version")
gosDir := flag.String("gosDir", "./gosdir", "Gos' directory")
- secretsConfig := filepath.Join(os.Getenv("HOME"), ".config/gos/gosec.json")
- secretsConfig = *flag.String("secretsConfig", secretsConfig, "Gos' secret config")
+ secretsConfigPath := filepath.Join(os.Getenv("HOME"), ".config/gos/gosec.json")
+ secretsConfigPath = *flag.String("secretsConfig", secretsConfigPath, "Gos' secret config")
platforms := flag.String("platforms", "Mastodon,LinkedIn", "Platforms enabled")
target := flag.Int("target", 2, "How many posts per week are the target?")
lookback := flag.Int("lookback", 30, "How many days look back in time for posting history")
flag.Parse()
- secrets, err := config.NewSecrets(secretsConfig)
+ secrets, err := config.NewSecrets(secretsConfigPath)
if err != nil {
log.Fatal(err)
}
args := config.Args{
- DryRun: *dry,
- GosDir: *gosDir,
- Platforms: strings.Split(*platforms, ","),
- Target: *target,
- Lookback: time.Duration(*lookback) * time.Hour * 24,
- Secrets: secrets,
+ DryRun: *dry,
+ GosDir: *gosDir,
+ Platforms: strings.Split(*platforms, ","),
+ Target: *target,
+ Lookback: time.Duration(*lookback) * time.Hour * 24,
+ SecretsConfigPath: secretsConfigPath,
+ Secrets: secrets,
}
if err := args.Validate(); err != nil {
diff --git a/internal/platforms/linkedin/oauth2.go b/gosdir/db/platforms/linkedin/oauth2/oauth2.go
index 0492059..cf3adf5 100644
--- a/internal/platforms/linkedin/oauth2.go
+++ b/gosdir/db/platforms/linkedin/oauth2/oauth2.go
@@ -1,7 +1,6 @@
-package linkedin
+package oauth2
import (
- "bytes"
"context"
"encoding/json"
"fmt"
@@ -14,7 +13,11 @@ import (
"golang.org/x/oauth2/linkedin"
)
-var oauthConfig *oauth2.Config
+var (
+ oauthConfig *oauth2.Config
+ oauthPersonId string
+ oauthAccessToken string
+)
func getLinkedInID(token *oauth2.Token) (string, error) {
const url = "https://api.linkedin.com/v2/userinfo"
@@ -50,50 +53,6 @@ func getLinkedInID(token *oauth2.Token) (string, error) {
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)
@@ -124,11 +83,14 @@ func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Successfully posted a message to LinkedIn!\n"))
}
-// TODO: Check for how logn the access token is valid for
// TODO: Fetch the access token and user ID and store it i na file in .config/gos/...
-// TODO: Refresh access token when it is about to expire or expired
// TODO: Separate posting of the message and fetching of the userID and access token
-func oauth(args config.Args) error {
+func AccessToken(args config.Args) (config.Secrets, error) {
+ if args.Secrets.LinkedInAccessToken != "" && args.Secrets.LinkedInPersonID != "" {
+ // TODO: Check, whether the access token is still valid. If not, get a new one.
+ return args.Secrets, nil
+ }
+
oauthConfig = &oauth2.Config{
ClientID: args.Secrets.LinkedInClientID,
ClientSecret: args.Secrets.LinkedInSecret,
@@ -141,5 +103,10 @@ func oauth(args config.Args) error {
http.HandleFunc("/callback", oauthCallbackHandler)
log.Println("Listening on http://localhost:8080 for LinkedIn oauth2")
- return http.ListenAndServe(":8080", nil)
+ err := http.ListenAndServe(":8080", nil)
+
+ args.Secrets.MastodonAccessToken = oauthAccessToken
+ args.Secrets.LinkedInPersonID = oauthPersonId
+
+ return args.Secrets, err
}
diff --git a/internal/config/args.go b/internal/config/args.go
index b380dcd..601a2d1 100644
--- a/internal/config/args.go
+++ b/internal/config/args.go
@@ -10,12 +10,13 @@ import (
var validPlatforms = []string{"mastodon", "linkedin"}
type Args struct {
- GosDir string
- DryRun bool
- Platforms []string
- Target int
- Lookback time.Duration
- Secrets Secrets
+ GosDir string
+ DryRun bool
+ Platforms []string
+ Target int
+ Lookback time.Duration
+ SecretsConfigPath string
+ Secrets Secrets
}
func (a Args) Validate() error {
diff --git a/internal/config/secrets.go b/internal/config/secrets.go
index e480e67..dc3f8e7 100644
--- a/internal/config/secrets.go
+++ b/internal/config/secrets.go
@@ -7,12 +7,17 @@ import (
"os"
)
+// The config file containing all the secrets and credentials.
type Secrets struct {
MastodonURL string
MastodonAccessToken string
LinkedInClientID string
LinkedInSecret string
LinkedInRedirectURL string
+ // Will be updated by gos automatically, after successful oauth2
+ LinkedInAccessToken string `json:"LinedInAccessToken,omitempty"`
+ // Will be updated by gos automatically, after successful oauth2
+ LinkedInPersonID string `json:"LinedInPersonID,omitempty"`
}
func NewSecrets(configPath string) (Secrets, error) {
@@ -34,3 +39,22 @@ func NewSecrets(configPath string) (Secrets, error) {
return sec, nil
}
+
+func (s Secrets) WriteToDisk(configPath string) error {
+ bytes, err := json.MarshalIndent(s, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal JSON: %w", err)
+ }
+ tmpConfigPath := fmt.Sprintf("%s.tmp", configPath)
+ file, err := os.Create(tmpConfigPath)
+ if err != nil {
+ return fmt.Errorf("failed to create file: %w", err)
+ }
+ defer file.Close()
+
+ if _, err := file.Write(bytes); err != nil {
+ return fmt.Errorf("failed to write to file: %w", err)
+ }
+
+ return os.Rename(tmpConfigPath, configPath)
+}
diff --git a/internal/platforms/linkedin/linkedin.go b/internal/platforms/linkedin/linkedin.go
index 0f6ef2c..6205234 100644
--- a/internal/platforms/linkedin/linkedin.go
+++ b/internal/platforms/linkedin/linkedin.go
@@ -1,12 +1,71 @@
package linkedin
import (
+ "bytes"
"context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "codeberg.org/snonux/gos/gosdir/db/platforms/linkedin/oauth2"
"codeberg.org/snonux/gos/internal/config"
"codeberg.org/snonux/gos/internal/entry"
)
+// TODO: Also implemebt a Text Platform output, which then laster can be
+// processed by Gemtexter as a page
func Post(ctx context.Context, args config.Args, ent entry.Entry) error {
- return oauth(args)
+ secrets, err := oauth2.AccessToken(args)
+ if err != err {
+ return err
+ }
+ // TODO: Don't log this anymore
+ log.Println("DEBUG", "Got access token", secrets)
+ return nil
+}
+
+func postMessage(secrets config.Secrets, message string) error {
+ const url = "https://api.linkedin.com/v2/posts"
+
+ post := map[string]interface{}{
+ "author": fmt.Sprintf("urn:li:person:%s", secrets.LinkedInPesonID),
+ "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 "+secrets.LinkedInAccessToken)
+ 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
}