diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/colour/colour.go | 8 | ||||
| -rw-r--r-- | internal/config/config.go | 17 | ||||
| -rw-r--r-- | internal/config/config_test.go | 10 | ||||
| -rw-r--r-- | internal/entry/entry.go | 23 | ||||
| -rw-r--r-- | internal/main.go | 36 | ||||
| -rw-r--r-- | internal/oi/oi.go | 22 | ||||
| -rw-r--r-- | internal/platforms/linkedin/linkedin.go | 40 | ||||
| -rw-r--r-- | internal/platforms/linkedin/oauth2/oauth2.go | 12 | ||||
| -rw-r--r-- | internal/platforms/linkedin/preview.go | 21 | ||||
| -rw-r--r-- | internal/platforms/mastodon/mastodon.go | 7 | ||||
| -rw-r--r-- | internal/run.go | 102 | ||||
| -rw-r--r-- | internal/version.go | 2 |
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(). |
