From e351c73a0a802da880a073f218a823221e72b0be Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Sun, 9 Sep 2018 13:09:56 +0200 Subject: [PATCH] Serve attachment info alongside with message (rest & ws) --- sam/repository/attachment.go | 34 +++++++++++----------- sam/repository/message.go | 6 ++-- sam/repository/message_test.go | 2 +- sam/service/attachment.go | 43 ++++++++++++++++++++++++++-- sam/service/channel.go | 2 +- sam/service/message.go | 35 +++++++++++++++++++++-- sam/service/service_mock_test.go | 47 ++++++++++++++++--------------- sam/types/attachment.go | 19 ++++++++++++- sam/types/message.go | 41 +++++++++++++++++++++------ sam/websocket/outgoing/message.go | 14 +++++++++ sam/websocket/payload.go | 22 ++++++++++++++- 11 files changed, 204 insertions(+), 61 deletions(-) diff --git a/sam/repository/attachment.go b/sam/repository/attachment.go index 67966af89..56d2b7378 100644 --- a/sam/repository/attachment.go +++ b/sam/repository/attachment.go @@ -2,6 +2,7 @@ package repository import ( "github.com/crusttech/crust/sam/types" + "github.com/jmoiron/sqlx" "github.com/titpetric/factory" "time" ) @@ -9,6 +10,7 @@ import ( type ( Attachment interface { FindAttachmentByID(id uint64) (*types.Attachment, error) + FindAttachmentByMessageID(IDs ...uint64) (types.MessageAttachmentSet, error) CreateAttachment(mod *types.Attachment) (*types.Attachment, error) DeleteAttachmentByID(id uint64) error BindAttachment(attachmentId, messageId uint64) error @@ -30,27 +32,23 @@ func (r *repository) FindAttachmentByID(id uint64) (*types.Attachment, error) { return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrAttachmentNotFound) } -func (r *repository) FindAttachmentByMessageID(id uint64) (*types.Attachment, error) { - sql := "SELECT a.* " + - " FROM attachments AS a" + - " INNER JOIN message_attachment AS ma ON a.id = ma.rel_attachment " + - " WHERE ma.rel_message = ? AND " + sqlAttachmentScope - mod := &types.Attachment{} +func (r *repository) FindAttachmentByMessageID(IDs ...uint64) (rval types.MessageAttachmentSet, err error) { + rval = make([]*types.MessageAttachment, 0) - return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrAttachmentNotFound) -} + if len(IDs) == 0 { + return + } -func (r *repository) FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID uint64) ([]*types.Attachment, error) { - rval := make([]*types.Attachment, 0) + sql := `SELECT a.*, rel_message + FROM attachments AS a + INNER JOIN message_attachment AS ma ON a.id = ma.rel_attachment + WHERE ma.rel_message IN (?) AND ` + sqlAttachmentScope - sql := ` - SELECT * - FROM attachments - WHERE id BETWEEN ? AND ? - AND rel_channel = ? - AND deleted_at IS NULL` - - return rval, r.db().Select(&rval, sql, fromAttachmentID, toAttachmentID, channelID) + if sql, args, err := sqlx.In(sql, IDs); err != nil { + return nil, err + } else { + return rval, r.db().Select(&rval, sql, args...) + } } func (r *repository) CreateAttachment(mod *types.Attachment) (*types.Attachment, error) { diff --git a/sam/repository/message.go b/sam/repository/message.go index 8bd5110be..b5cc70e9c 100644 --- a/sam/repository/message.go +++ b/sam/repository/message.go @@ -9,7 +9,7 @@ import ( type ( Message interface { FindMessageByID(id uint64) (*types.Message, error) - FindMessages(filter *types.MessageFilter) ([]*types.Message, error) + 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 @@ -41,9 +41,9 @@ func (r *repository) FindMessageByID(id uint64) (*types.Message, error) { return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrMessageNotFound) } -func (r *repository) FindMessages(filter *types.MessageFilter) ([]*types.Message, error) { +func (r *repository) FindMessages(filter *types.MessageFilter) (types.MessageSet, error) { params := make([]interface{}, 0) - rval := make([]*types.Message, 0) + rval := make(types.MessageSet, 0) sql := sqlMessagesSelect diff --git a/sam/repository/message_test.go b/sam/repository/message_test.go index f12f97300..b5c8c80ca 100644 --- a/sam/repository/message_test.go +++ b/sam/repository/message_test.go @@ -18,7 +18,7 @@ func TestMessage(t *testing.T) { var msg1, msg2 = "Test message v1", "Test message v2" - var mm []*types.Message + var mm types.MessageSet { msg.Message = msg1 diff --git a/sam/service/attachment.go b/sam/service/attachment.go index 79483f30c..b93865f1e 100644 --- a/sam/service/attachment.go +++ b/sam/service/attachment.go @@ -7,6 +7,7 @@ import ( "github.com/crusttech/crust/sam/types" "github.com/titpetric/factory" "io" + "net/http" "path" "strings" ) @@ -100,7 +101,45 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string, }) } -func (svc attachment) makePreview(att *types.Attachment, originalFh io.ReadSeeker) (err error) { +func (svc attachment) extractMeta(att *types.Attachment, file io.ReadSeeker) (err error) { + if _, err = file.Seek(0, 0); err != nil { + return err + } + + // Make sure we rewind... + defer file.Seek(0, 0) + + // See http.DetectContentType about 512 bytes + var buf = make([]byte, 512) + if _, err = file.Read(buf); err != nil { + return + } + + att.Mimetype = http.DetectContentType(buf) + + // @todo compare mime with extension (or better, enforce extension from mimetype) + //if extensions, err := mime.ExtensionsByType(att.Mimetype); err == nil { + // extensions[0] + //} + + // @todo extract image info so we can provide additional features if needed + //if strings.HasPrefix(att.Mimetype, "image/gif") { + // if cfg, err := gif.DecodeAll(file); err == nil { + // m.Width = cfg.Config.Width + // m.Height = cfg.Config.Height + // m.Animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1 + // } + //} else if strings.HasPrefix(att.Mimetype, "image") { + // if cfg, _, err := image.DecodeConfig(file); err == nil { + // m.Width = cfg.Width + // m.Height = cfg.Height + // } + //} + + return +} + +func (svc attachment) makePreview(att *types.Attachment, original io.ReadSeeker) (err error) { if true { return } @@ -109,7 +148,7 @@ func (svc attachment) makePreview(att *types.Attachment, originalFh io.ReadSeeke var ext = "jpg" att.PreviewUrl = svc.sto.Preview(att.ID, ext) - return svc.sto.Save(att.PreviewUrl, originalFh) + return svc.sto.Save(att.PreviewUrl, original) } var _ AttachmentService = &attachment{} diff --git a/sam/service/channel.go b/sam/service/channel.go index d1b39cfd7..21b559fb1 100644 --- a/sam/service/channel.go +++ b/sam/service/channel.go @@ -174,7 +174,7 @@ func (svc channel) Create(ctx context.Context, in *types.Channel) (out *types.Ch func (svc channel) Update(ctx context.Context, in *types.Channel) (out *types.Channel, err error) { return out, svc.rpo.BeginWith(ctx, func(r repository.Interfaces) (err error) { - var msgs []*types.Message + var msgs types.MessageSet // @todo [SECURITY] can user access this channel? if out, err = r.FindChannelByID(in.ID); err != nil { diff --git a/sam/service/message.go b/sam/service/message.go index a49c02d8a..c096fccb7 100644 --- a/sam/service/message.go +++ b/sam/service/message.go @@ -14,7 +14,7 @@ type ( } MessageService interface { - Find(ctx context.Context, filter *types.MessageFilter) ([]*types.Message, error) + Find(ctx context.Context, filter *types.MessageFilter) (types.MessageSet, error) Create(ctx context.Context, messages *types.Message) (*types.Message, error) Update(ctx context.Context, messages *types.Message) (*types.Message, error) @@ -47,7 +47,7 @@ func Message() *message { return m } -func (svc message) Find(ctx context.Context, filter *types.MessageFilter) ([]*types.Message, error) { +func (svc message) Find(ctx context.Context, filter *types.MessageFilter) (mm types.MessageSet, err error) { // @todo get user from context var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity() @@ -55,7 +55,36 @@ func (svc message) Find(ctx context.Context, filter *types.MessageFilter) ([]*ty _ = currentUserID _ = filter.ChannelID - return svc.rpo.FindMessages(filter) + mm, err = svc.rpo.FindMessages(filter) + if err != nil { + return nil, err + } + + { + var ids []uint64 + mm.Walk(func(m *types.Message) error { + if m.Type == "attachment" { + ids = append(ids, m.ID) + } + return nil + }) + + if set, err := svc.rpo.FindAttachmentByMessageID(ids...); err != nil { + return nil, err + } else { + set.Walk(func(a *types.MessageAttachment) error { + if a.MessageID > 0 { + if m := mm.FindById(a.MessageID); m != nil { + m.Attachment = &a.Attachment + } + } + + return nil + }) + } + } + + return } func (svc message) Direct(ctx context.Context, recipientID uint64, in *types.Message) (out *types.Message, err error) { diff --git a/sam/service/service_mock_test.go b/sam/service/service_mock_test.go index f4e266327..29364bf64 100644 --- a/sam/service/service_mock_test.go +++ b/sam/service/service_mock_test.go @@ -108,17 +108,21 @@ func (mr *MockRepositoryMockRecorder) FindAttachmentByID(id interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAttachmentByID", reflect.TypeOf((*MockRepository)(nil).FindAttachmentByID), id) } -// FindAttachmentByRange mocks base method -func (m *MockRepository) FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID uint64) ([]*types.Attachment, error) { - ret := m.ctrl.Call(m, "FindAttachmentByRange", channelID, fromAttachmentID, toAttachmentID) - ret0, _ := ret[0].([]*types.Attachment) +// FindAttachmentByMessageID mocks base method +func (m *MockRepository) FindAttachmentByMessageID(IDs ...uint64) (types.MessageAttachmentSet, error) { + varargs := []interface{}{} + for _, a := range IDs { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FindAttachmentByMessageID", varargs...) + ret0, _ := ret[0].(types.MessageAttachmentSet) ret1, _ := ret[1].(error) return ret0, ret1 } -// FindAttachmentByRange indicates an expected call of FindAttachmentByRange -func (mr *MockRepositoryMockRecorder) FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAttachmentByRange", reflect.TypeOf((*MockRepository)(nil).FindAttachmentByRange), channelID, fromAttachmentID, toAttachmentID) +// FindAttachmentByMessageID indicates an expected call of FindAttachmentByMessageID +func (mr *MockRepositoryMockRecorder) FindAttachmentByMessageID(IDs ...interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAttachmentByMessageID", reflect.TypeOf((*MockRepository)(nil).FindAttachmentByMessageID), IDs...) } // CreateAttachment mocks base method @@ -134,19 +138,6 @@ func (mr *MockRepositoryMockRecorder) CreateAttachment(mod interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockRepository)(nil).CreateAttachment), mod) } -// UpdateAttachment mocks base method -func (m *MockRepository) UpdateAttachment(mod *types.Attachment) (*types.Attachment, error) { - ret := m.ctrl.Call(m, "UpdateAttachment", mod) - ret0, _ := ret[0].(*types.Attachment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateAttachment indicates an expected call of UpdateAttachment -func (mr *MockRepositoryMockRecorder) UpdateAttachment(mod interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAttachment", reflect.TypeOf((*MockRepository)(nil).UpdateAttachment), mod) -} - // DeleteAttachmentByID mocks base method func (m *MockRepository) DeleteAttachmentByID(id uint64) error { ret := m.ctrl.Call(m, "DeleteAttachmentByID", id) @@ -159,6 +150,18 @@ func (mr *MockRepositoryMockRecorder) DeleteAttachmentByID(id interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAttachmentByID", reflect.TypeOf((*MockRepository)(nil).DeleteAttachmentByID), id) } +// BindAttachment mocks base method +func (m *MockRepository) BindAttachment(attachmentId, messageId uint64) error { + ret := m.ctrl.Call(m, "BindAttachment", attachmentId, messageId) + ret0, _ := ret[0].(error) + return ret0 +} + +// BindAttachment indicates an expected call of BindAttachment +func (mr *MockRepositoryMockRecorder) BindAttachment(attachmentId, messageId interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindAttachment", reflect.TypeOf((*MockRepository)(nil).BindAttachment), attachmentId, messageId) +} + // FindChannelByID mocks base method func (m *MockRepository) FindChannelByID(id uint64) (*types.Channel, error) { ret := m.ctrl.Call(m, "FindChannelByID", id) @@ -312,9 +315,9 @@ func (mr *MockRepositoryMockRecorder) FindMessageByID(id interface{}) *gomock.Ca } // FindMessages mocks base method -func (m *MockRepository) FindMessages(filter *types.MessageFilter) ([]*types.Message, error) { +func (m *MockRepository) FindMessages(filter *types.MessageFilter) (types.MessageSet, error) { ret := m.ctrl.Call(m, "FindMessages", filter) - ret0, _ := ret[0].([]*types.Message) + ret0, _ := ret[0].(types.MessageSet) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/sam/types/attachment.go b/sam/types/attachment.go index bb103fe60..78831a932 100644 --- a/sam/types/attachment.go +++ b/sam/types/attachment.go @@ -6,7 +6,7 @@ import ( type ( Attachment struct { - ID uint64 `db:"id" json:"id,omitempty"` + ID uint64 `db:"id" json:"ID,omitempty"` UserID uint64 `db:"rel_user" json:"userID,omitempty"` Url string `db:"url" json:"url,omitempty"` PreviewUrl string `db:"preview_url"json:"previewUrl,omitempty"` @@ -17,4 +17,21 @@ type ( UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"` DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` } + + MessageAttachment struct { + Attachment + MessageID uint64 `db:"rel_message" json:"-"` + } + + MessageAttachmentSet []*MessageAttachment ) + +func (aa MessageAttachmentSet) Walk(w func(*MessageAttachment) error) (err error) { + for i := range aa { + if err = w(aa[i]); err != nil { + return + } + } + + return +} diff --git a/sam/types/message.go b/sam/types/message.go index e2320b378..04bc90f0b 100644 --- a/sam/types/message.go +++ b/sam/types/message.go @@ -6,17 +6,20 @@ import ( type ( Message struct { - ID uint64 `json:"id" db:"id"` - Type string `json:"type" db:"type"` - Message string `json:"message" db:"message"` - UserID uint64 `json:"userId" db:"rel_user"` - ChannelID uint64 `json:"channelId" db:"rel_channel"` - ReplyTo uint64 `json:"replyTo" db:"reply_to"` - 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"` + ID uint64 `json:"id" db:"id"` + Type string `json:"type" db:"type"` + Message string `json:"message" db:"message"` + UserID uint64 `json:"userId" db:"rel_user"` + ChannelID uint64 `json:"channelId" db:"rel_channel"` + ReplyTo uint64 `json:"replyTo" db:"reply_to"` + 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"` + Attachment *Attachment `json:"attachment,omitempty"` } + MessageSet []*Message + MessageFilter struct { Query string ChannelID uint64 @@ -25,3 +28,23 @@ type ( Limit uint } ) + +func (mm MessageSet) Walk(w func(*Message) error) (err error) { + for i := range mm { + if err = w(mm[i]); err != nil { + return + } + } + + return +} + +func (mm MessageSet) FindById(ID uint64) *Message { + for i := range mm { + if mm[i].ID == ID { + return mm[i] + } + } + + return nil +} diff --git a/sam/websocket/outgoing/message.go b/sam/websocket/outgoing/message.go index 1a3b3a075..1f42c6d75 100644 --- a/sam/websocket/outgoing/message.go +++ b/sam/websocket/outgoing/message.go @@ -14,6 +14,8 @@ type ( ChannelID string `json:"cid"` ReplyTo string `json:"rid"` + Attachment *Attachment `json:"att,omitempty"` + CreatedAt time.Time `json:"cat,omitempty"` UpdatedAt *time.Time `json:"uat,omitempty"` } @@ -30,6 +32,18 @@ type ( MessageDelete struct { ID string `json:"id"` } + + Attachment struct { + ID string `json:"id"` + UserID string `json:"uid"` + Url string `json:"url"` + PreviewUrl string `json:"prw"` + Size int64 `json:"sze"` + Mimetype string `json:"typ"` + Name string `json:"nme"` + CreatedAt time.Time `json:"cat,omitempty"` + UpdatedAt *time.Time `json:"uat,omitempty"` + } ) func (p *Message) EncodeMessage() ([]byte, error) { diff --git a/sam/websocket/payload.go b/sam/websocket/payload.go index f80a9b857..6834f6364 100644 --- a/sam/websocket/payload.go +++ b/sam/websocket/payload.go @@ -14,12 +14,14 @@ func payloadFromMessage(msg *types.Message) *outgoing.Message { UserID: uint64toa(msg.UserID), ReplyTo: uint64toa(msg.ReplyTo), + Attachment: payloadFromAttachment(msg.Attachment), + CreatedAt: msg.CreatedAt, UpdatedAt: msg.UpdatedAt, } } -func payloadFromMessages(msg []*types.Message) *outgoing.Messages { +func payloadFromMessages(msg types.MessageSet) *outgoing.Messages { msgs := make([]*outgoing.Message, len(msg)) for k, m := range msg { msgs[k] = payloadFromMessage(m) @@ -70,3 +72,21 @@ func payloadFromUsers(users []*types.User) *outgoing.Users { retval := outgoing.Users(uu) return &retval } + +func payloadFromAttachment(in *types.Attachment) *outgoing.Attachment { + if in == nil { + return nil + } + + return &outgoing.Attachment{ + ID: uint64toa(in.ID), + UserID: uint64toa(in.UserID), + Url: in.Url, + PreviewUrl: in.PreviewUrl, + Size: in.Size, + Mimetype: in.Mimetype, + Name: in.Name, + CreatedAt: in.CreatedAt, + UpdatedAt: in.UpdatedAt, + } +}