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