Skip to content

WIP: HTML5 Push Notifications #10884

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

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
97adb3d
Add webpush-go dependency
jamesorlakin Mar 28, 2020
d8924ad
Generate VAPID keys and add to settings automatically
jamesorlakin Mar 28, 2020
4d37536
Document requirement of service worker
jamesorlakin Mar 28, 2020
bd138af
Provide public key to JS side
jamesorlakin Mar 28, 2020
c92df3c
Add demo button to enable push subscriptions
jamesorlakin Mar 28, 2020
eb1251a
Expand service worker to accept pushes and notifications
jamesorlakin Mar 28, 2020
2adca5b
Allow sending of notification events to hardcoded web push subscription
jamesorlakin Mar 28, 2020
1334af0
Link to issue comment if ID is present
jamesorlakin Mar 29, 2020
58106e0
Add licence to webpush_notification
jamesorlakin Mar 29, 2020
f994d68
Add WebPushSubscription table
jamesorlakin Mar 29, 2020
0946969
Add Web Push model functions
jamesorlakin Mar 29, 2020
da2323a
Refactor Web Push functions to model
jamesorlakin Mar 29, 2020
760262f
Create endpoint to create Web Push subscriptions
jamesorlakin Mar 29, 2020
7d0fd8c
Make Web Push endpoint unique
jamesorlakin Mar 29, 2020
4aada2e
Tweak endpoint to accept non-token authentication
jamesorlakin Mar 29, 2020
e82a5a9
Allow subscribing to push notifications in the UI
jamesorlakin Mar 29, 2020
c25003b
Vendor webpush-go dependency
jamesorlakin Mar 29, 2020
832fbbd
Merge branch 'master' into pushNotifications
jamesorlakin Mar 30, 2020
5265790
Merge branch 'master' into pushNotifications
jamesorlakin Apr 13, 2020
cd809b1
Attempt to 'make vendor' again
jamesorlakin Apr 13, 2020
5dafc6c
Fix spelling typo in Swagger
jamesorlakin Apr 13, 2020
1726fac
Only treat 4xx errors as invalid web push events
jamesorlakin Apr 13, 2020
f873c5d
Refactor notification participant logic to another function
jamesorlakin Apr 13, 2020
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
4 changes: 3 additions & 1 deletion docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `REACTIONS`: All available reactions. Allow users react with different emoji's.
- `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
- `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page.
- `USE_SERVICE_WORKER`: **true**: Whether to enable a Service Worker to cache frontend assets.
- `USE_SERVICE_WORKER`: **true**: Whether to enable a Service Worker to cache frontend assets and enable push notifications on supported browsers.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't piggy-back on the service-worker pref, these are two distinct features. Add a new one, like USE_PUSH_NOTIFICATIONS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True - I'll pop a note saying the service worker is required as well.


### UI - Admin (`ui.admin`)

Expand Down Expand Up @@ -289,6 +289,8 @@ set name for unique queues. Individual queues will default to

- `INSTALL_LOCK`: **false**: Disallow access to the install page.
- `SECRET_KEY`: **\<random at every install\>**: Global secret key. This should be changed.
- `WEB_PUSH_PUBLIC_KEY`: **\<random at every install\>**: VAPID key pair used for Web Push notifications
- `WEB_PUSH_PRIVATE_KEY`: **\<random at every install\>**: VAPID key pair used for Web Push notifications
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PUSH_NOTIFICATIONS_PUBLIC_KEY and PUSH_NOTIFICATIONS_PRIVATE_KEY would be more better imho.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the codebase I've tried to be consistent with 'Web Push', but I could change the user-facing config quite easily

