A vibe coded tangled fork which supports pijul.
at 5bdb64729ac3b4877615661d0ae6449104951cfb 357 lines 9.5 kB view raw
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}