diff options
| author | Paul Buetow <paul@buetow.org> | 2024-10-08 10:32:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2024-10-08 10:32:13 +0300 |
| commit | 2bc4585db96da4000fbf727beadfc25af64bcd4a (patch) | |
| tree | 2df64b440728e198a6752323f61447aea57f73b2 | |
| parent | cb38f62ea7fe65732180805a1555bf969e9394ee (diff) | |
initial restructure for oauth2
| -rw-r--r-- | cmd/gos/main.go | 19 | ||||
| -rw-r--r-- | gosdir/db/platforms/linkedin/oauth2/oauth2.go (renamed from internal/platforms/linkedin/oauth2.go) | 69 | ||||
| -rw-r--r-- | internal/config/args.go | 13 | ||||
| -rw-r--r-- | internal/config/secrets.go | 24 | ||||
| -rw-r--r-- | internal/platforms/linkedin/linkedin.go | 61 |
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 } |
