+4
-3
appview/ingester.go
+4
-3
appview/ingester.go
···
18
18
"tangled.sh/tangled.sh/core/appview/db"
19
19
"tangled.sh/tangled.sh/core/appview/pages/markup"
20
20
"tangled.sh/tangled.sh/core/appview/serververify"
21
+
"tangled.sh/tangled.sh/core/appview/validator"
21
22
"tangled.sh/tangled.sh/core/idresolver"
22
23
"tangled.sh/tangled.sh/core/rbac"
23
24
)
···
28
29
IdResolver *idresolver.Resolver
29
30
Config *config.Config
30
31
Logger *slog.Logger
32
+
Validator *validator.Validator
31
33
}
32
34
33
35
type processFunc func(ctx context.Context, e *models.Event) error
···
875
877
return fmt.Errorf("failed to parse comment from record: %w", err)
876
878
}
877
879
878
-
sanitizer := markup.NewSanitizer()
879
-
if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" {
880
-
return fmt.Errorf("body is empty after HTML sanitization")
880
+
if err := i.Validator.ValidateIssueComment(comment); err != nil {
881
+
return fmt.Errorf("failed to validate comment: %w", err)
881
882
}
882
883
883
884
_, err = db.AddIssueComment(ddb, *comment)
+1
-1
appview/issues/issues.go
+1
-1
appview/issues/issues.go
···
53
53
db *db.DB,
54
54
config *config.Config,
55
55
notifier notify.Notifier,
56
+
validator *validator.Validator,
56
57
) *Issues {
57
58
return &Issues{
58
59
oauth: oauth,
···
102
103
Reactions: reactionCountMap,
103
104
UserReacted: userReactions,
104
105
})
105
-
106
106
}
107
107
108
108
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
+2
-2
appview/pages/templates/layouts/repobase.html
+2
-2
appview/pages/templates/layouts/repobase.html
···
42
42
</section>
43
43
44
44
<section
45
-
class="w-full flex flex-col drop-shadow-sm"
45
+
class="w-full flex flex-col"
46
46
>
47
47
<nav class="w-full pl-4 overflow-auto">
48
48
<div class="flex z-60">
···
81
81
</div>
82
82
</nav>
83
83
<section
84
-
class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"
84
+
class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white"
85
85
>
86
86
{{ block "repoContent" . }}{{ end }}
87
87
</section>
+64
appview/pages/templates/repo/issues/fragments/commentList.html
+64
appview/pages/templates/repo/issues/fragments/commentList.html
···
1
+
{{ define "repo/issues/fragments/commentList" }}
2
+
<div class="flex flex-col gap-8">
3
+
{{ range $item := .CommentList }}
4
+
{{ template "commentListing" (list $ .) }}
5
+
{{ end }}
6
+
<div>
7
+
{{ end }}
8
+
9
+
{{ define "commentListing" }}
10
+
{{ $root := index . 0 }}
11
+
{{ $comment := index . 1 }}
12
+
{{ $params :=
13
+
(dict
14
+
"RepoInfo" $root.RepoInfo
15
+
"LoggedInUser" $root.LoggedInUser
16
+
"Issue" $root.Issue
17
+
"Comment" $comment.Self) }}
18
+
19
+
<div class="rounded border border-gray-300 dark:border-gray-700 w-full overflow-hidden shadow-sm">
20
+
{{ template "topLevelComment" $params }}
21
+
22
+
<div class="relative ml-4 border-l border-gray-300 dark:border-gray-700">
23
+
{{ range $index, $reply := $comment.Replies }}
24
+
<div class="relative ">
25
+
<!-- Horizontal connector -->
26
+
<div class="absolute left-0 top-6 w-4 h-px bg-gray-300 dark:bg-gray-700"></div>
27
+
28
+
<div class="pl-2">
29
+
{{
30
+
template "replyComment"
31
+
(dict
32
+
"RepoInfo" $root.RepoInfo
33
+
"LoggedInUser" $root.LoggedInUser
34
+
"Issue" $root.Issue
35
+
"Comment" $reply)
36
+
}}
37
+
</div>
38
+
</div>
39
+
{{ end }}
40
+
</div>
41
+
42
+
{{ if $root.LoggedInUser }}
43
+
{{ template "repo/issues/fragments/replyIssueCommentPlaceholder" $params }}
44
+
{{ else }}
45
+
<div class="p-2 border-t border-gray-300 dark:border-gray-700 text-gray-500 dark:text-gray-400">
46
+
<a class="underline" href="/login">login</a> to reply to this discussion
47
+
</div>
48
+
{{ end }}
49
+
</div>
50
+
{{ end }}
51
+
52
+
{{ define "topLevelComment" }}
53
+
<div class="rounded px-6 py-4 bg-white dark:bg-gray-800">
54
+
{{ template "repo/issues/fragments/issueCommentHeader" . }}
55
+
{{ template "repo/issues/fragments/issueCommentBody" . }}
56
+
</div>
57
+
{{ end }}
58
+
59
+
{{ define "replyComment" }}
60
+
<div class="p-4 w-full mx-auto overflow-hidden">
61
+
{{ template "repo/issues/fragments/issueCommentHeader" . }}
62
+
{{ template "repo/issues/fragments/issueCommentBody" . }}
63
+
</div>
64
+
{{ end }}
+18
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
+18
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
···
1
+
{{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }}
2
+
<div class="p-2 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700">
3
+
<img
4
+
src="{{ tinyAvatar .LoggedInUser.Did }}"
5
+
alt=""
6
+
class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700"
7
+
/>
8
+
<input
9
+
class="w-full py-2 border-none focus:outline-none"
10
+
placeholder="Leave a reply..."
11
+
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply"
12
+
hx-trigger="focus"
13
+
hx-target="closest div"
14
+
hx-swap="outerHTML"
15
+
>
16
+
</input>
17
+
</div>
18
+
{{ end }}
+2
-2
appview/pages/templates/repo/issues/issue.html
+2
-2
appview/pages/templates/repo/issues/issue.html
···
9
9
{{ end }}
10
10
11
11
{{ define "repoContent" }}
12
-
<header class="pb-4">
12
+
<header class="pb-2">
13
13
<h1 class="text-2xl">
14
14
{{ .Issue.Title | description }}
15
15
<span class="text-gray-500 dark:text-gray-400">#{{ .Issue.IssueId }}</span>
···
39
39
</div>
40
40
41
41
{{ if .Issue.Body }}
42
-
<article id="body" class="mt-8 prose dark:prose-invert">
42
+
<article id="body" class="mt-4 prose dark:prose-invert">
43
43
{{ .Issue.Body | markdown }}
44
44
</article>
45
45
{{ end }}
+1
-1
appview/state/router.go
+1
-1
appview/state/router.go
···
232
232
}
233
233
234
234
func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler {
235
-
issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier)
235
+
issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier, s.validator)
236
236
return issues.Router(mw)
237
237
}
238
238
+5
-2
appview/state/state.go
+5
-2
appview/state/state.go
···
28
28
"tangled.sh/tangled.sh/core/appview/pages"
29
29
posthogService "tangled.sh/tangled.sh/core/appview/posthog"
30
30
"tangled.sh/tangled.sh/core/appview/reporesolver"
31
+
"tangled.sh/tangled.sh/core/appview/validator"
31
32
xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient"
32
33
"tangled.sh/tangled.sh/core/eventconsumer"
33
34
"tangled.sh/tangled.sh/core/idresolver"
···
53
54
knotstream *eventconsumer.Consumer
54
55
spindlestream *eventconsumer.Consumer
55
56
logger *slog.Logger
57
+
validator *validator.Validator
56
58
}
57
59
58
60
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
73
75
}
74
76
75
77
pgs := pages.NewPages(config, res)
76
-
77
78
cache := cache.New(config.Redis.Addr)
78
79
sess := session.New(cache)
79
-
80
80
oauth := oauth.NewOAuth(config, sess)
81
+
validator := validator.New(d)
81
82
82
83
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
83
84
if err != nil {
···
121
122
IdResolver: res,
122
123
Config: config,
123
124
Logger: tlog.New("ingester"),
125
+
Validator: validator,
124
126
}
125
127
err = jc.StartJetstream(ctx, ingester.Ingest())
126
128
if err != nil {
···
160
162
knotstream,
161
163
spindlestream,
162
164
slog.Default(),
165
+
validator,
163
166
}
164
167
165
168
return state, nil
+35
appview/validator/issue.go
+35
appview/validator/issue.go
···
1
+
package validator
2
+
3
+
import (
4
+
"fmt"
5
+
"strings"
6
+
7
+
"tangled.sh/tangled.sh/core/appview/db"
8
+
"tangled.sh/tangled.sh/core/appview/pages/markup"
9
+
)
10
+
11
+
func (v *Validator) ValidateIssueComment(comment *db.IssueComment) error {
12
+
// if comments have parents, only ingest ones that are 1 level deep
13
+
if comment.ReplyTo != nil {
14
+
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
15
+
if err != nil {
16
+
return fmt.Errorf("failed to fetch parent comment: %w", err)
17
+
}
18
+
if len(parents) != 1 {
19
+
return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents))
20
+
}
21
+
22
+
// depth check
23
+
parent := parents[0]
24
+
if parent.ReplyTo != nil {
25
+
return fmt.Errorf("incorrect depth, this comment is replying at depth >1")
26
+
}
27
+
}
28
+
29
+
sanitizer := markup.NewSanitizer()
30
+
if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" {
31
+
return fmt.Errorf("body is empty after HTML sanitization")
32
+
}
33
+
34
+
return nil
35
+
}