Skip to content

Commit 99c859d

Browse files
authored
Merge pull request #1230 from getfider/mentions
Mentions
2 parents 3b3047f + 4864cd0 commit 99c859d

File tree

111 files changed

+11826
-20729
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+11826
-20729
lines changed

.babelrc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
22
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
3-
"plugins": ["@babel/plugin-proposal-class-properties", "macros"]
3+
"plugins": [
4+
"@babel/plugin-proposal-class-properties",
5+
["macros", {
6+
"lingui": {
7+
"version": 5
8+
}
9+
}],
10+
"@lingui/babel-plugin-lingui-macro"
11+
]
412
}

app/cmd/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ func routes(r *web.Engine) *web.Engine {
201201
membersApi.Post("/api/v1/posts/:number/comments", apiv1.PostComment())
202202
membersApi.Put("/api/v1/posts/:number/comments/:id", apiv1.UpdateComment())
203203
membersApi.Delete("/api/v1/posts/:number/comments/:id", apiv1.DeleteComment())
204+
membersApi.Get("/api/v1/taggable-users", apiv1.ListTaggableUsers())
204205
membersApi.Post("/api/v1/posts/:number/votes", apiv1.AddVote())
205206
membersApi.Delete("/api/v1/posts/:number/votes", apiv1.RemoveVote())
206207
membersApi.Post("/api/v1/posts/:number/votes/toggle", apiv1.ToggleVote())

app/handlers/apiv1/post.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package apiv1
22

33
import (
4+
"fmt"
5+
46
"github.com/getfider/fider/app/actions"
57
"github.com/getfider/fider/app/metrics"
68
"github.com/getfider/fider/app/models/cmd"
@@ -9,6 +11,7 @@ import (
911
"github.com/getfider/fider/app/models/query"
1012
"github.com/getfider/fider/app/pkg/bus"
1113
"github.com/getfider/fider/app/pkg/env"
14+
"github.com/getfider/fider/app/pkg/markdown"
1215
"github.com/getfider/fider/app/pkg/web"
1316
"github.com/getfider/fider/app/tasks"
1417
)
@@ -212,6 +215,11 @@ func ListComments() web.HandlerFunc {
212215
return c.Failure(err)
213216
}
214217

218+
// the content of the comment needs to be sanitized before it is returned
219+
for _, comment := range getComments.Result {
220+
comment.Content = markdown.StripMentionMetaData(comment.Content)
221+
}
222+
215223
return c.Ok(getComments.Result)
216224
}
217225
}
@@ -229,6 +237,8 @@ func GetComment() web.HandlerFunc {
229237
return c.Failure(err)
230238
}
231239

240+
commentByID.Result.Content = markdown.StripMentionMetaData(commentByID.Result.Content)
241+
232242
return c.Ok(commentByID.Result)
233243
}
234244
}
@@ -279,13 +289,18 @@ func PostComment() web.HandlerFunc {
279289
}
280290

281291
addNewComment := &cmd.AddNewComment{
282-
Post: getPost.Result,
283-
Content: action.Content,
292+
Post: getPost.Result,
293+
Content: entity.CommentString(action.Content).FormatMentionJson(func(mention entity.Mention) string {
294+
return fmt.Sprintf(`{"id":%d,"name":"%s"}`, mention.ID, mention.Name)
295+
}),
284296
}
285297
if err := bus.Dispatch(c, addNewComment); err != nil {
286298
return c.Failure(err)
287299
}
288300

