2022-08-25 04:17:34 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2023-09-14 04:28:38 +00:00
|
|
|
"strconv"
|
2022-08-25 04:17:34 +00:00
|
|
|
|
2023-09-14 04:28:38 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
2022-08-26 04:21:46 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/models"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
2022-08-25 04:17:34 +00:00
|
|
|
)
|
|
|
|
|
2023-09-14 04:28:38 +00:00
|
|
|
// Likes API posts a new like on something.
|
2022-08-25 04:17:34 +00:00
|
|
|
func Likes() http.HandlerFunc {
|
|
|
|
// Request JSON schema.
|
|
|
|
type Request struct {
|
|
|
|
TableName string `json:"name"`
|
2023-10-24 04:55:55 +00:00
|
|
|
TableID string `json:"id"`
|
2022-08-25 04:17:34 +00:00
|
|
|
Unlike bool `json:"unlike,omitempty"`
|
2022-08-30 03:00:15 +00:00
|
|
|
Referrer string `json:"page"`
|
2022-08-25 04:17:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-08-30 03:00:15 +00:00
|
|
|
// 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 = ""
|
|
|
|
}
|
|
|
|
|
2024-01-07 00:44:05 +00:00
|
|
|
// The link to attach to the notification.
|
|
|
|
var linkTo = req.Referrer
|
|
|
|
|
2023-10-24 04:55:55 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2022-08-25 04:17:34 +00:00
|
|
|
// Who do we notify about this like?
|
2022-08-30 03:00:15 +00:00
|
|
|
var (
|
|
|
|
targetUser *models.User
|
|
|
|
notificationMessage string
|
|
|
|
)
|
2022-08-25 04:17:34 +00:00
|
|
|
switch req.TableName {
|
|
|
|
case "photos":
|
2023-10-24 04:55:55 +00:00
|
|
|
if photo, err := models.GetPhoto(tableID); err == nil {
|
2022-08-25 04:17:34 +00:00
|
|
|
if user, err := models.GetUser(photo.UserID); err == nil {
|
2023-07-19 05:30:29 +00:00
|
|
|
// 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.
|
2023-09-14 04:28:38 +00:00
|
|
|
if currentUser.IsAdmin && currentUser.ID != user.ID {
|
2023-07-19 05:30:29 +00:00
|
|
|
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{
|
2023-09-14 04:28:38 +00:00
|
|
|
Error: "You are not allowed to like that photo.",
|
2023-07-19 05:30:29 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2023-09-17 06:07:32 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2022-08-25 04:17:34 +00:00
|
|
|
targetUser = user
|
|
|
|
}
|
|
|
|
} else {
|
2023-10-24 04:55:55 +00:00
|
|
|
log.Error("For like on photos table: didn't find photo %d: %s", tableID, err)
|
2022-08-25 04:17:34 +00:00
|
|
|
}
|
|
|
|
case "users":
|
2023-10-24 04:55:55 +00:00
|
|
|
log.Error("subject is users, find %d", tableID)
|
|
|
|
if user, err := models.GetUser(tableID); err == nil {
|
2022-08-25 04:17:34 +00:00
|
|
|
targetUser = user
|
|
|
|
log.Warn("found user %s", targetUser.Username)
|
2023-09-17 06:07:32 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2022-08-25 04:17:34 +00:00
|
|
|
} else {
|
2023-10-24 04:55:55 +00:00
|
|
|
log.Error("For like on users table: didn't find user %d: %s", tableID, err)
|
2022-08-25 04:17:34 +00:00
|
|
|
}
|
2022-08-30 03:00:15 +00:00
|
|
|
case "comments":
|
2023-10-24 04:55:55 +00:00
|
|
|
log.Error("subject is comments, find %d", tableID)
|
|
|
|
if comment, err := models.GetComment(tableID); err == nil {
|
2022-08-30 03:00:15 +00:00
|
|
|
targetUser = &comment.User
|
|
|
|
notificationMessage = comment.Message
|
2024-01-07 00:44:05 +00:00
|
|
|
|
|
|
|
// Set the notification link to the /go/comment route.
|
|
|
|
linkTo = fmt.Sprintf("/go/comment?id=%d", comment.ID)
|
2023-09-17 06:07:32 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2022-08-30 03:00:15 +00:00
|
|
|
} else {
|
2023-10-24 04:55:55 +00:00
|
|
|
log.Error("For like on users table: didn't find user %d: %s", tableID, err)
|
2022-08-30 03:00:15 +00:00
|
|
|
}
|
2022-08-25 04:17:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2023-10-24 04:55:55 +00:00
|
|
|
if err := models.Unlike(currentUser, req.TableName, tableID); err != nil {
|
2022-08-25 04:17:34 +00:00
|
|
|
SendJSON(w, http.StatusBadRequest, Response{
|
|
|
|
Error: fmt.Sprintf("Error unliking: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the target's notification about this like.
|
2023-10-24 04:55:55 +00:00
|
|
|
models.RemoveSpecificNotification(targetUser.ID, models.NotificationLike, req.TableName, tableID)
|
2022-08-25 04:17:34 +00:00
|
|
|
} else {
|
2023-10-24 04:55:55 +00:00
|
|
|
if err := models.AddLike(currentUser, req.TableName, tableID); err != nil {
|
2022-08-25 04:17:34 +00:00
|
|
|
SendJSON(w, http.StatusBadRequest, Response{
|
|
|
|
Error: fmt.Sprintf("Error liking: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Notify the recipient of the like.
|
2023-10-24 04:55:55 +00:00
|
|
|
log.Info("Added like on %s:%d, notifying owner %+v", req.TableName, tableID, targetUser)
|
2023-10-28 21:34:35 +00:00
|
|
|
if targetUser != nil && !targetUser.NotificationOptOut(config.NotificationOptOutLikes) {
|
2022-08-25 04:17:34 +00:00
|
|
|
notif := &models.Notification{
|
|
|
|
UserID: targetUser.ID,
|
2022-08-26 03:36:59 +00:00
|
|
|
AboutUser: *currentUser,
|
2022-08-25 04:17:34 +00:00
|
|
|
Type: models.NotificationLike,
|
|
|
|
TableName: req.TableName,
|
2023-10-24 04:55:55 +00:00
|
|
|
TableID: tableID,
|
2022-08-30 03:00:15 +00:00
|
|
|
Message: notificationMessage,
|
2024-01-07 00:44:05 +00:00
|
|
|
Link: linkTo,
|
2022-08-25 04:17:34 +00:00
|
|
|
}
|
|
|
|
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,
|
2023-10-24 04:55:55 +00:00
|
|
|
Likes: models.CountLikes(req.TableName, tableID),
|
2022-08-25 04:17:34 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2023-09-14 04:28:38 +00:00
|
|
|
|
|
|
|
// 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(),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|