summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/colour/colour.go8
-rw-r--r--internal/config/config.go17
-rw-r--r--internal/config/config_test.go10
-rw-r--r--internal/entry/entry.go23
-rw-r--r--internal/main.go36
-rw-r--r--internal/oi/oi.go22
-rw-r--r--internal/platforms/linkedin/linkedin.go40
-rw-r--r--internal/platforms/linkedin/oauth2/oauth2.go12
-rw-r--r--internal/platforms/linkedin/preview.go21
-rw-r--r--internal/platforms/mastodon/mastodon.go7
-rw-r--r--internal/run.go102
-rw-r--r--internal/version.go2
12 files changed, 220 insertions, 80 deletions
diff --git a/internal/colour/colour.go b/internal/colour/colour.go
index 3bdfffe..374a5b4 100644
--- a/internal/colour/colour.go
+++ b/internal/colour/colour.go
@@ -17,9 +17,15 @@ var (
warnCol = color.New(color.FgHiWhite, color.BgRed)
Warnln = warnCol.PrintlnFunc()
+ errorCol = color.New(color.FgRed)
+ Errorln = errorCol.PrintlnFunc()
+
successCol = color.New(color.FgWhite, color.BgGreen)
Successfln = func(format string, args ...any) {
- successCol.Printf(format, args...)
+ if _, err := successCol.Printf(format, args...); err != nil {
+ // Log the error but don't fail the operation since we've already printed the data
+ Errorln("Error printing success message:", err)
+ }
fmt.Print("\n")
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 8b101be..73dae9f 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -11,7 +11,8 @@ import (
"codeberg.org/snonux/gos/internal/colour"
)
-// The config file containing all the secrets and credentials plus maybe more.
+// Config holds the application configuration loaded from the JSON config file.
+// It contains secrets, credentials, and various settings for the application.
type Config struct {
LastRunEpoch int64 `json:"LastRunEpoch,omitempty"`
MastodonURL string
@@ -48,7 +49,12 @@ func New(configPath string, composeEntry bool) (Config, error) {
if err != nil {
return conf, fmt.Errorf("failed to open file: %w", err)
}
- defer file.Close()
+ defer func() {
+ if err := file.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already written the data
+ colour.Errorln("Error closing file:", err)
+ }
+ }()
bytes, err := io.ReadAll(file)
if err != nil {
@@ -77,7 +83,12 @@ func (s Config) WriteToDisk(configPath string) error {
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
- defer file.Close()
+ defer func() {
+ if err := file.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already written the data
+ colour.Errorln("Error closing file:", err)
+ }
+ }()
if _, err := file.Write(bytes); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index adecc18..65e7767 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,6 +1,7 @@
package config
import (
+ "fmt"
"testing"
"time"
)
@@ -159,9 +160,10 @@ func isPausedAtTime(c Config, testTime time.Time) (bool, error) {
func TestIsPausedCurrentTime(t *testing.T) {
// Test with actual current time using the real IsPaused method
+ currentYear := time.Now().Year()
config := Config{
- PauseStart: "2025-01-01",
- PauseEnd: "2025-12-31",
+ PauseStart: fmt.Sprintf("%d-01-01", currentYear),
+ PauseEnd: fmt.Sprintf("%d-12-31", currentYear),
}
paused, err := config.IsPaused()
@@ -169,9 +171,9 @@ func TestIsPausedCurrentTime(t *testing.T) {
t.Errorf("Unexpected error: %v", err)
}
- // Since we're in 2025, this should be paused
+ // Since we're in the current year, this should be paused
if !paused {
- t.Errorf("Expected to be paused in 2025, but got false")
+ t.Errorf("Expected to be paused in %d, but got false", currentYear)
}
// Test with dates in the past
diff --git a/internal/entry/entry.go b/internal/entry/entry.go
index 4454b01..2f6bd16 100644
--- a/internal/entry/entry.go
+++ b/internal/entry/entry.go
@@ -1,3 +1,5 @@
+// Package entry handles the representation and manipulation of social media post entries.
+// It defines the Entry struct and provides methods for parsing, modifying, and posting entries.
package entry
import (
@@ -15,20 +17,27 @@ import (
"codeberg.org/snonux/gos/internal/timestamp"
)
+// State represents the lifecycle state of an entry.
type State int
const (
+ // Unknown represents the unknown state of an entry.
Unknown State = iota
- Inboxed
- Queued
- Posted
+ // Inboxed represents the inboxed state of an entry.
+ Inboxed State = iota
+ // Queued represents the queued state of an entry.
+ Queued State = iota
+ // Posted represents the posted state of an entry.
+ Posted State = iota
)
-var (
- validTags = []string{"ask", "prio", "now"}
- ErrSizeLimitExceeded = errors.New("message size limit exceeded")
-)
+// validTags contains the list of valid tags that can be applied to entries.
+var validTags = []string{"ask", "prio", "now"}
+
+// ErrSizeLimitExceeded is returned when an entry exceeds the size limit for a platform.
+var ErrSizeLimitExceeded = errors.New("message size limit exceeded")
+// String returns the string representation of the State.
func (s State) String() string {
switch s {
case Unknown:
diff --git a/internal/main.go b/internal/main.go
index 198a584..ea5d527 100644
--- a/internal/main.go
+++ b/internal/main.go
@@ -13,15 +13,18 @@ import (
"codeberg.org/snonux/gos/internal/schedule"
)
+// Main is the entry point for the Gos application.
+// It parses command-line flags, loads configuration, and starts the main run loop.
+// composeModeDefault determines whether the compose mode is enabled by default.
func Main(composeModeDefault bool) {
+ // Parse flags
dry := flag.Bool("dry", false, "Dry run")
version := flag.Bool("version", false, "Display version")
composeMode := flag.Bool("compose", composeModeDefault, "Compose a new entry")
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")
+ // cacheDir := flag.String("cacheDir", filepath.Join(*gosDir, "cache"), "Go's cache dir")
browser := flag.String("browser", "firefox", "OAuth2 browser")
- configPath := filepath.Join(os.Getenv("HOME"), ".config/gos/gos.json")
- configPath = *flag.String("configPath", configPath, "Gos' config file path")
+ configPath := flag.String("configPath", filepath.Join(os.Getenv("HOME"), ".config/gos/gos.json"), "Gos' config file path")
platforms := flag.String("platforms", "Mastodon:500,LinkedIn:1000,Noop:2000", "Platforms enabled plus their post size limits")
target := flag.Int("target", 2, "How many posts per week are the target?")
minQueued := flag.Int("minQueued", 42, "Minimum of queued items until printing a warn message!")
@@ -30,16 +33,19 @@ func Main(composeModeDefault bool) {
runInterval := flag.Int("runInterval", 6, "How many hours to wait for the next run.")
lookback := flag.Int("lookback", 42, "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")
+ geminiCapsules := flag.String("geminiCapsules", "foo.zone", "Comma separated list Gemini capsules. Used by geminiEnable to detect Gemtext links")
gemtexterEnable := flag.Bool("gemtexterEnable", false, "Add special Gemtexter (the static site generator) tags to the Gemini Gemtext summary")
statsOnly := flag.Bool("stats", false, "Print statistics for all social networks and exit")
+
flag.Parse()
- conf, err := config.New(configPath, *composeMode)
- if err != nil {
- log.Fatal(err)
+ // Handle version flag
+ if *version {
+ printVersion()
+ return
}
+ // Create args from parsed flags
args := config.Args{
DryRun: *dry,
GosDir: *gosDir,
@@ -49,9 +55,7 @@ func Main(composeModeDefault bool) {
PauseDays: *pauseDays,
RunInterval: time.Duration(*runInterval) * time.Hour, // TODO: Document
Lookback: time.Duration(*lookback) * time.Hour * 24,
- ConfigPath: configPath,
- Config: conf,
- CacheDir: *cacheDir,
+ ConfigPath: *configPath,
OAuth2Browser: *browser,
GemtexterEnable: *gemtexterEnable,
GeminiCapsules: strings.Split(*geminiCapsules, ","),
@@ -62,15 +66,19 @@ func Main(composeModeDefault bool) {
args.GeminiSummaryFor = strings.Split(*geminiSummaryFor, ",")
}
- if err := args.ParsePlatforms(*platforms); err != nil {
+ // Load configuration
+ conf, err := config.New(args.ConfigPath, args.ComposeMode)
+ if err != nil {
log.Fatal(err)
}
+ args.Config = conf
- if *version {
- printVersion()
- return
+ // Parse platforms
+ if err := args.ParsePlatforms(*platforms); err != nil {
+ log.Fatal(err)
}
+ // Handle stats only flag
if args.StatsOnly {
// Call the new function to print all stats
schedule.PrintAllStats(args)
diff --git a/internal/oi/oi.go b/internal/oi/oi.go
index 2f1bc1a..eac0cc2 100644
--- a/internal/oi/oi.go
+++ b/internal/oi/oi.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ "codeberg.org/snonux/gos/internal/colour"
"golang.org/x/exp/rand"
)
@@ -117,7 +118,12 @@ func CopyFile(srcPath, dstPath string) error {
if err != nil {
return err
}
- defer source.Close()
+ defer func() {
+ if err := source.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing source file:", err)
+ }
+ }()
if err := EnsureParentDir(dstPath); err != nil {
return err
@@ -127,7 +133,12 @@ func CopyFile(srcPath, dstPath string) error {
if err != nil {
return err
}
- defer destination.Close()
+ defer func() {
+ if err := destination.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already written the data
+ colour.Errorln("Error closing destination file:", err)
+ }
+ }()
_, err = io.Copy(destination, source)
return err
@@ -154,7 +165,12 @@ func WriteFile(filePath, content string) error {
if err != nil {
return err
}
- defer file.Close()
+ defer func() {
+ if err := file.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already written the data
+ colour.Errorln("Error closing file:", err)
+ }
+ }()
_, err = file.WriteString(content)
if err != nil {
diff --git a/internal/platforms/linkedin/linkedin.go b/internal/platforms/linkedin/linkedin.go
index 16688d5..ced242a 100644
--- a/internal/platforms/linkedin/linkedin.go
+++ b/internal/platforms/linkedin/linkedin.go
@@ -116,11 +116,11 @@ func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, conten
payload, err := json.Marshal(post)
if err != nil {
- return fmt.Errorf("Error encoding JSON:%w", err)
+ return fmt.Errorf("error encoding JSON: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", linkedInPostsURL, bytes.NewBuffer(payload))
if err != nil {
- return fmt.Errorf("Error creating request: %w", err)
+ return fmt.Errorf("error creating request: %w", err)
}
// Use configured LinkedIn version if available
@@ -132,17 +132,21 @@ func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, conten
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
- return fmt.Errorf("Error sending request: %w", err)
+ return fmt.Errorf("error sending request: %w", err)
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
- err = fmt.Errorf("failed to post to LinkedIn. Status: %s\n%s\n",
- resp.Status, string(body))
+ err = fmt.Errorf("failed to post to LinkedIn. Status: %s: %s", resp.Status, string(body))
if resp.StatusCode == http.StatusUnauthorized {
err = errors.Join(err, errUnauthorized)
} else if resp.StatusCode == http.StatusUpgradeRequired {
@@ -190,7 +194,12 @@ func initializeImageUpload(ctx context.Context, personURN, accessToken string, l
if err != nil {
return "", "", fmt.Errorf("error sending request: %w", err)
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
body, err := io.ReadAll(resp.Body)
if err != nil {
@@ -198,8 +207,7 @@ func initializeImageUpload(ctx context.Context, personURN, accessToken string, l
}
if resp.StatusCode != http.StatusOK {
- err := fmt.Errorf("image upload initialization failed. Status: %s\n%s\n",
- resp.Status, string(body))
+ err := fmt.Errorf("image upload initialization failed. Status: %s: %s", resp.Status, string(body))
if resp.StatusCode == http.StatusUnauthorized {
err = errors.Join(err, errUnauthorized)
} else if resp.StatusCode == http.StatusUpgradeRequired {
@@ -229,7 +237,12 @@ func performImageUpload(ctx context.Context, imagePath, uploadURL, accessToken s
if err != nil {
return err
}
- defer file.Close()
+ defer func() {
+ if err := file.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing image file:", err)
+ }
+ }()
imageData, err := io.ReadAll(file)
if err != nil {
@@ -248,7 +261,12 @@ func performImageUpload(ctx context.Context, imagePath, uploadURL, accessToken s
if err != nil {
return fmt.Errorf("error sending upload request: %w", err)
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
diff --git a/internal/platforms/linkedin/oauth2/oauth2.go b/internal/platforms/linkedin/oauth2/oauth2.go
index 45cb9b7..b4d3bb3 100644
--- a/internal/platforms/linkedin/oauth2/oauth2.go
+++ b/internal/platforms/linkedin/oauth2/oauth2.go
@@ -41,7 +41,12 @@ func getOauthPersonID(token *oauth2.Token) (string, error) {
if err != nil {
return "", fmt.Errorf("Error making the request:%w", err)
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
@@ -171,7 +176,10 @@ func WaitUntilURLIsReachable(url string) error {
colour.Infofln("URL is not reachable: %v", err)
} else {
colour.Infofln("URL is reachable: %s - Status Code: %d", url, resp.StatusCode)
- resp.Body.Close()
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
return nil
}
}
diff --git a/internal/platforms/linkedin/preview.go b/internal/platforms/linkedin/preview.go
index b7387c9..54c24ba 100644
--- a/internal/platforms/linkedin/preview.go
+++ b/internal/platforms/linkedin/preview.go
@@ -89,7 +89,12 @@ func (p preview) DownloadImage(destPath string) (string, error) {
if err != nil {
return "", err
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("bad status while trying to download image: %s", resp.Status)
@@ -100,7 +105,12 @@ func (p preview) DownloadImage(destPath string) (string, error) {
if err != nil {
return destFile, fmt.Errorf("%s: %w", destFile, err)
}
- defer out.Close()
+ defer func() {
+ if err := out.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already written the data
+ colour.Errorln("Error closing output file:", err)
+ }
+ }()
_, err = io.Copy(out, resp.Body)
if err != nil {
@@ -177,7 +187,12 @@ func extractFromURL(ctx context.Context, url string) (string, string, error) {
if err != nil {
return "", "", fmt.Errorf("failed to get URL: %w", err)
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
if resp.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("failed to get a successful response: %v", resp.StatusCode)
diff --git a/internal/platforms/mastodon/mastodon.go b/internal/platforms/mastodon/mastodon.go
index ad4733b..3b68cde 100644
--- a/internal/platforms/mastodon/mastodon.go
+++ b/internal/platforms/mastodon/mastodon.go
@@ -52,7 +52,12 @@ func Post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
- defer resp.Body.Close()
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ // Log the error but don't fail the operation since we've already read the data
+ colour.Errorln("Error closing response body:", err)
+ }
+ }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
diff --git a/internal/run.go b/internal/run.go
index 34874fc..43874bf 100644
--- a/internal/run.go
+++ b/internal/run.go
@@ -23,49 +23,33 @@ func run(ctx context.Context, args config.Args) error {
printLogo()
// Check if posting is paused
- paused, err := args.Config.IsPaused()
- if err != nil {
- return fmt.Errorf("error checking pause status: %w", err)
- }
- if paused {
- colour.Infoln("Posting is paused until", args.Config.PauseEnd, "- skipping all posts")
- return nil
+ if err := checkPauseStatus(args); err != nil {
+ return err
}
+ // Handle compose mode
if args.ComposeMode {
- entryPath := fmt.Sprintf("%s/%d.ask.txt", args.GosDir, now)
- if err := prompt.EditFile(entryPath); err != nil {
+ if err := handleComposeMode(args); err != nil {
return err
}
}
- if err := queue.Run(args); err != nil {
- if !softError(err) {
- return err
- }
- colour.Infoln(err)
+ // Run queue operations
+ if err := runQueueOperations(args); err != nil {
+ return err
}
- sinceLastRun := time.Duration(now-args.Config.LastRunEpoch) * time.Second
- if sinceLastRun < args.RunInterval {
- colour.Infoln("Run interval of", args.RunInterval, "with", sinceLastRun, "not yet reached. Not posting anything!")
- return nil
+ // Check run interval
+ if err := checkRunInterval(args); err != nil {
+ return err
}
- for platformStr, sizeLimit := range args.Platforms {
- platform, err := platforms.New(platformStr)
- if err != nil {
- return err
- }
- if err := runPlatform(ctx, args, platform, sizeLimit); err != nil {
- if softError(err) {
- colour.Infoln(err)
- continue
- }
- return err
- }
+ // Post to platforms
+ if err := postToPlatforms(ctx, args); err != nil {
+ return err
}
+ // Update last run time
args.Config.LastRunEpoch = now
return args.Config.WriteToDisk(args.ConfigPath)
}
@@ -95,6 +79,64 @@ func runPlatform(ctx context.Context, args config.Args, platform platforms.Platf
return err
}
+func checkPauseStatus(args config.Args) error {
+ // Check if posting is paused
+ paused, err := args.Config.IsPaused()
+ if err != nil {
+ return fmt.Errorf("error checking pause status: %w", err)
+ }
+ if paused {
+ colour.Infoln("Posting is paused until", args.Config.PauseEnd, "- skipping all posts")
+ return nil
+ }
+ return nil
+}
+
+func handleComposeMode(args config.Args) error {
+ entryPath := fmt.Sprintf("%s/%d.ask.txt", args.GosDir, time.Now().Unix())
+ if err := prompt.EditFile(entryPath); err != nil {
+ return err
+ }
+ return nil
+}
+
+func runQueueOperations(args config.Args) error {
+ if err := queue.Run(args); err != nil {
+ if !softError(err) {
+ return err
+ }
+ colour.Infoln(err)
+ }
+ return nil
+}
+
+func checkRunInterval(args config.Args) error {
+ now := time.Now().Unix()
+ sinceLastRun := time.Duration(now-args.Config.LastRunEpoch) * time.Second
+ if sinceLastRun < args.RunInterval {
+ colour.Infoln("Run interval of", args.RunInterval, "with", sinceLastRun, "not yet reached. Not posting anything!")
+ return nil
+ }
+ return nil
+}
+
+func postToPlatforms(ctx context.Context, args config.Args) error {
+ for platformStr, sizeLimit := range args.Platforms {
+ platform, err := platforms.New(platformStr)
+ if err != nil {
+ return err
+ }
+ if err := runPlatform(ctx, args, platform, sizeLimit); err != nil {
+ if softError(err) {
+ colour.Infoln(err)
+ continue
+ }
+ return err
+ }
+ }
+ return nil
+}
+
func softError(err error) bool {
return errors.Is(err, prompt.ErrAborted)
}
diff --git a/internal/version.go b/internal/version.go
index 3f9eb70..c76328b 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -6,7 +6,7 @@ import (
"codeberg.org/snonux/gos/internal/table"
)
-const versionStr = "v1.2.5"
+const versionStr = "v1.2.6"
func printVersion() {
table.New().