Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API] Add notification endpoint #9488

Merged
merged 6 commits into from
Jan 9, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[API] Add notification endpoints
 * add func GetNotifications(opts FindNotificationOptions)
 * add func (n *Notification) APIFormat()
 * add func (nl NotificationList) APIFormat()
 * add func (n *Notification) APIURL()
 * add func (nl NotificationList) APIFormat()
 * add LoadAttributes functions (loadRepo, loadIssue, loadComment, loadUser)
 * add func (c *Comment) APIURL()
 * add func (issue *Issue) GetLastComment()
 * add endpoint GET /notifications
 * add endpoint PUT /notifications
 * add endpoint GET /repos/{owner}/{repo}/notifications
 * add endpoint PUT /repos/{owner}/{repo}/notifications
 * add endpoint GET /notifications/threads/{id}
 * add endpoint PATCH /notifications/threads/{id}
  • Loading branch information
6543 committed Jan 8, 2020
commit fea0d91ad915d658614407cf71c78d0309140eba
14 changes: 14 additions & 0 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,20 @@ func (issue *Issue) GetLastEventLabel() string {
return "repo.issues.opened_by"
}

// GetLastComment return last comment for the current issue.
func (issue *Issue) GetLastComment() (*Comment, error) {
var c Comment
exist, err := x.Where("type = ?", CommentTypeComment).
And("issue_id = ?", issue.ID).Desc("id").Get(&c)
if err != nil {
return nil, err
}
if !exist {
return nil, nil
}
return &c, nil
}

// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
func (issue *Issue) GetLastEventLabelFake() string {
if issue.IsClosed {
Expand Down
17 changes: 17 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package models

import (
"fmt"
"path"
"strings"

"code.gitea.io/gitea/modules/git"
Expand Down Expand Up @@ -235,6 +236,22 @@ func (c *Comment) HTMLURL() string {
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
}

// APIURL formats a API-string to the issue-comment
func (c *Comment) APIURL() string {
err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused:
log.Error("LoadIssue(%d): %v", c.IssueID, err)
return ""
}
err = c.Issue.loadRepo(x)
if err != nil { // Silently dropping errors :unamused:
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
return ""
}

return c.Issue.Repo.APIURL() + "/" + path.Join("issues/comments", fmt.Sprint(c.ID))
}

// IssueURL formats a URL-string to the issue
func (c *Comment) IssueURL() string {
err := c.LoadIssue()
Expand Down
216 changes: 203 additions & 13 deletions models/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ package models

import (
"fmt"
"path"

"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/builder"
"xorm.io/xorm"
)

type (
Expand Down Expand Up @@ -47,17 +53,67 @@ type Notification struct {
IssueID int64 `xorm:"INDEX NOT NULL"`
CommitID string `xorm:"INDEX"`
CommentID int64
Comment *Comment `xorm:"-"`

UpdatedBy int64 `xorm:"INDEX NOT NULL"`

Issue *Issue `xorm:"-"`
Repository *Repository `xorm:"-"`
Comment *Comment `xorm:"-"`
User *User `xorm:"-"`

CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
}

// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
type FindNotificationOptions struct {
UserID int64
RepoID int64
IssueID int64
Status NotificationStatus
UpdatedAfterUnix int64
UpdatedBeforeUnix int64
}

// ToCond will convert each condition into a xorm-Cond
func (opts *FindNotificationOptions) ToCond() builder.Cond {
cond := builder.NewCond()
if opts.UserID != 0 {
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
}
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
}
if opts.IssueID != 0 {
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
}
if opts.Status != 0 {
cond = cond.And(builder.Eq{"notification.status": opts.Status})
}
if opts.UpdatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
}
if opts.UpdatedBeforeUnix != 0 {
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
}
return cond
}

// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session {
return e.Where(opts.ToCond())
}

func getNotifications(e Engine, options FindNotificationOptions) (nl NotificationList, err error) {
err = options.ToSession(e).OrderBy("notification.updated_unix DESC").Find(&nl)
return
}

// GetNotifications returns all notifications that fit to the given options.
func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
return getNotifications(x, opts)
}

// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
Expand Down Expand Up @@ -238,21 +294,125 @@ func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, p
return
}

// APIFormat converts a Notification to api.NotificationThread
func (n *Notification) APIFormat() *api.NotificationThread {
6543 marked this conversation as resolved.
Show resolved Hide resolved
result := &api.NotificationThread{
ID: n.ID,
Unread: !(n.Status == NotificationStatusRead || n.Status == NotificationStatusPinned),
Pinned: n.Status == NotificationStatusPinned,
UpdatedAt: n.UpdatedUnix.AsTime(),
URL: n.APIURL(),
}

//since user only get notifications when he has access to use minimal access mode
if n.Repository != nil {
result.Repository = n.Repository.APIFormat(AccessModeRead)
}

//handle Subject
switch n.Source {
case NotificationSourceIssue:
result.Subject = &api.NotificationSubject{Type: "Issue"}
if n.Issue != nil {
result.Subject.Title = n.Issue.Title
result.Subject.URL = n.Issue.APIURL()
comment, err := n.Issue.GetLastComment()
if err == nil && comment != nil {
result.Subject.LatestCommentURL = comment.APIURL()
}
}
case NotificationSourcePullRequest:
result.Subject = &api.NotificationSubject{Type: "Pull"}
if n.Issue != nil {
result.Subject.Title = n.Issue.Title
result.Subject.URL = n.Issue.APIURL()
comment, err := n.Issue.GetLastComment()
if err == nil && comment != nil {
result.Subject.LatestCommentURL = comment.APIURL()
}
}
case NotificationSourceCommit:
result.Subject = &api.NotificationSubject{
Type: "Commit",
Title: n.CommitID,
}
//unused until now
}

return result
}

