From 963e392ceab68784d7f1023d00db512084eba2dd Mon Sep 17 00:00:00 2001 From: Marco Realacci Date: Tue, 22 Nov 2022 15:17:08 +0100 Subject: [PATCH] Add stream search, offset & limit, improved bans --- service/api/api-handler.go | 4 +++ service/api/comments.go | 2 +- service/api/followers.go | 4 +-- service/api/get-stream.go | 48 ++++++++++++++++++++++++++++ service/api/helpers/get-limits.go | 33 +++++++++++++++++++ service/api/likes.go | 2 +- service/api/search.go | 53 +++++++++++++++++++++++++++++++ service/database/database.go | 11 ++++--- service/database/db-comments.go | 12 +++++-- service/database/db-likes.go | 11 +++++-- service/database/db-photos.go | 37 --------------------- service/database/db-profile.go | 51 ++++++++++++++++++++++------- service/database/db-stream.go | 50 +++++++++++++++++++++++++++++ service/database/db-users.go | 45 +++++++++++++++++++++++--- 14 files changed, 298 insertions(+), 65 deletions(-) create mode 100644 service/api/get-stream.go create mode 100644 service/api/helpers/get-limits.go create mode 100644 service/api/search.go create mode 100644 service/database/db-stream.go diff --git a/service/api/api-handler.go b/service/api/api-handler.go index c35c519..60741cc 100644 --- a/service/api/api-handler.go +++ b/service/api/api-handler.go @@ -11,6 +11,8 @@ func (rt *_router) Handler() http.Handler { rt.router.PUT("/users/:user_id/username", rt.wrap(rt.UpdateUsername)) + rt.router.GET("/users", rt.wrap(rt.GetSearchUsers)) + rt.router.GET("/users/:user_id/followers", rt.wrap(rt.GetFollowersFollowing)) rt.router.PUT("/users/:user_id/followers/:follower_uid", rt.wrap(rt.PutFollow)) rt.router.DELETE("/users/:user_id/followers/:follower_uid", rt.wrap(rt.DeleteFollow)) @@ -33,6 +35,8 @@ func (rt *_router) Handler() http.Handler { rt.router.GET("/users/:user_id", rt.wrap(rt.GetUserProfile)) + rt.router.GET("/stream", rt.wrap(rt.GetUserStream)) //todo: why not "/users/:user_id/stream"? + rt.router.GET("/", rt.getHelloWorld) rt.router.GET("/context", rt.wrap(rt.getContextReply)) diff --git a/service/api/comments.go b/service/api/comments.go index 4d69497..0a1aa00 100644 --- a/service/api/comments.go +++ b/service/api/comments.go @@ -34,7 +34,7 @@ func (rt *_router) GetComments(w http.ResponseWriter, r *http.Request, ps httpro } // get the user's comments - success, comments, err := rt.db.GetComments(uid, photo_id) + success, comments, err := rt.db.GetComments(uid, photo_id, ctx.Auth.GetUserID()) if err != nil { helpers.SendInternalError(err, "Database error: GetComments", w, rt.baseLogger) diff --git a/service/api/followers.go b/service/api/followers.go index 36a46c2..37d3f02 100644 --- a/service/api/followers.go +++ b/service/api/followers.go @@ -28,10 +28,10 @@ func (rt *_router) GetFollowersFollowing(w http.ResponseWriter, r *http.Request, // Check if client is asking for followers or following if strings.HasSuffix(r.URL.Path, "/followers") { // Get the followers from the database - status, users, err = rt.db.GetUserFollowers(uid) + status, users, err = rt.db.GetUserFollowers(uid, ctx.Auth.GetUserID()) } else { // Get the following users from the database - status, users, err = rt.db.GetUserFollowing(uid) + status, users, err = rt.db.GetUserFollowing(uid, ctx.Auth.GetUserID()) } // Send a 500 response if there was an error diff --git a/service/api/get-stream.go b/service/api/get-stream.go new file mode 100644 index 0000000..e0fcecd --- /dev/null +++ b/service/api/get-stream.go @@ -0,0 +1,48 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/notherealmarco/WASAPhoto/service/api/authorization" + "github.com/notherealmarco/WASAPhoto/service/api/helpers" + "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" +) + +func (rt *_router) GetUserStream(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + // We must know who is requesting the stream + if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) { + return + } + + // Get user id, probably this should be changed as it's not really REST + uid := ctx.Auth.GetUserID() + + // Get start index and limit, or their default values + start_index, limit, err := helpers.GetLimits(r.URL.Query()) + + if err != nil { + helpers.SendBadRequest(w, "Invalid start_index or limit value", rt.baseLogger) + return + } + + // Get the stream + stream, err := rt.db.GetUserStream(uid, start_index, limit) + + if err != nil { + helpers.SendInternalError(err, "Database error: GetUserProfile", w, rt.baseLogger) + return + } + + // Return the stream in json format + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(stream) + + if err != nil { + helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger) + return + } +} diff --git a/service/api/helpers/get-limits.go b/service/api/helpers/get-limits.go new file mode 100644 index 0000000..2674d59 --- /dev/null +++ b/service/api/helpers/get-limits.go @@ -0,0 +1,33 @@ +package helpers + +import ( + "net/url" + "strconv" +) + +const ( + DEFAULT_LIMIT = 10 // todo: move to config + DEFAULT_OFFSET = 0 +) + +func GetLimits(query url.Values) (int, int, error) { + + limit := DEFAULT_LIMIT + start_index := DEFAULT_OFFSET + + var err error + + if query.Get("limit") != "" { + limit, err = strconv.Atoi(query.Get("limit")) + } + + if query.Get("start_index") != "" { + start_index, err = strconv.Atoi(query.Get("start_index")) + } + + if err != nil { + return 0, 0, err + } + + return start_index, limit, nil +} diff --git a/service/api/likes.go b/service/api/likes.go index 697e67a..38bed29 100644 --- a/service/api/likes.go +++ b/service/api/likes.go @@ -29,7 +29,7 @@ func (rt *_router) GetLikes(w http.ResponseWriter, r *http.Request, ps httproute } // get the user's likes - success, likes, err := rt.db.GetPhotoLikes(uid, photo_id) + success, likes, err := rt.db.GetPhotoLikes(uid, photo_id, ctx.Auth.GetUserID()) if err != nil { helpers.SendInternalError(err, "Database error: GetLikes", w, rt.baseLogger) diff --git a/service/api/search.go b/service/api/search.go new file mode 100644 index 0000000..0994067 --- /dev/null +++ b/service/api/search.go @@ -0,0 +1,53 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/notherealmarco/WASAPhoto/service/api/authorization" + "github.com/notherealmarco/WASAPhoto/service/api/helpers" + "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" +) + +func (rt *_router) GetSearchUsers(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + // We require user to be authenticated + if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) { + return + } + + // Get search query + query := r.URL.Query().Get("query") + + if query == "" { + helpers.SendBadRequest(w, "Missing query parameter", rt.baseLogger) + return + } + + // Get start index and limit, or their default values + start_index, limit, err := helpers.GetLimits(r.URL.Query()) + + if err != nil { + helpers.SendBadRequest(w, "Invalid start_index or limit value", rt.baseLogger) + return + } + + // Get search results + results, err := rt.db.SearchByName(query, ctx.Auth.GetUserID(), start_index, limit) + + if err != nil { + helpers.SendInternalError(err, "Database error: SearchByName", w, rt.baseLogger) + return + } + + // Return the results in json format + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(results) + + if err != nil { + helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger) + return + } +} diff --git a/service/database/database.go b/service/database/database.go index 84ec00d..c9f1607 100644 --- a/service/database/database.go +++ b/service/database/database.go @@ -44,10 +44,12 @@ type AppDatabase interface { UserExists(uid string) (bool, error) GetUserID(name string) (string, error) + SearchByName(name string, requesting_uid string, start_index int, limit int) (*[]structures.UIDName, error) + UpdateUsername(uid, name string) error - GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) // todo: maybe use a pointer to a slice? - GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error) + GetUserFollowers(uid string, requesting_uid string) (QueryResult, *[]structures.UIDName, error) // todo: maybe use a pointer to a slice? + GetUserFollowing(uid string, requesting_uid string) (QueryResult, *[]structures.UIDName, error) FollowUser(uid string, follow string) (QueryResult, error) UnfollowUser(uid string, unfollow string) (QueryResult, error) @@ -58,13 +60,14 @@ type AppDatabase interface { PostPhoto(uid string) (DBTransaction, int64, error) DeletePhoto(uid string, photo int64) (bool, error) - GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) + GetPhotoLikes(uid string, photo int64, requesting_uid string) (QueryResult, *[]structures.UIDName, error) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) GetUserProfile(uid string) (QueryResult, *structures.UserProfile, error) + GetUserStream(uid string, start_index int, limit int) (*[]structures.Photo, error) - GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) + GetComments(uid string, photo_id int64, requesting_uid string) (QueryResult, *[]structures.Comment, error) PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error) DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error) GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) diff --git a/service/database/db-comments.go b/service/database/db-comments.go index 9e78009..4f2890c 100644 --- a/service/database/db-comments.go +++ b/service/database/db-comments.go @@ -81,7 +81,7 @@ func (db *appdbimpl) DeleteComment(uid string, photo_id int64, comment_id int64) return SUCCESS, nil } -func (db *appdbimpl) GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) { +func (db *appdbimpl) GetComments(uid string, photo_id int64, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.Comment, error) { // Check if the photo exists, as it exist but have no comments exists, err := db.photoExists(uid, photo_id) @@ -89,7 +89,15 @@ func (db *appdbimpl) GetComments(uid string, photo_id int64) (QueryResult, *[]st return ERR_NOT_FOUND, nil, err } - rows, err := db.c.Query(`SELECT "id", "user", "comment", "date" FROM "comments" WHERE "photo" = ?`, photo_id) + rows, err := db.c.Query(`SELECT "c"."id", "c"."user", "c"."comment", "c"."date" FROM "comments" AS "c" + WHERE "c"."photo" = ? + AND "c"."user" NOT IN ( + SELECT "bans"."user" FROM "bans" + WHERE "bans"."user" = ? + AND "bans"."ban" = "c"."user" + ) + OFFSET ? + LIMIT ?`, photo_id, requesting_uid, start_index, limit) if err != nil { return ERR_INTERNAL, nil, err diff --git a/service/database/db-likes.go b/service/database/db-likes.go index 859997e..164129e 100644 --- a/service/database/db-likes.go +++ b/service/database/db-likes.go @@ -6,7 +6,7 @@ import ( ) // Get the list of users who liked a photo -func (db *appdbimpl) GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) { +func (db *appdbimpl) GetPhotoLikes(uid string, photo int64, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.UIDName, error) { // Check if the photo exists, as it could exist but have no likes exists, err := db.photoExists(uid, photo) @@ -20,7 +20,14 @@ func (db *appdbimpl) GetPhotoLikes(uid string, photo int64) (QueryResult, *[]str rows, err := db.c.Query(`SELECT "users"."uid", "users"."name" FROM "likes", "users" WHERE "likes"."photo_id" = ? - AND "likes"."user" = "users"."uid"`, photo) + AND "likes"."user" NOT IN ( + SELECT "bans"."user" FROM "bans" + WHERE "bans"."user" = ? + AND "bans"."ban" = "likes"."user" + ) + AND "likes"."user" = "users"."uid" + OFFSET ? + LIMIT ?`, photo, requesting_uid, start_index, limit) if err != nil { return ERR_INTERNAL, nil, err } diff --git a/service/database/db-photos.go b/service/database/db-photos.go index f4c11f1..5ad16d0 100644 --- a/service/database/db-photos.go +++ b/service/database/db-photos.go @@ -2,8 +2,6 @@ package database import ( "time" - - "github.com/notherealmarco/WASAPhoto/service/structures" ) // Post a new photo @@ -44,41 +42,6 @@ func (db *appdbimpl) DeletePhoto(uid string, photo int64) (bool, error) { return rows > 0, nil } -func (db *appdbimpl) getUserPhotos(uid string) (*[]structures.Photo, error) { - - // Get photos - rows, err := db.c.Query(`SELECT "p"."user", "p"."id", "p"."date", - ( - SELECT COUNT(*) AS "likes" FROM "likes" AS "l" - WHERE "l"."photo_id" = "p"."id" - ), - ( - SELECT COUNT(*) AS "comments" FROM "comments" AS "c" - WHERE "c"."photo" = "p"."id" - ) - FROM "photos" AS "p" - WHERE "p"."user" = ?`, uid) - if err != nil { - // Return the error - return nil, err - } - - photos := make([]structures.Photo, 0) - - for rows.Next() { - // If there is a next row, we create an instance of Photo and add it to the slice - var photo structures.Photo - err = rows.Scan(&photo.UID, &photo.ID, &photo.Date, &photo.Likes, &photo.Comments) - if err != nil { - // Return the error - return nil, err - } - photos = append(photos, photo) - } - - return &photos, nil -} - // Check if a given photo owned by a given user exists func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) { diff --git a/service/database/db-profile.go b/service/database/db-profile.go index 7947aa2..35acb98 100644 --- a/service/database/db-profile.go +++ b/service/database/db-profile.go @@ -5,6 +5,8 @@ import ( "github.com/notherealmarco/WASAPhoto/service/structures" ) +//this should be changed, but we need to change OpenAPI first + // Get user profile, including username, followers, following, and photos func (db *appdbimpl) GetUserProfile(uid string) (QueryResult, *structures.UserProfile, error) { // Get user info @@ -43,21 +45,46 @@ func (db *appdbimpl) GetUserProfile(uid string) (QueryResult, *structures.UserPr return ERR_INTERNAL, nil, err } - // Convert []Photo to []UserPhoto - user_photos := make([]structures.UserPhoto, 0) - for _, photo := range *photos { - user_photos = append(user_photos, structures.UserPhoto{ - ID: photo.ID, - Likes: photo.Likes, - Comments: photo.Comments, - Date: photo.Date, - }) - } - return SUCCESS, &structures.UserProfile{ UID: uid, Name: name, Following: following, Followers: followers, - Photos: &user_photos}, nil + Photos: photos, + }, nil +} + +func (db *appdbimpl) getUserPhotos(uid string) (*[]structures.UserPhoto, error) { + + // Get photos + rows, err := db.c.Query(`SELECT "p"."id", "p"."date", + ( + SELECT COUNT(*) AS "likes" FROM "likes" AS "l" + WHERE "l"."photo_id" = "p"."id" + ), + ( + SELECT COUNT(*) AS "comments" FROM "comments" AS "c" + WHERE "c"."photo" = "p"."id" + ) + FROM "photos" AS "p" + WHERE "p"."user" = ?`, uid) + if err != nil { + // Return the error + return nil, err + } + + photos := make([]structures.UserPhoto, 0) + + for rows.Next() { + // If there is a next row, we create an instance of Photo and add it to the slice + var photo structures.UserPhoto + err = rows.Scan(&photo.ID, &photo.Date, &photo.Likes, &photo.Comments) + if err != nil { + // Return the error + return nil, err + } + photos = append(photos, photo) + } + + return &photos, nil } diff --git a/service/database/db-stream.go b/service/database/db-stream.go new file mode 100644 index 0000000..4d7ab8d --- /dev/null +++ b/service/database/db-stream.go @@ -0,0 +1,50 @@ +package database + +import ( + "github.com/notherealmarco/WASAPhoto/service/structures" +) + +// Get user stream +// todo implement stidx + offset +func (db *appdbimpl) GetUserStream(uid string, start_index int, limit int) (*[]structures.Photo, error) { + + // Get photos + rows, err := db.c.Query(`SELECT "p"."user", "p"."id", "p"."date", + ( + SELECT COUNT(*) AS "likes" FROM "likes" AS "l" + WHERE "l"."photo_id" = "p"."id" + ), + ( + SELECT COUNT(*) AS "comments" FROM "comments" AS "c" + WHERE "c"."photo" = "p"."id" + ) + FROM "photos" AS "p" + WHERE "p"."user" IN ( + SELECT "followed" FROM "follows" WHERE "follower" = ? + ) + AND "p"."user" NOT IN ( + SELECT "user" FROM "bans" WHERE "ban" = ? + ) + ORDER BY "p"."date" DESC + OFFSET ? + LIMIT ?`, uid, uid, start_index, limit) + if err != nil { + // Return the error + return nil, err + } + + photos := make([]structures.Photo, 0) + + for rows.Next() { + // If there is a next row, we create an instance of Photo and add it to the slice + var photo structures.Photo + err = rows.Scan(&photo.UID, &photo.ID, &photo.Date, &photo.Likes, &photo.Comments) + if err != nil { + // Return the error + return nil, err + } + photos = append(photos, photo) + } + + return &photos, nil +} diff --git a/service/database/db-users.go b/service/database/db-users.go index c5526d4..5ef26a8 100644 --- a/service/database/db-users.go +++ b/service/database/db-users.go @@ -48,7 +48,7 @@ func (db *appdbimpl) UpdateUsername(uid string, name string) error { } // Get user followers -func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) { +func (db *appdbimpl) GetUserFollowers(uid string, requesting_uid string) (QueryResult, *[]structures.UIDName, error) { // user may exist but have no followers exists, err := db.UserExists(uid) @@ -63,7 +63,14 @@ func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UI rows, err := db.c.Query(`SELECT "follower", "user"."name" FROM "follows", "users" WHERE "follows"."follower" = "users"."uid" - AND "followed" = ?`, uid) + + AND "follows"."follower" NOT IN ( + SELECT "bans"."user" FROM "bans" + WHERE "bans"."user" = ? + AND "bans"."ban" = "follows"."follower" + ) + + AND "followed" = ?`, uid, requesting_uid) followers, err := db.uidNameQuery(rows, err) @@ -75,7 +82,7 @@ func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UI } // Get user following -func (db *appdbimpl) GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error) { +func (db *appdbimpl) GetUserFollowing(uid string, requesting_uid string) (QueryResult, *[]structures.UIDName, error) { // user may exist but have no followers exists, err := db.UserExists(uid) @@ -90,7 +97,14 @@ func (db *appdbimpl) GetUserFollowing(uid string) (QueryResult, *[]structures.UI rows, err := db.c.Query(`SELECT "followed", "user"."name" FROM "follows", "users" WHERE "follows"."followed" = "users"."uid" - AND "follower" = ?`, uid) + + AND "follows"."followed" NOT IN ( + SELECT "bans"."user" FROM "bans" + WHERE "bans"."user" = ? + AND "bans"."ban" = "follows"."followed" + ) + + AND "follower" = ?`, uid, requesting_uid) following, err := db.uidNameQuery(rows, err) @@ -216,3 +230,26 @@ func (db *appdbimpl) IsBanned(uid string, banner string) (bool, error) { return cnt > 0, nil } + +// Search by name +func (db *appdbimpl) SearchByName(name string, requesting_uid string, start_index int, limit int) (*[]structures.UIDName, error) { + + rows, err := db.c.Query(`SELECT "uid", "name" FROM "users" + WHERE "name" LIKE ? + + AND "uid" NOT IN ( + SELECT "bans"."user" FROM "bans" + WHERE "bans"."user" = "users"."uid" + AND "bans"."ban" = ? + ) + OFFSET ? + LIMIT ?`, name, requesting_uid, start_index, limit) + + users, err := db.uidNameQuery(rows, err) + + if err != nil { + return nil, err + } + + return users, nil +}