A vibe coded tangled fork which supports pijul.
1package handler
2
3import (
4 "errors"
5 "fmt"
6 "net/http"
7
8 "tangled.org/core/api/tangled"
9 "tangled.org/core/appview/db"
10 "tangled.org/core/appview/models"
11 "tangled.org/core/appview/pages"
12 "tangled.org/core/appview/pagination"
13 "tangled.org/core/appview/reporesolver"
14 isvc "tangled.org/core/appview/service/issue"
15 rsvc "tangled.org/core/appview/service/repo"
16 "tangled.org/core/appview/session"
17 "tangled.org/core/appview/web/request"
18 "tangled.org/core/log"
19 "tangled.org/core/orm"
20)
21
22func RepoIssues(is isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc {
23 return func(w http.ResponseWriter, r *http.Request) {
24 ctx := r.Context()
25 l := log.FromContext(ctx).With("handler", "RepoIssues")
26 repo, ok := request.RepoFromContext(ctx)
27 if !ok {
28 l.Error("malformed request")
29 p.Error503(w)
30 return
31 }
32 repoOwnerId, ok := request.OwnerFromContext(ctx)
33 if !ok {
34 l.Error("malformed request")
35 p.Error503(w)
36 return
37 }
38
39 query := r.URL.Query()
40 searchOpts := models.IssueSearchOptions{
41 RepoAt: repo.RepoAt().String(),
42 Keyword: query.Get("q"),
43 IsOpen: query.Get("state") != "closed",
44 Page: pagination.FromContext(ctx),
45 }
46
47 issues, err := is.GetIssues(ctx, repo, searchOpts)
48 if err != nil {
49 l.Error("failed to get issues")
50 p.Error503(w)
51 return
52 }
53
54 // render page
55 err = func() error {
56 labelDefs, err := db.GetLabelDefinitions(
57 d,
58 orm.FilterIn("at_uri", repo.Labels),
59 orm.FilterContains("scope", tangled.RepoIssueNSID),
60 )
61 if err != nil {
62 return err
63 }
64 defs := make(map[string]*models.LabelDefinition)
65 for _, l := range labelDefs {
66 defs[l.AtUri().String()] = &l
67 }
68 return p.RepoIssues(w, pages.RepoIssuesParams{
69 LoggedInUser: session.UserFromContext(ctx),
70 RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, repo, "", ""),
71
72 Issues: issues,
73 LabelDefs: defs,
74 FilteringByOpen: searchOpts.IsOpen,
75 FilterQuery: searchOpts.Keyword,
76 Page: searchOpts.Page,
77 })
78 }()
79 if err != nil {
80 l.Error("failed to render", "err", err)
81 p.Error503(w)
82 return
83 }
84 }
85}
86
87func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc {
88 return func(w http.ResponseWriter, r *http.Request) {
89 ctx := r.Context()
90 l := log.FromContext(ctx).With("handler", "Issue")
91 issue, ok := request.IssueFromContext(ctx)
92 if !ok {
93 l.Error("malformed request, failed to get issue")
94 p.Error503(w)
95 return
96 }
97 repoOwnerId, ok := request.OwnerFromContext(ctx)
98 if !ok {
99 l.Error("malformed request")
100 p.Error503(w)
101 return
102 }
103
104 // render
105 err := func() error {
106 reactionMap, err := db.GetReactionMap(d, 20, issue.AtUri())
107 if err != nil {
108 l.Error("failed to get issue reactions", "err", err)
109 return err
110 }
111
112 userReactions := map[models.ReactionKind]bool{}
113 if sess, ok := session.FromContext(ctx); ok {
114 userReactions = db.GetReactionStatusMap(d, sess.User.Did, issue.AtUri())
115 }
116
117 backlinks, err := db.GetBacklinks(d, issue.AtUri())
118 if err != nil {
119 l.Error("failed to fetch backlinks", "err", err)
120 return err
121 }
122
123 labelDefs, err := db.GetLabelDefinitions(
124 d,
125 orm.FilterIn("at_uri", issue.Repo.Labels),
126 orm.FilterContains("scope", tangled.RepoIssueNSID),
127 )
128 if err != nil {
129 l.Error("failed to fetch label defs", "err", err)
130 return err
131 }
132
133 defs := make(map[string]*models.LabelDefinition)
134 for _, l := range labelDefs {
135 defs[l.AtUri().String()] = &l
136 }
137
138 return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{
139 LoggedInUser: session.UserFromContext(ctx),
140 RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, issue.Repo, "", ""),
141 Issue: issue,
142 CommentList: issue.CommentList(),
143 Backlinks: backlinks,
144 Reactions: reactionMap,
145 UserReacted: userReactions,
146 LabelDefs: defs,
147 })
148 }()
149 if err != nil {
150 l.Error("failed to render", "err", err)
151 p.Error503(w)
152 return
153 }
154 }
155}
156
157func NewIssue(rs rsvc.Service, p *pages.Pages) http.HandlerFunc {
158 return func(w http.ResponseWriter, r *http.Request) {
159 ctx := r.Context()
160 l := log.FromContext(ctx).With("handler", "NewIssue")
161
162 // render
163 err := func() error {
164 repo, ok := request.RepoFromContext(ctx)
165 if !ok {
166 return fmt.Errorf("malformed request")
167 }
168 repoOwnerId, ok := request.OwnerFromContext(ctx)
169 if !ok {
170 return fmt.Errorf("malformed request")
171 }
172 return p.RepoNewIssue(w, pages.RepoNewIssueParams{
173 LoggedInUser: session.UserFromContext(ctx),
174 RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, repo, "", ""),
175 })
176 }()
177 if err != nil {
178 l.Error("failed to render", "err", err)
179 p.Error503(w)
180 return
181 }
182 }
183}
184
185func NewIssuePost(is isvc.Service, p *pages.Pages) http.HandlerFunc {
186 noticeId := "issues"
187 return func(w http.ResponseWriter, r *http.Request) {
188 ctx := r.Context()
189 l := log.FromContext(ctx).With("handler", "NewIssuePost")
190 repo, ok := request.RepoFromContext(ctx)
191 if !ok {
192 l.Error("malformed request, failed to get repo")
193 // TODO: 503 error with more detailed messages
194 p.Error503(w)
195 return
196 }
197 var (
198 title = r.FormValue("title")
199 body = r.FormValue("body")
200 )
201
202 issue, err := is.NewIssue(ctx, repo, title, body)
203 if err != nil {
204 if errors.Is(err, isvc.ErrDatabaseFail) {
205 p.Notice(w, noticeId, "Failed to create issue.")
206 } else if errors.Is(err, isvc.ErrPDSFail) {
207 p.Notice(w, noticeId, "Failed to create issue.")
208 } else {
209 p.Notice(w, noticeId, "Failed to create issue.")
210 }
211 return
212 }
213 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo)
214 p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
215 }
216}
217
218func IssueEdit(is isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc {
219 return func(w http.ResponseWriter, r *http.Request) {
220 ctx := r.Context()
221 l := log.FromContext(ctx).With("handler", "IssueEdit")
222 issue, ok := request.IssueFromContext(ctx)
223 if !ok {
224 l.Error("malformed request, failed to get issue")
225 p.Error503(w)
226 return
227 }
228 repoOwnerId, ok := request.OwnerFromContext(ctx)
229 if !ok {
230 l.Error("malformed request")
231 p.Error503(w)
232 return
233 }
234
235 // render
236 err := func() error {
237 return p.EditIssueFragment(w, pages.EditIssueParams{
238 LoggedInUser: session.UserFromContext(ctx),
239 RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, issue.Repo, "", ""),
240
241 Issue: issue,
242 })
243 }()
244 if err != nil {
245 l.Error("failed to render", "err", err)
246 p.Error503(w)
247 return
248 }
249 }
250}
251
252func IssueEditPost(is isvc.Service, p *pages.Pages) http.HandlerFunc {
253 noticeId := "issues"
254 return func(w http.ResponseWriter, r *http.Request) {
255 ctx := r.Context()
256 l := log.FromContext(ctx).With("handler", "IssueEdit")
257 issue, ok := request.IssueFromContext(ctx)
258 if !ok {
259 l.Error("malformed request, failed to get issue")
260 p.Error503(w)
261 return
262 }
263
264 newIssue := *issue
265 newIssue.Title = r.FormValue("title")
266 newIssue.Body = r.FormValue("body")
267
268 err := is.EditIssue(ctx, &newIssue)
269 if err != nil {
270 if errors.Is(err, isvc.ErrDatabaseFail) {
271 p.Notice(w, noticeId, "Failed to edit issue.")
272 } else if errors.Is(err, isvc.ErrPDSFail) {
273 p.Notice(w, noticeId, "Failed to edit issue.")
274 } else {
275 p.Notice(w, noticeId, "Failed to edit issue.")
276 }
277 return
278 }
279
280 p.HxRefresh(w)
281 }
282}
283
284func CloseIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc {
285 noticeId := "issue-action"
286 return func(w http.ResponseWriter, r *http.Request) {
287 ctx := r.Context()
288 l := log.FromContext(ctx).With("handler", "CloseIssue")
289 issue, ok := request.IssueFromContext(ctx)
290 if !ok {
291 l.Error("malformed request, failed to get issue")
292 p.Error503(w)
293 return
294 }
295
296 err := is.CloseIssue(ctx, issue)
297 if err != nil {
298 if errors.Is(err, isvc.ErrForbidden) {
299 http.Error(w, "forbidden", http.StatusUnauthorized)
300 } else {
301 p.Notice(w, noticeId, "Failed to close issue. Try again later.")
302 }
303 return
304 }
305
306 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo)
307 p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
308 }
309}
310
311func ReopenIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc {
312 noticeId := "issue-action"
313 return func(w http.ResponseWriter, r *http.Request) {
314 ctx := r.Context()
315 l := log.FromContext(ctx).With("handler", "ReopenIssue")
316 issue, ok := request.IssueFromContext(ctx)
317 if !ok {
318 l.Error("malformed request, failed to get issue")
319 p.Error503(w)
320 return
321 }
322
323 err := is.ReopenIssue(ctx, issue)
324 if err != nil {
325 if errors.Is(err, isvc.ErrForbidden) {
326 http.Error(w, "forbidden", http.StatusUnauthorized)
327 } else {
328 p.Notice(w, noticeId, "Failed to reopen issue. Try again later.")
329 }
330 return
331 }
332
333 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo)
334 p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
335 }
336}
337
338func IssueDelete(s isvc.Service, p *pages.Pages) http.HandlerFunc {
339 noticeId := "issue-actions-error"
340 return func(w http.ResponseWriter, r *http.Request) {
341 ctx := r.Context()
342 l := log.FromContext(ctx).With("handler", "IssueDelete")
343 issue, ok := request.IssueFromContext(ctx)
344 if !ok {
345 l.Error("failed to get issue")
346 // TODO: 503 error with more detailed messages
347 p.Error503(w)
348 return
349 }
350 err := s.DeleteIssue(ctx, issue)
351 if err != nil {
352 p.Notice(w, noticeId, "failed to delete issue")
353 return
354 }
355 p.HxLocation(w, "/")
356 }
357}