Albirew/nyaa-pantsu
Archivé
1
0
Bifurcation 0

Merge pull request #657 from NyaaPantsu/notifications

Notification for Users
Cette révision appartient à :
akuma06 2017-05-21 01:25:25 +02:00 révisé par GitHub
révision 328032fe0e
20 fichiers modifiés avec 255 ajouts et 13 suppressions

Voir le fichier

@ -18,6 +18,7 @@ const (
CommentsTableName = "comments"
UploadsOldTableName = "user_uploads_old"
FilesTableName = "files"
NotificationTableName = "notifications"
// for sukebei:
//LastOldTorrentID = 2303945

Voir le fichier

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

24
model/notification.go Fichier normal
Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

24
service/notifier/notifier.go Fichier normal
Voir le fichier

@ -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(&notification)
// 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{})
}

Voir le fichier

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

Voir le fichier

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

Voir le fichier

@ -4,12 +4,13 @@
<li class="dropdown">
{{if gt .ID 0}}
<a href="{{ genRoute "user_profile" "id" (print .ID) "username" .Username }}" class="dropdown-toggle profile-image" data-toggle="dropdown">
<img src="https://www.gravatar.com/avatar/{{ .MD5 }}?s=50" class="img-circle special-img"> {{ .Username }} <b class="caret"></b></a>
<img src="https://www.gravatar.com/avatar/{{ .MD5 }}?s=50" class="img-circle special-img"> {{ .Username }} <span class="badge">{{ .GetUnreadNotifications }}</span><b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{{ genRoute "user_profile" "id" (print .ID) "username" .Username }}"><i class="fa fa-cog"></i> {{call $.T "profile"}}</a></li>
<li><a href="{{ genRoute "user_notifications" }}"><i class="fa fa-cog"></i> {{ call $.T "my_notifications"}} <span class="badge">{{ .GetUnreadNotifications }}</span></a></li>
<li><a href="{{ genRoute "user_profile_edit" "id" (print .ID) "username" .Username }}"><i class="fa fa-cog"></i> {{call $.T "settings"}}</a></li>
{{if HasAdmin . }}<li><a href="{{ genRoute "mod_index" }}"><i class="fa fa-cog"></i> {{call $.T "moderation"}}</a></li>{{end}}
<li class="divider"></li>
{{if HasAdmin . }}<li><a href="{{ genRoute "mod_index" }}"><i class="fa fa-cog"></i> {{call $.T "moderation"}}</a></li><li class="divider"></li>{{end}}
<li><a href="{{ genRoute "user_logout" }}"><i class="fa fa-sign-out"></i> {{ call $.T "sign_out"}}</a></li>
</ul>
{{ else }}

Voir le fichier

@ -0,0 +1,20 @@
{{define "profile_notifications_content"}}
{{with .User}}
<div style="padding: 0.5rem; background: grey;">
<a href="{{ genRoute "user_notifications" }}?clear" class="btn btn-danger pull-right"><i class="glyphicon glyphicon-trash"></i> {{ call $.T "clear_notifications" }}</a>
<div style="clear: both;"></div>
</div>
{{ range (index $.Infos "infos")}}
<div class="alert alert-info"><a class="panel-close close" data-dismiss="alert">×</a><i class="glyphicon glyphicon-info-sign"></i> {{ . }}</div>
{{end}}
<table class="table table-hover table-striped">
{{ range .Notifications }}
<tr><td>
<div style="padding: 0 1rem 1rem;">
<a href="{{ .Url }}?notif"><h3><i class="glyphicon glyphicon-bell profile-usertitle-name" ></i>{{ .Content }}</h3></a>
</div>
</td></tr>
{{end}}
</table>
{{end}}
{{end}}

Voir le fichier

@ -44,6 +44,11 @@
<a href="{{ genRoute "user_profile" "id" ( print .ID ) "username" .Username }}"><i class="glyphicon glyphicon-home"></i>{{call $.T "torrents"}}</a>
</li>
{{if gt $.User.ID 0 }}
{{ if CurrentUserIdentical $.User .ID }}
<li>
<a href="{{ genRoute "user_notifications" "id" (print .ID) "username" .Username }}"><i class="glyphicon glyphicon-bell"></i>{{ call $.T "my_notifications"}}</a>
</li>
{{end}}
{{if CurrentOrAdmin $.User .ID }}
<li>
<a href="{{ genRoute "user_profile_details" "id" ( print .ID ) "username" .Username }}"><i class="glyphicon glyphicon-user"></i>{{call $.T "settings"}}</a>

Voir le fichier

@ -41,6 +41,11 @@
<a href="{{ genRoute "user_profile" "id" (print .ID) "username" .Username }}"><i class="glyphicon glyphicon-home"></i>{{call $.T "torrents"}}</a>
</li>
{{if gt $.User.ID 0 }}
{{ if CurrentUserIdentical $.User .ID }}
<li>
<a href="{{ genRoute "user_notifications" "id" (print .ID) "username" .Username }}"><i class="glyphicon glyphicon-bell"></i>{{ call $.T "my_notifications"}}</a>
</li>
{{end}}
{{if CurrentOrAdmin $.User .ID }}
<li class="active">
<a href="{{ genRoute "user_profile_edit" "id" (print .ID) "username" .Username }}"><i class="glyphicon glyphicon-user"></i>{{call $.T "settings"}}</a>

Voir le fichier

@ -0,0 +1,54 @@
{{define "title"}}{{ call $.T "profile_edit_page" .User.Username }}{{end}}
{{define "contclass"}}cont-view{{end}}
{{define "content"}}
<div class="row profile">
{{with .User }}
<div class="col-md-3">
<div class="profile-sidebar">
<!-- SIDEBAR USERPIC -->
<div class="profile-userpic">
<img src="{{ getAvatar .MD5 130 }}" class="img-responsive" alt="{{.Username}}">
</div>
<!-- END SIDEBAR USERPIC -->
<!-- SIDEBAR USER TITLE -->
<div class="profile-usertitle">
<div class="profile-usertitle-name">
{{.Username}}
</div>
<div class="profile-usertitle-job">
{{GetRole . }}
</div>
</div>
<!-- END SIDEBAR USER TITLE -->
<!-- SIDEBAR BUTTONS -->
<div class="profile-userbuttons">
<!-- <button type="button" class="btn btn-danger btn-sm">Message</button> -->
</div>
<!-- END SIDEBAR BUTTONS -->
<!-- SIDEBAR MENU -->
<div class="profile-usermenu">
<ul class="nav">
<li>
<a href="{{ genRoute "user_profile" "id" (print .ID) "username" .Username }}"><i class="glyphicon glyphicon-home"></i>{{call $.T "torrents"}}</a>
</li>
{{if gt .ID 0 }}
<li class="active">
<a href="{{ genRoute "user_notifications" }}"><i class="glyphicon glyphicon-bell"></i>{{call $.T "my_notifications"}}</a>
</li>
<li>
<a href="{{ genRoute "user_profile_edit" "id" (print .ID) "username" .Username }}"><i class="glyphicon glyphicon-user"></i>{{call $.T "settings"}}</a>
</li>
{{end}}
</ul>
</div>
<!-- END MENU -->
</div>
</div>
{{end}}
<div class="col-md-9">
<div class="profile-content">
{{ block "profile_notifications_content" . }}{{end}}
</div>
</div>
</div>
{{end}}

Voir le fichier

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