forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package issues
2
3import (
4 "fmt"
5 "log"
6 mathrand "math/rand/v2"
7 "net/http"
8 "slices"
9 "strconv"
10 "time"
11
12 comatproto "github.com/bluesky-social/indigo/api/atproto"
13 "github.com/bluesky-social/indigo/atproto/data"
14 lexutil "github.com/bluesky-social/indigo/lex/util"
15 "github.com/go-chi/chi/v5"
16 "github.com/posthog/posthog-go"
17
18 "tangled.sh/tangled.sh/core/api/tangled"
19 "tangled.sh/tangled.sh/core/appview"
20 "tangled.sh/tangled.sh/core/appview/config"
21 "tangled.sh/tangled.sh/core/appview/db"
22 "tangled.sh/tangled.sh/core/appview/idresolver"
23 "tangled.sh/tangled.sh/core/appview/oauth"
24 "tangled.sh/tangled.sh/core/appview/pages"
25 "tangled.sh/tangled.sh/core/appview/pagination"
26 "tangled.sh/tangled.sh/core/appview/reporesolver"
27)
28
29type Issues struct {
30 oauth *oauth.OAuth
31 repoResolver *reporesolver.RepoResolver
32 pages *pages.Pages
33 idResolver *idresolver.Resolver
34 db *db.DB
35 config *config.Config
36 posthog posthog.Client
37}
38
39func New(
40 oauth *oauth.OAuth,
41 repoResolver *reporesolver.RepoResolver,
42 pages *pages.Pages,
43 idResolver *idresolver.Resolver,
44 db *db.DB,
45 config *config.Config,
46 posthog posthog.Client,
47) *Issues {
48 return &Issues{
49 oauth: oauth,
50 repoResolver: repoResolver,
51 pages: pages,
52 idResolver: idResolver,
53 db: db,
54 config: config,
55 posthog: posthog,
56 }
57}
58
59func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
60 user := rp.oauth.GetUser(r)
61 f, err := rp.repoResolver.Resolve(r)
62 if err != nil {
63 log.Println("failed to get repo and knot", err)
64 return
65 }
66
67 issueId := chi.URLParam(r, "issue")
68 issueIdInt, err := strconv.Atoi(issueId)
69 if err != nil {
70 http.Error(w, "bad issue id", http.StatusBadRequest)
71 log.Println("failed to parse issue id", err)
72 return
73 }
74
75 issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt)
76 if err != nil {
77 log.Println("failed to get issue and comments", err)
78 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
79 return
80 }
81
82 issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
83 if err != nil {
84 log.Println("failed to resolve issue owner", err)
85 }
86
87 identsToResolve := make([]string, len(comments))
88 for i, comment := range comments {
89 identsToResolve[i] = comment.OwnerDid
90 }
91 resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
92 didHandleMap := make(map[string]string)
93 for _, identity := range resolvedIds {
94 if !identity.Handle.IsInvalidHandle() {
95 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
96 } else {
97 didHandleMap[identity.DID.String()] = identity.DID.String()
98 }
99 }
100
101 rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
102 LoggedInUser: user,
103 RepoInfo: f.RepoInfo(user),
104 Issue: *issue,
105 Comments: comments,
106
107 IssueOwnerHandle: issueOwnerIdent.Handle.String(),
108 DidHandleMap: didHandleMap,
109 })
110
111}
112
113func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
114 user := rp.oauth.GetUser(r)
115 f, err := rp.repoResolver.Resolve(r)
116 if err != nil {
117 log.Println("failed to get repo and knot", err)
118 return
119 }
120
121 issueId := chi.URLParam(r, "issue")
122 issueIdInt, err := strconv.Atoi(issueId)
123 if err != nil {
124 http.Error(w, "bad issue id", http.StatusBadRequest)
125 log.Println("failed to parse issue id", err)
126 return
127 }
128
129 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
130 if err != nil {
131 log.Println("failed to get issue", err)
132 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
133 return
134 }
135
136 collaborators, err := f.Collaborators(r.Context())
137 if err != nil {
138 log.Println("failed to fetch repo collaborators: %w", err)
139 }
140 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
141 return user.Did == collab.Did
142 })
143 isIssueOwner := user.Did == issue.OwnerDid
144
145 // TODO: make this more granular
146 if isIssueOwner || isCollaborator {
147
148 closed := tangled.RepoIssueStateClosed
149
150 client, err := rp.oauth.AuthorizedClient(r)
151 if err != nil {
152 log.Println("failed to get authorized client", err)
153 return
154 }
155 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
156 Collection: tangled.RepoIssueStateNSID,
157 Repo: user.Did,
158 Rkey: appview.TID(),
159 Record: &lexutil.LexiconTypeDecoder{
160 Val: &tangled.RepoIssueState{
161 Issue: issue.IssueAt,
162 State: closed,
163 },
164 },
165 })
166
167 if err != nil {
168 log.Println("failed to update issue state", err)
169 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
170 return
171 }
172
173 err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt)
174 if err != nil {
175 log.Println("failed to close issue", err)
176 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
177 return
178 }
179
180 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
181 return
182 } else {
183 log.Println("user is not permitted to close issue")
184 http.Error(w, "for biden", http.StatusUnauthorized)
185 return
186 }
187}
188
189func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
190 user := rp.oauth.GetUser(r)
191 f, err := rp.repoResolver.Resolve(r)
192 if err != nil {
193 log.Println("failed to get repo and knot", err)
194 return
195 }
196
197 issueId := chi.URLParam(r, "issue")
198 issueIdInt, err := strconv.Atoi(issueId)
199 if err != nil {
200 http.Error(w, "bad issue id", http.StatusBadRequest)
201 log.Println("failed to parse issue id", err)
202 return
203 }
204
205 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
206 if err != nil {
207 log.Println("failed to get issue", err)
208 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
209 return
210 }
211
212 collaborators, err := f.Collaborators(r.Context())
213 if err != nil {
214 log.Println("failed to fetch repo collaborators: %w", err)
215 }
216 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
217 return user.Did == collab.Did
218 })
219 isIssueOwner := user.Did == issue.OwnerDid
220
221 if isCollaborator || isIssueOwner {
222 err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt)
223 if err != nil {
224 log.Println("failed to reopen issue", err)
225 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
226 return
227 }
228 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
229 return
230 } else {
231 log.Println("user is not the owner of the repo")
232 http.Error(w, "forbidden", http.StatusUnauthorized)
233 return
234 }
235}
236
237func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
238 user := rp.oauth.GetUser(r)
239 f, err := rp.repoResolver.Resolve(r)
240 if err != nil {
241 log.Println("failed to get repo and knot", err)
242 return
243 }
244
245 issueId := chi.URLParam(r, "issue")
246 issueIdInt, err := strconv.Atoi(issueId)
247 if err != nil {
248 http.Error(w, "bad issue id", http.StatusBadRequest)
249 log.Println("failed to parse issue id", err)
250 return
251 }
252
253 switch r.Method {
254 case http.MethodPost:
255 body := r.FormValue("body")
256 if body == "" {
257 rp.pages.Notice(w, "issue", "Body is required")
258 return
259 }
260
261 commentId := mathrand.IntN(1000000)
262 rkey := appview.TID()
263
264 err := db.NewIssueComment(rp.db, &db.Comment{
265 OwnerDid: user.Did,
266 RepoAt: f.RepoAt,
267 Issue: issueIdInt,
268 CommentId: commentId,
269 Body: body,
270 Rkey: rkey,
271 })
272 if err != nil {
273 log.Println("failed to create comment", err)
274 rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
275 return
276 }
277
278 createdAt := time.Now().Format(time.RFC3339)
279 commentIdInt64 := int64(commentId)
280 ownerDid := user.Did
281 issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt)
282 if err != nil {
283 log.Println("failed to get issue at", err)
284 rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
285 return
286 }
287
288 atUri := f.RepoAt.String()
289 client, err := rp.oauth.AuthorizedClient(r)
290 if err != nil {
291 log.Println("failed to get authorized client", err)
292 rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
293 return
294 }
295 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
296 Collection: tangled.RepoIssueCommentNSID,
297 Repo: user.Did,
298 Rkey: rkey,
299 Record: &lexutil.LexiconTypeDecoder{
300 Val: &tangled.RepoIssueComment{
301 Repo: &atUri,
302 Issue: issueAt,
303 CommentId: &commentIdInt64,
304 Owner: &ownerDid,
305 Body: body,
306 CreatedAt: createdAt,
307 },
308 },
309 })
310 if err != nil {
311 log.Println("failed to create comment", err)
312 rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
313 return
314 }
315
316 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
317 return
318 }
319}
320
321func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
322 user := rp.oauth.GetUser(r)
323 f, err := rp.repoResolver.Resolve(r)
324 if err != nil {
325 log.Println("failed to get repo and knot", err)
326 return
327 }
328
329 issueId := chi.URLParam(r, "issue")
330 issueIdInt, err := strconv.Atoi(issueId)
331 if err != nil {
332 http.Error(w, "bad issue id", http.StatusBadRequest)
333 log.Println("failed to parse issue id", err)
334 return
335 }
336
337 commentId := chi.URLParam(r, "comment_id")
338 commentIdInt, err := strconv.Atoi(commentId)
339 if err != nil {
340 http.Error(w, "bad comment id", http.StatusBadRequest)
341 log.Println("failed to parse issue id", err)
342 return
343 }
344
345 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
346 if err != nil {
347 log.Println("failed to get issue", err)
348 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
349 return
350 }
351
352 comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
353 if err != nil {
354 http.Error(w, "bad comment id", http.StatusBadRequest)
355 return
356 }
357
358 identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid)
359 if err != nil {
360 log.Println("failed to resolve did")
361 return
362 }
363
364 didHandleMap := make(map[string]string)
365 if !identity.Handle.IsInvalidHandle() {
366 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
367 } else {
368 didHandleMap[identity.DID.String()] = identity.DID.String()
369 }
370
371 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
372 LoggedInUser: user,
373 RepoInfo: f.RepoInfo(user),
374 DidHandleMap: didHandleMap,
375 Issue: issue,
376 Comment: comment,
377 })
378}
379
380func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
381 user := rp.oauth.GetUser(r)
382 f, err := rp.repoResolver.Resolve(r)
383 if err != nil {
384 log.Println("failed to get repo and knot", err)
385 return
386 }
387
388 issueId := chi.URLParam(r, "issue")
389 issueIdInt, err := strconv.Atoi(issueId)
390 if err != nil {
391 http.Error(w, "bad issue id", http.StatusBadRequest)
392 log.Println("failed to parse issue id", err)
393 return
394 }
395
396 commentId := chi.URLParam(r, "comment_id")
397 commentIdInt, err := strconv.Atoi(commentId)
398 if err != nil {
399 http.Error(w, "bad comment id", http.StatusBadRequest)
400 log.Println("failed to parse issue id", err)
401 return
402 }
403
404 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
405 if err != nil {
406 log.Println("failed to get issue", err)
407 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
408 return
409 }
410
411 comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
412 if err != nil {
413 http.Error(w, "bad comment id", http.StatusBadRequest)
414 return
415 }
416
417 if comment.OwnerDid != user.Did {
418 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
419 return
420 }
421
422 switch r.Method {
423 case http.MethodGet:
424 rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
425 LoggedInUser: user,
426 RepoInfo: f.RepoInfo(user),
427 Issue: issue,
428 Comment: comment,
429 })
430 case http.MethodPost:
431 // extract form value
432 newBody := r.FormValue("body")
433 client, err := rp.oauth.AuthorizedClient(r)
434 if err != nil {
435 log.Println("failed to get authorized client", err)
436 rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
437 return
438 }
439 rkey := comment.Rkey
440
441 // optimistic update
442 edited := time.Now()
443 err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
444 if err != nil {
445 log.Println("failed to perferom update-description query", err)
446 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
447 return
448 }
449
450 // rkey is optional, it was introduced later
451 if comment.Rkey != "" {
452 // update the record on pds
453 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey)
454 if err != nil {
455 // failed to get record
456 log.Println(err, rkey)
457 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
458 return
459 }
460 value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
461 record, _ := data.UnmarshalJSON(value)
462
463 repoAt := record["repo"].(string)
464 issueAt := record["issue"].(string)
465 createdAt := record["createdAt"].(string)
466 commentIdInt64 := int64(commentIdInt)
467
468 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
469 Collection: tangled.RepoIssueCommentNSID,
470 Repo: user.Did,
471 Rkey: rkey,
472 SwapRecord: ex.Cid,
473 Record: &lexutil.LexiconTypeDecoder{
474 Val: &tangled.RepoIssueComment{
475 Repo: &repoAt,
476 Issue: issueAt,
477 CommentId: &commentIdInt64,
478 Owner: &comment.OwnerDid,
479 Body: newBody,
480 CreatedAt: createdAt,
481 },
482 },
483 })
484 if err != nil {
485 log.Println(err)
486 }
487 }
488
489 // optimistic update for htmx
490 didHandleMap := map[string]string{
491 user.Did: user.Handle,
492 }
493 comment.Body = newBody
494 comment.Edited = &edited
495
496 // return new comment body with htmx
497 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
498 LoggedInUser: user,
499 RepoInfo: f.RepoInfo(user),
500 DidHandleMap: didHandleMap,
501 Issue: issue,
502 Comment: comment,
503 })
504 return
505
506 }
507
508}
509
510func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
511 user := rp.oauth.GetUser(r)
512 f, err := rp.repoResolver.Resolve(r)
513 if err != nil {
514 log.Println("failed to get repo and knot", err)
515 return
516 }
517
518 issueId := chi.URLParam(r, "issue")
519 issueIdInt, err := strconv.Atoi(issueId)
520 if err != nil {
521 http.Error(w, "bad issue id", http.StatusBadRequest)
522 log.Println("failed to parse issue id", err)
523 return
524 }
525
526 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
527 if err != nil {
528 log.Println("failed to get issue", err)
529 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
530 return
531 }
532
533 commentId := chi.URLParam(r, "comment_id")
534 commentIdInt, err := strconv.Atoi(commentId)
535 if err != nil {
536 http.Error(w, "bad comment id", http.StatusBadRequest)
537 log.Println("failed to parse issue id", err)
538 return
539 }
540
541 comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
542 if err != nil {
543 http.Error(w, "bad comment id", http.StatusBadRequest)
544 return
545 }
546
547 if comment.OwnerDid != user.Did {
548 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
549 return
550 }
551
552 if comment.Deleted != nil {
553 http.Error(w, "comment already deleted", http.StatusBadRequest)
554 return
555 }
556
557 // optimistic deletion
558 deleted := time.Now()
559 err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
560 if err != nil {
561 log.Println("failed to delete comment")
562 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
563 return
564 }
565
566 // delete from pds
567 if comment.Rkey != "" {
568 client, err := rp.oauth.AuthorizedClient(r)
569 if err != nil {
570 log.Println("failed to get authorized client", err)
571 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.")
572 return
573 }
574 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
575 Collection: tangled.GraphFollowNSID,
576 Repo: user.Did,
577 Rkey: comment.Rkey,
578 })
579 if err != nil {
580 log.Println(err)
581 }
582 }
583
584 // optimistic update for htmx
585 didHandleMap := map[string]string{
586 user.Did: user.Handle,
587 }
588 comment.Body = ""
589 comment.Deleted = &deleted
590
591 // htmx fragment of comment after deletion
592 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
593 LoggedInUser: user,
594 RepoInfo: f.RepoInfo(user),
595 DidHandleMap: didHandleMap,
596 Issue: issue,
597 Comment: comment,
598 })
599 return
600}
601
602func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) {
603 params := r.URL.Query()
604 state := params.Get("state")
605 isOpen := true
606 switch state {
607 case "open":
608 isOpen = true
609 case "closed":
610 isOpen = false
611 default:
612 isOpen = true
613 }
614
615 page, ok := r.Context().Value("page").(pagination.Page)
616 if !ok {
617 log.Println("failed to get page")
618 page = pagination.FirstPage()
619 }
620
621 user := rp.oauth.GetUser(r)
622 f, err := rp.repoResolver.Resolve(r)
623 if err != nil {
624 log.Println("failed to get repo and knot", err)
625 return
626 }
627
628 issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page)
629 if err != nil {
630 log.Println("failed to get issues", err)
631 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
632 return
633 }
634
635 identsToResolve := make([]string, len(issues))
636 for i, issue := range issues {
637 identsToResolve[i] = issue.OwnerDid
638 }
639 resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
640 didHandleMap := make(map[string]string)
641 for _, identity := range resolvedIds {
642 if !identity.Handle.IsInvalidHandle() {
643 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
644 } else {
645 didHandleMap[identity.DID.String()] = identity.DID.String()
646 }
647 }
648
649 rp.pages.RepoIssues(w, pages.RepoIssuesParams{
650 LoggedInUser: rp.oauth.GetUser(r),
651 RepoInfo: f.RepoInfo(user),
652 Issues: issues,
653 DidHandleMap: didHandleMap,
654 FilteringByOpen: isOpen,
655 Page: page,
656 })
657 return
658}
659
660func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) {
661 user := rp.oauth.GetUser(r)
662
663 f, err := rp.repoResolver.Resolve(r)
664 if err != nil {
665 log.Println("failed to get repo and knot", err)
666 return
667 }
668
669 switch r.Method {
670 case http.MethodGet:
671 rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
672 LoggedInUser: user,
673 RepoInfo: f.RepoInfo(user),
674 })
675 case http.MethodPost:
676 title := r.FormValue("title")
677 body := r.FormValue("body")
678
679 if title == "" || body == "" {
680 rp.pages.Notice(w, "issues", "Title and body are required")
681 return
682 }
683
684 tx, err := rp.db.BeginTx(r.Context(), nil)
685 if err != nil {
686 rp.pages.Notice(w, "issues", "Failed to create issue, try again later")
687 return
688 }
689
690 err = db.NewIssue(tx, &db.Issue{
691 RepoAt: f.RepoAt,
692 Title: title,
693 Body: body,
694 OwnerDid: user.Did,
695 })
696 if err != nil {
697 log.Println("failed to create issue", err)
698 rp.pages.Notice(w, "issues", "Failed to create issue.")
699 return
700 }
701
702 issueId, err := db.GetIssueId(rp.db, f.RepoAt)
703 if err != nil {
704 log.Println("failed to get issue id", err)
705 rp.pages.Notice(w, "issues", "Failed to create issue.")
706 return
707 }
708
709 client, err := rp.oauth.AuthorizedClient(r)
710 if err != nil {
711 log.Println("failed to get authorized client", err)
712 rp.pages.Notice(w, "issues", "Failed to create issue.")
713 return
714 }
715 atUri := f.RepoAt.String()
716 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
717 Collection: tangled.RepoIssueNSID,
718 Repo: user.Did,
719 Rkey: appview.TID(),
720 Record: &lexutil.LexiconTypeDecoder{
721 Val: &tangled.RepoIssue{
722 Repo: atUri,
723 Title: title,
724 Body: &body,
725 Owner: user.Did,
726 IssueId: int64(issueId),
727 },
728 },
729 })
730 if err != nil {
731 log.Println("failed to create issue", err)
732 rp.pages.Notice(w, "issues", "Failed to create issue.")
733 return
734 }
735
736 err = db.SetIssueAt(rp.db, f.RepoAt, issueId, resp.Uri)
737 if err != nil {
738 log.Println("failed to set issue at", err)
739 rp.pages.Notice(w, "issues", "Failed to create issue.")
740 return
741 }
742
743 if !rp.config.Core.Dev {
744 err = rp.posthog.Enqueue(posthog.Capture{
745 DistinctId: user.Did,
746 Event: "new_issue",
747 Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId},
748 })
749 if err != nil {
750 log.Println("failed to enqueue posthog event:", err)
751 }
752 }
753
754 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
755 return
756 }
757}