Files

420 lines
9.0 KiB
Go
Raw Permalink Normal View History

2026-07-31 08:06:29 -05:00
package command
import (
2026-08-01 14:26:19 -05:00
"cmp"
"context"
2026-07-31 08:06:29 -05:00
"fmt"
2026-08-03 07:14:33 -05:00
"os"
"os/signal"
2026-08-01 14:26:19 -05:00
"slices"
2026-08-03 07:14:33 -05:00
"strconv"
2026-08-01 14:26:19 -05:00
"strings"
2026-08-03 07:14:33 -05:00
"syscall"
2026-08-01 14:26:19 -05:00
"time"
2026-07-31 08:06:29 -05:00
2026-08-01 14:26:19 -05:00
"github.com/StevanFreeborn/gator/internal/database"
"github.com/StevanFreeborn/gator/internal/rss"
2026-07-31 08:06:29 -05:00
"github.com/StevanFreeborn/gator/internal/state"
2026-08-01 14:26:19 -05:00
"github.com/google/uuid"
2026-07-31 08:06:29 -05:00
)
type Command struct {
Name string
Handler CommandHandler
}
type CommandHandler func(s *state.State) error
func newCommand(name string, handler CommandHandler) *Command {
return &Command{
Name: name,
Handler: handler,
}
}
type CommandRegistry struct {
commands map[string]*Command
}
func (c *CommandRegistry) register(cmd *Command) error {
c.commands[cmd.Name] = cmd
return nil
}
func NewRegistry() *CommandRegistry {
cr := CommandRegistry{
commands: map[string]*Command{},
}
commands := []*Command{
loginCommand(),
2026-08-01 14:26:19 -05:00
registerCommand(),
resetCommand(),
usersCommand(),
aggCommand(),
addFeedCommand(),
feedsCommand(),
2026-08-02 07:49:57 -05:00
followCommand(),
followingCommand(),
unfollowCommand(),
2026-08-03 07:14:33 -05:00
browseCommand(),
2026-07-31 08:06:29 -05:00
}
for _, cmd := range commands {
cr.register(cmd)
}
return &cr
}
func (c *CommandRegistry) RunCommand(cmdName string, s *state.State) error {
cmd, found := c.commands[cmdName]
if !found {
return fmt.Errorf("No '%s' registered", cmdName)
}
return cmd.Handler(s)
}
func loginCommand() *Command {
return newCommand("login", func(s *state.State) error {
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected username argument")
}
username := s.Arguments[0]
2026-08-01 14:26:19 -05:00
_, err := s.Database.GetUserByName(context.Background(), username)
if err != nil {
return fmt.Errorf("Failed to login")
}
err = s.Config.SetUser(username)
2026-07-31 08:06:29 -05:00
if err != nil {
return err
}
fmt.Printf("Current user set to '%s'\n", username)
return nil
})
}
2026-08-01 14:26:19 -05:00
func registerCommand() *Command {
return newCommand("register", func(s *state.State) error {
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected username argument")
}
createUserParams := database.CreateUserParams{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Name: s.Arguments[0],
}
createdUser, err := s.Database.CreateUser(context.Background(), createUserParams)
if err != nil {
return err
}
s.Config.SetUser(createdUser.Name)
fmt.Printf("Successfully registered user '%s':\n", createdUser.Name)
fmt.Printf(" Id => %s\n", createdUser.ID)
fmt.Printf(" CreatedAt => %s\n", createdUser.CreatedAt)
fmt.Printf(" UpdatedAt => %s\n", createdUser.UpdatedAt)
fmt.Printf("Current user set to user '%s':\n", createdUser.Name)
return nil
})
}
func resetCommand() *Command {
return newCommand("reset", func(s *state.State) error {
err := s.Database.DeleteAllUsers(context.Background())
if err != nil {
return err
}
fmt.Println("Successfully delete all users")
return nil
})
}
func usersCommand() *Command {
return newCommand("users", func(s *state.State) error {
users, err := s.Database.GetAllUsers(context.Background())
if err != nil {
return err
}
slices.SortFunc(users, func(a, b database.User) int {
return cmp.Compare(a.Name, b.Name)
})
for _, user := range users {
msg := "* %s"
if user.Name == s.Config.CurrentUserName {
msg += " (current)"
}
fmt.Printf(msg+"\n", user.Name)
}
return nil
})
}
func aggCommand() *Command {
return newCommand("agg", func(s *state.State) error {
2026-08-03 07:14:33 -05:00
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected time between requests argument")
2026-08-01 14:26:19 -05:00
}
2026-08-03 07:14:33 -05:00
timeBetweenRequests := s.Arguments[0]
validDuration, err := time.ParseDuration(timeBetweenRequests)
if err != nil {
return fmt.Errorf("Time between requests argument '%s' not valid duration string", timeBetweenRequests)
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
2026-08-04 18:00:54 -05:00
restore, err := enableKeypressExit(cancel)
2026-08-03 07:14:33 -05:00
if err != nil {
2026-08-04 18:00:54 -05:00
return err
2026-08-03 07:14:33 -05:00
}
2026-08-04 18:00:54 -05:00
defer restore()
2026-08-03 07:14:33 -05:00
ticker := time.NewTicker(validDuration)
defer ticker.Stop()
rss.ScrapeNextFeed(ctx, s)
fetchFeeds:
for {
select {
case <-ctx.Done():
fmt.Print("Stopping feed agg command\r\n")
break fetchFeeds
case <-ticker.C:
rss.ScrapeNextFeed(ctx, s)
}
}
2026-08-01 14:26:19 -05:00
return nil
})
}
func addFeedCommand() *Command {
2026-08-02 07:49:57 -05:00
return newCommand("addfeed", requiresLoggedInUser(func(s *state.State, currentUser database.User) error {
2026-08-01 14:26:19 -05:00
if len(s.Arguments) < 2 {
return fmt.Errorf("Did not receive expected feed name and url")
}
feedName := s.Arguments[0]
feedUrl := s.Arguments[1]
if strings.TrimSpace(feedName) == "" {
return fmt.Errorf("Feed name cannot be empty")
}
2026-08-04 18:00:54 -05:00
canonicalFeedUrl, err := normalizeFeedURL(feedUrl)
2026-08-01 14:26:19 -05:00
if err != nil {
return fmt.Errorf("Feed url '%s' is not a valid url", feedUrl)
}
2026-08-04 18:00:54 -05:00
_, 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)
}
2026-08-01 14:26:19 -05:00
createFeedParams := database.CreateFeedParams{
ID: uuid.New(),
UserID: currentUser.ID,
Name: feedName,
2026-08-04 18:00:54 -05:00
Url: canonicalFeedUrl,
2026-08-01 14:26:19 -05:00
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdFeed, err := s.Database.CreateFeed(context.Background(), createFeedParams)
if err != nil {
return err
}
2026-08-02 07:49:57 -05:00
createFollowParams := database.CreateFollowParams{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
UserID: currentUser.ID,
FeedID: createdFeed.ID,
}
_, err = s.Database.CreateFollow(context.Background(), createFollowParams)
if err != nil {
return err
}
fmt.Printf("Successfully added and followed feed '%s' with url '%s'\n", createdFeed.Name, createdFeed.Url)
2026-08-01 14:26:19 -05:00
fmt.Printf(" Id => %s\n", createdFeed.ID)
fmt.Printf(" UserId => %s\n", createdFeed.UserID)
fmt.Printf(" CreatedAt => %s\n", createdFeed.CreatedAt)
fmt.Printf(" UpdatedAt => %s\n", createdFeed.UpdatedAt)
return nil
2026-08-02 07:49:57 -05:00
}))
2026-08-01 14:26:19 -05:00
}
func feedsCommand() *Command {
return newCommand("feeds", func(s *state.State) error {
feeds, err := s.Database.GetAllFeeds(context.Background())
if err != nil {
return err
}
for _, feed := range feeds {
feedsUserName := "Unknown"
user, err := s.Database.GetUserById(context.Background(), feed.UserID)
if err == nil {
feedsUserName = user.Name
}
fmt.Printf("* %s [%s] (%s)\n", feed.Name, feed.Url, feedsUserName)
}
return nil
})
}
2026-08-02 07:49:57 -05:00
func followCommand() *Command {
return newCommand("follow", requiresLoggedInUser(func(s *state.State, currentUser database.User) error {
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected url argument")
}
2026-08-04 18:00:54 -05:00
feedIdentifier := s.Arguments[0]
2026-08-02 07:49:57 -05:00
2026-08-04 18:00:54 -05:00
feed, err := resolveFeedByNameOrURL(s, feedIdentifier)
2026-08-02 07:49:57 -05:00
if err != nil {
2026-08-04 18:00:54 -05:00
return err
2026-08-02 07:49:57 -05:00
}
createFollowParams := database.CreateFollowParams{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
FeedID: feed.ID,
UserID: currentUser.ID,
}
createdFollow, err := s.Database.CreateFollow(context.Background(), createFollowParams)
if err != nil {
return fmt.Errorf("Unable to follow feed '%s'", feed.Name)
}
fmt.Printf("'%s' successfully followed feed '%s'", createdFollow.UserName, createdFollow.FeedName)
return nil
}))
}
func followingCommand() *Command {
return newCommand("following", requiresLoggedInUser(func(s *state.State, currentUser database.User) error {
follows, err := s.Database.GetFollowsForUser(context.Background(), currentUser.ID)
if err != nil {
return fmt.Errorf("Unable to find follows for user")
}
for _, follow := range follows {
2026-08-03 07:14:33 -05:00
fmt.Printf("* %s\n", follow.FeedName)
2026-08-02 07:49:57 -05:00
}
return nil
}))
}
func unfollowCommand() *Command {
return newCommand("unfollow", requiresLoggedInUser(func(s *state.State, currentUser database.User) error {
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected feed url argument")
}
2026-08-04 18:00:54 -05:00
feedIdentifier := s.Arguments[0]
2026-08-02 07:49:57 -05:00
2026-08-04 18:00:54 -05:00
feed, err := resolveFeedByNameOrURL(s, feedIdentifier)
2026-08-02 07:49:57 -05:00
if err != nil {
2026-08-04 18:00:54 -05:00
return err
2026-08-02 07:49:57 -05:00
}
deleteFollowParams := database.DeleteFollowForUserParams{
UserID: currentUser.ID,
FeedID: feed.ID,
}
err = s.Database.DeleteFollowForUser(context.Background(), deleteFollowParams)
if err != nil {
return fmt.Errorf("Unable to unfollow feed '%s'", feed.Name)
}
fmt.Printf("Successfully unfollowed feed '%s'", feed.Name)
return nil
}))
}
2026-08-03 07:14:33 -05:00
func browseCommand() *Command {
return newCommand("browse", requiresLoggedInUser(func(s *state.State, currentUser database.User) error {
limit := int32(2)
if len(s.Arguments) > 0 {
parsedLimit, err := strconv.ParseInt(s.Arguments[0], 10, 32)
if err == nil {
limit = int32(parsedLimit)
}
}
getUserPostsParams := database.GetPostsForUserParams{
UserID: currentUser.ID,
Limit: limit,
}
posts, err := s.Database.GetPostsForUser(context.Background(), getUserPostsParams)
if err != nil {
return err
}
for _, post := range posts {
fmt.Printf("* %s - %s\n", post.FeedName, post.Title)
}
return nil
}))
}