- `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
- `COOKIE_USERNAME`: **gitea\_awesome**: Name of the cookie used to store the current username.
- `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/PuerkitoBio/goquery v1.5.0
github.com/RoaringBitmap/roaring v0.4.21 // indirect
github.com/SherClockHolmes/webpush-go v1.1.0
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/blevesearch/bleve v0.8.1
github.com/blevesearch/blevex v0.0.0-20180227211930-4b158bb555a3 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/RoaringBitmap/roaring v0.4.21 h1:WJ/zIlNX4wQZ9x8Ey33O1UaD9TCTakYsdLFSBcTwH+8=
github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/SherClockHolmes/webpush-go v1.1.0 h1:WjWbwo0Bf1Cbd8Yr0myrpYYlcN7VvQz/TVmUTjxL35g=
github.com/SherClockHolmes/webpush-go v1.1.0/go.mod h1:Jbd13H6kOFZubRMAaEHQS+e0EpP/aSHtLKeo9gsyO5k=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755/go.mod h1:voKvFVpXBJxdIPeqjoJuLK+UVcRlo/JLjeToGxPYu68=
Expand Down Expand Up @@ -637,6 +639,7 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
4 changes: 3 additions & 1 deletion models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,10 @@ var migrations = []Migration{
NewMigration("Add EmailHash Table", addEmailHashTable),
// v134 -> v135
NewMigration("Refix merge base for merged pull requests", refixMergeBase),
// v135 -> 136
// v135 -> v136
NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn),
// v136 -> v137
NewMigration("Add WebPushSubscription table", addWebPushSubcriptionTable),
}

// GetCurrentDBVersion returns the current db version
Expand Down
24 changes: 24 additions & 0 deletions models/migrations/v136.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2020 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 migrations

import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)

