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/cmd/app/main.go b/cmd/app/main.go index 3d4dbb6..65db49a 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -2,12 +2,18 @@ package main import ( "context" + "flag" "fmt" "github.com/go-park-mail-ru/2023_2_OND_team/internal/app" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) +var ( + logOutput = flag.String("log", "stdout", "file paths to write logging output to") + logErrorOutput = flag.String("logerror", "stderr", "path to write internal logger errors to.") +) + // @title Pinspire API // @version 1.0 // @description API for Pinspire project @@ -21,10 +27,15 @@ import ( // @license.url http://www.apache.org/licenses/LICENSE-2.0.html func main() { + flag.Parse() ctxBase, cancel := context.WithCancel(context.Background()) defer cancel() - log, err := logger.New(logger.RFC3339FormatTime()) + log, err := logger.New( + logger.RFC3339FormatTime(), + logger.SetOutputPaths(*logOutput), + logger.SetErrorOutputPaths(*logErrorOutput), + ) if err != nil { fmt.Println(err) return 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/go.sum b/go.sum index c53b878..1ec2c4a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -51,6 +53,7 @@ github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK github.com/go-gorp/gorp v2.0.0+incompatible h1:dIQPsBtl6/H1MjVseWuWPXa7ET4p6Dve4j3Hg+UjqYw= github.com/go-gorp/gorp v2.0.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -119,6 +122,9 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= @@ -145,6 +151,9 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pashagolub/pgxmock/v2 v2.12.0 h1:IVRmQtVFNCoq7NOZ+PdfvB6fwnLJmEuWDhnc3yrDxBs= github.com/pashagolub/pgxmock/v2 v2.12.0/go.mod h1:D3YslkN/nJ4+umVqWmbwfSXugJIjPMChkGBG47OJpNw= 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 5d3d18f..a561039 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -12,12 +12,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" @@ -25,9 +29,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" @@ -66,8 +74,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 { @@ -80,14 +109,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/messenger/usecase/message/mock/message_mock.go b/internal/microservices/messenger/usecase/message/mock/message_mock.go index 449be90..65e877a 100644 --- a/internal/microservices/messenger/usecase/message/mock/message_mock.go +++ b/internal/microservices/messenger/usecase/message/mock/message_mock.go @@ -49,6 +49,21 @@ func (mr *MockUsecaseMockRecorder) DeleteMessage(ctx, userID, mesID interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockUsecase)(nil).DeleteMessage), ctx, userID, mesID) } +// GetMessage mocks base method. +func (m *MockUsecase) GetMessage(ctx context.Context, messageID int) (*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessage", ctx, messageID) + ret0, _ := ret[0].(*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMessage indicates an expected call of GetMessage. +func (mr *MockUsecaseMockRecorder) GetMessage(ctx, messageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockUsecase)(nil).GetMessage), ctx, messageID) +} + // GetMessagesFromChat mocks base method. func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, chat message.Chat, count, lastID int) ([]message.Message, int, error) { m.ctrl.T.Helper() @@ -65,6 +80,22 @@ func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, chat, count, lastID return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessagesFromChat", reflect.TypeOf((*MockUsecase)(nil).GetMessagesFromChat), ctx, chat, count, lastID) } +// GetUserChatsWithOtherUsers mocks base method. +func (m *MockUsecase) GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (message.FeedUserChats, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserChatsWithOtherUsers", ctx, userID, count, lastID) + ret0, _ := ret[0].(message.FeedUserChats) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetUserChatsWithOtherUsers indicates an expected call of GetUserChatsWithOtherUsers. +func (mr *MockUsecaseMockRecorder) GetUserChatsWithOtherUsers(ctx, userID, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserChatsWithOtherUsers", reflect.TypeOf((*MockUsecase)(nil).GetUserChatsWithOtherUsers), ctx, userID, count, lastID) +} + // SendMessage mocks base method. func (m *MockUsecase) SendMessage(ctx context.Context, mes *message.Message) (int, error) { m.ctrl.T.Helper() 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/board.go b/internal/pkg/delivery/http/v1/board.go index 910062b..f55db48 100644 --- a/internal/pkg/delivery/http/v1/board.go +++ b/internal/pkg/delivery/http/v1/board.go @@ -2,57 +2,23 @@ package v1 import ( "encoding/json" - "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/board" + "github.com/mailru/easyjson" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) var TimeFormat = "2006-01-02" -// data for board creation/update -type BoardData struct { - Title *string `json:"title" example:"new board"` - Description *string `json:"description" example:"long desc"` - Public *bool `json:"public" example:"true"` - Tags []string `json:"tags" example:"['blue', 'car']"` -} - -// board view for delivery layer -type CertainBoard struct { - ID int `json:"board_id" example:"22"` - AuthorID int `json:"author_id" example:"22"` - Title string `json:"title" example:"new board"` - Description string `json:"description" example:"long desc"` - CreatedAt string `json:"created_at" example:"07-11-2023"` - PinsNumber int `json:"pins_number" example:"12"` - Pins []string `json:"pins" example:"['/pic1', '/pic2']"` - Tags []string `json:"tags" example:"['love', 'green']"` -} - -type CertainBoardWithUsername struct { - ID int `json:"board_id" example:"22"` - AuthorID int `json:"author_id" example:"22"` - AuthorUsername string `json:"author_username" example:"Bob"` - Title string `json:"title" example:"new board"` - Description string `json:"description" example:"long desc"` - CreatedAt string `json:"created_at" example:"07-11-2023"` - PinsNumber int `json:"pins_number" example:"12"` - Pins []string `json:"pins" example:"['/pic1', '/pic2']"` - Tags []string `json:"tags" example:"['love', 'green']"` -} - -type DeletePinFromBoard struct { - PinID int `json:"pin_id" example:"22"` -} - -func ToCertainBoardFromService(board entity.BoardWithContent) CertainBoard { - return CertainBoard{ +func ToCertainBoardFromService(board entity.BoardWithContent) structs.CertainBoard { + return structs.CertainBoard{ ID: board.BoardInfo.ID, AuthorID: board.BoardInfo.AuthorID, Title: board.BoardInfo.Title, @@ -64,8 +30,8 @@ func ToCertainBoardFromService(board entity.BoardWithContent) CertainBoard { } } -func ToCertainBoardUsernameFromService(board entity.BoardWithContent, username string) CertainBoardWithUsername { - return CertainBoardWithUsername{ +func ToCertainBoardUsernameFromService(board entity.BoardWithContent, username string) structs.CertainBoardWithUsername { + return structs.CertainBoardWithUsername{ ID: board.BoardInfo.ID, AuthorID: board.BoardInfo.AuthorID, AuthorUsername: username, @@ -78,40 +44,20 @@ func ToCertainBoardUsernameFromService(board entity.BoardWithContent, username s } } -func (data *BoardData) Validate() error { - if data.Title == nil || *data.Title == "" { - return ErrInvalidBoardTitle - } - if data.Description == nil { - data.Description = new(string) - *data.Description = "" - } - if data.Public == nil { - return ErrEmptyPubOpt - } - if !isValidBoardTitle(*data.Title) { - return ErrInvalidBoardTitle - } - if err := checkIsValidTagTitles(data.Tags); err != nil { - return fmt.Errorf("%s: %w", err.Error(), ErrInvalidTagTitles) - } - return nil -} - func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } - var newBoard BoardData - err := json.NewDecoder(r.Body).Decode(&newBoard) + var newBoard structs.BoardData + err := easyjson.UnmarshalFromReader(r.Body, &newBoard) defer r.Body.Close() if err != nil { logger.Info("create board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadBody) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) responseError(w, code, message) return } @@ -119,7 +65,7 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { err = newBoard.Validate() if err != nil { logger.Info("create board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -140,7 +86,7 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Info("create board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -149,7 +95,7 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -158,8 +104,8 @@ func (h *HandlerHTTP) GetUserBoards(w http.ResponseWriter, r *http.Request) { username := chi.URLParam(r, "username") if !isValidUsername(username) { - logger.Info("update board", log.F{"message", ErrInvalidUsername.Error()}) - code, message := getErrCodeMessage(ErrInvalidUsername) + logger.Info("update board", log.F{"message", errHTTP.ErrInvalidUsername.Error()}) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrInvalidUsername) responseError(w, code, message) return } @@ -167,12 +113,12 @@ func (h *HandlerHTTP) GetUserBoards(w http.ResponseWriter, r *http.Request) { boards, err := h.boardCase.GetBoardsByUsername(r.Context(), username) if err != nil { logger.Info("get user boards", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } - userBoards := make([]CertainBoard, 0, len(boards)) + userBoards := make([]structs.CertainBoard, 0, len(boards)) for _, board := range boards { userBoards = append(userBoards, ToCertainBoardFromService(board)) } @@ -180,7 +126,7 @@ func (h *HandlerHTTP) GetUserBoards(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -190,7 +136,7 @@ func (h *HandlerHTTP) GetCertainBoard(w http.ResponseWriter, r *http.Request) { boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("get certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } @@ -198,7 +144,7 @@ func (h *HandlerHTTP) GetCertainBoard(w http.ResponseWriter, r *http.Request) { board, username, err := h.boardCase.GetCertainBoard(r.Context(), int(boardID)) if err != nil { logger.Info("get certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -207,7 +153,7 @@ func (h *HandlerHTTP) GetCertainBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -217,7 +163,7 @@ func (h *HandlerHTTP) GetBoardInfoForUpdate(w http.ResponseWriter, r *http.Reque boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("get certain board info for update", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } @@ -225,7 +171,7 @@ func (h *HandlerHTTP) GetBoardInfoForUpdate(w http.ResponseWriter, r *http.Reque board, tagTitles, err := h.boardCase.GetBoardInfoForUpdate(r.Context(), int(boardID)) if err != nil { logger.Info("get certain board info for update", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -234,14 +180,14 @@ func (h *HandlerHTTP) GetBoardInfoForUpdate(w http.ResponseWriter, r *http.Reque if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } @@ -249,17 +195,17 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } - var updatedData BoardData - err = json.NewDecoder(r.Body).Decode(&updatedData) + var updatedData structs.BoardData + err = easyjson.UnmarshalFromReader(r.Body, &updatedData) defer r.Body.Close() if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadBody) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) responseError(w, code, message) return } @@ -267,7 +213,7 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { err = updatedData.Validate() if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -286,7 +232,7 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { err = h.boardCase.UpdateBoardInfo(r.Context(), updatedBoard, tagTitles) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -295,7 +241,7 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -305,7 +251,7 @@ func (h *HandlerHTTP) DeleteBoard(w http.ResponseWriter, r *http.Request) { boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } @@ -313,7 +259,7 @@ func (h *HandlerHTTP) DeleteBoard(w http.ResponseWriter, r *http.Request) { err = h.boardCase.DeleteCertainBoard(r.Context(), int(boardID)) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -322,7 +268,7 @@ func (h *HandlerHTTP) DeleteBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -390,21 +336,21 @@ func (h *HandlerHTTP) DeletePinFromBoard(w http.ResponseWriter, r *http.Request) boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("delete pin from board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } - delPinFromBoard := DeletePinFromBoard{} - err = json.NewDecoder(r.Body).Decode(&delPinFromBoard) + delPinFromBoard := structs.DeletePinFromBoard{} + err = easyjson.UnmarshalFromReader(r.Body, &delPinFromBoard) if err != nil { - code, message := getErrCodeMessage(ErrBadBody) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) responseError(w, code, message) return } @@ -413,7 +359,7 @@ func (h *HandlerHTTP) DeletePinFromBoard(w http.ResponseWriter, r *http.Request) err = h.boardCase.DeletePinFromBoard(r.Context(), int(boardID), delPinFromBoard.PinID) if err != nil { logger.Info("delete pin from board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -422,6 +368,6 @@ func (h *HandlerHTTP) DeletePinFromBoard(w http.ResponseWriter, r *http.Request) if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } 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/board_errors.go b/internal/pkg/delivery/http/v1/errors/board.go similarity index 83% rename from internal/pkg/delivery/http/v1/board_errors.go rename to internal/pkg/delivery/http/v1/errors/board.go index ab31833..0d2106c 100644 --- a/internal/pkg/delivery/http/v1/board_errors.go +++ b/internal/pkg/delivery/http/v1/errors/board.go @@ -1,4 +1,4 @@ -package v1 +package errors import ( "errors" @@ -16,8 +16,8 @@ var ( ) var ( - wrappedErrors = map[error]string{ErrInvalidTagTitles: "bad_Tagtitles"} - errCodeCompability = map[error]string{ + WrappedErrors = map[error]string{ErrInvalidTagTitles: "bad_Tagtitles"} + ErrCodeCompability = map[error]string{ ErrInvalidBoardTitle: "bad_boardTitle", ErrEmptyTitle: "empty_boardTitle", ErrEmptyPubOpt: "bad_pubOpt", @@ -29,7 +29,7 @@ var ( } ) -func getErrCodeMessage(err error) (string, string) { +func GetErrCodeMessage(err error) (string, string) { var ( code string general, specific bool @@ -40,9 +40,9 @@ func getErrCodeMessage(err error) (string, string) { return code, err.Error() } - code, specific = errCodeCompability[err] + code, specific = ErrCodeCompability[err] if !specific { - for wrappedErr, code_ := range wrappedErrors { + for wrappedErr, code_ := range WrappedErrors { if errors.Is(err, wrappedErr) { specific = true code = code_ diff --git a/internal/pkg/delivery/http/v1/errors/general.go b/internal/pkg/delivery/http/v1/errors/general.go new file mode 100644 index 0000000..d60b0c3 --- /dev/null +++ b/internal/pkg/delivery/http/v1/errors/general.go @@ -0,0 +1,111 @@ +package errors + +import ( + "errors" + "fmt" + "net/http" + + errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" +) + +// for backward compatibility - begin +var ( + ErrBadBody = errors.New("can't parse body, JSON with correct data types is expected") + ErrBadUrlParam = errors.New("bad URL param has been provided") + ErrBadQueryParam = errors.New("invalid query parameters have been provided") + ErrInternalError = errors.New("internal server error occured") + ErrBadContentType = errors.New("application/json is expected") +) + +var ( + generalErrCodeCompability = map[error]string{ + ErrBadBody: "bad_body", + ErrBadQueryParam: "bad_queryParams", + ErrInternalError: "internal_error", + ErrBadContentType: "bad_contentType", + ErrBadUrlParam: "bad_urlParam", + } +) + +// for backward compatibility - end + +type ErrInvalidBody struct{} + +func (e *ErrInvalidBody) Error() string { + return "invalid body" +} + +func (e *ErrInvalidBody) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidQueryParam struct { + Params map[string]string +} + +func (e *ErrInvalidQueryParam) Error() string { + return fmt.Sprintf("invalid query params: %v", e.Params) +} + +func (e *ErrInvalidQueryParam) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidContentType struct { + PreferredType string +} + +func (e *ErrInvalidContentType) Error() string { + return fmt.Sprintf("invalid content type, should be %s", e.PreferredType) +} + +func (e *ErrInvalidContentType) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidUrlParams struct { + Params map[string]string +} + +func (e *ErrInvalidUrlParams) Error() string { + return fmt.Sprintf("invalid URL params: %v", e.Params) +} + +func (e *ErrInvalidUrlParams) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrMissingBodyParams struct { + Params []string +} + +func (e *ErrMissingBodyParams) Error() string { + return fmt.Sprintf("missing body params: %v", e.Params) +} + +func (e *ErrMissingBodyParams) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +func GetCodeStatusHttp(err error) (ErrCode string, httpStatus int) { + + var declaredErr errPkg.DeclaredError + if errors.As(err, &declaredErr) { + switch declaredErr.Type() { + case errPkg.ErrInvalidInput: + return "bad_input", http.StatusBadRequest + case errPkg.ErrNotFound: + return "not_found", http.StatusNotFound + case errPkg.ErrAlreadyExists: + return "already_exists", http.StatusConflict + case errPkg.ErrNoAuth: + return "no_auth", http.StatusUnauthorized + case errPkg.ErrNoAccess: + return "no_access", http.StatusForbidden + case errPkg.ErrTimeout: + return "timeout", http.StatusRequestTimeout + } + } + + return "internal_error", http.StatusInternalServerError +} diff --git a/internal/pkg/delivery/http/v1/search_errors.go b/internal/pkg/delivery/http/v1/errors/search.go similarity index 96% rename from internal/pkg/delivery/http/v1/search_errors.go rename to internal/pkg/delivery/http/v1/errors/search.go index 1fb9f00..74abdec 100644 --- a/internal/pkg/delivery/http/v1/search_errors.go +++ b/internal/pkg/delivery/http/v1/errors/search.go @@ -1,4 +1,4 @@ -package v1 +package errors import errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" 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/http/v1/profile.go b/internal/pkg/delivery/http/v1/profile.go index b03ed6f..66ab2ab 100644 --- a/internal/pkg/delivery/http/v1/profile.go +++ b/internal/pkg/delivery/http/v1/profile.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/go-chi/chi/v5" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" userEntity "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/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" @@ -57,7 +58,7 @@ func (h *HandlerHTTP) GetUserInfo(w http.ResponseWriter, r *http.Request) { userIdParam := chi.URLParam(r, "userID") userID, err := strconv.ParseInt(userIdParam, 10, 64) if err != nil { - h.responseErr(w, r, &ErrInvalidUrlParams{map[string]string{"userID": userIdParam}}) + h.responseErr(w, r, &errHTTP.ErrInvalidUrlParams{Params: map[string]string{"userID": userIdParam}}) return } diff --git a/internal/pkg/delivery/http/v1/response.go b/internal/pkg/delivery/http/v1/response.go index 98ab544..7b77059 100644 --- a/internal/pkg/delivery/http/v1/response.go +++ b/internal/pkg/delivery/http/v1/response.go @@ -1,140 +1,26 @@ package v1 import ( - "encoding/json" - "errors" "fmt" "net/http" - errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/mailru/easyjson" ) -// for backward compatibility - begin -var ( - ErrBadBody = errors.New("can't parse body, JSON with correct data types is expected") - ErrBadUrlParam = errors.New("bad URL param has been provided") - ErrBadQueryParam = errors.New("invalid query parameters have been provided") - ErrInternalError = errors.New("internal server error occured") - ErrBadContentType = errors.New("application/json is expected") -) - -var ( - generalErrCodeCompability = map[error]string{ - ErrBadBody: "bad_body", - ErrBadQueryParam: "bad_queryParams", - ErrInternalError: "internal_error", - ErrBadContentType: "bad_contentType", - ErrBadUrlParam: "bad_urlParam", - } -) - -// for backward compatibility - end - -type ErrInvalidBody struct{} - -func (e *ErrInvalidBody) Error() string { - return "invalid body" -} - -func (e *ErrInvalidBody) Type() errPkg.Type { - return errPkg.ErrInvalidInput -} - -type ErrInvalidQueryParam struct { - params map[string]string -} - -func (e *ErrInvalidQueryParam) Error() string { - return fmt.Sprintf("invalid query params: %v", e.params) -} - -func (e *ErrInvalidQueryParam) Type() errPkg.Type { - return errPkg.ErrInvalidInput -} - -type ErrInvalidContentType struct { - preferredType string -} - -func (e *ErrInvalidContentType) Error() string { - return fmt.Sprintf("invalid content type, should be %s", e.preferredType) -} - -func (e *ErrInvalidContentType) Type() errPkg.Type { - return errPkg.ErrInvalidInput -} - -type ErrInvalidUrlParams struct { - params map[string]string -} - -func (e *ErrInvalidUrlParams) Error() string { - return fmt.Sprintf("invalid URL params: %v", e.params) -} - -func (e *ErrInvalidUrlParams) Type() errPkg.Type { - return errPkg.ErrInvalidInput -} - -type ErrMissingBodyParams struct { - params []string -} - -func (e *ErrMissingBodyParams) Error() string { - return fmt.Sprintf("missing body params: %v", e.params) -} - -func (e *ErrMissingBodyParams) Type() errPkg.Type { - return errPkg.ErrInvalidInput -} - -func getCodeStatusHttp(err error) (ErrCode string, httpStatus int) { - - var declaredErr errPkg.DeclaredError - if errors.As(err, &declaredErr) { - switch declaredErr.Type() { - case errPkg.ErrInvalidInput: - return "bad_input", http.StatusBadRequest - case errPkg.ErrNotFound: - return "not_found", http.StatusNotFound - case errPkg.ErrAlreadyExists: - return "already_exists", http.StatusConflict - case errPkg.ErrNoAuth: - return "no_auth", http.StatusUnauthorized - case errPkg.ErrNoAccess: - return "no_access", http.StatusForbidden - case errPkg.ErrTimeout: - return "timeout", http.StatusRequestTimeout - } - } - - return "internal_error", http.StatusInternalServerError -} - -type JsonResponse struct { - Status string `json:"status" example:"ok"` - Message string `json:"message" example:"Response message"` - Body interface{} `json:"body" extensions:"x-omitempty"` -} // @name JsonResponse - -type JsonErrResponse struct { - Status string `json:"status" example:"error"` - Message string `json:"message" example:"Error description"` - Code string `json:"code"` -} // @name JsonErrResponse - func SetContentTypeJSON(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") } func responseOk(statusCode int, w http.ResponseWriter, message string, body any) error { - res := JsonResponse{ + res := structs.JsonResponse{ Status: "ok", Message: message, Body: body, } - resBytes, err := json.Marshal(res) + resBytes, err := easyjson.Marshal(res) if err != nil { w.WriteHeader(http.StatusInternalServerError) return fmt.Errorf("responseOk: %w", err) @@ -146,12 +32,12 @@ func responseOk(statusCode int, w http.ResponseWriter, message string, body any) } func responseError(w http.ResponseWriter, code, message string) error { - res := JsonErrResponse{ + res := structs.JsonErrResponse{ Status: "error", Message: message, Code: code, } - resBytes, err := json.Marshal(res) + resBytes, err := easyjson.Marshal(res) if err != nil { return fmt.Errorf("responseError: %w", err) } @@ -162,7 +48,7 @@ func responseError(w http.ResponseWriter, code, message string) error { func (h *HandlerHTTP) responseErr(w http.ResponseWriter, r *http.Request, err error) error { log := logger.GetLoggerFromCtx(r.Context()) - code, status := getCodeStatusHttp(err) + code, status := errHTTP.GetCodeStatusHttp(err) var msg string if status == http.StatusInternalServerError { log.Warnf("unexpected application error: %s", err.Error()) @@ -171,12 +57,12 @@ func (h *HandlerHTTP) responseErr(w http.ResponseWriter, r *http.Request, err er msg = err.Error() } - res := JsonErrResponse{ + res := structs.JsonErrResponse{ Status: "error", Message: msg, Code: code, } - resBytes, err := json.Marshal(res) + resBytes, err := easyjson.Marshal(res) if err != nil { return fmt.Errorf("responseError: %w", err) } diff --git a/internal/pkg/delivery/http/v1/search.go b/internal/pkg/delivery/http/v1/search.go index f798d36..4ec1e79 100644 --- a/internal/pkg/delivery/http/v1/search.go +++ b/internal/pkg/delivery/http/v1/search.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" ) @@ -89,7 +90,7 @@ func GetSearchOpts(r *http.Request, sortOpts []string, defaultSortOpt string) (* } if len(invalidParams) > 0 { - return nil, &ErrInvalidQueryParam{params: invalidParams} + return nil, &errHTTP.ErrInvalidQueryParam{Params: invalidParams} } return opts, nil @@ -114,7 +115,7 @@ func GetGeneralOpts(r *http.Request, invalidParams map[string]string) (*search.G opts.Template = template } } else { - return nil, &ErrNoData{} + return nil, &errHTTP.ErrNoData{} } if sortOrder := r.URL.Query().Get("order"); sortOrder != "" { diff --git a/internal/pkg/delivery/http/v1/structs/board.go b/internal/pkg/delivery/http/v1/structs/board.go new file mode 100644 index 0000000..e6aa540 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/board.go @@ -0,0 +1,71 @@ +package structs + +import ( + "fmt" + + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" +) + +//go:generate easyjson board.go + +// data for board creation/update +// +//easyjson:json +type BoardData struct { + Title *string `json:"title" example:"new board"` + Description *string `json:"description" example:"long desc"` + Public *bool `json:"public" example:"true"` + Tags []string `json:"tags" example:"['blue', 'car']"` +} + +// board view for delivery layer +// +//easyjson:json +type CertainBoard struct { + ID int `json:"board_id" example:"22"` + AuthorID int `json:"author_id" example:"22"` + Title string `json:"title" example:"new board"` + Description string `json:"description" example:"long desc"` + CreatedAt string `json:"created_at" example:"07-11-2023"` + PinsNumber int `json:"pins_number" example:"12"` + Pins []string `json:"pins" example:"['/pic1', '/pic2']"` + Tags []string `json:"tags" example:"['love', 'green']"` +} + +//easyjson:json +type CertainBoardWithUsername struct { + ID int `json:"board_id" example:"22"` + AuthorID int `json:"author_id" example:"22"` + AuthorUsername string `json:"author_username" example:"Bob"` + Title string `json:"title" example:"new board"` + Description string `json:"description" example:"long desc"` + CreatedAt string `json:"created_at" example:"07-11-2023"` + PinsNumber int `json:"pins_number" example:"12"` + Pins []string `json:"pins" example:"['/pic1', '/pic2']"` + Tags []string `json:"tags" example:"['love', 'green']"` +} + +//easyjson:json +type DeletePinFromBoard struct { + PinID int `json:"pin_id" example:"22"` +} + +func (data *BoardData) Validate() error { + if data.Title == nil || *data.Title == "" { + return errHTTP.ErrInvalidBoardTitle + } + if data.Description == nil { + data.Description = new(string) + *data.Description = "" + } + if data.Public == nil { + return errHTTP.ErrEmptyPubOpt + } + if !isValidBoardTitle(*data.Title) { + return errHTTP.ErrInvalidBoardTitle + } + if err := checkIsValidTagTitles(data.Tags); err != nil { + return fmt.Errorf("%s: %w", err.Error(), errHTTP.ErrInvalidTagTitles) + } + return nil +} diff --git a/internal/pkg/delivery/http/v1/structs/board_easyjson.go b/internal/pkg/delivery/http/v1/structs/board_easyjson.go new file mode 100644 index 0000000..b847d66 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/board_easyjson.go @@ -0,0 +1,605 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +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 easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *DeletePinFromBoard) { + 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 "pin_id": + out.PinID = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in DeletePinFromBoard) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"pin_id\":" + out.RawString(prefix[1:]) + out.Int(int(in.PinID)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v DeletePinFromBoard) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v DeletePinFromBoard) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *DeletePinFromBoard) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *DeletePinFromBoard) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(in *jlexer.Lexer, out *CertainBoardWithUsername) { + 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 "board_id": + out.ID = int(in.Int()) + case "author_id": + out.AuthorID = int(in.Int()) + case "author_username": + out.AuthorUsername = string(in.String()) + case "title": + out.Title = string(in.String()) + case "description": + out.Description = string(in.String()) + case "created_at": + out.CreatedAt = string(in.String()) + case "pins_number": + out.PinsNumber = int(in.Int()) + case "pins": + if in.IsNull() { + in.Skip() + out.Pins = nil + } else { + in.Delim('[') + if out.Pins == nil { + if !in.IsDelim(']') { + out.Pins = make([]string, 0, 4) + } else { + out.Pins = []string{} + } + } else { + out.Pins = (out.Pins)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.Pins = append(out.Pins, v1) + in.WantComma() + } + in.Delim(']') + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v2 string + v2 = string(in.String()) + out.Tags = append(out.Tags, v2) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(out *jwriter.Writer, in CertainBoardWithUsername) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"board_id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"author_id\":" + out.RawString(prefix) + out.Int(int(in.AuthorID)) + } + { + const prefix string = ",\"author_username\":" + out.RawString(prefix) + out.String(string(in.AuthorUsername)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.String(string(in.CreatedAt)) + } + { + const prefix string = ",\"pins_number\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"pins\":" + out.RawString(prefix) + if in.Pins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v3, v4 := range in.Pins { + if v3 > 0 { + out.RawByte(',') + } + out.String(string(v4)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v5, v6 := range in.Tags { + if v5 > 0 { + out.RawByte(',') + } + out.String(string(v6)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v CertainBoardWithUsername) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v CertainBoardWithUsername) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *CertainBoardWithUsername) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *CertainBoardWithUsername) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(in *jlexer.Lexer, out *CertainBoard) { + 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 "board_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 "created_at": + out.CreatedAt = string(in.String()) + case "pins_number": + out.PinsNumber = int(in.Int()) + case "pins": + if in.IsNull() { + in.Skip() + out.Pins = nil + } else { + in.Delim('[') + if out.Pins == nil { + if !in.IsDelim(']') { + out.Pins = make([]string, 0, 4) + } else { + out.Pins = []string{} + } + } else { + out.Pins = (out.Pins)[:0] + } + for !in.IsDelim(']') { + var v7 string + v7 = string(in.String()) + out.Pins = append(out.Pins, v7) + in.WantComma() + } + in.Delim(']') + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v8 string + v8 = string(in.String()) + out.Tags = append(out.Tags, v8) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(out *jwriter.Writer, in CertainBoard) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"board_id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"author_id\":" + out.RawString(prefix) + out.Int(int(in.AuthorID)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.String(string(in.CreatedAt)) + } + { + const prefix string = ",\"pins_number\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"pins\":" + out.RawString(prefix) + if in.Pins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v9, v10 := range in.Pins { + if v9 > 0 { + out.RawByte(',') + } + out.String(string(v10)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v11, v12 := range in.Tags { + if v11 > 0 { + out.RawByte(',') + } + out.String(string(v12)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v CertainBoard) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v CertainBoard) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *CertainBoard) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *CertainBoard) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(in *jlexer.Lexer, out *BoardData) { + 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 "title": + if in.IsNull() { + in.Skip() + out.Title = nil + } else { + if out.Title == nil { + out.Title = new(string) + } + *out.Title = string(in.String()) + } + case "description": + if in.IsNull() { + in.Skip() + out.Description = nil + } else { + if out.Description == nil { + out.Description = new(string) + } + *out.Description = string(in.String()) + } + case "public": + if in.IsNull() { + in.Skip() + out.Public = nil + } else { + if out.Public == nil { + out.Public = new(bool) + } + *out.Public = bool(in.Bool()) + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v13 string + v13 = string(in.String()) + out.Tags = append(out.Tags, v13) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(out *jwriter.Writer, in BoardData) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"title\":" + out.RawString(prefix[1:]) + if in.Title == nil { + out.RawString("null") + } else { + out.String(string(*in.Title)) + } + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + if in.Description == nil { + out.RawString("null") + } else { + out.String(string(*in.Description)) + } + } + { + const prefix string = ",\"public\":" + out.RawString(prefix) + if in.Public == nil { + out.RawString("null") + } else { + out.Bool(bool(*in.Public)) + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v14, v15 := range in.Tags { + if v14 > 0 { + out.RawByte(',') + } + out.String(string(v15)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BoardData) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BoardData) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BoardData) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BoardData) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(l, v) +} diff --git a/internal/pkg/delivery/http/v1/board_validation.go b/internal/pkg/delivery/http/v1/structs/board_validation.go similarity index 98% rename from internal/pkg/delivery/http/v1/board_validation.go rename to internal/pkg/delivery/http/v1/structs/board_validation.go index 0cabf18..6181434 100644 --- a/internal/pkg/delivery/http/v1/board_validation.go +++ b/internal/pkg/delivery/http/v1/structs/board_validation.go @@ -1,4 +1,4 @@ -package v1 +package structs import ( "fmt" diff --git a/internal/pkg/delivery/http/v1/structs/response.go b/internal/pkg/delivery/http/v1/structs/response.go new file mode 100644 index 0000000..e922b82 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/response.go @@ -0,0 +1,17 @@ +package structs + +//go:generate easyjson subscription.go + +//easyjson:json +type JsonResponse struct { + Status string `json:"status" example:"ok"` + Message string `json:"message" example:"Response message"` + Body interface{} `json:"body" extensions:"x-omitempty"` +} // @name JsonResponse + +//easyjson:json +type JsonErrResponse struct { + Status string `json:"status" example:"error"` + Message string `json:"message" example:"Error description"` + Code string `json:"code"` +} // @name JsonErrResponse diff --git a/internal/pkg/delivery/http/v1/structs/response_easyjson.go b/internal/pkg/delivery/http/v1/structs/response_easyjson.go new file mode 100644 index 0000000..91dfdd8 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/response_easyjson.go @@ -0,0 +1,191 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +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 easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *JsonResponse) { + 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 "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 easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in JsonResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + { + 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 JsonResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v JsonResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *JsonResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *JsonResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} +func easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(in *jlexer.Lexer, out *JsonErrResponse) { + 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 "message": + out.Message = string(in.String()) + case "code": + out.Code = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(out *jwriter.Writer, in JsonErrResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.String(string(in.Code)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v JsonErrResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v JsonErrResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *JsonErrResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *JsonErrResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(l, v) +} diff --git a/internal/pkg/delivery/http/v1/structs/subscription.go b/internal/pkg/delivery/http/v1/structs/subscription.go new file mode 100644 index 0000000..73bee4d --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/subscription.go @@ -0,0 +1,17 @@ +package structs + +import errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + +//go:generate easyjson subscription.go + +//easyjson:json +type SubscriptionAction struct { + To *int `json:"to" example:"2"` +} + +func (s *SubscriptionAction) Validate() error { + if s.To == nil { + return &errHTTP.ErrMissingBodyParams{Params: []string{"to"}} + } + return nil +} diff --git a/internal/pkg/delivery/http/v1/structs/subscription_easyjson.go b/internal/pkg/delivery/http/v1/structs/subscription_easyjson.go new file mode 100644 index 0000000..b1e0fd9 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/subscription_easyjson.go @@ -0,0 +1,97 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +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 easyjsonFfbd3743DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *SubscriptionAction) { + 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 "to": + if in.IsNull() { + in.Skip() + out.To = nil + } else { + if out.To == nil { + out.To = new(int) + } + *out.To = int(in.Int()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonFfbd3743EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in SubscriptionAction) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"to\":" + out.RawString(prefix[1:]) + if in.To == nil { + out.RawString("null") + } else { + out.Int(int(*in.To)) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v SubscriptionAction) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonFfbd3743EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v SubscriptionAction) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonFfbd3743EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *SubscriptionAction) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonFfbd3743DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *SubscriptionAction) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonFfbd3743DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} diff --git a/internal/pkg/delivery/http/v1/subscription.go b/internal/pkg/delivery/http/v1/subscription.go index 4614476..7444bdf 100644 --- a/internal/pkg/delivery/http/v1/subscription.go +++ b/internal/pkg/delivery/http/v1/subscription.go @@ -1,12 +1,15 @@ package v1 import ( - "encoding/json" "net/http" "strconv" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" userEntity "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" ) var ( @@ -17,26 +20,16 @@ var ( maxCount = 50 ) -type SubscriptionAction struct { - To *int `json:"to" example:"2"` -} - -func (s *SubscriptionAction) Validate() error { - if s.To == nil { - return &ErrMissingBodyParams{[]string{"to"}} - } - return nil -} - func (h *HandlerHTTP) Subscribe(w http.ResponseWriter, r *http.Request) { if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - h.responseErr(w, r, &ErrInvalidContentType{preferredType: ApplicationJson}) + h.responseErr(w, r, &errHTTP.ErrInvalidContentType{PreferredType: ApplicationJson}) return } - sub := SubscriptionAction{} - if err := json.NewDecoder(r.Body).Decode(&sub); err != nil { - h.responseErr(w, r, &ErrInvalidBody{}) + sub := structs.SubscriptionAction{} + + if err := easyjson.UnmarshalFromReader(r.Body, &sub); err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidBody{}) return } defer r.Body.Close() @@ -56,13 +49,13 @@ func (h *HandlerHTTP) Subscribe(w http.ResponseWriter, r *http.Request) { func (h *HandlerHTTP) Unsubscribe(w http.ResponseWriter, r *http.Request) { if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - h.responseErr(w, r, &ErrInvalidContentType{preferredType: ApplicationJson}) + h.responseErr(w, r, &errHTTP.ErrInvalidContentType{PreferredType: ApplicationJson}) return } - sub := SubscriptionAction{} - if err := json.NewDecoder(r.Body).Decode(&sub); err != nil { - h.responseErr(w, r, &ErrInvalidBody{}) + sub := structs.SubscriptionAction{} + if err := easyjson.UnmarshalFromReader(r.Body, &sub); err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidBody{}) return } defer r.Body.Close() @@ -146,7 +139,7 @@ func GetOpts(r *http.Request) (*userEntity.SubscriptionOpts, error) { opts.Count = maxCount } if len(invalidParams) > 0 { - return nil, &ErrInvalidQueryParam{invalidParams} + return nil, &errHTTP.ErrInvalidQueryParam{invalidParams} } return opts, 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 07635de..5d55376 100644 --- a/internal/pkg/delivery/websocket/websocket.go +++ b/internal/pkg/delivery/websocket/websocket.go @@ -7,23 +7,22 @@ import ( "os" "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) @@ -36,15 +35,9 @@ 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())) - gRPCConn, err := grpc.Dial((os.Getenv("REALTIME_SERVICE_HOST") + ":" + os.Getenv("REALTIME_SERVICE_PORT")), grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - log.Error(fmt.Errorf("grpc dial: %w", err).Error()) - } +func New(log *log.Logger, mesCase usecase.Usecase, notify notifySubscriber, opts ...Option) *HandlerWebSocket { + handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, notifySub: notify} - client := rt.NewRealTimeClient(gRPCConn) - handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, client: client} for _, opt := range opts { opt(handlerWS) } @@ -52,178 +45,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]int{"id": id})) - _, 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", nil)) - _, 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", nil)) - _, 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..d695468 100644 --- a/internal/pkg/entity/board/board.go +++ b/internal/pkg/entity/board/board.go @@ -6,6 +6,9 @@ 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"` @@ -17,6 +20,7 @@ type Board struct { DeletedAt *time.Time `json:"-"` } +//easyjson:json type BoardWithContent struct { BoardInfo Board PinsNumber int diff --git a/internal/pkg/entity/board/board_easyjson.go b/internal/pkg/entity/board/board_easyjson.go new file mode 100644 index 0000000..12d08ef --- /dev/null +++ b/internal/pkg/entity/board/board_easyjson.go @@ -0,0 +1,293 @@ +// 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 *BoardWithContent) { + 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 "BoardInfo": + (out.BoardInfo).UnmarshalEasyJSON(in) + case "PinsNumber": + out.PinsNumber = int(in.Int()) + case "Pins": + if in.IsNull() { + in.Skip() + out.Pins = nil + } else { + in.Delim('[') + if out.Pins == nil { + if !in.IsDelim(']') { + out.Pins = make([]string, 0, 4) + } else { + out.Pins = []string{} + } + } else { + out.Pins = (out.Pins)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.Pins = append(out.Pins, v1) + in.WantComma() + } + in.Delim(']') + } + case "TagTitles": + if in.IsNull() { + in.Skip() + out.TagTitles = nil + } else { + in.Delim('[') + if out.TagTitles == nil { + if !in.IsDelim(']') { + out.TagTitles = make([]string, 0, 4) + } else { + out.TagTitles = []string{} + } + } else { + out.TagTitles = (out.TagTitles)[:0] + } + for !in.IsDelim(']') { + var v2 string + v2 = string(in.String()) + out.TagTitles = append(out.TagTitles, v2) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(out *jwriter.Writer, in BoardWithContent) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"BoardInfo\":" + out.RawString(prefix[1:]) + (in.BoardInfo).MarshalEasyJSON(out) + } + { + const prefix string = ",\"PinsNumber\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"Pins\":" + out.RawString(prefix) + if in.Pins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v3, v4 := range in.Pins { + if v3 > 0 { + out.RawByte(',') + } + out.String(string(v4)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"TagTitles\":" + out.RawString(prefix) + if in.TagTitles == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v5, v6 := range in.TagTitles { + if v5 > 0 { + out.RawByte(',') + } + out.String(string(v6)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BoardWithContent) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BoardWithContent) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BoardWithContent) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BoardWithContent) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(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 easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(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{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Board) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Board) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Board) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(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..78723f0 --- /dev/null +++ b/internal/pkg/entity/comment/comment_easyjson.go @@ -0,0 +1,121 @@ +// 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) + } + (*out.Author).UnmarshalEasyJSON(in) + } + 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 { + (*in.Author).MarshalEasyJSON(out) + } + } + { + 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) +} 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/search/search.go b/internal/pkg/entity/search/search.go index 76b0c6a..478aabe 100644 --- a/internal/pkg/entity/search/search.go +++ b/internal/pkg/entity/search/search.go @@ -8,6 +8,8 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate easyjson search.go + type Template string func (t *Template) Validate() bool { @@ -26,12 +28,14 @@ func (t *Template) GetSubStrings(sep string) []string { return strings.Split(string(*t), sep) } +//easyjson:json type BoardForSearch struct { BoardHeader board.Board PinsNumber int `json:"pins_number"` PreviewPins []string `json:"pins"` } +//easyjson:json type PinForSearch struct { ID int `json:"id"` Title string `json:"title"` @@ -39,6 +43,7 @@ type PinForSearch struct { Likes int `json:"likes"` } +//easyjson:json type UserForSearch struct { ID int `json:"id"` Username string `json:"username"` diff --git a/internal/pkg/entity/search/search_easyjson.go b/internal/pkg/entity/search/search_easyjson.go new file mode 100644 index 0000000..497b75f --- /dev/null +++ b/internal/pkg/entity/search/search_easyjson.go @@ -0,0 +1,312 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package search + +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 easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(in *jlexer.Lexer, out *UserForSearch) { + 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 "avatar": + out.Avatar = string(in.String()) + case "subscribers": + out.SubsCount = int(in.Int()) + case "is_subscribed": + out.HasSubscribeFromCurUser = bool(in.Bool()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(out *jwriter.Writer, in UserForSearch) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + out.RawString(prefix) + out.String(string(in.Username)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + { + const prefix string = ",\"subscribers\":" + out.RawString(prefix) + out.Int(int(in.SubsCount)) + } + { + const prefix string = ",\"is_subscribed\":" + out.RawString(prefix) + out.Bool(bool(in.HasSubscribeFromCurUser)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v UserForSearch) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v UserForSearch) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *UserForSearch) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *UserForSearch) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(l, v) +} +func easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(in *jlexer.Lexer, out *PinForSearch) { + 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 "title": + out.Title = string(in.String()) + case "picture": + out.Picture = string(in.String()) + case "likes": + out.Likes = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(out *jwriter.Writer, in PinForSearch) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"picture\":" + out.RawString(prefix) + out.String(string(in.Picture)) + } + { + const prefix string = ",\"likes\":" + out.RawString(prefix) + out.Int(int(in.Likes)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v PinForSearch) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v PinForSearch) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *PinForSearch) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *PinForSearch) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(l, v) +} +func easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(in *jlexer.Lexer, out *BoardForSearch) { + 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 "BoardHeader": + (out.BoardHeader).UnmarshalEasyJSON(in) + case "pins_number": + out.PinsNumber = int(in.Int()) + case "pins": + if in.IsNull() { + in.Skip() + out.PreviewPins = nil + } else { + in.Delim('[') + if out.PreviewPins == nil { + if !in.IsDelim(']') { + out.PreviewPins = make([]string, 0, 4) + } else { + out.PreviewPins = []string{} + } + } else { + out.PreviewPins = (out.PreviewPins)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.PreviewPins = append(out.PreviewPins, v1) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(out *jwriter.Writer, in BoardForSearch) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"BoardHeader\":" + out.RawString(prefix[1:]) + (in.BoardHeader).MarshalEasyJSON(out) + } + { + const prefix string = ",\"pins_number\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"pins\":" + out.RawString(prefix) + if in.PreviewPins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v2, v3 := range in.PreviewPins { + if v2 > 0 { + out.RawByte(',') + } + out.String(string(v3)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BoardForSearch) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BoardForSearch) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BoardForSearch) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BoardForSearch) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(l, v) +} diff --git a/internal/pkg/entity/user/user.go b/internal/pkg/entity/user/user.go index 1b270be..5a636fc 100644 --- a/internal/pkg/entity/user/user.go +++ b/internal/pkg/entity/user/user.go @@ -5,6 +5,11 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate easyjson user.go + +const UserUnknown = -1 + +//easyjson:json type User struct { ID int `json:"id,omitempty" example:"123"` Username string `json:"username" example:"Green"` @@ -16,6 +21,7 @@ type User struct { Password string `json:"password,omitempty" example:"pass123"` } // @name User +//easyjson:json type SubscriptionUser struct { ID int `json:"id"` Username string `json:"username"` diff --git a/internal/pkg/entity/user/user_easyjson.go b/internal/pkg/entity/user/user_easyjson.go new file mode 100644 index 0000000..e8b0dae --- /dev/null +++ b/internal/pkg/entity/user/user_easyjson.go @@ -0,0 +1,233 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package user + +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 easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(in *jlexer.Lexer, out *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 easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(out *jwriter.Writer, in 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('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v User) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v User) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *User) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *User) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(l, v) +} +func easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(in *jlexer.Lexer, out *SubscriptionUser) { + 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 "avatar": + out.Avatar = string(in.String()) + case "is_subscribed": + out.HasSubscribeFromCurUser = bool(in.Bool()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(out *jwriter.Writer, in SubscriptionUser) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + out.RawString(prefix) + out.String(string(in.Username)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + { + const prefix string = ",\"is_subscribed\":" + out.RawString(prefix) + out.Bool(bool(in.HasSubscribeFromCurUser)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v SubscriptionUser) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v SubscriptionUser) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *SubscriptionUser) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *SubscriptionUser) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(l, v) +} 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/repository/search/mock/search_mock.go b/internal/pkg/repository/search/mock/search_mock.go new file mode 100644 index 0000000..026411d --- /dev/null +++ b/internal/pkg/repository/search/mock/search_mock.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + search "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + 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 +} + +// GetFilteredBoards mocks base method. +func (m *MockRepository) GetFilteredBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFilteredBoards", ctx, opts) + ret0, _ := ret[0].([]search.BoardForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFilteredBoards indicates an expected call of GetFilteredBoards. +func (mr *MockRepositoryMockRecorder) GetFilteredBoards(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredBoards", reflect.TypeOf((*MockRepository)(nil).GetFilteredBoards), ctx, opts) +} + +// GetFilteredPins mocks base method. +func (m *MockRepository) GetFilteredPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFilteredPins", ctx, opts) + ret0, _ := ret[0].([]search.PinForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFilteredPins indicates an expected call of GetFilteredPins. +func (mr *MockRepositoryMockRecorder) GetFilteredPins(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredPins", reflect.TypeOf((*MockRepository)(nil).GetFilteredPins), ctx, opts) +} + +// GetFilteredUsers mocks base method. +func (m *MockRepository) GetFilteredUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFilteredUsers", ctx, opts) + ret0, _ := ret[0].([]search.UserForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFilteredUsers indicates an expected call of GetFilteredUsers. +func (mr *MockRepositoryMockRecorder) GetFilteredUsers(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredUsers", reflect.TypeOf((*MockRepository)(nil).GetFilteredUsers), ctx, opts) +} diff --git a/internal/pkg/repository/search/repo.go b/internal/pkg/repository/search/repo.go index 98ca7bf..b49c815 100644 --- a/internal/pkg/repository/search/repo.go +++ b/internal/pkg/repository/search/repo.go @@ -6,6 +6,7 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" ) +//go:generate mockgen -destination=./mock/search_mock.go -package=mock -source=repo.go Repository type Repository interface { GetFilteredUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) GetFilteredPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) diff --git a/internal/pkg/repository/subscription/mock/subscription_mock.go b/internal/pkg/repository/subscription/mock/subscription_mock.go new file mode 100644 index 0000000..28f115a --- /dev/null +++ b/internal/pkg/repository/subscription/mock/subscription_mock.go @@ -0,0 +1,94 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + 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 +} + +// CreateSubscriptionUser mocks base method. +func (m *MockRepository) CreateSubscriptionUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSubscriptionUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateSubscriptionUser indicates an expected call of CreateSubscriptionUser. +func (mr *MockRepositoryMockRecorder) CreateSubscriptionUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscriptionUser", reflect.TypeOf((*MockRepository)(nil).CreateSubscriptionUser), ctx, from, to) +} + +// DeleteSubscriptionUser mocks base method. +func (m *MockRepository) DeleteSubscriptionUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSubscriptionUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSubscriptionUser indicates an expected call of DeleteSubscriptionUser. +func (mr *MockRepositoryMockRecorder) DeleteSubscriptionUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscriptionUser", reflect.TypeOf((*MockRepository)(nil).DeleteSubscriptionUser), ctx, from, to) +} + +// GetUserSubscribers mocks base method. +func (m *MockRepository) GetUserSubscribers(ctx context.Context, userID, count, lastID, currUserID int) ([]user.SubscriptionUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSubscribers", ctx, userID, count, lastID, currUserID) + ret0, _ := ret[0].([]user.SubscriptionUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSubscribers indicates an expected call of GetUserSubscribers. +func (mr *MockRepositoryMockRecorder) GetUserSubscribers(ctx, userID, count, lastID, currUserID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSubscribers", reflect.TypeOf((*MockRepository)(nil).GetUserSubscribers), ctx, userID, count, lastID, currUserID) +} + +// GetUserSubscriptions mocks base method. +func (m *MockRepository) GetUserSubscriptions(ctx context.Context, userID, count, lastID, currUserID int) ([]user.SubscriptionUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSubscriptions", ctx, userID, count, lastID, currUserID) + ret0, _ := ret[0].([]user.SubscriptionUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSubscriptions indicates an expected call of GetUserSubscriptions. +func (mr *MockRepositoryMockRecorder) GetUserSubscriptions(ctx, userID, count, lastID, currUserID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSubscriptions", reflect.TypeOf((*MockRepository)(nil).GetUserSubscriptions), ctx, userID, count, lastID, currUserID) +} diff --git a/internal/pkg/repository/subscription/repo.go b/internal/pkg/repository/subscription/repo.go index 7d23950..6c8c841 100644 --- a/internal/pkg/repository/subscription/repo.go +++ b/internal/pkg/repository/subscription/repo.go @@ -6,6 +6,7 @@ import ( userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" ) +//go:generate mockgen -destination=./mock/subscription_mock.go -package=mock -source=repo.go Repository type Repository interface { CreateSubscriptionUser(ctx context.Context, from, to int) error DeleteSubscriptionUser(ctx context.Context, from, to int) error diff --git a/internal/pkg/usecase/auth/mock/auth_mock.go b/internal/pkg/usecase/auth/mock/auth_mock.go new file mode 100644 index 0000000..813ae3b --- /dev/null +++ b/internal/pkg/usecase/auth/mock/auth_mock.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + session "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/session" + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + 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 +} + +// GetUserIDBySession mocks base method. +func (m *MockUsecase) GetUserIDBySession(ctx context.Context, sess *session.Session) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserIDBySession", ctx, sess) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserIDBySession indicates an expected call of GetUserIDBySession. +func (mr *MockUsecaseMockRecorder) GetUserIDBySession(ctx, sess interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserIDBySession", reflect.TypeOf((*MockUsecase)(nil).GetUserIDBySession), ctx, sess) +} + +// Login mocks base method. +func (m *MockUsecase) Login(ctx context.Context, username, password string) (*session.Session, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Login", ctx, username, password) + ret0, _ := ret[0].(*session.Session) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Login indicates an expected call of Login. +func (mr *MockUsecaseMockRecorder) Login(ctx, username, password interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockUsecase)(nil).Login), ctx, username, password) +} + +// Logout mocks base method. +func (m *MockUsecase) Logout(ctx context.Context, sess *session.Session) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Logout", ctx, sess) + ret0, _ := ret[0].(error) + return ret0 +} + +// Logout indicates an expected call of Logout. +func (mr *MockUsecaseMockRecorder) Logout(ctx, sess interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockUsecase)(nil).Logout), ctx, sess) +} + +// Register mocks base method. +func (m *MockUsecase) Register(ctx context.Context, user *user.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Register", ctx, user) + ret0, _ := ret[0].(error) + return ret0 +} + +// Register indicates an expected call of Register. +func (mr *MockUsecaseMockRecorder) Register(ctx, user interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockUsecase)(nil).Register), ctx, user) +} diff --git a/internal/pkg/usecase/auth/usecase.go b/internal/pkg/usecase/auth/usecase.go index 2c23f5a..49ee9bf 100644 --- a/internal/pkg/usecase/auth/usecase.go +++ b/internal/pkg/usecase/auth/usecase.go @@ -10,6 +10,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +//go:generate mockgen -destination=./mock/auth_mock.go -package=mock -source=usecase.go Usecase type Usecase interface { Register(ctx context.Context, user *entity.User) error Login(ctx context.Context, username, password string) (*session.Session, error) 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 65e877a..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,38 +37,38 @@ 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. -func (m *MockUsecase) GetMessage(ctx context.Context, messageID int) (*message.Message, error) { +func (m *MockUsecase) GetMessage(ctx context.Context, userID, messageID int) (*message.Message, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMessage", ctx, messageID) + ret := m.ctrl.Call(m, "GetMessage", ctx, userID, messageID) ret0, _ := ret[0].(*message.Message) ret1, _ := ret[1].(error) return ret0, ret1 } // GetMessage indicates an expected call of GetMessage. -func (mr *MockUsecaseMockRecorder) GetMessage(ctx, messageID interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) GetMessage(ctx, userID, messageID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockUsecase)(nil).GetMessage), ctx, messageID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockUsecase)(nil).GetMessage), ctx, userID, messageID) } // GetMessagesFromChat mocks base method. -func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, chat message.Chat, count, lastID int) ([]message.Message, int, error) { +func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, userID int, chat message.Chat, count, lastID int) ([]message.Message, int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMessagesFromChat", ctx, chat, count, lastID) + ret := m.ctrl.Call(m, "GetMessagesFromChat", ctx, userID, chat, count, lastID) ret0, _ := ret[0].([]message.Message) ret1, _ := ret[1].(int) ret2, _ := ret[2].(error) @@ -75,9 +76,9 @@ func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, chat message.Chat } // GetMessagesFromChat indicates an expected call of GetMessagesFromChat. -func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, chat, count, lastID interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, userID, chat, count, lastID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessagesFromChat", reflect.TypeOf((*MockUsecase)(nil).GetMessagesFromChat), ctx, chat, count, lastID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessagesFromChat", reflect.TypeOf((*MockUsecase)(nil).GetMessagesFromChat), ctx, userID, chat, count, lastID) } // GetUserChatsWithOtherUsers mocks base method. @@ -97,18 +98,33 @@ func (mr *MockUsecaseMockRecorder) GetUserChatsWithOtherUsers(ctx, userID, count } // SendMessage mocks base method. -func (m *MockUsecase) SendMessage(ctx context.Context, mes *message.Message) (int, error) { +func (m *MockUsecase) SendMessage(ctx context.Context, userID int, mes *message.Message) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendMessage", ctx, mes) + ret := m.ctrl.Call(m, "SendMessage", ctx, userID, mes) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } // SendMessage indicates an expected call of SendMessage. -func (mr *MockUsecaseMockRecorder) SendMessage(ctx, mes interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) SendMessage(ctx, userID, mes interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockUsecase)(nil).SendMessage), ctx, mes) + 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. diff --git a/internal/pkg/usecase/message/usecase.go b/internal/pkg/usecase/message/usecase.go index 4cf8794..00bc0fc 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,40 @@ 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) +} + +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 +71,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 +107,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 +158,51 @@ 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, + } + + evMsg.Message, err = m.GetMessage(ctx, userID, msgObjID.MessageID) + if err != nil { + m.log.Error(err.Error()) + } + + if evMsg.Type == "delete" { + evMsg.Message.Content.String = "" + } + + 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()} + } +} diff --git a/internal/pkg/usecase/search/mock/search_mock.go b/internal/pkg/usecase/search/mock/search_mock.go new file mode 100644 index 0000000..dd303a6 --- /dev/null +++ b/internal/pkg/usecase/search/mock/search_mock.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + search "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + 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 +} + +// GetBoards mocks base method. +func (m *MockUsecase) GetBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBoards", ctx, opts) + ret0, _ := ret[0].([]search.BoardForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBoards indicates an expected call of GetBoards. +func (mr *MockUsecaseMockRecorder) GetBoards(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoards", reflect.TypeOf((*MockUsecase)(nil).GetBoards), ctx, opts) +} + +// GetPins mocks base method. +func (m *MockUsecase) GetPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPins", ctx, opts) + ret0, _ := ret[0].([]search.PinForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPins indicates an expected call of GetPins. +func (mr *MockUsecaseMockRecorder) GetPins(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPins", reflect.TypeOf((*MockUsecase)(nil).GetPins), ctx, opts) +} + +// GetUsers mocks base method. +func (m *MockUsecase) GetUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsers", ctx, opts) + ret0, _ := ret[0].([]search.UserForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUsers indicates an expected call of GetUsers. +func (mr *MockUsecaseMockRecorder) GetUsers(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockUsecase)(nil).GetUsers), ctx, opts) +} diff --git a/internal/pkg/usecase/search/usecase.go b/internal/pkg/usecase/search/usecase.go index 6b06e24..b6277ca 100644 --- a/internal/pkg/usecase/search/usecase.go +++ b/internal/pkg/usecase/search/usecase.go @@ -9,6 +9,7 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate mockgen -destination=./mock/search_mock.go -package=mock -source=usecase.go Usecase type Usecase interface { GetUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) GetBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) diff --git a/internal/pkg/usecase/subscription/mock/subscription_mock.go b/internal/pkg/usecase/subscription/mock/subscription_mock.go new file mode 100644 index 0000000..6d3aab8 --- /dev/null +++ b/internal/pkg/usecase/subscription/mock/subscription_mock.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + 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 +} + +// GetSubscriptionInfoForUser mocks base method. +func (m *MockUsecase) GetSubscriptionInfoForUser(ctx context.Context, subOpts *user.SubscriptionOpts) ([]user.SubscriptionUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubscriptionInfoForUser", ctx, subOpts) + ret0, _ := ret[0].([]user.SubscriptionUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubscriptionInfoForUser indicates an expected call of GetSubscriptionInfoForUser. +func (mr *MockUsecaseMockRecorder) GetSubscriptionInfoForUser(ctx, subOpts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptionInfoForUser", reflect.TypeOf((*MockUsecase)(nil).GetSubscriptionInfoForUser), ctx, subOpts) +} + +// SubscribeToUser mocks base method. +func (m *MockUsecase) SubscribeToUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeToUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// SubscribeToUser indicates an expected call of SubscribeToUser. +func (mr *MockUsecaseMockRecorder) SubscribeToUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeToUser", reflect.TypeOf((*MockUsecase)(nil).SubscribeToUser), ctx, from, to) +} + +// UnsubscribeFromUser mocks base method. +func (m *MockUsecase) UnsubscribeFromUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnsubscribeFromUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnsubscribeFromUser indicates an expected call of UnsubscribeFromUser. +func (mr *MockUsecaseMockRecorder) UnsubscribeFromUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsubscribeFromUser", reflect.TypeOf((*MockUsecase)(nil).UnsubscribeFromUser), ctx, from, to) +} diff --git a/internal/pkg/usecase/subscription/usecase.go b/internal/pkg/usecase/subscription/usecase.go index 4a88d83..a00705f 100644 --- a/internal/pkg/usecase/subscription/usecase.go +++ b/internal/pkg/usecase/subscription/usecase.go @@ -10,6 +10,7 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate mockgen -destination=./mock/subscription_mock.go -package=mock -source=usecase.go Usecase type Usecase interface { SubscribeToUser(ctx context.Context, from, to int) error UnsubscribeFromUser(ctx context.Context, from, to int) error diff --git a/pkg/logger/option.go b/pkg/logger/option.go index 96db06c..9639478 100644 --- a/pkg/logger/option.go +++ b/pkg/logger/option.go @@ -18,3 +18,15 @@ func SetFormatTime(layout string) ConfigOption { func RFC3339FormatTime() ConfigOption { return SetFormatTime(time.RFC3339) } + +func SetOutputPaths(files ...string) ConfigOption { + return func(cfg *zap.Config) { + cfg.OutputPaths = files + } +} + +func SetErrorOutputPaths(files ...string) ConfigOption { + return func(cfg *zap.Config) { + cfg.ErrorOutputPaths = files + } +}