// LoadAttributes load Repo Issue User and Comment if not loaded
func (n *Notification) LoadAttributes() (err error) {
return n.loadAttributes(x)
}

func (n *Notification) loadAttributes(e Engine) (err error) {
if err = n.loadRepo(e); err != nil {
return
}
if err = n.loadIssue(e); err != nil {
return
}
if err = n.loadUser(e); err != nil {
return
}
if err = n.loadComment(e); err != nil {
return
}
return
}

func (n *Notification) loadRepo(e Engine) (err error) {
if n.Repository == nil {
n.Repository, err = getRepositoryByID(e, n.RepoID)
if err != nil {
return fmt.Errorf("getRepositoryByID [%d]: %v", n.RepoID, err)
}
}
return nil
}

func (n *Notification) loadIssue(e Engine) (err error) {
if n.Issue == nil {
n.Issue, err = getIssueByID(e, n.IssueID)
if err != nil {
return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err)
}
return n.Issue.loadAttributes(e)
}
return nil
}

func (n *Notification) loadComment(e Engine) (err error) {
if n.Comment == nil && n.CommentID > 0 {
n.Comment, err = GetCommentByID(n.CommentID)
if err != nil {
return fmt.Errorf("GetCommentByID [%d]: %v", n.CommentID, err)
}
}
return nil
}

func (n *Notification) loadUser(e Engine) (err error) {
if n.User == nil {
n.User, err = getUserByID(e, n.UserID)
if err != nil {
return fmt.Errorf("getUserByID [%d]: %v", n.UserID, err)
}
}
return nil
}

// GetRepo returns the repo of the notification
func (n *Notification) GetRepo() (*Repository, error) {
n.Repository = new(Repository)
_, err := x.
Where("id = ?", n.RepoID).
Get(n.Repository)
err := n.loadRepo(x)
6543 marked this conversation as resolved.
Show resolved Hide resolved
6543 marked this conversation as resolved.
Show resolved Hide resolved
return n.Repository, err
6543 marked this conversation as resolved.
Show resolved Hide resolved
}

// GetIssue returns the issue of the notification
func (n *Notification) GetIssue() (*Issue, error) {
n.Issue = new(Issue)
_, err := x.
Where("id = ?", n.IssueID).
Get(n.Issue)
err := n.loadIssue(x)
6543 marked this conversation as resolved.
Show resolved Hide resolved
6543 marked this conversation as resolved.
Show resolved Hide resolved
return n.Issue, err
6543 marked this conversation as resolved.
Show resolved Hide resolved
}

Expand All @@ -264,9 +424,34 @@ func (n *Notification) HTMLURL() string {
return n.Issue.HTMLURL()
}

// APIURL formats a URL-string to the notification
func (n *Notification) APIURL() string {
return setting.AppURL + path.Join("api/v1/notifications/threads", fmt.Sprintf("%d", n.ID))
}

// NotificationList contains a list of notifications
type NotificationList []*Notification

// APIFormat converts a NotificationList to api.NotificationThread list
func (nl NotificationList) APIFormat() []*api.NotificationThread {
var result = make([]*api.NotificationThread, 0, len(nl))
for _, n := range nl {
result = append(result, n.APIFormat())
}
return result
}

// LoadAttributes load Repo Issue User and Comment if not loaded
func (nl NotificationList) LoadAttributes() (err error) {
for i := 0; i < len(nl); i++ {
err = nl[i].LoadAttributes()
if err != nil {
return
}
}
return
}

func (nl NotificationList) getPendingRepoIDs() []int64 {
var ids = make(map[int64]struct{}, len(nl))
for _, notification := range nl {
Expand Down Expand Up @@ -486,7 +671,7 @@ func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error {

// SetNotificationStatus change the notification status
func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error {
notification, err := getNotificationByID(notificationID)
notification, err := getNotificationByID(x, notificationID)
if err != nil {
return err
}
Expand All @@ -501,9 +686,14 @@ func SetNotificationStatus(notificationID int64, user *User, status Notification
return err
}

func getNotificationByID(notificationID int64) (*Notification, error) {
// GetNotificationByID return notification by ID
func GetNotificationByID(notificationID int64) (*Notification, error) {
return getNotificationByID(x, notificationID)
}

func getNotificationByID(e Engine, notificationID int64) (*Notification, error) {
notification := new(Notification)
ok, err := x.
ok, err := e.
Where("id = ?", notificationID).
Get(notification)

Expand All @@ -512,7 +702,7 @@ func getNotificationByID(notificationID int64) (*Notification, error) {
}

if !ok {
return nil, fmt.Errorf("Notification %d does not exists", notificationID)
return nil, ErrNotExist{ID: notificationID}
}

return notification, nil
Expand Down
28 changes: 28 additions & 0 deletions modules/structs/notifications.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package structs

import (
"time"
)

// NotificationThread expose Notification on API
type NotificationThread struct {
ID int64 `json:"id"`
Repository *Repository `json:"repository"`
Subject *NotificationSubject `json:"subject"`
Unread bool `json:"unread"`
Pinned bool `json:"pinned"`
UpdatedAt time.Time `json:"updated_at"`
URL string `json:"url"`
}

// NotificationSubject contains the notification subject (Issue/Pull/Commit)
type NotificationSubject struct {
Title string `json:"title"`
URL string `json:"url"`
LatestCommentURL string `json:"latest_comment_url"`
Type string `json:"type" binding:"In(Issue,Pull,Commit)"`
}
4 changes: 2 additions & 2 deletions routers/api/v1/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) {
// responses:
// "201":
// "$ref": "#/responses/User"
// "403":
// "$ref": "#/responses/forbidden"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"

Expand Down
Loading