Add stream search, offset & limit, improved bans

This commit is contained in:
Marco Realacci 2022-11-22 15:17:08 +01:00
parent 72f11a2b15
commit 963e392cea
14 changed files with 298 additions and 65 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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

48
service/api/get-stream.go Normal file
View file

@ -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
}
}

View file

@ -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
}

View file

@ -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)

53
service/api/search.go Normal file
View file

@ -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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}