From 18d7f4c3bfaad81f8f1b5953d6ccae46d071fda7 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Thu, 19 Jul 2018 18:50:48 +0200 Subject: [PATCH] SAM messaging, basics --- .../schema/mysql/20180704080000.base.up.sql | 94 +++-- sam/docs/src/spec.json | 43 +- sam/docs/src/spec/message.json | 127 ++++-- sam/repository/attachment.go | 77 ++++ sam/repository/generics.go | 4 +- sam/repository/message.go | 82 ++++ sam/repository/reaction.go | 72 ++++ sam/rest/auth.go | 6 +- sam/rest/event.go | 43 ++ sam/rest/message.go | 8 +- sam/rest/server/event.go | 51 +++ sam/rest/server/event_handlers.go | 51 +++ sam/rest/server/event_requests.go | 225 ++++++++++ sam/service/channel.go | 6 +- sam/service/mesage.go | 151 +++++++ sam/types/message.go | 384 +++++++++++++----- sam/types/message_filter.go | 8 + sam/types/message_type.go | 13 - 18 files changed, 1249 insertions(+), 196 deletions(-) create mode 100644 sam/repository/attachment.go create mode 100644 sam/repository/message.go create mode 100644 sam/repository/reaction.go create mode 100644 sam/rest/event.go create mode 100644 sam/rest/server/event.go create mode 100644 sam/rest/server/event_handlers.go create mode 100644 sam/rest/server/event_requests.go create mode 100644 sam/service/mesage.go create mode 100644 sam/types/message_filter.go delete mode 100644 sam/types/message_type.go diff --git a/sam/db/schema/mysql/20180704080000.base.up.sql b/sam/db/schema/mysql/20180704080000.base.up.sql index ac638b985..e651e4017 100644 --- a/sam/db/schema/mysql/20180704080000.base.up.sql +++ b/sam/db/schema/mysql/20180704080000.base.up.sql @@ -72,51 +72,6 @@ CREATE TABLE channel_members ( PRIMARY KEY (rel_channel, rel_user) ); -CREATE TABLE messages ( - id BIGINT UNSIGNED NOT NULL, - - -- basic set: - -- NULL for common text messages - -- image/*; disposition=inline for inline images (does not follow stdandard http headers, but - -- crust/system body holds one of defined system messages (user joining, parting...) - -- crust/reaction body holds reaction to a message - -- crust/preview body holds preview of the message it relates to in form of metadata (no binary blobs!) - -- crust/url body holds only URL - -- crust/pin message it relates to is pinned, body holds pin details - -- crust/flag message it relates to is flaged, body holds flag information - -- */* body holds reference to the uploaded file and its metadata - -- - mimetype VARCHAR(255) NULL, - - -- null body only valid when rel_message is set => message removal - body TEXT NULL, - - -- the contributor - rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id), - - -- message's channel - rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id), - - -- replies, edits, reactions, flags, attachments... - rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id), - - PRIMARY KEY (id) -); - - --- temp copy of messages (when they are pushed to the primary organisation, row gets removed) -CREATE TABLE messages_queue ( - id BIGINT UNSIGNED NOT NULL, - mimetype VARCHAR(255) NULL, - body TEXT NULL, - rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id), - rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id), - rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id), - - PRIMARY KEY (id) -); - - CREATE TABLE channel_views ( rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id), rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id), @@ -129,3 +84,52 @@ CREATE TABLE channel_views ( PRIMARY KEY (rel_user, rel_channel) ); + +CREATE TABLE channel_pins ( + rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id), + rel_message BIGINT UNSIGNED NULL REFERENCES messages(id), + rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id), + + PRIMARY KEY (rel_channel, rel_message) +); + +CREATE TABLE messages ( + id BIGINT UNSIGNED NOT NULL, + type TEXT, + message TEXT NOT NULL, + meta JSON, + rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id), + rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id), + reply_to BIGINT UNSIGNED NULL REFERENCES messages(id), + + updated_at DATETIME NULL, + deleted_at DATETIME NULL, + + PRIMARY KEY (id) +); + +CREATE TABLE reactions ( + 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), + reaction TEXT NOT NULL, + + PRIMARY KEY (id) +); + +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 NOT NULL, + preview_url TEXT NOT NULL, + size INT UNSIGNED NOT NULL, + mimetype TEXT NOT NULL, + name TEXT NOT NULL, + attachment JSON NOT NULL, + deleted_at DATETIME NULL, + + PRIMARY KEY (id) +); diff --git a/sam/docs/src/spec.json b/sam/docs/src/spec.json index 19856ec49..4d7776f75 100644 --- a/sam/docs/src/spec.json +++ b/sam/docs/src/spec.json @@ -303,16 +303,41 @@ "struct": [ { "name": "Message", + "imports": ["time"], "fields": [ - { "name": "Service", "type": "string" }, - { "name": "Channel", "type": "string" }, - { "name": "UserName", "type": "string" }, - { "name": "UserID", "type": "uint64" }, - { "name": "User", "type": "*User", "complex": true }, - { "name": "UserAvatar", "type": "string" }, - { "name": "Message", "type": "string" }, - { "name": "MessageID", "type": "string" }, - { "name": "Type", "type": "MessageType" } + { "name": "ID", "type": "uint64" }, + { "name": "Type", "type": "string" }, + { "name": "Message", "type": "string" }, + { "name": "UserId", "type": "uint64", "dbname": "rel_user" }, + { "name": "ChannelId", "type": "uint64", "dbname": "rel_channel"}, + { "name": "ReplyTo", "type": "uint64", "dbname": "rel_message" }, + { "name": "UpdatedAt", "type": "*time.Time", "tag": "json:\",omitempty\"", "complex": true }, + { "name": "DeletedAt", "type": "*time.Time", "tag": "json:\",omitempty\"", "complex": true } + + ] + }, + { + "name": "Reaction", + "imports": ["time"], + "fields": [ + { "name": "ID", "type": "uint64" }, + { "name": "UserId", "type": "uint64", "dbname": "rel_user" }, + { "name": "MessageId", "type": "uint64", "dbname": "rel_message"}, + { "name": "ChannelId", "type": "uint64", "dbname": "rel_channel"}, + { "name": "Reaction", "type": "string" }, + { "name": "DeletedAt", "type": "*time.Time", "tag": "json:\",omitempty\"", "complex": true } + ] + }, + { + "name": "Attachment", + "imports": ["time", "encoding/json"], + "fields": [ + { "name": "ID", "type": "uint64" }, + { "name": "UserId", "type": "uint64", "dbname": "rel_user" }, + { "name": "MessageId", "type": "uint64", "dbname": "rel_message"}, + { "name": "ChannelId", "type": "uint64", "dbname": "rel_channel"}, + { "name": "Attachment", "type": "json.RawMessage" , "complex": true}, + { "name": "DeletedAt", "type": "*time.Time", "tag": "json:\",omitempty\"", "complex": true } ] } ], diff --git a/sam/docs/src/spec/message.json b/sam/docs/src/spec/message.json index bb6b928aa..ec14369f1 100644 --- a/sam/docs/src/spec/message.json +++ b/sam/docs/src/spec/message.json @@ -6,28 +6,11 @@ { "fields": [ { - "name": "Service", - "type": "string" - }, - { - "name": "Channel", - "type": "string" - }, - { - "name": "UserName", - "type": "string" - }, - { - "name": "UserID", + "name": "ID", "type": "uint64" }, { - "complex": true, - "name": "User", - "type": "*User" - }, - { - "name": "UserAvatar", + "name": "Type", "type": "string" }, { @@ -35,15 +18,113 @@ "type": "string" }, { - "name": "MessageID", + "dbname": "rel_user", + "name": "UserId", + "type": "uint64" + }, + { + "dbname": "rel_channel", + "name": "ChannelId", + "type": "uint64" + }, + { + "dbname": "rel_message", + "name": "ReplyTo", + "type": "uint64" + }, + { + "complex": true, + "name": "UpdatedAt", + "tag": "json:\",omitempty\"", + "type": "*time.Time" + }, + { + "complex": true, + "name": "DeletedAt", + "tag": "json:\",omitempty\"", + "type": "*time.Time" + } + ], + "imports": [ + "time" + ], + "name": "Message" + }, + { + "fields": [ + { + "name": "ID", + "type": "uint64" + }, + { + "dbname": "rel_user", + "name": "UserId", + "type": "uint64" + }, + { + "dbname": "rel_message", + "name": "MessageId", + "type": "uint64" + }, + { + "dbname": "rel_channel", + "name": "ChannelId", + "type": "uint64" + }, + { + "name": "Reaction", "type": "string" }, { - "name": "Type", - "type": "MessageType" + "complex": true, + "name": "DeletedAt", + "tag": "json:\",omitempty\"", + "type": "*time.Time" } ], - "name": "Message" + "imports": [ + "time" + ], + "name": "Reaction" + }, + { + "fields": [ + { + "name": "ID", + "type": "uint64" + }, + { + "dbname": "rel_user", + "name": "UserId", + "type": "uint64" + }, + { + "dbname": "rel_message", + "name": "MessageId", + "type": "uint64" + }, + { + "dbname": "rel_channel", + "name": "ChannelId", + "type": "uint64" + }, + { + "complex": true, + "name": "Attachment", + "type": "json.RawMessage" + }, + { + "complex": true, + "name": "DeletedAt", + "tag": "json:\",omitempty\"", + "type": "*time.Time" + } + ], + "imports": [ + "time", + "encoding/json" + ], + "name": "Attachment" } ], "Protocol": "", diff --git a/sam/repository/attachment.go b/sam/repository/attachment.go new file mode 100644 index 000000000..120809d43 --- /dev/null +++ b/sam/repository/attachment.go @@ -0,0 +1,77 @@ +package repository + +import ( + "context" + "github.com/crusttech/crust/sam/types" + "github.com/titpetric/factory" +) + +const ( + sqlAttachmentScope = "deleted_at IS NULL AND archived_at IS NULL" + + ErrAttachmentNotFound = repositoryError("AttachmentNotFound") +) + +type ( + attachment struct{} +) + +func Attachment() attachment { + return attachment{} +} + +func (r attachment) FindById(ctx context.Context, id uint64) (*types.Attachment, error) { + db := factory.Database.MustGet() + + mod := &types.Attachment{} + if err := db.GetContext(ctx, mod, "SELECT * FROM attachments WHERE id = ? AND "+sqlAttachmentScope, id); err != nil { + return nil, ErrDatabaseError + } else if mod.ID == 0 { + return nil, ErrAttachmentNotFound + } else { + return mod, nil + } +} + +func (r attachment) FindByRange(ctx context.Context, channelId, fromAttachmentId, toAttachmentId uint64) ([]*types.Attachment, error) { + db := factory.Database.MustGet() + + sql := ` + SELECT * + FROM attachments + WHERE rel_attachment BETWEEN ? AND ? + AND rel_channel = ? + AND deleted_at IS NULL` + + rval := make([]*types.Attachment, 0) + if err := db.Select(&rval, sql, fromAttachmentId, toAttachmentId, channelId); err != nil { + return nil, ErrDatabaseError + } + + return rval, nil +} + +func (r attachment) Create(ctx context.Context, mod *types.Attachment) (*types.Attachment, error) { + db := factory.Database.MustGet() + + mod.SetID(factory.Sonyflake.NextID()) + if err := db.Insert("attachments", mod); err != nil { + return nil, ErrDatabaseError + } else { + return mod, nil + } +} + +func (r attachment) Update(ctx context.Context, mod *types.Attachment) (*types.Attachment, error) { + db := factory.Database.MustGet() + + if err := db.Replace("attachments", mod); err != nil { + return nil, ErrDatabaseError + } else { + return mod, nil + } +} + +func (r attachment) Delete(ctx context.Context, id uint64) error { + return simpleDelete(ctx, "attachments", id) +} diff --git a/sam/repository/generics.go b/sam/repository/generics.go index e72f364af..3ac7f539f 100644 --- a/sam/repository/generics.go +++ b/sam/repository/generics.go @@ -11,7 +11,7 @@ func simpleUpdate(ctx context.Context, tableName, columnName string, value inter sql := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", tableName, columnName) - if _, err := db.Exec(sql, value, id); err != nil { + if _, err := db.ExecContext(ctx, sql, value, id); err != nil { return ErrDatabaseError } else { return nil @@ -23,7 +23,7 @@ func simpleDelete(ctx context.Context, tableName string, id uint64) error { sql := fmt.Sprintf("DELETE %s WHERE id = ?", tableName) - if _, err := db.Exec(sql, id); err != nil { + if _, err := db.ExecContext(ctx, sql, id); err != nil { return ErrDatabaseError } else { return nil diff --git a/sam/repository/message.go b/sam/repository/message.go new file mode 100644 index 000000000..2b3a6b4c9 --- /dev/null +++ b/sam/repository/message.go @@ -0,0 +1,82 @@ +package repository + +import ( + "context" + "github.com/crusttech/crust/sam/types" + "github.com/titpetric/factory" +) + +const ( + sqlMessageScope = "deleted_at IS NULL" + + ErrMessageNotFound = repositoryError("MessageNotFound") +) + +type ( + message struct{} +) + +func Message() message { + return message{} +} + +func (r message) FindById(ctx context.Context, id uint64) (*types.Message, error) { + db := factory.Database.MustGet() + + mod := &types.Message{} + if err := db.GetContext(ctx, mod, "SELECT * FROM messages WHERE id = ? AND "+sqlMessageScope, id); err != nil { + return nil, ErrDatabaseError + } else if mod.ID == 0 { + return nil, ErrMessageNotFound + } else { + return mod, nil + } +} + +func (r message) Find(ctx context.Context, filter *types.MessageFilter) ([]*types.Message, error) { + db := factory.Database.MustGet() + + var params = make([]interface{}, 0) + sql := "SELECT * FROM messages WHERE " + sqlMessageScope + + if filter != nil { + if filter.Query != "" { + sql += "AND name LIKE ?" + params = append(params, filter.Query+"%") + } + } + + sql += " ORDER BY name ASC" + + rval := make([]*types.Message, 0) + if err := db.SelectContext(ctx, &rval, sql, params...); err != nil { + return nil, ErrDatabaseError + } else { + return rval, nil + } +} + +func (r message) Create(ctx context.Context, mod *types.Message) (*types.Message, error) { + db := factory.Database.MustGet() + + mod.SetID(factory.Sonyflake.NextID()) + if err := db.Insert("messages", mod); err != nil { + return nil, ErrDatabaseError + } else { + return mod, nil + } +} + +func (r message) Update(ctx context.Context, mod *types.Message) (*types.Message, error) { + db := factory.Database.MustGet() + + if err := db.Replace("messages", mod); err != nil { + return nil, ErrDatabaseError + } else { + return mod, nil + } +} + +func (r message) Delete(ctx context.Context, id uint64) error { + return simpleDelete(ctx, "messages", id) +} diff --git a/sam/repository/reaction.go b/sam/repository/reaction.go new file mode 100644 index 000000000..86be9988b --- /dev/null +++ b/sam/repository/reaction.go @@ -0,0 +1,72 @@ +package repository + +import ( + "context" + "github.com/crusttech/crust/sam/types" + "github.com/titpetric/factory" +) + +const ( + sqlReactionScope = "deleted_at IS NULL AND archived_at IS NULL" + + ErrReactionNotFound = repositoryError("ReactionNotFound") +) + +type ( + reaction struct{} +) + +func Reaction() reaction { + return reaction{} +} + +func (r reaction) FindById(ctx context.Context, id uint64) (*types.Reaction, error) { + db := factory.Database.MustGet() + + mod := &types.Reaction{} + if err := db.GetContext(ctx, mod, "SELECT * FROM reactions WHERE id = ? AND "+sqlReactionScope, id); err != nil { + return nil, ErrDatabaseError + } else if mod.ID == 0 { + return nil, ErrReactionNotFound + } else { + return mod, nil + } +} + +func (r reaction) FindByRange(ctx context.Context, channelId, fromReactionId, toReactionId uint64) ([]*types.Reaction, error) { + db := factory.Database.MustGet() + + sql := ` + SELECT * + FROM reactions + WHERE rel_reaction BETWEEN ? AND ? + AND rel_channel = ?` + + rval := make([]*types.Reaction, 0) + if err := db.Select(&rval, sql, fromReactionId, toReactionId, channelId); err != nil { + return nil, ErrDatabaseError + } + + return rval, nil +} + +func (r reaction) Create(ctx context.Context, mod *types.Reaction) (*types.Reaction, error) { + db := factory.Database.MustGet() + + mod.SetID(factory.Sonyflake.NextID()) + if err := db.Insert("reactions", mod); err != nil { + return nil, ErrDatabaseError + } else { + return mod, nil + } +} + +func (r reaction) Delete(ctx context.Context, id uint64) error { + db := factory.Database.MustGet() + + if _, err := db.ExecContext(ctx, "DELETE FROM reactions WHERE id = ?", id); err != nil { + return ErrDatabaseError + } else { + return nil + } +} diff --git a/sam/rest/auth.go b/sam/rest/auth.go index 0380e9220..acdc2e63d 100644 --- a/sam/rest/auth.go +++ b/sam/rest/auth.go @@ -18,11 +18,15 @@ func (*Team) Authenticator() func(http.Handler) http.Handler { return pass } +func (*Message) Authenticator() func(http.Handler) http.Handler { + return pass +} + func (*Channel) Authenticator() func(http.Handler) http.Handler { return pass } -func (*Message) Authenticator() func(http.Handler) http.Handler { +func (*Event) Authenticator() func(http.Handler) http.Handler { return pass } diff --git a/sam/rest/event.go b/sam/rest/event.go new file mode 100644 index 000000000..1761c6e33 --- /dev/null +++ b/sam/rest/event.go @@ -0,0 +1,43 @@ +package rest + +import ( + "context" + "github.com/crusttech/crust/sam/rest/server" + "github.com/pkg/errors" +) + +var _ = errors.Wrap + +type Event struct{} + +func (Event) New() *Event { + return &Event{} +} + +func (ctrl *Event) Edit(ctx context.Context, r *server.EventEditRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} + +func (ctrl *Event) Attach(ctx context.Context, r *server.EventAttachRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} + +func (ctrl *Event) Remove(ctx context.Context, r *server.EventRemoveRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} + +func (ctrl *Event) Read(ctx context.Context, r *server.EventReadRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} + +func (ctrl *Event) Search(ctx context.Context, r *server.EventSearchRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} + +func (ctrl *Event) Pin(ctx context.Context, r *server.EventPinRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} + +func (ctrl *Event) Flag(ctx context.Context, r *server.EventFlagRequest) (interface{}, error) { + return nil, errors.New("Not implemented: Event.....") +} diff --git a/sam/rest/message.go b/sam/rest/message.go index c99ea7795..7915babf0 100644 --- a/sam/rest/message.go +++ b/sam/rest/message.go @@ -2,20 +2,18 @@ package rest import ( "context" - "github.com/pkg/errors" - "github.com/crusttech/crust/sam/rest/server" - _ "github.com/crusttech/crust/sam/types" + "github.com/pkg/errors" ) +var _ = errors.Wrap + type Message struct{} func (Message) New() *Message { return &Message{} } -var _ = errors.Wrap - func (*Message) Edit(ctx context.Context, r *server.MessageEditRequest) (interface{}, error) { return nil, errors.New("Not implemented: Message.edit") } diff --git a/sam/rest/server/event.go b/sam/rest/server/event.go new file mode 100644 index 000000000..8c6392d3e --- /dev/null +++ b/sam/rest/server/event.go @@ -0,0 +1,51 @@ +package server + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `event.go`, `event.util.go` or `event_test.go` to + implement your API calls, helper functions and tests. The file `event.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + "net/http" +) + +// HTTP handlers are a superset of internal APIs +type EventHandlers struct { + Event EventAPI +} + +// Internal API interface +type EventAPI interface { + Edit(context.Context, *EventEditRequest) (interface{}, error) + Attach(context.Context, *EventAttachRequest) (interface{}, error) + Remove(context.Context, *EventRemoveRequest) (interface{}, error) + Read(context.Context, *EventReadRequest) (interface{}, error) + Search(context.Context, *EventSearchRequest) (interface{}, error) + Pin(context.Context, *EventPinRequest) (interface{}, error) + Flag(context.Context, *EventFlagRequest) (interface{}, error) + + // Authenticate API requests + Authenticator() func(http.Handler) http.Handler +} + +// HTTP API interface +type EventHandlersAPI interface { + Edit(http.ResponseWriter, *http.Request) + Attach(http.ResponseWriter, *http.Request) + Remove(http.ResponseWriter, *http.Request) + Read(http.ResponseWriter, *http.Request) + Search(http.ResponseWriter, *http.Request) + Pin(http.ResponseWriter, *http.Request) + Flag(http.ResponseWriter, *http.Request) +} diff --git a/sam/rest/server/event_handlers.go b/sam/rest/server/event_handlers.go new file mode 100644 index 000000000..56c93190d --- /dev/null +++ b/sam/rest/server/event_handlers.go @@ -0,0 +1,51 @@ +package server + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `event.go`, `event.util.go` or `event_test.go` to + implement your API calls, helper functions and tests. The file `event.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "net/http" + + "github.com/titpetric/factory/resputil" +) + +func (eh *EventHandlers) Edit(w http.ResponseWriter, r *http.Request) { + params := EventEditRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Edit(r.Context(), params) }) +} +func (eh *EventHandlers) Attach(w http.ResponseWriter, r *http.Request) { + params := EventAttachRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Attach(r.Context(), params) }) +} +func (eh *EventHandlers) Remove(w http.ResponseWriter, r *http.Request) { + params := EventRemoveRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Remove(r.Context(), params) }) +} +func (eh *EventHandlers) Read(w http.ResponseWriter, r *http.Request) { + params := EventReadRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Read(r.Context(), params) }) +} +func (eh *EventHandlers) Search(w http.ResponseWriter, r *http.Request) { + params := EventSearchRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Search(r.Context(), params) }) +} +func (eh *EventHandlers) Pin(w http.ResponseWriter, r *http.Request) { + params := EventPinRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Pin(r.Context(), params) }) +} +func (eh *EventHandlers) Flag(w http.ResponseWriter, r *http.Request) { + params := EventFlagRequest{}.new() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { return eh.Event.Flag(r.Context(), params) }) +} diff --git a/sam/rest/server/event_requests.go b/sam/rest/server/event_requests.go new file mode 100644 index 000000000..d47ee965a --- /dev/null +++ b/sam/rest/server/event_requests.go @@ -0,0 +1,225 @@ +package server + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `event.go`, `event.util.go` or `event_test.go` to + implement your API calls, helper functions and tests. The file `event.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "github.com/go-chi/chi" + "net/http" +) + +var _ = chi.URLParam + +// Event edit request parameters +type EventEditRequest struct { + ID uint64 + Channel_id uint64 + Contents string +} + +func (EventEditRequest) new() *EventEditRequest { + return &EventEditRequest{} +} + +func (e *EventEditRequest) Fill(r *http.Request) error { + 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]) + } + + e.ID = parseUInt64(post["id"]) + + e.Channel_id = parseUInt64(post["channel_id"]) + + e.Contents = post["contents"] + return nil +} + +var _ RequestFiller = EventEditRequest{}.new() + +// Event attach request parameters +type EventAttachRequest struct { +} + +func (EventAttachRequest) new() *EventAttachRequest { + return &EventAttachRequest{} +} + +func (e *EventAttachRequest) Fill(r *http.Request) error { + 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]) + } + return nil +} + +var _ RequestFiller = EventAttachRequest{}.new() + +// Event remove request parameters +type EventRemoveRequest struct { + ID uint64 +} + +func (EventRemoveRequest) new() *EventRemoveRequest { + return &EventRemoveRequest{} +} + +func (e *EventRemoveRequest) Fill(r *http.Request) error { + 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]) + } + + e.ID = parseUInt64(get["id"]) + return nil +} + +var _ RequestFiller = EventRemoveRequest{}.new() + +// Event read request parameters +type EventReadRequest struct { + Channel_id uint64 +} + +func (EventReadRequest) new() *EventReadRequest { + return &EventReadRequest{} +} + +func (e *EventReadRequest) Fill(r *http.Request) error { + 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]) + } + + e.Channel_id = parseUInt64(post["channel_id"]) + return nil +} + +var _ RequestFiller = EventReadRequest{}.new() + +// Event search request parameters +type EventSearchRequest struct { + Query string + Message_type string +} + +func (EventSearchRequest) new() *EventSearchRequest { + return &EventSearchRequest{} +} + +func (e *EventSearchRequest) Fill(r *http.Request) error { + 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]) + } + + e.Query = get["query"] + + e.Message_type = get["message_type"] + return nil +} + +var _ RequestFiller = EventSearchRequest{}.new() + +// Event pin request parameters +type EventPinRequest struct { + ID uint64 +} + +func (EventPinRequest) new() *EventPinRequest { + return &EventPinRequest{} +} + +func (e *EventPinRequest) Fill(r *http.Request) error { + 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]) + } + + e.ID = parseUInt64(post["id"]) + return nil +} + +var _ RequestFiller = EventPinRequest{}.new() + +// Event flag request parameters +type EventFlagRequest struct { + ID uint64 +} + +func (EventFlagRequest) new() *EventFlagRequest { + return &EventFlagRequest{} +} + +func (e *EventFlagRequest) Fill(r *http.Request) error { + 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]) + } + + e.ID = parseUInt64(post["id"]) + return nil +} + +var _ RequestFiller = EventFlagRequest{}.new() diff --git a/sam/service/channel.go b/sam/service/channel.go index 6f7979e35..fc7632539 100644 --- a/sam/service/channel.go +++ b/sam/service/channel.go @@ -39,7 +39,7 @@ func (svc channel) Find(ctx context.Context, filter *types.ChannelFilter) ([]*ty } func (svc channel) Create(ctx context.Context, mod *types.Channel) (*types.Channel, error) { - // @todo: topic message/log entry + // @todo: topic channelEvent/log entry // @todo: channel name cmessage/log entry // @todo: permission check if channel can add channel @@ -47,8 +47,8 @@ func (svc channel) Create(ctx context.Context, mod *types.Channel) (*types.Chann } func (svc channel) Update(ctx context.Context, mod *types.Channel) (*types.Channel, error) { - // @todo: topic change message/log entry - // @todo: channel name change message/log entry + // @todo: topic change channelEvent/log entry + // @todo: channel name change channelEvent/log entry // @todo: permission check if current user can edit channel // @todo: make sure archived & deleted entries can not be edited // @todo: handle channel movinga diff --git a/sam/service/mesage.go b/sam/service/mesage.go new file mode 100644 index 000000000..409e0b08f --- /dev/null +++ b/sam/service/mesage.go @@ -0,0 +1,151 @@ +package service + +import ( + "context" + "github.com/crusttech/crust/sam/repository" + "github.com/crusttech/crust/sam/types" +) + +type ( + message struct { + repository struct { + message messageRepository + reaction messageReactionRepository + attachment messageAttachmentRepository + } + } + + messageRepository interface { + FindById(context.Context, uint64) (*types.Message, error) + Find(context.Context, *types.MessageFilter) ([]*types.Message, error) + + Create(context.Context, *types.Message) (*types.Message, error) + Update(context.Context, *types.Message) (*types.Message, error) + + deleter + } + + messageReactionRepository interface { + FindById(context.Context, uint64) (*types.Reaction, error) + Create(context.Context, *types.Reaction) (*types.Reaction, error) + Delete(context.Context, uint64) error + } + + messageAttachmentRepository interface { + FindById(context.Context, uint64) (*types.Attachment, error) + Create(context.Context, *types.Attachment) (*types.Attachment, error) + Delete(context.Context, uint64) error + } +) + +func Message() *message { + m := &message{} + m.repository.message = repository.Message() + m.repository.reaction = repository.Reaction() + m.repository.attachment = repository.Attachment() + + return m +} + +func (svc message) Find(ctx context.Context, filter *types.MessageFilter) ([]*types.Message, error) { + // @todo get user from context + var currentUserId uint64 = 0 + + // @todo verify if current user can access & write to this channel + _ = currentUserId + + return svc.repository.message.Find(ctx, filter) +} + +func (svc message) Create(ctx context.Context, mod *types.Message) (*types.Message, error) { + // @todo get user from context + var currentUserId uint64 = 0 + + // @todo verify if current user can access & write to this channel + _ = currentUserId + + return svc.repository.message.Create(ctx, mod) +} + +func (svc message) Update(ctx context.Context, mod *types.Message) (*types.Message, error) { + // @todo get user from context + var currentUserId uint64 = 0 + + // @todo verify if current user can access & write to this channel + _ = currentUserId + + // @todo load current message + + // @todo verify ownership + + return svc.repository.message.Update(ctx, mod) +} + +func (svc message) Delete(ctx context.Context, id uint64) error { + // @todo get user from context + var currentUserId uint64 = 0 + + // @todo verify if current user can access & write to this channel + _ = currentUserId + + // @todo load current message + + // @todo verify ownership + + return svc.repository.message.Delete(ctx, id) +} + +func (svc message) React(ctx context.Context, messageId uint64, reaction string) (*types.Reaction, error) { + // @todo get user from context + var currentUserId uint64 = 0 + + // @todo verify if current user can access & write to this channel + var m *types.Message + + // @todo validate reaction + + r := &types.Reaction{ + UserId: currentUserId, + MessageId: messageId, + ChannelId: m.ChannelId, + Reaction: reaction, + } + + return svc.repository.reaction.Create(ctx, r) +} + +func (svc message) Unreact(ctx context.Context, reactionId uint64) error { + // @todo get user from context + var currentUserId uint64 = 0 + + // @todo verify if current user can access & write to this channel + _ = currentUserId + + // @todo load reaction and verify ownership + + return svc.repository.reaction.Delete(ctx, reactionId) +} + +func (svc message) Attach(ctx context.Context) (*types.Attachment, error) { + // @todo define func signature + + // @todo get user from context + var currentUserId uint64 = 0 + + // @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 = 0 + + // @todo verify if current user can access & write to this channel + _ = currentUserId + + // @todo verify if current user can remove this attachment + + return svc.repository.attachment.Delete(ctx, attachmentId) +} diff --git a/sam/types/message.go b/sam/types/message.go index eb75d1f3c..8ad731228 100644 --- a/sam/types/message.go +++ b/sam/types/message.go @@ -15,18 +15,46 @@ package types is only generated the first time, and will not be overwritten if it exists. */ +import ( + "encoding/json" + "time" +) + type ( // Messages - Message struct { - Service string `db:"service"` - Channel string `db:"channel"` - UserName string `db:"user_name"` - UserID uint64 `db:"user_id"` - User *User `db:"user"` - UserAvatar string `db:"user_avatar"` - Message string `db:"message"` - MessageID string `db:"message_id"` - Type MessageType `db:"type"` + ID uint64 `db:"id"` + Type string `db:"type"` + Message string `db:"message"` + UserId uint64 `db:"rel_user"` + ChannelId uint64 `db:"rel_channel"` + ReplyTo uint64 `db:"rel_message"` + UpdatedAt *time.Time `json:",omitempty" db:"updated_at"` + DeletedAt *time.Time `json:",omitempty" db:"deleted_at"` + + changed []string + } + + // Messages - + Reaction struct { + ID uint64 `db:"id"` + UserId uint64 `db:"rel_user"` + MessageId uint64 `db:"rel_message"` + ChannelId uint64 `db:"rel_channel"` + Reaction string `db:"reaction"` + DeletedAt *time.Time `json:",omitempty" db:"deleted_at"` + + changed []string + } + + // Messages - + 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"` + DeletedAt *time.Time `json:",omitempty" db:"deleted_at"` changed []string } @@ -37,84 +65,40 @@ func (Message) New() *Message { return &Message{} } -// Get the value of Service -func (m *Message) GetService() string { - return m.Service +// New constructs a new instance of Reaction +func (Reaction) New() *Reaction { + return &Reaction{} } -// Set the value of Service -func (m *Message) SetService(value string) *Message { - if m.Service != value { - m.changed = append(m.changed, "Service") - m.Service = value +// New constructs a new instance of Attachment +func (Attachment) New() *Attachment { + return &Attachment{} +} + +// Get the value of ID +func (m *Message) GetID() uint64 { + return m.ID +} + +// Set the value of ID +func (m *Message) SetID(value uint64) *Message { + if m.ID != value { + m.changed = append(m.changed, "ID") + m.ID = value } return m } -// Get the value of Channel -func (m *Message) GetChannel() string { - return m.Channel +// Get the value of Type +func (m *Message) GetType() string { + return m.Type } -// Set the value of Channel -func (m *Message) SetChannel(value string) *Message { - if m.Channel != value { - m.changed = append(m.changed, "Channel") - m.Channel = value - } - return m -} - -// Get the value of UserName -func (m *Message) GetUserName() string { - return m.UserName -} - -// Set the value of UserName -func (m *Message) SetUserName(value string) *Message { - if m.UserName != value { - m.changed = append(m.changed, "UserName") - m.UserName = value - } - return m -} - -// Get the value of UserID -func (m *Message) GetUserID() uint64 { - return m.UserID -} - -// Set the value of UserID -func (m *Message) SetUserID(value uint64) *Message { - if m.UserID != value { - m.changed = append(m.changed, "UserID") - m.UserID = value - } - return m -} - -// Get the value of User -func (m *Message) GetUser() *User { - return m.User -} - -// Set the value of User -func (m *Message) SetUser(value *User) *Message { - m.changed = append(m.changed, "User") - m.User = value - return m -} - -// Get the value of UserAvatar -func (m *Message) GetUserAvatar() string { - return m.UserAvatar -} - -// Set the value of UserAvatar -func (m *Message) SetUserAvatar(value string) *Message { - if m.UserAvatar != value { - m.changed = append(m.changed, "UserAvatar") - m.UserAvatar = value +// Set the value of Type +func (m *Message) SetType(value string) *Message { + if m.Type != value { + m.changed = append(m.changed, "Type") + m.Type = value } return m } @@ -133,35 +117,245 @@ func (m *Message) SetMessage(value string) *Message { return m } -// Get the value of MessageID -func (m *Message) GetMessageID() string { - return m.MessageID +// Get the value of UserId +func (m *Message) GetUserId() uint64 { + return m.UserId } -// Set the value of MessageID -func (m *Message) SetMessageID(value string) *Message { - if m.MessageID != value { - m.changed = append(m.changed, "MessageID") - m.MessageID = value +// Set the value of UserId +func (m *Message) SetUserId(value uint64) *Message { + if m.UserId != value { + m.changed = append(m.changed, "UserId") + m.UserId = value } return m } -// Get the value of Type -func (m *Message) GetType() MessageType { - return m.Type +// Get the value of ChannelId +func (m *Message) GetChannelId() uint64 { + return m.ChannelId } -// Set the value of Type -func (m *Message) SetType(value MessageType) *Message { - if m.Type != value { - m.changed = append(m.changed, "Type") - m.Type = value +// Set the value of ChannelId +func (m *Message) SetChannelId(value uint64) *Message { + if m.ChannelId != value { + m.changed = append(m.changed, "ChannelId") + m.ChannelId = value } return m } +// Get the value of ReplyTo +func (m *Message) GetReplyTo() uint64 { + return m.ReplyTo +} + +// Set the value of ReplyTo +func (m *Message) SetReplyTo(value uint64) *Message { + if m.ReplyTo != value { + m.changed = append(m.changed, "ReplyTo") + m.ReplyTo = value + } + return m +} + +// Get the value of UpdatedAt +func (m *Message) GetUpdatedAt() *time.Time { + return m.UpdatedAt +} + +// Set the value of UpdatedAt +func (m *Message) SetUpdatedAt(value *time.Time) *Message { + m.changed = append(m.changed, "UpdatedAt") + m.UpdatedAt = value + return m +} + +// Get the value of DeletedAt +func (m *Message) GetDeletedAt() *time.Time { + return m.DeletedAt +} + +// Set the value of DeletedAt +func (m *Message) SetDeletedAt(value *time.Time) *Message { + m.changed = append(m.changed, "DeletedAt") + m.DeletedAt = value + return m +} + // Changes returns the names of changed fields func (m *Message) Changes() []string { return m.changed } + +// Get the value of ID +func (m *Reaction) GetID() uint64 { + return m.ID +} + +// Set the value of ID +func (m *Reaction) SetID(value uint64) *Reaction { + if m.ID != value { + m.changed = append(m.changed, "ID") + m.ID = value + } + return m +} + +// Get the value of UserId +func (m *Reaction) GetUserId() uint64 { + return m.UserId +} + +// Set the value of UserId +func (m *Reaction) SetUserId(value uint64) *Reaction { + if m.UserId != value { + m.changed = append(m.changed, "UserId") + m.UserId = value + } + return m +} + +// Get the value of MessageId +func (m *Reaction) GetMessageId() uint64 { + return m.MessageId +} + +// Set the value of MessageId +func (m *Reaction) SetMessageId(value uint64) *Reaction { + if m.MessageId != value { + m.changed = append(m.changed, "MessageId") + m.MessageId = value + } + return m +} + +// Get the value of ChannelId +func (m *Reaction) GetChannelId() uint64 { + return m.ChannelId +} + +// Set the value of ChannelId +func (m *Reaction) SetChannelId(value uint64) *Reaction { + if m.ChannelId != value { + m.changed = append(m.changed, "ChannelId") + m.ChannelId = value + } + return m +} + +// Get the value of Reaction +func (m *Reaction) GetReaction() string { + return m.Reaction +} + +// Set the value of Reaction +func (m *Reaction) SetReaction(value string) *Reaction { + if m.Reaction != value { + m.changed = append(m.changed, "Reaction") + m.Reaction = value + } + return m +} + +// Get the value of DeletedAt +func (m *Reaction) GetDeletedAt() *time.Time { + return m.DeletedAt +} + +// Set the value of DeletedAt +func (m *Reaction) SetDeletedAt(value *time.Time) *Reaction { + m.changed = append(m.changed, "DeletedAt") + m.DeletedAt = value + return m +} + +// Changes returns the names of changed fields +func (m *Reaction) Changes() []string { + return m.changed +} + +// Get the value of ID +func (m *Attachment) GetID() uint64 { + return m.ID +} + +// Set the value of ID +func (m *Attachment) SetID(value uint64) *Attachment { + if m.ID != value { + m.changed = append(m.changed, "ID") + m.ID = value + } + return m +} + +// Get the value of UserId +func (m *Attachment) GetUserId() uint64 { + return m.UserId +} + +// Set the value of UserId +func (m *Attachment) SetUserId(value uint64) *Attachment { + if m.UserId != value { + m.changed = append(m.changed, "UserId") + m.UserId = value + } + return m +} + +// Get the value of MessageId +func (m *Attachment) GetMessageId() uint64 { + return m.MessageId +} + +// Set the value of MessageId +func (m *Attachment) SetMessageId(value uint64) *Attachment { + if m.MessageId != value { + m.changed = append(m.changed, "MessageId") + m.MessageId = value + } + return m +} + +// Get the value of ChannelId +func (m *Attachment) GetChannelId() uint64 { + return m.ChannelId +} + +// Set the value of ChannelId +func (m *Attachment) SetChannelId(value uint64) *Attachment { + if m.ChannelId != value { + m.changed = append(m.changed, "ChannelId") + m.ChannelId = value + } + return m +} + +// Get the value of Attachment +func (m *Attachment) GetAttachment() json.RawMessage { + return m.Attachment +} + +// Set the value of Attachment +func (m *Attachment) SetAttachment(value json.RawMessage) *Attachment { + m.changed = append(m.changed, "Attachment") + m.Attachment = value + return m +} + +// Get the value of DeletedAt +func (m *Attachment) GetDeletedAt() *time.Time { + return m.DeletedAt +} + +// Set the value of DeletedAt +func (m *Attachment) SetDeletedAt(value *time.Time) *Attachment { + m.changed = append(m.changed, "DeletedAt") + m.DeletedAt = value + return m +} + +// Changes returns the names of changed fields +func (m *Attachment) Changes() []string { + return m.changed +} diff --git a/sam/types/message_filter.go b/sam/types/message_filter.go new file mode 100644 index 000000000..3b40ce19b --- /dev/null +++ b/sam/types/message_filter.go @@ -0,0 +1,8 @@ +package types + +type ( + MessageFilter struct { + Query string + ChannelId string + } +) diff --git a/sam/types/message_type.go b/sam/types/message_type.go deleted file mode 100644 index 16ec9d670..000000000 --- a/sam/types/message_type.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -// MessageType is a type used to determine the CRUD state of a message. -type MessageType string - -const ( - // MessageTypeCreate is the message type for message creation. - MessageTypeCreate MessageType = "create" - // MessageTypeUpdate is the message type for message updates. - MessageTypeUpdate = "update" - // MessageTypeDelete is the message type for message deletion. - MessageTypeDelete = "delete" -)