feat: address TODOs and write README.md

This commit is contained in:
Stevan Freeborn
2026-08-04 18:00:54 -05:00
parent 79090b3d39
commit ecaa816f1c
4 changed files with 200 additions and 40 deletions
+18 -31
View File
@@ -4,7 +4,6 @@ import (
"cmp"
"context"
"fmt"
"net/url"
"os"
"os/signal"
"slices"
@@ -17,7 +16,6 @@ import (
"github.com/StevanFreeborn/gator/internal/rss"
"github.com/StevanFreeborn/gator/internal/state"
"github.com/google/uuid"
"golang.org/x/term"
)
type Command struct {
@@ -192,30 +190,13 @@ func aggCommand() *Command {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
restore, err := enableKeypressExit(cancel)
if err != nil {
return fmt.Errorf("Failed to enable raw mode: %v", err)
return err
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
go func() {
buf := make([]byte, 1)
for {
n, err := os.Stdin.Read(buf)
if err != nil || n == 0 {
return
}
if buf[0] == 'q' || buf[0] == 'Q' || buf[0] == 3 {
cancel()
return
}
}
}()
defer restore()
ticker := time.NewTicker(validDuration)
defer ticker.Stop()
@@ -246,21 +227,27 @@ func addFeedCommand() *Command {
feedName := s.Arguments[0]
feedUrl := s.Arguments[1]
validatedFeedUrl, err := url.Parse(feedUrl)
if strings.TrimSpace(feedName) == "" {
return fmt.Errorf("Feed name cannot be empty")
}
canonicalFeedUrl, err := normalizeFeedURL(feedUrl)
if err != nil {
return fmt.Errorf("Feed url '%s' is not a valid url", feedUrl)
}
_, err = s.Database.GetFeedByUrl(context.Background(), canonicalFeedUrl)
if err == nil {
return fmt.Errorf("Feed with url '%s' already exists; use 'follow <name>' to follow it", canonicalFeedUrl)
}
createFeedParams := database.CreateFeedParams{
ID: uuid.New(),
UserID: currentUser.ID,
Name: feedName,
Url: validatedFeedUrl.String(),
Url: canonicalFeedUrl,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -325,12 +312,12 @@ func followCommand() *Command {
return fmt.Errorf("Did not receive expected url argument")
}
urlForFeedToFollow := s.Arguments[0]
feedIdentifier := s.Arguments[0]
feed, err := s.Database.GetFeedByUrl(context.Background(), urlForFeedToFollow)
feed, err := resolveFeedByNameOrURL(s, feedIdentifier)
if err != nil {
return fmt.Errorf("Unable to find feed with url '%s'", urlForFeedToFollow)
return err
}
createFollowParams := database.CreateFollowParams{
@@ -375,12 +362,12 @@ func unfollowCommand() *Command {
return fmt.Errorf("Did not receive expected feed url argument")
}
feedUrl := s.Arguments[0]
feedIdentifier := s.Arguments[0]
feed, err := s.Database.GetFeedByUrl(context.Background(), feedUrl)
feed, err := resolveFeedByNameOrURL(s, feedIdentifier)
if err != nil {
return fmt.Errorf("Unable to find feed with url '%s'", feedUrl)
return err
}
deleteFollowParams := database.DeleteFollowForUserParams{
+107
View File
@@ -0,0 +1,107 @@
package command
import (
"context"
"fmt"
"net/url"
"os"
"strings"
"github.com/StevanFreeborn/gator/internal/database"
"github.com/StevanFreeborn/gator/internal/state"
"golang.org/x/term"
)
func normalizeFeedURL(raw string) (string, error) {
u, err := url.Parse(raw)
if err != nil {
return "", err
}
u.Scheme = strings.ToLower(u.Scheme)
u.Host = strings.ToLower(u.Host)
if (u.Scheme == "http" && u.Port() == "80") || (u.Scheme == "https" && u.Port() == "443") {
u.Host = u.Hostname()
}
if len(u.Path) > 1 {
u.Path = strings.TrimSuffix(u.Path, "/")
}
u.Fragment = ""
return u.String(), nil
}
func resolveFeedByNameOrURL(s *state.State, identifier string) (database.Feed, error) {
if strings.TrimSpace(identifier) == "" {
return database.Feed{}, fmt.Errorf("Feed identifier cannot be empty")
}
feeds, err := s.Database.GetAllFeeds(context.Background())
if err != nil {
return database.Feed{}, fmt.Errorf("Failed to load feeds")
}
var matches []database.Feed
for _, feed := range feeds {
if strings.EqualFold(feed.Name, identifier) {
matches = append(matches, feed)
}
}
if len(matches) == 1 {
return matches[0], nil
}
if len(matches) > 1 {
return database.Feed{}, fmt.Errorf("Multiple feeds match name '%s'; use a url instead", identifier)
}
normalizedURL, err := normalizeFeedURL(identifier)
if err != nil {
return database.Feed{}, fmt.Errorf("No feed found matching name or url '%s'", identifier)
}
feed, err := s.Database.GetFeedByUrl(context.Background(), normalizedURL)
if err != nil {
return database.Feed{}, fmt.Errorf("No feed found matching name or url '%s'", identifier)
}
return feed, nil
}
func enableKeypressExit(cancel context.CancelFunc) (func(), error) {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return nil, fmt.Errorf("Failed to enable raw mode: %v", err)
}
go func() {
buf := make([]byte, 1)
for {
n, err := os.Stdin.Read(buf)
if err != nil || n == 0 {
return
}
if buf[0] == 'q' || buf[0] == 'Q' || buf[0] == 3 {
cancel()
return
}
}
}()
return func() {
term.Restore(int(os.Stdin.Fd()), oldState)
}, nil
}