diff --git a/internal/database/createPost.sql.go b/internal/database/createPost.sql.go new file mode 100644 index 0000000..1f6d57b --- /dev/null +++ b/internal/database/createPost.sql.go @@ -0,0 +1,73 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: createPost.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createPost = `-- name: CreatePost :one +INSERT INTO posts (id, created_at, updated_at, url, title, description, feed_id, published_at) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8 + ) ON CONFLICT (url) DO UPDATE + SET + id = $1, + created_at = $2, + updated_at = $3, + title = $5, + description = $6, + feed_id = $7, + published_at = $8 + WHERE posts.url = $4 +RETURNING id, created_at, updated_at, title, url, description, published_at, feed_id +` + +type CreatePostParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Url string + Title string + Description string + FeedID uuid.UUID + PublishedAt time.Time +} + +func (q *Queries) CreatePost(ctx context.Context, arg CreatePostParams) (Post, error) { + row := q.db.QueryRowContext(ctx, createPost, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Url, + arg.Title, + arg.Description, + arg.FeedID, + arg.PublishedAt, + ) + var i Post + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Title, + &i.Url, + &i.Description, + &i.PublishedAt, + &i.FeedID, + ) + return i, err +} diff --git a/internal/database/getPostsForUser.sql.go b/internal/database/getPostsForUser.sql.go new file mode 100644 index 0000000..dab737e --- /dev/null +++ b/internal/database/getPostsForUser.sql.go @@ -0,0 +1,54 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: getPostsForUser.sql + +package database + +import ( + "context" + + "github.com/google/uuid" +) + +const getPostsForUser = `-- name: GetPostsForUser :many +SELECT id, created_at, updated_at, title, url, description, published_at, feed_id FROM posts WHERE feed_id IN (SELECT feed_follows.feed_id FROM feed_follows WHERE feed_follows.user_id = $1) +ORDER BY published_at DESC LIMIT $2 +` + +type GetPostsForUserParams struct { + UserID uuid.UUID + Limit int32 +} + +func (q *Queries) GetPostsForUser(ctx context.Context, arg GetPostsForUserParams) ([]Post, error) { + rows, err := q.db.QueryContext(ctx, getPostsForUser, arg.UserID, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Post + for rows.Next() { + var i Post + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Title, + &i.Url, + &i.Description, + &i.PublishedAt, + &i.FeedID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/database/models.go b/internal/database/models.go index 68f3984..ec2b920 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -29,6 +29,17 @@ type FeedFollow struct { FeedID uuid.UUID } +type Post struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Title string + Url string + Description string + PublishedAt time.Time + FeedID uuid.UUID +} + type User struct { ID uuid.UUID CreatedAt time.Time diff --git a/internal/handlers/browseHandler.go b/internal/handlers/browseHandler.go new file mode 100644 index 0000000..810645e --- /dev/null +++ b/internal/handlers/browseHandler.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "context" + "fmt" + "github.com/rdarius/boot-dev-blog-aggregator/internal/config" + "github.com/rdarius/boot-dev-blog-aggregator/internal/database" + "strconv" +) + +func BrowseHandler(s *config.State, cmd config.Command, user database.User) error { + var limit int32 + if len(cmd.Args) < 1 { + limit = 2 + } else { + l, err := strconv.Atoi(cmd.Args[0]) + if err != nil { + return err + } + limit = int32(l) + } + + posts, err := s.DB.GetPostsForUser(context.Background(), database.GetPostsForUserParams{ + UserID: user.ID, + Limit: limit, + }) + if err != nil { + return err + } + + for _, post := range posts { + fmt.Printf("Title: %s\nURL: %s\nArticle: %sPublished: %s\n\n", post.Title, post.Url, post.Description, post.PublishedAt) + } + + return nil +} diff --git a/internal/handlers/scrapeFeedsHandler.go b/internal/handlers/scrapeFeedsHandler.go index 85e0c1c..7eb3771 100644 --- a/internal/handlers/scrapeFeedsHandler.go +++ b/internal/handlers/scrapeFeedsHandler.go @@ -3,8 +3,12 @@ package handlers import ( "context" "fmt" + "github.com/google/uuid" "github.com/rdarius/boot-dev-blog-aggregator/internal/config" + "github.com/rdarius/boot-dev-blog-aggregator/internal/database" + "github.com/rdarius/boot-dev-blog-aggregator/internal/parser" "github.com/rdarius/boot-dev-blog-aggregator/internal/rss" + "time" ) func ScrapeFeedsHandler(s *config.State, cmd config.Command) error { @@ -29,7 +33,24 @@ func ScrapeFeedsHandler(s *config.State, cmd config.Command) error { fmt.Println("Feed Title: " + feedData.Channel.Title) for _, i := range feedData.Channel.Item { - fmt.Println("Item Title: " + i.Title) + + pubTime, err := parser.ParseTimestamp(i.PubDate) + if err != nil { + return err + } + _, err = s.DB.CreatePost(ctx, database.CreatePostParams{ + ID: uuid.New(), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + FeedID: nextFeed.ID, + Url: i.Link, + Title: i.Title, + Description: i.Description, + PublishedAt: pubTime, + }) + if err != nil { + return err + } } return nil diff --git a/internal/parser/TimestampParser.go b/internal/parser/TimestampParser.go new file mode 100644 index 0000000..e26bf49 --- /dev/null +++ b/internal/parser/TimestampParser.go @@ -0,0 +1,29 @@ +package parser + +import ( + "fmt" + "time" +) + +func ParseTimestamp(str string) (time.Time, error) { + layouts := []string{ + time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST" + time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700" + time.RFC3339, // "2006-01-02T15:04:05Z07:00" + time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00" + "2006-01-02 15:04:05", // Common SQL format + "02 Jan 2006 15:04:05", // "02 Jan 2025 15:04:05" + "02-Jan-2006 15:04:05", // "02-Jan-2025 15:04:05" + "02/01/2006 15:04:05", // "02/01/2025 15:04:05" + } + + var t time.Time + var err error + for _, layout := range layouts { + t, err = time.Parse(layout, str) + if err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("unknown timestamp format: %s", str) +} diff --git a/main.go b/main.go index 8109243..857a82d 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ func main() { commands.Register("follow", middlewareLoggedIn(handlers.FollowFeedHandler)) commands.Register("unfollow", middlewareLoggedIn(handlers.UnfollowFeedHandler)) commands.Register("following", middlewareLoggedIn(handlers.GetFeedFollowsByUserHandler)) + commands.Register("browse", middlewareLoggedIn(handlers.BrowseHandler)) if len(os.Args) < 2 { log.Fatal("usage: boot-dev-blog-aggregator [args...]") diff --git a/sql/queries/createPost.sql b/sql/queries/createPost.sql new file mode 100644 index 0000000..f4850b9 --- /dev/null +++ b/sql/queries/createPost.sql @@ -0,0 +1,22 @@ +-- name: CreatePost :one +INSERT INTO posts (id, created_at, updated_at, url, title, description, feed_id, published_at) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8 + ) ON CONFLICT (url) DO UPDATE + SET + id = $1, + created_at = $2, + updated_at = $3, + title = $5, + description = $6, + feed_id = $7, + published_at = $8 + WHERE posts.url = $4 +RETURNING *; \ No newline at end of file diff --git a/sql/queries/getPostsForUser.sql b/sql/queries/getPostsForUser.sql new file mode 100644 index 0000000..2636ce3 --- /dev/null +++ b/sql/queries/getPostsForUser.sql @@ -0,0 +1,3 @@ +-- name: GetPostsForUser :many +SELECT * FROM posts WHERE feed_id IN (SELECT feed_follows.feed_id FROM feed_follows WHERE feed_follows.user_id = $1) +ORDER BY published_at DESC LIMIT $2; \ No newline at end of file diff --git a/sql/schema/0005_posts.sql b/sql/schema/0005_posts.sql new file mode 100644 index 0000000..9382dbf --- /dev/null +++ b/sql/schema/0005_posts.sql @@ -0,0 +1,14 @@ +-- +goose Up +CREATE TABLE posts( + id UUID PRIMARY KEY, + created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP not null, + title VARCHAR(128) not null, + url VARCHAR(512) not null UNIQUE, + description TEXT not null, + published_at TIMESTAMP not null , + feed_id UUID not null REFERENCES feeds(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE posts; \ No newline at end of file