3
0

Refactor and polish flags (reactions, pins & bookmarks)

This commit is contained in:
Denis Arh
2018-11-01 17:39:30 +01:00
parent 71986df8d3
commit 242408e83e
18 changed files with 581 additions and 381 deletions
+32 -2
View File
@@ -29,8 +29,11 @@ func Message(ctx context.Context, msg *samTypes.Message) *outgoing.Message {
ReplyTo: msg.ReplyTo,
Replies: msg.Replies,
User: User(msg.User),
Attachment: Attachment(msg.Attachment),
User: User(msg.User),
Attachment: Attachment(msg.Attachment),
Reactions: MessageReactions(msg.Flags),
IsPinned: msg.Flags.IsPinned(),
IsBookmarked: msg.Flags.IsBookmarked(currentUserID),
CanReply: canReply,
CanEdit: canEdit,
@@ -51,6 +54,33 @@ func Messages(ctx context.Context, msg samTypes.MessageSet) *outgoing.MessageSet
return &retval
}
func MessageReactions(flags samTypes.MessageFlagSet) outgoing.ReactionSet {
var (
rr = make([]*outgoing.Reaction, 0)
rIndex = map[string]int{}
has bool
i int
)
_ = flags.Walk(func(flag *samTypes.MessageFlag) error {
if flag.IsReaction() {
r := &outgoing.Reaction{Reaction: flag.Flag, UserIDs: []string{}, Count: 0}
if i, has = rIndex[flag.Flag]; !has {
i, rIndex[flag.Flag] = len(rr), len(rr)
rr = append(rr, r)
}
rr[i].UserIDs = append(rr[i].UserIDs, Uint64toa(flag.UserID))
rr[i].Count++
}
return nil
})
return rr
}
func Channel(ch *samTypes.Channel) *outgoing.Channel {
return &outgoing.Channel{
ID: Uint64toa(ch.ID),
+13 -2
View File
@@ -14,8 +14,11 @@ type (
ReplyTo uint64 `json:"replyTo,omitempty,string"`
Replies uint `json:"replies,omitempty"`
User *User `json:"user"`
Attachment *Attachment `json:"att,omitempty"`
User *User `json:"user"`
Attachment *Attachment `json:"att,omitempty"`
Reactions ReactionSet `json:"reactions,omitempty"`
IsBookmarked bool `json:"isBookmarked"`
IsPinned bool `json:"isPinned"`
CanReply bool `json:"canReply"`
CanEdit bool `json:"canEdit"`
@@ -27,6 +30,14 @@ type (
}
MessageSet []*Message
Reaction struct {
UserIDs []string `json:"userIDs"`
Reaction string `json:"reaction"`
Count uint `json:"count"`
}
ReactionSet []*Reaction
)
func (p *Message) EncodeMessage() ([]byte, error) {
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
DROP TABLE channel_pins;
DROP TABLE reactions;
CREATE TABLE message_flags (
id BIGINT UNSIGNED NOT NULL,
rel_channel BIGINT UNSIGNED NOT NULL,
rel_message BIGINT UNSIGNED NOT NULL,
rel_user BIGINT UNSIGNED NOT NULL,
flag TEXT,
created_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+20 -20
View File
@@ -453,20 +453,6 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Pin message to channel (public bookmark)
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/pin` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Returns all replies to a message
#### Method
@@ -500,6 +486,20 @@ The following event types may be sent with a message event:
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/pin` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Pin message to channel (public bookmark)
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/pin` | HTTP/S | DELETE | Client ID, Session ID |
@@ -510,13 +510,13 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Flag a message (private bookmark)
## Bookmark a message (private bookmark)
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/flag` | HTTP/S | POST | Client ID, Session ID |
| `/channels/{channelID}/messages/{messageID}/bookmark` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
@@ -524,13 +524,13 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Remove flag from message (private bookmark)
## Remove boomark from message (private bookmark)
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/flag` | HTTP/S | DELETE | Client ID, Session ID |
| `/channels/{channelID}/messages/{messageID}/bookmark` | HTTP/S | DELETE | Client ID, Session ID |
#### Request parameters
@@ -544,7 +544,7 @@ The following event types may be sent with a message event:
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/reaction/{reaction}` | HTTP/S | PUT | Client ID, Session ID |
| `/channels/{channelID}/messages/{messageID}/reaction/{reaction}` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
@@ -559,7 +559,7 @@ The following event types may be sent with a message event:
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/react/{reaction}` | HTTP/S | DELETE | Client ID, Session ID |
| `/channels/{channelID}/messages/{messageID}/reaction/{reaction}` | HTTP/S | DELETE | Client ID, Session ID |
#### Request parameters
+21 -21
View File
@@ -387,17 +387,6 @@
]
}
},
{
"name": "pin",
"path": "/{messageID}/pin",
"method": "POST",
"title": "Pin message to channel (public bookmark)",
"parameters": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
]
}
},
{
"name": "getReplies",
"path": "/{messageID}/replies",
@@ -424,7 +413,18 @@
}
},
{
"name": "unpin",
"name": "pin",
"path": "/{messageID}/pin",
"method": "POST",
"title": "Pin message to channel (public bookmark)",
"parameters": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
]
}
},
{
"name": "removePin",
"path": "/{messageID}/pin",
"method": "DELETE",
"title": "Pin message to channel (public bookmark)",
@@ -435,10 +435,10 @@
}
},
{
"name": "flag",
"path": "/{messageID}/flag",
"name": "bookmark",
"path": "/{messageID}/bookmark",
"method": "POST",
"title": "Flag a message (private bookmark)",
"title": "Bookmark a message (private bookmark)",
"parameters": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
@@ -446,10 +446,10 @@
}
},
{
"name": "unflag",
"path": "/{messageID}/flag",
"name": "removeBookmark",
"path": "/{messageID}/bookmark",
"method": "DELETE",
"title": "Remove flag from message (private bookmark)",
"title": "Remove boomark from message (private bookmark)",
"parameters": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
@@ -459,7 +459,7 @@
{
"name": "react",
"path": "/{messageID}/reaction/{reaction}",
"method": "PUT",
"method": "POST",
"title": "React to a message",
"parameters": {
"path": [
@@ -469,8 +469,8 @@
}
},
{
"name": "unreact",
"path": "/{messageID}/react/{reaction}",
"name": "removeReaction",
"path": "/{messageID}/reaction/{reaction}",
"method": "DELETE",
"title": "Delete reaction from a message",
"parameters": {
+26 -26
View File
@@ -92,22 +92,6 @@
]
}
},
{
"Name": "pin",
"Method": "POST",
"Title": "Pin message to channel (public bookmark)",
"Path": "/{messageID}/pin",
"Parameters": {
"path": [
{
"name": "messageID",
"required": true,
"title": "Message ID",
"type": "uint64"
}
]
}
},
{
"Name": "getReplies",
"Method": "GET",
@@ -149,7 +133,23 @@
}
},
{
"Name": "unpin",
"Name": "pin",
"Method": "POST",
"Title": "Pin message to channel (public bookmark)",
"Path": "/{messageID}/pin",
"Parameters": {
"path": [
{
"name": "messageID",
"required": true,
"title": "Message ID",
"type": "uint64"
}
]
}
},
{
"Name": "removePin",
"Method": "DELETE",
"Title": "Pin message to channel (public bookmark)",
"Path": "/{messageID}/pin",
@@ -165,10 +165,10 @@
}
},
{
"Name": "flag",
"Name": "bookmark",
"Method": "POST",
"Title": "Flag a message (private bookmark)",
"Path": "/{messageID}/flag",
"Title": "Bookmark a message (private bookmark)",
"Path": "/{messageID}/bookmark",
"Parameters": {
"path": [
{
@@ -181,10 +181,10 @@
}
},
{
"Name": "unflag",
"Name": "removeBookmark",
"Method": "DELETE",
"Title": "Remove flag from message (private bookmark)",
"Path": "/{messageID}/flag",
"Title": "Remove boomark from message (private bookmark)",
"Path": "/{messageID}/bookmark",
"Parameters": {
"path": [
{
@@ -198,7 +198,7 @@
},
{
"Name": "react",
"Method": "PUT",
"Method": "POST",
"Title": "React to a message",
"Path": "/{messageID}/reaction/{reaction}",
"Parameters": {
@@ -219,10 +219,10 @@
}
},
{
"Name": "unreact",
"Name": "removeReaction",
"Method": "DELETE",
"Title": "Delete reaction from a message",
"Path": "/{messageID}/react/{reaction}",
"Path": "/{messageID}/reaction/{reaction}",
"Parameters": {
"path": [
{
+83
View File
@@ -0,0 +1,83 @@
package repository
import (
"context"
"time"
"github.com/jmoiron/sqlx"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/types"
)
type (
MessageFlagRepository interface {
With(ctx context.Context, db *factory.DB) MessageFlagRepository
FindByID(ID uint64) (*types.MessageFlag, error)
FindByMessageIDs(IDs ...uint64) ([]*types.MessageFlag, error)
FindByFlag(messageID, userID uint64, flag string) (*types.MessageFlag, error)
Create(mod *types.MessageFlag) (*types.MessageFlag, error)
DeleteByID(ID uint64) error
}
reaction struct {
*repository
}
)
const (
ErrMessageFlagNotFound = repositoryError("MessageFlagNotFound")
)
func MessageFlag(ctx context.Context, db *factory.DB) MessageFlagRepository {
return (&reaction{}).With(ctx, db)
}
func (r *reaction) With(ctx context.Context, db *factory.DB) MessageFlagRepository {
return &reaction{
repository: r.repository.With(ctx, db),
}
}
func (r *reaction) FindByID(ID uint64) (*types.MessageFlag, error) {
sql := "SELECT * FROM message_flags WHERE id = ?"
mod := &types.MessageFlag{}
return mod, isFound(r.db().Get(mod, sql, ID), mod.ID > 0, ErrMessageFlagNotFound)
}
func (r *reaction) FindByFlag(messageID, userID uint64, flag string) (*types.MessageFlag, error) {
args := []interface{}{messageID, flag}
sql := "SELECT * FROM message_flags WHERE rel_message = ? AND flag = ? "
if userID > 0 {
sql += "AND rel_user = ? "
args = append(args, userID)
}
mod := &types.MessageFlag{}
return mod, isFound(r.db().Get(mod, sql, args...), mod.ID > 0, ErrMessageFlagNotFound)
}
// FindByMessageRange returns all flags by message id range
func (r *reaction) FindByMessageIDs(IDs ...uint64) ([]*types.MessageFlag, error) {
rval := make([]*types.MessageFlag, 0)
sql := `SELECT * FROM message_flags WHERE rel_message IN (?)`
if sql, args, err := sqlx.In(sql, IDs); err != nil {
return nil, err
} else {
return rval, r.db().Select(&rval, sql, args...)
}
}
func (r *reaction) Create(mod *types.MessageFlag) (*types.MessageFlag, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
return mod, r.db().Insert("message_flags", mod)
}
func (r *reaction) DeleteByID(ID uint64) error {
return exec(r.db().Exec("DELETE FROM message_flags WHERE id = ?", ID))
}
+50
View File
@@ -0,0 +1,50 @@
package repository
import (
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestReaction(t *testing.T) {
var err error
if testing.Short() {
t.Skip("skipping test in short mode.")
return
}
rpo := MessageFlag(context.Background(), factory.Database.MustGet())
tx(t, func() error {
var chID = factory.Sonyflake.NextID()
var msgID = factory.Sonyflake.NextID()
var f *types.MessageFlag
var ff types.MessageFlagSet
f, err = rpo.Create(&types.MessageFlag{
ChannelID: chID,
MessageID: msgID,
UserID: 3,
Flag: "success",
})
assert(t, err == nil, "Should create message flag without an error, got: %v", err)
f, err = rpo.FindByID(f.ID)
assert(t, err == nil, "Should fetch message flag without an error, got: %v", err)
assert(t, f != nil && f.ChannelID == chID, "fetch should return valid type struct")
ff, err = rpo.FindByMessageIDs(msgID)
assert(t, err == nil, "Should fetch message flag by range without an error, got: %v", err)
assert(t, len(ff) == 1, "fetch by range should return 1 message")
err = rpo.DeleteByID(f.ID)
assert(t, err == nil, "Should delete message flag without an error, got: %v", err)
return nil
})
}
-61
View File
@@ -1,61 +0,0 @@
package repository
import (
"context"
"time"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/types"
)
type (
ReactionRepository interface {
With(ctx context.Context, db *factory.DB) ReactionRepository
FindReactionByID(id uint64) (*types.Reaction, error)
FindReactionsByRange(channelID, fromReactionID, toReactionID uint64) ([]*types.Reaction, error)
CreateReaction(mod *types.Reaction) (*types.Reaction, error)
DeleteReactionByID(id uint64) error
}
reaction struct {
*repository
}
)
const (
ErrReactionNotFound = repositoryError("ReactionNotFound")
)
func Reaction(ctx context.Context, db *factory.DB) ReactionRepository {
return (&reaction{}).With(ctx, db)
}
func (r *reaction) With(ctx context.Context, db *factory.DB) ReactionRepository {
return &reaction{
repository: r.repository.With(ctx, db),
}
}
func (r *reaction) FindReactionByID(id uint64) (*types.Reaction, error) {
sql := "SELECT * FROM reactions WHERE id=?"
mod := &types.Reaction{}
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrReactionNotFound)
}
func (r *reaction) FindReactionsByRange(channelID, fromReactionID, toReactionID uint64) ([]*types.Reaction, error) {
rval := make([]*types.Reaction, 0)
sql := `SELECT * FROM reactions WHERE rel_reaction BETWEEN ? AND ? AND rel_channel=?`
return rval, r.db().Select(&rval, sql, fromReactionID, toReactionID, channelID)
}
func (r *reaction) CreateReaction(mod *types.Reaction) (*types.Reaction, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
return mod, r.db().Insert("reactions", mod)
}
func (r *reaction) DeleteReactionByID(id uint64) error {
return exec(r.db().Exec("DELETE FROM reactions WHERE id=?", id))
}
-43
View File
@@ -1,43 +0,0 @@
package repository
import (
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestReaction(t *testing.T) {
var err error
if testing.Short() {
t.Skip("skipping test in short mode.")
return
}
rpo := Reaction(context.Background(), factory.Database.MustGet())
react := &types.Reaction{}
var reaction = ":laugh:"
{
react.Reaction = reaction
react, err = rpo.CreateReaction(react)
assert(t, err == nil, "CreateReaction error: %v", err)
assert(t, react.Reaction == reaction, "Changes were not stored")
{
react, err = rpo.FindReactionByID(react.ID)
assert(t, err == nil, "FindReactionByID error: %v", err)
assert(t, react.Reaction == reaction, "Changes were not stored")
}
{
err = rpo.DeleteReactionByID(react.ID)
assert(t, err == nil, "DeleteReactionByID error: %v", err)
}
}
}
+42 -42
View File
@@ -31,30 +31,30 @@ type MessageAPI interface {
History(context.Context, *request.MessageHistory) (interface{}, error)
Edit(context.Context, *request.MessageEdit) (interface{}, error)
Delete(context.Context, *request.MessageDelete) (interface{}, error)
Pin(context.Context, *request.MessagePin) (interface{}, error)
GetReplies(context.Context, *request.MessageGetReplies) (interface{}, error)
CreateReply(context.Context, *request.MessageCreateReply) (interface{}, error)
Unpin(context.Context, *request.MessageUnpin) (interface{}, error)
Flag(context.Context, *request.MessageFlag) (interface{}, error)
Unflag(context.Context, *request.MessageUnflag) (interface{}, error)
Pin(context.Context, *request.MessagePin) (interface{}, error)
RemovePin(context.Context, *request.MessageRemovePin) (interface{}, error)
Bookmark(context.Context, *request.MessageBookmark) (interface{}, error)
RemoveBookmark(context.Context, *request.MessageRemoveBookmark) (interface{}, error)
React(context.Context, *request.MessageReact) (interface{}, error)
Unreact(context.Context, *request.MessageUnreact) (interface{}, error)
RemoveReaction(context.Context, *request.MessageRemoveReaction) (interface{}, error)
}
// HTTP API interface
type Message struct {
Create func(http.ResponseWriter, *http.Request)
History func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Pin func(http.ResponseWriter, *http.Request)
GetReplies func(http.ResponseWriter, *http.Request)
CreateReply func(http.ResponseWriter, *http.Request)
Unpin func(http.ResponseWriter, *http.Request)
Flag func(http.ResponseWriter, *http.Request)
Unflag func(http.ResponseWriter, *http.Request)
React func(http.ResponseWriter, *http.Request)
Unreact func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
History func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
GetReplies func(http.ResponseWriter, *http.Request)
CreateReply func(http.ResponseWriter, *http.Request)
Pin func(http.ResponseWriter, *http.Request)
RemovePin func(http.ResponseWriter, *http.Request)
Bookmark func(http.ResponseWriter, *http.Request)
RemoveBookmark func(http.ResponseWriter, *http.Request)
React func(http.ResponseWriter, *http.Request)
RemoveReaction func(http.ResponseWriter, *http.Request)
}
func NewMessage(mh MessageAPI) *Message {
@@ -87,13 +87,6 @@ func NewMessage(mh MessageAPI) *Message {
return mh.Delete(r.Context(), params)
})
},
Pin: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessagePin()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Pin(r.Context(), params)
})
},
GetReplies: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageGetReplies()
@@ -108,25 +101,32 @@ func NewMessage(mh MessageAPI) *Message {
return mh.CreateReply(r.Context(), params)
})
},
Unpin: func(w http.ResponseWriter, r *http.Request) {
Pin: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageUnpin()
params := request.NewMessagePin()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Unpin(r.Context(), params)
return mh.Pin(r.Context(), params)
})
},
Flag: func(w http.ResponseWriter, r *http.Request) {
RemovePin: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageFlag()
params := request.NewMessageRemovePin()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Flag(r.Context(), params)
return mh.RemovePin(r.Context(), params)
})
},
Unflag: func(w http.ResponseWriter, r *http.Request) {
Bookmark: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageUnflag()
params := request.NewMessageBookmark()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Unflag(r.Context(), params)
return mh.Bookmark(r.Context(), params)
})
},
RemoveBookmark: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageRemoveBookmark()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RemoveBookmark(r.Context(), params)
})
},
React: func(w http.ResponseWriter, r *http.Request) {
@@ -136,11 +136,11 @@ func NewMessage(mh MessageAPI) *Message {
return mh.React(r.Context(), params)
})
},
Unreact: func(w http.ResponseWriter, r *http.Request) {
RemoveReaction: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageUnreact()
params := request.NewMessageRemoveReaction()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Unreact(r.Context(), params)
return mh.RemoveReaction(r.Context(), params)
})
},
}
@@ -154,14 +154,14 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Get("/", mh.History)
r.Put("/{messageID}", mh.Edit)
r.Delete("/{messageID}", mh.Delete)
r.Post("/{messageID}/pin", mh.Pin)
r.Get("/{messageID}/replies", mh.GetReplies)
r.Post("/{messageID}/replies", mh.CreateReply)
r.Delete("/{messageID}/pin", mh.Unpin)
r.Post("/{messageID}/flag", mh.Flag)
r.Delete("/{messageID}/flag", mh.Unflag)
r.Put("/{messageID}/reaction/{reaction}", mh.React)
r.Delete("/{messageID}/react/{reaction}", mh.Unreact)
r.Post("/{messageID}/pin", mh.Pin)
r.Delete("/{messageID}/pin", mh.RemovePin)
r.Post("/{messageID}/bookmark", mh.Bookmark)
r.Delete("/{messageID}/bookmark", mh.RemoveBookmark)
r.Post("/{messageID}/reaction/{reaction}", mh.React)
r.Delete("/{messageID}/reaction/{reaction}", mh.RemoveReaction)
})
})
}
+8 -8
View File
@@ -72,24 +72,24 @@ func (ctrl *Message) Pin(ctx context.Context, r *request.MessagePin) (interface{
return nil, ctrl.svc.msg.With(ctx).Pin(r.MessageID)
}
func (ctrl *Message) Unpin(ctx context.Context, r *request.MessageUnpin) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Unpin(r.MessageID)
func (ctrl *Message) RemovePin(ctx context.Context, r *request.MessageRemovePin) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).RemovePin(r.MessageID)
}
func (ctrl *Message) Flag(ctx context.Context, r *request.MessageFlag) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Flag(r.MessageID)
func (ctrl *Message) Bookmark(ctx context.Context, r *request.MessageBookmark) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Bookmark(r.MessageID)
}
func (ctrl *Message) Unflag(ctx context.Context, r *request.MessageUnflag) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Unflag(r.MessageID)
func (ctrl *Message) RemoveBookmark(ctx context.Context, r *request.MessageRemoveBookmark) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).RemoveBookmark(r.MessageID)
}
func (ctrl *Message) React(ctx context.Context, r *request.MessageReact) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).React(r.MessageID, r.Reaction)
}
func (ctrl *Message) Unreact(ctx context.Context, r *request.MessageUnreact) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Unreact(r.MessageID, r.Reaction)
func (ctrl *Message) RemoveReaction(ctx context.Context, r *request.MessageRemoveReaction) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).RemoveReaction(r.MessageID, r.Reaction)
}
func (ctrl *Message) wrap(ctx context.Context) func(m *types.Message, err error) (*outgoing.Message, error) {
return func(m *types.Message, err error) (*outgoing.Message, error) {
+69 -69
View File
@@ -221,51 +221,6 @@ func (m *MessageDelete) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageDelete()
// Message pin request parameters
type MessagePin struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessagePin() *MessagePin {
return &MessagePin{}
}
func (m *MessagePin) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
m.MessageID = parseUInt64(chi.URLParam(r, "messageID"))
m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
return err
}
var _ RequestFiller = NewMessagePin()
// Message getReplies request parameters
type MessageGetReplies struct {
MessageID uint64 `json:",string"`
@@ -361,17 +316,17 @@ func (m *MessageCreateReply) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageCreateReply()
// Message unpin request parameters
type MessageUnpin struct {
// Message pin request parameters
type MessagePin struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessageUnpin() *MessageUnpin {
return &MessageUnpin{}
func NewMessagePin() *MessagePin {
return &MessagePin{}
}
func (m *MessageUnpin) Fill(r *http.Request) (err error) {
func (m *MessagePin) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -404,19 +359,19 @@ func (m *MessageUnpin) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewMessageUnpin()
var _ RequestFiller = NewMessagePin()
// Message flag request parameters
type MessageFlag struct {
// Message removePin request parameters
type MessageRemovePin struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessageFlag() *MessageFlag {
return &MessageFlag{}
func NewMessageRemovePin() *MessageRemovePin {
return &MessageRemovePin{}
}
func (m *MessageFlag) Fill(r *http.Request) (err error) {
func (m *MessageRemovePin) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -449,19 +404,19 @@ func (m *MessageFlag) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewMessageFlag()
var _ RequestFiller = NewMessageRemovePin()
// Message unflag request parameters
type MessageUnflag struct {
// Message bookmark request parameters
type MessageBookmark struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessageUnflag() *MessageUnflag {
return &MessageUnflag{}
func NewMessageBookmark() *MessageBookmark {
return &MessageBookmark{}
}
func (m *MessageUnflag) Fill(r *http.Request) (err error) {
func (m *MessageBookmark) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -494,7 +449,52 @@ func (m *MessageUnflag) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewMessageUnflag()
var _ RequestFiller = NewMessageBookmark()
// Message removeBookmark request parameters
type MessageRemoveBookmark struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessageRemoveBookmark() *MessageRemoveBookmark {
return &MessageRemoveBookmark{}
}
func (m *MessageRemoveBookmark) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
m.MessageID = parseUInt64(chi.URLParam(r, "messageID"))
m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
return err
}
var _ RequestFiller = NewMessageRemoveBookmark()
// Message react request parameters
type MessageReact struct {
@@ -543,18 +543,18 @@ func (m *MessageReact) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageReact()
// Message unreact request parameters
type MessageUnreact struct {
// Message removeReaction request parameters
type MessageRemoveReaction struct {
MessageID uint64 `json:",string"`
Reaction string
ChannelID uint64 `json:",string"`
}
func NewMessageUnreact() *MessageUnreact {
return &MessageUnreact{}
func NewMessageRemoveReaction() *MessageRemoveReaction {
return &MessageRemoveReaction{}
}
func (m *MessageUnreact) Fill(r *http.Request) (err error) {
func (m *MessageRemoveReaction) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -588,4 +588,4 @@ func (m *MessageUnreact) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewMessageUnreact()
var _ RequestFiller = NewMessageRemoveReaction()
+116 -70
View File
@@ -23,7 +23,7 @@ type (
cmember repository.ChannelMemberRepository
cview repository.ChannelViewRepository
message repository.MessageRepository
reaction repository.ReactionRepository
mflag repository.MessageFlagRepository
usr authService.UserService
evl EventService
@@ -39,13 +39,13 @@ type (
Update(messages *types.Message) (*types.Message, error)
React(messageID uint64, reaction string) error
Unreact(messageID uint64, reaction string) error
RemoveReaction(messageID uint64, reaction string) error
Pin(messageID uint64) error
Unpin(messageID uint64) error
RemovePin(messageID uint64) error
Flag(messageID uint64) error
Unflag(messageID uint64) error
Bookmark(messageID uint64) error
RemoveBookmark(messageID uint64) error
Delete(ID uint64) error
}
@@ -76,7 +76,7 @@ func (svc *message) With(ctx context.Context) MessageService {
cmember: repository.ChannelMember(ctx, db),
cview: repository.ChannelView(ctx, db),
message: repository.Message(ctx, db),
reaction: repository.Reaction(ctx, db),
mflag: repository.MessageFlag(ctx, db),
}
}
@@ -92,9 +92,7 @@ func (svc *message) Find(filter *types.MessageFilter) (mm types.MessageSet, err
return nil, err
}
svc.preloadUsers(mm)
return mm, svc.preloadAttachments(mm)
return mm, svc.preload(mm)
}
func (svc *message) FindThreads(filter *types.MessageFilter) (mm types.MessageSet, err error) {
@@ -109,9 +107,7 @@ func (svc *message) FindThreads(filter *types.MessageFilter) (mm types.MessageSe
return nil, err
}
svc.preloadUsers(mm)
return mm, svc.preloadAttachments(mm)
return mm, svc.preload(mm)
}
func (svc *message) Create(in *types.Message) (message *types.Message, err error) {
@@ -290,80 +286,114 @@ func (svc *message) Delete(ID uint64) error {
})
}
// React on a message with an emoji
func (svc *message) React(messageID uint64, reaction string) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
var m *types.Message
// @todo validate reaction
r := &types.Reaction{
UserID: currentUserID,
MessageID: messageID,
ChannelID: m.ChannelID,
Reaction: reaction,
}
if _, err := svc.reaction.CreateReaction(r); err != nil {
return err
}
return nil
return svc.flag(messageID, reaction, false)
}
func (svc *message) Unreact(messageID uint64, reaction string) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
_ = currentUserID
// @todo load reaction and verify ownership
var r *types.Reaction
return svc.reaction.DeleteReactionByID(r.ID)
// Remove reaction on a message
func (svc *message) RemoveReaction(messageID uint64, reaction string) error {
return svc.flag(messageID, reaction, true)
}
// Pin message to the channel
func (svc *message) Pin(messageID uint64) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
_ = currentUserID
return nil
return svc.flag(messageID, types.MessageFlagPinnedToChannel, false)
}
func (svc *message) Unpin(messageID uint64) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
_ = currentUserID
return nil
// Remove pin from message
func (svc *message) RemovePin(messageID uint64) error {
return svc.flag(messageID, types.MessageFlagPinnedToChannel, true)
}
func (svc *message) Flag(messageID uint64) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
_ = currentUserID
return nil
// Bookmark message (private)
func (svc *message) Bookmark(messageID uint64) error {
return svc.flag(messageID, types.MessageFlagBookmarkedMessage, false)
}
func (svc *message) Unflag(messageID uint64) error {
// Remove bookmark message (private)
func (svc *message) RemoveBookmark(messageID uint64) error {
return svc.flag(messageID, types.MessageFlagBookmarkedMessage, true)
}
// React on a message with an emoji
func (svc *message) flag(messageID uint64, flag string, remove bool) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
_ = currentUserID
return nil
if strings.TrimSpace(flag) == "" {
// Sanitize
flag = types.MessageFlagPinnedToChannel
}
// @todo validate flags beyond empty string
err := svc.db.Transaction(func() (err error) {
var flagOwnerId = currentUserID
var f *types.MessageFlag
// @todo [SECURITY] verify if current user can access & write to this channel
if flag == types.MessageFlagPinnedToChannel {
// It does not matter how is the owner of the pin,
flagOwnerId = 0
}
f, err = svc.mflag.FindByFlag(messageID, flagOwnerId, flag)
if f.ID == 0 && remove {
// Skip removing, flag does not exists
return nil
} else if f.ID > 0 && !remove {
// Skip adding, flag already exists
return nil
} else if err != nil && err != repository.ErrMessageFlagNotFound {
// Other errors, exit
return
}
// Check message
var msg *types.Message
msg, err = svc.message.FindMessageByID(messageID)
if err != nil {
return
}
if remove {
err = svc.mflag.DeleteByID(f.ID)
} else {
_, err = svc.mflag.Create(&types.MessageFlag{
UserID: currentUserID,
ChannelID: msg.ChannelID,
MessageID: msg.ID,
Flag: flag,
})
}
svc.sendEvent(msg)
return
})
return errors.Wrap(err, "Can not flag/un-flag message")
}
func (svc *message) preload(mm types.MessageSet) (err error) {
if err = svc.preloadUsers(mm); err != nil {
return
}
if err = svc.preloadAttachments(mm); err != nil {
return
}
if err = svc.preloadFlags(mm); err != nil {
return
}
return
}
// Preload for all messages
@@ -388,6 +418,21 @@ func (svc *message) preloadUsers(mm types.MessageSet) (err error) {
return
}
// Preload for all messages
func (svc *message) preloadFlags(mm types.MessageSet) (err error) {
var ff types.MessageFlagSet
ff, err = svc.mflag.FindByMessageIDs(mm.IDs()...)
if err != nil {
return
}
return ff.Walk(func(flag *types.MessageFlag) error {
mm.FindById(flag.MessageID).Flags = append(mm.FindById(flag.MessageID).Flags, flag)
return nil
})
}
func (svc *message) preloadAttachments(mm types.MessageSet) (err error) {
var (
ids []uint64
@@ -422,8 +467,9 @@ func (svc *message) preloadAttachments(mm types.MessageSet) (err error) {
// Sends message to event loop
func (svc *message) sendEvent(mm ...*types.Message) (err error) {
svc.preloadAttachments(mm)
svc.preloadUsers(mm)
if err = svc.preload(mm); err != nil {
return
}
for _, msg := range mm {
if msg.User == nil {
+11
View File
@@ -21,6 +21,7 @@ type (
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
Attachment *Attachment `json:"attachment,omitempty"`
User *authTypes.User `json:"user,omitempty"`
Flags MessageFlagSet `json:"flags,omitempty"`
}
MessageSet []*Message
@@ -85,6 +86,16 @@ func (mm MessageSet) FindById(ID uint64) *Message {
return nil
}
func (mm MessageSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(mm))
for i := range mm {
IDs[i] = mm[i].ID
}
return
}
func (mtype MessageType) String() string {
return string(mtype)
}
+75
View File
@@ -0,0 +1,75 @@
package types
import (
"time"
)
type (
MessageFlag struct {
ID uint64 `json:"id" db:"id"`
UserID uint64 `json:"userId" db:"rel_user"`
MessageID uint64 `json:"messageId" db:"rel_message"`
ChannelID uint64 `json:"channelId" db:"rel_channel"`
Flag string `json:"flag" db:"flag"`
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
}
MessageFlagSet []*MessageFlag
)
const (
MessageFlagPinnedToChannel string = "pin"
MessageFlagBookmarkedMessage string = "bookmark"
)
func (ff MessageFlagSet) Walk(w func(*MessageFlag) error) (err error) {
for i := range ff {
if err = w(ff[i]); err != nil {
return
}
}
return
}
func (ff MessageFlagSet) FindById(ID uint64) *MessageFlag {
for i := range ff {
if ff[i].ID == ID {
return ff[i]
}
}
return nil
}
func (ff MessageFlagSet) IsBookmarked(UserID uint64) bool {
for i := range ff {
if ff[i].UserID == UserID && ff[i].IsBookmark() {
return true
}
}
return false
}
func (ff MessageFlagSet) IsPinned() bool {
for i := range ff {
if ff[i].IsPin() {
return true
}
}
return false
}
func (f *MessageFlag) IsReaction() bool {
return f.Flag != MessageFlagPinnedToChannel && f.Flag != MessageFlagBookmarkedMessage
}
func (f *MessageFlag) IsPin() bool {
return f.Flag == MessageFlagPinnedToChannel
}
func (f *MessageFlag) IsBookmark() bool {
return f.Flag == MessageFlagBookmarkedMessage
}
-16
View File
@@ -1,16 +0,0 @@
package types
import (
"time"
)
type (
Reaction struct {
ID uint64 `json:"id" db:"id"`
UserID uint64 `json:"userId" db:"rel_user"`
MessageID uint64 `json:"messageId" db:"rel_message"`
ChannelID uint64 `json:"channelId" db:"rel_channel"`
Reaction string `json:"reaction" db:"reaction"`
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
}
)