func addWebPushSubcriptionTable(x *xorm.Engine) error {
type WebPushSubscription struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX"`

Endpoint string `xorm:"UNIQUE"`
Auth string
P256DH string

CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
return x.Sync2(new(WebPushSubscription))
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func init() {
new(Task),
new(LanguageStat),
new(EmailHash),
new(WebPushSubscription),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
174 changes: 123 additions & 51 deletions models/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ package models
import (
"fmt"
"path"
"strconv"

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

Expand Down Expand Up @@ -116,6 +118,97 @@ func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
return getNotifications(x, opts)
}

// GetEligibleNotificationParticipants returns a list of users, as well as the issue, who are eligible to
// receive a new or updated notification.
// receiverID > 0 just targets the one user.
func GetEligibleNotificationParticipants(issue *Issue, notificationAuthorID, receiverID int64) (map[int64]struct{}, error) {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return nil, err
}

result, err := getEligibleNotificationParticipants(sess, issue, notificationAuthorID, receiverID)
if err != nil {
return nil, err
}

err = sess.Commit()
if err != nil {
return nil, err
}

return result, err
}

func getEligibleNotificationParticipants(e Engine, issue *Issue, notificationAuthorID, receiverID int64) (map[int64]struct{}, error) {
var toNotify map[int64]struct{}

if receiverID > 0 {
toNotify = make(map[int64]struct{}, 1)
toNotify[receiverID] = struct{}{}
return toNotify, nil

}

toNotify = make(map[int64]struct{}, 32)
issueWatches, err := getIssueWatchersIDs(e, issue.ID, true)
if err != nil {
return nil, err
}
for _, id := range issueWatches {
toNotify[id] = struct{}{}
}

repoWatches, err := getRepoWatchersIDs(e, issue.RepoID)
if err != nil {
return nil, err
}
for _, id := range repoWatches {
toNotify[id] = struct{}{}
}
issueParticipants, err := issue.getParticipantIDsByIssue(e)
if err != nil {
return nil, err
}
for _, id := range issueParticipants {
toNotify[id] = struct{}{}
}

// dont notify user who cause notification
delete(toNotify, notificationAuthorID)
// filter out explicit unwatch on issue
issueUnWatches, err := getIssueWatchersIDs(e, issue.ID, false)
if err != nil {
return nil, err
}
for _, id := range issueUnWatches {
delete(toNotify, id)
}

err = issue.loadRepo(e)
if err != nil {
return nil, err
}
units := issue.Repo.Units
issue.Repo.Units = nil // <- Not sure why this was here before refactoring, but we put it back later
defer func() {
issue.Repo.Units = units
}()

// Filter out those who can't view the linked issue/PR, but were previously involved
for userID := range toNotify {
if issue.IsPull && !issue.Repo.checkUnitUser(e, userID, false, UnitTypePullRequests) {
delete(toNotify, userID)
}
if !issue.IsPull && !issue.Repo.checkUnitUser(e, userID, false, UnitTypeIssues) {
delete(toNotify, userID)
}
}

return toNotify, nil
}

// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
// receiverID > 0 just send to reciver, else send to all watcher
Expand All @@ -135,9 +228,7 @@ func CreateOrUpdateIssueNotifications(issueID, commentID, notificationAuthorID,

func createOrUpdateIssueNotifications(e Engine, issueID, commentID, notificationAuthorID, receiverID int64) error {
// init
var toNotify map[int64]struct{}
notifications, err := getNotificationsByIssueID(e, issueID)

if err != nil {
return err
}
Expand All @@ -147,63 +238,24 @@ func createOrUpdateIssueNotifications(e Engine, issueID, commentID, notification
return err
}

if receiverID > 0 {
toNotify = make(map[int64]struct{}, 1)
toNotify[receiverID] = struct{}{}
} else {
toNotify = make(map[int64]struct{}, 32)
issueWatches, err := getIssueWatchersIDs(e, issueID, true)
if err != nil {
return err
}
for _, id := range issueWatches {
toNotify[id] = struct{}{}
}

repoWatches, err := getRepoWatchersIDs(e, issue.RepoID)
if err != nil {
return err
}
for _, id := range repoWatches {
toNotify[id] = struct{}{}
}
issueParticipants, err := issue.getParticipantIDsByIssue(e)
if err != nil {
return err
}
for _, id := range issueParticipants {
toNotify[id] = struct{}{}
}

// dont notify user who cause notification
delete(toNotify, notificationAuthorID)
// explicit unwatch on issue
issueUnWatches, err := getIssueWatchersIDs(e, issueID, false)
if err != nil {
return err
}
for _, id := range issueUnWatches {
delete(toNotify, id)
}
}

err = issue.loadRepo(e)
toNotify, err := getEligibleNotificationParticipants(e, issue, notificationAuthorID, receiverID)
if err != nil {
return err
}
if len(toNotify) == 0 {
return nil
}

// notify
for userID := range toNotify {
issue.Repo.Units = nil
if issue.IsPull && !issue.Repo.checkUnitUser(e, userID, false, UnitTypePullRequests) {
continue
}
if !issue.IsPull && !issue.Repo.checkUnitUser(e, userID, false, UnitTypeIssues) {
continue

err := notificationSendWebPushNotification(userID, issue, commentID)
if err != nil {
log.Error("problem sending webhook notification: %v", err)
}

if notificationExists(notifications, issue.ID, userID) {
if err = updateIssueNotification(e, userID, issue.ID, commentID, notificationAuthorID); err != nil {
if notificationExists(notifications, issueID, userID) {
if err = updateIssueNotification(e, userID, issueID, commentID, notificationAuthorID); err != nil {
return err
}
continue
Expand Down Expand Up @@ -779,3 +831,23 @@ func UpdateNotificationStatuses(user *User, currentStatus NotificationStatus, de
Update(n)
return err
}

func notificationSendWebPushNotification(userID int64, issue *Issue, commentID int64) error {
issueType := "issue"
if issue.IsPull {
issueType = "pull request"
}

anchorURL := ""
if commentID != 0 {
anchorURL = "#issuecomment-" + strconv.FormatInt(commentID, 10)
}

notificationPayload := &structs.NotificationPayload{
Title: setting.AppName + " - " + issue.Repo.MustOwner().Name + "/" + issue.Repo.Name,
Text: "New activity on " + issueType + " #" + strconv.FormatInt(issue.Index, 10) + " " + issue.Title + ".\nClick to open.",
URL: issue.HTMLURL() + anchorURL,
}
err := SendWebPushNotificationToUser(userID, notificationPayload)
return err
}
Loading