301+
// For processing, restore the original content
302+
addNewComment.Result.Content = action.Content
303+
289304
if err := bus.Dispatch(c, &cmd.SetAttachments{
290305
Post: getPost.Result,
291306
Comment: addNewComment.Result,
@@ -294,7 +309,7 @@ func PostComment() web.HandlerFunc {
294309
return c.Failure(err)
295310
}
296311

297-
c.Enqueue(tasks.NotifyAboutNewComment(getPost.Result, action.Content))
312+
c.Enqueue(tasks.NotifyAboutNewComment(addNewComment.Result, getPost.Result))
298313

299314
metrics.TotalComments.Inc()
300315
return c.Ok(web.Map{
@@ -311,14 +326,23 @@ func UpdateComment() web.HandlerFunc {
311326
return c.HandleValidation(result)
312327
}
313328

329+
getPost := &query.GetPostByID{PostID: action.Post.ID}
330+
if err := bus.Dispatch(c, getPost); err != nil {
331+
return c.Failure(err)
332+
}
333+
334+
contentToSave := entity.CommentString(action.Content).FormatMentionJson(func(mention entity.Mention) string {
335+
return fmt.Sprintf(`{"id":%d,"name":"%s"}`, mention.ID, mention.Name)
336+
})
337+
314338
err := bus.Dispatch(c,
315339
&cmd.UploadImages{
316340
Images: action.Attachments,
317341
Folder: "attachments",
318342
},
319343
&cmd.UpdateComment{
320344
CommentID: action.ID,
321-
Content: action.Content,
345+
Content: contentToSave,
322346
},
323347
&cmd.SetAttachments{
324348
Post: action.Post,
@@ -330,6 +354,10 @@ func UpdateComment() web.HandlerFunc {
330354
return c.Failure(err)
331355
}
332356

357+
// Update the content
358+
359+
c.Enqueue(tasks.NotifyAboutUpdatedComment(action.Content, getPost.Result))
360+
333361
return c.Ok(web.Map{})
334362
}
335363
}

app/handlers/apiv1/post_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,36 @@ func TestPostCommentHandler(t *testing.T) {
723723
Expect(newComment.Content).Equals("This is a comment!")
724724
}
725725

726+
func TestPostCommentHandlerMentions(t *testing.T) {
727+
RegisterT(t)
728+
729+
post := &entity.Post{ID: 1, Number: 1, Title: "The Post #1", Description: "The Description #1"}
730+
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
731+
q.Result = post
732+
return nil
733+
})
734+
735+
var newComment *cmd.AddNewComment
736+
bus.AddHandler(func(ctx context.Context, c *cmd.AddNewComment) error {
737+
newComment = c
738+
c.Result = &entity.Comment{ID: 1, Content: c.Content}
739+
return nil
740+
})
741+
742+
bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil })
743+
bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil })
744+
745+
code, _ := mock.NewServer().
746+
OnTenant(mock.DemoTenant).
747+
AsUser(mock.JonSnow).
748+
AddParam("number", post.Number).
749+
ExecutePost(apiv1.PostComment(), `{ "content": "Hello @{\"id\":1,\"name\":\"Jon Snow\",\"isNew\":true}!" }`)
750+
751+
Expect(code).Equals(http.StatusOK)
752+
Expect(newComment.Post).Equals(post)
753+
Expect(newComment.Content).Equals("Hello @{\"id\":1,\"name\":\"Jon Snow\"}!")
754+
}
755+
726756
func TestPostCommentHandler_WithoutContent(t *testing.T) {
727757
RegisterT(t)
728758

@@ -758,6 +788,11 @@ func TestUpdateCommentHandler_Authorized(t *testing.T) {
758788
return nil
759789
})
760790

791+
bus.AddHandler(func(ctx context.Context, q *query.GetPostByID) error {
792+
q.Result = post
793+
return nil
794+
})
795+
761796
bus.AddHandler(func(ctx context.Context, q *query.GetAttachments) error { return nil })
762797
bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil })
763798
bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil })

app/handlers/apiv1/user.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ func ListUsers() web.HandlerFunc {
2323
}
2424
}
2525

