package api import ( "fmt" "net/http" "strconv" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/session" ) // Likes API posts a new like on something. func Likes() http.HandlerFunc { // Request JSON schema. type Request struct { TableName string `json:"name"` TableID string `json:"id"` Unlike bool `json:"unlike,omitempty"` Referrer string `json:"page"` } // Response JSON schema. type Response struct { OK bool `json:"OK"` Error string `json:"error,omitempty"` Likes int64 `json:"likes"` } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { SendJSON(w, http.StatusNotAcceptable, Response{ Error: "POST method only", }) return } // Get the current user. currentUser, err := session.CurrentUser(r) if err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: "Couldn't get current user!", }) return } // Parse request payload. var req Request if err := ParseJSON(r, &req); err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: fmt.Sprintf("Error with request payload: %s", err), }) return } // Sanity check things. The page= param (Referrer) must be a relative URL, the path // is useful for "liked your comment" notifications to supply the Link URL for the // notification. if len(req.Referrer) > 0 && req.Referrer[0] != '/' { req.Referrer = "" } // The link to attach to the notification. var linkTo = req.Referrer // Is the ID an integer? var tableID uint64 if v, err := strconv.Atoi(req.TableID); err != nil { // Non-integer must be usernames? if req.TableName == "users" { user, err := models.FindUser(req.TableID) if err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: "User not found.", }) return } tableID = user.ID } else { SendJSON(w, http.StatusBadRequest, Response{ Error: "Invalid ID.", }) return } } else { tableID = uint64(v) } // Who do we notify about this like? var ( targetUser *models.User notificationMessage string ) switch req.TableName { case "photos": if photo, err := models.GetPhoto(tableID); err == nil { if user, err := models.GetUser(photo.UserID); err == nil { // Admin safety check: in case the admin clicked 'Like' on a friends-only or private // picture they shouldn't have been expected to see, do not log a like. if currentUser.IsAdmin && currentUser.ID != user.ID { if (photo.Visibility == models.PhotoFriends && !models.AreFriends(user.ID, currentUser.ID)) || (photo.Visibility == models.PhotoPrivate && !models.IsPrivateUnlocked(user.ID, currentUser.ID)) { SendJSON(w, http.StatusForbidden, Response{ Error: "You are not allowed to like that photo.", }) return } } // Blocking safety check: if either user blocks the other, liking is not allowed. if models.IsBlocking(currentUser.ID, user.ID) { SendJSON(w, http.StatusForbidden, Response{ Error: "You are not allowed to like that photo.", }) return } targetUser = user } } else { log.Error("For like on photos table: didn't find photo %d: %s", tableID, err) } case "users": log.Error("subject is users, find %d", tableID) if user, err := models.GetUser(tableID); err == nil { targetUser = user log.Warn("found user %s", targetUser.Username) // Blocking safety check: if either user blocks the other, liking is not allowed. if models.IsBlocking(currentUser.ID, user.ID) { SendJSON(w, http.StatusForbidden, Response{ Error: "You are not allowed to like that profile.", }) return } } else { log.Error("For like on users table: didn't find user %d: %s", tableID, err) } case "comments": log.Error("subject is comments, find %d", tableID) if comment, err := models.GetComment(tableID); err == nil { targetUser = &comment.User notificationMessage = comment.Message // Set the notification link to the /go/comment route. linkTo = fmt.Sprintf("/go/comment?id=%d", comment.ID) // Blocking safety check: if either user blocks the other, liking is not allowed. if models.IsBlocking(currentUser.ID, targetUser.ID) { SendJSON(w, http.StatusForbidden, Response{ Error: "You are not allowed to like that comment.", }) return } } else { log.Error("For like on users table: didn't find user %d: %s", tableID, err) } } // Is the table likeable? if _, ok := models.LikeableTables[req.TableName]; !ok { SendJSON(w, http.StatusBadRequest, Response{ Error: fmt.Sprintf("Can't like table %s: not allowed.", req.TableName), }) return } // Put in a like. if req.Unlike { if err := models.Unlike(currentUser, req.TableName, tableID); err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: fmt.Sprintf("Error unliking: %s", err), }) return } // Remove the target's notification about this like. models.RemoveSpecificNotification(targetUser.ID, models.NotificationLike, req.TableName, tableID) } else { if err := models.AddLike(currentUser, req.TableName, tableID); err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: fmt.Sprintf("Error liking: %s", err), }) return } // Notify the recipient of the like. log.Info("Added like on %s:%d, notifying owner %+v", req.TableName, tableID, targetUser) if targetUser != nil && !targetUser.NotificationOptOut(config.NotificationOptOutLikes) { notif := &models.Notification{ UserID: targetUser.ID, AboutUser: *currentUser, Type: models.NotificationLike, TableName: req.TableName, TableID: tableID, Message: notificationMessage, Link: linkTo, } if err := models.CreateNotification(notif); err != nil { log.Error("Couldn't create Likes notification: %s", err) } } } // Send success response. SendJSON(w, http.StatusOK, Response{ OK: true, Likes: models.CountLikes(req.TableName, tableID), }) }) } // WhoLikes API checks who liked something. func WhoLikes() http.HandlerFunc { // Response JSON schema. type Liker struct { Username string `json:"username"` Avatar string `json:"avatar"` Relationship models.UserRelationship `json:"relationship"` } type Response struct { OK bool `json:"OK"` Error string `json:"error,omitempty"` Likes []Liker `json:"likes,omitempty"` Pager *models.Pagination `json:"pager,omitempty"` Pages int `json:"pages,omitempty"` } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { SendJSON(w, http.StatusNotAcceptable, Response{ Error: "GET method only", }) return } // Parse request parameters. var ( tableName = r.FormValue("table_name") tableID, _ = strconv.Atoi(r.FormValue("table_id")) page, _ = strconv.Atoi(r.FormValue("page")) ) if tableName == "" { SendJSON(w, http.StatusBadRequest, Response{ Error: "Missing required table_name", }) return } else if tableID == 0 { SendJSON(w, http.StatusBadRequest, Response{ Error: "Missing required table_id", }) return } if page < 1 { page = 1 } // Get the current user. currentUser, err := session.CurrentUser(r) if err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: "Couldn't get current user!", }) return } // Get a page of users who've liked this. var pager = &models.Pagination{ Page: page, PerPage: config.PageSizeLikeList, Sort: "created_at desc", } users, err := models.PaginateLikes(currentUser, tableName, uint64(tableID), pager) if err != nil { SendJSON(w, http.StatusInternalServerError, Response{ Error: fmt.Sprintf("Error getting likes: %s", err), }) return } // Map user data to just the essentials for front-end. var result = []Liker{} for _, user := range users { result = append(result, Liker{ Username: user.Username, Avatar: user.VisibleAvatarURL(currentUser), Relationship: user.UserRelationship, }) } // Send success response. SendJSON(w, http.StatusOK, Response{ OK: true, Likes: result, Pager: pager, Pages: pager.Pages(), }) }) }