feat: address TODOs and write README.md
This commit is contained in:
+18
-31
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user