Skip to content

Commit fea0d91

Browse files
committed
[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}
1 parent c884735 commit fea0d91

File tree

12 files changed

+994
-15
lines changed

12 files changed

+994
-15
lines changed

models/issue.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,20 @@ func (issue *Issue) GetLastEventLabel() string {
842842
return "repo.issues.opened_by"
843843
}
844844

845+
// GetLastComment return last comment for the current issue.
846+
func (issue *Issue) GetLastComment() (*Comment, error) {
847+
var c Comment
848+
exist, err := x.Where("type = ?", CommentTypeComment).
849+
And("issue_id = ?", issue.ID).Desc("id").Get(&c)
850+
if err != nil {
851+
return nil, err
852+
}
853+
if !exist {
854+
return nil, nil
855+
}
856+
return &c, nil
857+
}
858+
845859
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
846860
func (issue *Issue) GetLastEventLabelFake() string {
847861
if issue.IsClosed {

models/issue_comment.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package models
88

99
import (
1010
"fmt"
11+
"path"
1112
"strings"
1213

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

239+
// APIURL formats a API-string to the issue-comment
240+
func (c *Comment) APIURL() string {
241+
err := c.LoadIssue()
242+
if err != nil { // Silently dropping errors :unamused:
243+
log.Error("LoadIssue(%d): %v", c.IssueID, err)
244+
return ""
245+
}
246+
err = c.Issue.loadRepo(x)
247+
if err != nil { // Silently dropping errors :unamused:
248+
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
249+
return ""
250+
}
251+
252+
return c.Issue.Repo.APIURL() + "/" + path.Join("issues/comments", fmt.Sprint(c.ID))
253+
}
254+
238255
// IssueURL formats a URL-string to the issue
239256
func (c *Comment) IssueURL() string {
240257
err := c.LoadIssue()

models/notification.go

Lines changed: 203 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ package models
66

77
import (
88
"fmt"
9+
"path"
910

11+
"code.gitea.io/gitea/modules/setting"
12+
api "code.gitea.io/gitea/modules/structs"
1013
"code.gitea.io/gitea/modules/timeutil"
14+
15+
"xorm.io/builder"
16+
"xorm.io/xorm"
1117
)
1218

1319
type (
@@ -47,17 +53,67 @@ type Notification struct {
4753
IssueID int64 `xorm:"INDEX NOT NULL"`
4854
CommitID string `xorm:"INDEX"`
4955
CommentID int64
50-
Comment *Comment `xorm:"-"`
5156

5257
UpdatedBy int64 `xorm:"INDEX NOT NULL"`
5358

5459
Issue *Issue `xorm:"-"`
5560
Repository *Repository `xorm:"-"`
61+
Comment *Comment `xorm:"-"`
62+
User *User `xorm:"-"`
5663

5764
CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
5865
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
5966
}
6067

68+
// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
69+
type FindNotificationOptions struct {
70+
UserID int64
71+
RepoID int64
72+
IssueID int64
73+
Status NotificationStatus
74+
UpdatedAfterUnix int64
75+
UpdatedBeforeUnix int64
76+
}
77+
78+
// ToCond will convert each condition into a xorm-Cond
79+
func (opts *FindNotificationOptions) ToCond() builder.Cond {
80+
cond := builder.NewCond()
81+
if opts.UserID != 0 {
82+
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
83+
}
84+
if opts.RepoID != 0 {
85+
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
86+
}
87+
if opts.IssueID != 0 {
88+
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
89+
}
90+
if opts.Status != 0 {
91+
cond = cond.And(builder.Eq{"notification.status": opts.Status})
92+
}
93+
if opts.UpdatedAfterUnix != 0 {
94+
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
95+
}
96+
if opts.UpdatedBeforeUnix != 0 {
97+
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
98+
}
99+
return cond
100+
}
101+
102+
// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
103+
func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session {
104+
return e.Where(opts.ToCond())
105+
}
106+
107+
func getNotifications(e Engine, options FindNotificationOptions) (nl NotificationList, err error) {
108+
err = options.ToSession(e).OrderBy("notification.updated_unix DESC").Find(&nl)
109+
return
110+
}
111+
112+
// GetNotifications returns all notifications that fit to the given options.
113+
func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
114+
return getNotifications(x, opts)
115+
}
116+
61117
// CreateOrUpdateIssueNotifications creates an issue notification
62118
// for each watcher, or updates it if already exists
63119
func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
@@ -238,21 +294,125 @@ func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, p
238294
return
239295
}
240296

297+
// APIFormat converts a Notification to api.NotificationThread
298+
func (n *Notification) APIFormat() *api.NotificationThread {
299+
result := &api.NotificationThread{
300+
ID: n.ID,
301+
Unread: !(n.Status == NotificationStatusRead || n.Status == NotificationStatusPinned),
302+
Pinned: n.Status == NotificationStatusPinned,
303+
UpdatedAt: n.UpdatedUnix.AsTime(),
304+
URL: n.APIURL(),
305+
}
306+
307+
//since user only get notifications when he has access to use minimal access mode
308+
if n.Repository != nil {
309+
result.Repository = n.Repository.APIFormat(AccessModeRead)
310+
}
311+
312+
//handle Subject
313+
switch n.Source {
314+
case NotificationSourceIssue:
315+
result.Subject = &api.NotificationSubject{Type: "Issue"}
316+
if n.Issue != nil {
317+
result.Subject.Title = n.Issue.Title
318+
result.Subject.URL = n.Issue.APIURL()
319+
comment, err := n.Issue.GetLastComment()
320+
if err == nil && comment != nil {
321+
result.Subject.LatestCommentURL = comment.APIURL()
322+
}
323+
}
324+
case NotificationSourcePullRequest:
325+
result.Subject = &api.NotificationSubject{Type: "Pull"}
326+
if n.Issue != nil {
327+
result.Subject.Title = n.Issue.Title
328+
result.Subject.URL = n.Issue.APIURL()
329+
comment, err := n.Issue.GetLastComment()
330+
if err == nil && comment != nil {
331+
result.Subject.LatestCommentURL = comment.APIURL()
332+
}
333+
}
334+
case NotificationSourceCommit:
335+
result.Subject = &api.NotificationSubject{
336+
Type: "Commit",
337+
Title: n.CommitID,
338+
}
339+
//unused until now
340+
}
341+
342+
return result
343+
}
344+
345+
// LoadAttributes load Repo Issue User and Comment if not loaded
346+
func (n *Notification) LoadAttributes() (err error) {
347+
return n.loadAttributes(x)
348+
}
349+
350+
func (n *Notification) loadAttributes(e Engine) (err error) {
351+
if err = n.loadRepo(e); err != nil {
352+
return
353+
}
354+
if err = n.loadIssue(e); err != nil {
355+
return
356+
}
357+
if err = n.loadUser(e); err != nil {
358+
return
359+
}
360+
if err = n.loadComment(e); err != nil {
361+
return
362+
}
363+
return
364+
}
365+
366+
func (n *Notification) loadRepo(e Engine) (err error) {
367+
if n.Repository == nil {
368+
n.Repository, err = getRepositoryByID(e, n.RepoID)
369+
if err != nil {
370+
return fmt.Errorf("getRepositoryByID [%d]: %v", n.RepoID, err)
371+
}
372+
}
373+
return nil
374+
}
375+
376+
func (n *Notification) loadIssue(e Engine) (err error) {
377+
if n.Issue == nil {
378+
n.Issue, err = getIssueByID(e, n.IssueID)
379+
if err != nil {
380+
return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err)
381+
}
382+
return n.Issue.loadAttributes(e)
383+
}
384+
return nil
385+
}
386+
387+
func (n *Notification) loadComment(e Engine) (err error) {
388+
if n.Comment == nil && n.CommentID > 0 {
389+
n.Comment, err = GetCommentByID(n.CommentID)
390+
if err != nil {
391+
return fmt.Errorf("GetCommentByID [%d]: %v", n.CommentID, err)
392+
}
393+
}
394+
return nil
395+
}
396+
397+
func (n *Notification) loadUser(e Engine) (err error) {
398+
if n.User == nil {
399+
n.User, err = getUserByID(e, n.UserID)
400+
if err != nil {
401+
return fmt.Errorf("getUserByID [%d]: %v", n.UserID, err)
402+
}
403+
}
404+
return nil
405+
}
406+
241407
// GetRepo returns the repo of the notification
242408
func (n *Notification) GetRepo() (*Repository, error) {
243-
n.Repository = new(Repository)
244-
_, err := x.
245-
Where("id = ?", n.RepoID).
246-
Get(n.Repository)
409+
err := n.loadRepo(x)
247410
return n.Repository, err
248411
}
249412

250413
// GetIssue returns the issue of the notification
251414
func (n *Notification) GetIssue() (*Issue, error) {
252-
n.Issue = new(Issue)
253-
_, err := x.
254-
Where("id = ?", n.IssueID).
255-
Get(n.Issue)
415+
err := n.loadIssue(x)
256416
return n.Issue, err
257417
}
258418

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

427+
// APIURL formats a URL-string to the notification
428+
func (n *Notification) APIURL() string {
429+
return setting.AppURL + path.Join("api/v1/notifications/threads", fmt.Sprintf("%d", n.ID))
430+
}
431+
267432
// NotificationList contains a list of notifications
268433
type NotificationList []*Notification
269434

435+
// APIFormat converts a NotificationList to api.NotificationThread list
436+
func (nl NotificationList) APIFormat() []*api.NotificationThread {
437+
var result = make([]*api.NotificationThread, 0, len(nl))
438+
for _, n := range nl {
439+
result = append(result, n.APIFormat())
440+
}
441+
return result
442+
}
443+
444+
// LoadAttributes load Repo Issue User and Comment if not loaded
445+
func (nl NotificationList) LoadAttributes() (err error) {
446+
for i := 0; i < len(nl); i++ {
447+
err = nl[i].LoadAttributes()
448+
if err != nil {
449+
return
450+
}
451+
}
452+
return
453+
}
454+
270455
func (nl NotificationList) getPendingRepoIDs() []int64 {
271456
var ids = make(map[int64]struct{}, len(nl))
272457
for _, notification := range nl {
@@ -486,7 +671,7 @@ func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error {
486671

487672
// SetNotificationStatus change the notification status
488673
func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error {
489-
notification, err := getNotificationByID(notificationID)
674+
notification, err := getNotificationByID(x, notificationID)
490675
if err != nil {
491676
return err
492677
}
@@ -501,9 +686,14 @@ func SetNotificationStatus(notificationID int64, user *User, status Notification
501686
return err
502687
}
503688

504-
func getNotificationByID(notificationID int64) (*Notification, error) {
689+
// GetNotificationByID return notification by ID
690+
func GetNotificationByID(notificationID int64) (*Notification, error) {
691+
return getNotificationByID(x, notificationID)
692+
}
693+
694+
func getNotificationByID(e Engine, notificationID int64) (*Notification, error) {
505695
notification := new(Notification)
506-
ok, err := x.
696+
ok, err := e.
507697
Where("id = ?", notificationID).
508698
Get(notification)
509699

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

514704
if !ok {
515-
return nil, fmt.Errorf("Notification %d does not exists", notificationID)
705+
return nil, ErrNotExist{ID: notificationID}
516706
}
517707

518708
return notification, nil

modules/structs/notifications.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package structs
6+
7+
import (
8+
"time"
9+
)
10+
11+
// NotificationThread expose Notification on API
12+
type NotificationThread struct {
13+
ID int64 `json:"id"`
14+
Repository *Repository `json:"repository"`
15+
Subject *NotificationSubject `json:"subject"`
16+
Unread bool `json:"unread"`
17+
Pinned bool `json:"pinned"`
18+
UpdatedAt time.Time `json:"updated_at"`
19+
URL string `json:"url"`
20+
}
21+
22+
// NotificationSubject contains the notification subject (Issue/Pull/Commit)
23+
type NotificationSubject struct {
24+
Title string `json:"title"`
25+
URL string `json:"url"`
26+
LatestCommentURL string `json:"latest_comment_url"`
27+
Type string `json:"type" binding:"In(Issue,Pull,Commit)"`
28+
}

routers/api/v1/admin/user.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) {
5656
// responses:
5757
// "201":
5858
// "$ref": "#/responses/User"
59-
// "403":
60-
// "$ref": "#/responses/forbidden"
6159
// "400":
6260
// "$ref": "#/responses/error"
61+
// "403":
62+
// "$ref": "#/responses/forbidden"
6363
// "422":
6464
// "$ref": "#/responses/validationError"
6565

0 commit comments

Comments
 (0)