diff --git a/appview/ingester.go b/appview/ingester.go index 59d011ae..dbbd125e 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -18,6 +18,7 @@ import ( "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/pages/markup" "tangled.sh/tangled.sh/core/appview/serververify" + "tangled.sh/tangled.sh/core/appview/validator" "tangled.sh/tangled.sh/core/idresolver" "tangled.sh/tangled.sh/core/rbac" ) @@ -28,6 +29,7 @@ type Ingester struct { IdResolver *idresolver.Resolver Config *config.Config Logger *slog.Logger + Validator *validator.Validator } type processFunc func(ctx context.Context, e *models.Event) error @@ -875,9 +877,8 @@ func (i *Ingester) ingestIssueComment(e *models.Event) error { return fmt.Errorf("failed to parse comment from record: %w", err) } - sanitizer := markup.NewSanitizer() - if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { - return fmt.Errorf("body is empty after HTML sanitization") + if err := i.Validator.ValidateIssueComment(comment); err != nil { + return fmt.Errorf("failed to validate comment: %w", err) } _, err = db.AddIssueComment(ddb, *comment) diff --git a/appview/issues/issues.go b/appview/issues/issues.go index bbe1036b..de509f41 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -53,6 +53,7 @@ func New( db *db.DB, config *config.Config, notifier notify.Notifier, + validator *validator.Validator, ) *Issues { return &Issues{ oauth: oauth, @@ -102,7 +103,6 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { Reactions: reactionCountMap, UserReacted: userReactions, }) - } func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html index 23668d1b..5ec26337 100644 --- a/appview/pages/templates/layouts/repobase.html +++ b/appview/pages/templates/layouts/repobase.html @@ -42,7 +42,7 @@
{{ block "repoContent" . }}{{ end }}
diff --git a/appview/pages/templates/repo/issues/fragments/commentList.html b/appview/pages/templates/repo/issues/fragments/commentList.html new file mode 100644 index 00000000..da98f53b --- /dev/null +++ b/appview/pages/templates/repo/issues/fragments/commentList.html @@ -0,0 +1,64 @@ +{{ define "repo/issues/fragments/commentList" }} +
+ {{ range $item := .CommentList }} + {{ template "commentListing" (list $ .) }} + {{ end }} +
+{{ end }} + +{{ define "commentListing" }} + {{ $root := index . 0 }} + {{ $comment := index . 1 }} + {{ $params := + (dict + "RepoInfo" $root.RepoInfo + "LoggedInUser" $root.LoggedInUser + "Issue" $root.Issue + "Comment" $comment.Self) }} + +
+ {{ template "topLevelComment" $params }} + +
+ {{ range $index, $reply := $comment.Replies }} +
+ +
+ +
+ {{ + template "replyComment" + (dict + "RepoInfo" $root.RepoInfo + "LoggedInUser" $root.LoggedInUser + "Issue" $root.Issue + "Comment" $reply) + }} +
+
+ {{ end }} +
+ + {{ if $root.LoggedInUser }} + {{ template "repo/issues/fragments/replyIssueCommentPlaceholder" $params }} + {{ else }} +
+ login to reply to this discussion +
+ {{ end }} +
+{{ end }} + +{{ define "topLevelComment" }} +
+ {{ template "repo/issues/fragments/issueCommentHeader" . }} + {{ template "repo/issues/fragments/issueCommentBody" . }} +
+{{ end }} + +{{ define "replyComment" }} +
+ {{ template "repo/issues/fragments/issueCommentHeader" . }} + {{ template "repo/issues/fragments/issueCommentBody" . }} +
+{{ end }} diff --git a/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html b/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html new file mode 100644 index 00000000..4cd53b63 --- /dev/null +++ b/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html @@ -0,0 +1,18 @@ +{{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }} +
+ + + +
+{{ end }} diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html index 8ef2545d..40343789 100644 --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -9,7 +9,7 @@ {{ end }} {{ define "repoContent" }} -
+

{{ .Issue.Title | description }} #{{ .Issue.IssueId }} @@ -39,7 +39,7 @@

{{ if .Issue.Body }} -
+
{{ .Issue.Body | markdown }}
{{ end }} diff --git a/appview/state/router.go b/appview/state/router.go index 86edfd7d..fe28e712 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -232,7 +232,7 @@ func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler { } 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.notifier) + issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier, s.validator) return issues.Router(mw) } diff --git a/appview/state/state.go b/appview/state/state.go index 9abda38a..932a129f 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -28,6 +28,7 @@ import ( "tangled.sh/tangled.sh/core/appview/pages" posthogService "tangled.sh/tangled.sh/core/appview/posthog" "tangled.sh/tangled.sh/core/appview/reporesolver" + "tangled.sh/tangled.sh/core/appview/validator" xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient" "tangled.sh/tangled.sh/core/eventconsumer" "tangled.sh/tangled.sh/core/idresolver" @@ -53,6 +54,7 @@ type State struct { knotstream *eventconsumer.Consumer spindlestream *eventconsumer.Consumer logger *slog.Logger + validator *validator.Validator } func Make(ctx context.Context, config *config.Config) (*State, error) { @@ -73,11 +75,10 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { } pgs := pages.NewPages(config, res) - cache := cache.New(config.Redis.Addr) sess := session.New(cache) - oauth := oauth.NewOAuth(config, sess) + validator := validator.New(d) posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint}) if err != nil { @@ -121,6 +122,7 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { IdResolver: res, Config: config, Logger: tlog.New("ingester"), + Validator: validator, } err = jc.StartJetstream(ctx, ingester.Ingest()) if err != nil { @@ -160,6 +162,7 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { knotstream, spindlestream, slog.Default(), + validator, } return state, nil diff --git a/appview/validator/issue.go b/appview/validator/issue.go new file mode 100644 index 00000000..b505d456 --- /dev/null +++ b/appview/validator/issue.go @@ -0,0 +1,35 @@ +package validator + +import ( + "fmt" + "strings" + + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/pages/markup" +) + +func (v *Validator) ValidateIssueComment(comment *db.IssueComment) error { + // if comments have parents, only ingest ones that are 1 level deep + if comment.ReplyTo != nil { + parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo)) + if err != nil { + return fmt.Errorf("failed to fetch parent comment: %w", err) + } + if len(parents) != 1 { + return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) + } + + // depth check + parent := parents[0] + if parent.ReplyTo != nil { + return fmt.Errorf("incorrect depth, this comment is replying at depth >1") + } + } + + sanitizer := markup.NewSanitizer() + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { + return fmt.Errorf("body is empty after HTML sanitization") + } + + return nil +} diff --git a/appview/validator/validator.go b/appview/validator/validator.go new file mode 100644 index 00000000..cd1f41df --- /dev/null +++ b/appview/validator/validator.go @@ -0,0 +1,13 @@ +package validator + +import "tangled.sh/tangled.sh/core/appview/db" + +type Validator struct { + db *db.DB +} + +func New(db *db.DB) *Validator { + return &Validator{ + db: db, + } +}