Implement message threads
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user