420 lines
9.0 KiB
Go
420 lines
9.0 KiB
Go
package command
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/StevanFreeborn/gator/internal/database"
|
|
"github.com/StevanFreeborn/gator/internal/rss"
|
|
"github.com/StevanFreeborn/gator/internal/state"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
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(),
|
|
registerCommand(),
|
|
resetCommand(),
|
|
usersCommand(),
|
|
aggCommand(),
|
|
addFeedCommand(),
|
|
feedsCommand(),
|
|
followCommand(),
|
|
followingCommand(),
|
|
unfollowCommand(),
|
|
browseCommand(),
|
|
}
|
|
|
|
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]
|
|
|
|
_, err := s.Database.GetUserByName(context.Background(), username)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to login")
|
|
}
|
|
|
|
err = s.Config.SetUser(username)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Current user set to '%s'\n", username)
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
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 time between requests argument")
|
|
}
|
|
|
|
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()
|
|
|
|
restore, err := enableKeypressExit(cancel)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer restore()
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func addFeedCommand() *Command {
|
|
return newCommand("addfeed", requiresLoggedInUser(func(s *state.State, currentUser database.User) error {
|
|
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")
|
|
}
|
|
|
|
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: canonicalFeedUrl,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
createdFeed, err := s.Database.CreateFeed(context.Background(), createFeedParams)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
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
|
|
}))
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
feedIdentifier := s.Arguments[0]
|
|
|
|
feed, err := resolveFeedByNameOrURL(s, feedIdentifier)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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\n", 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")
|
|
}
|
|
|
|
feedIdentifier := s.Arguments[0]
|
|
|
|
feed, err := resolveFeedByNameOrURL(s, feedIdentifier)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}))
|
|
}
|
|
|
|
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
|
|
}))
|
|
}
|