26+
func ListTaggableUsers() web.HandlerFunc {
27+
return func(c *web.Context) error {
28+
allUsers := &query.GetAllUsersNames{}
29+
if err := bus.Dispatch(c, allUsers); err != nil {
30+
return c.Failure(err)
31+
}
32+
return c.Ok(allUsers.Result)
33+
}
34+
}
35+
2636
// CreateUser is used to create new users
2737
func CreateUser() web.HandlerFunc {
2838
return func(c *web.Context) error {

app/models/dto/user.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dto
2+
3+
type UserNames struct {
4+
ID int `json:"id"`
5+
Name string `json:"name"`
6+
}

app/models/entity/comment.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package entity
22

3-
import "time"
3+
import (
4+
"time"
5+
)
46

57
type ReactionCounts struct {
68
Emoji string `json:"emoji"`
@@ -18,4 +20,10 @@ type Comment struct {
1820
EditedAt *time.Time `json:"editedAt,omitempty"`
1921
EditedBy *User `json:"editedBy,omitempty"`
2022
ReactionCounts []ReactionCounts `json:"reactionCounts,omitempty"`
23+
Mentions []Mention `json:"_"`
24+
}
25+
26+
func (c *Comment) ParseMentions() {
27+
mentionString := CommentString(c.Content)
28+
c.Mentions = mentionString.ParseMentions()
2129
}

app/models/entity/mention.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package entity
2+
3+
import (
4+
"encoding/json"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
type Mention struct {
10+
ID int `json:"id"`
11+
Name string `json:"name"`
12+
IsNew bool `json:"isNew"`
13+
}
14+
15+
type CommentString string
16+
17+
const mentionRegex = `@{([^{}]+)}`
18+
19+
func (commentString CommentString) ParseMentions() []Mention {
20+
r, _ := regexp.Compile(mentionRegex)
21+
22+
// Remove escaped quotes from the input string
23+
input := strings.ReplaceAll(string(commentString), `\"`, `"`)
24+
25+
matches := r.FindAllString(input, -1)
26+
27+
mentions := []Mention{}
28+
29+
for _, match := range matches {
30+
31+
jsonMention := match[1:]
32+
33+
var mention Mention
34+
err := json.Unmarshal([]byte(jsonMention), &mention)
35+
if err == nil {
36+
if mention.ID > 0 && mention.Name != "" {
37+
mentions = append(mentions, mention)
38+
}
39+
}
40+
}
41+
42+
return mentions
43+
}
44+
45+
func (mentionString CommentString) FormatMentionJson(jsonOperator func(Mention) string) string {
46+
47+
r, _ := regexp.Compile(mentionRegex)
48+
49+
// Remove escaped quotes from the input string
50+
input := strings.ReplaceAll(string(mentionString), `\"`, `"`)
51+
52+
return r.ReplaceAllStringFunc(input, func(match string) string {
53+
jsonMention := match[1:]
54+
55+
var mention Mention
56+
57+
err := json.Unmarshal([]byte(jsonMention), &mention)
58+
if err != nil {
59+
return match
60+
}
61+
62+
if mention.ID == 0 || mention.Name == "" {
63+
return match
64+
}
65+
66+
return "@" + jsonOperator(mention)
67+
})
68+
69+
}

app/models/entity/mention_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package entity_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/getfider/fider/app/models/entity"
7+
8+
. "github.com/getfider/fider/app/pkg/assert"
9+
)
10+
11+
func TestComment_ParseMentions(t *testing.T) {
12+
RegisterT(t)
13+
tests := []struct {
14+
name string
15+
content string
16+
expected []entity.Mention
17+
}{
18+
{
19+
name: "no mentions",
20+
content: "This is a regular comment \\\" just here",
21+
expected: []entity.Mention{},
22+
},
23+
{
24+
name: "Simple mention",
25+
content: `Hello there @{"id":1,"name":"John Doe","isNew":false} how are you`,
26+
expected: []entity.Mention{{ID: 1, Name: "John Doe", IsNew: false}},
27+
},
28+
{
29+
name: "Simple mention 2",
30+
content: `Hello there @{"id":2,"name":"John Doe Smith","isNew":true} how are you`,
31+
expected: []entity.Mention{{ID: 2, Name: "John Doe Smith", IsNew: true}},
32+
},
33+
{
34+
name: "Multiple mentions",
35+
content: `Hello there @{"id":2,"name":"John Doe Smith","isNew":true} and @{"id":1,"name":"John Doe","isNew":false} how are you`,
36+
expected: []entity.Mention{{ID: 2, Name: "John Doe Smith", IsNew: true}, {ID: 1, Name: "John Doe", IsNew: false}},
37+
},
38+
{
39+
name: "Some odd JSON",
40+
content: `Hello there @{"id":2,name:"John Doe Smith","isNew":true} how are you`,
41+
expected: []entity.Mention{},
42+
},
43+
}
44+
45+
for _, tt := range tests {
46+
t.Run(tt.name, func(t *testing.T) {
47+
comment := &entity.Comment{Content: tt.content}
48+
comment.ParseMentions()
49+
Expect(comment.Mentions).Equals(tt.expected)
50+
})
51+
}
52+
}
53+
54+
func TestStripMentionMetaData(t *testing.T) {
55+
RegisterT(t)
56+
57+
for input, expected := range map[string]string{
58+
`@{\"id\":1,\"name\":\"John Doe quoted\"}`: "@John Doe quoted",
59+
`@{"id":1,"name":"John Doe"}`: "@John Doe",
60+
`@{"id":1,"name":"JohnDoe"}`: "@JohnDoe",
61+
`@{\"id\":1,\"name\":\"JohnDoe quoted\"}`: "@JohnDoe quoted",
62+
`@{"id":1,"name":"John Smith Doe"}`: "@John Smith Doe",
63+
`@{\"id\":1,\"name\":\"John Smith Doe quoted\"}`: "@John Smith Doe quoted",
64+
"Hello there how are you": "Hello there how are you",
65+
`Hello there @{"id":1,"name":"John Doe"}`: "Hello there @John Doe",
66+
`Hello there @{"id":1,"name":"John Doe quoted"}`: "Hello there @John Doe quoted",
67+
`Hello both @{"id":1,"name":"John Doe"} and @{"id":2,"name":"John Smith"}`: "Hello both @John Doe and @John Smith",
68+
} {
69+
output := entity.CommentString(input).FormatMentionJson(func(mention entity.Mention) string {
70+
return mention.Name
71+
})
72+
Expect(output).Equals(expected)
73+
}
74+
}
75+
76+
func TestStripMentionMetaDataDoesntBreakUserInput(t *testing.T) {
77+
RegisterT(t)
78+
79+
for input, expected := range map[string]string{
80+
`There is nothing here`: "There is nothing here",
81+
`There is nothing here {ok}`: "There is nothing here {ok}",
82+
`This is a message for {{matt}}`: "This is a message for {{matt}}",
83+
`This is a message for {{id:1,wiggles:true}}`: "This is a message for {{id:1,wiggles:true}}",
84+
`Although uncommon, someone could enter @{something} like this`: "Although uncommon, someone could enter @{something} like this",
85+
`Or @{"id":100,"wiggles":"yes"} something like this`: `Or @{"id":100,"wiggles":"yes"} something like this`,
86+
} {
87+
output := entity.CommentString(input).FormatMentionJson(func(mention entity.Mention) string {
88+
return mention.Name
89+
})
90+
Expect(output).Equals(expected)
91+
}
92+
}

0 commit comments

Comments
 (0)