A vibe coded tangled fork which supports pijul.
at bc79d65e925e99649841ce39fad638b6a2bb6414 390 lines 10 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 "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}