diff --git a/api/proto/realtime.proto b/api/proto/realtime.proto index d04bc35..3fca899 100644 --- a/api/proto/realtime.proto +++ b/api/proto/realtime.proto @@ -8,7 +8,11 @@ package realtime; service RealTime { rpc Publish(PublishMessage) returns (google.protobuf.Empty) {} - rpc Subscribe(Channel) returns (stream Message) {} + rpc Subscribe(Channels) returns (stream Message) {} +} + +message Channels { + repeated Channel chans = 1; } message Channel { @@ -27,10 +31,15 @@ message EventObject { EventType type = 2; } +message EventMap { + int64 type = 1; + map m = 2; +} + message Message { oneof body { EventObject object = 1; - string content = 2; + EventMap content = 2; } } diff --git a/go.mod b/go.mod index f2512ed..9f4f39f 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/google/uuid v1.3.1 github.com/jackc/pgx/v5 v5.4.3 github.com/joho/godotenv v1.5.1 + github.com/mailru/easyjson v0.7.7 github.com/microcosm-cc/bluemonday v1.0.26 github.com/pashagolub/pgxmock/v2 v2.12.0 github.com/prometheus/client_golang v1.17.0 @@ -69,7 +70,6 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/internal/api/realtime/realtime.pb.go b/internal/api/realtime/realtime.pb.go index 8e17fe5..3b3ed8f 100644 --- a/internal/api/realtime/realtime.pb.go +++ b/internal/api/realtime/realtime.pb.go @@ -70,6 +70,53 @@ func (EventType) EnumDescriptor() ([]byte, []int) { return file_api_proto_realtime_proto_rawDescGZIP(), []int{0} } +type Channels struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Chans []*Channel `protobuf:"bytes,1,rep,name=chans,proto3" json:"chans,omitempty"` +} + +func (x *Channels) Reset() { + *x = Channels{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Channels) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Channels) ProtoMessage() {} + +func (x *Channels) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Channels.ProtoReflect.Descriptor instead. +func (*Channels) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{0} +} + +func (x *Channels) GetChans() []*Channel { + if x != nil { + return x.Chans + } + return nil +} + type Channel struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -82,7 +129,7 @@ type Channel struct { func (x *Channel) Reset() { *x = Channel{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[0] + mi := &file_api_proto_realtime_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -95,7 +142,7 @@ func (x *Channel) String() string { func (*Channel) ProtoMessage() {} func (x *Channel) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[0] + mi := &file_api_proto_realtime_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -108,7 +155,7 @@ func (x *Channel) ProtoReflect() protoreflect.Message { // Deprecated: Use Channel.ProtoReflect.Descriptor instead. func (*Channel) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{0} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{1} } func (x *Channel) GetTopic() string { @@ -137,7 +184,7 @@ type EventObject struct { func (x *EventObject) Reset() { *x = EventObject{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[1] + mi := &file_api_proto_realtime_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -150,7 +197,7 @@ func (x *EventObject) String() string { func (*EventObject) ProtoMessage() {} func (x *EventObject) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[1] + mi := &file_api_proto_realtime_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -163,7 +210,7 @@ func (x *EventObject) ProtoReflect() protoreflect.Message { // Deprecated: Use EventObject.ProtoReflect.Descriptor instead. func (*EventObject) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{1} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{2} } func (x *EventObject) GetId() int64 { @@ -180,6 +227,61 @@ func (x *EventObject) GetType() EventType { return EventType_EV_CREATE } +type EventMap struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type int64 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"` + M map[string]string `protobuf:"bytes,2,rep,name=m,proto3" json:"m,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *EventMap) Reset() { + *x = EventMap{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EventMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventMap) ProtoMessage() {} + +func (x *EventMap) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventMap.ProtoReflect.Descriptor instead. +func (*EventMap) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{3} +} + +func (x *EventMap) GetType() int64 { + if x != nil { + return x.Type + } + return 0 +} + +func (x *EventMap) GetM() map[string]string { + if x != nil { + return x.M + } + return nil +} + type Message struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -195,7 +297,7 @@ type Message struct { func (x *Message) Reset() { *x = Message{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[2] + mi := &file_api_proto_realtime_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -208,7 +310,7 @@ func (x *Message) String() string { func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[2] + mi := &file_api_proto_realtime_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -221,7 +323,7 @@ func (x *Message) ProtoReflect() protoreflect.Message { // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{2} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{4} } func (m *Message) GetBody() isMessage_Body { @@ -238,11 +340,11 @@ func (x *Message) GetObject() *EventObject { return nil } -func (x *Message) GetContent() string { +func (x *Message) GetContent() *EventMap { if x, ok := x.GetBody().(*Message_Content); ok { return x.Content } - return "" + return nil } type isMessage_Body interface { @@ -254,7 +356,7 @@ type Message_Object struct { } type Message_Content struct { - Content string `protobuf:"bytes,2,opt,name=content,proto3,oneof"` + Content *EventMap `protobuf:"bytes,2,opt,name=content,proto3,oneof"` } func (*Message_Object) isMessage_Body() {} @@ -273,7 +375,7 @@ type PublishMessage struct { func (x *PublishMessage) Reset() { *x = PublishMessage{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[3] + mi := &file_api_proto_realtime_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -286,7 +388,7 @@ func (x *PublishMessage) String() string { func (*PublishMessage) ProtoMessage() {} func (x *PublishMessage) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[3] + mi := &file_api_proto_realtime_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -299,7 +401,7 @@ func (x *PublishMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use PublishMessage.ProtoReflect.Descriptor instead. func (*PublishMessage) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{3} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{5} } func (x *PublishMessage) GetChannel() *Channel { @@ -323,43 +425,56 @@ var file_api_proto_realtime_proto_rawDesc = []byte{ 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x33, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, - 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, - 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x46, 0x0a, 0x0b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x5e, - 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x6f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x61, 0x6c, - 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x48, 0x00, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1a, 0x0a, 0x07, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x6a, - 0x0a, 0x0e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x2b, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0x38, 0x0a, 0x09, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x43, 0x52, - 0x45, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x44, 0x45, 0x4c, - 0x45, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x55, 0x50, 0x44, 0x41, - 0x54, 0x45, 0x10, 0x02, 0x32, 0x80, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x6c, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x3d, 0x0a, 0x07, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x12, 0x18, 0x2e, 0x72, - 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, - 0x12, 0x35, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x11, 0x2e, - 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x1a, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x70, 0x61, 0x72, 0x6b, 0x2d, 0x6d, 0x61, - 0x69, 0x6c, 0x2d, 0x72, 0x75, 0x2f, 0x32, 0x30, 0x32, 0x33, 0x5f, 0x32, 0x5f, 0x4f, 0x4e, 0x44, - 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, - 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x22, 0x33, 0x0a, 0x08, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x27, 0x0a, + 0x05, 0x63, 0x68, 0x61, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, + 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, + 0x05, 0x63, 0x68, 0x61, 0x6e, 0x73, 0x22, 0x33, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x46, 0x0a, 0x0b, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, + 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x22, 0x7d, 0x0a, 0x08, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x01, 0x6d, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, + 0x61, 0x70, 0x2e, 0x4d, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x01, 0x6d, 0x1a, 0x34, 0x0a, 0x06, + 0x4d, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x72, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2f, 0x0a, + 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x48, 0x00, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2e, + 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x4d, 0x61, 0x70, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x42, 0x06, + 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x6a, 0x0a, 0x0e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x07, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x2b, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, + 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x2a, 0x38, 0x0a, 0x09, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x0d, 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x0d, + 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, + 0x09, 0x45, 0x56, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x32, 0x81, 0x01, 0x0a, + 0x08, 0x52, 0x65, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x07, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x73, 0x68, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x12, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, + 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x1a, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, + 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x6f, 0x2d, 0x70, 0x61, 0x72, 0x6b, 0x2d, 0x6d, 0x61, 0x69, 0x6c, 0x2d, 0x72, 0x75, 0x2f, 0x32, + 0x30, 0x32, 0x33, 0x5f, 0x32, 0x5f, 0x4f, 0x4e, 0x44, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -375,29 +490,35 @@ func file_api_proto_realtime_proto_rawDescGZIP() []byte { } var file_api_proto_realtime_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_api_proto_realtime_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_api_proto_realtime_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_api_proto_realtime_proto_goTypes = []interface{}{ (EventType)(0), // 0: realtime.EventType - (*Channel)(nil), // 1: realtime.Channel - (*EventObject)(nil), // 2: realtime.EventObject - (*Message)(nil), // 3: realtime.Message - (*PublishMessage)(nil), // 4: realtime.PublishMessage - (*empty.Empty)(nil), // 5: google.protobuf.Empty + (*Channels)(nil), // 1: realtime.Channels + (*Channel)(nil), // 2: realtime.Channel + (*EventObject)(nil), // 3: realtime.EventObject + (*EventMap)(nil), // 4: realtime.EventMap + (*Message)(nil), // 5: realtime.Message + (*PublishMessage)(nil), // 6: realtime.PublishMessage + nil, // 7: realtime.EventMap.MEntry + (*empty.Empty)(nil), // 8: google.protobuf.Empty } var file_api_proto_realtime_proto_depIdxs = []int32{ - 0, // 0: realtime.EventObject.type:type_name -> realtime.EventType - 2, // 1: realtime.Message.object:type_name -> realtime.EventObject - 1, // 2: realtime.PublishMessage.channel:type_name -> realtime.Channel - 3, // 3: realtime.PublishMessage.message:type_name -> realtime.Message - 4, // 4: realtime.RealTime.Publish:input_type -> realtime.PublishMessage - 1, // 5: realtime.RealTime.Subscribe:input_type -> realtime.Channel - 5, // 6: realtime.RealTime.Publish:output_type -> google.protobuf.Empty - 3, // 7: realtime.RealTime.Subscribe:output_type -> realtime.Message - 6, // [6:8] is the sub-list for method output_type - 4, // [4:6] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 2, // 0: realtime.Channels.chans:type_name -> realtime.Channel + 0, // 1: realtime.EventObject.type:type_name -> realtime.EventType + 7, // 2: realtime.EventMap.m:type_name -> realtime.EventMap.MEntry + 3, // 3: realtime.Message.object:type_name -> realtime.EventObject + 4, // 4: realtime.Message.content:type_name -> realtime.EventMap + 2, // 5: realtime.PublishMessage.channel:type_name -> realtime.Channel + 5, // 6: realtime.PublishMessage.message:type_name -> realtime.Message + 6, // 7: realtime.RealTime.Publish:input_type -> realtime.PublishMessage + 1, // 8: realtime.RealTime.Subscribe:input_type -> realtime.Channels + 8, // 9: realtime.RealTime.Publish:output_type -> google.protobuf.Empty + 5, // 10: realtime.RealTime.Subscribe:output_type -> realtime.Message + 9, // [9:11] is the sub-list for method output_type + 7, // [7:9] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_api_proto_realtime_proto_init() } @@ -407,7 +528,7 @@ func file_api_proto_realtime_proto_init() { } if !protoimpl.UnsafeEnabled { file_api_proto_realtime_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Channel); i { + switch v := v.(*Channels); i { case 0: return &v.state case 1: @@ -419,7 +540,7 @@ func file_api_proto_realtime_proto_init() { } } file_api_proto_realtime_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EventObject); i { + switch v := v.(*Channel); i { case 0: return &v.state case 1: @@ -431,7 +552,7 @@ func file_api_proto_realtime_proto_init() { } } file_api_proto_realtime_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message); i { + switch v := v.(*EventObject); i { case 0: return &v.state case 1: @@ -443,6 +564,30 @@ func file_api_proto_realtime_proto_init() { } } file_api_proto_realtime_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EventMap); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PublishMessage); i { case 0: return &v.state @@ -455,7 +600,7 @@ func file_api_proto_realtime_proto_init() { } } } - file_api_proto_realtime_proto_msgTypes[2].OneofWrappers = []interface{}{ + file_api_proto_realtime_proto_msgTypes[4].OneofWrappers = []interface{}{ (*Message_Object)(nil), (*Message_Content)(nil), } @@ -465,7 +610,7 @@ func file_api_proto_realtime_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_proto_realtime_proto_rawDesc, NumEnums: 1, - NumMessages: 4, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/api/realtime/realtime_grpc.pb.go b/internal/api/realtime/realtime_grpc.pb.go index 759e3ff..420b61e 100644 --- a/internal/api/realtime/realtime_grpc.pb.go +++ b/internal/api/realtime/realtime_grpc.pb.go @@ -24,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type RealTimeClient interface { Publish(ctx context.Context, in *PublishMessage, opts ...grpc.CallOption) (*empty.Empty, error) - Subscribe(ctx context.Context, in *Channel, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) + Subscribe(ctx context.Context, in *Channels, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) } type realTimeClient struct { @@ -44,7 +44,7 @@ func (c *realTimeClient) Publish(ctx context.Context, in *PublishMessage, opts . return out, nil } -func (c *realTimeClient) Subscribe(ctx context.Context, in *Channel, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) { +func (c *realTimeClient) Subscribe(ctx context.Context, in *Channels, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) { stream, err := c.cc.NewStream(ctx, &RealTime_ServiceDesc.Streams[0], "/realtime.RealTime/Subscribe", opts...) if err != nil { return nil, err @@ -81,7 +81,7 @@ func (x *realTimeSubscribeClient) Recv() (*Message, error) { // for forward compatibility type RealTimeServer interface { Publish(context.Context, *PublishMessage) (*empty.Empty, error) - Subscribe(*Channel, RealTime_SubscribeServer) error + Subscribe(*Channels, RealTime_SubscribeServer) error mustEmbedUnimplementedRealTimeServer() } @@ -92,7 +92,7 @@ type UnimplementedRealTimeServer struct { func (UnimplementedRealTimeServer) Publish(context.Context, *PublishMessage) (*empty.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Publish not implemented") } -func (UnimplementedRealTimeServer) Subscribe(*Channel, RealTime_SubscribeServer) error { +func (UnimplementedRealTimeServer) Subscribe(*Channels, RealTime_SubscribeServer) error { return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") } func (UnimplementedRealTimeServer) mustEmbedUnimplementedRealTimeServer() {} @@ -127,7 +127,7 @@ func _RealTime_Publish_Handler(srv interface{}, ctx context.Context, dec func(in } func _RealTime_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(Channel) + m := new(Channels) if err := stream.RecvMsg(m); err != nil { return err } diff --git a/internal/api/server/router/router.go b/internal/api/server/router/router.go index 66b6f54..434ed62 100644 --- a/internal/api/server/router/router.go +++ b/internal/api/server/router/router.go @@ -101,6 +101,15 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, wsHandler *deli r.Delete("/like/{pinID:\\d+}", handler.DeleteLikePin) r.Delete("/delete/{pinID:\\d+}", handler.DeletePin) }) + + r.Route("/comment", func(r chi.Router) { + r.Get("/feed/{pinID:\\d+}", handler.ViewFeedComment) + + r.With(auth.RequireAuth).Group(func(r chi.Router) { + r.Post("/{pinID:\\d+}", handler.WriteComment) + r.Delete("/{commentID:\\d+}", handler.DeleteComment) + }) + }) }) r.Route("/board", func(r chi.Router) { @@ -132,6 +141,7 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, wsHandler *deli }) r.Mux.With(auth.RequireAuth).Route("/websocket/connect", func(r chi.Router) { - r.Get("/chat", wsHandler.WebSocketConnect) + r.Get("/chat", wsHandler.Chat) + r.Get("/notification", wsHandler.Notification) }) } diff --git a/internal/app/app.go b/internal/app/app.go index 0437156..57ae57a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -11,12 +11,16 @@ import ( authProto "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/server" "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/server/router" deliveryHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1" deliveryWS "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/websocket" + notify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/metrics" + commentNotify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification/comment" boardRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/board/postgres" + commentRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/comment" imgRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/image" pinRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/pin" searchRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/search/postgres" @@ -24,9 +28,13 @@ import ( userRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/board" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/comment" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/image" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/chat" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/notification" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/search" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/subscription" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" @@ -64,8 +72,29 @@ func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { } defer connMessMS.Close() + connRealtime, err := grpc.Dial("localhost:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error(err.Error()) + return + } + defer connRealtime.Close() + + rtClient := rt.NewRealTimeClient(connRealtime) + + commentRepository := commentRepo.NewCommentRepoPG(pool) + imgCase := image.New(log, imgRepo.NewImageRepoFS(uploadFiles)) - messageCase := message.New(messenger.NewMessengerClient(connMessMS)) + messageCase := message.New(log, messenger.NewMessengerClient(connMessMS), chat.New(realtime.NewRealTimeChatClient(rtClient), log)) + pinCase := pin.New(log, imgCase, pinRepo.NewPinRepoPG(pool)) + + notifyBuilder, err := notify.NewWithType(notify.NotifyComment) + if err != nil { + log.Error(err.Error()) + return + } + + notifyCase := notification.New(realtime.NewRealTimeNotificationClient(rtClient), log, + notification.Register(commentNotify.NewCommentNotify(notifyBuilder, comment.New(commentRepository, pinCase, nil), pinCase))) conn, err := grpc.Dial(cfg.AddrAuthServer, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { @@ -78,14 +107,15 @@ func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { handler := deliveryHTTP.New(log, deliveryHTTP.UsecaseHub{ AuhtCase: ac, UserCase: user.New(log, imgCase, userRepo.NewUserRepoPG(pool)), - PinCase: pin.New(log, imgCase, pinRepo.NewPinRepoPG(pool)), + PinCase: pinCase, BoardCase: board.New(log, boardRepo.NewBoardRepoPG(pool), userRepo.NewUserRepoPG(pool), bluemonday.UGCPolicy()), SubscriptionCase: subscription.New(log, subRepo.NewSubscriptionRepoPG(pool), userRepo.NewUserRepoPG(pool), bluemonday.UGCPolicy()), SearchCase: search.New(log, searchRepo.NewSearchRepoPG(pool), bluemonday.UGCPolicy()), MessageCase: messageCase, + CommentCase: comment.New(commentRepo.NewCommentRepoPG(pool), pinCase, notifyCase), }) - wsHandler := deliveryWS.New(log, messageCase, + wsHandler := deliveryWS.New(log, messageCase, notifyCase, deliveryWS.SetOriginPatterns([]string{"pinspire.online", "pinspire.online:*"})) cfgServ, err := server.NewConfig(cfg.ServerConfigFile) diff --git a/internal/microservices/realtime/server.go b/internal/microservices/realtime/server.go index c83d166..0574eef 100644 --- a/internal/microservices/realtime/server.go +++ b/internal/microservices/realtime/server.go @@ -42,7 +42,7 @@ func (s Server) Publish(ctx context.Context, pm *rt.PublishMessage) (*empty.Empt return &empty.Empty{}, nil } -func (s Server) Subscribe(c *rt.Channel, ss rt.RealTime_SubscribeServer) error { +func (s Server) Subscribe(chans *rt.Channels, ss rt.RealTime_SubscribeServer) error { id, err := uuid.NewRandom() if err != nil { return status.Error(codes.Internal, "generate uuid v4") @@ -52,7 +52,9 @@ func (s Server) Subscribe(c *rt.Channel, ss rt.RealTime_SubscribeServer) error { transport: ss, } - s.node.AddSubscriber(c, client) + for _, ch := range chans.GetChans() { + s.node.AddSubscriber(ch, client) + } <-ss.Context().Done() return nil diff --git a/internal/pkg/delivery/http/v1/chat.go b/internal/pkg/delivery/http/v1/chat.go index ac6adf5..09e64cb 100644 --- a/internal/pkg/delivery/http/v1/chat.go +++ b/internal/pkg/delivery/http/v1/chat.go @@ -90,7 +90,7 @@ func (h *HandlerHTTP) DeleteMessage(w http.ResponseWriter, r *http.Request) { return } - err = h.messageCase.DeleteMessage(r.Context(), userID, messageID) + err = h.messageCase.DeleteMessage(r.Context(), userID, &message.Message{ID: messageID}) if err != nil { logger.Warn(err.Error()) err = responseError(w, "delete_message", "fail deleting a message") diff --git a/internal/pkg/delivery/http/v1/comment.go b/internal/pkg/delivery/http/v1/comment.go new file mode 100644 index 0000000..d85aa7a --- /dev/null +++ b/internal/pkg/delivery/http/v1/comment.go @@ -0,0 +1,119 @@ +package v1 + +import ( + "net/http" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "github.com/mailru/easyjson" +) + +func (h *HandlerHTTP) WriteComment(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + + pinID, err := fetchURLParamInt(r, "pinID") + if err != nil { + err = responseError(w, "parse_url", "the request url could not be get pin id") + if err != nil { + logger.Error(err.Error()) + return + } + } + + comment := &comment.Comment{} + + err = easyjson.UnmarshalFromReader(r.Body, comment) + defer r.Body.Close() + if err != nil { + logger.Warn(err.Error()) + + err = responseError(w, "parse_body", "the request body could not be parsed to send a comment") + if err != nil { + logger.Error(err.Error()) + return + } + } + + comment.PinID = pinID + _, err = h.commentCase.PutCommentOnPin(r.Context(), userID, comment) + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "create_comment", "couldn't leave a comment under the selected pin") + } else { + err = responseOk(http.StatusCreated, w, "the comment has been added successfully", nil) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) DeleteComment(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + + commentID, err := fetchURLParamInt(r, "commentID") + if err != nil { + err = responseError(w, "parse_url", "the request url could not be get pin id") + if err != nil { + logger.Error(err.Error()) + return + } + } + + err = h.commentCase.DeleteComment(r.Context(), userID, commentID) + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "delete_comment", "couldn't delete pin comment") + } else { + err = responseOk(http.StatusOK, w, "the comment was successfully deleted", nil) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) ViewFeedComment(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID, ok := r.Context().Value(auth.KeyCurrentUserID).(int) + if !ok { + userID = user.UserUnknown + } + + pinID, err := fetchURLParamInt(r, "pinID") + if err != nil { + err = responseError(w, "parse_url", "the request url could not be get pin id") + if err != nil { + logger.Error(err.Error()) + return + } + } + + count, lastID, err := FetchValidParamForLoadFeed(r.URL) + if err != nil { + err = responseError(w, "query_param", "the parameters for displaying the pin feed could not be extracted from the request") + if err != nil { + logger.Error(err.Error()) + return + } + } + + feed, newLastID, err := h.commentCase.GetFeedCommentOnPin(r.Context(), userID, pinID, count, lastID) + if err != nil && len(feed) == 0 { + err = responseError(w, "feed_view", "error displaying pin comments") + if err != nil { + logger.Error(err.Error()) + } + return + } + + if err != nil { + logger.Error(err.Error()) + } + + err = responseOk(http.StatusOK, w, "feed comment to pin", map[string]any{"comments": feed, "lastID": newLastID}) + if err != nil { + logger.Error(err.Error()) + } +} diff --git a/internal/pkg/delivery/http/v1/feed.go b/internal/pkg/delivery/http/v1/feed.go index 4a32690..02d71a1 100644 --- a/internal/pkg/delivery/http/v1/feed.go +++ b/internal/pkg/delivery/http/v1/feed.go @@ -8,8 +8,8 @@ import ( "strconv" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" - usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) @@ -17,7 +17,7 @@ func (h *HandlerHTTP) FeedPins(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) userID, isAuth := r.Context().Value(auth.KeyCurrentUserID).(int) if !isAuth { - userID = usecase.UserUnknown + userID = user.UserUnknown } logger.Info("request on getting feed of pins", log.F{"rawQuery", r.URL.RawQuery}) diff --git a/internal/pkg/delivery/http/v1/handler.go b/internal/pkg/delivery/http/v1/handler.go index 837e590..736654e 100644 --- a/internal/pkg/delivery/http/v1/handler.go +++ b/internal/pkg/delivery/http/v1/handler.go @@ -3,6 +3,7 @@ package v1 import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/board" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/comment" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/search" @@ -20,6 +21,7 @@ type HandlerHTTP struct { subCase subscription.Usecase searchCase search.Usecase messageCase message.Usecase + commentCase comment.Usecase } func New(log *logger.Logger, hub UsecaseHub) *HandlerHTTP { @@ -32,6 +34,7 @@ func New(log *logger.Logger, hub UsecaseHub) *HandlerHTTP { subCase: hub.SubscriptionCase, searchCase: hub.SearchCase, messageCase: hub.MessageCase, + commentCase: hub.CommentCase, } } @@ -43,4 +46,5 @@ type UsecaseHub struct { SubscriptionCase subscription.Usecase SearchCase search.Usecase MessageCase message.Usecase + CommentCase comment.Usecase } diff --git a/internal/pkg/delivery/http/v1/pin.go b/internal/pkg/delivery/http/v1/pin.go index be7c2c9..f9ba931 100644 --- a/internal/pkg/delivery/http/v1/pin.go +++ b/internal/pkg/delivery/http/v1/pin.go @@ -168,7 +168,7 @@ func (h *HandlerHTTP) ViewPin(w http.ResponseWriter, r *http.Request) { userID, ok := r.Context().Value(auth.KeyCurrentUserID).(int) if !ok { - userID = usecase.UserUnknown + userID = user.UserUnknown } pin, err := h.pinCase.ViewAnPin(r.Context(), int(pinID), userID) if err != nil { diff --git a/internal/pkg/delivery/websocket/chat.go b/internal/pkg/delivery/websocket/chat.go new file mode 100644 index 0000000..50b0f21 --- /dev/null +++ b/internal/pkg/delivery/websocket/chat.go @@ -0,0 +1,114 @@ +package websocket + +import ( + "context" + "fmt" + "net/http" + + ws "nhooyr.io/websocket" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +func (h *HandlerWebSocket) Chat(w http.ResponseWriter, r *http.Request) { + conn, err := h.upgradeWSConnect(w, r) + if err != nil { + h.log.Error(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"status":"error","code":"websocket_connect","message":"fail connect"}`)) + return + } + defer conn.CloseNow() + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + ctx, cancel := context.WithTimeout(context.Background(), _ctxOnServeConnect) + defer cancel() + + socket := newSocketJSON(conn) + + err = h.subscribeOnChat(ctx, socket, userID) + if err != nil { + h.log.Error(err.Error()) + conn.Close(ws.StatusInternalError, "subscribe_fail") + return + } + + err = h.serveChat(ctx, socket, userID) + if err != nil && ws.CloseStatus(err) == -1 { + h.log.Error(err.Error()) + conn.Close(ws.StatusInternalError, "serve_chat") + } +} + +func (h *HandlerWebSocket) serveChat(ctx context.Context, rw CtxReadWriter, userID int) error { + request := &PublishRequest{} + var err error + for { + err = rw.Read(ctx, request) + if err != nil { + h.log.Error(err.Error()) + return fmt.Errorf("read message: %w", err) + } + + h.handlePublishRequestMessage(ctx, rw, userID, request) + } +} + +func (h *HandlerWebSocket) handlePublishRequestMessage(ctx context.Context, w CtxWriter, userID int, req *PublishRequest) { + fmt.Println(req) + switch req.Message.Type { + case "create": + req.Message.Message.From = userID + id, err := h.messageCase.SendMessage(ctx, userID, &req.Message.Message) + if err != nil { + h.log.Warn(err.Error()) + return + } + w.Write(ctx, newResponseOnRequest(req.ID, "ok", "", "publish success", map[string]any{"id": id, "eventType": "create"})) + + case "update": + err := h.messageCase.UpdateContentMessage(ctx, userID, &req.Message.Message) + if err != nil { + h.log.Warn(err.Error()) + return + } + w.Write(ctx, newResponseOnRequest(req.ID, "ok", "", "publish success", map[string]string{"eventType": "update"})) + + case "delete": + err := h.messageCase.DeleteMessage(ctx, userID, &req.Message.Message) + if err != nil { + h.log.Warn(err.Error()) + return + } + w.Write(ctx, newResponseOnRequest(req.ID, "ok", "", "publish success", map[string]string{"eventType": "delete"})) + + default: + w.Write(ctx, newResponseOnRequest(req.ID, "error", "unsupported", "unsupported eventType", nil)) + } +} + +func (h *HandlerWebSocket) subscribeOnChat(ctx context.Context, w CtxWriter, userID int) error { + chanEvMsg, err := h.messageCase.SubscribeUserToAllChats(ctx, userID) + if err != nil { + return fmt.Errorf("subscribe user on chat: %w", err) + } + + go func() { + for eventMessage := range chanEvMsg { + if eventMessage.Err != nil { + h.log.Error(eventMessage.Err.Error()) + return + } + + err = w.Write(ctx, newMessageFromChannel("ok", "", Object{ + Type: eventMessage.Type, + Message: *eventMessage.Message, + })) + if err != nil { + h.log.Error(err.Error()) + return + } + } + }() + return nil +} diff --git a/internal/pkg/delivery/websocket/notification.go b/internal/pkg/delivery/websocket/notification.go new file mode 100644 index 0000000..a24852e --- /dev/null +++ b/internal/pkg/delivery/websocket/notification.go @@ -0,0 +1,54 @@ +package websocket + +import ( + "context" + "fmt" + "net/http" + + ws "nhooyr.io/websocket" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +func (h *HandlerWebSocket) Notification(w http.ResponseWriter, r *http.Request) { + conn, err := h.upgradeWSConnect(w, r) + if err != nil { + h.log.Error(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"status":"error","code":"websocket_connect","message":"fail connect"}`)) + return + } + defer conn.CloseNow() + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + ctx, cancel := context.WithTimeout(context.Background(), _ctxOnServeConnect) + defer cancel() + + socket := newSocketJSON(conn) + + err = h.subscribeOnNotificationAndServe(ctx, socket, userID) + if err != nil && ws.CloseStatus(err) == -1 { + h.log.Error(err.Error()) + conn.Close(ws.StatusInternalError, "subscribe_fail") + } +} + +func (h *HandlerWebSocket) subscribeOnNotificationAndServe(ctx context.Context, w CtxWriter, userID int) error { + chanNotify, err := h.notifySub.SubscribeOnAllNotifications(ctx, userID) + if err != nil { + return fmt.Errorf("subscribe on Notification") + } + + for notify := range chanNotify { + if notify.Err() != nil { + return notify.Err() + } + + err = w.Write(ctx, notify) + if err != nil { + h.log.Error(err.Error()) + } + } + + return nil +} diff --git a/internal/pkg/delivery/websocket/socket.go b/internal/pkg/delivery/websocket/socket.go new file mode 100644 index 0000000..4269c6a --- /dev/null +++ b/internal/pkg/delivery/websocket/socket.go @@ -0,0 +1,37 @@ +package websocket + +import ( + "context" + + ws "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +type CtxReader interface { + Read(ctx context.Context, v any) error +} + +type CtxWriter interface { + Write(ctx context.Context, v any) error +} + +type CtxReadWriter interface { + CtxReader + CtxWriter +} + +type socketJSON struct { + *ws.Conn +} + +func newSocketJSON(conn *ws.Conn) socketJSON { + return socketJSON{conn} +} + +func (s socketJSON) Write(ctx context.Context, v any) error { + return wsjson.Write(ctx, s.Conn, v) +} + +func (s socketJSON) Read(ctx context.Context, v any) error { + return wsjson.Read(ctx, s.Conn, v) +} diff --git a/internal/pkg/delivery/websocket/types.go b/internal/pkg/delivery/websocket/types.go index b0ff2c9..1271683 100644 --- a/internal/pkg/delivery/websocket/types.go +++ b/internal/pkg/delivery/websocket/types.go @@ -2,26 +2,20 @@ package websocket import "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" -type Channel struct { - Name string `json:"name"` - Topic string `json:"topic"` -} +//go:generate easyjson --all type Object struct { Type string `json:"eventType,omitempty"` Message message.Message `json:"message"` } -type Request struct { - ID int `json:"requestID"` - Action string - Channel Channel - Message Object +type PublishRequest struct { + ID int `json:"requestID"` + Message Object `json:"message"` } type MessageFromChannel struct { Type string `json:"type"` - Channel Channel `json:"channel"` Message ResponseMessage `json:"message"` } @@ -52,10 +46,9 @@ func newResponseOnRequest(id int, status, code, message string, body any) *Respo } } -func newMessageFromChannel(channel Channel, status, code string, v any) *MessageFromChannel { +func newMessageFromChannel(status, code string, v any) *MessageFromChannel { mes := &MessageFromChannel{ - Type: "event", - Channel: channel, + Type: "event", Message: ResponseMessage{ Status: status, Code: code, diff --git a/internal/pkg/delivery/websocket/types_easyjson.go b/internal/pkg/delivery/websocket/types_easyjson.go new file mode 100644 index 0000000..11db088 --- /dev/null +++ b/internal/pkg/delivery/websocket/types_easyjson.go @@ -0,0 +1,451 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package websocket + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(in *jlexer.Lexer, out *ResponseOnRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "requestID": + out.ID = int(in.Int()) + case "type": + out.Type = string(in.String()) + case "status": + out.Status = string(in.String()) + case "code": + out.Code = string(in.String()) + case "message": + out.Message = string(in.String()) + case "body": + if m, ok := out.Body.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := out.Body.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + out.Body = in.Interface() + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(out *jwriter.Writer, in ResponseOnRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"requestID\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"type\":" + out.RawString(prefix) + out.String(string(in.Type)) + } + { + const prefix string = ",\"status\":" + out.RawString(prefix) + out.String(string(in.Status)) + } + if in.Code != "" { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.String(string(in.Code)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + if in.Body != nil { + const prefix string = ",\"body\":" + out.RawString(prefix) + if m, ok := in.Body.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := in.Body.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(in.Body)) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ResponseOnRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ResponseOnRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ResponseOnRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ResponseOnRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(in *jlexer.Lexer, out *ResponseMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "status": + out.Status = string(in.String()) + case "code": + out.Code = string(in.String()) + case "messageText": + out.MessageText = string(in.String()) + case "eventType": + out.Type = string(in.String()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(out *jwriter.Writer, in ResponseMessage) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + if in.Code != "" { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.String(string(in.Code)) + } + if in.MessageText != "" { + const prefix string = ",\"messageText\":" + out.RawString(prefix) + out.String(string(in.MessageText)) + } + if in.Type != "" { + const prefix string = ",\"eventType\":" + out.RawString(prefix) + out.String(string(in.Type)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ResponseMessage) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ResponseMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ResponseMessage) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ResponseMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(in *jlexer.Lexer, out *PublishRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "requestID": + out.ID = int(in.Int()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(out *jwriter.Writer, in PublishRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"requestID\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v PublishRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v PublishRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *PublishRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *PublishRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(in *jlexer.Lexer, out *Object) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "eventType": + out.Type = string(in.String()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(out *jwriter.Writer, in Object) { + out.RawByte('{') + first := true + _ = first + if in.Type != "" { + const prefix string = ",\"eventType\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"message\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Object) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Object) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Object) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Object) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(in *jlexer.Lexer, out *MessageFromChannel) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(out *jwriter.Writer, in MessageFromChannel) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v MessageFromChannel) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v MessageFromChannel) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *MessageFromChannel) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *MessageFromChannel) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(l, v) +} diff --git a/internal/pkg/delivery/websocket/websocket.go b/internal/pkg/delivery/websocket/websocket.go index d6d9753..3b9bf3d 100644 --- a/internal/pkg/delivery/websocket/websocket.go +++ b/internal/pkg/delivery/websocket/websocket.go @@ -6,23 +6,22 @@ import ( "net/http" "time" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" ws "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" - rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) +type notifySubscriber interface { + SubscribeOnAllNotifications(ctx context.Context, userID int) (<-chan *notification.NotifyMessage, error) +} + type HandlerWebSocket struct { originPatterns []string log *log.Logger messageCase usecase.Usecase - client rt.RealTimeClient + notifySub notifySubscriber } type Option func(h *HandlerWebSocket) @@ -35,14 +34,8 @@ func SetOriginPatterns(patterns []string) Option { } } -func New(log *log.Logger, mesCase usecase.Usecase, opts ...Option) *HandlerWebSocket { - gRPCConn, err := grpc.Dial("localhost:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - log.Error(fmt.Errorf("grpc dial: %w", err).Error()) - } - - client := rt.NewRealTimeClient(gRPCConn) - handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, client: client} +func New(log *log.Logger, mesCase usecase.Usecase, notify notifySubscriber, opts ...Option) *HandlerWebSocket { + handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, notifySub: notify} for _, opt := range opts { opt(handlerWS) } @@ -50,178 +43,10 @@ func New(log *log.Logger, mesCase usecase.Usecase, opts ...Option) *HandlerWebSo return handlerWS } -func (h *HandlerWebSocket) WebSocketConnect(w http.ResponseWriter, r *http.Request) { +func (h *HandlerWebSocket) upgradeWSConnect(w http.ResponseWriter, r *http.Request) (*ws.Conn, error) { conn, err := ws.Accept(w, r, &ws.AcceptOptions{OriginPatterns: h.originPatterns}) if err != nil { - h.log.Error(err.Error()) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"status":"error","code":"websocket_connect","message":"fail connect"}`)) - return - } - defer conn.CloseNow() - - userID := r.Context().Value(auth.KeyCurrentUserID).(int) - ctx, cancel := context.WithTimeout(context.Background(), _ctxOnServeConnect) - defer cancel() - - err = h.serveWebSocketConn(ctx, conn, userID) - if err != nil { - h.log.Error(err.Error()) - } -} - -func (h *HandlerWebSocket) serveWebSocketConn(ctx context.Context, conn *ws.Conn, userID int) error { - request := &Request{} - var err error - for { - err = wsjson.Read(ctx, conn, request) - if err != nil { - h.log.Error(err.Error()) - return fmt.Errorf("read message: %w", err) - } - switch request.Action { - case "Publish": - switch request.Message.Type { - case "create": - mesCopy := &message.Message{} - *mesCopy = request.Message.Message - mesCopy.From = userID - id, err := h.messageCase.SendMessage(ctx, userID, mesCopy) - if err != nil { - h.log.Warn(err.Error()) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "publish success", map[string]any{"id": id, "eventType": "create"})) - _, err = h.client.Publish(ctx, &rt.PublishMessage{ - Channel: &rt.Channel{ - Name: request.Channel.Name, - Topic: request.Channel.Topic, - }, - Message: &rt.Message{ - Body: &rt.Message_Object{ - Object: &rt.EventObject{ - Type: rt.EventType_EV_CREATE, - Id: int64(id), - }, - }, - }, - }) - if err != nil { - h.log.Error(err.Error()) - } - case "update": - mesCopy := &message.Message{} - *mesCopy = request.Message.Message - err = h.messageCase.UpdateContentMessage(ctx, userID, mesCopy) - if err != nil { - h.log.Warn(err.Error()) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "publish success", map[string]string{"eventType": "update"})) - _, err = h.client.Publish(ctx, &rt.PublishMessage{ - Channel: &rt.Channel{ - Name: request.Channel.Name, - Topic: request.Channel.Topic, - }, - Message: &rt.Message{ - Body: &rt.Message_Object{ - Object: &rt.EventObject{ - Type: rt.EventType_EV_UPDATE, - Id: int64(request.Message.Message.ID), - }, - }, - }, - }) - if err != nil { - h.log.Error(err.Error()) - } - - case "delete": - err = h.messageCase.DeleteMessage(ctx, userID, request.Message.Message.ID) - if err != nil { - h.log.Warn(err.Error()) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "publish success", map[string]string{"eventType": "delete"})) - _, err = h.client.Publish(ctx, &rt.PublishMessage{ - Channel: &rt.Channel{ - Name: request.Channel.Name, - Topic: request.Channel.Topic, - }, - Message: &rt.Message{ - Body: &rt.Message_Object{ - Object: &rt.EventObject{ - Type: rt.EventType_EV_DELETE, - Id: int64(request.Message.Message.ID), - }, - }, - }, - }) - if err != nil { - h.log.Error(err.Error()) - } - default: - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "error", "unsupported", "unsupported eventType", nil)) - } - case "Subscribe": - err = h.subscribe(ctx, h.client, request, conn, userID) - if err != nil { - h.log.Warn(err.Error()) - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "error", "subscribe_fail", "failed to subscribe to the channel", nil)) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "you have successfully subscribed to the channel", nil)) - default: - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "error", "unsupported", "unsupported action", nil)) - } - } -} - -func (h *HandlerWebSocket) subscribe(ctx context.Context, client rt.RealTimeClient, req *Request, conn *ws.Conn, userID int) error { - sc, err := client.Subscribe(ctx, &rt.Channel{ - Name: req.Channel.Name, - Topic: req.Channel.Topic, - }) - if err != nil { - return fmt.Errorf("subscribe: %w", err) + return nil, fmt.Errorf("upgrade to websocket connect: %w", err) } - go func() { - for { - obj, err := sc.Recv() - if err != nil { - return - } - mes, ok := obj.Body.(*rt.Message_Object) - if ok { - var msg *message.Message - if mes.Object.Type == rt.EventType_EV_DELETE { - msg = &message.Message{ID: int(mes.Object.Id)} - } else { - msg, err = h.messageCase.GetMessage(ctx, userID, int(mes.Object.Id)) - if err != nil { - h.log.Error(err.Error()) - return - } - } - objType := "" - switch mes.Object.Type { - case rt.EventType_EV_CREATE: - objType = "create" - case rt.EventType_EV_UPDATE: - objType = "update" - case rt.EventType_EV_DELETE: - objType = "delete" - } - err = wsjson.Write(ctx, conn, newMessageFromChannel(req.Channel, "ok", "", Object{ - Type: objType, - Message: *msg, - })) - if err != nil { - h.log.Error(err.Error()) - return - } - } - } - }() - return nil + return conn, nil } diff --git a/internal/pkg/entity/board/board.go b/internal/pkg/entity/board/board.go index cf783a5..3ce9b66 100644 --- a/internal/pkg/entity/board/board.go +++ b/internal/pkg/entity/board/board.go @@ -6,6 +6,8 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate easyjson board.go +//easyjson:json type Board struct { ID int `json:"id,omitempty" example:"15"` AuthorID int `json:"author_id,omitempty"` diff --git a/internal/pkg/entity/board/board_easyjson.go b/internal/pkg/entity/board/board_easyjson.go new file mode 100644 index 0000000..7e6adbe --- /dev/null +++ b/internal/pkg/entity/board/board_easyjson.go @@ -0,0 +1,142 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package board + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" + time "time" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(in *jlexer.Lexer, out *Board) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "author_id": + out.AuthorID = int(in.Int()) + case "title": + out.Title = string(in.String()) + case "description": + out.Description = string(in.String()) + case "public": + out.Public = bool(in.Bool()) + case "created_at": + if in.IsNull() { + in.Skip() + out.CreatedAt = nil + } else { + if out.CreatedAt == nil { + out.CreatedAt = new(time.Time) + } + if data := in.Raw(); in.Ok() { + in.AddError((*out.CreatedAt).UnmarshalJSON(data)) + } + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(out *jwriter.Writer, in Board) { + out.RawByte('{') + first := true + _ = first + if in.ID != 0 { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + if in.AuthorID != 0 { + const prefix string = ",\"author_id\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int(int(in.AuthorID)) + } + { + const prefix string = ",\"title\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Title)) + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + { + const prefix string = ",\"public\":" + out.RawString(prefix) + out.Bool(bool(in.Public)) + } + if in.CreatedAt != nil { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.Raw((*in.CreatedAt).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Board) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Board) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Board) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Board) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(l, v) +} diff --git a/internal/pkg/entity/comment/comment.go b/internal/pkg/entity/comment/comment.go new file mode 100644 index 0000000..493e153 --- /dev/null +++ b/internal/pkg/entity/comment/comment.go @@ -0,0 +1,16 @@ +package comment + +import ( + "github.com/jackc/pgx/v5/pgtype" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" +) + +//go:generate easyjson comment.go +//easyjson:json +type Comment struct { + ID int `json:"id"` + Author *user.User `json:"author"` + PinID int `json:"pinID"` + Content pgtype.Text `json:"content"` +} diff --git a/internal/pkg/entity/comment/comment_easyjson.go b/internal/pkg/entity/comment/comment_easyjson.go new file mode 100644 index 0000000..c3bb7ab --- /dev/null +++ b/internal/pkg/entity/comment/comment_easyjson.go @@ -0,0 +1,224 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package comment + +import ( + json "encoding/json" + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(in *jlexer.Lexer, out *Comment) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "author": + if in.IsNull() { + in.Skip() + out.Author = nil + } else { + if out.Author == nil { + out.Author = new(user.User) + } + easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(in, out.Author) + } + case "pinID": + out.PinID = int(in.Int()) + case "content": + if data := in.Raw(); in.Ok() { + in.AddError((out.Content).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(out *jwriter.Writer, in Comment) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"author\":" + out.RawString(prefix) + if in.Author == nil { + out.RawString("null") + } else { + easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(out, *in.Author) + } + } + { + const prefix string = ",\"pinID\":" + out.RawString(prefix) + out.Int(int(in.PinID)) + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.Raw((in.Content).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Comment) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Comment) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Comment) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Comment) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(l, v) +} +func easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(in *jlexer.Lexer, out *user.User) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "username": + out.Username = string(in.String()) + case "name": + if data := in.Raw(); in.Ok() { + in.AddError((out.Name).UnmarshalJSON(data)) + } + case "surname": + if data := in.Raw(); in.Ok() { + in.AddError((out.Surname).UnmarshalJSON(data)) + } + case "email": + out.Email = string(in.String()) + case "avatar": + out.Avatar = string(in.String()) + case "about_me": + if data := in.Raw(); in.Ok() { + in.AddError((out.AboutMe).UnmarshalJSON(data)) + } + case "password": + out.Password = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(out *jwriter.Writer, in user.User) { + out.RawByte('{') + first := true + _ = first + if in.ID != 0 { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Username)) + } + if true { + const prefix string = ",\"name\":" + out.RawString(prefix) + out.Raw((in.Name).MarshalJSON()) + } + if true { + const prefix string = ",\"surname\":" + out.RawString(prefix) + out.Raw((in.Surname).MarshalJSON()) + } + if in.Email != "" { + const prefix string = ",\"email\":" + out.RawString(prefix) + out.String(string(in.Email)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + if true { + const prefix string = ",\"about_me\":" + out.RawString(prefix) + out.Raw((in.AboutMe).MarshalJSON()) + } + if in.Password != "" { + const prefix string = ",\"password\":" + out.RawString(prefix) + out.String(string(in.Password)) + } + out.RawByte('}') +} diff --git a/internal/pkg/entity/message/message.go b/internal/pkg/entity/message/message.go index f6f893d..ba42c98 100644 --- a/internal/pkg/entity/message/message.go +++ b/internal/pkg/entity/message/message.go @@ -6,10 +6,13 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" ) +//go:generate easyjson message.go + type Chat [2]int +//easyjson:json type Message struct { - ID int + ID int `json:"id,omitempty"` From int `json:"from"` To int `json:"to"` Content pgtype.Text `json:"content"` diff --git a/internal/pkg/entity/message/message_easyjson.go b/internal/pkg/entity/message/message_easyjson.go new file mode 100644 index 0000000..91a2ec5 --- /dev/null +++ b/internal/pkg/entity/message/message_easyjson.go @@ -0,0 +1,114 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package message + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(in *jlexer.Lexer, out *Message) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "from": + out.From = int(in.Int()) + case "to": + out.To = int(in.Int()) + case "content": + if data := in.Raw(); in.Ok() { + in.AddError((out.Content).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(out *jwriter.Writer, in Message) { + out.RawByte('{') + first := true + _ = first + if in.ID != 0 { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"from\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int(int(in.From)) + } + { + const prefix string = ",\"to\":" + out.RawString(prefix) + out.Int(int(in.To)) + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.Raw((in.Content).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Message) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Message) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Message) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Message) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(l, v) +} diff --git a/internal/pkg/entity/notification/message.go b/internal/pkg/entity/notification/message.go new file mode 100644 index 0000000..aaac18b --- /dev/null +++ b/internal/pkg/entity/notification/message.go @@ -0,0 +1,26 @@ +package notification + +//go:generate easyjson +//easyjson:json +type NotifyMessage struct { + Type string `json:"type"` + Content string `json:"content"` + err error +} + +func (n *NotifyMessage) Err() error { + return n.err +} + +func NewNotifyMessage(t NotifyType, content string) *NotifyMessage { + return &NotifyMessage{ + Type: TypeString(t), + Content: content, + } +} + +func NewNotifyMessageWithError(err error) *NotifyMessage { + return &NotifyMessage{ + err: err, + } +} diff --git a/internal/pkg/entity/notification/message_easyjson.go b/internal/pkg/entity/notification/message_easyjson.go new file mode 100644 index 0000000..a774aab --- /dev/null +++ b/internal/pkg/entity/notification/message_easyjson.go @@ -0,0 +1,92 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package notification + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(in *jlexer.Lexer, out *NotifyMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "content": + out.Content = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(out *jwriter.Writer, in NotifyMessage) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.String(string(in.Content)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v NotifyMessage) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v NotifyMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *NotifyMessage) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *NotifyMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(l, v) +} diff --git a/internal/pkg/entity/notification/notification.go b/internal/pkg/entity/notification/notification.go new file mode 100644 index 0000000..a21f772 --- /dev/null +++ b/internal/pkg/entity/notification/notification.go @@ -0,0 +1,86 @@ +package notification + +import ( + "bytes" + "fmt" + "sync" + "text/template" +) + +type NotifyType uint8 + +const _defaultCapBuffer = 128 + +const ( + _ NotifyType = iota + NotifyComment + + _notifyCustom +) + +type notify struct { + NotifyType NotifyType + buf *sync.Pool + tmp *template.Template +} + +func NewWithTemplate(tmp *template.Template) notify { + return notify{ + NotifyType: _notifyCustom, + buf: &sync.Pool{ + New: func() any { return bytes.NewBuffer(make([]byte, 0, _defaultCapBuffer)) }, + }, + tmp: tmp, + } +} + +func NewWithType(t NotifyType) (notify, error) { + content, ok := notifyTypeTemplate[t] + if !ok { + return notify{}, fmt.Errorf("new notify with type %s: %w", TypeString(t), ErrUnknownNotifyType) + } + + res := notify{ + NotifyType: t, + buf: &sync.Pool{ + New: func() any { return bytes.NewBuffer(make([]byte, 0, _defaultCapBuffer)) }, + }, + } + + tmp, err := template.New(TypeString(t)).Parse(content) + if err != nil { + return notify{}, fmt.Errorf("new notify with type %s: %w", TypeString(t), err) + } + + res.tmp = tmp + return res, nil +} + +func (n notify) Type() NotifyType { + return n.NotifyType +} + +func (n notify) BuildNotifyMessage(data any) (*NotifyMessage, error) { + content, err := n.FormatContent(data) + if err != nil { + return nil, fmt.Errorf("build notify message: %w", err) + } + + return NewNotifyMessage(n.NotifyType, content), nil +} + +func (n notify) FormatContent(data any) (string, error) { + buf := n.buf.Get().(*bytes.Buffer) + + defer func() { + buf.Reset() + n.buf.Put(buf) + }() + + err := n.tmp.Execute(buf, data) + if err != nil { + return "", fmt.Errorf("") + } + + return buf.String(), nil +} diff --git a/internal/pkg/entity/notification/template.go b/internal/pkg/entity/notification/template.go new file mode 100644 index 0000000..55ad3cb --- /dev/null +++ b/internal/pkg/entity/notification/template.go @@ -0,0 +1,5 @@ +package notification + +var notifyTypeTemplate = map[NotifyType]string{ + NotifyComment: `Пользователь {{.Username}} оставил комментарий под пином "{{.TitlePin}}".`, +} diff --git a/internal/pkg/entity/notification/type.go b/internal/pkg/entity/notification/type.go new file mode 100644 index 0000000..3050265 --- /dev/null +++ b/internal/pkg/entity/notification/type.go @@ -0,0 +1,20 @@ +package notification + +import "errors" + +var ErrUnknownNotifyType = errors.New("unknown notify type") + +func TypeString(t NotifyType) string { + switch t { + case NotifyComment: + return "comment" + case _notifyCustom: + return "custom" + } + + return "" +} + +func NotifyTemplateByType(t NotifyType) string { + return notifyTypeTemplate[t] +} diff --git a/internal/pkg/entity/user/user.go b/internal/pkg/entity/user/user.go index 1b270be..70f2e14 100644 --- a/internal/pkg/entity/user/user.go +++ b/internal/pkg/entity/user/user.go @@ -5,6 +5,8 @@ import ( "github.com/microcosm-cc/bluemonday" ) +const UserUnknown = -1 + type User struct { ID int `json:"id,omitempty" example:"123"` Username string `json:"username" example:"Green"` diff --git a/internal/pkg/notification/comment/comment.go b/internal/pkg/notification/comment/comment.go new file mode 100644 index 0000000..cc7c5c9 --- /dev/null +++ b/internal/pkg/notification/comment/comment.go @@ -0,0 +1,57 @@ +package comment + +import ( + "context" + "fmt" + "strconv" + + comm "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification" +) + +type commentGetter interface { + GetCommentWithAuthor(ctx context.Context, commentID int) (*comm.Comment, error) +} + +type pinGetter interface { + GetPinWithAuthor(ctx context.Context, pinID int) (*pin.Pin, error) +} + +type commentNotify struct { + notification.NotifyBuilder + + com commentGetter + pin pinGetter +} + +func NewCommentNotify(builder notification.NotifyBuilder, com commentGetter, pin pinGetter) commentNotify { + return commentNotify{builder, com, pin} +} + +func (c commentNotify) Type() entity.NotifyType { + return c.NotifyBuilder.Type() +} + +func (c commentNotify) MessageNotify(data notification.M) (*entity.NotifyMessage, error) { + return c.NotifyBuilder.BuildNotifyMessage(data) +} + +func (c commentNotify) ChannelsNameForSubscribe(_ context.Context, userID int) ([]string, error) { + return []string{strconv.Itoa(userID)}, nil +} + +func (c commentNotify) ChannelNameForPublishWithData(ctx context.Context, commentID int) (string, notification.M, error) { + com, err := c.com.GetCommentWithAuthor(ctx, commentID) + if err != nil { + return "", nil, fmt.Errorf("get comment for receive channel name on publish: %w", err) + } + + pin, err := c.pin.GetPinWithAuthor(ctx, com.PinID) + if err != nil { + return "", nil, fmt.Errorf("get pin for receive channel name on publish: %w", err) + } + + return strconv.Itoa(pin.Author.ID), notification.M{"Username": com.Author.Username, "TitlePin": pin.Title.String}, nil +} diff --git a/internal/pkg/notification/notifier.go b/internal/pkg/notification/notifier.go new file mode 100644 index 0000000..5f3bac8 --- /dev/null +++ b/internal/pkg/notification/notifier.go @@ -0,0 +1,27 @@ +package notification + +import ( + "context" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" +) + +type M map[string]string + +type TypeNotifier interface { + Type() entity.NotifyType +} + +type Notifier interface { + TypeNotifier + + ChannelNameForPublishWithData(ctx context.Context, entityID int) (string, M, error) + ChannelsNameForSubscribe(ctx context.Context, userID int) ([]string, error) + MessageNotify(data M) (*entity.NotifyMessage, error) +} + +type NotifyBuilder interface { + TypeNotifier + + BuildNotifyMessage(data any) (*entity.NotifyMessage, error) +} diff --git a/internal/pkg/repository/comment/mock/comment_mock.go b/internal/pkg/repository/comment/mock/comment_mock.go new file mode 100644 index 0000000..21cd598 --- /dev/null +++ b/internal/pkg/repository/comment/mock/comment_mock.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + comment "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// AddComment mocks base method. +func (m *MockRepository) AddComment(ctx context.Context, comment *comment.Comment) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddComment", ctx, comment) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddComment indicates an expected call of AddComment. +func (mr *MockRepositoryMockRecorder) AddComment(ctx, comment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddComment", reflect.TypeOf((*MockRepository)(nil).AddComment), ctx, comment) +} + +// EditStatusCommentOnDeletedByID mocks base method. +func (m *MockRepository) EditStatusCommentOnDeletedByID(ctx context.Context, id int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditStatusCommentOnDeletedByID", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditStatusCommentOnDeletedByID indicates an expected call of EditStatusCommentOnDeletedByID. +func (mr *MockRepositoryMockRecorder) EditStatusCommentOnDeletedByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditStatusCommentOnDeletedByID", reflect.TypeOf((*MockRepository)(nil).EditStatusCommentOnDeletedByID), ctx, id) +} + +// GetCommensToPin mocks base method. +func (m *MockRepository) GetCommensToPin(ctx context.Context, pinID, lastID, count int) ([]comment.Comment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommensToPin", ctx, pinID, lastID, count) + ret0, _ := ret[0].([]comment.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommensToPin indicates an expected call of GetCommensToPin. +func (mr *MockRepositoryMockRecorder) GetCommensToPin(ctx, pinID, lastID, count interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommensToPin", reflect.TypeOf((*MockRepository)(nil).GetCommensToPin), ctx, pinID, lastID, count) +} + +// GetCommentByID mocks base method. +func (m *MockRepository) GetCommentByID(ctx context.Context, id int) (*comment.Comment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommentByID", ctx, id) + ret0, _ := ret[0].(*comment.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommentByID indicates an expected call of GetCommentByID. +func (mr *MockRepositoryMockRecorder) GetCommentByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommentByID", reflect.TypeOf((*MockRepository)(nil).GetCommentByID), ctx, id) +} diff --git a/internal/pkg/repository/comment/queries.go b/internal/pkg/repository/comment/queries.go new file mode 100644 index 0000000..c944664 --- /dev/null +++ b/internal/pkg/repository/comment/queries.go @@ -0,0 +1,19 @@ +package comment + +const ( + InsertNewComment = "INSERT INTO comment (author, pin_id, content) VALUES ($1, $2, $3) RETURNING id;" + + UpdateCommentOnDeleted = "UPDATE comment SET deleted_at = now() WHERE id = $1;" + + SelectCommentByID = `SELECT p.id, p.username, p.avatar, c.pin_id, c.content + FROM comment AS c INNER JOIN profile AS p + ON c.author = p.id + WHERE c.id = $1 AND c.deleted_at IS NULL;` + + SelectCommentsByPinID = `SELECT c.id, p.id, p.username, p.avatar, c.content + FROM comment AS c INNER JOIN profile AS p + ON c.author = p.id + WHERE c.pin_id = $1 AND (c.id < $2 OR $2 = 0) AND c.deleted_at IS NULL + ORDER BY c.id DESC + LIMIT $3;` +) diff --git a/internal/pkg/repository/comment/repo.go b/internal/pkg/repository/comment/repo.go new file mode 100644 index 0000000..cfaf576 --- /dev/null +++ b/internal/pkg/repository/comment/repo.go @@ -0,0 +1,86 @@ +package comment + +import ( + "context" + "errors" + "fmt" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/internal/pgtype" +) + +//go:generate mockgen -destination=./mock/comment_mock.go -package=mock -source=repo.go Repository +type Repository interface { + AddComment(ctx context.Context, comment *entity.Comment) (int, error) + GetCommentByID(ctx context.Context, id int) (*entity.Comment, error) + EditStatusCommentOnDeletedByID(ctx context.Context, id int) error + GetCommensToPin(ctx context.Context, pinID, lastID, count int) ([]entity.Comment, error) +} + +var ErrUserRequired = errors.New("the comment does not have its author specified") + +type commentRepoPG struct { + db pgtype.PgxPoolIface +} + +func NewCommentRepoPG(db pgtype.PgxPoolIface) *commentRepoPG { + return &commentRepoPG{db} +} + +func (c *commentRepoPG) AddComment(ctx context.Context, comment *entity.Comment) (int, error) { + if comment.Author == nil { + return 0, ErrUserRequired + } + + var idInsertedComment int + err := c.db.QueryRow(ctx, InsertNewComment, comment.Author.ID, comment.PinID, comment.Content). + Scan(&idInsertedComment) + if err != nil { + return 0, fmt.Errorf("add comment in storage: %w", err) + } + return idInsertedComment, nil +} + +func (c *commentRepoPG) GetCommentByID(ctx context.Context, id int) (*entity.Comment, error) { + comment := &entity.Comment{ID: id, Author: &user.User{}} + + err := c.db.QueryRow(ctx, SelectCommentByID, id). + Scan(&comment.Author.ID, &comment.Author.Username, &comment.Author.Avatar, &comment.PinID, &comment.Content) + if err != nil { + return nil, fmt.Errorf("get comment by id from storage: %w", err) + } + + return comment, nil +} + +func (c *commentRepoPG) EditStatusCommentOnDeletedByID(ctx context.Context, id int) error { + if _, err := c.db.Exec(ctx, UpdateCommentOnDeleted, id); err != nil { + return fmt.Errorf("edit status comment on deleted comment by id from storage: %w", err) + } + return nil +} + +func (c *commentRepoPG) GetCommensToPin(ctx context.Context, pinID, lastID, count int) ([]entity.Comment, error) { + rows, err := c.db.Query(ctx, SelectCommentsByPinID, pinID, lastID, count) + if err != nil { + return nil, fmt.Errorf("get comments to pin from storage: %w", err) + } + defer rows.Close() + + cmts := make([]entity.Comment, 0, count) + cmt := entity.Comment{ + Author: &user.User{}, + PinID: pinID, + } + + for rows.Next() { + err = rows.Scan(&cmt.ID, &cmt.Author.ID, &cmt.Author.Username, &cmt.Author.Avatar, &cmt.Content) + if err != nil { + return cmts, fmt.Errorf("scan a comment when getting comments on a pin: %w", err) + } + + cmts = append(cmts, cmt) + } + return cmts, nil +} diff --git a/internal/pkg/usecase/comment/check.go b/internal/pkg/usecase/comment/check.go new file mode 100644 index 0000000..a278b68 --- /dev/null +++ b/internal/pkg/usecase/comment/check.go @@ -0,0 +1,29 @@ +package comment + +import ( + "context" + "errors" + "fmt" +) + +var ErrNotAvailableAction = errors.New("action not available for user") + +func (c *commentCase) isAvailableCommentForDelete(ctx context.Context, userID, commentID int) error { + comment, err := c.repo.GetCommentByID(ctx, commentID) + if err != nil { + return fmt.Errorf("get comment for check available comment for delete: %w", err) + } + + if comment.Author.ID == userID { + return nil + } + + authorPinID, err := c.GetAuthorIdOfThePin(ctx, comment.PinID) + if err != nil { + return fmt.Errorf("get author pin for check availabel comment: %w", err) + } + if authorPinID != userID { + return ErrNotAvailableAction + } + return nil +} diff --git a/internal/pkg/usecase/comment/mock/comment_mock.go b/internal/pkg/usecase/comment/mock/comment_mock.go new file mode 100644 index 0000000..92bec48 --- /dev/null +++ b/internal/pkg/usecase/comment/mock/comment_mock.go @@ -0,0 +1,133 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + comment "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// DeleteComment mocks base method. +func (m *MockUsecase) DeleteComment(ctx context.Context, userID, commentID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteComment", ctx, userID, commentID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteComment indicates an expected call of DeleteComment. +func (mr *MockUsecaseMockRecorder) DeleteComment(ctx, userID, commentID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteComment", reflect.TypeOf((*MockUsecase)(nil).DeleteComment), ctx, userID, commentID) +} + +// GetFeedCommentOnPin mocks base method. +func (m *MockUsecase) GetFeedCommentOnPin(ctx context.Context, userID, pinID, count, lastID int) ([]comment.Comment, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeedCommentOnPin", ctx, userID, pinID, count, lastID) + ret0, _ := ret[0].([]comment.Comment) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetFeedCommentOnPin indicates an expected call of GetFeedCommentOnPin. +func (mr *MockUsecaseMockRecorder) GetFeedCommentOnPin(ctx, userID, pinID, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeedCommentOnPin", reflect.TypeOf((*MockUsecase)(nil).GetFeedCommentOnPin), ctx, userID, pinID, count, lastID) +} + +// PutCommentOnPin mocks base method. +func (m *MockUsecase) PutCommentOnPin(ctx context.Context, userID int, comment *comment.Comment) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutCommentOnPin", ctx, userID, comment) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutCommentOnPin indicates an expected call of PutCommentOnPin. +func (mr *MockUsecaseMockRecorder) PutCommentOnPin(ctx, userID, comment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutCommentOnPin", reflect.TypeOf((*MockUsecase)(nil).PutCommentOnPin), ctx, userID, comment) +} + +// MockavailablePinChecker is a mock of availablePinChecker interface. +type MockavailablePinChecker struct { + ctrl *gomock.Controller + recorder *MockavailablePinCheckerMockRecorder +} + +// MockavailablePinCheckerMockRecorder is the mock recorder for MockavailablePinChecker. +type MockavailablePinCheckerMockRecorder struct { + mock *MockavailablePinChecker +} + +// NewMockavailablePinChecker creates a new mock instance. +func NewMockavailablePinChecker(ctrl *gomock.Controller) *MockavailablePinChecker { + mock := &MockavailablePinChecker{ctrl: ctrl} + mock.recorder = &MockavailablePinCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockavailablePinChecker) EXPECT() *MockavailablePinCheckerMockRecorder { + return m.recorder +} + +// GetAuthorIdOfThePin mocks base method. +func (m *MockavailablePinChecker) GetAuthorIdOfThePin(ctx context.Context, pinID int) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthorIdOfThePin", ctx, pinID) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthorIdOfThePin indicates an expected call of GetAuthorIdOfThePin. +func (mr *MockavailablePinCheckerMockRecorder) GetAuthorIdOfThePin(ctx, pinID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorIdOfThePin", reflect.TypeOf((*MockavailablePinChecker)(nil).GetAuthorIdOfThePin), ctx, pinID) +} + +// IsAvailablePinForViewingUser mocks base method. +func (m *MockavailablePinChecker) IsAvailablePinForViewingUser(ctx context.Context, userID, pinID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsAvailablePinForViewingUser", ctx, userID, pinID) + ret0, _ := ret[0].(error) + return ret0 +} + +// IsAvailablePinForViewingUser indicates an expected call of IsAvailablePinForViewingUser. +func (mr *MockavailablePinCheckerMockRecorder) IsAvailablePinForViewingUser(ctx, userID, pinID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAvailablePinForViewingUser", reflect.TypeOf((*MockavailablePinChecker)(nil).IsAvailablePinForViewingUser), ctx, userID, pinID) +} diff --git a/internal/pkg/usecase/comment/usecase.go b/internal/pkg/usecase/comment/usecase.go new file mode 100644 index 0000000..b7f4272 --- /dev/null +++ b/internal/pkg/usecase/comment/usecase.go @@ -0,0 +1,110 @@ +package comment + +import ( + "context" + "fmt" + "time" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + commentRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/notification" +) + +//go:generate mockgen -destination=./mock/comment_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + PutCommentOnPin(ctx context.Context, userID int, comment *entity.Comment) (int, error) + GetFeedCommentOnPin(ctx context.Context, userID, pinID, count, lastID int) ([]entity.Comment, int, error) + DeleteComment(ctx context.Context, userID, commentID int) error + GetCommentWithAuthor(ctx context.Context, commentID int) (*entity.Comment, error) +} + +type availablePinChecker interface { + IsAvailablePinForViewingUser(ctx context.Context, userID, pinID int) error + GetAuthorIdOfThePin(ctx context.Context, pinID int) (int, error) +} + +const _timeoutNotification = 5 * time.Minute + +type commentCase struct { + availablePinChecker + + notifyCase notification.Usecase + repo commentRepo.Repository + + notifyIsEnable bool +} + +func New(repo commentRepo.Repository, checker availablePinChecker, notifyCase notification.Usecase) *commentCase { + comCase := &commentCase{ + availablePinChecker: checker, + repo: repo, + notifyCase: notifyCase, + } + + if notifyCase != nil { + comCase.notifyIsEnable = true + } + return comCase +} + +func (c *commentCase) PutCommentOnPin(ctx context.Context, userID int, comment *entity.Comment) (int, error) { + err := c.IsAvailablePinForViewingUser(ctx, userID, comment.PinID) + if err != nil { + return 0, fmt.Errorf("put comment on not available pin: %w", err) + } + + comment.Author = &user.User{ID: userID} + + id, err := c.repo.AddComment(ctx, comment) + if err != nil { + return 0, fmt.Errorf("put comment on available pin: %w", err) + } + + if c.notifyIsEnable { + ctx, _ = context.WithTimeout(context.Background(), _timeoutNotification) + go c.notifyCase.NotifyCommentLeftOnPin(ctx, id) + } + + return id, nil +} + +func (c *commentCase) GetFeedCommentOnPin(ctx context.Context, userID, pinID, count, lastID int) ([]entity.Comment, int, error) { + err := c.IsAvailablePinForViewingUser(ctx, userID, pinID) + if err != nil { + return nil, 0, fmt.Errorf("put comment on not available pin: %w", err) + } + + feed, err := c.repo.GetCommensToPin(ctx, pinID, lastID, count) + if err != nil { + err = fmt.Errorf("get feed comment on pin: %w", err) + } + + var newLastID int + if len(feed) > 0 { + newLastID = feed[len(feed)-1].ID + } + return feed, newLastID, err +} + +func (c *commentCase) DeleteComment(ctx context.Context, userID, commentID int) error { + err := c.isAvailableCommentForDelete(ctx, userID, commentID) + if err != nil { + return fmt.Errorf("check available delete comment: %w", err) + } + + err = c.repo.EditStatusCommentOnDeletedByID(ctx, commentID) + if err != nil { + return fmt.Errorf("delete comment: %w", err) + } + return nil +} + +func (c *commentCase) GetCommentWithAuthor(ctx context.Context, commentID int) (*entity.Comment, error) { + comment, err := c.repo.GetCommentByID(ctx, commentID) + if err != nil { + return nil, fmt.Errorf("get comment with author: %w", err) + } + + return comment, nil +} diff --git a/internal/pkg/usecase/message/mock/message_mock.go b/internal/pkg/usecase/message/mock/message_mock.go index 18f0333..3b6d816 100644 --- a/internal/pkg/usecase/message/mock/message_mock.go +++ b/internal/pkg/usecase/message/mock/message_mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" message "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + message0 "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" gomock "github.com/golang/mock/gomock" ) @@ -36,17 +37,17 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { } // DeleteMessage mocks base method. -func (m *MockUsecase) DeleteMessage(ctx context.Context, userID, mesID int) error { +func (m *MockUsecase) DeleteMessage(ctx context.Context, userID int, mes *message.Message) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteMessage", ctx, userID, mesID) + ret := m.ctrl.Call(m, "DeleteMessage", ctx, userID, mes) ret0, _ := ret[0].(error) return ret0 } // DeleteMessage indicates an expected call of DeleteMessage. -func (mr *MockUsecaseMockRecorder) DeleteMessage(ctx, userID, mesID interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) DeleteMessage(ctx, userID, mes interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockUsecase)(nil).DeleteMessage), ctx, userID, mesID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockUsecase)(nil).DeleteMessage), ctx, userID, mes) } // GetMessage mocks base method. @@ -111,6 +112,21 @@ func (mr *MockUsecaseMockRecorder) SendMessage(ctx, userID, mes interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockUsecase)(nil).SendMessage), ctx, userID, mes) } +// SubscribeUserToAllChats mocks base method. +func (m *MockUsecase) SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan message0.EventMessage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeUserToAllChats", ctx, userID) + ret0, _ := ret[0].(<-chan message0.EventMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeUserToAllChats indicates an expected call of SubscribeUserToAllChats. +func (mr *MockUsecaseMockRecorder) SubscribeUserToAllChats(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeUserToAllChats", reflect.TypeOf((*MockUsecase)(nil).SubscribeUserToAllChats), ctx, userID) +} + // UpdateContentMessage mocks base method. func (m *MockUsecase) UpdateContentMessage(ctx context.Context, userID int, mes *message.Message) error { m.ctrl.T.Helper() diff --git a/internal/pkg/usecase/message/usecase.go b/internal/pkg/usecase/message/usecase.go index 4cf8794..552d8c8 100644 --- a/internal/pkg/usecase/message/usecase.go +++ b/internal/pkg/usecase/message/usecase.go @@ -12,9 +12,13 @@ import ( mess "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" messMS "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/messenger/delivery/grpc" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/chat" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) var ErrNoAccess = errors.New("there is no access to perform this action") +var ErrRealTimeDisable = errors.New("realtime disable") +var ErrUnknowObj = errors.New("unknow object") //go:generate mockgen -destination=./mock/message_mock.go -package=mock -source=usecase.go Usecase type Usecase interface { @@ -22,16 +26,42 @@ type Usecase interface { SendMessage(ctx context.Context, userID int, mes *entity.Message) (int, error) GetMessagesFromChat(ctx context.Context, userID int, chat entity.Chat, count, lastID int) (feed []entity.Message, newLastID int, err error) UpdateContentMessage(ctx context.Context, userID int, mes *entity.Message) error - DeleteMessage(ctx context.Context, userID, mesID int) error + DeleteMessage(ctx context.Context, userID int, mes *entity.Message) error GetMessage(ctx context.Context, userID int, messageID int) (*entity.Message, error) + SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan EventMessage, error) +} + +const _topicChat = "chat" + +type EventMessage struct { + Type string + Message *entity.Message + Err error +} + +func makeErrEventMessage(err error) EventMessage { + return EventMessage{Err: err} } type messageCase struct { - client mess.MessengerClient + client mess.MessengerClient + realtimeChatCase chat.Usecase + log *logger.Logger + realtimeIsEnable bool } -func New(repo mess.MessengerClient) *messageCase { - return &messageCase{repo} +func New(log *logger.Logger, cl mess.MessengerClient, rtChatCase chat.Usecase) *messageCase { + m := &messageCase{ + client: cl, + log: log, + } + + if rtChatCase != nil { + m.realtimeChatCase = rtChatCase + m.realtimeIsEnable = true + } + + return m } func (m *messageCase) SendMessage(ctx context.Context, userID int, mes *entity.Message) (int, error) { @@ -43,6 +73,11 @@ func (m *messageCase) SendMessage(ctx context.Context, userID int, mes *entity.M if err != nil { return 0, fmt.Errorf("send message by grpc client") } + + if m.realtimeIsEnable { + go m.realtimeChatCase.PublishNewMessage(ctx, mes.To, int(msgID.GetId())) + } + return int(msgID.GetId()), nil } @@ -74,13 +109,23 @@ func (m *messageCase) UpdateContentMessage(ctx context.Context, userID int, mes }); err != nil { return fmt.Errorf("update messege by grpc client") } + + if m.realtimeIsEnable { + go m.realtimeChatCase.PublishUpdateMessage(ctx, mes.To, mes.ID) + } + return nil } -func (m *messageCase) DeleteMessage(ctx context.Context, userID, mesID int) error { - if _, err := m.client.DeleteMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.MsgID{Id: int64(mesID)}); err != nil { +func (m *messageCase) DeleteMessage(ctx context.Context, userID int, mes *entity.Message) error { + if _, err := m.client.DeleteMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.MsgID{Id: int64(mes.ID)}); err != nil { return fmt.Errorf("delete messege by grpc client") } + + if m.realtimeIsEnable { + go m.realtimeChatCase.PublishDeleteMessage(ctx, mes.To, mes.ID) + } + return nil } @@ -115,6 +160,50 @@ func (m *messageCase) GetUserChatsWithOtherUsers(ctx context.Context, userID, co return convertFeedChat(feed), int(feed.GetLastID()), errRes } +func (m *messageCase) SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan EventMessage, error) { + if !m.realtimeIsEnable { + return nil, ErrRealTimeDisable + } + + subClient, err := m.realtimeChatCase.SubscribeUserToAllChats(ctx, userID) + if err != nil { + return nil, fmt.Errorf("subscribe: %w", err) + } + + chanEvMsg := make(chan EventMessage) + go m.receiveFromSubClient(ctx, userID, subClient, chanEvMsg) + return chanEvMsg, nil +} + +func (m *messageCase) receiveFromSubClient(ctx context.Context, userID int, subClient <-chan chat.EventMessageObjectID, chanEvMsg chan<- EventMessage) { + defer close(chanEvMsg) + + var ( + evMsg EventMessage + err error + ) + for msgObjID := range subClient { + if msgObjID.Err != nil { + chanEvMsg <- makeErrEventMessage(fmt.Errorf("receive from subcribtion client: %w", msgObjID.Err)) + return + } + + evMsg = EventMessage{ + Type: msgObjID.Type, + } + if evMsg.Type == "delete" { + evMsg.Message = &entity.Message{ID: msgObjID.MessageID} + } else { + evMsg.Message, err = m.GetMessage(ctx, userID, msgObjID.MessageID) + if err != nil { + m.log.Error(err.Error()) + } + } + + chanEvMsg <- evMsg + } +} + func setAuthenticatedMetadataCtx(ctx context.Context, userID int) context.Context { return metadata.AppendToOutgoingContext(ctx, messMS.AuthenticatedMetadataKey, strconv.FormatInt(int64(userID), 10)) } diff --git a/internal/pkg/usecase/pin/check.go b/internal/pkg/usecase/pin/check.go index 396a45d..4d0a751 100644 --- a/internal/pkg/usecase/pin/check.go +++ b/internal/pkg/usecase/pin/check.go @@ -6,6 +6,7 @@ import ( "fmt" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" ) var ( @@ -18,8 +19,6 @@ var ( const MaxSizeBatchPin = 100 -const UserUnknown = -1 - func (p *pinCase) IsAvailablePinForFixOnBoard(ctx context.Context, pinID, userID int) error { pin, err := p.repo.GetPinByID(ctx, pinID, false) if err != nil { @@ -55,7 +54,7 @@ func (p *pinCase) isAvailablePinForViewingUser(ctx context.Context, pin *entity. if pin.Public || pin.Author.ID == userID { return nil } - if userID == UserUnknown { + if userID == user.UserUnknown { return ErrPinNotAccess } @@ -71,9 +70,13 @@ func (p *pinCase) isAvailablePinForViewingUser(ctx context.Context, pin *entity. } func (p *pinCase) isAvailablePinForSetLike(ctx context.Context, pinID, userID int) error { + return p.IsAvailablePinForViewingUser(ctx, userID, pinID) +} + +func (p *pinCase) IsAvailablePinForViewingUser(ctx context.Context, userID, pinID int) error { pin, err := p.repo.GetPinByID(ctx, pinID, false) if err != nil { - return fmt.Errorf("get a pin to check for the availability of a like: %w", err) + return fmt.Errorf("get a pin to check for the availability: %w", err) } return p.isAvailablePinForViewingUser(ctx, pin, userID) diff --git a/internal/pkg/usecase/pin/usecase.go b/internal/pkg/usecase/pin/usecase.go index 6385475..3a23401 100644 --- a/internal/pkg/usecase/pin/usecase.go +++ b/internal/pkg/usecase/pin/usecase.go @@ -8,6 +8,7 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" repo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/pin" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/image" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" @@ -97,9 +98,26 @@ func (p *pinCase) ViewFeedPin(ctx context.Context, userID int, cfg pin.FeedPinCo return pin.FeedPin{}, ErrForbiddenAction } - if !hasBoard && (userID == UserUnknown || !hasUser || userID != user) && cfg.Protection != pin.FeedProtectionPublic { + if !hasBoard && (userID == userEntity.UserUnknown || !hasUser || userID != user) && cfg.Protection != pin.FeedProtectionPublic { return pin.FeedPin{}, ErrForbiddenAction } return p.repo.GetFeedPins(ctx, cfg) } + +func (p *pinCase) GetAuthorIdOfThePin(ctx context.Context, pinID int) (int, error) { + user, err := p.repo.GetAuthorPin(ctx, pinID) + if err != nil { + return 0, fmt.Errorf("get author id of the pin: %w", err) + } + return user.ID, nil +} + +func (p *pinCase) GetPinWithAuthor(ctx context.Context, pinID int) (*pin.Pin, error) { + pin, err := p.repo.GetPinByID(ctx, pinID, true) + if err != nil { + return nil, fmt.Errorf("get a pin with author: %w", err) + } + + return pin, nil +} diff --git a/internal/pkg/usecase/realtime/chat/chat.go b/internal/pkg/usecase/realtime/chat/chat.go new file mode 100644 index 0000000..7514d2a --- /dev/null +++ b/internal/pkg/usecase/realtime/chat/chat.go @@ -0,0 +1,114 @@ +package chat + +import ( + "context" + "fmt" + "strconv" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +type EventMessageObjectID struct { + Type string + MessageID int + Err error +} + +func makeErrEventMessageObjectID(err error) EventMessageObjectID { + return EventMessageObjectID{Err: err} +} + +type Usecase interface { + PublishNewMessage(ctx context.Context, userToWhom, msgID int) error + PublishUpdateMessage(ctx context.Context, userToWhom, msgID int) error + PublishDeleteMessage(ctx context.Context, userToWhom, msgID int) error + SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan EventMessageObjectID, error) +} + +type realtimeCase struct { + client realtime.RealTimeClient + log *logger.Logger +} + +func New(client realtime.RealTimeClient, log *logger.Logger) *realtimeCase { + return &realtimeCase{client, log} +} + +func (r *realtimeCase) PublishNewMessage(ctx context.Context, userToWhom, msgID int) error { + err := r.publishMessage(ctx, userToWhom, msgID, rt.EventType_EV_CREATE) + if err != nil { + r.log.Error(err.Error()) + return fmt.Errorf("publish new message: %w", err) + } + return nil +} + +func (r *realtimeCase) PublishUpdateMessage(ctx context.Context, userToWhom, msgID int) error { + err := r.publishMessage(ctx, userToWhom, msgID, rt.EventType_EV_UPDATE) + if err != nil { + r.log.Error(err.Error()) + return fmt.Errorf("publish update message: %w", err) + } + return nil +} + +func (r *realtimeCase) PublishDeleteMessage(ctx context.Context, userToWhom, msgID int) error { + err := r.publishMessage(ctx, userToWhom, msgID, rt.EventType_EV_DELETE) + if err != nil { + r.log.Error(err.Error()) + return fmt.Errorf("publish delete message: %w", err) + } + return nil +} + +func (r *realtimeCase) SubscribeUserToAllChats(ctx context.Context, userToWhom int) (<-chan EventMessageObjectID, error) { + chPack, err := r.client.Subscribe(ctx, []string{strconv.Itoa(userToWhom)}) + if err != nil { + return nil, fmt.Errorf("subscribe user to all chats: %w", err) + } + + chanEvMsg := make(chan EventMessageObjectID) + go r.receiveFromSubClient(ctx, chPack, chanEvMsg) + + return chanEvMsg, nil +} + +func (r *realtimeCase) receiveFromSubClient(ctx context.Context, subClient <-chan realtime.Pack, chanEvMsg chan<- EventMessageObjectID) { + defer close(chanEvMsg) + + for pack := range subClient { + if pack.Err != nil { + chanEvMsg <- makeErrEventMessageObjectID(pack.Err) + return + } + + msg, ok := pack.Body.(*rt.Message_Object) + if !ok { + chanEvMsg <- makeErrEventMessageObjectID(realtime.ErrUnknownTypeObject) + return + } + + evMsgID := EventMessageObjectID{MessageID: int(msg.Object.GetId())} + switch msg.Object.GetType() { + case rt.EventType_EV_CREATE: + evMsgID.Type = "create" + case rt.EventType_EV_UPDATE: + evMsgID.Type = "update" + case rt.EventType_EV_DELETE: + evMsgID.Type = "delete" + } + + chanEvMsg <- evMsgID + } +} + +func (r *realtimeCase) publishMessage(ctx context.Context, userID, msgID int, t rt.EventType) error { + return r.client.Publish(ctx, strconv.Itoa(userID), &rt.Message_Object{ + Object: &rt.EventObject{ + Type: t, + Id: int64(msgID), + }, + }) +} diff --git a/internal/pkg/usecase/realtime/notification/comment.go b/internal/pkg/usecase/realtime/notification/comment.go new file mode 100644 index 0000000..0208aad --- /dev/null +++ b/internal/pkg/usecase/realtime/notification/comment.go @@ -0,0 +1,36 @@ +package notification + +import ( + "context" + "fmt" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" +) + +func (n *notificationClient) NotifyCommentLeftOnPin(ctx context.Context, commentID int) error { + notifier, ok := n.notifiers[entity.NotifyComment] + if !ok { + n.log.Error(ErrNotifierNotRegistered.Error()) + return ErrNotifierNotRegistered + } + + chanName, data, err := notifier.ChannelNameForPublishWithData(ctx, commentID) + if err != nil { + n.log.Error(err.Error()) + return fmt.Errorf("notify comment left on pin: %w", err) + } + + err = n.client.Publish(ctx, chanName, &rt.Message_Content{ + Content: &rt.EventMap{ + Type: int64(entity.NotifyComment), + M: data, + }, + }) + if err != nil { + n.log.Error(err.Error()) + return fmt.Errorf("publish to client: %w", err) + } + + return nil +} diff --git a/internal/pkg/usecase/realtime/notification/notification.go b/internal/pkg/usecase/realtime/notification/notification.go new file mode 100644 index 0000000..57d5ae2 --- /dev/null +++ b/internal/pkg/usecase/realtime/notification/notification.go @@ -0,0 +1,101 @@ +package notification + +import ( + "context" + "errors" + "fmt" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" + notify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +var ErrNotifierNotRegistered = errors.New("notifier with this type not registered") + +type Usecase interface { + NotifyCommentLeftOnPin(ctx context.Context, commentID int) error +} + +type notificationClient struct { + client realtime.RealTimeClient + log *logger.Logger + notifiers map[entity.NotifyType]notify.Notifier +} + +func New(cl realtime.RealTimeClient, log *logger.Logger, opts ...Option) *notificationClient { + client := ¬ificationClient{ + client: cl, + log: log, + notifiers: make(map[entity.NotifyType]notify.Notifier), + } + + for _, opt := range opts { + opt.apply(client) + } + + return client +} + +func (n *notificationClient) SubscribeOnAllNotifications(ctx context.Context, userID int) (<-chan *entity.NotifyMessage, error) { + setChans := make(map[string]struct{}) + for t, notifier := range n.notifiers { + nameChans, err := notifier.ChannelsNameForSubscribe(ctx, userID) + if err != nil { + return nil, fmt.Errorf("receiving name channels for subscribe on %s notifier: %w", entity.TypeString(t), err) + } + + for _, name := range nameChans { + setChans[name] = struct{}{} + } + } + + uniqChans := make([]string, 0, len(setChans)) + + for nameChan := range setChans { + uniqChans = append(uniqChans, nameChan) + } + + chanPack, err := n.client.Subscribe(ctx, uniqChans) + if err != nil { + return nil, fmt.Errorf("subscribe on all notifications: %w", err) + } + + chanNotifyMsg := make(chan *entity.NotifyMessage) + + go n.pipelineNotify(chanPack, chanNotifyMsg) + + return chanNotifyMsg, nil +} + +func (n *notificationClient) pipelineNotify(chRecv <-chan realtime.Pack, chSend chan<- *entity.NotifyMessage) { + defer close(chSend) + + for pack := range chRecv { + if pack.Err != nil { + chSend <- entity.NewNotifyMessageWithError(pack.Err) + return + } + + notifyData, ok := pack.Body.(*rt.Message_Content) + if !ok { + chSend <- entity.NewNotifyMessageWithError(realtime.ErrUnknownTypeObject) + return + } + + notifier, ok := n.notifiers[entity.NotifyType(notifyData.Content.GetType())] + if !ok { + chSend <- entity.NewNotifyMessageWithError(ErrNotifierNotRegistered) + return + } + + msg, err := notifier.MessageNotify(notifyData.Content.GetM()) + if err != nil { + chSend <- entity.NewNotifyMessageWithError(err) + return + } + + chSend <- msg + } +} diff --git a/internal/pkg/usecase/realtime/notification/option.go b/internal/pkg/usecase/realtime/notification/option.go new file mode 100644 index 0000000..0978634 --- /dev/null +++ b/internal/pkg/usecase/realtime/notification/option.go @@ -0,0 +1,19 @@ +package notification + +import notify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification" + +type Option interface { + apply(*notificationClient) +} + +type funcOption func(*notificationClient) + +func (f funcOption) apply(cl *notificationClient) { + f(cl) +} + +func Register(notifier notify.Notifier) Option { + return funcOption(func(cl *notificationClient) { + cl.notifiers[notifier.Type()] = notifier + }) +} diff --git a/internal/pkg/usecase/realtime/realtime.go b/internal/pkg/usecase/realtime/realtime.go new file mode 100644 index 0000000..e5af26f --- /dev/null +++ b/internal/pkg/usecase/realtime/realtime.go @@ -0,0 +1,109 @@ +package realtime + +import ( + "context" + "errors" + "fmt" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" +) + +var ErrUnknownTypeObject = errors.New("unknown type") + +const ( + _topicChat = "chat" + _topicNotification = "notification" +) + +type RealTimeClient interface { + Subscribe(ctx context.Context, nameChans []string) (<-chan Pack, error) + Publish(ctx context.Context, chanName string, object any) error +} + +type Pack struct { + Body any + Err error +} + +type realtimeClient struct { + client rt.RealTimeClient + topic string +} + +func NewRealTimeChatClient(client rt.RealTimeClient) realtimeClient { + return realtimeClient{ + client: client, + topic: _topicChat, + } +} + +func NewRealTimeNotificationClient(client rt.RealTimeClient) realtimeClient { + return realtimeClient{ + client: client, + topic: _topicNotification, + } +} + +func (r realtimeClient) Publish(ctx context.Context, chanName string, object any) error { + pubMsg := &rt.PublishMessage{ + Channel: &rt.Channel{ + Topic: r.topic, + Name: chanName, + }, + Message: &rt.Message{}, + } + + switch body := object.(type) { + case *rt.Message_Object: + pubMsg.Message.Body = body + case *rt.Message_Content: + pubMsg.Message.Body = body + default: + return ErrUnknownTypeObject + } + + _, err := r.client.Publish(ctx, pubMsg) + if err != nil { + return fmt.Errorf("publish as a realtime client: %w", err) + } + return nil +} + +func (r realtimeClient) Subscribe(ctx context.Context, nameChans []string) (<-chan Pack, error) { + chans := &rt.Channels{ + Chans: make([]*rt.Channel, len(nameChans)), + } + + for _, name := range nameChans { + chans.Chans = append(chans.Chans, &rt.Channel{Topic: r.topic, Name: name}) + } + + subClient, err := r.client.Subscribe(ctx, chans) + if err != nil { + return nil, fmt.Errorf("subscribe as a realtime client: %w", err) + } + + ch := make(chan Pack) + go runServeSubscribeClient(subClient, ch) + + return ch, nil +} + +func runServeSubscribeClient(client rt.RealTime_SubscribeClient, ch chan<- Pack) { + defer close(ch) + + var ( + mes *rt.Message + err error + ) + + for { + mes, err = client.Recv() + if err != nil { + ch <- Pack{Err: err} + return + } + + ch <- Pack{Body: mes.GetBody()} + } +}