~gitpocket/gitpocket

1
4017ccaf45bdbd751e5aeaa391d7510005b657f5
mainfeed-discovery
gitpocket/inbox.go -rw-r--r--4.9 KiB ViewLogBlameView rawPermalink c0e915d500 fix(web): throttle posts per account 2 days ago
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package main

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"
)

const inboxSchema = `
create table if not exists notifications (
	id integer primary key,
	user integer not null references users(id) on delete cascade,
	repo integer not null references repos(id) on delete cascade,
	kind text not null,
	number integer not null,
	title text not null,
	actor integer not null references users(id) on delete cascade,
	verb text not null,
	created integer not null,
	unread integer not null default 1,
	unique(user, repo, kind, number)
);
`

type Notification struct {
	ID      int64
	Repo    Repo
	Kind    string
	Number  int64
	Title   string
	Actor   string
	Verb    string
	Created time.Time
	Unread  bool
}

func (n Notification) Link() string {
	return fmt.Sprintf("/%s/%ss/%d", n.Repo.Path(), n.Kind, n.Number)
}

func (s *Store) Watchers(repo Repo, kind string, item, author int64) []int64 {
	ids := []int64{author}
	if s.IsOrg(repo.Owner.ID) {
		for _, m := range s.Members(repo.Owner.ID) {
			ids = append(ids, m.ID)
		}
	} else {
		ids = append(ids, repo.Owner.ID)
	}
	q := `select author from comments where pull = ?1 union select author from reviews where pull = ?1 union select author from prreviews where pull = ?1`
	if kind == "issue" {
		q = `select author from issue_comments where issue = ?1`
	}
	rows, err := s.db.Query(q, item)
	if err != nil {
		return ids
	}
	defer rows.Close()
	for rows.Next() {
		var id int64
		if rows.Scan(&id) == nil {
			ids = append(ids, id)
		}
	}
	return ids
}

func (s *Store) Notify(users []int64, actor User, repo Repo, kind string, number int64, title, verb string) {
	seen := map[int64]bool{actor.ID: true}
	now := time.Now().Unix()
	for _, id := range users {
		if seen[id] {
			continue
		}
		seen[id] = true
		if repo.Private && !s.CanRead(repo, &User{ID: id}) {
			continue
		}
		s.db.Exec(`insert into notifications(user, repo, kind, number, title, actor, verb, created) values (?, ?, ?, ?, ?, ?, ?, ?)
			on conflict(user, repo, kind, number) do update set title = excluded.title, actor = excluded.actor, verb = excluded.verb, created = excluded.created, unread = 1`,
			id, repo.ID, kind, number, title, actor.ID, verb, now)
	}
}

func (s *Store) Notifications(user int64, limit int) []Notification {
	rows, err := s.db.Query(`select n.id, n.kind, n.number, n.title, a.name, n.verb, n.created, n.unread, r.id, r.name, r.private, o.id, o.name
		from notifications n join users a on a.id = n.actor join repos r on r.id = n.repo join users o on o.id = r.owner
		where n.user = ? order by n.unread desc, n.created desc limit ?`, user, limit)
	if err != nil {
		return nil
	}
	var all []Notification
	for rows.Next() {
		var n Notification
		var created, private int64
		if rows.Scan(&n.ID, &n.Kind, &n.Number, &n.Title, &n.Actor, &n.Verb, &created, &n.Unread, &n.Repo.ID, &n.Repo.Name, &private, &n.Repo.Owner.ID, &n.Repo.Owner.Name) == nil {
			n.Created = time.Unix(created, 0)
			n.Repo.Private = private == 1
			all = append(all, n)
		}
	}
	rows.Close()
	shown := all[:0]
	for _, n := range all {
		if !n.Repo.Private || s.CanRead(n.Repo, &User{ID: user}) {
			shown = append(shown, n)
		}
	}
	return shown
}

func (s *Store) ReadThread(user, repo int64, kind string, number int64) {
	var unread bool
	s.db.QueryRow(`select 1 from notifications where user = ? and repo = ? and kind = ? and number = ? and unread = 1`, user, repo, kind, number).Scan(&unread)
	if !unread {
		return
	}
	s.db.Exec(`update notifications set unread = 0 where user = ? and repo = ? and kind = ? and number = ? and unread = 1`, user, repo, kind, number)
}

func (s *Server) inbox(w http.ResponseWriter, r *http.Request) {
	u := s.require(w, r)
	if u == nil {
		return
	}
	if r.Method == "POST" {
		s.store.db.Exec(`update notifications set unread = 0 where user = ?`, u.ID)
		http.Redirect(w, r, "/notifications", http.StatusSeeOther)
		return
	}
	s.store.db.Exec(`delete from notifications where user = ? and unread = 0 and created < ?`, u.ID, time.Now().AddDate(0, 0, -90).Unix())
	s.render(w, r, "notifications", page{Title: "Notifications", User: u, Data: map[string]any{"Items": s.store.Notifications(u.ID, 100)}})
}

func (s *Server) openNotification(w http.ResponseWriter, r *http.Request) {
	u := s.require(w, r)
	if u == nil {
		return
	}
	id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
	var n Notification
	var repo int64
	if s.store.db.QueryRow(`select repo, kind, number from notifications where id = ? and user = ?`, id, u.ID).Scan(&repo, &n.Kind, &n.Number) != nil {
		http.Redirect(w, r, "/notifications", http.StatusSeeOther)
		return
	}
	n.Repo, _ = s.store.RepoByID(repo)
	if !s.store.CanRead(n.Repo, u) {
		http.Redirect(w, r, "/notifications", http.StatusSeeOther)
		return
	}
	s.store.ReadThread(u.ID, repo, n.Kind, n.Number)
	http.Redirect(w, r, n.Link(), http.StatusSeeOther)
}

func shortVerb(verb string) string {
	if strings.HasPrefix(verb, "reviewed ") {
		return "reviewed"
	}
	return verb
}