Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mentions #1230

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/cmd/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func routes(r *web.Engine) *web.Engine {
membersApi.Post("/api/v1/posts/:number/comments", apiv1.PostComment())
membersApi.Put("/api/v1/posts/:number/comments/:id", apiv1.UpdateComment())
membersApi.Delete("/api/v1/posts/:number/comments/:id", apiv1.DeleteComment())
membersApi.Get("/api/v1/taggable-users", apiv1.ListTaggableUsers())
membersApi.Post("/api/v1/posts/:number/votes", apiv1.AddVote())
membersApi.Delete("/api/v1/posts/:number/votes", apiv1.RemoveVote())
membersApi.Post("/api/v1/posts/:number/subscription", apiv1.Subscribe())
Expand Down
36 changes: 32 additions & 4 deletions app/handlers/apiv1/post.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package apiv1

import (
"fmt"

"github.com/getfider/fider/app/actions"
"github.com/getfider/fider/app/metrics"
"github.com/getfider/fider/app/models/cmd"
"github.com/getfider/fider/app/models/entity"
"github.com/getfider/fider/app/models/enum"
"github.com/getfider/fider/app/models/query"
"github.com/getfider/fider/app/pkg/bus"
"github.com/getfider/fider/app/pkg/markdown"
"github.com/getfider/fider/app/pkg/web"
"github.com/getfider/fider/app/tasks"
)
Expand Down Expand Up @@ -197,6 +200,11 @@ func ListComments() web.HandlerFunc {
return c.Failure(err)
}

// the content of the comment needs to be sanitized before it is returned
for _, comment := range getComments.Result {
comment.Content = markdown.StripMentionMetaData(comment.Content)
}

return c.Ok(getComments.Result)
}
}
Expand All @@ -214,6 +222,8 @@ func GetComment() web.HandlerFunc {
return c.Failure(err)
}

commentByID.Result.Content = markdown.StripMentionMetaData(commentByID.Result.Content)

return c.Ok(commentByID.Result)
}
}
Expand Down Expand Up @@ -264,13 +274,18 @@ func PostComment() web.HandlerFunc {
}

addNewComment := &cmd.AddNewComment{
Post: getPost.Result,
Content: action.Content,
Post: getPost.Result,
Content: entity.CommentString(action.Content).FormatMentionJson(func(mention entity.Mention) string {
return fmt.Sprintf(`{"id":%d,"name":"%s"}`, mention.ID, mention.Name)
}),
}
if err := bus.Dispatch(c, addNewComment); err != nil {
return c.Failure(err)
}

// For processing, restore the original content
addNewComment.Result.Content = action.Content

if err := bus.Dispatch(c, &cmd.SetAttachments{
Post: getPost.Result,
Comment: addNewComment.Result,
Expand All @@ -279,7 +294,7 @@ func PostComment() web.HandlerFunc {
return c.Failure(err)
}

c.Enqueue(tasks.NotifyAboutNewComment(getPost.Result, action.Content))
c.Enqueue(tasks.NotifyAboutNewComment(addNewComment.Result, getPost.Result))

metrics.TotalComments.Inc()
return c.Ok(web.Map{
Expand All @@ -296,14 +311,23 @@ func UpdateComment() web.HandlerFunc {
return c.HandleValidation(result)
}

getPost := &query.GetPostByID{PostID: action.Post.ID}
if err := bus.Dispatch(c, getPost); err != nil {
return c.Failure(err)
}

contentToSave := entity.CommentString(action.Content).FormatMentionJson(func(mention entity.Mention) string {
return fmt.Sprintf(`{"id":%d,"name":"%s"}`, mention.ID, mention.Name)
})

err := bus.Dispatch(c,
&cmd.UploadImages{
Images: action.Attachments,
Folder: "attachments",
},
&cmd.UpdateComment{
CommentID: action.ID,
Content: action.Content,
Content: contentToSave,
},
&cmd.SetAttachments{
Post: action.Post,
Expand All @@ -315,6 +339,10 @@ func UpdateComment() web.HandlerFunc {
return c.Failure(err)
}

// Update the content

c.Enqueue(tasks.NotifyAboutUpdatedComment(action.Content, getPost.Result))

return c.Ok(web.Map{})
}
}
Expand Down
30 changes: 30 additions & 0 deletions app/handlers/apiv1/post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,36 @@ func TestPostCommentHandler(t *testing.T) {
Expect(newComment.Content).Equals("This is a comment!")
}

func TestPostCommentHandlerMentions(t *testing.T) {
RegisterT(t)

post := &entity.Post{ID: 1, Number: 1, Title: "The Post #1", Description: "The Description #1"}
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
q.Result = post
return nil
})

var newComment *cmd.AddNewComment
bus.AddHandler(func(ctx context.Context, c *cmd.AddNewComment) error {
newComment = c
c.Result = &entity.Comment{ID: 1, Content: c.Content}
return nil
})

bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil })
bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil })

code, _ := mock.NewServer().
OnTenant(mock.DemoTenant).
AsUser(mock.JonSnow).
AddParam("number", post.Number).
ExecutePost(apiv1.PostComment(), `{ "content": "Hello @{\"id\":1,\"name\":\"Jon Snow\",\"isNew\":true}!" }`)

Expect(code).Equals(http.StatusOK)
Expect(newComment.Post).Equals(post)
Expect(newComment.Content).Equals("Hello @{\"id\":1,\"name\":\"Jon Snow\"}!")
}

func TestPostCommentHandler_WithoutContent(t *testing.T) {
RegisterT(t)

Expand Down
10 changes: 10 additions & 0 deletions app/handlers/apiv1/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ func ListUsers() web.HandlerFunc {
}
}

func ListTaggableUsers() web.HandlerFunc {
return func(c *web.Context) error {
allUsers := &query.GetAllUsersNames{}
if err := bus.Dispatch(c, allUsers); err != nil {
return c.Failure(err)
}
return c.Ok(allUsers.Result)
}
}

// CreateUser is used to create new users
func CreateUser() web.HandlerFunc {
return func(c *web.Context) error {
Expand Down
6 changes: 6 additions & 0 deletions app/models/dto/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dto

type UserNames struct {
ID int `json:"id"`
Name string `json:"name"`
}
10 changes: 9 additions & 1 deletion app/models/entity/comment.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package entity

import "time"
import (
"time"
)

type ReactionCounts struct {
Emoji string `json:"emoji"`
Expand All @@ -18,4 +20,10 @@ type Comment struct {
EditedAt *time.Time `json:"editedAt,omitempty"`
EditedBy *User `json:"editedBy,omitempty"`
ReactionCounts []ReactionCounts `json:"reactionCounts,omitempty"`
Mentions []Mention `json:"_"`
}

func (c *Comment) ParseMentions() {
mentionString := CommentString(c.Content)
c.Mentions = mentionString.ParseMentions()
}
69 changes: 69 additions & 0 deletions app/models/entity/mention.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package entity

import (
"encoding/json"
"regexp"
"strings"
)

type Mention struct {
ID int `json:"id"`
Name string `json:"name"`
IsNew bool `json:"isNew"`
}

type CommentString string

const mentionRegex = `@{([^{}]+)}`

func (commentString CommentString) ParseMentions() []Mention {
r, _ := regexp.Compile(mentionRegex)

// Remove escaped quotes from the input string
input := strings.ReplaceAll(string(commentString), `\"`, `"`)

matches := r.FindAllString(input, -1)

mentions := []Mention{}

for _, match := range matches {

jsonMention := match[1:]

var mention Mention
err := json.Unmarshal([]byte(jsonMention), &mention)
if err == nil {
if mention.ID > 0 && mention.Name != "" {
mentions = append(mentions, mention)
}
}
}

return mentions
}

func (mentionString CommentString) FormatMentionJson(jsonOperator func(Mention) string) string {

r, _ := regexp.Compile(mentionRegex)

// Remove escaped quotes from the input string
input := strings.ReplaceAll(string(mentionString), `\"`, `"`)

return r.ReplaceAllStringFunc(input, func(match string) string {
jsonMention := match[1:]

var mention Mention

err := json.Unmarshal([]byte(jsonMention), &mention)
if err != nil {
return match
}

if mention.ID == 0 || mention.Name == "" {
return match
}

return "@" + jsonOperator(mention)
})

}
92 changes: 92 additions & 0 deletions app/models/entity/mention_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package entity_test

import (
"testing"

"github.com/getfider/fider/app/models/entity"

. "github.com/getfider/fider/app/pkg/assert"
)

func TestComment_ParseMentions(t *testing.T) {
RegisterT(t)
tests := []struct {
name string
content string
expected []entity.Mention
}{
{
name: "no mentions",
content: "This is a regular comment \\\" just here",
expected: []entity.Mention{},
},
{
name: "Simple mention",
content: `Hello there @{"id":1,"name":"John Doe","isNew":false} how are you`,
expected: []entity.Mention{{ID: 1, Name: "John Doe", IsNew: false}},
},
{
name: "Simple mention 2",
content: `Hello there @{"id":2,"name":"John Doe Smith","isNew":true} how are you`,
expected: []entity.Mention{{ID: 2, Name: "John Doe Smith", IsNew: true}},
},
{
name: "Multiple mentions",
content: `Hello there @{"id":2,"name":"John Doe Smith","isNew":true} and @{"id":1,"name":"John Doe","isNew":false} how are you`,
expected: []entity.Mention{{ID: 2, Name: "John Doe Smith", IsNew: true}, {ID: 1, Name: "John Doe", IsNew: false}},
},
{
name: "Some odd JSON",
content: `Hello there @{"id":2,name:"John Doe Smith","isNew":true} how are you`,
expected: []entity.Mention{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
comment := &entity.Comment{Content: tt.content}
comment.ParseMentions()
Expect(comment.Mentions).Equals(tt.expected)
})
}
}

func TestStripMentionMetaData(t *testing.T) {
RegisterT(t)

for input, expected := range map[string]string{
`@{\"id\":1,\"name\":\"John Doe quoted\"}`: "@John Doe quoted",
`@{"id":1,"name":"John Doe"}`: "@John Doe",
`@{"id":1,"name":"JohnDoe"}`: "@JohnDoe",
`@{\"id\":1,\"name\":\"JohnDoe quoted\"}`: "@JohnDoe quoted",
`@{"id":1,"name":"John Smith Doe"}`: "@John Smith Doe",
`@{\"id\":1,\"name\":\"John Smith Doe quoted\"}`: "@John Smith Doe quoted",
"Hello there how are you": "Hello there how are you",
`Hello there @{"id":1,"name":"John Doe"}`: "Hello there @John Doe",
`Hello there @{"id":1,"name":"John Doe quoted"}`: "Hello there @John Doe quoted",
`Hello both @{"id":1,"name":"John Doe"} and @{"id":2,"name":"John Smith"}`: "Hello both @John Doe and @John Smith",
} {
output := entity.CommentString(input).FormatMentionJson(func(mention entity.Mention) string {
return mention.Name
})
Expect(output).Equals(expected)
}
}

func TestStripMentionMetaDataDoesntBreakUserInput(t *testing.T) {
RegisterT(t)

for input, expected := range map[string]string{
`There is nothing here`: "There is nothing here",
`There is nothing here {ok}`: "There is nothing here {ok}",
`This is a message for {{matt}}`: "This is a message for {{matt}}",
`This is a message for {{id:1,wiggles:true}}`: "This is a message for {{id:1,wiggles:true}}",
`Although uncommon, someone could enter @{something} like this`: "Although uncommon, someone could enter @{something} like this",
`Or @{"id":100,"wiggles":"yes"} something like this`: `Or @{"id":100,"wiggles":"yes"} something like this`,
} {
output := entity.CommentString(input).FormatMentionJson(func(mention entity.Mention) string {
return mention.Name
})
Expect(output).Equals(expected)
}
}
Loading
Loading