From aeb18ace026578942582474e72bcf4d7500529d2 Mon Sep 17 00:00:00 2001 From: Evgenia Kivotova Date: Fri, 27 Sep 2024 20:48:50 +0600 Subject: [PATCH 1/2] chat cli client --- libraries/api/chat/v1/chat.pb.go | 156 ++++++-- libraries/api/chat/v1/chat.pb.validate.go | 106 +++++ libraries/api/chat/v1/chat.proto | 6 + libraries/api/chat/v1/chat_grpc.pb.go | 66 +++- services/auth/internal/model/auth.go | 7 +- .../internal/service/auth/get_access_token.go | 1 + .../service/auth/get_refresh_token.go | 1 + services/auth/internal/utils/token/token.go | 1 + services/chat_client/Makefile | 5 + services/chat_client/cmd/cli/connect.go | 41 ++ services/chat_client/cmd/cli/create/chat.go | 56 +++ services/chat_client/cmd/cli/create/create.go | 13 + .../chat_client/cmd/cli/create/message.go | 58 +++ services/chat_client/cmd/cli/root.go | 51 +++ services/chat_client/cmd/main.go | 8 + services/chat_client/go.mod | 36 ++ services/chat_client/go.sum | 51 +++ services/chat_client/internal/app/app.go | 134 +++++++ .../internal/app/service_provider.go | 174 +++++++++ services/chat_client/internal/cli/connect.go | 46 +++ services/chat_client/internal/cli/create.go | 25 ++ services/chat_client/internal/cli/login.go | 19 + .../chat_client/internal/cli/send_message.go | 37 ++ services/chat_client/internal/cli/service.go | 24 ++ .../internal/client/service/auth/client.go | 47 +++ .../internal/client/service/auth/grpc.go | 49 +++ .../internal/client/service/chat/client.go | 86 +++++ .../client/service/chat/converter/chat.go | 15 + .../client/service/chat/converter/message.go | 26 ++ .../internal/client/service/chat/grpc.go | 64 ++++ .../internal/client/service/service.go | 33 ++ .../chat_client/internal/config/config.go | 28 ++ .../chat_client/internal/config/env/grpc.go | 62 +++ .../internal/config/env/profile.go | 45 +++ .../internal/config/ini/profile.go | 65 ++++ services/chat_client/internal/model/chat.go | 10 + .../chat_client/internal/model/message.go | 10 + services/chat_client/internal/utils/token.go | 37 ++ services/chat_server/Makefile | 5 + .../chat_server/internal/api/chat/connect.go | 44 +++ .../chat_server/internal/api/chat/service.go | 4 +- .../chat_server/internal/converter/message.go | 12 + .../internal/model/chat_connection.go | 13 + .../internal/service/chat/connect.go | 91 +++++ .../internal/service/chat/send_message.go | 9 + .../internal/service/chat/service.go | 8 + .../service/mocks/chat_service_minimock.go | 361 ++++++++++++++++++ .../chat_server/internal/service/service.go | 1 + 48 files changed, 2203 insertions(+), 44 deletions(-) create mode 100644 services/chat_client/Makefile create mode 100644 services/chat_client/cmd/cli/connect.go create mode 100644 services/chat_client/cmd/cli/create/chat.go create mode 100644 services/chat_client/cmd/cli/create/create.go create mode 100644 services/chat_client/cmd/cli/create/message.go create mode 100644 services/chat_client/cmd/cli/root.go create mode 100644 services/chat_client/cmd/main.go create mode 100644 services/chat_client/go.mod create mode 100644 services/chat_client/go.sum create mode 100644 services/chat_client/internal/app/app.go create mode 100644 services/chat_client/internal/app/service_provider.go create mode 100644 services/chat_client/internal/cli/connect.go create mode 100644 services/chat_client/internal/cli/create.go create mode 100644 services/chat_client/internal/cli/login.go create mode 100644 services/chat_client/internal/cli/send_message.go create mode 100644 services/chat_client/internal/cli/service.go create mode 100644 services/chat_client/internal/client/service/auth/client.go create mode 100644 services/chat_client/internal/client/service/auth/grpc.go create mode 100644 services/chat_client/internal/client/service/chat/client.go create mode 100644 services/chat_client/internal/client/service/chat/converter/chat.go create mode 100644 services/chat_client/internal/client/service/chat/converter/message.go create mode 100644 services/chat_client/internal/client/service/chat/grpc.go create mode 100644 services/chat_client/internal/client/service/service.go create mode 100644 services/chat_client/internal/config/config.go create mode 100644 services/chat_client/internal/config/env/grpc.go create mode 100644 services/chat_client/internal/config/env/profile.go create mode 100644 services/chat_client/internal/config/ini/profile.go create mode 100644 services/chat_client/internal/model/chat.go create mode 100644 services/chat_client/internal/model/message.go create mode 100644 services/chat_client/internal/utils/token.go create mode 100644 services/chat_server/internal/api/chat/connect.go create mode 100644 services/chat_server/internal/model/chat_connection.go create mode 100644 services/chat_server/internal/service/chat/connect.go diff --git a/libraries/api/chat/v1/chat.pb.go b/libraries/api/chat/v1/chat.pb.go index c61ab36..36696ba 100644 --- a/libraries/api/chat/v1/chat.pb.go +++ b/libraries/api/chat/v1/chat.pb.go @@ -462,6 +462,61 @@ func (x *SendMessageRequest) GetMessage() *Message { return nil } +type ConnectChatRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ChatId int64 `protobuf:"varint,1,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` +} + +func (x *ConnectChatRequest) Reset() { + *x = ConnectChatRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_chat_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConnectChatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectChatRequest) ProtoMessage() {} + +func (x *ConnectChatRequest) ProtoReflect() protoreflect.Message { + mi := &file_chat_proto_msgTypes[8] + 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 ConnectChatRequest.ProtoReflect.Descriptor instead. +func (*ConnectChatRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{8} +} + +func (x *ConnectChatRequest) GetChatId() int64 { + if x != nil { + return x.ChatId + } + return 0 +} + +func (x *ConnectChatRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + var File_chat_proto protoreflect.FileDescriptor var file_chat_proto_rawDesc = []byte{ @@ -509,24 +564,32 @@ var file_chat_proto_rawDesc = []byte{ 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xc1, 0x01, 0x0a, - 0x06, 0x43, 0x68, 0x61, 0x74, 0x56, 0x31, 0x12, 0x39, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x12, 0x16, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x68, 0x61, 0x74, - 0x5f, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x63, - 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0b, - 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x63, 0x68, - 0x61, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x47, - 0x65, 0x6e, 0x76, 0x65, 0x6b, 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x2d, 0x63, 0x68, 0x61, 0x74, 0x2f, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x68, - 0x61, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x49, 0x0a, 0x12, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x68, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x63, 0x68, 0x61, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x81, 0x02, 0x0a, 0x06, 0x43, 0x68, 0x61, 0x74, + 0x56, 0x31, 0x12, 0x39, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x63, + 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, + 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, + 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x68, 0x61, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x68, 0x61, + 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x68, 0x61, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, + 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x42, 0x3b, 0x5a, 0x39, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x47, 0x65, 0x6e, 0x76, 0x65, 0x6b, + 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x2d, 0x63, 0x68, 0x61, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x72, 0x61, + 0x72, 0x69, 0x65, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x68, 0x61, 0x74, 0x2f, 0x76, 0x31, + 0x3b, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -541,7 +604,7 @@ func file_chat_proto_rawDescGZIP() []byte { return file_chat_proto_rawDescData } -var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_chat_proto_goTypes = []interface{}{ (*Chat)(nil), // 0: chat_v1.Chat (*ChatInfo)(nil), // 1: chat_v1.ChatInfo @@ -551,27 +614,30 @@ var file_chat_proto_goTypes = []interface{}{ (*DeleteRequest)(nil), // 5: chat_v1.DeleteRequest (*Message)(nil), // 6: chat_v1.Message (*SendMessageRequest)(nil), // 7: chat_v1.SendMessageRequest - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 9: google.protobuf.Empty + (*ConnectChatRequest)(nil), // 8: chat_v1.ConnectChatRequest + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 10: google.protobuf.Empty } var file_chat_proto_depIdxs = []int32{ - 1, // 0: chat_v1.Chat.info:type_name -> chat_v1.ChatInfo - 2, // 1: chat_v1.Chat.members:type_name -> chat_v1.ChatMember - 8, // 2: chat_v1.Chat.created_at:type_name -> google.protobuf.Timestamp - 8, // 3: chat_v1.ChatMember.joined_at:type_name -> google.protobuf.Timestamp - 8, // 4: chat_v1.Message.timestamp:type_name -> google.protobuf.Timestamp - 6, // 5: chat_v1.SendMessageRequest.message:type_name -> chat_v1.Message - 3, // 6: chat_v1.ChatV1.Create:input_type -> chat_v1.CreateRequest - 5, // 7: chat_v1.ChatV1.Delete:input_type -> chat_v1.DeleteRequest - 7, // 8: chat_v1.ChatV1.SendMessage:input_type -> chat_v1.SendMessageRequest - 4, // 9: chat_v1.ChatV1.Create:output_type -> chat_v1.CreateResponse - 9, // 10: chat_v1.ChatV1.Delete:output_type -> google.protobuf.Empty - 9, // 11: chat_v1.ChatV1.SendMessage:output_type -> google.protobuf.Empty - 9, // [9:12] is the sub-list for method output_type - 6, // [6:9] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 1, // 0: chat_v1.Chat.info:type_name -> chat_v1.ChatInfo + 2, // 1: chat_v1.Chat.members:type_name -> chat_v1.ChatMember + 9, // 2: chat_v1.Chat.created_at:type_name -> google.protobuf.Timestamp + 9, // 3: chat_v1.ChatMember.joined_at:type_name -> google.protobuf.Timestamp + 9, // 4: chat_v1.Message.timestamp:type_name -> google.protobuf.Timestamp + 6, // 5: chat_v1.SendMessageRequest.message:type_name -> chat_v1.Message + 3, // 6: chat_v1.ChatV1.Create:input_type -> chat_v1.CreateRequest + 5, // 7: chat_v1.ChatV1.Delete:input_type -> chat_v1.DeleteRequest + 7, // 8: chat_v1.ChatV1.SendMessage:input_type -> chat_v1.SendMessageRequest + 8, // 9: chat_v1.ChatV1.ConnectChat:input_type -> chat_v1.ConnectChatRequest + 4, // 10: chat_v1.ChatV1.Create:output_type -> chat_v1.CreateResponse + 10, // 11: chat_v1.ChatV1.Delete:output_type -> google.protobuf.Empty + 10, // 12: chat_v1.ChatV1.SendMessage:output_type -> google.protobuf.Empty + 6, // 13: chat_v1.ChatV1.ConnectChat:output_type -> chat_v1.Message + 10, // [10:14] is the sub-list for method output_type + 6, // [6:10] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_chat_proto_init() } @@ -676,6 +742,18 @@ func file_chat_proto_init() { return nil } } + file_chat_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConnectChatRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -683,7 +761,7 @@ func file_chat_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_chat_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/libraries/api/chat/v1/chat.pb.validate.go b/libraries/api/chat/v1/chat.pb.validate.go index 668ff08..74f376b 100644 --- a/libraries/api/chat/v1/chat.pb.validate.go +++ b/libraries/api/chat/v1/chat.pb.validate.go @@ -1027,3 +1027,109 @@ var _ interface { Cause() error ErrorName() string } = SendMessageRequestValidationError{} + +// Validate checks the field values on ConnectChatRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *ConnectChatRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ConnectChatRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ConnectChatRequestMultiError, or nil if none found. +func (m *ConnectChatRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *ConnectChatRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for ChatId + + // no validation rules for Username + + if len(errors) > 0 { + return ConnectChatRequestMultiError(errors) + } + + return nil +} + +// ConnectChatRequestMultiError is an error wrapping multiple validation errors +// returned by ConnectChatRequest.ValidateAll() if the designated constraints +// aren't met. +type ConnectChatRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ConnectChatRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ConnectChatRequestMultiError) AllErrors() []error { return m } + +// ConnectChatRequestValidationError is the validation error returned by +// ConnectChatRequest.Validate if the designated constraints aren't met. +type ConnectChatRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ConnectChatRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ConnectChatRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ConnectChatRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ConnectChatRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ConnectChatRequestValidationError) ErrorName() string { + return "ConnectChatRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e ConnectChatRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sConnectChatRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ConnectChatRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ConnectChatRequestValidationError{} diff --git a/libraries/api/chat/v1/chat.proto b/libraries/api/chat/v1/chat.proto index b664406..556bfe9 100644 --- a/libraries/api/chat/v1/chat.proto +++ b/libraries/api/chat/v1/chat.proto @@ -12,6 +12,7 @@ service ChatV1{ rpc Create(CreateRequest) returns (CreateResponse); rpc Delete(DeleteRequest) returns (google.protobuf.Empty); rpc SendMessage(SendMessageRequest) returns (google.protobuf.Empty); + rpc ConnectChat (ConnectChatRequest) returns (stream Message); } message Chat { @@ -52,4 +53,9 @@ message Message { message SendMessageRequest { Message message = 4; +} + +message ConnectChatRequest { + int64 chat_id = 1; + string username = 2; } \ No newline at end of file diff --git a/libraries/api/chat/v1/chat_grpc.pb.go b/libraries/api/chat/v1/chat_grpc.pb.go index f64ddf3..051915f 100644 --- a/libraries/api/chat/v1/chat_grpc.pb.go +++ b/libraries/api/chat/v1/chat_grpc.pb.go @@ -26,6 +26,7 @@ type ChatV1Client interface { Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ConnectChat(ctx context.Context, in *ConnectChatRequest, opts ...grpc.CallOption) (ChatV1_ConnectChatClient, error) } type chatV1Client struct { @@ -63,6 +64,38 @@ func (c *chatV1Client) SendMessage(ctx context.Context, in *SendMessageRequest, return out, nil } +func (c *chatV1Client) ConnectChat(ctx context.Context, in *ConnectChatRequest, opts ...grpc.CallOption) (ChatV1_ConnectChatClient, error) { + stream, err := c.cc.NewStream(ctx, &ChatV1_ServiceDesc.Streams[0], "/chat_v1.ChatV1/ConnectChat", opts...) + if err != nil { + return nil, err + } + x := &chatV1ConnectChatClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type ChatV1_ConnectChatClient interface { + Recv() (*Message, error) + grpc.ClientStream +} + +type chatV1ConnectChatClient struct { + grpc.ClientStream +} + +func (x *chatV1ConnectChatClient) Recv() (*Message, error) { + m := new(Message) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // ChatV1Server is the server API for ChatV1 service. // All implementations must embed UnimplementedChatV1Server // for forward compatibility @@ -70,6 +103,7 @@ type ChatV1Server interface { Create(context.Context, *CreateRequest) (*CreateResponse, error) Delete(context.Context, *DeleteRequest) (*emptypb.Empty, error) SendMessage(context.Context, *SendMessageRequest) (*emptypb.Empty, error) + ConnectChat(*ConnectChatRequest, ChatV1_ConnectChatServer) error mustEmbedUnimplementedChatV1Server() } @@ -86,6 +120,9 @@ func (UnimplementedChatV1Server) Delete(context.Context, *DeleteRequest) (*empty func (UnimplementedChatV1Server) SendMessage(context.Context, *SendMessageRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SendMessage not implemented") } +func (UnimplementedChatV1Server) ConnectChat(*ConnectChatRequest, ChatV1_ConnectChatServer) error { + return status.Errorf(codes.Unimplemented, "method ConnectChat not implemented") +} func (UnimplementedChatV1Server) mustEmbedUnimplementedChatV1Server() {} // UnsafeChatV1Server may be embedded to opt out of forward compatibility for this service. @@ -153,6 +190,27 @@ func _ChatV1_SendMessage_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _ChatV1_ConnectChat_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ConnectChatRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ChatV1Server).ConnectChat(m, &chatV1ConnectChatServer{stream}) +} + +type ChatV1_ConnectChatServer interface { + Send(*Message) error + grpc.ServerStream +} + +type chatV1ConnectChatServer struct { + grpc.ServerStream +} + +func (x *chatV1ConnectChatServer) Send(m *Message) error { + return x.ServerStream.SendMsg(m) +} + // ChatV1_ServiceDesc is the grpc.ServiceDesc for ChatV1 service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -173,6 +231,12 @@ var ChatV1_ServiceDesc = grpc.ServiceDesc{ Handler: _ChatV1_SendMessage_Handler, }, }, - Streams: []grpc.StreamDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "ConnectChat", + Handler: _ChatV1_ConnectChat_Handler, + ServerStreams: true, + }, + }, Metadata: "chat.proto", } diff --git a/services/auth/internal/model/auth.go b/services/auth/internal/model/auth.go index 682eda6..d1b4e6e 100644 --- a/services/auth/internal/model/auth.go +++ b/services/auth/internal/model/auth.go @@ -4,7 +4,8 @@ import "github.com/golang-jwt/jwt" // UserClaims is data for token type UserClaims struct { - jwt.StandardClaims - Username string `json:"username"` - Role int `json:"role"` + jwt.StandardClaims + ID int64 `json:"id"` + Username string `json:"username"` + Role int `json:"role"` } diff --git a/services/auth/internal/service/auth/get_access_token.go b/services/auth/internal/service/auth/get_access_token.go index 56bb298..c480236 100644 --- a/services/auth/internal/service/auth/get_access_token.go +++ b/services/auth/internal/service/auth/get_access_token.go @@ -15,6 +15,7 @@ func (s *authService) GetAccessToken(ctx context.Context, refreshToken string) ( } accessToken, err := s.accessTokenProvider.Generate(ctx, &model.User{ + ID: claims.ID, Name: claims.Username, Role: claims.Role, }) diff --git a/services/auth/internal/service/auth/get_refresh_token.go b/services/auth/internal/service/auth/get_refresh_token.go index 4272c8d..0d0bf94 100644 --- a/services/auth/internal/service/auth/get_refresh_token.go +++ b/services/auth/internal/service/auth/get_refresh_token.go @@ -15,6 +15,7 @@ func (s *authService) GetRefreshToken(ctx context.Context, oldRefreshTocken stri } refreshToken, err := s.refreshTokenProvider.Generate(ctx, &model.User{ + ID: claims.ID, Name: claims.Username, Role: claims.Role, }) diff --git a/services/auth/internal/utils/token/token.go b/services/auth/internal/utils/token/token.go index 8483e51..652b3d5 100644 --- a/services/auth/internal/utils/token/token.go +++ b/services/auth/internal/utils/token/token.go @@ -38,6 +38,7 @@ func (t *tokenProvider) Generate(ctx context.Context, user *model.User) (string, StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(t.ttl).Unix(), }, + ID: user.ID, Username: user.Name, Role: user.Role, } diff --git a/services/chat_client/Makefile b/services/chat_client/Makefile new file mode 100644 index 0000000..676f18a --- /dev/null +++ b/services/chat_client/Makefile @@ -0,0 +1,5 @@ +LOCAL_BIN:=$(CURDIR)/bin + +build: + go build -o ${LOCAL_BIN}/cli-chat cmd/main.go + chmod 777 bin/cli-chat diff --git a/services/chat_client/cmd/cli/connect.go b/services/chat_client/cmd/cli/connect.go new file mode 100644 index 0000000..58a8e86 --- /dev/null +++ b/services/chat_client/cmd/cli/connect.go @@ -0,0 +1,41 @@ +package cli + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/app" +) + +const ( + connectFlagChatID = "chat_id" + connectFlagChatIDShort = "i" +) + +var connectCmd = &cobra.Command{ + Use: "connect", + Short: "Connect to chat", + RunE: func(cmd *cobra.Command, args []string) error { + chatID, err := cmd.Flags().GetInt64(connectFlagChatID) + if err != nil { + return fmt.Errorf("failed to read flag %s: %v", connectFlagChatID, err) + } + + err = app.Connect(cmd.Context(), chatID) + if err != nil { + return fmt.Errorf("error occurred during connection: %v", err) + } + + return nil + }, +} + +func init() { + connectCmd.Flags().Int64P(connectFlagChatID, connectFlagChatIDShort, 0, "Id of a chat") + err := connectCmd.MarkFlagRequired(connectFlagChatID) + if err != nil { + os.Exit(1) + } +} diff --git a/services/chat_client/cmd/cli/create/chat.go b/services/chat_client/cmd/cli/create/chat.go new file mode 100644 index 0000000..708eb3b --- /dev/null +++ b/services/chat_client/cmd/cli/create/chat.go @@ -0,0 +1,56 @@ +package create + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/app" +) + +const ( + chatFlagName = "name" + chatFlagNameShort = "n" + + chatFlagUsernames = "usernames" + chatFlagUsernamesShort = "u" +) + +var chatCmd = &cobra.Command{ + Use: "chat", + Short: "Create new chat", + RunE: func(cmd *cobra.Command, args []string) error { + name, err := cmd.Flags().GetString(chatFlagName) + if err != nil { + return fmt.Errorf("failed to read flag %s: %v", chatFlagName, err) + } + + usernames, err := cmd.Flags().GetStringSlice(chatFlagUsernames) + if err != nil { + + return fmt.Errorf("failed to read flag %s: %v", chatFlagUsernames, err) + } + + err = app.CreateChat(cmd.Context(), name, usernames) + if err != nil { + return fmt.Errorf("failed to create chat: %v", err) + } + + return nil + }, +} + +func init() { + chatCmd.Flags().StringP(chatFlagName, chatFlagNameShort, "", "Name of a chat") + err := chatCmd.MarkFlagRequired(chatFlagName) + if err != nil { + os.Exit(1) + } + + chatCmd.Flags().StringSliceP(chatFlagUsernames, chatFlagUsernamesShort, []string{}, "Usernames of participants") + err = chatCmd.MarkFlagRequired(chatFlagUsernames) + if err != nil { + os.Exit(1) + } +} diff --git a/services/chat_client/cmd/cli/create/create.go b/services/chat_client/cmd/cli/create/create.go new file mode 100644 index 0000000..6e326eb --- /dev/null +++ b/services/chat_client/cmd/cli/create/create.go @@ -0,0 +1,13 @@ +package create + +import "github.com/spf13/cobra" + +var CreateCmd = &cobra.Command{ + Use: "create", + Short: "Create something", +} + +func init() { + CreateCmd.AddCommand(chatCmd) + CreateCmd.AddCommand(messageCmd) +} diff --git a/services/chat_client/cmd/cli/create/message.go b/services/chat_client/cmd/cli/create/message.go new file mode 100644 index 0000000..7a1eec2 --- /dev/null +++ b/services/chat_client/cmd/cli/create/message.go @@ -0,0 +1,58 @@ +package create + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/app" +) + +const ( + msgFlagChatID = "chat_id" + msgFlagChatIDShort = "i" + + msgFlagMessage = "message" + msgFlagMessageShort = "m" +) + +var messageCmd = &cobra.Command{ + Use: "message", + Short: "Create new message", + RunE: func(cmd *cobra.Command, args []string) error { + chatID, err := cmd.Flags().GetInt64(msgFlagChatID) + if err != nil { + return fmt.Errorf("failed to read flag %s: %v", msgFlagChatID, err) + } + + message, err := cmd.Flags().GetString(msgFlagMessage) + if err != nil { + return fmt.Errorf("failed to read flag %s: %v", msgFlagMessage, err) + } + + if message == "" { + return fmt.Errorf("message is too short") + } + + err = app.SendMessage(cmd.Context(), chatID, message) + if err != nil { + return fmt.Errorf("failed to send message: %v", err) + } + + return nil + }, +} + +func init() { + messageCmd.Flags().Int64P(msgFlagChatID, msgFlagChatIDShort, 0, "Id of a chat") + err := messageCmd.MarkFlagRequired(msgFlagChatID) + if err != nil { + os.Exit(1) + } + messageCmd.Flags().StringP(msgFlagMessage, msgFlagMessageShort, "", "Text of a message") + err = messageCmd.MarkFlagRequired(msgFlagMessage) + if err != nil { + os.Exit(1) + } +} diff --git a/services/chat_client/cmd/cli/root.go b/services/chat_client/cmd/cli/root.go new file mode 100644 index 0000000..13f92dd --- /dev/null +++ b/services/chat_client/cmd/cli/root.go @@ -0,0 +1,51 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/Genvekt/cli-chat/services/chat-client/cmd/cli/create" + "github.com/Genvekt/cli-chat/services/chat-client/internal/app" +) + +var ( + profileName string + iniConfig string + envConfig string +) + +var ( + rootCmd = &cobra.Command{ + Use: "cli-chat", + Short: "CLI Chat application", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // We initialise application before execution any command + var err error + + err = app.InitApp(cmd.Context(), profileName, iniConfig, envConfig) + if err != nil { + return fmt.Errorf("failed to initialise application: %v", err) + } + + return nil + }, + } +) + +func init() { + rootCmd.PersistentFlags().StringVar(&profileName, "profile", "", "profile name") + rootCmd.PersistentFlags().StringVar(&iniConfig, "ini-config", "config.ini", "ini config path") + rootCmd.PersistentFlags().StringVar(&envConfig, "env-config", ".env", "env config path") + + rootCmd.AddCommand(create.CreateCmd) + rootCmd.AddCommand(connectCmd) +} + +// Execute acts as application entrypoint +func Execute() { + err := rootCmd.Execute() + if err != nil { + fmt.Printf("Error: %v\n", err) + } +} diff --git a/services/chat_client/cmd/main.go b/services/chat_client/cmd/main.go new file mode 100644 index 0000000..00d9ec5 --- /dev/null +++ b/services/chat_client/cmd/main.go @@ -0,0 +1,8 @@ +package main + +import "github.com/Genvekt/cli-chat/services/chat-client/cmd/cli" + +func main() { + cli.Execute() + +} diff --git a/services/chat_client/go.mod b/services/chat_client/go.mod new file mode 100644 index 0000000..760f332 --- /dev/null +++ b/services/chat_client/go.mod @@ -0,0 +1,36 @@ +module github.com/Genvekt/cli-chat/services/chat-client + +replace github.com/Genvekt/cli-chat/libraries/api => ../../libraries/api + +replace github.com/Genvekt/cli-chat/libraries/closer => ../../libraries/closer + +replace github.com/Genvekt/cli-chat/libraries/logger => ../../libraries/logger + +go 1.22.5 + +require ( + github.com/Genvekt/cli-chat/libraries/api v0.0.0-00010101000000-000000000000 + github.com/Genvekt/cli-chat/libraries/closer v0.0.0-00010101000000-000000000000 + github.com/Genvekt/cli-chat/libraries/logger v0.0.0-00010101000000-000000000000 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/joho/godotenv v1.5.1 + github.com/natefinch/lumberjack v2.0.0+incompatible + github.com/spf13/cobra v1.8.1 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.66.2 + google.golang.org/protobuf v1.34.2 + gopkg.in/ini.v1 v1.67.0 +) + +require ( + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/services/chat_client/go.sum b/services/chat_client/go.sum new file mode 100644 index 0000000..7385ec8 --- /dev/null +++ b/services/chat_client/go.sum @@ -0,0 +1,51 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/services/chat_client/internal/app/app.go b/services/chat_client/internal/app/app.go new file mode 100644 index 0000000..534270c --- /dev/null +++ b/services/chat_client/internal/app/app.go @@ -0,0 +1,134 @@ +package app + +import ( + "context" + "fmt" + "os" + + "github.com/natefinch/lumberjack" + "github.com/spf13/cobra" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" + "github.com/Genvekt/cli-chat/services/chat-client/internal/cli" + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" +) + +var ( + profileName string + iniConfig string + envConfig string + + application *App +) + +type App struct { + provider *ServiceProvider + cliRoot *cobra.Command + cliService cli.CliService +} + +// InitApp initialises app and all its dependencies +func InitApp(ctx context.Context, profile string, iniConf string, envConf string) error { + profileName = profile + iniConfig = iniConf + envConfig = envConf + + application = &App{} + + err := application.initDeps(ctx) + if err != nil { + return err + } + + return nil +} + +func (a *App) initDeps(ctx context.Context) error { + deps := []func(context.Context) error{ + a.initConfig, + a.initLogger, + a.initServiceProvider, + } + + for _, dep := range deps { + if err := dep(ctx); err != nil { + return err + } + } + + return nil +} + +func (a *App) initConfig(_ context.Context) error { + err := config.LoadEnv(envConfig) + if err != nil { + return err + } + + return nil +} + +func (a *App) initLogger(_ context.Context) error { + logLevel, err := a.getLogAtomicLevel() + if err != nil { + return err + } + logger.Init(a.getLogCore(*logLevel)) + return nil +} + +func (a *App) initServiceProvider(_ context.Context) error { + a.provider = newServiceProvider(iniConfig, profileName) + + return nil +} + +func CreateChat(ctx context.Context, name string, usernames []string) error { + return application.provider.ChatCliService().CreateChat(ctx, name, usernames) +} + +func Connect(ctx context.Context, chatID int64) error { + return application.provider.ChatCliService().Connect(ctx, chatID) +} + +func SendMessage(ctx context.Context, chatID int64, message string) error { + return application.provider.ChatCliService().SendMessage(ctx, chatID, message) +} + +func (a *App) getLogCore(level zap.AtomicLevel) zapcore.Core { + stdout := zapcore.AddSync(os.Stdout) + + file := zapcore.AddSync(&lumberjack.Logger{ + Filename: "logs/app.log", + MaxSize: 10, // megabytes + MaxBackups: 3, + MaxAge: 7, // days + }) + + productionCfg := zap.NewProductionEncoderConfig() + productionCfg.TimeKey = "timestamp" + productionCfg.EncodeTime = zapcore.ISO8601TimeEncoder + + developmentCfg := zap.NewDevelopmentEncoderConfig() + developmentCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder + + consoleEncoder := zapcore.NewConsoleEncoder(developmentCfg) + fileEncoder := zapcore.NewJSONEncoder(productionCfg) + + return zapcore.NewTee( + zapcore.NewCore(consoleEncoder, stdout, level), + zapcore.NewCore(fileEncoder, file, level), + ) +} + +func (a *App) getLogAtomicLevel() (*zap.AtomicLevel, error) { + var level zapcore.Level + if err := level.Set(zapcore.InfoLevel.String()); err != nil { + return nil, fmt.Errorf("failed to set log level: %v", err) + } + atomicLevel := zap.NewAtomicLevelAt(level) + + return &atomicLevel, nil +} diff --git a/services/chat_client/internal/app/service_provider.go b/services/chat_client/internal/app/service_provider.go new file mode 100644 index 0000000..8377697 --- /dev/null +++ b/services/chat_client/internal/app/service_provider.go @@ -0,0 +1,174 @@ +package app + +import ( + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/Genvekt/cli-chat/libraries/closer/pkg/closer" + "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" + "github.com/Genvekt/cli-chat/services/chat-client/internal/cli" + "github.com/Genvekt/cli-chat/services/chat-client/internal/config/ini" + + chatCli "github.com/Genvekt/cli-chat/services/chat-client/internal/cli" + serviceClient "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service" + authClient "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service/auth" + chatClient "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service/chat" + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" + "github.com/Genvekt/cli-chat/services/chat-client/internal/config/env" +) + +type ServiceProvider struct { + iniConfPath string + profileName string + + chatConnection *grpc.ClientConn + chatGRPCConfig config.GRPCConfig + chatClient serviceClient.ChatClient + + authConnection *grpc.ClientConn + authGRPCConfig config.GRPCConfig + authClient serviceClient.AuthClient + + profileConfig config.ProfileConfig + chatCliService *cli.CliService +} + +func newServiceProvider(iniConfPath string, profileName string) *ServiceProvider { + return &ServiceProvider{ + iniConfPath: iniConfPath, + profileName: profileName, + } +} + +func (s *ServiceProvider) ChatGRPCConfig() config.GRPCConfig { + if s.chatGRPCConfig == nil { + conf, err := env.NewChatGRPCConfigEnv() + if err != nil { + logger.Fatal( + "cannot load chat client env vars: %v", + zap.Error(err), + ) + } + + s.chatGRPCConfig = conf + } + + return s.chatGRPCConfig +} + +func (s *ServiceProvider) AuthGRPCConfig() config.GRPCConfig { + if s.authGRPCConfig == nil { + conf, err := env.NewAuthGRPCConfigEnv() + if err != nil { + logger.Fatal( + "cannot load chat client env vars: %v", + zap.Error(err), + ) + } + + s.authGRPCConfig = conf + } + + return s.authGRPCConfig +} + +// ChatConnection provides grpc connection to chat service +func (s *ServiceProvider) ChatConnection() *grpc.ClientConn { + if s.chatConnection == nil { + var err error + creds := insecure.NewCredentials() + + conn, err := grpc.NewClient( + s.ChatGRPCConfig().Address(), + grpc.WithTransportCredentials(creds), + ) + if err != nil { + logger.Fatal("failed to connect to chat service", zap.Error(err)) + } + + closer.Add(conn.Close) + + s.chatConnection = conn + } + + return s.chatConnection +} + +// AuthConnection provides grpc connection to auth service +func (s *ServiceProvider) AuthConnection() *grpc.ClientConn { + if s.authConnection == nil { + var err error + creds := insecure.NewCredentials() + + conn, err := grpc.NewClient( + s.AuthGRPCConfig().Address(), + grpc.WithTransportCredentials(creds), + ) + if err != nil { + logger.Fatal("failed to connect to auth service", zap.Error(err)) + } + + closer.Add(conn.Close) + + s.authConnection = conn + } + + return s.authConnection +} + +// ChatClient provides chat service client dependency +func (s *ServiceProvider) ChatClient() serviceClient.ChatClient { + if s.chatClient == nil { + s.chatClient = chatClient.NewChatClient(chatClient.NewChatGrpcClientWrapper(s.ChatConnection())) + } + + return s.chatClient +} + +// AuthClient provides auth service client dependency +func (s *ServiceProvider) AuthClient() serviceClient.AuthClient { + if s.authClient == nil { + s.authClient = authClient.NewAuthClient(authClient.NewAuthGrpcClientWrapper(s.AuthConnection())) + } + + return s.authClient +} + +func (s *ServiceProvider) ProfileConfig(configPath string, profileName string) config.ProfileConfig { + if s.profileConfig == nil { + if profileName == "" { + // try to get profile from env before referencing ini config + confEnv, err := env.NewProfileConfigEnv() + if err == nil { + s.profileConfig = confEnv + } + } + + if s.profileConfig == nil { + // try to get profile from ini config + confIni, err := ini.NewProfileConfigIni(configPath, profileName) + if err != nil { + logger.Fatal("cannot load profile data", zap.Error(err)) + } + + s.profileConfig = confIni + + } + } + + return s.profileConfig +} + +// ChatCliService provides chat cli application dependency +func (s *ServiceProvider) ChatCliService() *cli.CliService { + if s.chatCliService == nil { + s.chatCliService = chatCli.NewChatCliService( + s.ProfileConfig(s.iniConfPath, s.profileName), + s.ChatClient(), + s.AuthClient(), + ) + } + + return s.chatCliService +} diff --git a/services/chat_client/internal/cli/connect.go b/services/chat_client/internal/cli/connect.go new file mode 100644 index 0000000..6cce7e8 --- /dev/null +++ b/services/chat_client/internal/cli/connect.go @@ -0,0 +1,46 @@ +package cli + +import ( + "context" + "fmt" + "time" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/utils" +) + +func (s *CliService) Connect(ctx context.Context, chatID int64) error { + accessToken, err := s.login(ctx) + if err != nil { + return fmt.Errorf("failed to login: %v", err) + } + + ctxWithToken := utils.PutAccessTokenToCtx(ctx, accessToken) + userID, err := utils.GetUserIdFromToken(accessToken) + if err != nil { + return fmt.Errorf("missing current user id in token") + } + + msgChannel, err := s.chatClient.Connect(ctxWithToken, chatID, s.profileConfig.Username()) + if err != nil { + return fmt.Errorf("failed to connect to chat: %v", err) + } + + fmt.Printf("Connected to chat with ID %d\n", chatID) + + for { + select { + case msg, ok := <-msgChannel: + if !ok { + return nil + } + + if msg.SenderID == userID { + fmt.Printf("[%s] YOU: %s\n", msg.Timestamp.Format(time.DateTime), msg.Text) + } else { + fmt.Printf("[%s] %d: %s\n", msg.Timestamp.Format(time.DateTime), msg.SenderID, msg.Text) + } + case <-ctx.Done(): + return nil + } + } +} diff --git a/services/chat_client/internal/cli/create.go b/services/chat_client/internal/cli/create.go new file mode 100644 index 0000000..54d598f --- /dev/null +++ b/services/chat_client/internal/cli/create.go @@ -0,0 +1,25 @@ +package cli + +import ( + "context" + "fmt" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/utils" +) + +func (s *CliService) CreateChat(ctx context.Context, name string, usernames []string) error { + token, err := s.login(ctx) + if err != nil { + return fmt.Errorf("failed to login: %v", err) + } + + ctxWithToken := utils.PutAccessTokenToCtx(ctx, token) + + chatID, err := s.chatClient.Create(ctxWithToken, name, usernames) + if err != nil { + return err + } + + fmt.Printf("created chat '%s' (id: %d)", name, chatID) + return nil +} diff --git a/services/chat_client/internal/cli/login.go b/services/chat_client/internal/cli/login.go new file mode 100644 index 0000000..7ec6305 --- /dev/null +++ b/services/chat_client/internal/cli/login.go @@ -0,0 +1,19 @@ +package cli + +import ( + "context" +) + +func (s *CliService) login(ctx context.Context) (string, error) { + refreshToken, err := s.authClient.Login(ctx, s.profileConfig.Username(), s.profileConfig.Password()) + if err != nil { + return "", err + } + + accessToken, err := s.authClient.GetAccessToken(ctx, refreshToken) + if err != nil { + return "", err + } + + return accessToken, nil +} diff --git a/services/chat_client/internal/cli/send_message.go b/services/chat_client/internal/cli/send_message.go new file mode 100644 index 0000000..829759f --- /dev/null +++ b/services/chat_client/internal/cli/send_message.go @@ -0,0 +1,37 @@ +package cli + +import ( + "context" + "fmt" + "time" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/model" + "github.com/Genvekt/cli-chat/services/chat-client/internal/utils" +) + +func (s *CliService) SendMessage(ctx context.Context, chatID int64, message string) error { + accessToken, err := s.login(ctx) + if err != nil { + return fmt.Errorf("failed to login: %v", err) + } + + ctxWithToken := utils.PutAccessTokenToCtx(ctx, accessToken) + + userID, err := utils.GetUserIdFromToken(accessToken) + if err != nil { + return fmt.Errorf("missing id if current user in token") + } + + err = s.chatClient.SendMessage(ctxWithToken, &model.Message{ + ChatID: chatID, + SenderID: userID, + Text: message, + Timestamp: time.Now(), + }) + + if err != nil { + return fmt.Errorf("failed to send message: %v", err) + } + + return nil +} diff --git a/services/chat_client/internal/cli/service.go b/services/chat_client/internal/cli/service.go new file mode 100644 index 0000000..1531d75 --- /dev/null +++ b/services/chat_client/internal/cli/service.go @@ -0,0 +1,24 @@ +package cli + +import ( + clientService "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service" + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" +) + +type CliService struct { + profileConfig config.ProfileConfig + chatClient clientService.ChatClient + authClient clientService.AuthClient +} + +func NewChatCliService( + profileConfig config.ProfileConfig, + chatClient clientService.ChatClient, + authClient clientService.AuthClient, +) *CliService { + return &CliService{ + profileConfig: profileConfig, + chatClient: chatClient, + authClient: authClient, + } +} diff --git a/services/chat_client/internal/client/service/auth/client.go b/services/chat_client/internal/client/service/auth/client.go new file mode 100644 index 0000000..1961c5e --- /dev/null +++ b/services/chat_client/internal/client/service/auth/client.go @@ -0,0 +1,47 @@ +package auth + +import ( + "context" + "fmt" + + authApi "github.com/Genvekt/cli-chat/libraries/api/auth/v1" + "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service" +) + +var _ service.AuthClient = (*authGRPCClient)(nil) + +type authGRPCClient struct { + client service.AuthGRPCClient +} + +// NewAuthClient initialises grpc client to auth service +func NewAuthClient(client service.AuthGRPCClient) *authGRPCClient { + return &authGRPCClient{ + client: client, + } +} + +// Login retrieves refresh token for user +func (c *authGRPCClient) Login(ctx context.Context, username string, password string) (string, error) { + req := &authApi.LoginRequest{ + Username: username, + Password: password, + } + + res, err := c.client.Login(ctx, req) + if err != nil { + return "", fmt.Errorf("failed to login: %v", err) + } + + return res.RefreshToken, nil +} + +// GetAccessToken retrieves access token by refresh token +func (c *authGRPCClient) GetAccessToken(ctx context.Context, refreshToken string) (string, error) { + resp, err := c.client.GetAccessToken(ctx, &authApi.GetAccessTokenRequest{RefreshToken: refreshToken}) + if err != nil { + return "", fmt.Errorf("failed to retrieve access token: %v", err) + } + + return resp.AccessToken, nil +} diff --git a/services/chat_client/internal/client/service/auth/grpc.go b/services/chat_client/internal/client/service/auth/grpc.go new file mode 100644 index 0000000..e44b7b8 --- /dev/null +++ b/services/chat_client/internal/client/service/auth/grpc.go @@ -0,0 +1,49 @@ +package auth + +import ( + "context" + + "google.golang.org/grpc" + + authApi "github.com/Genvekt/cli-chat/libraries/api/auth/v1" + "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service" +) + +var _ service.AuthGRPCClient = (*authGrpcClientWrapper)(nil) + +type authGrpcClientWrapper struct { + client authApi.AuthV1Client +} + +// NewAuthGrpcClientWrapper initialises wrapper around grpc client +func NewAuthGrpcClientWrapper(conn *grpc.ClientConn) *authGrpcClientWrapper { + return &authGrpcClientWrapper{ + client: authApi.NewAuthV1Client(conn), + } +} + +// Login performs login request +func (c *authGrpcClientWrapper) Login( + ctx context.Context, + req *authApi.LoginRequest, +) (*authApi.LoginResponse, error) { + + resp, err := c.client.Login(ctx, req) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *authGrpcClientWrapper) GetAccessToken( + ctx context.Context, + req *authApi.GetAccessTokenRequest, +) (*authApi.GetAccessTokenResponse, error) { + resp, err := c.client.GetAccessToken(ctx, req) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/services/chat_client/internal/client/service/chat/client.go b/services/chat_client/internal/client/service/chat/client.go new file mode 100644 index 0000000..ab4aafe --- /dev/null +++ b/services/chat_client/internal/client/service/chat/client.go @@ -0,0 +1,86 @@ +package chat + +import ( + "context" + "fmt" + "io" + + "go.uber.org/zap" + + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" + "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" + "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service" + "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service/chat/converter" + "github.com/Genvekt/cli-chat/services/chat-client/internal/model" +) + +var _ service.ChatClient = (*chatGRPCClient)(nil) + +type chatGRPCClient struct { + client service.ChatGRPCClient +} + +// NewChatClient initialises grpc client to chat service +func NewChatClient(client service.ChatGRPCClient) *chatGRPCClient { + return &chatGRPCClient{ + client: client, + } +} + +// Create creates new chat +func (c *chatGRPCClient) Create(ctx context.Context, name string, usernames []string) (int64, error) { + req := &chatApi.CreateRequest{ + Name: name, + Usernames: usernames, + } + + res, err := c.client.Create(ctx, req) + if err != nil { + return 0, fmt.Errorf("failed to create chat: %v", err) + } + + return res.Id, nil +} + +func (c *chatGRPCClient) SendMessage(ctx context.Context, message *model.Message) error { + messageProto := converter.ToProtoFromMessage(message) + _, err := c.client.SendMessage(ctx, &chatApi.SendMessageRequest{ + Message: messageProto, + }) + + if err != nil { + return fmt.Errorf("failed to create message: %v", err) + } + + return nil +} + +func (c *chatGRPCClient) Connect(ctx context.Context, chatID int64, username string) (chan *model.Message, error) { + stream, err := c.client.Connect(ctx, &chatApi.ConnectChatRequest{ChatId: chatID, Username: username}) + if err != nil { + return nil, fmt.Errorf("failed to connect to chat: %v", err) + } + + messageChannel := make(chan *model.Message, 100) + + go func() { + for { + resp, err := stream.Recv() + if err == io.EOF { + close(messageChannel) + return + } + + if err != nil { + close(messageChannel) + logger.Error("error receiving message from chat", zap.Error(err)) + return + } + + message := converter.FromProtoToMessage(resp) + messageChannel <- message + } + }() + + return messageChannel, nil +} diff --git a/services/chat_client/internal/client/service/chat/converter/chat.go b/services/chat_client/internal/client/service/chat/converter/chat.go new file mode 100644 index 0000000..aeec6fe --- /dev/null +++ b/services/chat_client/internal/client/service/chat/converter/chat.go @@ -0,0 +1,15 @@ +package converter + +import ( + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" + "github.com/Genvekt/cli-chat/services/chat-client/internal/model" +) + +// ToChatFromProto converts user model of repository layer to user model of service layer +func ToChatFromProto(chat *chatApi.Chat) *model.Chat { + return &model.Chat{ + ID: chat.Id, + Name: chat.Info.Name, + CreatedAt: chat.CreatedAt.AsTime(), + } +} diff --git a/services/chat_client/internal/client/service/chat/converter/message.go b/services/chat_client/internal/client/service/chat/converter/message.go new file mode 100644 index 0000000..3d9fdd5 --- /dev/null +++ b/services/chat_client/internal/client/service/chat/converter/message.go @@ -0,0 +1,26 @@ +package converter + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" + "github.com/Genvekt/cli-chat/services/chat-client/internal/model" +) + +func ToProtoFromMessage(message *model.Message) *chatApi.Message { + return &chatApi.Message{ + SenderId: message.SenderID, + ChatId: message.ChatID, + Text: message.Text, + Timestamp: timestamppb.New(message.Timestamp), + } +} + +func FromProtoToMessage(protoMessage *chatApi.Message) *model.Message { + return &model.Message{ + SenderID: protoMessage.SenderId, + ChatID: protoMessage.ChatId, + Text: protoMessage.Text, + Timestamp: protoMessage.Timestamp.AsTime(), + } +} diff --git a/services/chat_client/internal/client/service/chat/grpc.go b/services/chat_client/internal/client/service/chat/grpc.go new file mode 100644 index 0000000..a6e080a --- /dev/null +++ b/services/chat_client/internal/client/service/chat/grpc.go @@ -0,0 +1,64 @@ +package chat + +import ( + "context" + + "google.golang.org/protobuf/types/known/emptypb" + + "google.golang.org/grpc" + + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" + "github.com/Genvekt/cli-chat/services/chat-client/internal/client/service" +) + +var _ service.ChatGRPCClient = (*chatGrpcClientWrapper)(nil) + +type chatGrpcClientWrapper struct { + client chatApi.ChatV1Client +} + +// NewChatGrpcClientWrapper initialises wrapper around grpc client +func NewChatGrpcClientWrapper(conn *grpc.ClientConn) *chatGrpcClientWrapper { + return &chatGrpcClientWrapper{ + client: chatApi.NewChatV1Client(conn), + } +} + +// Create performs Create chat request +func (c *chatGrpcClientWrapper) Create( + ctx context.Context, + req *chatApi.CreateRequest, +) (*chatApi.CreateResponse, error) { + + resp, err := c.client.Create(ctx, req) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *chatGrpcClientWrapper) SendMessage( + ctx context.Context, + req *chatApi.SendMessageRequest, +) (*emptypb.Empty, error) { + + resp, err := c.client.SendMessage(ctx, req) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *chatGrpcClientWrapper) Connect( + ctx context.Context, + req *chatApi.ConnectChatRequest, +) (chatApi.ChatV1_ConnectChatClient, error) { + resp, err := c.client.ConnectChat(ctx, req) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/services/chat_client/internal/client/service/service.go b/services/chat_client/internal/client/service/service.go new file mode 100644 index 0000000..3336766 --- /dev/null +++ b/services/chat_client/internal/client/service/service.go @@ -0,0 +1,33 @@ +package service + +import ( + "context" + + "google.golang.org/protobuf/types/known/emptypb" + + authApi "github.com/Genvekt/cli-chat/libraries/api/auth/v1" + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" + "github.com/Genvekt/cli-chat/services/chat-client/internal/model" +) + +type ChatClient interface { + Create(ctx context.Context, name string, usernames []string) (int64, error) + SendMessage(ctx context.Context, message *model.Message) error + Connect(ctx context.Context, chatID int64, username string) (chan *model.Message, error) +} + +type ChatGRPCClient interface { + Create(ctx context.Context, req *chatApi.CreateRequest) (*chatApi.CreateResponse, error) + SendMessage(ctx context.Context, req *chatApi.SendMessageRequest) (*emptypb.Empty, error) + Connect(ctx context.Context, req *chatApi.ConnectChatRequest) (chatApi.ChatV1_ConnectChatClient, error) +} + +type AuthClient interface { + Login(ctx context.Context, username string, password string) (string, error) + GetAccessToken(ctx context.Context, refreshToken string) (string, error) +} + +type AuthGRPCClient interface { + Login(ctx context.Context, req *authApi.LoginRequest) (*authApi.LoginResponse, error) + GetAccessToken(ctx context.Context, req *authApi.GetAccessTokenRequest) (*authApi.GetAccessTokenResponse, error) +} diff --git a/services/chat_client/internal/config/config.go b/services/chat_client/internal/config/config.go new file mode 100644 index 0000000..59042ec --- /dev/null +++ b/services/chat_client/internal/config/config.go @@ -0,0 +1,28 @@ +package config + +import "github.com/joho/godotenv" + +// LoadEnv reads .env file into environment vars +func LoadEnv(filePath string) error { + if filePath == "" { + // nothing to load + return nil + } + + err := godotenv.Load(filePath) + if err != nil { + return err + } + + return nil +} + +// GRPCConfig provides envs related to grpc client +type GRPCConfig interface { + Address() string +} + +type ProfileConfig interface { + Username() string + Password() string +} diff --git a/services/chat_client/internal/config/env/grpc.go b/services/chat_client/internal/config/env/grpc.go new file mode 100644 index 0000000..52552ce --- /dev/null +++ b/services/chat_client/internal/config/env/grpc.go @@ -0,0 +1,62 @@ +package env + +import ( + "fmt" + "net" + "os" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" +) + +var _ config.GRPCConfig = (*gRPCConfigEnv)(nil) + +const ( + chatGrpcHostEnv = "CHAT_GRPC_HOST" + chatGrpcPortEnv = "CHAT_GRPC_PORT" + + authGrpcHostEnv = "AUTH_GRPC_HOST" + authGrpcPortEnv = "AUTH_GRPC_PORT" +) + +type gRPCConfigEnv struct { + host string + port string + tlsEnabled bool + tlsCertFile string + tlsKeyFile string +} + +// NewChatGRPCConfigEnv retrieves grpc config for chat service connection +func NewChatGRPCConfigEnv() (*gRPCConfigEnv, error) { + host := os.Getenv(chatGrpcHostEnv) + if host == "" { + return nil, fmt.Errorf("environment variable %q not set", chatGrpcHostEnv) + } + + port := os.Getenv(chatGrpcPortEnv) + if port == "" { + return nil, fmt.Errorf("environment variable %q not set", chatGrpcPortEnv) + } + + return &gRPCConfigEnv{host: host, port: port}, nil +} + +// NewAuthGRPCConfigEnv retrieves grpc config for auth service connection +func NewAuthGRPCConfigEnv() (*gRPCConfigEnv, error) { + host := os.Getenv(authGrpcHostEnv) + if host == "" { + return nil, fmt.Errorf("environment variable %q not set", authGrpcHostEnv) + } + + port := os.Getenv(authGrpcPortEnv) + if port == "" { + return nil, fmt.Errorf("environment variable %q not set", authGrpcPortEnv) + } + + return &gRPCConfigEnv{host: host, port: port}, nil +} + +// Address returns host:port string for grpc server +func (e *gRPCConfigEnv) Address() string { + return net.JoinHostPort(e.host, e.port) +} diff --git a/services/chat_client/internal/config/env/profile.go b/services/chat_client/internal/config/env/profile.go new file mode 100644 index 0000000..e31dc75 --- /dev/null +++ b/services/chat_client/internal/config/env/profile.go @@ -0,0 +1,45 @@ +package env + +import ( + "fmt" + "os" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" +) + +const ( + usernameEnv = "CLI_CHAT_USERNAME" + passwordEnv = "CLI_CHAT_PASSWORD" +) + +var _ config.ProfileConfig = (*profileConfigEnv)(nil) + +type profileConfigEnv struct { + username string + password string +} + +func NewProfileConfigEnv() (*profileConfigEnv, error) { + username := os.Getenv(usernameEnv) + if username == "" { + return nil, fmt.Errorf("environment variable %s is not set", usernameEnv) + } + + password := os.Getenv(passwordEnv) + if password == "" { + return nil, fmt.Errorf("environment variable %s is not set", passwordEnv) + } + + return &profileConfigEnv{ + username: username, + password: password, + }, nil +} + +func (c *profileConfigEnv) Username() string { + return c.username +} + +func (c *profileConfigEnv) Password() string { + return c.password +} diff --git a/services/chat_client/internal/config/ini/profile.go b/services/chat_client/internal/config/ini/profile.go new file mode 100644 index 0000000..4301d25 --- /dev/null +++ b/services/chat_client/internal/config/ini/profile.go @@ -0,0 +1,65 @@ +package ini + +import ( + "fmt" + + "gopkg.in/ini.v1" + + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" +) + +const ( + defaultProfile = "default" + profilePrefix = "profile " + usernameKey = "username" + passwordKey = "password" +) + +var _ config.ProfileConfig = (*profileConfigIni)(nil) + +type profileConfigIni struct { + username string + password string +} + +func NewProfileConfigIni(configPath string, profileName string) (*profileConfigIni, error) { + cfg, err := ini.Load(configPath) + if err != nil { + return nil, fmt.Errorf("failed to load config file: %v", err) + } + + profile, err := cfg.GetSection(fmt.Sprintf("%s%s", profilePrefix, defaultProfile)) + if profileName == "" && err != nil { + return nil, fmt.Errorf("failed to get default profile: %v", err) + } + + if profileName != "" { + profile, err = cfg.GetSection(fmt.Sprintf("%s%s", profilePrefix, profileName)) + if err != nil { + return nil, fmt.Errorf("failed to get profile %s: %v", profileName, err) + } + } + + username, err := profile.GetKey(usernameKey) + if err != nil { + return nil, fmt.Errorf("failed to get username: %v", err) + } + + password, err := profile.GetKey(passwordKey) + if err != nil { + return nil, fmt.Errorf("failed to get password: %v", err) + } + + return &profileConfigIni{ + username: username.String(), + password: password.String(), + }, nil +} + +func (c *profileConfigIni) Username() string { + return c.username +} + +func (c *profileConfigIni) Password() string { + return c.password +} diff --git a/services/chat_client/internal/model/chat.go b/services/chat_client/internal/model/chat.go new file mode 100644 index 0000000..ae670b7 --- /dev/null +++ b/services/chat_client/internal/model/chat.go @@ -0,0 +1,10 @@ +package model + +import "time" + +// Chat service model +type Chat struct { + ID int64 `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/services/chat_client/internal/model/message.go b/services/chat_client/internal/model/message.go new file mode 100644 index 0000000..438ebca --- /dev/null +++ b/services/chat_client/internal/model/message.go @@ -0,0 +1,10 @@ +package model + +import "time" + +type Message struct { + SenderID int64 + ChatID int64 + Text string + Timestamp time.Time +} diff --git a/services/chat_client/internal/utils/token.go b/services/chat_client/internal/utils/token.go new file mode 100644 index 0000000..83a23a8 --- /dev/null +++ b/services/chat_client/internal/utils/token.go @@ -0,0 +1,37 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/golang-jwt/jwt" + "google.golang.org/grpc/metadata" +) + +const ( + authorisationHeader = "authorization" + authPrefix = "Bearer " +) + +func PutAccessTokenToCtx(ctx context.Context, accessToken string) context.Context { + md := metadata.New(map[string]string{authorisationHeader: authPrefix + accessToken}) + return metadata.NewOutgoingContext(ctx, md) +} + +func GetUserIdFromToken(tokenString string) (int64, error) { + var userID int64 + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + if err != nil { + return 0, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + if iUserID, found := claims["id"]; found { + userID = int64(iUserID.(float64)) + } else { + return 0, fmt.Errorf("user id not found in token payload") + } + } + + return userID, nil +} diff --git a/services/chat_server/Makefile b/services/chat_server/Makefile index d9d6c72..7fd5c7d 100644 --- a/services/chat_server/Makefile +++ b/services/chat_server/Makefile @@ -7,6 +7,11 @@ install-deps: lint: $(LOCAL_BIN)/golangci-lint run ./... --config ../../.golangci.pipeline.yaml +generate-mocks: + go generate ./internal/service/ + go generate ./internal/repository/ + go generate ./internal/config/ + test: go clean -testcache go test ./... -covermode count -coverpkg=github.com/Genvekt/cli-chat/services/chat-server/internal/service/...,github.com/Genvekt/cli-chat/services/chat-server/internal/api/... -count 5 diff --git a/services/chat_server/internal/api/chat/connect.go b/services/chat_server/internal/api/chat/connect.go new file mode 100644 index 0000000..fcb36cd --- /dev/null +++ b/services/chat_server/internal/api/chat/connect.go @@ -0,0 +1,44 @@ +package chat + +import ( + "go.uber.org/zap" + + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" + "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" + "github.com/Genvekt/cli-chat/services/chat-server/internal/converter" +) + +// ConnectChat creates stream connection to chat +func (s *Service) ConnectChat(req *chatApi.ConnectChatRequest, stream chatApi.ChatV1_ConnectChatServer) error { + messageChan, err := s.chatService.Connect(stream.Context(), req.GetChatId(), req.GetUsername()) + if err != nil { + return err + } + + logger.Debug("user connected to chat", + zap.Int64("chat_id", req.GetChatId()), + zap.String("username", req.GetUsername()), + ) + + defer logger.Debug("user disconnected from chat", + zap.Int64("chat_id", req.GetChatId()), + zap.String("username", req.GetUsername()), + ) + + for { + select { + + case msg, ok := <-messageChan: + if !ok { + return nil + } + if err = stream.Send(converter.ToProtoFromMessage(msg)); err != nil { + return err + } + + case <-stream.Context().Done(): + return nil + } + } + +} diff --git a/services/chat_server/internal/api/chat/service.go b/services/chat_server/internal/api/chat/service.go index ccda5a6..fad2626 100644 --- a/services/chat_server/internal/api/chat/service.go +++ b/services/chat_server/internal/api/chat/service.go @@ -13,5 +13,7 @@ type Service struct { // NewService initialises chat api implementation func NewService(chatService service.ChatService) *Service { - return &Service{chatService: chatService} + return &Service{ + chatService: chatService, + } } diff --git a/services/chat_server/internal/converter/message.go b/services/chat_server/internal/converter/message.go index ab1c1d0..6fd14d7 100644 --- a/services/chat_server/internal/converter/message.go +++ b/services/chat_server/internal/converter/message.go @@ -1,6 +1,8 @@ package converter import ( + "google.golang.org/protobuf/types/known/timestamppb" + chatApi "github.com/Genvekt/cli-chat/libraries/api/chat/v1" "github.com/Genvekt/cli-chat/services/chat-server/internal/model" ) @@ -14,3 +16,13 @@ func ToMessageFromProto(message *chatApi.Message) *model.Message { Timestamp: message.Timestamp.AsTime(), } } + +// ToProtoFromMessage converts message service model to message repository model +func ToProtoFromMessage(message *model.Message) *chatApi.Message { + return &chatApi.Message{ + SenderId: message.SenderID, + ChatId: message.ChatID, + Text: message.Content, + Timestamp: timestamppb.New(message.Timestamp), + } +} diff --git a/services/chat_server/internal/model/chat_connection.go b/services/chat_server/internal/model/chat_connection.go new file mode 100644 index 0000000..159db5d --- /dev/null +++ b/services/chat_server/internal/model/chat_connection.go @@ -0,0 +1,13 @@ +package model + +import ( + "sync" +) + +// ChatConnection represents active chat with user connections and inner buffer +type ChatConnection struct { + Mx sync.RWMutex + ChatID int64 + Buffer chan *Message + Connections map[string]chan *Message +} diff --git a/services/chat_server/internal/service/chat/connect.go b/services/chat_server/internal/service/chat/connect.go new file mode 100644 index 0000000..0a86cb2 --- /dev/null +++ b/services/chat_server/internal/service/chat/connect.go @@ -0,0 +1,91 @@ +package chat + +import ( + "context" + "fmt" + + "go.uber.org/zap" + + "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" + "github.com/Genvekt/cli-chat/services/chat-server/internal/model" +) + +// Connect initialises the connection of user to chat +func (s *chatService) Connect(ctx context.Context, id int64, username string) (chan *model.Message, error) { + s.mxChat.Lock() + chat, isActive := s.chatConnections[id] + if !isActive { + chat = &model.ChatConnection{ + ChatID: id, + Buffer: make(chan *model.Message, 100), + Connections: map[string]chan *model.Message{}, + } + + // Start chat broadcasting logic + go s.processChatBuffer(chat) + + s.chatConnections[id] = chat + + logger.Debug("chat activated", zap.Int64("chat_id", id)) + } + s.mxChat.Unlock() + + chat.Mx.Lock() + defer chat.Mx.Unlock() + + if _, connected := chat.Connections[username]; connected { + return nil, fmt.Errorf("already connected") + } + + userConnection := make(chan *model.Message, 100) + + chat.Connections[username] = userConnection + + go s.handleDisconnect(ctx, id, username) + + return userConnection, nil +} + +func (s *chatService) handleDisconnect(ctx context.Context, chatID int64, username string) { + <-ctx.Done() + s.mxChat.RLock() + chat, isActive := s.chatConnections[chatID] + s.mxChat.RUnlock() + + if !isActive { + return + } + + chat.Mx.Lock() + delete(chat.Connections, username) + chat.Mx.Unlock() + + if len(chat.Connections) == 0 { + // deactivate chat if there are no connected users + chat.Mx.Lock() + close(chat.Buffer) + chat.Mx.Unlock() + + s.mxChat.Lock() + delete(s.chatConnections, chatID) + s.mxChat.Unlock() + + logger.Debug("chat deactivated", zap.Int64("chat_id", chatID)) + } +} + +func (s *chatService) processChatBuffer(chatConnection *model.ChatConnection) { + for { + msg, ok := <-chatConnection.Buffer + if !ok { + logger.Debug("chat buffer closed", zap.Int64("chat_id", chatConnection.ChatID)) + return + } + + chatConnection.Mx.RLock() + for _, connection := range chatConnection.Connections { + connection <- msg + } + chatConnection.Mx.RUnlock() + } +} diff --git a/services/chat_server/internal/service/chat/send_message.go b/services/chat_server/internal/service/chat/send_message.go index 44268d7..4f105e8 100644 --- a/services/chat_server/internal/service/chat/send_message.go +++ b/services/chat_server/internal/service/chat/send_message.go @@ -16,15 +16,24 @@ func (s *chatService) SendMessage(ctx context.Context, message *model.Message) e return fmt.Errorf("failed to check the membership of user in chat: %v", err) } + // check user membership if !isMember { return fmt.Errorf("user is not a member of a chat") } + // save message to storage err = s.messageRepo.Create(ctx, message) if err != nil { return fmt.Errorf("cannot create message: %v", err) } + // send to active chat. Do nothing for inactive chats + s.mxChat.Lock() + if chatConnection, isActive := s.chatConnections[message.ChatID]; isActive { + chatConnection.Buffer <- message + } + s.mxChat.Unlock() + return nil } diff --git a/services/chat_server/internal/service/chat/service.go b/services/chat_server/internal/service/chat/service.go index 1fd7c99..f953679 100644 --- a/services/chat_server/internal/service/chat/service.go +++ b/services/chat_server/internal/service/chat/service.go @@ -1,7 +1,10 @@ package chat import ( + "sync" + "github.com/Genvekt/cli-chat/libraries/db_client/pkg/db" + "github.com/Genvekt/cli-chat/services/chat-server/internal/model" serviceCli "github.com/Genvekt/cli-chat/services/chat-server/internal/client/service" "github.com/Genvekt/cli-chat/services/chat-server/internal/repository" @@ -16,6 +19,9 @@ type chatService struct { messageRepo repository.MessageRepository userCli serviceCli.AuthClient txManager db.TxManager + + mxChat sync.RWMutex + chatConnections map[int64]*model.ChatConnection } // NewChatService initialises service layer for chat business logic @@ -32,5 +38,7 @@ func NewChatService( messageRepo: messageRepository, userCli: userCli, txManager: txManager, + + chatConnections: map[int64]*model.ChatConnection{}, } } diff --git a/services/chat_server/internal/service/mocks/chat_service_minimock.go b/services/chat_server/internal/service/mocks/chat_service_minimock.go index 45eb272..6b3ef58 100644 --- a/services/chat_server/internal/service/mocks/chat_service_minimock.go +++ b/services/chat_server/internal/service/mocks/chat_service_minimock.go @@ -17,6 +17,12 @@ type ChatServiceMock struct { t minimock.Tester finishOnce sync.Once + funcConnect func(ctx context.Context, id int64, username string) (ch1 chan *model.Message, err error) + inspectFuncConnect func(ctx context.Context, id int64, username string) + afterConnectCounter uint64 + beforeConnectCounter uint64 + ConnectMock mChatServiceMockConnect + funcCreate func(ctx context.Context, name string, usernames []string) (i1 int64, err error) inspectFuncCreate func(ctx context.Context, name string, usernames []string) afterCreateCounter uint64 @@ -44,6 +50,9 @@ func NewChatServiceMock(t minimock.Tester) *ChatServiceMock { controller.RegisterMocker(m) } + m.ConnectMock = mChatServiceMockConnect{mock: m} + m.ConnectMock.callArgs = []*ChatServiceMockConnectParams{} + m.CreateMock = mChatServiceMockCreate{mock: m} m.CreateMock.callArgs = []*ChatServiceMockCreateParams{} @@ -58,6 +67,355 @@ func NewChatServiceMock(t minimock.Tester) *ChatServiceMock { return m } +type mChatServiceMockConnect struct { + optional bool + mock *ChatServiceMock + defaultExpectation *ChatServiceMockConnectExpectation + expectations []*ChatServiceMockConnectExpectation + + callArgs []*ChatServiceMockConnectParams + mutex sync.RWMutex + + expectedInvocations uint64 +} + +// ChatServiceMockConnectExpectation specifies expectation struct of the ChatService.Connect +type ChatServiceMockConnectExpectation struct { + mock *ChatServiceMock + params *ChatServiceMockConnectParams + paramPtrs *ChatServiceMockConnectParamPtrs + results *ChatServiceMockConnectResults + Counter uint64 +} + +// ChatServiceMockConnectParams contains parameters of the ChatService.Connect +type ChatServiceMockConnectParams struct { + ctx context.Context + id int64 + username string +} + +// ChatServiceMockConnectParamPtrs contains pointers to parameters of the ChatService.Connect +type ChatServiceMockConnectParamPtrs struct { + ctx *context.Context + id *int64 + username *string +} + +// ChatServiceMockConnectResults contains results of the ChatService.Connect +type ChatServiceMockConnectResults struct { + ch1 chan *model.Message + err error +} + +// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning +// the test will fail minimock's automatic final call check if the mocked method was not called at least once. +// Optional() makes method check to work in '0 or more' mode. +// It is NOT RECOMMENDED to use this option by default unless you really need it, as it helps to +// catch the problems when the expected method call is totally skipped during test run. +func (mmConnect *mChatServiceMockConnect) Optional() *mChatServiceMockConnect { + mmConnect.optional = true + return mmConnect +} + +// Expect sets up expected params for ChatService.Connect +func (mmConnect *mChatServiceMockConnect) Expect(ctx context.Context, id int64, username string) *mChatServiceMockConnect { + if mmConnect.mock.funcConnect != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Set") + } + + if mmConnect.defaultExpectation == nil { + mmConnect.defaultExpectation = &ChatServiceMockConnectExpectation{} + } + + if mmConnect.defaultExpectation.paramPtrs != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by ExpectParams functions") + } + + mmConnect.defaultExpectation.params = &ChatServiceMockConnectParams{ctx, id, username} + for _, e := range mmConnect.expectations { + if minimock.Equal(e.params, mmConnect.defaultExpectation.params) { + mmConnect.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmConnect.defaultExpectation.params) + } + } + + return mmConnect +} + +// ExpectCtxParam1 sets up expected param ctx for ChatService.Connect +func (mmConnect *mChatServiceMockConnect) ExpectCtxParam1(ctx context.Context) *mChatServiceMockConnect { + if mmConnect.mock.funcConnect != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Set") + } + + if mmConnect.defaultExpectation == nil { + mmConnect.defaultExpectation = &ChatServiceMockConnectExpectation{} + } + + if mmConnect.defaultExpectation.params != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Expect") + } + + if mmConnect.defaultExpectation.paramPtrs == nil { + mmConnect.defaultExpectation.paramPtrs = &ChatServiceMockConnectParamPtrs{} + } + mmConnect.defaultExpectation.paramPtrs.ctx = &ctx + + return mmConnect +} + +// ExpectIdParam2 sets up expected param id for ChatService.Connect +func (mmConnect *mChatServiceMockConnect) ExpectIdParam2(id int64) *mChatServiceMockConnect { + if mmConnect.mock.funcConnect != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Set") + } + + if mmConnect.defaultExpectation == nil { + mmConnect.defaultExpectation = &ChatServiceMockConnectExpectation{} + } + + if mmConnect.defaultExpectation.params != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Expect") + } + + if mmConnect.defaultExpectation.paramPtrs == nil { + mmConnect.defaultExpectation.paramPtrs = &ChatServiceMockConnectParamPtrs{} + } + mmConnect.defaultExpectation.paramPtrs.id = &id + + return mmConnect +} + +// ExpectUsernameParam3 sets up expected param username for ChatService.Connect +func (mmConnect *mChatServiceMockConnect) ExpectUsernameParam3(username string) *mChatServiceMockConnect { + if mmConnect.mock.funcConnect != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Set") + } + + if mmConnect.defaultExpectation == nil { + mmConnect.defaultExpectation = &ChatServiceMockConnectExpectation{} + } + + if mmConnect.defaultExpectation.params != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Expect") + } + + if mmConnect.defaultExpectation.paramPtrs == nil { + mmConnect.defaultExpectation.paramPtrs = &ChatServiceMockConnectParamPtrs{} + } + mmConnect.defaultExpectation.paramPtrs.username = &username + + return mmConnect +} + +// Inspect accepts an inspector function that has same arguments as the ChatService.Connect +func (mmConnect *mChatServiceMockConnect) Inspect(f func(ctx context.Context, id int64, username string)) *mChatServiceMockConnect { + if mmConnect.mock.inspectFuncConnect != nil { + mmConnect.mock.t.Fatalf("Inspect function is already set for ChatServiceMock.Connect") + } + + mmConnect.mock.inspectFuncConnect = f + + return mmConnect +} + +// Return sets up results that will be returned by ChatService.Connect +func (mmConnect *mChatServiceMockConnect) Return(ch1 chan *model.Message, err error) *ChatServiceMock { + if mmConnect.mock.funcConnect != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Set") + } + + if mmConnect.defaultExpectation == nil { + mmConnect.defaultExpectation = &ChatServiceMockConnectExpectation{mock: mmConnect.mock} + } + mmConnect.defaultExpectation.results = &ChatServiceMockConnectResults{ch1, err} + return mmConnect.mock +} + +// Set uses given function f to mock the ChatService.Connect method +func (mmConnect *mChatServiceMockConnect) Set(f func(ctx context.Context, id int64, username string) (ch1 chan *model.Message, err error)) *ChatServiceMock { + if mmConnect.defaultExpectation != nil { + mmConnect.mock.t.Fatalf("Default expectation is already set for the ChatService.Connect method") + } + + if len(mmConnect.expectations) > 0 { + mmConnect.mock.t.Fatalf("Some expectations are already set for the ChatService.Connect method") + } + + mmConnect.mock.funcConnect = f + return mmConnect.mock +} + +// When sets expectation for the ChatService.Connect which will trigger the result defined by the following +// Then helper +func (mmConnect *mChatServiceMockConnect) When(ctx context.Context, id int64, username string) *ChatServiceMockConnectExpectation { + if mmConnect.mock.funcConnect != nil { + mmConnect.mock.t.Fatalf("ChatServiceMock.Connect mock is already set by Set") + } + + expectation := &ChatServiceMockConnectExpectation{ + mock: mmConnect.mock, + params: &ChatServiceMockConnectParams{ctx, id, username}, + } + mmConnect.expectations = append(mmConnect.expectations, expectation) + return expectation +} + +// Then sets up ChatService.Connect return parameters for the expectation previously defined by the When method +func (e *ChatServiceMockConnectExpectation) Then(ch1 chan *model.Message, err error) *ChatServiceMock { + e.results = &ChatServiceMockConnectResults{ch1, err} + return e.mock +} + +// Times sets number of times ChatService.Connect should be invoked +func (mmConnect *mChatServiceMockConnect) Times(n uint64) *mChatServiceMockConnect { + if n == 0 { + mmConnect.mock.t.Fatalf("Times of ChatServiceMock.Connect mock can not be zero") + } + mm_atomic.StoreUint64(&mmConnect.expectedInvocations, n) + return mmConnect +} + +func (mmConnect *mChatServiceMockConnect) invocationsDone() bool { + if len(mmConnect.expectations) == 0 && mmConnect.defaultExpectation == nil && mmConnect.mock.funcConnect == nil { + return true + } + + totalInvocations := mm_atomic.LoadUint64(&mmConnect.mock.afterConnectCounter) + expectedInvocations := mm_atomic.LoadUint64(&mmConnect.expectedInvocations) + + return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations) +} + +// Connect implements service.ChatService +func (mmConnect *ChatServiceMock) Connect(ctx context.Context, id int64, username string) (ch1 chan *model.Message, err error) { + mm_atomic.AddUint64(&mmConnect.beforeConnectCounter, 1) + defer mm_atomic.AddUint64(&mmConnect.afterConnectCounter, 1) + + if mmConnect.inspectFuncConnect != nil { + mmConnect.inspectFuncConnect(ctx, id, username) + } + + mm_params := ChatServiceMockConnectParams{ctx, id, username} + + // Record call args + mmConnect.ConnectMock.mutex.Lock() + mmConnect.ConnectMock.callArgs = append(mmConnect.ConnectMock.callArgs, &mm_params) + mmConnect.ConnectMock.mutex.Unlock() + + for _, e := range mmConnect.ConnectMock.expectations { + if minimock.Equal(*e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.ch1, e.results.err + } + } + + if mmConnect.ConnectMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmConnect.ConnectMock.defaultExpectation.Counter, 1) + mm_want := mmConnect.ConnectMock.defaultExpectation.params + mm_want_ptrs := mmConnect.ConnectMock.defaultExpectation.paramPtrs + + mm_got := ChatServiceMockConnectParams{ctx, id, username} + + if mm_want_ptrs != nil { + + if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) { + mmConnect.t.Errorf("ChatServiceMock.Connect got unexpected parameter ctx, want: %#v, got: %#v%s\n", *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx)) + } + + if mm_want_ptrs.id != nil && !minimock.Equal(*mm_want_ptrs.id, mm_got.id) { + mmConnect.t.Errorf("ChatServiceMock.Connect got unexpected parameter id, want: %#v, got: %#v%s\n", *mm_want_ptrs.id, mm_got.id, minimock.Diff(*mm_want_ptrs.id, mm_got.id)) + } + + if mm_want_ptrs.username != nil && !minimock.Equal(*mm_want_ptrs.username, mm_got.username) { + mmConnect.t.Errorf("ChatServiceMock.Connect got unexpected parameter username, want: %#v, got: %#v%s\n", *mm_want_ptrs.username, mm_got.username, minimock.Diff(*mm_want_ptrs.username, mm_got.username)) + } + + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmConnect.t.Errorf("ChatServiceMock.Connect got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmConnect.ConnectMock.defaultExpectation.results + if mm_results == nil { + mmConnect.t.Fatal("No results are set for the ChatServiceMock.Connect") + } + return (*mm_results).ch1, (*mm_results).err + } + if mmConnect.funcConnect != nil { + return mmConnect.funcConnect(ctx, id, username) + } + mmConnect.t.Fatalf("Unexpected call to ChatServiceMock.Connect. %v %v %v", ctx, id, username) + return +} + +// ConnectAfterCounter returns a count of finished ChatServiceMock.Connect invocations +func (mmConnect *ChatServiceMock) ConnectAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmConnect.afterConnectCounter) +} + +// ConnectBeforeCounter returns a count of ChatServiceMock.Connect invocations +func (mmConnect *ChatServiceMock) ConnectBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmConnect.beforeConnectCounter) +} + +// Calls returns a list of arguments used in each call to ChatServiceMock.Connect. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmConnect *mChatServiceMockConnect) Calls() []*ChatServiceMockConnectParams { + mmConnect.mutex.RLock() + + argCopy := make([]*ChatServiceMockConnectParams, len(mmConnect.callArgs)) + copy(argCopy, mmConnect.callArgs) + + mmConnect.mutex.RUnlock() + + return argCopy +} + +// MinimockConnectDone returns true if the count of the Connect invocations corresponds +// the number of defined expectations +func (m *ChatServiceMock) MinimockConnectDone() bool { + if m.ConnectMock.optional { + // Optional methods provide '0 or more' call count restriction. + return true + } + + for _, e := range m.ConnectMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + return m.ConnectMock.invocationsDone() +} + +// MinimockConnectInspect logs each unmet expectation +func (m *ChatServiceMock) MinimockConnectInspect() { + for _, e := range m.ConnectMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to ChatServiceMock.Connect with params: %#v", *e.params) + } + } + + afterConnectCounter := mm_atomic.LoadUint64(&m.afterConnectCounter) + // if default expectation was set then invocations count should be greater than zero + if m.ConnectMock.defaultExpectation != nil && afterConnectCounter < 1 { + if m.ConnectMock.defaultExpectation.params == nil { + m.t.Error("Expected call to ChatServiceMock.Connect") + } else { + m.t.Errorf("Expected call to ChatServiceMock.Connect with params: %#v", *m.ConnectMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcConnect != nil && afterConnectCounter < 1 { + m.t.Error("Expected call to ChatServiceMock.Connect") + } + + if !m.ConnectMock.invocationsDone() && afterConnectCounter > 0 { + m.t.Errorf("Expected %d calls to ChatServiceMock.Connect but found %d calls", + mm_atomic.LoadUint64(&m.ConnectMock.expectedInvocations), afterConnectCounter) + } +} + type mChatServiceMockCreate struct { optional bool mock *ChatServiceMock @@ -1051,6 +1409,8 @@ func (m *ChatServiceMock) MinimockSendMessageInspect() { func (m *ChatServiceMock) MinimockFinish() { m.finishOnce.Do(func() { if !m.minimockDone() { + m.MinimockConnectInspect() + m.MinimockCreateInspect() m.MinimockDeleteInspect() @@ -1080,6 +1440,7 @@ func (m *ChatServiceMock) MinimockWait(timeout mm_time.Duration) { func (m *ChatServiceMock) minimockDone() bool { done := true return done && + m.MinimockConnectDone() && m.MinimockCreateDone() && m.MinimockDeleteDone() && m.MinimockSendMessageDone() diff --git a/services/chat_server/internal/service/service.go b/services/chat_server/internal/service/service.go index 9c133c0..1dd3986 100644 --- a/services/chat_server/internal/service/service.go +++ b/services/chat_server/internal/service/service.go @@ -11,4 +11,5 @@ type ChatService interface { Create(ctx context.Context, name string, usernames []string) (int64, error) Delete(ctx context.Context, id int64) error SendMessage(ctx context.Context, message *model.Message) error + Connect(ctx context.Context, id int64, username string) (chan *model.Message, error) } From d76f5958df1358bd02e961f07021677f1fb4e761 Mon Sep 17 00:00:00 2001 From: Evgenia Kivotova Date: Fri, 27 Sep 2024 20:53:34 +0600 Subject: [PATCH 2/2] fix lint --- services/auth/internal/model/auth.go | 8 +- services/chat_client/cmd/cli/root.go | 62 ++++----- services/chat_client/cmd/main.go | 1 - services/chat_client/internal/app/app.go | 162 +++++++++++------------ 4 files changed, 116 insertions(+), 117 deletions(-) diff --git a/services/auth/internal/model/auth.go b/services/auth/internal/model/auth.go index d1b4e6e..859551f 100644 --- a/services/auth/internal/model/auth.go +++ b/services/auth/internal/model/auth.go @@ -4,8 +4,8 @@ import "github.com/golang-jwt/jwt" // UserClaims is data for token type UserClaims struct { - jwt.StandardClaims - ID int64 `json:"id"` - Username string `json:"username"` - Role int `json:"role"` + jwt.StandardClaims + ID int64 `json:"id"` + Username string `json:"username"` + Role int `json:"role"` } diff --git a/services/chat_client/cmd/cli/root.go b/services/chat_client/cmd/cli/root.go index 13f92dd..7dd26d6 100644 --- a/services/chat_client/cmd/cli/root.go +++ b/services/chat_client/cmd/cli/root.go @@ -1,51 +1,51 @@ package cli import ( - "fmt" + "fmt" - "github.com/spf13/cobra" + "github.com/spf13/cobra" - "github.com/Genvekt/cli-chat/services/chat-client/cmd/cli/create" - "github.com/Genvekt/cli-chat/services/chat-client/internal/app" + "github.com/Genvekt/cli-chat/services/chat-client/cmd/cli/create" + "github.com/Genvekt/cli-chat/services/chat-client/internal/app" ) var ( - profileName string - iniConfig string - envConfig string + profileName string + iniConfig string + envConfig string ) var ( - rootCmd = &cobra.Command{ - Use: "cli-chat", - Short: "CLI Chat application", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // We initialise application before execution any command - var err error - - err = app.InitApp(cmd.Context(), profileName, iniConfig, envConfig) - if err != nil { - return fmt.Errorf("failed to initialise application: %v", err) - } - - return nil - }, - } + rootCmd = &cobra.Command{ + Use: "cli-chat", + Short: "CLI Chat application", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // We initialise application before execution any command + var err error + + err = app.InitApp(cmd.Context(), profileName, iniConfig, envConfig) + if err != nil { + return fmt.Errorf("failed to initialise application: %v", err) + } + + return nil + }, + } ) func init() { - rootCmd.PersistentFlags().StringVar(&profileName, "profile", "", "profile name") - rootCmd.PersistentFlags().StringVar(&iniConfig, "ini-config", "config.ini", "ini config path") - rootCmd.PersistentFlags().StringVar(&envConfig, "env-config", ".env", "env config path") + rootCmd.PersistentFlags().StringVar(&profileName, "profile", "", "profile name") + rootCmd.PersistentFlags().StringVar(&iniConfig, "ini-config", "config.ini", "ini config path") + rootCmd.PersistentFlags().StringVar(&envConfig, "env-config", ".env", "env config path") - rootCmd.AddCommand(create.CreateCmd) - rootCmd.AddCommand(connectCmd) + rootCmd.AddCommand(create.CreateCmd) + rootCmd.AddCommand(connectCmd) } // Execute acts as application entrypoint func Execute() { - err := rootCmd.Execute() - if err != nil { - fmt.Printf("Error: %v\n", err) - } + err := rootCmd.Execute() + if err != nil { + fmt.Printf("Error: %v\n", err) + } } diff --git a/services/chat_client/cmd/main.go b/services/chat_client/cmd/main.go index 00d9ec5..6e1d796 100644 --- a/services/chat_client/cmd/main.go +++ b/services/chat_client/cmd/main.go @@ -4,5 +4,4 @@ import "github.com/Genvekt/cli-chat/services/chat-client/cmd/cli" func main() { cli.Execute() - } diff --git a/services/chat_client/internal/app/app.go b/services/chat_client/internal/app/app.go index 534270c..2683486 100644 --- a/services/chat_client/internal/app/app.go +++ b/services/chat_client/internal/app/app.go @@ -1,134 +1,134 @@ package app import ( - "context" - "fmt" - "os" - - "github.com/natefinch/lumberjack" - "github.com/spf13/cobra" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - - "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" - "github.com/Genvekt/cli-chat/services/chat-client/internal/cli" - "github.com/Genvekt/cli-chat/services/chat-client/internal/config" + "context" + "fmt" + "os" + + "github.com/natefinch/lumberjack" + "github.com/spf13/cobra" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/Genvekt/cli-chat/libraries/logger/pkg/logger" + "github.com/Genvekt/cli-chat/services/chat-client/internal/cli" + "github.com/Genvekt/cli-chat/services/chat-client/internal/config" ) var ( - profileName string - iniConfig string - envConfig string + profileName string + iniConfig string + envConfig string - application *App + application *App ) type App struct { - provider *ServiceProvider - cliRoot *cobra.Command - cliService cli.CliService + provider *ServiceProvider + cliRoot *cobra.Command + cliService cli.CliService } // InitApp initialises app and all its dependencies func InitApp(ctx context.Context, profile string, iniConf string, envConf string) error { - profileName = profile - iniConfig = iniConf - envConfig = envConf + profileName = profile + iniConfig = iniConf + envConfig = envConf - application = &App{} + application = &App{} - err := application.initDeps(ctx) - if err != nil { - return err - } + err := application.initDeps(ctx) + if err != nil { + return err + } - return nil + return nil } func (a *App) initDeps(ctx context.Context) error { - deps := []func(context.Context) error{ - a.initConfig, - a.initLogger, - a.initServiceProvider, - } - - for _, dep := range deps { - if err := dep(ctx); err != nil { - return err - } - } - - return nil + deps := []func(context.Context) error{ + a.initConfig, + a.initLogger, + a.initServiceProvider, + } + + for _, dep := range deps { + if err := dep(ctx); err != nil { + return err + } + } + + return nil } func (a *App) initConfig(_ context.Context) error { - err := config.LoadEnv(envConfig) - if err != nil { - return err - } + err := config.LoadEnv(envConfig) + if err != nil { + return err + } - return nil + return nil } func (a *App) initLogger(_ context.Context) error { - logLevel, err := a.getLogAtomicLevel() - if err != nil { - return err - } - logger.Init(a.getLogCore(*logLevel)) - return nil + logLevel, err := a.getLogAtomicLevel() + if err != nil { + return err + } + logger.Init(a.getLogCore(*logLevel)) + return nil } func (a *App) initServiceProvider(_ context.Context) error { - a.provider = newServiceProvider(iniConfig, profileName) + a.provider = newServiceProvider(iniConfig, profileName) - return nil + return nil } func CreateChat(ctx context.Context, name string, usernames []string) error { - return application.provider.ChatCliService().CreateChat(ctx, name, usernames) + return application.provider.ChatCliService().CreateChat(ctx, name, usernames) } func Connect(ctx context.Context, chatID int64) error { - return application.provider.ChatCliService().Connect(ctx, chatID) + return application.provider.ChatCliService().Connect(ctx, chatID) } func SendMessage(ctx context.Context, chatID int64, message string) error { - return application.provider.ChatCliService().SendMessage(ctx, chatID, message) + return application.provider.ChatCliService().SendMessage(ctx, chatID, message) } func (a *App) getLogCore(level zap.AtomicLevel) zapcore.Core { - stdout := zapcore.AddSync(os.Stdout) + stdout := zapcore.AddSync(os.Stdout) - file := zapcore.AddSync(&lumberjack.Logger{ - Filename: "logs/app.log", - MaxSize: 10, // megabytes - MaxBackups: 3, - MaxAge: 7, // days - }) + file := zapcore.AddSync(&lumberjack.Logger{ + Filename: "logs/app.log", + MaxSize: 10, // megabytes + MaxBackups: 3, + MaxAge: 7, // days + }) - productionCfg := zap.NewProductionEncoderConfig() - productionCfg.TimeKey = "timestamp" - productionCfg.EncodeTime = zapcore.ISO8601TimeEncoder + productionCfg := zap.NewProductionEncoderConfig() + productionCfg.TimeKey = "timestamp" + productionCfg.EncodeTime = zapcore.ISO8601TimeEncoder - developmentCfg := zap.NewDevelopmentEncoderConfig() - developmentCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder + developmentCfg := zap.NewDevelopmentEncoderConfig() + developmentCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder - consoleEncoder := zapcore.NewConsoleEncoder(developmentCfg) - fileEncoder := zapcore.NewJSONEncoder(productionCfg) + consoleEncoder := zapcore.NewConsoleEncoder(developmentCfg) + fileEncoder := zapcore.NewJSONEncoder(productionCfg) - return zapcore.NewTee( - zapcore.NewCore(consoleEncoder, stdout, level), - zapcore.NewCore(fileEncoder, file, level), - ) + return zapcore.NewTee( + zapcore.NewCore(consoleEncoder, stdout, level), + zapcore.NewCore(fileEncoder, file, level), + ) } func (a *App) getLogAtomicLevel() (*zap.AtomicLevel, error) { - var level zapcore.Level - if err := level.Set(zapcore.InfoLevel.String()); err != nil { - return nil, fmt.Errorf("failed to set log level: %v", err) - } - atomicLevel := zap.NewAtomicLevelAt(level) + var level zapcore.Level + if err := level.Set(zapcore.InfoLevel.String()); err != nil { + return nil, fmt.Errorf("failed to set log level: %v", err) + } + atomicLevel := zap.NewAtomicLevelAt(level) - return &atomicLevel, nil + return &atomicLevel, nil }