diff --git a/config/config.go b/config/config.go index 98798f01..ae7e4472 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ const ( CommentsTableName = "comments" UploadsOldTableName = "user_uploads_old" FilesTableName = "files" + NotificationTableName = "notifications" // for sukebei: //LastOldTorrentID = 2303945 diff --git a/db/gorm.go b/db/gorm.go index ebe2fd43..185c26f1 100644 --- a/db/gorm.go +++ b/db/gorm.go @@ -54,7 +54,7 @@ func GormInit(conf *config.Config, logger Logger) (*gorm.DB, error) { db.SetLogger(logger) } - db.AutoMigrate(&model.User{}, &model.UserFollows{}, &model.UserUploadsOld{}) + db.AutoMigrate(&model.User{}, &model.UserFollows{}, &model.UserUploadsOld{}, &model.Notification{}) if db.Error != nil { return db, db.Error } diff --git a/model/notification.go b/model/notification.go new file mode 100644 index 00000000..857df4ac --- /dev/null +++ b/model/notification.go @@ -0,0 +1,24 @@ +package model + +import ( + "github.com/NyaaPantsu/nyaa/config" +) + +type Notification struct { + ID uint + Content string + Read bool + Identifier string + Url string + UserID uint +// User *User `gorm:"AssociationForeignKey:UserID;ForeignKey:user_id"` // Don't think that we need it here +} + +func NewNotification(identifier string, c string, url string) Notification { + return Notification{Identifier: identifier, Content: c, Url: url} +} + +func (n *Notification) TableName() string { + return config.NotificationTableName +} + diff --git a/model/torrent.go b/model/torrent.go index 4358a0ab..361991e3 100644 --- a/model/torrent.go +++ b/model/torrent.go @@ -86,6 +86,10 @@ func (t Torrent) TableName() string { return config.TorrentsTableName } +func (t Torrent) Identifier() string { + return "torrent_"+strconv.Itoa(int(t.ID)) +} + func (t Torrent) IsNormal() bool { return t.Status == TorrentStatusNormal } diff --git a/model/user.go b/model/user.go index 6d98ca21..d6dc565c 100644 --- a/model/user.go +++ b/model/user.go @@ -26,13 +26,14 @@ type User struct { Language string `gorm:"column:language"` // TODO: move this to PublicUser - LikingCount int `json:"likingCount" gorm:"-"` - LikedCount int `json:"likedCount" gorm:"-"` Likings []User // Don't work `gorm:"foreignkey:user_id;associationforeignkey:follower_id;many2many:user_follows"` Liked []User // Don't work `gorm:"foreignkey:follower_id;associationforeignkey:user_id;many2many:user_follows"` MD5 string `json:"md5" gorm:"column:md5"` // Hash of email address, used for Gravatar Torrents []Torrent `gorm:"ForeignKey:UploaderID"` + + UnreadNotifications int `gorm:"-"` // We don't want to loop every notifications when accessing user unread notif + Notifications []Notification `gorm:"ForeignKey:UserID"` } type UserJSON struct { @@ -72,6 +73,17 @@ func (u User) IsModerator() bool { return u.Status == UserStatusModerator } +func (u User) GetUnreadNotifications() int { + if u.UnreadNotifications == 0 { + for _, notif := range u.Notifications { + if !notif.Read { + u.UnreadNotifications++ + } + } + } + return u.UnreadNotifications +} + type PublicUser struct { User *User } @@ -98,8 +110,8 @@ func (u *User) ToJSON() UserJSON { Username: u.Username, Status: u.Status, CreatedAt: u.CreatedAt.Format(time.RFC3339), - LikingCount: u.LikingCount, - LikedCount: u.LikedCount, + LikingCount: len(u.Likings), + LikedCount: len(u.Liked), } return json } diff --git a/router/router.go b/router/router.go index b38e4a27..1879e384 100755 --- a/router/router.go +++ b/router/router.go @@ -28,6 +28,7 @@ func init() { gzipUserProfileHandler := http.HandlerFunc(UserProfileHandler) gzipUserDetailsHandler := http.HandlerFunc(UserDetailsHandler) gzipUserProfileFormHandler := http.HandlerFunc(UserProfileFormHandler) + gzipUserNotificationsHandler := http.HandlerFunc(UserNotificationsHandler) gzipDumpsHandler := handlers.CompressHandler(dumpsHandler) gzipGpgKeyHandler := handlers.CompressHandler(gpgKeyHandler) gzipDatabaseDumpHandler := handlers.CompressHandler(http.HandlerFunc(DatabaseDumpHandler)) @@ -63,6 +64,7 @@ func init() { Router.HandleFunc("/user/{id}/{username}/follow", UserFollowHandler).Name("user_follow").Methods("GET") Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserDetailsHandler)).Name("user_profile_details").Methods("GET") Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserProfileFormHandler)).Name("user_profile_edit").Methods("POST") + Router.Handle("/user/notifications", wrapHandler(gzipUserNotificationsHandler)).Name("user_notifications") Router.HandleFunc("/user/{id}/{username}/feed", RSSHandler).Name("feed_user") Router.HandleFunc("/user/{id}/{username}/feed/{page}", RSSHandler).Name("feed_user_page") diff --git a/router/template.go b/router/template.go index f4e922c7..f64c10b1 100644 --- a/router/template.go +++ b/router/template.go @@ -20,6 +20,7 @@ var homeTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, + viewProfileNotifTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate, @@ -99,6 +100,11 @@ func ReloadTemplates() { name: "user_profile", file: filepath.Join("user", "profile.html"), }, + templateLoader{ + templ: &viewProfileNotifTemplate, + name: "user_profile", + file: filepath.Join("user", "profile_notifications.html"), + }, templateLoader{ templ: &viewProfileEditTemplate, name: "user_profile", diff --git a/router/template_variables.go b/router/template_variables.go index 530da521..c23f7cd6 100644 --- a/router/template_variables.go +++ b/router/template_variables.go @@ -106,6 +106,16 @@ type UserProfileVariables struct { Route *mux.Route // For getting current route in templates } +type UserProfileNotifVariables struct { + Infos map[string][]string + Search SearchForm + Navigation Navigation + T languages.TemplateTfunc + User *model.User + URL *url.URL // For parsing Url in templates + Route *mux.Route // For getting current route in templates +} + type HomeTemplateVariables struct { ListTorrents []model.TorrentJSON Search SearchForm diff --git a/router/upload_handler.go b/router/upload_handler.go index 162a0bfe..75e08f78 100644 --- a/router/upload_handler.go +++ b/router/upload_handler.go @@ -1,6 +1,7 @@ package router import ( + "fmt" "net/http" "strconv" "time" @@ -8,7 +9,9 @@ import ( "github.com/NyaaPantsu/nyaa/db" "github.com/NyaaPantsu/nyaa/model" "github.com/NyaaPantsu/nyaa/service/captcha" + "github.com/NyaaPantsu/nyaa/service/notifier" "github.com/NyaaPantsu/nyaa/service/upload" + "github.com/NyaaPantsu/nyaa/service/user" "github.com/NyaaPantsu/nyaa/service/user/permission" "github.com/NyaaPantsu/nyaa/util/languages" msg "github.com/NyaaPantsu/nyaa/util/messages" @@ -75,6 +78,20 @@ func UploadPostHandler(w http.ResponseWriter, r *http.Request) { UploaderID: user.ID} db.ORM.Create(&torrent) + url, err := Router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10)) + + if (user.ID > 0) { // If we are a member + userService.GetLikings(user) // We populate the liked field for users + if len(user.Likings) > 0 { // If we are followed by at least someone + for _, follower := range user.Likings { + T, _, _ := languages.TfuncAndLanguageWithFallback(user.Language, user.Language) // We need to send the notification to every user in their language + + notifierService.NotifyUser(&follower, torrent.Identifier(), fmt.Sprintf(T("new_torrent_uploaded"), torrent.Name, user.Username), url.String()) + + } + } + } + // add filelist to files db, if we have one if len(uploadForm.FileList) > 0 { for _, uploadedFile := range uploadForm.FileList { @@ -87,7 +104,6 @@ func UploadPostHandler(w http.ResponseWriter, r *http.Request) { } } - url, err := Router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/router/user_handler.go b/router/user_handler.go index 0e50da24..57ee1afc 100644 --- a/router/user_handler.go +++ b/router/user_handler.go @@ -7,10 +7,12 @@ import ( "github.com/NyaaPantsu/nyaa/model" "github.com/NyaaPantsu/nyaa/service/captcha" + "github.com/NyaaPantsu/nyaa/service/notifier" "github.com/NyaaPantsu/nyaa/service/user" "github.com/NyaaPantsu/nyaa/service/user/form" "github.com/NyaaPantsu/nyaa/service/user/permission" "github.com/NyaaPantsu/nyaa/util/languages" + msg "github.com/NyaaPantsu/nyaa/util/messages" "github.com/NyaaPantsu/nyaa/util/modelHelper" "github.com/gorilla/mux" ) @@ -142,7 +144,7 @@ func UserProfileFormHandler(w http.ResponseWriter, r *http.Request) { id := vars["id"] currentUser := GetUser(r) userProfile, _, errorUser := userService.RetrieveUserForAdmin(id) - if errorUser != nil || !userPermission.CurrentOrAdmin(currentUser, userProfile.ID) { + if errorUser != nil || !userPermission.CurrentOrAdmin(currentUser, userProfile.ID) || userProfile.ID == 0 { NotFoundHandler(w, r) return } @@ -307,7 +309,7 @@ func UserFollowHandler(w http.ResponseWriter, r *http.Request) { id := vars["id"] currentUser := GetUser(r) user, _, errorUser := userService.RetrieveUserForAdmin(id) - if errorUser == nil { + if errorUser == nil && user.ID > 0 { if !userPermission.IsFollower(&user, currentUser) { followAction = "followed" userService.SetFollow(&user, currentUser) @@ -319,3 +321,24 @@ func UserFollowHandler(w http.ResponseWriter, r *http.Request) { url, _ := Router.Get("user_profile").URL("id", strconv.Itoa(int(user.ID)), "username", user.Username) http.Redirect(w, r, url.String()+"?"+followAction, http.StatusSeeOther) } + +func UserNotificationsHandler(w http.ResponseWriter, r *http.Request) { + currentUser := GetUser(r) + if currentUser.ID > 0 { + messages := msg.GetMessages(r) + Ts, _ := languages.GetTfuncAndLanguageFromRequest(r) + T := languages.GetTfuncFromRequest(r) + if r.URL.Query()["clear"] != nil { + notifierService.DeleteAllNotifications(currentUser.ID) + messages.AddInfo("infos", Ts("notifications_cleared")) + currentUser.Notifications = []model.Notification{} + } + htv := UserProfileNotifVariables{messages.GetAllInfos(), NewSearchForm(), NewNavigation(), T, currentUser, r.URL, mux.CurrentRoute(r)} + err := viewProfileNotifTemplate.ExecuteTemplate(w, "index.html", htv) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + NotFoundHandler(w, r) + } +} \ No newline at end of file diff --git a/router/view_torrent_handler.go b/router/view_torrent_handler.go index bd7e3a42..4eaa3b1a 100644 --- a/router/view_torrent_handler.go +++ b/router/view_torrent_handler.go @@ -9,6 +9,7 @@ import ( "github.com/NyaaPantsu/nyaa/db" "github.com/NyaaPantsu/nyaa/model" "github.com/NyaaPantsu/nyaa/service/captcha" + "github.com/NyaaPantsu/nyaa/service/notifier" "github.com/NyaaPantsu/nyaa/service/torrent" "github.com/NyaaPantsu/nyaa/service/user/permission" "github.com/NyaaPantsu/nyaa/util" @@ -22,19 +23,24 @@ func ViewHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] messages := msg.GetMessages(r) + user := GetUser(r) if (r.URL.Query()["success"] != nil) { messages.AddInfo("infos", "Torrent uploaded successfully!") } torrent, err := torrentService.GetTorrentById(id) + + if (r.URL.Query()["notif"] != nil) { + notifierService.ToggleReadNotification(torrent.Identifier(), user.ID) + } + if err != nil { NotFoundHandler(w, r) return } b := torrent.ToJSON() captchaID := "" - user := GetUser(r) if userPermission.NeedsCaptcha(user) { captchaID = captcha.GetID() } diff --git a/service/notifier/notifier.go b/service/notifier/notifier.go new file mode 100644 index 00000000..cd930d24 --- /dev/null +++ b/service/notifier/notifier.go @@ -0,0 +1,24 @@ +package notifierService + +import ( + "github.com/NyaaPantsu/nyaa/db" + "github.com/NyaaPantsu/nyaa/model" +) + + +func NotifyUser(user *model.User, name string, msg string, url string) { + if (user.ID > 0) { + notification := model.NewNotification(name, msg, url) + notification.UserID = user.ID + db.ORM.Create(¬ification) + // TODO: Email notification + } +} + +func ToggleReadNotification(identifier string, id uint) { // + db.ORM.Model(&model.Notification{}).Where("identifier = ? AND user_id = ?", identifier, id).Updates(model.Notification{Read: true}) +} + +func DeleteAllNotifications(id uint) { // + db.ORM.Where("user_id = ?", id).Delete(&model.Notification{}) +} \ No newline at end of file diff --git a/service/user/cookie_helper.go b/service/user/cookie_helper.go index 3c092b36..b4d7f640 100644 --- a/service/user/cookie_helper.go +++ b/service/user/cookie_helper.go @@ -141,7 +141,7 @@ func CurrentUser(r *http.Request) (model.User, error) { if userFromContext.ID > 0 && user_id == userFromContext.ID { user = userFromContext } else { - if db.ORM.Where("user_id = ?", user_id).First(&user).RecordNotFound() { + if db.ORM.Preload("Notifications").Where("user_id = ?", user_id).First(&user).RecordNotFound() { // We only load unread notifications return user, errors.New("User not found") } else { setUserToContext(r, user) diff --git a/service/user/user.go b/service/user/user.go index dd8e445c..54b7b058 100644 --- a/service/user/user.go +++ b/service/user/user.go @@ -280,7 +280,7 @@ func RetrieveOldUploadsByUsername(username string) ([]uint, error) { // RetrieveUserForAdmin retrieves a user for an administrator. func RetrieveUserForAdmin(id string) (model.User, int, error) { var user model.User - if db.ORM.Preload("Torrents").Last(&user, id).RecordNotFound() { + if db.ORM.Preload("Notifications").Preload("Torrents").Last(&user, id).RecordNotFound() { return user, http.StatusNotFound, errors.New("user not found") } var liked, likings []model.User @@ -300,6 +300,19 @@ func RetrieveUsersForAdmin(limit int, offset int) ([]model.User, int) { return users, nbUsers } +func GetLiked(user *model.User) *model.User { + var liked []model.User + db.ORM.Joins("JOIN user_follows on user_follows.following=?", user.ID).Where("users.user_id = user_follows.user_id").Group("users.user_id").Find(&liked) + user.Liked = liked + return user +} +func GetLikings(user *model.User) *model.User { + var likings []model.User + db.ORM.Joins("JOIN user_follows on user_follows.user_id=?", user.ID).Where("users.user_id = user_follows.following").Group("users.user_id").Find(&likings) + user.Likings = likings + return user +} + // CreateUserAuthentication creates user authentication. func CreateUserAuthentication(w http.ResponseWriter, r *http.Request) (int, error) { var form formStruct.LoginForm diff --git a/templates/_badgemenu.html b/templates/_badgemenu.html index 9ba9fcf6..0995cb0a 100644 --- a/templates/_badgemenu.html +++ b/templates/_badgemenu.html @@ -4,12 +4,13 @@ {{if gt $.User.ID 0 }} + {{ if CurrentUserIdentical $.User .ID }} +
  • + {{ call $.T "my_notifications"}} +
  • + {{end}} {{if CurrentOrAdmin $.User .ID }}
  • {{call $.T "settings"}} diff --git a/templates/user/profile_edit.html b/templates/user/profile_edit.html index 290482ac..182f65ca 100755 --- a/templates/user/profile_edit.html +++ b/templates/user/profile_edit.html @@ -41,6 +41,11 @@ {{call $.T "torrents"}}
  • {{if gt $.User.ID 0 }} + {{ if CurrentUserIdentical $.User .ID }} +
  • + {{ call $.T "my_notifications"}} +
  • + {{end}} {{if CurrentOrAdmin $.User .ID }}
  • {{call $.T "settings"}} diff --git a/templates/user/profile_notifications.html b/templates/user/profile_notifications.html new file mode 100644 index 00000000..e8114281 --- /dev/null +++ b/templates/user/profile_notifications.html @@ -0,0 +1,54 @@ +{{define "title"}}{{ call $.T "profile_edit_page" .User.Username }}{{end}} +{{define "contclass"}}cont-view{{end}} +{{define "content"}} +
    +{{with .User }} +
    +
    + +
    + {{.Username}} +
    + + +
    +
    + {{.Username}} +
    +
    + {{GetRole . }} +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    +{{end}} +
    +
    + {{ block "profile_notifications_content" . }}{{end}} +
    +
    +
    +{{end}} diff --git a/translations/en-us.all.json b/translations/en-us.all.json index 064170b3..8bd2d54d 100644 --- a/translations/en-us.all.json +++ b/translations/en-us.all.json @@ -758,5 +758,21 @@ { "id": "no_database_dumps_available", "translation": "No database dumps are available at this moment." + }, + { + "id": "clear_notifications", + "translation": "Clear Notifications" + }, + { + "id": "notifications_cleared", + "translation": "Notifications erased!" + }, + { + "id": "my_notifications", + "translation": "My Notifications" + }, + { + "id": "new_torrent_uploaded", + "translation": "New torrent: \"%s\" from %s" } ]