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-01 14:26:19 -05:00
|
|
|
"net/url"
|
|
|
|
|
"slices"
|
|
|
|
|
"strings"
|
|
|
|
|
"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-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 {
|
|
|
|
|
// if len(s.Arguments) == 0 {
|
|
|
|
|
// return fmt.Errorf("Did not receive expected feed argument")
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// feed := s.Arguments[0]
|
|
|
|
|
//
|
|
|
|
|
// validUrl, err := url.Parse(feed)
|
|
|
|
|
//
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return fmt.Errorf("Feed '%s' is not a valid url", feed)
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
rssFeed, err := rss.FetchFeed(context.Background(), "https://www.wagslane.dev/index.xml")
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("%v\n", rssFeed)
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
validatedFeedUrl, err := url.Parse(feedUrl)
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(feedName) == "" {
|
|
|
|
|
return fmt.Errorf("Feed name cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Feed url '%s' is not a valid url", feedUrl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createFeedParams := database.CreateFeedParams{
|
|
|
|
|
ID: uuid.New(),
|
|
|
|
|
UserID: currentUser.ID,
|
|
|
|
|
Name: feedName,
|
|
|
|
|
Url: validatedFeedUrl.String(),
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
urlForFeedToFollow := s.Arguments[0]
|
|
|
|
|
|
|
|
|
|
feed, err := s.Database.GetFeedByUrl(context.Background(), urlForFeedToFollow)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Unable to find feed with url '%s'", urlForFeedToFollow)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
fmt.Printf("* %s", follow.FeedName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
feedUrl := s.Arguments[0]
|
|
|
|
|
|
|
|
|
|
feed, err := s.Database.GetFeedByUrl(context.Background(), feedUrl)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Unable to find feed with url '%s'", feedUrl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}))
|
|
|
|
|
}
|