forked from tangled.org/core
this repo has no description

appview: issues: move to own package

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi 5ddc9dfd fdcb952d

verified
Changed files
+801 -726
appview
+757
appview/issues/issues.go
···
+
package issues
+
+
import (
+
"fmt"
+
"log"
+
mathrand "math/rand/v2"
+
"net/http"
+
"slices"
+
"strconv"
+
"time"
+
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/data"
+
lexutil "github.com/bluesky-social/indigo/lex/util"
+
"github.com/go-chi/chi/v5"
+
"github.com/posthog/posthog-go"
+
+
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview"
+
"tangled.sh/tangled.sh/core/appview/config"
+
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/idresolver"
+
"tangled.sh/tangled.sh/core/appview/oauth"
+
"tangled.sh/tangled.sh/core/appview/pages"
+
"tangled.sh/tangled.sh/core/appview/pagination"
+
"tangled.sh/tangled.sh/core/appview/reporesolver"
+
)
+
+
type Issues struct {
+
oauth *oauth.OAuth
+
repoResolver *reporesolver.RepoResolver
+
pages *pages.Pages
+
idResolver *idresolver.Resolver
+
db *db.DB
+
config *config.Config
+
posthog posthog.Client
+
}
+
+
func New(
+
oauth *oauth.OAuth,
+
repoResolver *reporesolver.RepoResolver,
+
pages *pages.Pages,
+
idResolver *idresolver.Resolver,
+
db *db.DB,
+
config *config.Config,
+
posthog posthog.Client,
+
) *Issues {
+
return &Issues{
+
oauth: oauth,
+
repoResolver: repoResolver,
+
pages: pages,
+
idResolver: idResolver,
+
db: db,
+
config: config,
+
posthog: posthog,
+
}
+
}
+
+
func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue and comments", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
+
return
+
}
+
+
issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
+
if err != nil {
+
log.Println("failed to resolve issue owner", err)
+
}
+
+
identsToResolve := make([]string, len(comments))
+
for i, comment := range comments {
+
identsToResolve[i] = comment.OwnerDid
+
}
+
resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
+
didHandleMap := make(map[string]string)
+
for _, identity := range resolvedIds {
+
if !identity.Handle.IsInvalidHandle() {
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+
} else {
+
didHandleMap[identity.DID.String()] = identity.DID.String()
+
}
+
}
+
+
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Issue: *issue,
+
Comments: comments,
+
+
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
+
DidHandleMap: didHandleMap,
+
})
+
+
}
+
+
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
+
return
+
}
+
+
collaborators, err := f.Collaborators(r.Context())
+
if err != nil {
+
log.Println("failed to fetch repo collaborators: %w", err)
+
}
+
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
+
return user.Did == collab.Did
+
})
+
isIssueOwner := user.Did == issue.OwnerDid
+
+
// TODO: make this more granular
+
if isIssueOwner || isCollaborator {
+
+
closed := tangled.RepoIssueStateClosed
+
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
log.Println("failed to get authorized client", err)
+
return
+
}
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoIssueStateNSID,
+
Repo: user.Did,
+
Rkey: appview.TID(),
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoIssueState{
+
Issue: issue.IssueAt,
+
State: closed,
+
},
+
},
+
})
+
+
if err != nil {
+
log.Println("failed to update issue state", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
+
return
+
}
+
+
err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to close issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
+
return
+
}
+
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
+
return
+
} else {
+
log.Println("user is not permitted to close issue")
+
http.Error(w, "for biden", http.StatusUnauthorized)
+
return
+
}
+
}
+
+
func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
+
return
+
}
+
+
collaborators, err := f.Collaborators(r.Context())
+
if err != nil {
+
log.Println("failed to fetch repo collaborators: %w", err)
+
}
+
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
+
return user.Did == collab.Did
+
})
+
isIssueOwner := user.Did == issue.OwnerDid
+
+
if isCollaborator || isIssueOwner {
+
err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to reopen issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
+
return
+
}
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
+
return
+
} else {
+
log.Println("user is not the owner of the repo")
+
http.Error(w, "forbidden", http.StatusUnauthorized)
+
return
+
}
+
}
+
+
func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
switch r.Method {
+
case http.MethodPost:
+
body := r.FormValue("body")
+
if body == "" {
+
rp.pages.Notice(w, "issue", "Body is required")
+
return
+
}
+
+
commentId := mathrand.IntN(1000000)
+
rkey := appview.TID()
+
+
err := db.NewIssueComment(rp.db, &db.Comment{
+
OwnerDid: user.Did,
+
RepoAt: f.RepoAt,
+
Issue: issueIdInt,
+
CommentId: commentId,
+
Body: body,
+
Rkey: rkey,
+
})
+
if err != nil {
+
log.Println("failed to create comment", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
+
return
+
}
+
+
createdAt := time.Now().Format(time.RFC3339)
+
commentIdInt64 := int64(commentId)
+
ownerDid := user.Did
+
issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue at", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
+
return
+
}
+
+
atUri := f.RepoAt.String()
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
+
return
+
}
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoIssueCommentNSID,
+
Repo: user.Did,
+
Rkey: rkey,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoIssueComment{
+
Repo: &atUri,
+
Issue: issueAt,
+
CommentId: &commentIdInt64,
+
Owner: &ownerDid,
+
Body: body,
+
CreatedAt: createdAt,
+
},
+
},
+
})
+
if err != nil {
+
log.Println("failed to create comment", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
+
return
+
}
+
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
+
return
+
}
+
}
+
+
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
commentId := chi.URLParam(r, "comment_id")
+
commentIdInt, err := strconv.Atoi(commentId)
+
if err != nil {
+
http.Error(w, "bad comment id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
+
return
+
}
+
+
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
+
if err != nil {
+
http.Error(w, "bad comment id", http.StatusBadRequest)
+
return
+
}
+
+
identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid)
+
if err != nil {
+
log.Println("failed to resolve did")
+
return
+
}
+
+
didHandleMap := make(map[string]string)
+
if !identity.Handle.IsInvalidHandle() {
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+
} else {
+
didHandleMap[identity.DID.String()] = identity.DID.String()
+
}
+
+
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
DidHandleMap: didHandleMap,
+
Issue: issue,
+
Comment: comment,
+
})
+
}
+
+
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
commentId := chi.URLParam(r, "comment_id")
+
commentIdInt, err := strconv.Atoi(commentId)
+
if err != nil {
+
http.Error(w, "bad comment id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
+
return
+
}
+
+
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
+
if err != nil {
+
http.Error(w, "bad comment id", http.StatusBadRequest)
+
return
+
}
+
+
if comment.OwnerDid != user.Did {
+
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
+
return
+
}
+
+
switch r.Method {
+
case http.MethodGet:
+
rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Issue: issue,
+
Comment: comment,
+
})
+
case http.MethodPost:
+
// extract form value
+
newBody := r.FormValue("body")
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
+
return
+
}
+
rkey := comment.Rkey
+
+
// optimistic update
+
edited := time.Now()
+
err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
+
if err != nil {
+
log.Println("failed to perferom update-description query", err)
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
+
return
+
}
+
+
// rkey is optional, it was introduced later
+
if comment.Rkey != "" {
+
// update the record on pds
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey)
+
if err != nil {
+
// failed to get record
+
log.Println(err, rkey)
+
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
+
return
+
}
+
value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
+
record, _ := data.UnmarshalJSON(value)
+
+
repoAt := record["repo"].(string)
+
issueAt := record["issue"].(string)
+
createdAt := record["createdAt"].(string)
+
commentIdInt64 := int64(commentIdInt)
+
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoIssueCommentNSID,
+
Repo: user.Did,
+
Rkey: rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoIssueComment{
+
Repo: &repoAt,
+
Issue: issueAt,
+
CommentId: &commentIdInt64,
+
Owner: &comment.OwnerDid,
+
Body: newBody,
+
CreatedAt: createdAt,
+
},
+
},
+
})
+
if err != nil {
+
log.Println(err)
+
}
+
}
+
+
// optimistic update for htmx
+
didHandleMap := map[string]string{
+
user.Did: user.Handle,
+
}
+
comment.Body = newBody
+
comment.Edited = &edited
+
+
// return new comment body with htmx
+
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
DidHandleMap: didHandleMap,
+
Issue: issue,
+
Comment: comment,
+
})
+
return
+
+
}
+
+
}
+
+
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issueId := chi.URLParam(r, "issue")
+
issueIdInt, err := strconv.Atoi(issueId)
+
if err != nil {
+
http.Error(w, "bad issue id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
+
if err != nil {
+
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
+
return
+
}
+
+
commentId := chi.URLParam(r, "comment_id")
+
commentIdInt, err := strconv.Atoi(commentId)
+
if err != nil {
+
http.Error(w, "bad comment id", http.StatusBadRequest)
+
log.Println("failed to parse issue id", err)
+
return
+
}
+
+
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
+
if err != nil {
+
http.Error(w, "bad comment id", http.StatusBadRequest)
+
return
+
}
+
+
if comment.OwnerDid != user.Did {
+
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
+
return
+
}
+
+
if comment.Deleted != nil {
+
http.Error(w, "comment already deleted", http.StatusBadRequest)
+
return
+
}
+
+
// optimistic deletion
+
deleted := time.Now()
+
err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
+
if err != nil {
+
log.Println("failed to delete comment")
+
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
+
return
+
}
+
+
// delete from pds
+
if comment.Rkey != "" {
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to delete comment.")
+
return
+
}
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.GraphFollowNSID,
+
Repo: user.Did,
+
Rkey: comment.Rkey,
+
})
+
if err != nil {
+
log.Println(err)
+
}
+
}
+
+
// optimistic update for htmx
+
didHandleMap := map[string]string{
+
user.Did: user.Handle,
+
}
+
comment.Body = ""
+
comment.Deleted = &deleted
+
+
// htmx fragment of comment after deletion
+
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
DidHandleMap: didHandleMap,
+
Issue: issue,
+
Comment: comment,
+
})
+
return
+
}
+
+
func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) {
+
params := r.URL.Query()
+
state := params.Get("state")
+
isOpen := true
+
switch state {
+
case "open":
+
isOpen = true
+
case "closed":
+
isOpen = false
+
default:
+
isOpen = true
+
}
+
+
page, ok := r.Context().Value("page").(pagination.Page)
+
if !ok {
+
log.Println("failed to get page")
+
page = pagination.FirstPage()
+
}
+
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page)
+
if err != nil {
+
log.Println("failed to get issues", err)
+
rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
+
return
+
}
+
+
identsToResolve := make([]string, len(issues))
+
for i, issue := range issues {
+
identsToResolve[i] = issue.OwnerDid
+
}
+
resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
+
didHandleMap := make(map[string]string)
+
for _, identity := range resolvedIds {
+
if !identity.Handle.IsInvalidHandle() {
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+
} else {
+
didHandleMap[identity.DID.String()] = identity.DID.String()
+
}
+
}
+
+
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
+
LoggedInUser: rp.oauth.GetUser(r),
+
RepoInfo: f.RepoInfo(user),
+
Issues: issues,
+
DidHandleMap: didHandleMap,
+
FilteringByOpen: isOpen,
+
Page: page,
+
})
+
return
+
}
+
+
func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
switch r.Method {
+
case http.MethodGet:
+
rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
})
+
case http.MethodPost:
+
title := r.FormValue("title")
+
body := r.FormValue("body")
+
+
if title == "" || body == "" {
+
rp.pages.Notice(w, "issues", "Title and body are required")
+
return
+
}
+
+
tx, err := rp.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
rp.pages.Notice(w, "issues", "Failed to create issue, try again later")
+
return
+
}
+
+
err = db.NewIssue(tx, &db.Issue{
+
RepoAt: f.RepoAt,
+
Title: title,
+
Body: body,
+
OwnerDid: user.Did,
+
})
+
if err != nil {
+
log.Println("failed to create issue", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
+
return
+
}
+
+
issueId, err := db.GetIssueId(rp.db, f.RepoAt)
+
if err != nil {
+
log.Println("failed to get issue id", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
+
return
+
}
+
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
+
return
+
}
+
atUri := f.RepoAt.String()
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoIssueNSID,
+
Repo: user.Did,
+
Rkey: appview.TID(),
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoIssue{
+
Repo: atUri,
+
Title: title,
+
Body: &body,
+
Owner: user.Did,
+
IssueId: int64(issueId),
+
},
+
},
+
})
+
if err != nil {
+
log.Println("failed to create issue", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
+
return
+
}
+
+
err = db.SetIssueAt(rp.db, f.RepoAt, issueId, resp.Uri)
+
if err != nil {
+
log.Println("failed to set issue at", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
+
return
+
}
+
+
if !rp.config.Core.Dev {
+
err = rp.posthog.Enqueue(posthog.Capture{
+
DistinctId: user.Did,
+
Event: "new_issue",
+
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId},
+
})
+
if err != nil {
+
log.Println("failed to enqueue posthog event:", err)
+
}
+
}
+
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
+
return
+
}
+
}
+34
appview/issues/router.go
···
+
package issues
+
+
import (
+
"net/http"
+
+
"github.com/go-chi/chi/v5"
+
"tangled.sh/tangled.sh/core/appview/middleware"
+
)
+
+
func (i *Issues) Router(mw *middleware.Middleware) http.Handler {
+
r := chi.NewRouter()
+
+
r.Route("/", func(r chi.Router) {
+
r.With(middleware.Paginate).Get("/", i.RepoIssues)
+
r.Get("/{issue}", i.RepoSingleIssue)
+
+
r.Group(func(r chi.Router) {
+
r.Use(middleware.AuthMiddleware(i.oauth))
+
r.Get("/new", i.NewIssue)
+
r.Post("/new", i.NewIssue)
+
r.Post("/{issue}/comment", i.NewIssueComment)
+
r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) {
+
r.Get("/", i.IssueComment)
+
r.Delete("/", i.DeleteIssueComment)
+
r.Get("/edit", i.EditIssueComment)
+
r.Post("/edit", i.EditIssueComment)
+
})
+
r.Post("/{issue}/close", i.CloseIssue)
+
r.Post("/{issue}/reopen", i.ReopenIssue)
+
})
+
})
+
+
return r
+
}
-703
appview/repo/repo.go
···
"fmt"
"io"
"log"
-
mathrand "math/rand/v2"
"net/http"
"path"
"slices"
···
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/pages/markup"
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
-
"tangled.sh/tangled.sh/core/appview/pagination"
"tangled.sh/tangled.sh/core/appview/reporesolver"
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/types"
-
"github.com/bluesky-social/indigo/atproto/data"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing"
···
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
Branches: result.Branches,
})
-
}
-
}
-
-
func (rp *Repo) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue and comments", err)
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
-
return
-
}
-
-
issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
-
if err != nil {
-
log.Println("failed to resolve issue owner", err)
-
}
-
-
identsToResolve := make([]string, len(comments))
-
for i, comment := range comments {
-
identsToResolve[i] = comment.OwnerDid
-
}
-
resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
-
didHandleMap := make(map[string]string)
-
for _, identity := range resolvedIds {
-
if !identity.Handle.IsInvalidHandle() {
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
-
} else {
-
didHandleMap[identity.DID.String()] = identity.DID.String()
-
}
-
}
-
-
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Issue: *issue,
-
Comments: comments,
-
-
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
-
DidHandleMap: didHandleMap,
-
})
-
-
}
-
-
func (rp *Repo) CloseIssue(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue", err)
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
-
return
-
}
-
-
collaborators, err := f.Collaborators(r.Context())
-
if err != nil {
-
log.Println("failed to fetch repo collaborators: %w", err)
-
}
-
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
-
return user.Did == collab.Did
-
})
-
isIssueOwner := user.Did == issue.OwnerDid
-
-
// TODO: make this more granular
-
if isIssueOwner || isCollaborator {
-
-
closed := tangled.RepoIssueStateClosed
-
-
client, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
log.Println("failed to get authorized client", err)
-
return
-
}
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoIssueStateNSID,
-
Repo: user.Did,
-
Rkey: appview.TID(),
-
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.RepoIssueState{
-
Issue: issue.IssueAt,
-
State: closed,
-
},
-
},
-
})
-
-
if err != nil {
-
log.Println("failed to update issue state", err)
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
-
return
-
}
-
-
err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to close issue", err)
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
-
return
-
}
-
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
-
return
-
} else {
-
log.Println("user is not permitted to close issue")
-
http.Error(w, "for biden", http.StatusUnauthorized)
-
return
-
}
-
}
-
-
func (rp *Repo) ReopenIssue(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue", err)
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
-
return
-
}
-
-
collaborators, err := f.Collaborators(r.Context())
-
if err != nil {
-
log.Println("failed to fetch repo collaborators: %w", err)
-
}
-
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
-
return user.Did == collab.Did
-
})
-
isIssueOwner := user.Did == issue.OwnerDid
-
-
if isCollaborator || isIssueOwner {
-
err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to reopen issue", err)
-
rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
-
return
-
}
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
-
return
-
} else {
-
log.Println("user is not the owner of the repo")
-
http.Error(w, "forbidden", http.StatusUnauthorized)
-
return
-
}
-
}
-
-
func (rp *Repo) NewIssueComment(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
switch r.Method {
-
case http.MethodPost:
-
body := r.FormValue("body")
-
if body == "" {
-
rp.pages.Notice(w, "issue", "Body is required")
-
return
-
}
-
-
commentId := mathrand.IntN(1000000)
-
rkey := appview.TID()
-
-
err := db.NewIssueComment(rp.db, &db.Comment{
-
OwnerDid: user.Did,
-
RepoAt: f.RepoAt,
-
Issue: issueIdInt,
-
CommentId: commentId,
-
Body: body,
-
Rkey: rkey,
-
})
-
if err != nil {
-
log.Println("failed to create comment", err)
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
-
return
-
}
-
-
createdAt := time.Now().Format(time.RFC3339)
-
commentIdInt64 := int64(commentId)
-
ownerDid := user.Did
-
issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue at", err)
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
-
return
-
}
-
-
atUri := f.RepoAt.String()
-
client, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
log.Println("failed to get authorized client", err)
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
-
return
-
}
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoIssueCommentNSID,
-
Repo: user.Did,
-
Rkey: rkey,
-
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.RepoIssueComment{
-
Repo: &atUri,
-
Issue: issueAt,
-
CommentId: &commentIdInt64,
-
Owner: &ownerDid,
-
Body: body,
-
CreatedAt: createdAt,
-
},
-
},
-
})
-
if err != nil {
-
log.Println("failed to create comment", err)
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
-
return
-
}
-
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
-
return
-
}
-
}
-
-
func (rp *Repo) IssueComment(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
commentId := chi.URLParam(r, "comment_id")
-
commentIdInt, err := strconv.Atoi(commentId)
-
if err != nil {
-
http.Error(w, "bad comment id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue", err)
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
-
return
-
}
-
-
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
-
if err != nil {
-
http.Error(w, "bad comment id", http.StatusBadRequest)
-
return
-
}
-
-
identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid)
-
if err != nil {
-
log.Println("failed to resolve did")
-
return
-
}
-
-
didHandleMap := make(map[string]string)
-
if !identity.Handle.IsInvalidHandle() {
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
-
} else {
-
didHandleMap[identity.DID.String()] = identity.DID.String()
-
}
-
-
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
DidHandleMap: didHandleMap,
-
Issue: issue,
-
Comment: comment,
-
})
-
}
-
-
func (rp *Repo) EditIssueComment(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
commentId := chi.URLParam(r, "comment_id")
-
commentIdInt, err := strconv.Atoi(commentId)
-
if err != nil {
-
http.Error(w, "bad comment id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue", err)
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
-
return
-
}
-
-
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
-
if err != nil {
-
http.Error(w, "bad comment id", http.StatusBadRequest)
-
return
-
}
-
-
if comment.OwnerDid != user.Did {
-
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
-
return
-
}
-
-
switch r.Method {
-
case http.MethodGet:
-
rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Issue: issue,
-
Comment: comment,
-
})
-
case http.MethodPost:
-
// extract form value
-
newBody := r.FormValue("body")
-
client, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
log.Println("failed to get authorized client", err)
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
-
return
-
}
-
rkey := comment.Rkey
-
-
// optimistic update
-
edited := time.Now()
-
err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
-
if err != nil {
-
log.Println("failed to perferom update-description query", err)
-
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
-
return
-
}
-
-
// rkey is optional, it was introduced later
-
if comment.Rkey != "" {
-
// update the record on pds
-
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey)
-
if err != nil {
-
// failed to get record
-
log.Println(err, rkey)
-
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
-
return
-
}
-
value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
-
record, _ := data.UnmarshalJSON(value)
-
-
repoAt := record["repo"].(string)
-
issueAt := record["issue"].(string)
-
createdAt := record["createdAt"].(string)
-
commentIdInt64 := int64(commentIdInt)
-
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoIssueCommentNSID,
-
Repo: user.Did,
-
Rkey: rkey,
-
SwapRecord: ex.Cid,
-
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.RepoIssueComment{
-
Repo: &repoAt,
-
Issue: issueAt,
-
CommentId: &commentIdInt64,
-
Owner: &comment.OwnerDid,
-
Body: newBody,
-
CreatedAt: createdAt,
-
},
-
},
-
})
-
if err != nil {
-
log.Println(err)
-
}
-
}
-
-
// optimistic update for htmx
-
didHandleMap := map[string]string{
-
user.Did: user.Handle,
-
}
-
comment.Body = newBody
-
comment.Edited = &edited
-
-
// return new comment body with htmx
-
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
DidHandleMap: didHandleMap,
-
Issue: issue,
-
Comment: comment,
-
})
-
return
-
-
}
-
-
}
-
-
func (rp *Repo) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issueId := chi.URLParam(r, "issue")
-
issueIdInt, err := strconv.Atoi(issueId)
-
if err != nil {
-
http.Error(w, "bad issue id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
-
if err != nil {
-
log.Println("failed to get issue", err)
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
-
return
-
}
-
-
commentId := chi.URLParam(r, "comment_id")
-
commentIdInt, err := strconv.Atoi(commentId)
-
if err != nil {
-
http.Error(w, "bad comment id", http.StatusBadRequest)
-
log.Println("failed to parse issue id", err)
-
return
-
}
-
-
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
-
if err != nil {
-
http.Error(w, "bad comment id", http.StatusBadRequest)
-
return
-
}
-
-
if comment.OwnerDid != user.Did {
-
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
-
return
-
}
-
-
if comment.Deleted != nil {
-
http.Error(w, "comment already deleted", http.StatusBadRequest)
-
return
-
}
-
-
// optimistic deletion
-
deleted := time.Now()
-
err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
-
if err != nil {
-
log.Println("failed to delete comment")
-
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
-
return
-
}
-
-
// delete from pds
-
if comment.Rkey != "" {
-
client, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
log.Println("failed to get authorized client", err)
-
rp.pages.Notice(w, "issue-comment", "Failed to delete comment.")
-
return
-
}
-
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
-
Collection: tangled.GraphFollowNSID,
-
Repo: user.Did,
-
Rkey: comment.Rkey,
-
})
-
if err != nil {
-
log.Println(err)
-
}
-
}
-
-
// optimistic update for htmx
-
didHandleMap := map[string]string{
-
user.Did: user.Handle,
-
}
-
comment.Body = ""
-
comment.Deleted = &deleted
-
-
// htmx fragment of comment after deletion
-
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
DidHandleMap: didHandleMap,
-
Issue: issue,
-
Comment: comment,
-
})
-
return
-
}
-
-
func (rp *Repo) RepoIssues(w http.ResponseWriter, r *http.Request) {
-
params := r.URL.Query()
-
state := params.Get("state")
-
isOpen := true
-
switch state {
-
case "open":
-
isOpen = true
-
case "closed":
-
isOpen = false
-
default:
-
isOpen = true
-
}
-
-
page, ok := r.Context().Value("page").(pagination.Page)
-
if !ok {
-
log.Println("failed to get page")
-
page = pagination.FirstPage()
-
}
-
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page)
-
if err != nil {
-
log.Println("failed to get issues", err)
-
rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
-
return
-
}
-
-
identsToResolve := make([]string, len(issues))
-
for i, issue := range issues {
-
identsToResolve[i] = issue.OwnerDid
-
}
-
resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
-
didHandleMap := make(map[string]string)
-
for _, identity := range resolvedIds {
-
if !identity.Handle.IsInvalidHandle() {
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
-
} else {
-
didHandleMap[identity.DID.String()] = identity.DID.String()
-
}
-
}
-
-
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
-
LoggedInUser: rp.oauth.GetUser(r),
-
RepoInfo: f.RepoInfo(user),
-
Issues: issues,
-
DidHandleMap: didHandleMap,
-
FilteringByOpen: isOpen,
-
Page: page,
-
})
-
return
-
}
-
-
func (rp *Repo) NewIssue(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
return
-
}
-
-
switch r.Method {
-
case http.MethodGet:
-
rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
})
-
case http.MethodPost:
-
title := r.FormValue("title")
-
body := r.FormValue("body")
-
-
if title == "" || body == "" {
-
rp.pages.Notice(w, "issues", "Title and body are required")
-
return
-
}
-
-
tx, err := rp.db.BeginTx(r.Context(), nil)
-
if err != nil {
-
rp.pages.Notice(w, "issues", "Failed to create issue, try again later")
-
return
-
}
-
-
err = db.NewIssue(tx, &db.Issue{
-
RepoAt: f.RepoAt,
-
Title: title,
-
Body: body,
-
OwnerDid: user.Did,
-
})
-
if err != nil {
-
log.Println("failed to create issue", err)
-
rp.pages.Notice(w, "issues", "Failed to create issue.")
-
return
-
}
-
-
issueId, err := db.GetIssueId(rp.db, f.RepoAt)
-
if err != nil {
-
log.Println("failed to get issue id", err)
-
rp.pages.Notice(w, "issues", "Failed to create issue.")
-
return
-
}
-
-
client, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
log.Println("failed to get authorized client", err)
-
rp.pages.Notice(w, "issues", "Failed to create issue.")
-
return
-
}
-
atUri := f.RepoAt.String()
-
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoIssueNSID,
-
Repo: user.Did,
-
Rkey: appview.TID(),
-
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.RepoIssue{
-
Repo: atUri,
-
Title: title,
-
Body: &body,
-
Owner: user.Did,
-
IssueId: int64(issueId),
-
},
-
},
-
})
-
if err != nil {
-
log.Println("failed to create issue", err)
-
rp.pages.Notice(w, "issues", "Failed to create issue.")
-
return
-
}
-
-
err = db.SetIssueAt(rp.db, f.RepoAt, issueId, resp.Uri)
-
if err != nil {
-
log.Println("failed to set issue at", err)
-
rp.pages.Notice(w, "issues", "Failed to create issue.")
-
return
-
}
-
-
if !rp.config.Core.Dev {
-
err = rp.posthog.Enqueue(posthog.Capture{
-
DistinctId: user.Did,
-
Event: "new_issue",
-
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId},
-
})
-
if err != nil {
-
log.Println("failed to enqueue posthog event:", err)
-
}
-
}
-
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
-
return
-20
appview/repo/router.go
···
r.Get("/blob/{ref}/*", rp.RepoBlob)
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
-
r.Route("/issues", func(r chi.Router) {
-
r.With(middleware.Paginate).Get("/", rp.RepoIssues)
-
r.Get("/{issue}", rp.RepoSingleIssue)
-
-
r.Group(func(r chi.Router) {
-
r.Use(middleware.AuthMiddleware(rp.oauth))
-
r.Get("/new", rp.NewIssue)
-
r.Post("/new", rp.NewIssue)
-
r.Post("/{issue}/comment", rp.NewIssueComment)
-
r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) {
-
r.Get("/", rp.IssueComment)
-
r.Delete("/", rp.DeleteIssueComment)
-
r.Get("/edit", rp.EditIssueComment)
-
r.Post("/edit", rp.EditIssueComment)
-
})
-
r.Post("/{issue}/close", rp.CloseIssue)
-
r.Post("/{issue}/reopen", rp.ReopenIssue)
-
})
-
})
-
r.Route("/fork", func(r chi.Router) {
r.Use(middleware.AuthMiddleware(rp.oauth))
r.Get("/", rp.ForkRepo)
+10 -3
appview/state/router.go
···
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
+
"tangled.sh/tangled.sh/core/appview/issues"
"tangled.sh/tangled.sh/core/appview/middleware"
-
oauth "tangled.sh/tangled.sh/core/appview/oauth/handler"
+
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
"tangled.sh/tangled.sh/core/appview/pulls"
"tangled.sh/tangled.sh/core/appview/repo"
"tangled.sh/tangled.sh/core/appview/settings"
···
r.Use(mw.GoImport())
r.Mount("/", s.RepoRouter(mw))
-
+
r.Mount("/issues", s.IssuesRouter(mw))
r.Mount("/pulls", s.PullsRouter(mw))
// These routes get proxied to the knot
···
func (s *State) OAuthRouter() http.Handler {
store := sessions.NewCookieStore([]byte(s.config.Core.CookieSecret))
-
oauth := oauth.New(s.config, s.pages, s.idResolver, s.db, store, s.oauth, s.enforcer, s.posthog)
+
oauth := oauthhandler.New(s.config, s.pages, s.idResolver, s.db, store, s.oauth, s.enforcer, s.posthog)
return oauth.Router()
}
···
}
return settings.Router()
+
}
+
+
func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler {
+
issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.posthog)
+
return issues.Router(mw)
+
}
func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler {