Add upload/attachment capabilities
This commit is contained in:
parent
bf9ca1ac1e
commit
0400451823
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Auth login request parameters
|
||||
type AuthLogin struct {
|
||||
|
||||
@ -9,11 +9,13 @@ import (
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
{foreach $calls as $call}
|
||||
// {name} {call.name} request parameters
|
||||
@ -58,14 +60,18 @@ func ({self} *{name|expose}{call.name|capitalize}) Fill(r *http.Request) error {
|
||||
{foreach $params as $param}
|
||||
{if strtolower($method) === "path"}
|
||||
{self}.{param.name|expose} = {if ($param.type !== "string")}{$parsers[$param.type]}({/if}chi.URLParam(r, "{param.name}"){if ($param.type !== "string")}){/if}
|
||||
{elseif $param.type === "*multipart.FileHeader"}
|
||||
if _, {self}.{param.name|expose}, err = r.FormFile("{$param.name}"); err != nil {
|
||||
return errors.Wrap(err, "error procesing uploaded file")
|
||||
}
|
||||
{elseif substr($param.type, 0, 2) !== '[]'}
|
||||
if val, ok := {method|strtolower}["{param.name}"]; ok {
|
||||
{if $param.type === "types.JSONText"}
|
||||
if {self}.{param.name|expose}, err = {$parsers[$param.type]}(val); err != nil {
|
||||
return err
|
||||
}
|
||||
if {self}.{param.name|expose}, err = {$parsers[$param.type]}(val); err != nil {
|
||||
return err
|
||||
}
|
||||
{else}
|
||||
{self}.{param.name|expose} = {if ($param.type !== "string")}{$parsers[$param.type]}(val){else}val{/if}
|
||||
{self}.{param.name|expose} = {if ($param.type !== "string")}{$parsers[$param.type]}(val){else}val{/if}
|
||||
{/if}
|
||||
}{/if}
|
||||
{/foreach}
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Field list request parameters
|
||||
type FieldList struct {
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Module list request parameters
|
||||
type ModuleList struct {
|
||||
|
||||
@ -10,7 +10,7 @@ CREATE TABLE organisations (
|
||||
deleted_at DATETIME NULL, -- organisation soft delete
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Keeps all known teams
|
||||
CREATE TABLE teams (
|
||||
@ -24,7 +24,7 @@ CREATE TABLE teams (
|
||||
deleted_at DATETIME NULL, -- team soft delete
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Keeps all known channels
|
||||
CREATE TABLE channels (
|
||||
@ -46,7 +46,7 @@ CREATE TABLE channels (
|
||||
rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Keeps all known users, home and external organisation
|
||||
-- changes are stored in audit log
|
||||
@ -64,7 +64,7 @@ CREATE TABLE users (
|
||||
deleted_at DATETIME NULL, -- user soft delete
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Keeps team memberships
|
||||
CREATE TABLE team_members (
|
||||
@ -72,7 +72,7 @@ CREATE TABLE team_members (
|
||||
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
|
||||
|
||||
PRIMARY KEY (rel_team, rel_user)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- handles channel membership
|
||||
CREATE TABLE channel_members (
|
||||
@ -85,7 +85,7 @@ CREATE TABLE channel_members (
|
||||
updated_at DATETIME NULL,
|
||||
|
||||
PRIMARY KEY (rel_channel, rel_user)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE channel_views (
|
||||
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
|
||||
@ -98,7 +98,7 @@ CREATE TABLE channel_views (
|
||||
new_since INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (rel_user, rel_channel)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE channel_pins (
|
||||
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
|
||||
@ -108,7 +108,7 @@ CREATE TABLE channel_pins (
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (rel_channel, rel_message)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE messages (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
@ -124,7 +124,7 @@ CREATE TABLE messages (
|
||||
deleted_at DATETIME NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE reactions (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
@ -136,26 +136,32 @@ CREATE TABLE reactions (
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE attachments (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
|
||||
rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),
|
||||
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
|
||||
url TEXT,
|
||||
preview_url TEXT,
|
||||
|
||||
url VARCHAR(512),
|
||||
preview_url VARCHAR(512),
|
||||
|
||||
size INT UNSIGNED,
|
||||
mimetype TEXT,
|
||||
mimetype VARCHAR(255),
|
||||
name TEXT,
|
||||
attachment JSON NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
updated_at DATETIME NULL,
|
||||
deleted_at DATETIME NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE message_attachment (
|
||||
rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),
|
||||
rel_attachment BIGINT UNSIGNED NOT NULL REFERENCES attachment(id),
|
||||
|
||||
PRIMARY KEY (rel_message)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE event_queue (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
@ -164,11 +170,11 @@ CREATE TABLE event_queue (
|
||||
payload JSON,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE event_queue_synced (
|
||||
origin BIGINT UNSIGNED NOT NULL,
|
||||
rel_last BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
PRIMARY KEY (origin)
|
||||
);
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
@ -351,6 +351,21 @@ A channel is a representation of a sequence of messages. It has meta data like c
|
||||
| channelID | uint64 | PATH | Channel ID | N/A | YES |
|
||||
| userID | []uint64 | POST | User ID | N/A | NO |
|
||||
|
||||
## Attach file to channel
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/channels/{channelID}/attach` | HTTP/S | POST | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| channelID | uint64 | PATH | Channel ID | N/A | YES |
|
||||
| upload | *multipart.FileHeader | POST | File to upload | N/A | YES |
|
||||
|
||||
|
||||
|
||||
|
||||
@ -433,19 +448,6 @@ The following event types may be sent with a message event:
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| messageID | uint64 | PATH | Message ID | N/A | YES |
|
||||
|
||||
## Attach file to message
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/channels/{channelID}/messages/{messageID}/attach` | HTTP/S | PUT | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
|
||||
## Search messages
|
||||
|
||||
#### Method
|
||||
|
||||
@ -304,7 +304,21 @@
|
||||
{ "name": "userID", "type": "[]uint64", "required": false, "title": "User ID" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attach",
|
||||
"path": "/{channelID}/attach",
|
||||
"method": "POST",
|
||||
"title": "Attach file to channel",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{ "name": "channelID", "type": "uint64", "required": true, "title": "Channel ID" }
|
||||
],
|
||||
"post": [
|
||||
{ "name": "upload", "type": "*multipart.FileHeader", "required": true, "title": "File to upload" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -366,20 +380,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attach",
|
||||
"path": "/{messageID}/attach",
|
||||
"method": "PUT",
|
||||
"title": "Attach file to message",
|
||||
"params": {
|
||||
"path": [
|
||||
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
|
||||
],
|
||||
"post": [
|
||||
{ "name": "name", "type": "string", "title": "File name to drop" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"path": "/search",
|
||||
|
||||
@ -201,6 +201,30 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "attach",
|
||||
"Method": "POST",
|
||||
"Title": "Attach file to channel",
|
||||
"Path": "/{channelID}/attach",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "channelID",
|
||||
"required": true,
|
||||
"title": "Channel ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"name": "upload",
|
||||
"required": true,
|
||||
"title": "File to upload",
|
||||
"type": "*multipart.FileHeader"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -92,13 +92,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "attach",
|
||||
"Method": "PUT",
|
||||
"Title": "Attach file to message",
|
||||
"Path": "/{messageID}/attach",
|
||||
"Parameters": null
|
||||
},
|
||||
{
|
||||
"Name": "search",
|
||||
"Method": "GET",
|
||||
|
||||
@ -9,10 +9,9 @@ import (
|
||||
type (
|
||||
Attachment interface {
|
||||
FindAttachmentByID(id uint64) (*types.Attachment, error)
|
||||
FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID uint64) ([]*types.Attachment, error)
|
||||
CreateAttachment(mod *types.Attachment) (*types.Attachment, error)
|
||||
UpdateAttachment(mod *types.Attachment) (*types.Attachment, error)
|
||||
DeleteAttachmentByID(id uint64) error
|
||||
BindAttachment(attachmentId, messageId uint64) error
|
||||
}
|
||||
)
|
||||
|
||||
@ -31,6 +30,16 @@ 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{}
|
||||
|
||||
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrAttachmentNotFound)
|
||||
}
|
||||
|
||||
func (r *repository) FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID uint64) ([]*types.Attachment, error) {
|
||||
rval := make([]*types.Attachment, 0)
|
||||
|
||||
@ -45,21 +54,24 @@ func (r *repository) FindAttachmentByRange(channelID, fromAttachmentID, toAttach
|
||||
}
|
||||
|
||||
func (r *repository) CreateAttachment(mod *types.Attachment) (*types.Attachment, error) {
|
||||
mod.ID = factory.Sonyflake.NextID()
|
||||
if mod.ID == 0 {
|
||||
mod.ID = factory.Sonyflake.NextID()
|
||||
}
|
||||
|
||||
mod.CreatedAt = time.Now()
|
||||
|
||||
mod.Attachment = coalesceJson(mod.Attachment, []byte("{}"))
|
||||
|
||||
return mod, r.db().Insert("attachments", mod)
|
||||
}
|
||||
|
||||
func (r *repository) UpdateAttachment(mod *types.Attachment) (*types.Attachment, error) {
|
||||
mod.UpdatedAt = timeNowPtr()
|
||||
mod.Attachment = coalesceJson(mod.Attachment, []byte("{}"))
|
||||
|
||||
return mod, r.db().Replace("attachments", mod)
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAttachmentByID(id uint64) error {
|
||||
return r.updateColumnByID("attachments", "deleted_at", nil, id)
|
||||
}
|
||||
|
||||
func (r *repository) BindAttachment(attachmentId, messageId uint64) error {
|
||||
bond := struct {
|
||||
RelAttachment uint64 `db:"rel_attachment"`
|
||||
RelMessage uint64 `db:"rel_message"`
|
||||
}{attachmentId, messageId}
|
||||
|
||||
return r.db().Insert("message_attachment", bond)
|
||||
}
|
||||
|
||||
@ -25,14 +25,6 @@ func TestAttachment(t *testing.T) {
|
||||
assert(t, err == nil, "CreateAttachment error: %v", err)
|
||||
assert(t, att.ChannelID == 1, "Changes were not stored")
|
||||
|
||||
{
|
||||
att.ChannelID = 2
|
||||
|
||||
att, err = rpo.UpdateAttachment(att)
|
||||
assert(t, err == nil, "UpdateAttachment error: %v", err)
|
||||
assert(t, att.ChannelID == 2, "Changes were not stored")
|
||||
}
|
||||
|
||||
{
|
||||
att, err = rpo.FindAttachmentByID(att.ID)
|
||||
assert(t, err == nil, "FindAttachmentByID error: %v", err)
|
||||
|
||||
@ -7,18 +7,30 @@ import (
|
||||
"github.com/crusttech/crust/sam/service"
|
||||
"github.com/crusttech/crust/sam/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
|
||||
type (
|
||||
Channel struct {
|
||||
svc service.ChannelService
|
||||
svc struct {
|
||||
ch service.ChannelService
|
||||
at channelAttachmentService
|
||||
}
|
||||
}
|
||||
|
||||
channelAttachmentService interface {
|
||||
Create(ctx context.Context, channelID uint64, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
|
||||
}
|
||||
)
|
||||
|
||||
func (Channel) New(channel service.ChannelService) *Channel {
|
||||
return &Channel{channel}
|
||||
func (Channel) New(chSvc service.ChannelService, atSvc service.AttachmentService) *Channel {
|
||||
ctrl := &Channel{}
|
||||
ctrl.svc.ch = chSvc
|
||||
ctrl.svc.at = atSvc
|
||||
|
||||
return ctrl
|
||||
}
|
||||
|
||||
func (ctrl *Channel) Create(ctx context.Context, r *request.ChannelCreate) (interface{}, error) {
|
||||
@ -27,7 +39,7 @@ func (ctrl *Channel) Create(ctx context.Context, r *request.ChannelCreate) (inte
|
||||
Topic: r.Topic,
|
||||
}
|
||||
|
||||
return ctrl.svc.Create(ctx, channel)
|
||||
return ctrl.svc.ch.Create(ctx, channel)
|
||||
}
|
||||
|
||||
func (ctrl *Channel) Edit(ctx context.Context, r *request.ChannelEdit) (interface{}, error) {
|
||||
@ -36,20 +48,20 @@ func (ctrl *Channel) Edit(ctx context.Context, r *request.ChannelEdit) (interfac
|
||||
Topic: r.Topic,
|
||||
}
|
||||
|
||||
return ctrl.svc.Update(ctx, channel)
|
||||
return ctrl.svc.ch.Update(ctx, channel)
|
||||
|
||||
}
|
||||
|
||||
func (ctrl *Channel) Delete(ctx context.Context, r *request.ChannelDelete) (interface{}, error) {
|
||||
return nil, ctrl.svc.Delete(ctx, r.ChannelID)
|
||||
return nil, ctrl.svc.ch.Delete(ctx, r.ChannelID)
|
||||
}
|
||||
|
||||
func (ctrl *Channel) Read(ctx context.Context, r *request.ChannelRead) (interface{}, error) {
|
||||
return ctrl.svc.FindByID(ctx, r.ChannelID)
|
||||
return ctrl.svc.ch.FindByID(ctx, r.ChannelID)
|
||||
}
|
||||
|
||||
func (ctrl *Channel) List(ctx context.Context, r *request.ChannelList) (interface{}, error) {
|
||||
return ctrl.svc.Find(ctx, &types.ChannelFilter{Query: r.Query})
|
||||
return ctrl.svc.ch.Find(ctx, &types.ChannelFilter{Query: r.Query})
|
||||
}
|
||||
|
||||
func (ctrl *Channel) Members(ctx context.Context, r *request.ChannelMembers) (interface{}, error) {
|
||||
@ -67,3 +79,19 @@ func (ctrl *Channel) Part(ctx context.Context, r *request.ChannelPart) (interfac
|
||||
func (ctrl *Channel) Invite(ctx context.Context, r *request.ChannelInvite) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ctrl *Channel) Attach(ctx context.Context, r *request.ChannelAttach) (interface{}, error) {
|
||||
file, err := r.Upload.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
return ctrl.svc.at.Create(
|
||||
ctx,
|
||||
r.ChannelID,
|
||||
r.Upload.Filename,
|
||||
r.Upload.Size,
|
||||
file)
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ type ChannelAPI interface {
|
||||
Join(context.Context, *request.ChannelJoin) (interface{}, error)
|
||||
Part(context.Context, *request.ChannelPart) (interface{}, error)
|
||||
Invite(context.Context, *request.ChannelInvite) (interface{}, error)
|
||||
Attach(context.Context, *request.ChannelAttach) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
@ -49,6 +50,7 @@ type Channel struct {
|
||||
Join func(http.ResponseWriter, *http.Request)
|
||||
Part func(http.ResponseWriter, *http.Request)
|
||||
Invite func(http.ResponseWriter, *http.Request)
|
||||
Attach func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewChannel(ch ChannelAPI) *Channel {
|
||||
@ -116,6 +118,13 @@ func NewChannel(ch ChannelAPI) *Channel {
|
||||
return ch.Invite(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Attach: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewChannelAttach()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ch.Attach(r.Context(), params)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,6 +141,7 @@ func (ch *Channel) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
|
||||
r.Post("/{channelID}/members/{userID}", ch.Join)
|
||||
r.Delete("/{channelID}/members/{userID}", ch.Part)
|
||||
r.Post("/{channelID}/invite", ch.Invite)
|
||||
r.Post("/{channelID}/attach", ch.Attach)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -31,7 +31,6 @@ type MessageAPI interface {
|
||||
History(context.Context, *request.MessageHistory) (interface{}, error)
|
||||
Edit(context.Context, *request.MessageEdit) (interface{}, error)
|
||||
Delete(context.Context, *request.MessageDelete) (interface{}, error)
|
||||
Attach(context.Context, *request.MessageAttach) (interface{}, error)
|
||||
Search(context.Context, *request.MessageSearch) (interface{}, error)
|
||||
Pin(context.Context, *request.MessagePin) (interface{}, error)
|
||||
Unpin(context.Context, *request.MessageUnpin) (interface{}, error)
|
||||
@ -47,7 +46,6 @@ type Message struct {
|
||||
History func(http.ResponseWriter, *http.Request)
|
||||
Edit func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Attach func(http.ResponseWriter, *http.Request)
|
||||
Search func(http.ResponseWriter, *http.Request)
|
||||
Pin func(http.ResponseWriter, *http.Request)
|
||||
Unpin func(http.ResponseWriter, *http.Request)
|
||||
@ -87,13 +85,6 @@ func NewMessage(mh MessageAPI) *Message {
|
||||
return mh.Delete(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Attach: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewMessageAttach()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return mh.Attach(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Search: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewMessageSearch()
|
||||
@ -154,7 +145,6 @@ 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.Put("/{messageID}/attach", mh.Attach)
|
||||
r.Get("/search", mh.Search)
|
||||
r.Post("/{messageID}/pin", mh.Pin)
|
||||
r.Delete("/{messageID}/pin", mh.Unpin)
|
||||
|
||||
@ -46,10 +46,6 @@ func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (inte
|
||||
return nil, ctrl.svc.Delete(ctx, r.MessageID)
|
||||
}
|
||||
|
||||
func (ctrl *Message) Attach(ctx context.Context, r *request.MessageAttach) (interface{}, error) {
|
||||
return ctrl.svc.Attach(ctx)
|
||||
}
|
||||
|
||||
func (ctrl *Message) Search(ctx context.Context, r *request.MessageSearch) (interface{}, error) {
|
||||
return ctrl.svc.Find(ctx, &types.MessageFilter{
|
||||
ChannelID: r.ChannelID,
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Auth login request parameters
|
||||
type AuthLogin struct {
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Channel list request parameters
|
||||
type ChannelList struct {
|
||||
@ -439,3 +441,49 @@ func (c *ChannelInvite) Fill(r *http.Request) error {
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewChannelInvite()
|
||||
|
||||
// Channel attach request parameters
|
||||
type ChannelAttach struct {
|
||||
ChannelID uint64
|
||||
Upload *multipart.FileHeader
|
||||
}
|
||||
|
||||
func NewChannelAttach() *ChannelAttach {
|
||||
return &ChannelAttach{}
|
||||
}
|
||||
|
||||
func (c *ChannelAttach) Fill(r *http.Request) error {
|
||||
var err error
|
||||
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(c)
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
c.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
|
||||
if _, c.Upload, err = r.FormFile("upload"); err != nil {
|
||||
return errors.Wrap(err, "error procesing uploaded file")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewChannelAttach()
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Message create request parameters
|
||||
type MessageCreate struct {
|
||||
@ -215,48 +217,6 @@ func (m *MessageDelete) Fill(r *http.Request) error {
|
||||
|
||||
var _ RequestFiller = NewMessageDelete()
|
||||
|
||||
// Message attach request parameters
|
||||
type MessageAttach struct {
|
||||
ChannelID uint64
|
||||
}
|
||||
|
||||
func NewMessageAttach() *MessageAttach {
|
||||
return &MessageAttach{}
|
||||
}
|
||||
|
||||
func (m *MessageAttach) 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.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewMessageAttach()
|
||||
|
||||
// Message search request parameters
|
||||
type MessageSearch struct {
|
||||
Query string
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Organisation list request parameters
|
||||
type OrganisationList struct {
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Team list request parameters
|
||||
type TeamList struct {
|
||||
|
||||
@ -21,12 +21,14 @@ import (
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// User search request parameters
|
||||
type UserSearch struct {
|
||||
|
||||
@ -5,13 +5,21 @@ import (
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
"github.com/crusttech/crust/sam/rest/handlers"
|
||||
"github.com/crusttech/crust/sam/service"
|
||||
"github.com/crusttech/crust/store"
|
||||
"github.com/go-chi/chi"
|
||||
"log"
|
||||
)
|
||||
|
||||
func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
|
||||
// Initialize services
|
||||
fs, err := store.New("/tmp/crust/messages")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize stor: %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
channelSvc = service.Channel()
|
||||
attachmentSvc = service.Attachment(fs)
|
||||
messageSvc = service.Message()
|
||||
organisationSvc = service.Organisation()
|
||||
teamSvc = service.Team()
|
||||
@ -19,7 +27,7 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
|
||||
)
|
||||
|
||||
var (
|
||||
channel = Channel{}.New(channelSvc)
|
||||
channel = Channel{}.New(channelSvc, attachmentSvc)
|
||||
message = Message{}.New(messageSvc)
|
||||
organisation = Organisation{}.New(organisationSvc)
|
||||
team = Team{}.New(teamSvc)
|
||||
|
||||
115
sam/service/attachment.go
Normal file
115
sam/service/attachment.go
Normal file
@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/crusttech/crust/auth"
|
||||
"github.com/crusttech/crust/sam/repository"
|
||||
"github.com/crusttech/crust/sam/types"
|
||||
"github.com/titpetric/factory"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
attachment struct {
|
||||
rpo attachmentRepository
|
||||
sto attachmentStore
|
||||
}
|
||||
|
||||
AttachmentService interface {
|
||||
Create(ctx context.Context, channelId uint64, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
|
||||
}
|
||||
|
||||
attachmentRepository interface {
|
||||
repository.Transactionable
|
||||
repository.Attachment
|
||||
}
|
||||
|
||||
attachmentStore interface {
|
||||
Original(id uint64, ext string) string
|
||||
Preview(id uint64, ext string) string
|
||||
|
||||
Save(filename string, contents io.Reader) error
|
||||
Remove(filename string) error
|
||||
Open(filename string) (io.Reader, error)
|
||||
}
|
||||
)
|
||||
|
||||
func Attachment(store attachmentStore) *attachment {
|
||||
svc := &attachment{}
|
||||
svc.rpo = repository.New()
|
||||
svc.sto = store
|
||||
// @todo bind file store
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (svc attachment) Create(ctx context.Context, channelId uint64, name string, size int64, fh io.ReadSeeker) (att *types.Attachment, err error) {
|
||||
var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// @todo verify if current user can access this channel
|
||||
// @todo verify if current user can upload to this channel
|
||||
|
||||
att = &types.Attachment{
|
||||
ID: factory.Sonyflake.NextID(),
|
||||
UserID: currentUserID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Mimetype: "application/octet-stream",
|
||||
Size: size,
|
||||
}
|
||||
|
||||
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
|
||||
var ext = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
|
||||
|
||||
// @todo extract mimetype and update att.Mimetype
|
||||
|
||||
if svc.sto != nil {
|
||||
att.Url = svc.sto.Original(att.ID, ext)
|
||||
if err = svc.sto.Save(att.Url, fh); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to make preview
|
||||
svc.makePreview(att, fh)
|
||||
}
|
||||
|
||||
return att, svc.rpo.BeginWith(ctx, func(r repository.Interfaces) (err error) {
|
||||
|
||||
if att, err = r.CreateAttachment(att); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg := &types.Message{
|
||||
Message: name,
|
||||
Type: "attachment",
|
||||
ChannelID: channelId,
|
||||
}
|
||||
|
||||
// Create the first message, doing this directly with repository to circumvent
|
||||
// message service constraints
|
||||
if msg, err = r.CreateMessage(msg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = r.BindAttachment(att.ID, msg.ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (svc attachment) makePreview(att *types.Attachment, originalFh io.ReadSeeker) (err error) {
|
||||
if true {
|
||||
return
|
||||
}
|
||||
|
||||
// Can and how we make a preview of this attachment?
|
||||
var ext = "jpg"
|
||||
att.PreviewUrl = svc.sto.Preview(att.ID, ext)
|
||||
|
||||
return svc.sto.Save(att.PreviewUrl, originalFh)
|
||||
}
|
||||
|
||||
var _ AttachmentService = &attachment{}
|
||||
@ -28,9 +28,6 @@ type (
|
||||
Flag(ctx context.Context, messageID uint64) error
|
||||
Unflag(ctx context.Context, messageID uint64) error
|
||||
|
||||
Attach(ctx context.Context) (*types.Attachment, error)
|
||||
Detach(ctx context.Context, messageID uint64) error
|
||||
|
||||
Direct(ctx context.Context, recipientID uint64, in *types.Message) (out *types.Message, err error)
|
||||
|
||||
deleter
|
||||
@ -226,28 +223,4 @@ func (svc message) Unflag(ctx context.Context, messageID uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc message) Attach(ctx context.Context) (*types.Attachment, error) {
|
||||
// @todo define func signature
|
||||
|
||||
// @todo get user from context
|
||||
var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// @todo verify if current user can access & write to this channel
|
||||
_ = currentUserID
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (svc message) Detach(ctx context.Context, attachmentID uint64) error {
|
||||
// @todo get user from context
|
||||
var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// @todo verify if current user can access & write to this channel
|
||||
_ = currentUserID
|
||||
|
||||
// @todo verify if current user can remove this attachment
|
||||
|
||||
return svc.rpo.DeleteAttachmentByID(attachmentID)
|
||||
}
|
||||
|
||||
var _ MessageService = &message{}
|
||||
|
||||
@ -1,24 +1,20 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Attachment struct {
|
||||
ID uint64 `db:"id"`
|
||||
UserID uint64 `db:"rel_user"`
|
||||
MessageID uint64 `db:"rel_message"`
|
||||
ChannelID uint64 `db:"rel_channel"`
|
||||
Attachment json.RawMessage `db:"attachment"`
|
||||
Url string `db:"url"`
|
||||
PreviewUrl string `db:"preview_url"`
|
||||
Size uint `db:"size"`
|
||||
Mimetype string `db:"mimetype"`
|
||||
Name string `db:"name"`
|
||||
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 `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"`
|
||||
Size int64 `db:"size" json:"size,omitempty"`
|
||||
Mimetype string `db:"mimetype" json:"mimetype,omitempty"`
|
||||
Name string `db:"name" json:"name,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"`
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
|
||||
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user