Implement message threads

This commit is contained in:
Denis Arh
2018-10-19 15:07:09 +02:00
parent 58d7506969
commit 4f13e8304f
28 changed files with 583 additions and 109 deletions
+3 -2
View File
@@ -2,9 +2,10 @@ package repository
import (
"context"
"time"
"github.com/crusttech/crust/auth/types"
"github.com/titpetric/factory"
"time"
)
type (
@@ -114,5 +115,5 @@ func (r *user) UnsuspendUserByID(id uint64) error {
}
func (r *user) DeleteUserByID(id uint64) error {
return r.updateColumnByID("users", "deleted_at", nil, id)
return r.updateColumnByID("users", "deleted_at", time.Now(), id)
}
+8 -7
View File
@@ -2,23 +2,24 @@ package incoming
type (
MessageCreate struct {
ChannelID string `json:"channelId"`
ChannelID string `json:"channelID"`
ReplyTo uint64 `json:"replyTo,omitempty,string"`
Message string `json:"message"`
}
MessageUpdate struct {
ID string `json:"id"`
ID string `json:"messageID"`
Message string `json:"message"`
}
MessageDelete struct {
ChannelID string `json:"channelId"`
ID string `json:"id"`
ID string `json:"messageID"`
}
Messages struct {
ChannelID string `json:"channelId"`
FromID string `json:"fromId,omitempty"`
UntilID string `json:"untilId,omitempty"`
ChannelID uint64 `json:"channelId,string"`
FirstID uint64 `json:"firstID,string"`
LastID uint64 `json:"lastID,string"`
RepliesTo uint64 `json:"repliesTo,string"`
}
)
+3 -2
View File
@@ -16,11 +16,12 @@ const (
func Message(msg *sam.Message) *outgoing.Message {
return &outgoing.Message{
ID: Uint64toa(msg.ID),
ID: msg.ID,
ChannelID: Uint64toa(msg.ChannelID),
Message: msg.Message,
Type: string(msg.Type),
ReplyTo: Uint64toa(msg.ReplyTo),
ReplyTo: msg.ReplyTo,
Replies: msg.Replies,
User: User(msg.User),
Attachment: Attachment(msg.Attachment),
+3 -2
View File
@@ -7,11 +7,12 @@ import (
type (
Message struct {
ID string `json:"ID"`
ID uint64 `json:"ID,string"`
Type string `json:"type"`
Message string `json:"message"`
ChannelID string `json:"channelID"`
ReplyTo string `json:"replyID"`
ReplyTo uint64 `json:"replyTo,omitempty,string"`
Replies uint `json:"replies,omitempty"`
User *User `json:"user"`
Attachment *Attachment `json:"att,omitempty"`
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
ALTER TABLE messages CHANGE reply_to reply_to BIGINT UNSIGNED NOT NULL DEFAULT 0;
ALTER TABLE messages ADD replies INT UNSIGNED NOT NULL DEFAULT 0;
+29
View File
@@ -481,6 +481,35 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Returns all replies to a message
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/replies` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Reply to a message
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/replies` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
| message | string | POST | Message contents (markdown) | N/A | YES |
## Pin message to channel (public bookmark)
#### Method
+25
View File
@@ -413,6 +413,31 @@
]
}
},
{
"name": "getReplies",
"path": "/{messageID}/replies",
"method": "GET",
"title": "Returns all replies to a message",
"parameters": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
]
}
},
{
"name": "createReply",
"path": "/{messageID}/replies",
"method": "POST",
"title": "Reply to a message",
"parameters": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
],
"post": [
{ "type": "string", "name": "message", "required": true, "title": "Message contents (markdown)" }
]
}
},
{
"name": "unpin",
"path": "/{messageID}/pin",
+40
View File
@@ -136,6 +136,46 @@
]
}
},
{
"Name": "getReplies",
"Method": "GET",
"Title": "Returns all replies to a message",
"Path": "/{messageID}/replies",
"Parameters": {
"path": [
{
"name": "messageID",
"required": true,
"title": "Message ID",
"type": "uint64"
}
]
}
},
{
"Name": "createReply",
"Method": "POST",
"Title": "Reply to a message",
"Path": "/{messageID}/replies",
"Parameters": {
"path": [
{
"name": "messageID",
"required": true,
"title": "Message ID",
"type": "uint64"
}
],
"post": [
{
"name": "message",
"required": true,
"title": "Message contents (markdown)",
"type": "string"
}
]
}
},
{
"Name": "unpin",
"Method": "DELETE",
+13 -10
View File
@@ -1,8 +1,13 @@
package repository
import (
"github.com/crusttech/crust/sam/types"
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestAttachment(t *testing.T) {
@@ -13,28 +18,26 @@ func TestAttachment(t *testing.T) {
return
}
rpo := New()
rpo := Attachment(context.Background(), factory.Database.MustGet())
att := &types.Attachment{}
var aa []*types.Attachment
att.ChannelID = 1
att.UserID = 1
{
att, err = rpo.CreateAttachment(att)
assert(t, err == nil, "CreateAttachment error: %v", err)
assert(t, att.ChannelID == 1, "Changes were not stored")
assert(t, att.UserID == 1, "Changes were not stored")
{
att, err = rpo.FindAttachmentByID(att.ID)
assert(t, err == nil, "FindAttachmentByID error: %v", err)
assert(t, att.ChannelID == 2, "Changes were not stored")
assert(t, att.UserID == 1, "Changes were not stored")
}
{
aa, err = rpo.FindAttachmentByRange(2, 0, att.ID)
assert(t, err == nil, "FindAttachmentByRange error: %v", err)
assert(t, len(aa) > 0, "No results found")
att, err = rpo.FindAttachmentByID(att.ID)
assert(t, err == nil, "FindAttachmentByMessageID error: %v", err)
assert(t, att != nil, "No results found")
}
{
+7 -2
View File
@@ -1,8 +1,13 @@
package repository
import (
"github.com/crusttech/crust/sam/types"
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestChannel(t *testing.T) {
@@ -13,7 +18,7 @@ func TestChannel(t *testing.T) {
return
}
rpo := New()
rpo := Channel(context.Background(), factory.Database.MustGet())
chn := &types.Channel{}
var name1, name2 = "Test channel v1", "Test channel v2"
+44 -14
View File
@@ -17,7 +17,9 @@ type (
FindMessages(filter *types.MessageFilter) (types.MessageSet, error)
CreateMessage(mod *types.Message) (*types.Message, error)
UpdateMessage(mod *types.Message) (*types.Message, error)
DeleteMessageByID(id uint64) error
DeleteMessageByID(ID uint64) error
IncReplyCount(ID uint64) error
DecReplyCount(ID uint64) error
}
message struct {
@@ -26,6 +28,8 @@ type (
)
const (
MESSAGES_MAX_LIMIT = 100
sqlMessageScope = "deleted_at IS NULL"
sqlMessagesSelect = `SELECT id,
@@ -33,13 +37,17 @@ const (
message,
rel_user,
rel_channel,
COALESCE(reply_to, 0) AS reply_to,
reply_to,
replies,
created_at,
updated_at,
deleted_at
FROM messages
WHERE ` + sqlMessageScope
sqlMessageRepliesIncCount = `UPDATE messages SET replies = replies + 1 WHERE id = ? AND reply_to = 0`
sqlMessageRepliesDecCount = `UPDATE messages SET replies = replies - 1 WHERE id = ? AND reply_to = 0`
ErrMessageNotFound = repositoryError("MessageNotFound")
)
@@ -78,23 +86,35 @@ func (r *message) FindMessages(filter *types.MessageFilter) (types.MessageSet, e
params = append(params, filter.ChannelID)
}
if filter.FromMessageID > 0 {
sql += " AND id > ? "
params = append(params, filter.FromMessageID)
if filter.RepliesTo > 0 {
sql += " AND reply_to = ? "
params = append(params, filter.RepliesTo)
} else {
sql += " AND reply_to = 0 "
}
if filter.UntilMessageID > 0 {
sql += " AND id < ? "
params = append(params, filter.UntilMessageID)
if filter.FirstID > 0 || filter.LastID > 0 {
// Fetching (exclusively) range of messages, without reply
if filter.FirstID > 0 {
sql += " AND id > ? "
params = append(params, filter.FirstID)
}
if filter.LastID > 0 {
sql += " AND id < ? "
params = append(params, filter.LastID)
}
}
sql += " ORDER BY id DESC"
if filter.Limit > 0 {
// @todo implement some kind of protection
sql += " LIMIT ? "
params = append(params, filter.Limit)
if filter.Limit == 0 || filter.Limit > MESSAGES_MAX_LIMIT {
filter.Limit = MESSAGES_MAX_LIMIT
}
sql += " LIMIT ? "
params = append(params, filter.Limit)
return rval, r.db().Select(&rval, sql, params...)
}
@@ -111,6 +131,16 @@ func (r *message) UpdateMessage(mod *types.Message) (*types.Message, error) {
return mod, r.db().Replace("messages", mod)
}
func (r *message) DeleteMessageByID(id uint64) error {
return r.updateColumnByID("messages", "deleted_at", nil, id)
func (r *message) DeleteMessageByID(ID uint64) error {
return r.updateColumnByID("messages", "deleted_at", time.Now(), ID)
}
func (r *message) IncReplyCount(ID uint64) error {
_, err := r.db().Exec(sqlMessageRepliesIncCount, ID)
return err
}
func (r *message) DecReplyCount(ID uint64) error {
_, err := r.db().Exec(sqlMessageRepliesDecCount, ID)
return err
}
+76 -3
View File
@@ -1,8 +1,13 @@
package repository
import (
"github.com/crusttech/crust/sam/types"
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestMessage(t *testing.T) {
@@ -13,7 +18,7 @@ func TestMessage(t *testing.T) {
return
}
rpo := New()
rpo := Message(context.Background(), factory.Database.MustGet())
msg := &types.Message{}
var msg1, msg2 = "Test message v1", "Test message v2"
@@ -35,7 +40,7 @@ func TestMessage(t *testing.T) {
{
msg, err = rpo.FindMessageByID(msg.ID)
assert(t, err == nil, "FFindMessageByID error: %v", err)
assert(t, err == nil, "FindMessageByID error: %v", err)
assert(t, msg.Message == msg2, "Changes were not stored")
}
@@ -51,3 +56,71 @@ func TestMessage(t *testing.T) {
}
}
}
func TestReplies(t *testing.T) {
var err error
if testing.Short() {
t.Skip("skipping test in short mode.")
return
}
chID := factory.Sonyflake.NextID()
rpo := Message(context.Background(), factory.Database.MustGet())
msg := &types.Message{ChannelID: chID}
rpl := &types.Message{ChannelID: chID}
var mm types.MessageSet
tx(t, func() error {
msg, err = rpo.CreateMessage(msg)
assert(t, err == nil, "CreateMessage error: %v", err)
assert(t, msg.ID > 0, "Message did not get its ID")
rpl.ReplyTo = msg.ID
rpl, err = rpo.CreateMessage(rpl)
assert(t, err == nil, "CreateMessage error: %v", err)
assert(t, rpl.ID > 0, "Reply did not get its ID")
{
mm, err = rpo.FindMessages(&types.MessageFilter{
RepliesTo: msg.ID,
ChannelID: chID,
})
assert(t, err == nil, "FindMessages error: %v", err)
assert(t, len(mm) == 1, "Failed to fetch only reply")
assert(t, mm[0].ID == rpl.ID, "Reply ID does not match")
}
{
mm, err = rpo.FindMessages(&types.MessageFilter{
ChannelID: chID,
})
assert(t, err == nil, "FindMessages error: %v", err)
assert(t, len(mm) == 1, "Failed to fetch only original message")
assert(t, mm[0].ID == msg.ID, "Reply ID does not match")
}
{
rpo.IncReplyCount(msg.ID)
rpo.IncReplyCount(msg.ID)
rpo.IncReplyCount(msg.ID)
msg, err = rpo.FindMessageByID(msg.ID)
assert(t, err == nil, "FindMessageByID error: %v", err)
assert(t, msg.Replies == 3, "Reply counter check failed, expecting 3, got %v", msg.Replies)
rpo.DecReplyCount(msg.ID)
rpo.DecReplyCount(msg.ID)
msg, err = rpo.FindMessageByID(msg.ID)
assert(t, err == nil, "FindMessageByID error: %v", err)
assert(t, msg.Replies == 1, "Reply counter check failed, expecting 1, got %v", msg.Replies)
}
return nil
})
}
+1 -1
View File
@@ -89,5 +89,5 @@ func (r *organisation) UnarchiveOrganisationByID(id uint64) error {
}
func (r *organisation) DeleteOrganisationByID(id uint64) error {
return r.updateColumnByID("organisations", "deleted_at", nil, id)
return r.updateColumnByID("organisations", "deleted_at", time.Now(), id)
}
+7 -2
View File
@@ -1,8 +1,13 @@
package repository
import (
"github.com/crusttech/crust/sam/types"
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestOrganisation(t *testing.T) {
@@ -13,7 +18,7 @@ func TestOrganisation(t *testing.T) {
return
}
rpo := New()
rpo := Organisation(context.Background(), factory.Database.MustGet())
org := &types.Organisation{}
var name1, name2 = "Test organisation v1", "Test organisation v2"
+7 -2
View File
@@ -1,8 +1,13 @@
package repository
import (
"github.com/crusttech/crust/sam/types"
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestReaction(t *testing.T) {
@@ -13,7 +18,7 @@ func TestReaction(t *testing.T) {
return
}
rpo := New()
rpo := Reaction(context.Background(), factory.Database.MustGet())
react := &types.Reaction{}
var reaction = ":laugh:"
+16 -3
View File
@@ -2,10 +2,23 @@ package repository
import (
"context"
"testing"
)
func TestEvents(t *testing.T) {
repo := &repository{}
repo.With(context.Background(), nil)
func tx(t *testing.T, f func() error) {
db := DB(context.Background())
if err := db.Begin(); err != nil {
t.Errorf("Could not begin transaction: %v", err)
}
if err := f(); err != nil {
t.Errorf("Test transaction resulted in an error: %v", err)
}
if err := db.Rollback(); err != nil {
t.Errorf("Could not rollback transaction: %v", err)
}
}
+1 -1
View File
@@ -92,7 +92,7 @@ func (r *team) UnarchiveTeamByID(id uint64) error {
}
func (r *team) DeleteTeamByID(id uint64) error {
return r.updateColumnByID("teams", "deleted_at", nil, id)
return r.updateColumnByID("teams", "deleted_at", time.Now(), id)
}
func (r *team) MergeTeamByID(id, targetTeamID uint64) error {
+7 -2
View File
@@ -1,8 +1,13 @@
package repository
import (
"github.com/crusttech/crust/sam/types"
"context"
"github.com/titpetric/factory"
"testing"
"github.com/crusttech/crust/sam/types"
)
func TestTeam(t *testing.T) {
@@ -13,7 +18,7 @@ func TestTeam(t *testing.T) {
return
}
rpo := New()
rpo := Team(context.Background(), factory.Database.MustGet())
team := &types.Team{}
var name1, name2 = "Test team v1", "Test team v2"
+31 -11
View File
@@ -33,6 +33,8 @@ type MessageAPI interface {
Delete(context.Context, *request.MessageDelete) (interface{}, error)
Search(context.Context, *request.MessageSearch) (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)
@@ -42,17 +44,19 @@ type MessageAPI interface {
// 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)
Search func(http.ResponseWriter, *http.Request)
Pin 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)
Search 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)
}
func NewMessage(mh MessageAPI) *Message {
@@ -99,6 +103,20 @@ func NewMessage(mh MessageAPI) *Message {
return mh.Pin(r.Context(), params)
})
},
GetReplies: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageGetReplies()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.GetReplies(r.Context(), params)
})
},
CreateReply: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageCreateReply()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.CreateReply(r.Context(), params)
})
},
Unpin: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageUnpin()
@@ -147,6 +165,8 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Delete("/{messageID}", mh.Delete)
r.Get("/search", mh.Search)
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)
+17 -3
View File
@@ -34,10 +34,25 @@ func (ctrl *Message) Create(ctx context.Context, r *request.MessageCreate) (inte
}))
}
func (ctrl *Message) CreateReply(ctx context.Context, r *request.MessageCreateReply) (interface{}, error) {
return ctrl.wrap(ctrl.svc.msg.With(ctx).Create(&types.Message{
ChannelID: r.ChannelID,
ReplyTo: r.MessageID,
Message: r.Message,
}))
}
func (ctrl *Message) GetReplies(ctx context.Context, r *request.MessageGetReplies) (interface{}, error) {
return ctrl.wrapSet(ctrl.svc.msg.With(ctx).Find(&types.MessageFilter{
ChannelID: r.ChannelID,
RepliesTo: r.MessageID,
}))
}
func (ctrl *Message) History(ctx context.Context, r *request.MessageHistory) (interface{}, error) {
return ctrl.wrapSet(ctrl.svc.msg.With(ctx).Find(&types.MessageFilter{
ChannelID: r.ChannelID,
FromMessageID: r.LastMessageID,
ChannelID: r.ChannelID,
FirstID: r.LastMessageID,
}))
}
@@ -83,7 +98,6 @@ func (ctrl *Message) React(ctx context.Context, r *request.MessageReact) (interf
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) wrap(m *types.Message, err error) (*outgoing.Message, error) {
if err != nil {
return nil, err
+93
View File
@@ -313,6 +313,99 @@ func (m *MessagePin) Fill(r *http.Request) error {
var _ RequestFiller = NewMessagePin()
// Message getReplies request parameters
type MessageGetReplies struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessageGetReplies() *MessageGetReplies {
return &MessageGetReplies{}
}
func (m *MessageGetReplies) Fill(r *http.Request) error {
var 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")
}
}
r.ParseForm()
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 = NewMessageGetReplies()
// Message createReply request parameters
type MessageCreateReply struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
Message string
}
func NewMessageCreateReply() *MessageCreateReply {
return &MessageCreateReply{}
}
func (m *MessageCreateReply) Fill(r *http.Request) error {
var 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")
}
}
r.ParseForm()
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"))
if val, ok := post["message"]; ok {
m.Message = val
}
return err
}
var _ RequestFiller = NewMessageCreateReply()
// Message unpin request parameters
type MessageUnpin struct {
MessageID uint64 `json:",string"`
+79 -14
View File
@@ -72,6 +72,32 @@ func (mr *MockChannelServiceMockRecorder) Find(filter interface{}) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockChannelService)(nil).Find), filter)
}
// Create mocks base method
func (m *MockChannelService) Create(channel *types.Channel) (*types.Channel, error) {
ret := m.ctrl.Call(m, "Create", channel)
ret0, _ := ret[0].(*types.Channel)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockChannelServiceMockRecorder) Create(channel interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockChannelService)(nil).Create), channel)
}
// Update mocks base method
func (m *MockChannelService) Update(channel *types.Channel) (*types.Channel, error) {
ret := m.ctrl.Call(m, "Update", channel)
ret0, _ := ret[0].(*types.Channel)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update
func (mr *MockChannelServiceMockRecorder) Update(channel interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockChannelService)(nil).Update), channel)
}
// FindByMembership mocks base method
func (m *MockChannelService) FindByMembership() ([]*types.Channel, error) {
ret := m.ctrl.Call(m, "FindByMembership")
@@ -98,30 +124,57 @@ func (mr *MockChannelServiceMockRecorder) FindMembers(channelID interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMembers", reflect.TypeOf((*MockChannelService)(nil).FindMembers), channelID)
}
// Create mocks base method
func (m *MockChannelService) Create(channel *types.Channel) (*types.Channel, error) {
ret := m.ctrl.Call(m, "Create", channel)
ret0, _ := ret[0].(*types.Channel)
// InviteUser mocks base method
func (m *MockChannelService) InviteUser(channelID uint64, memberIDs ...uint64) (types.ChannelMemberSet, error) {
varargs := []interface{}{channelID}
for _, a := range memberIDs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "InviteUser", varargs...)
ret0, _ := ret[0].(types.ChannelMemberSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockChannelServiceMockRecorder) Create(channel interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockChannelService)(nil).Create), channel)
// InviteUser indicates an expected call of InviteUser
func (mr *MockChannelServiceMockRecorder) InviteUser(channelID interface{}, memberIDs ...interface{}) *gomock.Call {
varargs := append([]interface{}{channelID}, memberIDs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteUser", reflect.TypeOf((*MockChannelService)(nil).InviteUser), varargs...)
}
// Update mocks base method
func (m *MockChannelService) Update(channel *types.Channel) (*types.Channel, error) {
ret := m.ctrl.Call(m, "Update", channel)
ret0, _ := ret[0].(*types.Channel)
// AddMember mocks base method
func (m *MockChannelService) AddMember(channelID uint64, memberIDs ...uint64) (types.ChannelMemberSet, error) {
varargs := []interface{}{channelID}
for _, a := range memberIDs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddMember", varargs...)
ret0, _ := ret[0].(types.ChannelMemberSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update
func (mr *MockChannelServiceMockRecorder) Update(channel interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockChannelService)(nil).Update), channel)
// AddMember indicates an expected call of AddMember
func (mr *MockChannelServiceMockRecorder) AddMember(channelID interface{}, memberIDs ...interface{}) *gomock.Call {
varargs := append([]interface{}{channelID}, memberIDs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMember", reflect.TypeOf((*MockChannelService)(nil).AddMember), varargs...)
}
// DeleteMember mocks base method
func (m *MockChannelService) DeleteMember(channelID uint64, memberIDs ...uint64) error {
varargs := []interface{}{channelID}
for _, a := range memberIDs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteMember", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteMember indicates an expected call of DeleteMember
func (mr *MockChannelServiceMockRecorder) DeleteMember(channelID interface{}, memberIDs ...interface{}) *gomock.Call {
varargs := append([]interface{}{channelID}, memberIDs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMember", reflect.TypeOf((*MockChannelService)(nil).DeleteMember), varargs...)
}
// Archive mocks base method
@@ -159,3 +212,15 @@ func (m *MockChannelService) Delete(ID uint64) error {
func (mr *MockChannelServiceMockRecorder) Delete(ID interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockChannelService)(nil).Delete), ID)
}
// RecordView mocks base method
func (m *MockChannelService) RecordView(channelID, userID, lastMessageID uint64) error {
ret := m.ctrl.Call(m, "RecordView", channelID, userID, lastMessageID)
ret0, _ := ret[0].(error)
return ret0
}
// RecordView indicates an expected call of RecordView
func (mr *MockChannelServiceMockRecorder) RecordView(channelID, userID, lastMessageID interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordView", reflect.TypeOf((*MockChannelService)(nil).RecordView), channelID, userID, lastMessageID)
}
+41 -4
View File
@@ -3,6 +3,7 @@ package service
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory"
authService "github.com/crusttech/crust/auth/service"
@@ -122,11 +123,34 @@ func (svc *message) Create(mod *types.Message) (message *types.Message, err erro
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
// @todo verify if current user can access & write to this channel
mod.UserID = currentUserID
return message, svc.db.Transaction(func() (err error) {
if mod.ReplyTo > 0 {
original, err := svc.message.FindMessageByID(mod.ReplyTo)
if err != nil {
return err
}
if original.ReplyTo > 0 {
// We do not want to have multi-level threads
// Take original's reply-to and use it
mod.ReplyTo = original.ReplyTo
}
mod.ChannelID = original.ChannelID
if err = svc.message.IncReplyCount(original.ID); err != nil {
return err
}
}
if mod.ChannelID == 0 {
return errors.New("ChannelID missing")
}
// @todo [SECURITY] verify if current user can access & write to this channel
if message, err = svc.message.CreateMessage(mod); err != nil {
return err
}
@@ -159,7 +183,7 @@ func (svc *message) Update(mod *types.Message) (*types.Message, error) {
return message, svc.sendEvent(message)
}
func (svc *message) Delete(id uint64) error {
func (svc *message) Delete(ID uint64) error {
// @todo get user from context
var currentUserID uint64 = repository.Identity(svc.ctx)
@@ -170,7 +194,20 @@ func (svc *message) Delete(id uint64) error {
// @todo verify ownership
return svc.db.Transaction(func() (err error) {
if err = svc.message.DeleteMessageByID(id); err != nil {
msg, err := svc.message.FindMessageByID(ID)
if err != nil {
return err
}
if msg.ReplyTo > 0 {
// This is a reply to another message,
// decrease
if err = svc.message.DecReplyCount(msg.ReplyTo); err != nil {
return
}
}
if err = svc.message.DeleteMessageByID(ID); err != nil {
return
}
-13
View File
@@ -157,19 +157,6 @@ func (mr *MockMessageServiceMockRecorder) Unflag(messageID interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unflag", reflect.TypeOf((*MockMessageService)(nil).Unflag), messageID)
}
// Direct mocks base method
func (m *MockMessageService) Direct(recipientID uint64, in *types.Message) (*types.Message, error) {
ret := m.ctrl.Call(m, "Direct", recipientID, in)
ret0, _ := ret[0].(*types.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Direct indicates an expected call of Direct
func (mr *MockMessageServiceMockRecorder) Direct(recipientID, in interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Direct", reflect.TypeOf((*MockMessageService)(nil).Direct), recipientID, in)
}
// Delete mocks base method
func (m *MockMessageService) Delete(ID uint64) error {
ret := m.ctrl.Call(m, "Delete", ID)
+16 -5
View File
@@ -15,6 +15,7 @@ type (
UserID uint64 `json:"userId" db:"rel_user"`
ChannelID uint64 `json:"channelId" db:"rel_channel"`
ReplyTo uint64 `json:"replyTo" db:"reply_to"`
Replies uint `json:"replies" db:"replies"`
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
@@ -25,11 +26,21 @@ type (
MessageSet []*Message
MessageFilter struct {
Query string
ChannelID uint64
FromMessageID uint64
UntilMessageID uint64
Limit uint
Query string
// All messages that belong to a channel
ChannelID uint64
// Return all replies to a single message
RepliesTo uint64
// (FirstID...LastID), for paging
//
// Include all messsages which IDs range from "first" to "last" (exclusive!)
FirstID uint64
LastID uint64
Limit uint
}
MessageType string
+1 -1
View File
@@ -27,7 +27,7 @@ func (s *Session) execCommand(ctx context.Context, c *incoming.ExecCommand) erro
}
return s.sendReply(&outgoing.Message{
ID: payload.Uint64toa(factory.Sonyflake.NextID()),
ID: factory.Sonyflake.NextID(),
User: payload.User(user),
CreatedAt: time.Now(),
Type: "hallucination",
+12 -4
View File
@@ -11,6 +11,7 @@ import (
func (s *Session) messageCreate(ctx context.Context, p *incoming.MessageCreate) error {
_, err := s.svc.msg.With(ctx).Create(&types.Message{
ChannelID: payload.ParseUInt64(p.ChannelID),
ReplyTo: p.ReplyTo,
Message: p.Message,
})
@@ -33,9 +34,11 @@ func (s *Session) messageDelete(ctx context.Context, p *incoming.MessageDelete)
func (s *Session) messageHistory(ctx context.Context, p *incoming.Messages) error {
var (
filter = &types.MessageFilter{
ChannelID: payload.ParseUInt64(p.ChannelID),
FromMessageID: payload.ParseUInt64(p.FromID),
UntilMessageID: payload.ParseUInt64(p.UntilID),
ChannelID: p.ChannelID,
FirstID: p.FirstID,
LastID: p.LastID,
RepliesTo: p.RepliesTo,
// Max no. of messages we will return
Limit: 50,
@@ -47,5 +50,10 @@ func (s *Session) messageHistory(ctx context.Context, p *incoming.Messages) erro
return err
}
return s.sendReply(payload.Messages(messages))
err = s.sendReply(payload.Messages(messages))
if err != nil {
return err
}
return nil
}