diff --git a/docs/content/doc/development/events-and-listeners.md b/docs/content/doc/development/events-and-listeners.md index 6886cd0ce77..96e34118ce6 100644 --- a/docs/content/doc/development/events-and-listeners.md +++ b/docs/content/doc/development/events-and-listeners.md @@ -125,7 +125,7 @@ All listeners must implement this interface: ```golang // Listener represents something that listens to events type Listener interface { - Handle(payload message.Payload) error + Handle(msg *message.Message) error Name() string } ``` diff --git a/docs/content/doc/development/notifications.md b/docs/content/doc/development/notifications.md index d93f52072fb..893ef03f9f5 100644 --- a/docs/content/doc/development/notifications.md +++ b/docs/content/doc/development/notifications.md @@ -22,6 +22,7 @@ Each notification has to implement this interface: type Notification interface { ToMail() *Mail ToDB() interface{} + Name() string } ``` @@ -59,7 +60,9 @@ If not provided, the `from` field of the mail contains the value configured in [ ### Database notifications -All data returned from the `ToDB()` method is serialized to json and saved into the database, along with the id of the notifiable and a time stamp. +All data returned from the `ToDB()` method is serialized to json and saved into the database, along with the id of the +notifiable, the name of the notification and a time stamp. +If you don't use the database notification, the `Name()` function can return an empty string. ## Creating a new notification diff --git a/magefile.go b/magefile.go index a10d9064b46..aee52b05328 100644 --- a/magefile.go +++ b/magefile.go @@ -828,9 +828,9 @@ func (s *` + name + `) Name() string { } // Handle is executed when the event ` + name + ` listens on is fired -func (s *` + name + `) Handle(payload message.Payload) (err error) { +func (s *` + name + `) Handle(msg *message.Message) (err error) { event := &` + event + `{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } @@ -900,6 +900,8 @@ func (Dev) MakeNotification(name, module string) error { name += "Notification" } + notficationName := strings.ReplaceAll(strcase.ToDelimited(name, '.'), ".notification", "") + newNotificationCode := ` // ` + name + ` represents a ` + name + ` notification type ` + name + ` struct { @@ -918,6 +920,12 @@ func (n *` + name + `) ToMail() *notifications.Mail { func (n *` + name + `) ToDB() interface{} { return nil } + +// Name returns the name of the notification +func (n *` + name + `) Name() string { + return "` + notficationName + `" +} + ` filename := "./pkg/" + module + "/notifications.go" if err := appendToFile(filename, newNotificationCode); err != nil { diff --git a/pkg/events/events.go b/pkg/events/events.go index 72fbfd8c98e..c1ddac2ebad 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -71,9 +71,7 @@ func InitEvents() (err error) { for topic, funcs := range listeners { for _, handler := range funcs { - router.AddNoPublisherHandler(topic+"."+handler.Name(), topic, pubsub, func(msg *message.Message) error { - return handler.Handle(msg.Payload) - }) + router.AddNoPublisherHandler(topic+"."+handler.Name(), topic, pubsub, handler.Handle) } } diff --git a/pkg/events/listeners.go b/pkg/events/listeners.go index f0b1f4266a4..38cacf65755 100644 --- a/pkg/events/listeners.go +++ b/pkg/events/listeners.go @@ -20,7 +20,7 @@ import "github.com/ThreeDotsLabs/watermill/message" // Listener represents something that listens to events type Listener interface { - Handle(payload message.Payload) error + Handle(msg *message.Message) error Name() string } diff --git a/pkg/migration/20210220222121.go b/pkg/migration/20210220222121.go new file mode 100644 index 00000000000..01fe6d7adba --- /dev/null +++ b/pkg/migration/20210220222121.go @@ -0,0 +1,45 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "time" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type notifications20210220222121 struct { + ReadAt time.Time `xorm:"datetime null" json:"read_at"` +} + +func (notifications20210220222121) TableName() string { + return "notifications" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210220222121", + Description: "Add a read_at column to notifications", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(notifications20210220222121{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/migration/20210221111953.go b/pkg/migration/20210221111953.go new file mode 100644 index 00000000000..ca54ba8e9f8 --- /dev/null +++ b/pkg/migration/20210221111953.go @@ -0,0 +1,63 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type notifications20210221111953 struct { + Name string `xorm:"varchar(250) index null" json:"name"` +} + +func (notifications20210221111953) TableName() string { + return "notifications" +} + +type notifications20210221111954 struct { + Name string `xorm:"varchar(250) index not null" json:"name"` +} + +func (notifications20210221111954) TableName() string { + return "notifications" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210221111953", + Description: "Add name property to database notifications", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(notifications20210221111953{}) + if err != nil { + return err + } + + _, err = tx. + Cols("name"). + Update(¬ifications20210221111953{}) + if err != nil { + return err + } + + return tx.Sync2(notifications20210221111954{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index c86bd3b8125..ced052a6d84 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -59,7 +59,7 @@ func (s *IncreaseTaskCounter) Name() string { } // Hanlde is executed when the event IncreaseTaskCounter listens on is fired -func (s *IncreaseTaskCounter) Handle(payload message.Payload) (err error) { +func (s *IncreaseTaskCounter) Handle(msg *message.Message) (err error) { return keyvalue.IncrBy(metrics.TaskCountKey, 1) } @@ -73,7 +73,7 @@ func (s *DecreaseTaskCounter) Name() string { } // Hanlde is executed when the event DecreaseTaskCounter listens on is fired -func (s *DecreaseTaskCounter) Handle(payload message.Payload) (err error) { +func (s *DecreaseTaskCounter) Handle(msg *message.Message) (err error) { return keyvalue.DecrBy(metrics.TaskCountKey, 1) } @@ -87,9 +87,9 @@ func (s *SendTaskCommentNotification) Name() string { } // Handle is executed when the event SendTaskCommentNotification listens on is fired -func (s *SendTaskCommentNotification) Handle(payload message.Payload) (err error) { +func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) { event := &TaskCommentCreatedEvent{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } @@ -133,9 +133,9 @@ func (s *SendTaskAssignedNotification) Name() string { } // Handle is executed when the event SendTaskAssignedNotification listens on is fired -func (s *SendTaskAssignedNotification) Handle(payload message.Payload) (err error) { +func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error) { event := &TaskAssigneeCreatedEvent{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } @@ -179,9 +179,9 @@ func (s *SendTaskDeletedNotification) Name() string { } // Handle is executed when the event SendTaskDeletedNotification listens on is fired -func (s *SendTaskDeletedNotification) Handle(payload message.Payload) (err error) { +func (s *SendTaskDeletedNotification) Handle(msg *message.Message) (err error) { event := &TaskDeletedEvent{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } @@ -223,9 +223,9 @@ func (s *SubscribeAssigneeToTask) Name() string { } // Handle is executed when the event SubscribeAssigneeToTask listens on is fired -func (s *SubscribeAssigneeToTask) Handle(payload message.Payload) (err error) { +func (s *SubscribeAssigneeToTask) Handle(msg *message.Message) (err error) { event := &TaskAssigneeCreatedEvent{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } @@ -257,7 +257,7 @@ func (s *IncreaseListCounter) Name() string { return "list.counter.increase" } -func (s *IncreaseListCounter) Handle(payload message.Payload) (err error) { +func (s *IncreaseListCounter) Handle(msg *message.Message) (err error) { return keyvalue.IncrBy(metrics.ListCountKey, 1) } @@ -268,7 +268,7 @@ func (s *DecreaseListCounter) Name() string { return "list.counter.decrease" } -func (s *DecreaseListCounter) Handle(payload message.Payload) (err error) { +func (s *DecreaseListCounter) Handle(msg *message.Message) (err error) { return keyvalue.DecrBy(metrics.ListCountKey, 1) } @@ -282,9 +282,9 @@ func (s *SendListCreatedNotification) Name() string { } // Handle is executed when the event SendListCreatedNotification listens on is fired -func (s *SendListCreatedNotification) Handle(payload message.Payload) (err error) { +func (s *SendListCreatedNotification) Handle(msg *message.Message) (err error) { event := &ListCreatedEvent{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } @@ -330,7 +330,7 @@ func (s *IncreaseNamespaceCounter) Name() string { } // Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired -func (s *IncreaseNamespaceCounter) Handle(payload message.Payload) (err error) { +func (s *IncreaseNamespaceCounter) Handle(msg *message.Message) (err error) { return keyvalue.IncrBy(metrics.NamespaceCountKey, 1) } @@ -344,7 +344,7 @@ func (s *DecreaseNamespaceCounter) Name() string { } // Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired -func (s *DecreaseNamespaceCounter) Handle(payload message.Payload) (err error) { +func (s *DecreaseNamespaceCounter) Handle(msg *message.Message) (err error) { return keyvalue.DecrBy(metrics.NamespaceCountKey, 1) } @@ -361,7 +361,7 @@ func (s *IncreaseTeamCounter) Name() string { } // Hanlde is executed when the event IncreaseTeamCounter listens on is fired -func (s *IncreaseTeamCounter) Handle(payload message.Payload) (err error) { +func (s *IncreaseTeamCounter) Handle(msg *message.Message) (err error) { return keyvalue.IncrBy(metrics.TeamCountKey, 1) } @@ -375,7 +375,7 @@ func (s *DecreaseTeamCounter) Name() string { } // Hanlde is executed when the event DecreaseTeamCounter listens on is fired -func (s *DecreaseTeamCounter) Handle(payload message.Payload) (err error) { +func (s *DecreaseTeamCounter) Handle(msg *message.Message) (err error) { return keyvalue.DecrBy(metrics.TeamCountKey, 1) } @@ -389,9 +389,9 @@ func (s *SendTeamMemberAddedNotification) Name() string { } // Handle is executed when the event SendTeamMemberAddedNotification listens on is fired -func (s *SendTeamMemberAddedNotification) Handle(payload message.Payload) (err error) { +func (s *SendTeamMemberAddedNotification) Handle(msg *message.Message) (err error) { event := &TeamMemberAddedEvent{} - err = json.Unmarshal(payload, event) + err = json.Unmarshal(msg.Payload, event) if err != nil { return err } diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index be96c98d052..46e0dd4e0ee 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -28,8 +28,8 @@ import ( // ReminderDueNotification represents a ReminderDueNotification notification type ReminderDueNotification struct { - User *user.User - Task *Task + User *user.User `json:"user"` + Task *Task `json:"task"` } // ToMail returns the mail notification for ReminderDueNotification @@ -48,11 +48,16 @@ func (n *ReminderDueNotification) ToDB() interface{} { return nil } +// Name returns the name of the notification +func (n *ReminderDueNotification) Name() string { + return "" +} + // TaskCommentNotification represents a TaskCommentNotification notification type TaskCommentNotification struct { - Doer *user.User - Task *Task - Comment *TaskComment + Doer *user.User `json:"doer"` + Task *Task `json:"task"` + Comment *TaskComment `json:"comment"` } // ToMail returns the mail notification for TaskCommentNotification @@ -76,11 +81,16 @@ func (n *TaskCommentNotification) ToDB() interface{} { return n } +// Name returns the name of the notification +func (n *TaskCommentNotification) Name() string { + return "task.comment" +} + // TaskAssignedNotification represents a TaskAssignedNotification notification type TaskAssignedNotification struct { - Doer *user.User - Task *Task - Assignee *user.User + Doer *user.User `json:"doer"` + Task *Task `json:"task"` + Assignee *user.User `json:"assignee"` } // ToMail returns the mail notification for TaskAssignedNotification @@ -96,10 +106,15 @@ func (n *TaskAssignedNotification) ToDB() interface{} { return n } +// Name returns the name of the notification +func (n *TaskAssignedNotification) Name() string { + return "task.assigned" +} + // TaskDeletedNotification represents a TaskDeletedNotification notification type TaskDeletedNotification struct { - Doer *user.User - Task *Task + Doer *user.User `json:"doer"` + Task *Task `json:"task"` } // ToMail returns the mail notification for TaskDeletedNotification @@ -114,10 +129,15 @@ func (n *TaskDeletedNotification) ToDB() interface{} { return n } +// Name returns the name of the notification +func (n *TaskDeletedNotification) Name() string { + return "task.deleted" +} + // ListCreatedNotification represents a ListCreatedNotification notification type ListCreatedNotification struct { - Doer *user.User - List *List + Doer *user.User `json:"doer"` + List *List `json:"list"` } // ToMail returns the mail notification for ListCreatedNotification @@ -130,14 +150,19 @@ func (n *ListCreatedNotification) ToMail() *notifications.Mail { // ToDB returns the ListCreatedNotification notification in a format which can be saved in the db func (n *ListCreatedNotification) ToDB() interface{} { - return nil + return n +} + +// Name returns the name of the notification +func (n *ListCreatedNotification) Name() string { + return "list.created" } // TeamMemberAddedNotification represents a TeamMemberAddedNotification notification type TeamMemberAddedNotification struct { - Member *user.User - Doer *user.User - Team *Team + Member *user.User `json:"member"` + Doer *user.User `json:"doer"` + Team *Team `json:"team"` } // ToMail returns the mail notification for TeamMemberAddedNotification @@ -152,5 +177,10 @@ func (n *TeamMemberAddedNotification) ToMail() *notifications.Mail { // ToDB returns the TeamMemberAddedNotification notification in a format which can be saved in the db func (n *TeamMemberAddedNotification) ToDB() interface{} { - return nil + return n +} + +// Name returns the name of the notification +func (n *TeamMemberAddedNotification) Name() string { + return "team.member.added" } diff --git a/pkg/models/notifications_database.go b/pkg/models/notifications_database.go new file mode 100644 index 00000000000..278b5a3b6e6 --- /dev/null +++ b/pkg/models/notifications_database.go @@ -0,0 +1,84 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "code.vikunja.io/api/pkg/notifications" + "code.vikunja.io/web" + "xorm.io/xorm" +) + +// DatabaseNotifications is a wrapper around the crud operations that come with a database notification. +type DatabaseNotifications struct { + notifications.DatabaseNotification + + // Whether or not to mark this notification as read or unread. + // True is read, false is unread. + Read bool `xorm:"-" json:"read"` + + web.CRUDable `xorm:"-" json:"-"` + web.Rights `xorm:"-" json:"-"` +} + +// ReadAll returns all database notifications for a user +// @Summary Get all notifications for the current user +// @Description Returns an array with all notifications for the current user. +// @tags subscriptions +// @Accept json +// @Produce json +// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned." +// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page." +// @Security JWTKeyAuth +// @Success 200 {array} notifications.DatabaseNotification "The notifications" +// @Failure 403 {object} web.HTTPError "Link shares cannot have notifications." +// @Failure 500 {object} models.Message "Internal error" +// @Router /notifications [get] +func (d *DatabaseNotifications) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) { + if _, is := a.(*LinkSharing); is { + return nil, 0, 0, ErrGenericForbidden{} + } + + limit, start := getLimitFromPageIndex(page, perPage) + return notifications.GetNotificationsForUser(s, a.GetID(), limit, start) +} + +// CanUpdate checks if a user can mark a notification as read. +func (d *DatabaseNotifications) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { + if _, is := a.(*LinkSharing); is { + return false, nil + } + + return notifications.CanMarkNotificationAsRead(s, &d.DatabaseNotification, a.GetID()) +} + +// Update marks a notification as read. +// @Summary Mark a notification as (un-)read +// @Description Marks a notification as either read or unread. A user can only mark their own notifications as read. +// @tags subscriptions +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Notification ID" +// @Success 200 {object} models.DatabaseNotifications "The notification to mark as read." +// @Failure 403 {object} web.HTTPError "The user does not have access to that notification." +// @Failure 403 {object} web.HTTPError "Link shares cannot have notifications." +// @Failure 404 {object} web.HTTPError "The notification does not exist." +// @Failure 500 {object} models.Message "Internal error" +// @Router /notifications/{id} [post] +func (d *DatabaseNotifications) Update(s *xorm.Session, a web.Auth) (err error) { + return notifications.MarkNotificationAsRead(s, &d.DatabaseNotification, d.Read) +} diff --git a/pkg/notifications/database.go b/pkg/notifications/database.go new file mode 100644 index 00000000000..655d3f565e5 --- /dev/null +++ b/pkg/notifications/database.go @@ -0,0 +1,90 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package notifications + +import ( + "time" + + "xorm.io/xorm" +) + +// DatabaseNotification represents a notification that was saved to the database +type DatabaseNotification struct { + // The unique, numeric id of this notification. + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"notificationid"` + + // The ID of the notifiable this notification is associated with. + NotifiableID int64 `xorm:"bigint not null" json:"-"` + // The actual content of the notification. + Notification interface{} `xorm:"json not null" json:"notification"` + // The name of the notification + Name string `xorm:"varchar(250) index not null" json:"name"` + + // When this notification is marked as read, this will be updated with the current timestamp. + ReadAt time.Time `xorm:"datetime null" json:"read_at"` + + // A timestamp when this notification was created. You cannot change this value. + Created time.Time `xorm:"created not null" json:"created"` +} + +// TableName resolves to a better table name for notifications +func (d *DatabaseNotification) TableName() string { + return "notifications" +} + +// GetNotificationsForUser returns all notifications for a user. It is possible to limit the amount of notifications +// to return with the limit and start parameters. +// We're not passing a user object in directly because every other package imports this one so we'd get import cycles. +func GetNotificationsForUser(s *xorm.Session, notifiableID int64, limit, start int) (notifications []*DatabaseNotification, resultCount int, total int64, err error) { + err = s. + Where("notifiable_id = ?", notifiableID). + Limit(limit, start). + OrderBy("id DESC"). + Find(¬ifications) + if err != nil { + return nil, 0, 0, err + } + + total, err = s. + Where("notifiable_id = ?", notifiableID). + Count(&DatabaseNotification{}) + return notifications, len(notifications), total, err +} + +// CanMarkNotificationAsRead checks if a user can mark a notification as read. +func CanMarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, notifiableID int64) (can bool, err error) { + can, err = s. + Where("notifiable_id = ? AND id = ?", notifiableID, notification.ID). + NoAutoCondition(). + Get(notification) + return +} + +// MarkNotificationAsRead marks a notification as read. It should be called only after CanMarkNotificationAsRead has +// been called. +func MarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, read bool) (err error) { + notification.ReadAt = time.Time{} + if read { + notification.ReadAt = time.Now() + } + + _, err = s. + Where("id = ?", notification.ID). + Cols("read_at"). + Update(notification) + return +} diff --git a/pkg/notifications/notification.go b/pkg/notifications/notification.go index 4d6bba46bfb..f4df92be412 100644 --- a/pkg/notifications/notification.go +++ b/pkg/notifications/notification.go @@ -18,7 +18,6 @@ package notifications import ( "encoding/json" - "time" "code.vikunja.io/api/pkg/db" ) @@ -27,6 +26,7 @@ import ( type Notification interface { ToMail() *Mail ToDB() interface{} + Name() string } // Notifiable is an entity which can be notified. Usually a user. @@ -37,25 +37,6 @@ type Notifiable interface { RouteForDB() int64 } -// DatabaseNotification represents a notification that was saved to the database -type DatabaseNotification struct { - // The unique, numeric id of this notification. - ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"` - - // The ID of the notifiable this notification is associated with. - NotifiableID int64 `xorm:"bigint not null" json:"-"` - // The actual content of the notification. - Notification interface{} `xorm:"json not null" json:"notification"` - - // A timestamp when this notification was created. You cannot change this value. - Created time.Time `xorm:"created not null" json:"created"` -} - -// TableName resolves to a better table name for notifications -func (d *DatabaseNotification) TableName() string { - return "notifications" -} - // Notify notifies a notifiable of a notification func Notify(notifiable Notifiable, notification Notification) (err error) { @@ -98,6 +79,7 @@ func notifyDB(notifiable Notifiable, notification Notification) (err error) { dbNotification := &DatabaseNotification{ NotifiableID: notifiable.RouteForDB(), Notification: content, + Name: notification.Name(), } _, err = s.Insert(dbNotification) diff --git a/pkg/notifications/notification_test.go b/pkg/notifications/notification_test.go index 15275356f29..5296e29f12c 100644 --- a/pkg/notifications/notification_test.go +++ b/pkg/notifications/notification_test.go @@ -44,6 +44,11 @@ func (n *testNotification) ToDB() interface{} { return data } +// Name returns the name of the notification +func (n *testNotification) Name() string { + return "test.notification" +} + type testNotifiable struct { } diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 63f7b28b41a..2a3a4c45907 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -521,6 +521,15 @@ func registerAPIRoutes(a *echo.Group) { a.PUT("/subscriptions/:entity/:entityID", subscriptionHandler.CreateWeb) a.DELETE("/subscriptions/:entity/:entityID", subscriptionHandler.DeleteWeb) + // Notifications + notificationHandler := &handler.WebHandler{ + EmptyStruct: func() handler.CObject { + return &models.DatabaseNotifications{} + }, + } + a.GET("/notifications", notificationHandler.ReadAllWeb) + a.POST("/notifications/:notificationid", notificationHandler.UpdateWeb) + // Migrations m := a.Group("/migration") diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 177120579a7..29f71362fbf 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -3901,6 +3901,118 @@ var doc = `{ } } }, + "/notifications": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns an array with all notifications for the current user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscriptions" + ], + "summary": "Get all notifications for the current user", + "parameters": [ + { + "type": "integer", + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.", + "name": "per_page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The notifications", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/notifications.DatabaseNotification" + } + } + }, + "403": { + "description": "Link shares cannot have notifications.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, + "/notifications/{id}": { + "post": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Marks a notification as either read or unread. A user can only mark their own notifications as read.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscriptions" + ], + "summary": "Mark a notification as (un-)read", + "parameters": [ + { + "type": "integer", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The notification to mark as read.", + "schema": { + "$ref": "#/definitions/models.DatabaseNotifications" + } + }, + "403": { + "description": "Link shares cannot have notifications.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "The notification does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/register": { "post": { "description": "Creates a new user account.", @@ -7200,6 +7312,35 @@ var doc = `{ } } }, + "models.DatabaseNotifications": { + "type": "object", + "properties": { + "created": { + "description": "A timestamp when this notification was created. You cannot change this value.", + "type": "string" + }, + "id": { + "description": "The unique, numeric id of this notification.", + "type": "integer" + }, + "name": { + "description": "The name of the notification", + "type": "string" + }, + "notification": { + "description": "The actual content of the notification.", + "type": "object" + }, + "read": { + "description": "Whether or not to mark this notification as read or unread.\nTrue is read, false is unread.", + "type": "boolean" + }, + "read_at": { + "description": "When this notification is marked as read, this will be updated with the current timestamp.", + "type": "string" + } + } + }, "models.Label": { "type": "object", "properties": { @@ -8078,6 +8219,31 @@ var doc = `{ } } }, + "notifications.DatabaseNotification": { + "type": "object", + "properties": { + "created": { + "description": "A timestamp when this notification was created. You cannot change this value.", + "type": "string" + }, + "id": { + "description": "The unique, numeric id of this notification.", + "type": "integer" + }, + "name": { + "description": "The name of the notification", + "type": "string" + }, + "notification": { + "description": "The actual content of the notification.", + "type": "object" + }, + "read_at": { + "description": "When this notification is marked as read, this will be updated with the current timestamp.", + "type": "string" + } + } + }, "openid.Callback": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 6ac22ecc1f8..3137dac4d1e 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -3884,6 +3884,118 @@ } } }, + "/notifications": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns an array with all notifications for the current user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscriptions" + ], + "summary": "Get all notifications for the current user", + "parameters": [ + { + "type": "integer", + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.", + "name": "per_page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The notifications", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/notifications.DatabaseNotification" + } + } + }, + "403": { + "description": "Link shares cannot have notifications.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, + "/notifications/{id}": { + "post": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Marks a notification as either read or unread. A user can only mark their own notifications as read.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscriptions" + ], + "summary": "Mark a notification as (un-)read", + "parameters": [ + { + "type": "integer", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The notification to mark as read.", + "schema": { + "$ref": "#/definitions/models.DatabaseNotifications" + } + }, + "403": { + "description": "Link shares cannot have notifications.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "The notification does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/register": { "post": { "description": "Creates a new user account.", @@ -7183,6 +7295,35 @@ } } }, + "models.DatabaseNotifications": { + "type": "object", + "properties": { + "created": { + "description": "A timestamp when this notification was created. You cannot change this value.", + "type": "string" + }, + "id": { + "description": "The unique, numeric id of this notification.", + "type": "integer" + }, + "name": { + "description": "The name of the notification", + "type": "string" + }, + "notification": { + "description": "The actual content of the notification.", + "type": "object" + }, + "read": { + "description": "Whether or not to mark this notification as read or unread.\nTrue is read, false is unread.", + "type": "boolean" + }, + "read_at": { + "description": "When this notification is marked as read, this will be updated with the current timestamp.", + "type": "string" + } + } + }, "models.Label": { "type": "object", "properties": { @@ -8061,6 +8202,31 @@ } } }, + "notifications.DatabaseNotification": { + "type": "object", + "properties": { + "created": { + "description": "A timestamp when this notification was created. You cannot change this value.", + "type": "string" + }, + "id": { + "description": "The unique, numeric id of this notification.", + "type": "integer" + }, + "name": { + "description": "The name of the notification", + "type": "string" + }, + "notification": { + "description": "The actual content of the notification.", + "type": "object" + }, + "read_at": { + "description": "When this notification is marked as read, this will be updated with the current timestamp.", + "type": "string" + } + } + }, "openid.Callback": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 98bf1b8276f..0df8e9df2f0 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -229,6 +229,29 @@ definitions: description: A timestamp when this task was last updated. You cannot change this value. type: string type: object + models.DatabaseNotifications: + properties: + created: + description: A timestamp when this notification was created. You cannot change this value. + type: string + id: + description: The unique, numeric id of this notification. + type: integer + name: + description: The name of the notification + type: string + notification: + description: The actual content of the notification. + type: object + read: + description: |- + Whether or not to mark this notification as read or unread. + True is read, false is unread. + type: boolean + read_at: + description: When this notification is marked as read, this will be updated with the current timestamp. + type: string + type: object models.Label: properties: created: @@ -884,6 +907,24 @@ definitions: minLength: 1 type: string type: object + notifications.DatabaseNotification: + properties: + created: + description: A timestamp when this notification was created. You cannot change this value. + type: string + id: + description: The unique, numeric id of this notification. + type: integer + name: + description: The name of the notification + type: string + notification: + description: The actual content of the notification. + type: object + read_at: + description: When this notification is marked as read, this will be updated with the current timestamp. + type: string + type: object openid.Callback: properties: code: @@ -3643,6 +3684,77 @@ paths: summary: Update a user <-> namespace relation tags: - sharing + /notifications: + get: + consumes: + - application/json + description: Returns an array with all notifications for the current user. + parameters: + - description: The page number. Used for pagination. If not provided, the first page of results is returned. + in: query + name: page + type: integer + - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + in: query + name: per_page + type: integer + produces: + - application/json + responses: + "200": + description: The notifications + schema: + items: + $ref: '#/definitions/notifications.DatabaseNotification' + type: array + "403": + description: Link shares cannot have notifications. + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Get all notifications for the current user + tags: + - subscriptions + /notifications/{id}: + post: + consumes: + - application/json + description: Marks a notification as either read or unread. A user can only mark their own notifications as read. + parameters: + - description: Notification ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: The notification to mark as read. + schema: + $ref: '#/definitions/models.DatabaseNotifications' + "403": + description: Link shares cannot have notifications. + schema: + $ref: '#/definitions/web.HTTPError' + "404": + description: The notification does not exist. + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Mark a notification as (un-)read + tags: + - subscriptions /register: post: consumes: diff --git a/pkg/user/listeners.go b/pkg/user/listeners.go index 572461b2c32..0e96969d41a 100644 --- a/pkg/user/listeners.go +++ b/pkg/user/listeners.go @@ -40,6 +40,6 @@ func (s *IncreaseUserCounter) Name() string { } // Hanlde is executed when the event IncreaseUserCounter listens on is fired -func (s *IncreaseUserCounter) Handle(payload message.Payload) (err error) { +func (s *IncreaseUserCounter) Handle(msg *message.Message) (err error) { return keyvalue.IncrBy(metrics.UserCountKey, 1) } diff --git a/pkg/user/notifications.go b/pkg/user/notifications.go index b3c2b4c46cc..2fa527ce968 100644 --- a/pkg/user/notifications.go +++ b/pkg/user/notifications.go @@ -54,6 +54,11 @@ func (n *EmailConfirmNotification) ToDB() interface{} { return nil } +// Name returns the name of the notification +func (n *EmailConfirmNotification) Name() string { + return "" +} + // PasswordChangedNotification represents a PasswordChangedNotification notification type PasswordChangedNotification struct { User *User @@ -73,6 +78,11 @@ func (n *PasswordChangedNotification) ToDB() interface{} { return nil } +// Name returns the name of the notification +func (n *PasswordChangedNotification) Name() string { + return "" +} + // ResetPasswordNotification represents a ResetPasswordNotification notification type ResetPasswordNotification struct { User *User @@ -92,3 +102,8 @@ func (n *ResetPasswordNotification) ToMail() *notifications.Mail { func (n *ResetPasswordNotification) ToDB() interface{} { return nil } + +// Name returns the name of the notification +func (n *ResetPasswordNotification) Name() string { + return "" +}