summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-02-26 15:24:33 +0200
committerPaul Buetow <paul@buetow.org>2025-02-26 15:24:33 +0200
commit547afd8c0921a7de16f0a5ce8b5d8d09bb2268c8 (patch)
tree8490aee787d4c6f9b3e025b67d888a5367def91e /internal
parente735b56a704b7328e7e84def46fd6d248c51e38d (diff)
introduce run interval
Diffstat (limited to 'internal')
-rw-r--r--internal/config/args.go33
-rw-r--r--internal/config/config.go (renamed from internal/config/secrets.go)36
-rw-r--r--internal/main.go36
-rw-r--r--internal/platforms/linkedin/linkedin.go4
-rw-r--r--internal/platforms/linkedin/oauth2/oauth2.go18
-rw-r--r--internal/platforms/mastodon/mastodon.go4
-rw-r--r--internal/run.go12
7 files changed, 82 insertions, 61 deletions
diff --git a/internal/config/args.go b/internal/config/args.go
index e2b9dc1..d1a23b9 100644
--- a/internal/config/args.go
+++ b/internal/config/args.go
@@ -9,22 +9,23 @@ import (
)
type Args struct {
- GosDir string
- CacheDir string
- DryRun bool
- Platforms map[string]int // Platform and post size limits
- Target int
- MinQueued int
- MaxDaysQueued int
- PauseDays int
- Lookback time.Duration
- SecretsConfigPath string
- Secrets Secrets
- OAuth2Browser string
- GeminiSummaryFor []string
- GemtexterEnable bool
- GeminiCapsules []string
- ComposeMode bool
+ GosDir string
+ CacheDir string
+ DryRun bool
+ Platforms map[string]int // Platform and post size limits
+ Target int
+ MinQueued int
+ MaxDaysQueued int
+ PauseDays int
+ RunInterval time.Duration
+ Lookback time.Duration
+ ConfigPath string
+ Config Config
+ OAuth2Browser string
+ GeminiSummaryFor []string
+ GemtexterEnable bool
+ GeminiCapsules []string
+ ComposeMode bool
}
func (a *Args) ParsePlatforms(platformStrs string) error {
diff --git a/internal/config/secrets.go b/internal/config/config.go
index b4e629f..f865b67 100644
--- a/internal/config/secrets.go
+++ b/internal/config/config.go
@@ -5,12 +5,14 @@ import (
"fmt"
"io"
"os"
+ "path/filepath"
"codeberg.org/snonux/gos/internal/colour"
)
-// The config file containing all the secrets and credentials.
-type Secrets struct {
+// The config file containing all the secrets and credentials plus maybe more.
+type Config struct {
+ LastRunEpoch int64 `json:"LastRunEpoch,omitempty"`
MastodonURL string
MastodonAccessToken string
LinkedInClientID string
@@ -22,33 +24,41 @@ type Secrets struct {
LinkedInPersonID string `json:"LinkedInPersonID,omitempty"`
}
-func NewSecrets(configPath string, composeEntry bool) (Secrets, error) {
- var sec Secrets
- if composeEntry {
- // In compose mode, no need to read the secrets.
- return sec, nil
+func New(configPath string, composeEntry bool) (Config, error) {
+ var conf Config
+
+ _, err := os.Stat(configPath)
+ if os.IsNotExist(err) {
+ if !composeEntry {
+ return conf, fmt.Errorf("No config file %s", configPath)
+ }
+ // Create empty new config for compose mode.
+ return conf, conf.WriteToDisk(configPath)
}
file, err := os.Open(configPath)
if err != nil {
- return sec, fmt.Errorf("failed to open file: %w", err)
+ return conf, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
bytes, err := io.ReadAll(file)
if err != nil {
- return sec, fmt.Errorf("failed to read file: %w", err)
+ return conf, fmt.Errorf("failed to read file: %w", err)
}
- if err := json.Unmarshal(bytes, &sec); err != nil {
- return sec, fmt.Errorf("failed to unmarshal JSON: %w", err)
+ if err := json.Unmarshal(bytes, &conf); err != nil {
+ return conf, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
- return sec, nil
+ return conf, nil
}
-func (s Secrets) WriteToDisk(configPath string) error {
+func (s Config) WriteToDisk(configPath string) error {
colour.Infoln("Writing", configPath)
+ if err := os.MkdirAll(filepath.Dir(configPath), os.ModePerm); err != nil {
+ return err
+ }
bytes, err := json.MarshalIndent(s, "", " ")
if err != nil {
diff --git a/internal/main.go b/internal/main.go
index 7ae8390..d047bb5 100644
--- a/internal/main.go
+++ b/internal/main.go
@@ -20,13 +20,14 @@ func Main(composeModeDefault bool) {
gosDir := flag.String("gosDir", filepath.Join(os.Getenv("HOME"), ".gosdir"), "Gos' queue and DB directory")
cacheDir := flag.String("cacheDir", filepath.Join(*gosDir, "cache"), "Go's cache dir")
browser := flag.String("browser", "firefox", "OAuth2 browser")
- secretsConfigPath := filepath.Join(os.Getenv("HOME"), ".config/gos/gosec.json")
- secretsConfigPath = *flag.String("secretsConfig", secretsConfigPath, "Gos' secret config")
+ configPath := filepath.Join(os.Getenv("HOME"), ".config/gos/gos.json")
+ configPath = *flag.String("configPath", configPath, "Gos' config file path")
platforms := flag.String("platforms", "Mastodon:500,LinkedIn:1000", "Platforms enabled plus their post size limits")
target := flag.Int("target", 2, "How many posts per week are the target?")
minQueued := flag.Int("minQueued", 4, "Minimum of queued items until printing a warn message!")
maxDaysQueued := flag.Int("maxDaysQueued", 365, "Maximum days worth of queued posts until target++ and pauseDays--")
pauseDays := flag.Int("pauseDays", 3, "How many days until next post can be posted?")
+ runIntervalHours := flag.Int("runInterval", 18, "How many hours to wait for the next run.")
lookback := flag.Int("lookback", 30, "How many days look back in time for posting history")
geminiSummaryFor := flag.String("geminiSummaryFor", "", "Generate a summary in Gemini Gemtext format, format is coma separated string of months, e.g. 202410,202411")
geminiCapsules := flag.String("geminiCapsules", "foo.zone", "Comma sepaeated list Gemini capsules. Used by geminiEnable to detect Gemtext links")
@@ -43,26 +44,27 @@ func Main(composeModeDefault bool) {
os.Exit(0)
}
- secrets, err := config.NewSecrets(secretsConfigPath, *composeMode)
+ conf, err := config.New(configPath, *composeMode)
if err != nil {
log.Fatal(err)
}
args := config.Args{
- DryRun: *dry,
- GosDir: *gosDir,
- Target: *target,
- MinQueued: *minQueued,
- MaxDaysQueued: *maxDaysQueued,
- PauseDays: *pauseDays,
- Lookback: time.Duration(*lookback) * time.Hour * 24,
- SecretsConfigPath: secretsConfigPath,
- CacheDir: *cacheDir,
- Secrets: secrets,
- OAuth2Browser: *browser,
- GemtexterEnable: *gemtexterEnable,
- GeminiCapsules: strings.Split(*geminiCapsules, ","),
- ComposeMode: *composeMode,
+ DryRun: *dry,
+ GosDir: *gosDir,
+ Target: *target,
+ MinQueued: *minQueued,
+ MaxDaysQueued: *maxDaysQueued,
+ PauseDays: *pauseDays,
+ RunInterval: time.Duration(*runIntervalHours) * time.Hour, // TODO: Document
+ Lookback: time.Duration(*lookback) * time.Hour * 24,
+ ConfigPath: configPath,
+ Config: conf,
+ CacheDir: *cacheDir,
+ OAuth2Browser: *browser,
+ GemtexterEnable: *gemtexterEnable,
+ GeminiCapsules: strings.Split(*geminiCapsules, ","),
+ ComposeMode: *composeMode,
}
if *geminiSummaryFor != "" {
args.GeminiSummaryFor = strings.Split(*geminiSummaryFor, ",")
diff --git a/internal/platforms/linkedin/linkedin.go b/internal/platforms/linkedin/linkedin.go
index 2a3c7b2..4c63f9a 100644
--- a/internal/platforms/linkedin/linkedin.go
+++ b/internal/platforms/linkedin/linkedin.go
@@ -26,7 +26,7 @@ func Post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry)
err := post(ctx, args, sizeLimit, en)
if errors.Is(err, errUnauthorized) {
colour.Infoln(err, "=> trying to refresh LinkedIn access token")
- args.Secrets.LinkedInAccessToken = "" // Reset the token
+ args.Config.LinkedInAccessToken = "" // Reset the token
return post(ctx, args, sizeLimit, en)
}
return err
@@ -38,7 +38,7 @@ func post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry)
}
timeout := linkedInTimeout
- if args.Secrets.LinkedInAccessToken == "" {
+ if args.Config.LinkedInAccessToken == "" {
// Refreshing access token requires more time due to human interaction
timeout = 1 * time.Minute
}
diff --git a/internal/platforms/linkedin/oauth2/oauth2.go b/internal/platforms/linkedin/oauth2/oauth2.go
index dfaad19..8fe25ab 100644
--- a/internal/platforms/linkedin/oauth2/oauth2.go
+++ b/internal/platforms/linkedin/oauth2/oauth2.go
@@ -91,15 +91,15 @@ func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
}
func LinkedInCreds(ctx context.Context, args config.Args) (string, string, error) {
- secrets := args.Secrets
- if secrets.LinkedInAccessToken != "" && secrets.LinkedInPersonID != "" {
- return secrets.LinkedInPersonID, secrets.LinkedInAccessToken, nil
+ conf := args.Config
+ if conf.LinkedInAccessToken != "" && conf.LinkedInPersonID != "" {
+ return conf.LinkedInPersonID, conf.LinkedInAccessToken, nil
}
oauthConfig = &oauth2.Config{
- ClientID: secrets.LinkedInClientID,
- ClientSecret: secrets.LinkedInSecret,
- RedirectURL: secrets.LinkedInRedirectURL,
+ ClientID: conf.LinkedInClientID,
+ ClientSecret: conf.LinkedInSecret,
+ RedirectURL: conf.LinkedInRedirectURL,
Scopes: []string{"openid", "profile", "w_member_social"},
Endpoint: linkedin.Endpoint,
}
@@ -133,9 +133,9 @@ func LinkedInCreds(ctx context.Context, args config.Args) (string, string, error
return "", "", errs
}
- secrets.LinkedInAccessToken = oauthAccessToken
- secrets.LinkedInPersonID = oauthPersonID
- return oauthPersonID, oauthAccessToken, secrets.WriteToDisk(args.SecretsConfigPath)
+ conf.LinkedInAccessToken = oauthAccessToken
+ conf.LinkedInPersonID = oauthPersonID
+ return oauthPersonID, oauthAccessToken, conf.WriteToDisk(args.ConfigPath)
}
func openURLInFirefox(browser, url string) error {
diff --git a/internal/platforms/mastodon/mastodon.go b/internal/platforms/mastodon/mastodon.go
index f52f905..ad4733b 100644
--- a/internal/platforms/mastodon/mastodon.go
+++ b/internal/platforms/mastodon/mastodon.go
@@ -39,12 +39,12 @@ func Post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry)
newCtx, cancel := context.WithTimeout(ctx, mastodonTimeout)
defer cancel()
- req, err := http.NewRequestWithContext(newCtx, "POST", args.Secrets.MastodonURL, bytes.NewBuffer(payloadBytes))
+ req, err := http.NewRequestWithContext(newCtx, "POST", args.Config.MastodonURL, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
- req.Header.Set("Authorization", "Bearer "+args.Secrets.MastodonAccessToken)
+ req.Header.Set("Authorization", "Bearer "+args.Config.MastodonAccessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
diff --git a/internal/run.go b/internal/run.go
index ac3f75e..27d6aeb 100644
--- a/internal/run.go
+++ b/internal/run.go
@@ -19,9 +19,10 @@ func run(ctx context.Context, args config.Args) error {
if len(args.GeminiSummaryFor) > 0 {
return summary.Run(ctx, args)
}
+ now := time.Now().Unix()
if args.ComposeMode {
- entryPath := fmt.Sprintf("%s/%d.ask.txt", args.GosDir, time.Now().Unix())
+ entryPath := fmt.Sprintf("%s/%d.ask.txt", args.GosDir, now)
if err := prompt.EditFile(entryPath); err != nil {
return err
}
@@ -34,6 +35,12 @@ func run(ctx context.Context, args config.Args) error {
colour.Infoln(err)
}
+ sinceLastRun := time.Duration(now-args.Config.LastRunEpoch) * time.Second
+ if sinceLastRun < args.RunInterval {
+ colour.Infoln("Run interval", args.RunInterval, "with", sinceLastRun, "not yet reached. Not posting anything!")
+ return nil
+ }
+
for platformStr, sizeLimit := range args.Platforms {
platform, err := platforms.New(platformStr)
if err != nil {
@@ -48,7 +55,8 @@ func run(ctx context.Context, args config.Args) error {
}
}
- return nil
+ args.Config.LastRunEpoch = now
+ return args.Config.WriteToDisk(args.ConfigPath)
}
func runPlatform(ctx context.Context, args config.Args, platform platforms.Platform, sizeLimit int) error {